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