Source code for devctrl.gui.widgets.key_value_input

from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QSpacerItem, QSpinBox, QDoubleSpinBox
from PyQt6.QtWidgets import QGridLayout, QMenu, QListWidget, QPushButton, QFormLayout, QDialog, QDialogButtonBox
from PyQt6.QtCore import pyqtSignal

"""QWidget containing a variable number of key-value input pairs

Each pair appears on one line in a dense grid layout. Values can be text (QLineEdit),
integers (QSpinBox), or floating-point numbers (QDoubleSpinBox).
"""
import logging
log = logging.getLogger(__name__)

[docs] class KeyValueInput(QWidget): dataChanged = pyqtSignal() def __init__(self, elements: list[tuple[str, str]]|dict[str,str]=None, label=None): """Initialize the key-value input widget :param elements: List of (key, value) tuples or dictionary of initial elements, or None :param label: Optional label text to display above the input grid """ super().__init__() # set layout self.l_vbox = QVBoxLayout() if isinstance(label, str): self.l_vbox.addWidget(QLabel(label)) self.l_grid = QGridLayout() self.l_grid.setColumnMinimumWidth(0, 100) self.l_grid.setColumnMinimumWidth(1, 100) self.l_grid.setContentsMargins(0, 0, 0, 0) self.l_grid.setSpacing(0) # first row: key value <new element button> self.l_grid.addWidget(QLabel("Key"), 0, 0) self.l_grid.addWidget(QLabel("Value"), 0, 1) self.btn_new_element = QPushButton("+") self.btn_new_element.setFixedSize(20, 20) self.btn_new_element.clicked.connect(self.add_element_dialog) self.l_grid.addWidget(self.btn_new_element, 0, 2) self.l_grid.setContentsMargins(4, 4, 4, 4) self.l_grid.setSpacing(4) self.l_vbox.addLayout(self.l_grid) self.setLayout(self.l_vbox) self.layout_changed() # call even when no element was added # key-value widgets self.ws_elements = {} if type(elements) == dict: self.set_from_dict(elements) elif type(elements) == list: for (n, v) in elements: self.add_element(n, v) def layout_changed(self): # add stretch to the last row for r in range(self.l_grid.rowCount()): self.l_grid.setRowStretch(r, 0) self.l_grid.setRowStretch(self.l_grid.rowCount(), 1)
[docs] def add_element_dialog(self): """Prompt for a new key-value pair using a dialog with a form layout""" dialog = QDialog(self) dialog.setWindowTitle("Add new field") dialog.layout = QFormLayout() dialog.key_input = QLineEdit() dialog.value_input = QLineEdit() dialog.layout.addRow("Key", dialog.key_input) dialog.layout.addRow("Value", dialog.value_input) # ok and cancel buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) dialog.layout.addRow(buttons) dialog.setLayout(dialog.layout) ret = dialog.exec() if ret == QDialog.DialogCode.Accepted: self.add_element(dialog.key_input.text(), dialog.value_input.text()) self.dataChanged.emit()
def add_element(self, key, init_val=""): if key in self.ws_elements: raise RuntimeError(f"Can not add field '{key}', it already exists") if key == "": raise RuntimeError(f"Can not add field with empty key") log.debug(f"Adding element: '{key}': '{init_val}'") row = self.l_grid.rowCount() w_label = QLabel(key) self.l_grid.addWidget(w_label, row, 0) w_input= QLineEdit(str(init_val)) w_input.editingFinished.connect(self.dataChanged) self.l_grid.addWidget(w_input, row, 1) # Add a small button with an X from the theme btn_remove = QPushButton("X") btn_remove.setFixedSize(20, 20) btn_remove.clicked.connect(lambda: self.remove_element(key)) self.l_grid.addWidget(btn_remove, row, 2) self.ws_elements[key] = (w_label, w_input, btn_remove) self.layout_changed() def remove_element(self, name): if name not in self.ws_elements: raise RuntimeError(f"Can not remove field '{name}', it does not exist") for w in self.ws_elements[name]: self.l_grid.removeWidget(w) del self.ws_elements[name] self.layout_changed() self.dataChanged.emit()
[docs] def set_from_dict(self, d: dict[str, str]): """Set the widgets from a dictionary Removes existing widgets not in the new dictionary before updating. :param d: Dictionary of key-value pairs to set """ # first remove widgets not in new dict for key in list(self.ws_elements.keys()): if key not in d: self.remove_element(key) self.update_from_dict(d)
[docs] def update_from_dict(self, d: dict[str, str]): """Update widgets from a dictionary Adds new elements for keys not already present, updates existing elements. :param d: Dictionary of key-value pairs to update """ for key, value in d.items(): if key not in self.ws_elements: self.add_element(key, value) else: self.ws_elements[key][1].setText(value)
[docs] def get_dict(self) -> dict[str, str]: """Get all key-value pairs from the input widgets :return: Dictionary of all key-value pairs """ d = {} for name, (w_label, w_input, _) in self.ws_elements.items(): key = w_label.text() value = w_input.text() d[key] = value return d