From ffda7eb7d8e92578bdf9e519249a035944c08439 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Mon, 4 May 2026 22:57:53 -0500 Subject: [PATCH 01/11] two new classes in shortcuts.py. KeyboardShortcutManager stores/handles a user's individual shortcut preferences. ShortcutEditorDialog handles UI interactions and updates a KSM class instance accordingly. base_instrument.py was updated to implement these new classes and add a button to edit keyboard shortcuts. --- src/instrumentserver/gui/base_instrument.py | 53 +++++- src/instrumentserver/gui/shortcuts.py | 191 ++++++++++++++++++++ 2 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/instrumentserver/gui/shortcuts.py diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 5e76af5..7644fd7 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,6 +107,7 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets +from .shortcuts import KeyboardShortcutManager, ShortcutEditorDialog class ItemBase(QtGui.QStandardItem): @@ -481,7 +482,7 @@ def filterAcceptsRow( item = parent.child(source_row, 0) # The order in which things get constructed seems to impact this. - # When the application is first starting, the proxy model does not have the trash attribute. + # When the application is first starting, the proxy model does not have the trash attribute. if hasattr(self, "trash"): if self.trash: # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion @@ -780,6 +781,7 @@ def __init__( proxyModelType: type = InstrumentSortFilterProxyModel, viewType: type = InstrumentTreeViewBase, callSignals: bool = True, + shortcutManager: Optional[KeyboardShortcutManager] = None, parent: Optional[QtWidgets.QWidget] = None, **modelKwargs: Any, ) -> None: @@ -795,6 +797,8 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) + self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() + self.layout_ = QtWidgets.QVBoxLayout() self.lineEdit = QtWidgets.QLineEdit(self) @@ -830,6 +834,10 @@ def connectSignals(self) -> None: self.proxyModel.onSortingIndicatorChanged ) + self.shortcutManager.register("focus_filter", self.lineEdit.setFocus, self) + self.shortcutManager.register("star_item", self._starCurrentItem, self) + self.shortcutManager.register("trash_item", self._trashCurrentItem, self) + def makeToolbar(self) -> QtWidgets.QToolBar: """ Creates the toolbar, override to add more buttons to the toolbar. @@ -842,6 +850,7 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "refresh all items from the instrument", ) refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("refresh_all", refreshAction) # type: ignore[union-attr] toolbar.addSeparator() @@ -850,12 +859,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "expand tree", ) expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("expand_all", expandAction) # type: ignore[union-attr] collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("collapse_all", collapseAction) # type: ignore[union-attr] toolbar.addSeparator() @@ -864,12 +875,21 @@ def makeToolbar(self) -> QtWidgets.QToolBar: ) starAction.setCheckable(True) # type: ignore[union-attr] starAction.triggered.connect(lambda x: self.promoteStar()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("toggle_star", starAction) # type: ignore[union-attr] trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) trashAction.setCheckable(True) # type: ignore[union-attr] trashAction.triggered.connect(lambda x: self.hideTrash()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("toggle_trash", trashAction) # type: ignore[union-attr] + + toolbar.addSeparator() + + shortcutsAction = toolbar.addAction( + QtGui.QIcon(":/icons/code.svg"), "Edit keyboard shortcuts" + ) + shortcutsAction.triggered.connect(self.openShortcutEditor) # type: ignore[union-attr] # Debugging tools keep commented for commits. # printAction = toolbar.addAction( @@ -894,6 +914,37 @@ def promoteStar(self) -> None: def refreshAll(self) -> None: self.model.refreshAll() + @QtCore.Slot() + def _starCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + if isinstance(item, ItemBase): + self.view.lastSelectedItem = item + self.view.itemStarToggle.emit(item) + + @QtCore.Slot() + def _trashCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + if isinstance(item, ItemBase): + self.view.lastSelectedItem = item + self.view.itemTrashToggle.emit(item) + + @QtCore.Slot() + def openShortcutEditor(self) -> None: + dialog = ShortcutEditorDialog(self.shortcutManager, self) + dialog.exec_() + def debuggingMethod(self) -> None: """ This is just a debugging method. diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py new file mode 100644 index 0000000..3740b68 --- /dev/null +++ b/src/instrumentserver/gui/shortcuts.py @@ -0,0 +1,191 @@ +import json +import logging +from typing import Callable, Optional + +from instrumentserver import QtCore, QtGui, QtWidgets + +from .misc import BaseDialog + +logger = logging.getLogger(__name__) + +class KeyboardShortcutManager: + """ + Manages keyboard shortcut mappings for the instrument GUI. + + Holds a registry of named actions with default key sequences and descriptions. + The active mapping starts from defaults and can be customized by the user and + persisted to a JSON file. + + Qt does not poll for key presses — instead, register() and apply_to_action() + hand each mapping entry to Qt's event system (QShortcut / QAction.setShortcut), + which fires the associated callback when the key is pressed. + """ + + REGISTRY: dict[str, tuple[str, str]] = { + # action_id: (default_key_sequence, description) + "refresh_all": ("Ctrl+R", "Refresh all parameters from instrument"), + "expand_all": ("Ctrl+E", "Expand all tree nodes"), + "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), + "toggle_star": ("Ctrl+Shift+S", "Toggle star filter"), + "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), + "focus_filter": ("Ctrl+F", "Focus the filter search bar"), + "star_item": ("Ctrl+S", "Star/un-star the selected parameter"), + "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + } + + def __init__(self) -> None: + self.mapping: dict[str, str] = {k: v[0] for k, v in self.REGISTRY.items()} + self._shortcut_map: dict[str, QtWidgets.QShortcut] = {} + self._action_map: dict[str, QtWidgets.QAction] = {} + + def load(self, path: str) -> None: + """Override the current mapping with entries read from a JSON file.""" + with open(path) as f: + data = json.load(f) + self.mapping.update(data) + + def save(self, path: str) -> None: + """Write the current mapping to a JSON file.""" + with open(path, "w") as f: + json.dump(self.mapping, f, indent=2) + + def apply_to_action(self, action_id: str, qaction: QtWidgets.QAction) -> None: + """Set the shortcut from the current mapping on an existing QAction and retain a reference for live rebinding.""" + key = self.mapping.get(action_id) + if key: + qaction.setShortcut(QtGui.QKeySequence(key)) + self._action_map[action_id] = qaction + + def register( + self, action_id: str, callback: Callable, widget: QtWidgets.QWidget + ) -> None: + """ + Create a QShortcut for action_id on widget and connect it to callback. + + The shortcut fires when widget or any of its children has focus. + The QShortcut object is retained internally so it is not garbage-collected + and can be updated live via rebind(). + """ + key = self.mapping.get(action_id) + if key: + sc = QtWidgets.QShortcut(QtGui.QKeySequence(key), widget) + sc.activated.connect(callback) + self._shortcut_map[action_id] = sc + + def rebind(self, action_id: str, new_key: str) -> None: + """Update a shortcut immediately. Updates the mapping and the live Qt objects.""" + self.mapping[action_id] = new_key + if action_id in self._shortcut_map: + self._shortcut_map[action_id].setKey(QtGui.QKeySequence(new_key)) + if action_id in self._action_map: + self._action_map[action_id].setShortcut(QtGui.QKeySequence(new_key)) + logger.debug(f"Rebound '{action_id}' to '{new_key}'") + + +class ShortcutEditorDialog(BaseDialog): + """ + Dialog for viewing and editing keyboard shortcuts. + + Displays all registered actions in a table. The Shortcut column is editable. + Use 'Save to file' to persist changes; 'Load from file' to restore a saved mapping. + Changes take effect on the next application start. + """ + + def __init__( + self, + manager: KeyboardShortcutManager, + parent: Optional[QtWidgets.QWidget] = None, + ) -> None: + super().__init__(parent) + self.manager = manager + self.setWindowTitle("Keyboard Shortcuts") + + self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 3, self) + self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut"]) + self._table.horizontalHeader().setStretchLastSection(True) # type: ignore[union-attr] + self._table.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows + ) + self._populateTable() + + btnLoad = QtWidgets.QPushButton("Load from file") + btnLoad.clicked.connect(self._loadFromFile) + btnSave = QtWidgets.QPushButton("Save to file") + btnSave.clicked.connect(self._saveToFile) + btnReset = QtWidgets.QPushButton("Reset to defaults") + btnReset.clicked.connect(self._resetDefaults) + btnClose = QtWidgets.QPushButton("Close") + btnClose.clicked.connect(self.accept) + + btnRow = QtWidgets.QHBoxLayout() + btnRow.addWidget(btnLoad) + btnRow.addWidget(btnSave) + btnRow.addStretch() + btnRow.addWidget(btnReset) + btnRow.addWidget(btnClose) + + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self._table) + layout.addLayout(btnRow) + self.setLayout(layout) + self.resize(600, 300) + + def _populateTable(self) -> None: + self._table.clearContents() + for row, (action_id, (_, description)) in enumerate( + self.manager.REGISTRY.items() + ): + current = self.manager.mapping.get(action_id, "") + id_item = QtWidgets.QTableWidgetItem(action_id) + id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + desc_item = QtWidgets.QTableWidgetItem(description) + desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + self._table.setItem(row, 0, id_item) + self._table.setItem(row, 1, desc_item) + key_edit = QtWidgets.QKeySequenceEdit( + QtGui.QKeySequence(current), self._table + ) + self._table.setCellWidget(row, 2, key_edit) + self._table.resizeColumnsToContents() + + def _commitTableToManager(self) -> None: + for row, action_id in enumerate(self.manager.REGISTRY): + widget = self._table.cellWidget(row, 2) + if isinstance(widget, QtWidgets.QKeySequenceEdit): + self.manager.rebind(action_id, widget.keySequence().toString()) + + @QtCore.Slot() + def _loadFromFile(self) -> None: + path, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Load Shortcuts", ".", "JSON Files (*.json);;All Files (*)" + ) + if path: + try: + self.manager.load(path) + self._populateTable() + logger.info(f"Loaded shortcuts from {path}") + except Exception as e: + logger.warning(f"Failed to load shortcuts from {path} : {e}") + + @QtCore.Slot() + def _saveToFile(self) -> None: + self._commitTableToManager() + path, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "Save Shortcuts", "shortcuts.json", "JSON Files (*.json);;All Files (*)" + ) + if path: + try: + self.manager.save(path) + logger.info(f"Saved shortcuts to {path}") + except Exception as e: + logger.warning(f"Failed to save shortcuts to {path} : {e}") + + @QtCore.Slot() + def _resetDefaults(self) -> None: + for row, (action_id, (default_key, _)) in enumerate( + self.manager.REGISTRY.items() + ): + self.manager.rebind(action_id, default_key) + widget = self._table.cellWidget(row, 2) + if isinstance(widget, QtWidgets.QKeySequenceEdit): + widget.setKeySequence(QtGui.QKeySequence(default_key)) From f531047a357a4936f5ec31aabd7fbcef79a7a896 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 6 May 2026 20:25:38 -0500 Subject: [PATCH 02/11] new structure for shortcuts. new permanent tab for shortcuts similar to Station or Log. alert column on right side indicates three states: ok (white), unsaved (orange), and duplicate (red). alert displays tooltip for each tooltip. ServerGui now holds a shortcut editor and shortcut manager, which is passed to an instrument tab when opened. --- src/instrumentserver/gui/base_instrument.py | 14 +- src/instrumentserver/gui/instruments.py | 6 + src/instrumentserver/gui/shortcuts.py | 156 +++++++++++++++--- .../resource/icons/alert-octagon-orange.svg | 21 +++ src/instrumentserver/server/application.py | 7 + 5 files changed, 168 insertions(+), 36 deletions(-) create mode 100644 src/instrumentserver/resource/icons/alert-octagon-orange.svg diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 7644fd7..ba017ed 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,7 +107,7 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets -from .shortcuts import KeyboardShortcutManager, ShortcutEditorDialog +from .shortcuts import KeyboardShortcutManager class ItemBase(QtGui.QStandardItem): @@ -884,13 +884,6 @@ def makeToolbar(self) -> QtWidgets.QToolBar: trashAction.triggered.connect(lambda x: self.hideTrash()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("toggle_trash", trashAction) # type: ignore[union-attr] - toolbar.addSeparator() - - shortcutsAction = toolbar.addAction( - QtGui.QIcon(":/icons/code.svg"), "Edit keyboard shortcuts" - ) - shortcutsAction.triggered.connect(self.openShortcutEditor) # type: ignore[union-attr] - # Debugging tools keep commented for commits. # printAction = toolbar.addAction( # QtGui.QIcon(":/icons/code.svg"), @@ -940,11 +933,6 @@ def _trashCurrentItem(self) -> None: self.view.lastSelectedItem = item self.view.itemTrashToggle.emit(item) - @QtCore.Slot() - def openShortcutEditor(self) -> None: - dialog = ShortcutEditorDialog(self.shortcutManager, self) - dialog.exec_() - def debuggingMethod(self) -> None: """ This is just a debugging method. diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 1a41ae0..60ba01f 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -465,6 +465,8 @@ def __init__( if "sub_port" in kwargs: modelKwargs["sub_port"] = kwargs.pop("sub_port") + shortcutManager = kwargs.pop("shortcutManager", None) + super().__init__( instrument=instrument, parent=parent, @@ -473,6 +475,7 @@ def __init__( modelType=ModelParameters, viewType=viewType, callSignals=callSignals, + shortcutManager=shortcutManager, **modelKwargs, ) @@ -767,11 +770,14 @@ def __init__(self, instrument: Any, **kwargs: Any) -> None: if "methods-hide" in kwargs: modelKwargs["itemsHide"] = kwargs.pop("methods-hide") + shortcutManager = kwargs.pop("shortcutManager", None) + super().__init__( instrument=instrument, attr="functions", modelType=MethodsModel, viewType=MethodsTreeView, + shortcutManager=shortcutManager, **modelKwargs, ) diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 3740b68..0804f7a 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -1,10 +1,13 @@ import json import logging +import os +from collections import defaultdict from typing import Callable, Optional -from instrumentserver import QtCore, QtGui, QtWidgets +from instrumentserver import QtCore, QtGui, QtWidgets, getInstrumentserverPath + +_ICON_DIR = getInstrumentserverPath("resource", "icons") -from .misc import BaseDialog logger = logging.getLogger(__name__) @@ -82,13 +85,23 @@ def rebind(self, action_id: str, new_key: str) -> None: logger.debug(f"Rebound '{action_id}' to '{new_key}'") -class ShortcutEditorDialog(BaseDialog): +class ShortcutEditorWidget(QtWidgets.QWidget): """ - Dialog for viewing and editing keyboard shortcuts. + Permanent widget for viewing and editing keyboard shortcuts. + + Intended to be embedded as a tab in the server window. Changes made in the + table are applied live to the manager (and therefore all registered shortcuts) + when Save is clicked. Use 'Save to file' / 'Load from file' to persist across sessions. - Displays all registered actions in a table. The Shortcut column is editable. - Use 'Save to file' to persist changes; 'Load from file' to restore a saved mapping. - Changes take effect on the next application start. + Each row has a small colored indicator dot in the rightmost column: + - white : saved and unique + - orange: unsaved change (widget value differs from manager.mapping) + - red : duplicate key sequence shared with another action (takes priority) + + QKeySequenceEdit emits a spurious keySequenceChanged after its finishing timeout + resets the internal recording state. _onEditingFinished blocks that widget's signals + for one event-loop tick (swallowing the revert signal at the source), then restores + the display if the widget actually changed its stored sequence during the block. """ def __init__( @@ -98,61 +111,157 @@ def __init__( ) -> None: super().__init__(parent) self.manager = manager - self.setWindowTitle("Keyboard Shortcuts") - self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 3, self) - self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut"]) - self._table.horizontalHeader().setStretchLastSection(True) # type: ignore[union-attr] + self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 4, self) + self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut", ""]) + header = self._table.horizontalHeader() + header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] + header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] + header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) # type: ignore[union-attr] + header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Fixed) # type: ignore[union-attr] + self._table.setColumnWidth(3, 32) self._table.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows ) + + self._indicators: list[QtWidgets.QLabel] = [] self._populateTable() btnLoad = QtWidgets.QPushButton("Load from file") btnLoad.clicked.connect(self._loadFromFile) - btnSave = QtWidgets.QPushButton("Save to file") - btnSave.clicked.connect(self._saveToFile) + btnSaveFile = QtWidgets.QPushButton("Save to file") + btnSaveFile.clicked.connect(self._saveToFile) btnReset = QtWidgets.QPushButton("Reset to defaults") btnReset.clicked.connect(self._resetDefaults) - btnClose = QtWidgets.QPushButton("Close") - btnClose.clicked.connect(self.accept) + btnSave = QtWidgets.QPushButton("Save") + btnSave.clicked.connect(self._save) btnRow = QtWidgets.QHBoxLayout() btnRow.addWidget(btnLoad) - btnRow.addWidget(btnSave) + btnRow.addWidget(btnSaveFile) btnRow.addStretch() btnRow.addWidget(btnReset) - btnRow.addWidget(btnClose) + btnRow.addWidget(btnSave) layout = QtWidgets.QVBoxLayout() layout.addWidget(self._table) layout.addLayout(btnRow) self.setLayout(layout) - self.resize(600, 300) def _populateTable(self) -> None: + self._indicators.clear() self._table.clearContents() for row, (action_id, (_, description)) in enumerate( self.manager.REGISTRY.items() ): current = self.manager.mapping.get(action_id, "") + id_item = QtWidgets.QTableWidgetItem(action_id) id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) desc_item = QtWidgets.QTableWidgetItem(description) desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) + key_edit = QtWidgets.QKeySequenceEdit( QtGui.QKeySequence(current), self._table ) + key_edit.keySequenceChanged.connect(self._onUnsavedChange) + key_edit.editingFinished.connect( + lambda w=key_edit: self._onEditingFinished(w) + ) self._table.setCellWidget(row, 2, key_edit) - self._table.resizeColumnsToContents() - def _commitTableToManager(self) -> None: + dot = QtWidgets.QLabel() + dot.setFixedSize(20, 20) + dot.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + dot.setStyleSheet( + "QToolTip { color: black; background-color: white;" + " border: 1px solid #cccccc; }" + ) + container = QtWidgets.QWidget() + cl = QtWidgets.QHBoxLayout(container) + cl.addWidget(dot) + cl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + cl.setContentsMargins(0, 0, 0, 0) + self._table.setCellWidget(row, 3, container) + self._indicators.append(dot) + + self._updateAllIndicators() + + def _collectDuplicates(self) -> dict[str, list[str]]: + """Return {key_sequence: [action_ids]} for every key bound to more than one action.""" + seen: dict[str, list[str]] = defaultdict(list) + for row, action_id in enumerate(self.manager.REGISTRY): + widget = self._table.cellWidget(row, 2) + if isinstance(widget, QtWidgets.QKeySequenceEdit): + key = widget.keySequence().toString() + if key: + seen[key].append(action_id) + return {k: v for k, v in seen.items() if len(v) > 1} + + def _updateAllIndicators(self) -> None: + duplicates = self._collectDuplicates() + for row, action_id in enumerate(self.manager.REGISTRY): + if row >= len(self._indicators): + break + dot = self._indicators[row] + widget = self._table.cellWidget(row, 2) + if not isinstance(widget, QtWidgets.QKeySequenceEdit): + continue + current = widget.keySequence().toString() + if current in duplicates: + others = [a for a in duplicates[current] if a != action_id] + self._applyIndicator(dot, "duplicate", + f"Duplicate: also bound to {', '.join(others)}") + elif current != self.manager.mapping.get(action_id, ""): + self._applyIndicator(dot, "unsaved", "Unsaved change") + else: + self._applyIndicator(dot, "ok", "") + + @staticmethod + def _applyIndicator(dot: QtWidgets.QLabel, state: str, tooltip: str) -> None: + dot.setToolTip(tooltip) + if state == "ok": + icon_file = "alert-octagon.svg" + elif state == "unsaved": + icon_file = "alert-octagon-orange.svg" + else: # duplicate + icon_file = "alert-octagon-red.svg" + pix = QtGui.QIcon(os.path.join(_ICON_DIR, icon_file)).pixmap(20, 20) + dot.setPixmap(pix) + + @QtCore.Slot() + def _onUnsavedChange(self) -> None: + self._updateAllIndicators() + + def _onEditingFinished(self, widget: QtWidgets.QKeySequenceEdit) -> None: + # Capture the intended value before Qt resets the recording state. + # Block signals for one event-loop tick so the spurious keySequenceChanged + # that follows the internal reset never reaches _onUnsavedChange. + intended = widget.keySequence().toString() + widget.blockSignals(True) + QtCore.QTimer.singleShot( + 0, lambda: self._restoreAfterRevert(intended, widget) + ) + + def _restoreAfterRevert( + self, intended: str, widget: QtWidgets.QKeySequenceEdit + ) -> None: + # If the widget reverted its stored sequence during the block window, + # put it back so the display and _save() read the correct value. + if widget.keySequence().toString() != intended: + widget.setKeySequence(QtGui.QKeySequence(intended)) + widget.blockSignals(False) + + @QtCore.Slot() + def _save(self) -> None: for row, action_id in enumerate(self.manager.REGISTRY): widget = self._table.cellWidget(row, 2) if isinstance(widget, QtWidgets.QKeySequenceEdit): self.manager.rebind(action_id, widget.keySequence().toString()) + self._updateAllIndicators() + logger.info("Shortcuts saved locally") @QtCore.Slot() def _loadFromFile(self) -> None: @@ -165,11 +274,11 @@ def _loadFromFile(self) -> None: self._populateTable() logger.info(f"Loaded shortcuts from {path}") except Exception as e: - logger.warning(f"Failed to load shortcuts from {path} : {e}") + logger.warning(f"Failed to load shortcuts from {path}: {e}") @QtCore.Slot() def _saveToFile(self) -> None: - self._commitTableToManager() + self._save() path, _ = QtWidgets.QFileDialog.getSaveFileName( self, "Save Shortcuts", "shortcuts.json", "JSON Files (*.json);;All Files (*)" ) @@ -178,7 +287,7 @@ def _saveToFile(self) -> None: self.manager.save(path) logger.info(f"Saved shortcuts to {path}") except Exception as e: - logger.warning(f"Failed to save shortcuts to {path} : {e}") + logger.warning(f"Failed to save shortcuts to {path}: {e}") @QtCore.Slot() def _resetDefaults(self) -> None: @@ -189,3 +298,4 @@ def _resetDefaults(self) -> None: widget = self._table.cellWidget(row, 2) if isinstance(widget, QtWidgets.QKeySequenceEdit): widget.setKeySequence(QtGui.QKeySequence(default_key)) + self._updateAllIndicators() diff --git a/src/instrumentserver/resource/icons/alert-octagon-orange.svg b/src/instrumentserver/resource/icons/alert-octagon-orange.svg new file mode 100644 index 0000000..061888c --- /dev/null +++ b/src/instrumentserver/resource/icons/alert-octagon-orange.svg @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 83fcc0a..8cc2854 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -14,8 +14,10 @@ from ..gui.instruments import GenericInstrument from ..gui.misc import BaseDialog, DetachableTabWidget from ..gui.parameters import AnyInputForMethod +from ..gui.shortcuts import KeyboardShortcutManager, ShortcutEditorWidget from .core import InstrumentModuleBluePrint, ParameterBluePrint, StationServer + logger = logging.getLogger(__name__) @@ -659,6 +661,10 @@ def __init__( self.serverStatus = ServerStatus() self.tabs.addUnclosableTab(self.serverStatus, "Server") + self.shortcutManager = KeyboardShortcutManager() + self.shortcutEditor = ShortcutEditorWidget(self.shortcutManager) + self.tabs.addUnclosableTab(self.shortcutEditor, "Shortcuts") + # Toolbar. self.toolBar = self.addToolBar("Tools") self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] @@ -889,6 +895,7 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: kwargs = self._guiConfig[name]["gui"]["kwargs"] kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) # type: ignore[union-attr] + kwargs["shortcutManager"] = self.shortcutManager insWidget = widgetClass(ins, parent=self, **kwargs) index = self.tabs.addTab(insWidget, ins.name) self.instrumentTabsOpen[ins.name] = insWidget From 1c205a4f0115848d7b783b3887611b8b09273b66 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 6 May 2026 21:29:13 -0500 Subject: [PATCH 03/11] added more shortcuts and implemented there backend actions --- src/instrumentserver/gui/base_instrument.py | 23 +++++++++- src/instrumentserver/gui/instruments.py | 51 ++++++++++++++++++++- src/instrumentserver/gui/shortcuts.py | 25 ++++++---- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index ba017ed..7157e4b 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -834,9 +834,11 @@ def connectSignals(self) -> None: self.proxyModel.onSortingIndicatorChanged ) - self.shortcutManager.register("focus_filter", self.lineEdit.setFocus, self) + self.shortcutManager.register("jump_filter", self.lineEdit.setFocus, self) self.shortcutManager.register("star_item", self._starCurrentItem, self) self.shortcutManager.register("trash_item", self._trashCurrentItem, self) + self.shortcutManager.register("fit_column", self._fitCurrentColumn, self) + self.shortcutManager.register("sort_column", self._sortCurrentColumn, self) def makeToolbar(self) -> QtWidgets.QToolBar: """ @@ -933,6 +935,25 @@ def _trashCurrentItem(self) -> None: self.view.lastSelectedItem = item self.view.itemTrashToggle.emit(item) + @QtCore.Slot() + def _fitCurrentColumn(self) -> None: + col = self.view.currentIndex().column() + self.view.resizeColumnToContents(col if col >= 0 else 0) + + @QtCore.Slot() + def _sortCurrentColumn(self) -> None: + header = self.view.header() + col = self.view.currentIndex().column() + if col < 0: + col = header.sortIndicatorSection() + current_order = header.sortIndicatorOrder() + new_order = ( + QtCore.Qt.SortOrder.AscendingOrder + if current_order == QtCore.Qt.SortOrder.DescendingOrder + else QtCore.Qt.SortOrder.DescendingOrder + ) + header.setSortIndicator(col, new_order) + def debuggingMethod(self) -> None: """ This is just a debugging method. diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 60ba01f..9b62444 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -19,7 +19,7 @@ InstrumentTreeViewBase, ItemBase, ) -from .parameters import AnyInputForMethod, ParameterWidget +from .parameters import AnyInput, AnyInputForMethod, ParameterWidget # TODO: all styles set through a global style sheet. # TODO: [maybe] add a column for information on valid input values? @@ -482,6 +482,36 @@ def __init__( def connectSignals(self) -> None: super().connectSignals() self.model.itemNewValue.connect(self.view.onItemNewValue) + self.shortcutManager.register("refresh_item", self._refreshCurrentItem, self) + self.shortcutManager.register("toggle_python", self._togglePythonCurrentItem, self) + + @QtCore.Slot() + def _refreshCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + widget = self.view.delegate.parameters.get(item.name) + if widget is not None: + widget.setWidgetFromParameter() + + @QtCore.Slot() + def _togglePythonCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + widget = self.view.delegate.parameters.get(item.name) + if widget is not None and isinstance(widget.paramWidget, AnyInput): + widget.paramWidget.doEval.toggle() # ----------------- Parameters Display Classes - Ending -------------------------------- @@ -618,6 +648,23 @@ def connectSignals(self) -> None: self.parameterCreationError.connect(self.addParam.setError) self.parameterCreated.connect(self.addParam.clear) self.profileManager.indexChanged.connect(self.loadProfile) + self.shortcutManager.register("delete_item", self._deleteCurrentItem, self) + self.shortcutManager.register("clear_add", self.addParam.clear, self) + self.shortcutManager.register("add_item", self.addParam.nameEdit.setFocus, self) + self.shortcutManager.register("load_items", self.loadFromFile, self) + self.shortcutManager.register("save_items", self.saveToFile, self) + + @QtCore.Slot() + def _deleteCurrentItem(self) -> None: + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + self.removeParameter(item.name) def makeToolbar(self) -> QtWidgets.QToolBar: toolbar = super().makeToolbar() @@ -629,12 +676,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: "Load parameters from file", ) loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("load_items", loadParamAction) # type: ignore[union-attr] saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] + self.shortcutManager.apply_to_action("save_items", saveParamAction) # type: ignore[union-attr] return toolbar diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 0804f7a..5912f8b 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -26,14 +26,23 @@ class KeyboardShortcutManager: REGISTRY: dict[str, tuple[str, str]] = { # action_id: (default_key_sequence, description) - "refresh_all": ("Ctrl+R", "Refresh all parameters from instrument"), - "expand_all": ("Ctrl+E", "Expand all tree nodes"), - "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), - "toggle_star": ("Ctrl+Shift+S", "Toggle star filter"), - "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), - "focus_filter": ("Ctrl+F", "Focus the filter search bar"), - "star_item": ("Ctrl+S", "Star/un-star the selected parameter"), - "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + "jump_filter": ("Ctrl+F", "Jump cursor to the filter search bar"), + "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), + "expand_all": ("Ctrl+E", "Expand all tree nodes"), + "toggle_star": ("Ctrl+Shift+A", "Toggle star filter"), + "star_item": ("Ctrl+A", "Star/un-star the selected parameter"), + "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), + "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + "refresh_all": ("Ctrl+Shift+R", "Refresh all parameters from instrument"), + "refresh_item": ("Ctrl+R", "Refresh the selected parameter"), + "toggle_python": ("Ctrl+P", "Toggle Python eval for selected parameter"), + "delete_item": ("Ctrl+Backspace", "Delete the selected parameter"), + "clear_add": ("Ctrl+Shift+N", "Clear regions of add parameter bar"), + "add_item": ("Ctrl+N", "Jump cursor to the add parameter bar"), + "load_items": ("Ctrl+O", "Load parameters from JSON file"), + "save_items": ("Ctrl+S", "Save parameters to JSON file"), + "fit_column": ("Ctrl+Shift+D", "Fits column width"), + "sort_column": ("Ctrl+D", "Toggle sorting of selected column") } def __init__(self) -> None: From c64a4897a809683298d348f15d9d2c193d03116c Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 6 May 2026 23:01:15 -0500 Subject: [PATCH 04/11] ruff edits v2 --- src/instrumentserver/gui/base_instrument.py | 152 ++++++++++---------- src/instrumentserver/gui/instruments.py | 48 ++++--- src/instrumentserver/gui/shortcuts.py | 82 ++++++----- src/instrumentserver/server/application.py | 99 +++++++------ 4 files changed, 205 insertions(+), 176 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 7157e4b..fc15dcd 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,6 +107,7 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets + from .shortcuts import KeyboardShortcutManager @@ -150,8 +151,8 @@ class DelegateBase(QtWidgets.QStyledItemDelegate): def getItem(cls, QModelIndex: QtCore.QModelIndex) -> QtGui.QStandardItem: proxyModel = QModelIndex.model() - model = proxyModel.sourceModel() # type: ignore[union-attr] - item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] + model = proxyModel.sourceModel() + item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) if item.column != 0: parent = item.parent() row = item.row() @@ -224,7 +225,7 @@ def __init__( self.loadingItems = False @staticmethod - def _matches_any_pattern(name: str, patterns: List[str]) -> bool: + def _matches_any_pattern(name: str, patterns: Optional[List[str]]) -> bool: """ Check if a name matches any glob pattern in the list. @@ -238,6 +239,8 @@ def _matches_any_pattern(name: str, patterns: List[str]) -> bool: :param patterns: List of glob patterns to match against (e.g., 'power_*', '*_frequency') :return: True if name matches any pattern, False otherwise """ + if not patterns: + return False for pattern in patterns: if fnmatch.fnmatch(name, pattern): return True @@ -259,13 +262,13 @@ def loadItems(self, module: Any = None, prefix: Optional[str] = None) -> None: # constructor if prefix is not None: objectName = ".".join([prefix, objectName]) - if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] + if not self._matches_any_pattern(objectName, self.itemsHide): item = self.addItem( fullName=objectName, star=False, trash=False, element=obj ) - if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsTrash): self.onItemTrashToggle(item) - if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsStar): self.onItemStarToggle(item) for submodName, submod in module.submodules.items(): @@ -332,20 +335,20 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: - if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] + if not self._matches_any_pattern(smName, self.itemsHide): + self.insertItemTo(parent, subModItem) + if self._matches_any_pattern(smName, self.itemsTrash): self.onItemTrashToggle(subModItem) - if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsStar): self.onItemStarToggle(subModItem) else: - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - parent = subModItem # type: ignore[assignment] + self.insertItemTo(parent, subModItem) + parent = subModItem else: - parent = items[0] # type: ignore[assignment] + parent = items[0] newItem = self.itemClass(name=fullName, **kwargs) - self.insertItemTo(parent, newItem) # type: ignore[arg-type] + self.insertItemTo(parent, newItem) return newItem @@ -465,7 +468,7 @@ def _isParentTrash(self, parent: Optional["ItemBase"]) -> bool: if parent.trash: return True - return self._isParentTrash(parent.parent()) # type: ignore[arg-type] + return self._isParentTrash(parent.parent()) def filterAcceptsRow( self, source_row: int, source_parent: QtCore.QModelIndex @@ -488,7 +491,7 @@ def filterAcceptsRow( # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] + if self._isParentTrash(parent) or getattr( item, "trash", False ): # item could be None when it's trashed and hidden return False @@ -510,15 +513,15 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: rightItem = model.itemFromIndex(right) if hasattr(leftItem, "star") and hasattr(rightItem, "star"): if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: return True - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: return False elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: return False - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: return True return super().lessThan(left, right) @@ -542,7 +545,7 @@ def __init__( super().__init__(parent=parent) # Indicates if a column is using delegates. - self.delegateColumns = delegateColumns + self.delegateColumns: List[int] = delegateColumns or [] self.lastSelectedItem = None # Stores the last collapsed state before a change in filtering to restore it afterwards. # The keys are persistent indexes from the original model (not the proxy one) and the values a bool @@ -563,8 +566,8 @@ def __init__( self.setSortingEnabled(False) # The tree should not have anything to do with filtering itself since that is left for the proxy model. - self.header().setSortIndicatorShown(True) # type: ignore[union-attr] - self.header().setSectionsClickable(True) # type: ignore[union-attr] + self.header().setSortIndicatorShown(True) + self.header().setSectionsClickable(True) self.setAlternatingRowColors(True) @@ -601,8 +604,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(index) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if item.hasChildren(): # type: ignore[union-attr] - self.fillCollapsedDict(item) # type: ignore[arg-type] + if item.hasChildren(): + self.fillCollapsedDict(item) else: for i in range(parentItem.rowCount()): child = parentItem.child(i, 0) @@ -613,8 +616,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(childIndex) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if child.hasChildren(): # type: ignore[union-attr] - self.fillCollapsedDict(child) # type: ignore[arg-type] + if child.hasChildren(): + self.fillCollapsedDict(child) @QtCore.Slot() def restoreCollapsedDict(self) -> None: @@ -623,24 +626,23 @@ def restoreCollapsedDict(self) -> None: the persistent editors and triggers a resizing of delegates. """ for persistentIndex, state in self.collapsedState.items(): - modelIndex = self.modelActual.index( # type: ignore[union-attr] + modelIndex = self.modelActual.index( persistentIndex.row(), persistentIndex.column(), persistentIndex.parent(), ) - item = self.modelActual.itemFromIndex(modelIndex) # type: ignore[union-attr] - proxyIndex = self.model().mapFromSource(modelIndex) # type: ignore[union-attr] + item = self.modelActual.itemFromIndex(modelIndex) + proxyIndex = self.model().mapFromSource(modelIndex) self.setExpanded(proxyIndex, state) if item.showDelegate: delegateIndexes = [ - self.modelActual.index( # type: ignore[union-attr] + self.modelActual.index( persistentIndex.row(), x, persistentIndex.parent() ) - for x in self.delegateColumns # type: ignore[union-attr] + for x in self.delegateColumns ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) # type: ignore[union-attr] - for index in delegateIndexes + self.model().mapFromSource(index) for index in delegateIndexes ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) @@ -655,14 +657,14 @@ def setAllDelegatesPersistent( :param parentIndex: If None, start the process. if it's an item, it will go through the children """ if parentIndex is None: - for i in range(self.model().rowCount()): # type: ignore[union-attr] - for column in self.delegateColumns: # type: ignore[union-attr] - index = self.model().index(i, column) # type: ignore[union-attr] - index0 = self.model().index( # type: ignore[union-attr] + for i in range(self.model().rowCount()): + for column in self.delegateColumns: + index = self.model().index(i, column) + index0 = self.model().index( i, 0 ) # Only items at column 0 hold children and model info - item0 = self.modelActual.itemFromIndex( # type: ignore[union-attr] - self.model().mapToSource(index0) # type: ignore[union-attr] + item0 = self.modelActual.itemFromIndex( + self.model().mapToSource(index0) ) if item0.showDelegate: self.openPersistentEditor(index) @@ -670,18 +672,18 @@ def setAllDelegatesPersistent( self.setAllDelegatesPersistent(index0) else: - parentItem = self.modelActual.itemFromIndex( # type: ignore[union-attr] - self.model().mapToSource(parentIndex) # type: ignore[union-attr] + parentItem = self.modelActual.itemFromIndex( + self.model().mapToSource(parentIndex) ) for i in range(parentItem.rowCount()): - for column in self.delegateColumns: # type: ignore[union-attr] + for column in self.delegateColumns: item = parentItem.child(i, column) item0 = parentItem.child(i, 0) - index = self.model().mapFromSource( # type: ignore[union-attr] - self.modelActual.indexFromItem(item) # type: ignore[union-attr] + index = self.model().mapFromSource( + self.modelActual.indexFromItem(item) ) - index0 = self.model().mapFromSource( # type: ignore[union-attr] - self.modelActual.indexFromItem(item0) # type: ignore[union-attr] + index0 = self.model().mapFromSource( + self.modelActual.indexFromItem(item0) ) if item0.showDelegate: self.openPersistentEditor(index) @@ -699,13 +701,13 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: if item.showDelegate: row = item.row() parent = item.parent() - for column in self.delegateColumns: # type: ignore[union-attr] + for column in self.delegateColumns: if parent is None: - sibling = self.modelActual.item(row, column) # type: ignore[union-attr] + sibling = self.modelActual.item(row, column) else: sibling = parent.child(row, column) - index = self.model().mapFromSource( # type: ignore[union-attr] - self.modelActual.indexFromItem(sibling) # type: ignore[union-attr] + index = self.model().mapFromSource( + self.modelActual.indexFromItem(sibling) ) self.openPersistentEditor(index) self.scheduleDelayedItemsLayout() @@ -714,9 +716,9 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: def onContextMenuRequested(self, pos: QtCore.QPoint) -> None: # We get the item from the real model, not the proxy model - originalModel = self.model().sourceModel() # type: ignore[union-attr] + originalModel = self.model().sourceModel() proxyIndex = self.indexAt(pos) - index = self.model().mapToSource(proxyIndex) # type: ignore[union-attr] + index = self.model().mapToSource(proxyIndex) # catch the case if the user rightcliks on any other column if index.column() != 0: @@ -797,7 +799,11 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() + self.shortcutManager = ( + shortcutManager + if shortcutManager is not None + else KeyboardShortcutManager() + ) self.layout_ = QtWidgets.QVBoxLayout() @@ -851,8 +857,8 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/refresh.svg"), "refresh all items from the instrument", ) - refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("refresh_all", refreshAction) # type: ignore[union-attr] + refreshAction.triggered.connect(lambda x: self.refreshAll()) + self.shortcutManager.apply_to_action("refresh_all", refreshAction) toolbar.addSeparator() @@ -860,31 +866,31 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/expand.svg"), "expand tree", ) - expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("expand_all", expandAction) # type: ignore[union-attr] + expandAction.triggered.connect(lambda x: self.view.expandAll()) + self.shortcutManager.apply_to_action("expand_all", expandAction) collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) - collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("collapse_all", collapseAction) # type: ignore[union-attr] + collapseAction.triggered.connect(lambda x: self.view.collapseAll()) + self.shortcutManager.apply_to_action("collapse_all", collapseAction) toolbar.addSeparator() starAction = toolbar.addAction( QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) - starAction.setCheckable(True) # type: ignore[union-attr] - starAction.triggered.connect(lambda x: self.promoteStar()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("toggle_star", starAction) # type: ignore[union-attr] + starAction.setCheckable(True) + starAction.triggered.connect(lambda x: self.promoteStar()) + self.shortcutManager.apply_to_action("toggle_star", starAction) trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) - trashAction.setCheckable(True) # type: ignore[union-attr] - trashAction.triggered.connect(lambda x: self.hideTrash()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("toggle_trash", trashAction) # type: ignore[union-attr] + trashAction.setCheckable(True) + trashAction.triggered.connect(lambda x: self.hideTrash()) + self.shortcutManager.apply_to_action("toggle_trash", trashAction) # Debugging tools keep commented for commits. # printAction = toolbar.addAction( @@ -917,7 +923,7 @@ def _starCurrentItem(self) -> None: source_index = self.proxyModel.mapToSource(proxy_index) if source_index.column() != 0: source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + item = self.model.itemFromIndex(source_index) if isinstance(item, ItemBase): self.view.lastSelectedItem = item self.view.itemStarToggle.emit(item) @@ -930,7 +936,7 @@ def _trashCurrentItem(self) -> None: source_index = self.proxyModel.mapToSource(proxy_index) if source_index.column() != 0: source_index = source_index.sibling(source_index.row(), 0) - item = self.model.itemFromIndex(source_index) # type: ignore[union-attr] + item = self.model.itemFromIndex(source_index) if isinstance(item, ItemBase): self.view.lastSelectedItem = item self.view.itemTrashToggle.emit(item) @@ -963,13 +969,13 @@ def debuggingMethod(self) -> None: def fillChildren(parent: QtGui.QStandardItem) -> None: for i in range(parent.rowCount()): item = parent.child(i, 0) - items[item.name] = { # type: ignore[union-attr] + items[item.name] = { "item": item, - "star": item.star, # type: ignore[union-attr] - "trash": item.trash, # type: ignore[union-attr] + "star": item.star, + "trash": item.trash, } - if item.hasChildren(): # type: ignore[union-attr] - fillChildren(item) # type: ignore[arg-type] + if item.hasChildren(): + fillChildren(item) for i in range(self.model.rowCount()): item = self.model.item(i, 0) diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 9b62444..6f2918c 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -158,7 +158,7 @@ def clear(self) -> None: self.unitEdit.setText("") if self.typeInput: self.typeSelect.setCurrentText( - parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] + parameterTypes[ParameterTypes.numeric]["name"] ) self.valsArgsEdit.setText("") @@ -287,7 +287,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor( # type: ignore[override] + def createEditor( self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, @@ -297,10 +297,10 @@ def createEditor( # type: ignore[override] This is the function that is supposed to create the widget. It should return it. """ item = self.getItem(index) - element = item.element # type: ignore[attr-defined] + element = item.element ret = ParameterWidget(element, widget) - self.parameters[item.name] = ret # type: ignore[attr-defined] + self.parameters[item.name] = ret # Try to fetch and display current value immediately # ---- Chao: removed because the constructor of ParameterWidget object already calls parameter get ---- # if element.gettable: @@ -333,7 +333,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.subClient = SubClient([self.instrument.name], **subClientArgs) self.subClient.moveToThread(self.cliThread) - self.cliThread.started.connect(self.subClient.connect) # type: ignore[arg-type] + self.cliThread.started.connect(self.subClient.connect) self.subClient.update.connect(self.updateParameter) self.subClient.finished.connect(self.cliThread.quit) @@ -398,8 +398,8 @@ def insertItemTo( if item is not None: # A parameter might not have a unit unit = "" - if item.element is not None: # type: ignore[attr-defined] - unit = item.element.unit # type: ignore[attr-defined] + if item.element is not None: + unit = item.element.unit unitItem = QtGui.QStandardItem(unit) extraItem = QtGui.QStandardItem() @@ -483,7 +483,9 @@ def connectSignals(self) -> None: super().connectSignals() self.model.itemNewValue.connect(self.view.onItemNewValue) self.shortcutManager.register("refresh_item", self._refreshCurrentItem, self) - self.shortcutManager.register("toggle_python", self._togglePythonCurrentItem, self) + self.shortcutManager.register( + "toggle_python", self._togglePythonCurrentItem, self + ) @QtCore.Slot() def _refreshCurrentItem(self) -> None: @@ -524,18 +526,18 @@ class ParameterDeleteDelegate(ParameterDelegate): #: Emits the name of the parameter to be deleted when the user presses the delete button. removeParameter = QtCore.Signal(str) - def createEditor( # type: ignore[override] + def createEditor( self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element # type: ignore[attr-defined] - rw = self.makeRemoveWidget(item.name, widget) # type: ignore[attr-defined] + element = item.element + rw = self.makeRemoveWidget(item.name, widget) ret = ParameterWidget(parameter=element, parent=widget, additionalWidgets=[rw]) - self.parameters[item.name] = ret # type: ignore[attr-defined] + self.parameters[item.name] = ret return ret @@ -583,7 +585,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.setEditable(False) - self.params = self.parent().instrument # type: ignore[union-attr] + self.params = self.parent().instrument self.refreshing = False loadingProfile = None @@ -675,15 +677,15 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/load.svg"), "Load parameters from file", ) - loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("load_items", loadParamAction) # type: ignore[union-attr] + loadParamAction.triggered.connect(lambda x: self.loadFromFile()) + self.shortcutManager.apply_to_action("load_items", loadParamAction) saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) - saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] - self.shortcutManager.apply_to_action("save_items", saveParamAction) # type: ignore[union-attr] + saveParamAction.triggered.connect(lambda x: self.saveToFile()) + self.shortcutManager.apply_to_action("save_items", saveParamAction) return toolbar @@ -768,22 +770,22 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: self.methods: Dict[str, "MethodDisplay"] = {} - def createEditor( # type: ignore[override] + def createEditor( self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element # type: ignore[attr-defined] - ret = MethodDisplay(element, item.name, parent=widget) # type: ignore[attr-defined] + element = item.element + ret = MethodDisplay(element, item.name, parent=widget) parent = self.parent() assert hasattr(parent, "clearAlertsAction") # connecting the widget with the clear alert signal - parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) # type: ignore[union-attr] + parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) - self.methods[item.name] = ret # type: ignore[attr-defined] + self.methods[item.name] = ret return ret @@ -883,7 +885,7 @@ def __init__( self.parametersList.view.resizeColumnToContents(1) self.methodsList.view.resizeColumnToContents(0) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # type: ignore[override] + def closeEvent(self, event: QtGui.QCloseEvent) -> None: """Stop the parameter subscriber thread before destruction.""" model = getattr(self.parametersList, "model", None) if model is not None and hasattr(model, "stopListener"): diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 5912f8b..376f1cc 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -11,6 +11,7 @@ logger = logging.getLogger(__name__) + class KeyboardShortcutManager: """ Manages keyboard shortcut mappings for the instrument GUI. @@ -26,23 +27,23 @@ class KeyboardShortcutManager: REGISTRY: dict[str, tuple[str, str]] = { # action_id: (default_key_sequence, description) - "jump_filter": ("Ctrl+F", "Jump cursor to the filter search bar"), - "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), - "expand_all": ("Ctrl+E", "Expand all tree nodes"), - "toggle_star": ("Ctrl+Shift+A", "Toggle star filter"), - "star_item": ("Ctrl+A", "Star/un-star the selected parameter"), - "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), - "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), - "refresh_all": ("Ctrl+Shift+R", "Refresh all parameters from instrument"), - "refresh_item": ("Ctrl+R", "Refresh the selected parameter"), - "toggle_python": ("Ctrl+P", "Toggle Python eval for selected parameter"), - "delete_item": ("Ctrl+Backspace", "Delete the selected parameter"), - "clear_add": ("Ctrl+Shift+N", "Clear regions of add parameter bar"), - "add_item": ("Ctrl+N", "Jump cursor to the add parameter bar"), - "load_items": ("Ctrl+O", "Load parameters from JSON file"), - "save_items": ("Ctrl+S", "Save parameters to JSON file"), - "fit_column": ("Ctrl+Shift+D", "Fits column width"), - "sort_column": ("Ctrl+D", "Toggle sorting of selected column") + "jump_filter": ("Ctrl+F", "Jump cursor to the filter search bar"), + "collapse_all": ("Ctrl+Shift+E", "Collapse all tree nodes"), + "expand_all": ("Ctrl+E", "Expand all tree nodes"), + "toggle_star": ("Ctrl+Shift+A", "Toggle star filter"), + "star_item": ("Ctrl+A", "Star/un-star the selected parameter"), + "toggle_trash": ("Ctrl+Shift+T", "Toggle trash filter"), + "trash_item": ("Ctrl+T", "Trash/un-trash the selected parameter"), + "refresh_all": ("Ctrl+Shift+R", "Refresh all parameters from instrument"), + "refresh_item": ("Ctrl+R", "Refresh the selected parameter"), + "toggle_python": ("Ctrl+P", "Toggle Python eval for selected parameter"), + "delete_item": ("Ctrl+Backspace", "Delete the selected parameter"), + "clear_add": ("Ctrl+Shift+N", "Clear regions of add parameter bar"), + "add_item": ("Ctrl+N", "Jump cursor to the add parameter bar"), + "load_items": ("Ctrl+O", "Load parameters from JSON file"), + "save_items": ("Ctrl+S", "Save parameters to JSON file"), + "fit_column": ("Ctrl+Shift+D", "Fits column width"), + "sort_column": ("Ctrl+D", "Toggle sorting of selected column"), } def __init__(self) -> None: @@ -61,8 +62,12 @@ def save(self, path: str) -> None: with open(path, "w") as f: json.dump(self.mapping, f, indent=2) - def apply_to_action(self, action_id: str, qaction: QtWidgets.QAction) -> None: + def apply_to_action( + self, action_id: str, qaction: Optional[QtWidgets.QAction] + ) -> None: """Set the shortcut from the current mapping on an existing QAction and retain a reference for live rebinding.""" + if qaction is None: + return key = self.mapping.get(action_id) if key: qaction.setShortcut(QtGui.QKeySequence(key)) @@ -124,10 +129,14 @@ def __init__( self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 4, self) self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut", ""]) header = self._table.horizontalHeader() - header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] - header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents) # type: ignore[union-attr] - header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) # type: ignore[union-attr] - header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Fixed) # type: ignore[union-attr] + header.setSectionResizeMode( + 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents + ) + header.setSectionResizeMode( + 1, QtWidgets.QHeaderView.ResizeMode.ResizeToContents + ) + header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeMode.Fixed) self._table.setColumnWidth(3, 32) self._table.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows @@ -166,9 +175,15 @@ def _populateTable(self) -> None: current = self.manager.mapping.get(action_id, "") id_item = QtWidgets.QTableWidgetItem(action_id) - id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + id_item.setFlags( + QtCore.Qt.ItemFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + ) desc_item = QtWidgets.QTableWidgetItem(description) - desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + desc_item.setFlags( + QtCore.Qt.ItemFlags( + desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable + ) + ) self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) @@ -221,8 +236,9 @@ def _updateAllIndicators(self) -> None: current = widget.keySequence().toString() if current in duplicates: others = [a for a in duplicates[current] if a != action_id] - self._applyIndicator(dot, "duplicate", - f"Duplicate: also bound to {', '.join(others)}") + self._applyIndicator( + dot, "duplicate", f"Duplicate: also bound to {', '.join(others)}" + ) elif current != self.manager.mapping.get(action_id, ""): self._applyIndicator(dot, "unsaved", "Unsaved change") else: @@ -245,20 +261,13 @@ def _onUnsavedChange(self) -> None: self._updateAllIndicators() def _onEditingFinished(self, widget: QtWidgets.QKeySequenceEdit) -> None: - # Capture the intended value before Qt resets the recording state. - # Block signals for one event-loop tick so the spurious keySequenceChanged - # that follows the internal reset never reaches _onUnsavedChange. intended = widget.keySequence().toString() widget.blockSignals(True) - QtCore.QTimer.singleShot( - 0, lambda: self._restoreAfterRevert(intended, widget) - ) + QtCore.QTimer.singleShot(0, lambda: self._restoreAfterRevert(intended, widget)) def _restoreAfterRevert( self, intended: str, widget: QtWidgets.QKeySequenceEdit ) -> None: - # If the widget reverted its stored sequence during the block window, - # put it back so the display and _save() read the correct value. if widget.keySequence().toString() != intended: widget.setKeySequence(QtGui.QKeySequence(intended)) widget.blockSignals(False) @@ -289,7 +298,10 @@ def _loadFromFile(self) -> None: def _saveToFile(self) -> None: self._save() path, _ = QtWidgets.QFileDialog.getSaveFileName( - self, "Save Shortcuts", "shortcuts.json", "JSON Files (*.json);;All Files (*)" + self, + "Save Shortcuts", + "shortcuts.json", + "JSON Files (*.json);;All Files (*)", ) if path: try: diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 8cc2854..4bf2a42 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -17,7 +17,6 @@ from ..gui.shortcuts import KeyboardShortcutManager, ShortcutEditorWidget from .core import InstrumentModuleBluePrint, ParameterBluePrint, StationServer - logger = logging.getLogger(__name__) @@ -62,7 +61,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) ) self.deleteAction.triggered.connect(self.onDeleteAction) self.itemSelectionChanged.connect(self._processSelection) @@ -131,7 +130,7 @@ class ServerStatus(QtWidgets.QWidget): def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) - self.layout = QtWidgets.QVBoxLayout(self) # type: ignore[assignment,method-assign] + self.layout = QtWidgets.QVBoxLayout(self) # At the top: a status label, and a button for emitting a test message self.addressLabel = QtWidgets.QLabel() @@ -145,13 +144,13 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: ) ) - self.layout.addLayout(self.statusLayout) # type: ignore[attr-defined] + self.layout.addLayout(self.statusLayout) # next row: a window for displaying the incoming messages. - self.layout.addWidget(QtWidgets.QLabel("Messages:")) # type: ignore[attr-defined] + self.layout.addWidget(QtWidgets.QLabel("Messages:")) self.messages = QtWidgets.QTextEdit() self.messages.setReadOnly(True) - self.layout.addWidget(self.messages) # type: ignore[attr-defined] + self.layout.addWidget(self.messages) @QtCore.Slot(str) def setListeningAddress(self, addr: str) -> None: @@ -329,7 +328,7 @@ def __init__(self, guiConfig: Optional[dict] = None, *args: Any) -> None: self.contextMenu.addSeparator() self.contextMenu.addAction(self.deletePossibleInstrumentAction) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) ) self.basedInstrumentAction.triggered.connect(self.onBasedInstrumentAction) @@ -433,16 +432,16 @@ def onRemoveInstrumentFromTree(self) -> None: for item in items: if item.childCount() == 0: parent = item.parent() - if item.configName is not None and item.configName in self.config: # type: ignore[attr-defined] - del self.config[item.configName] # type: ignore[attr-defined] - parent.removeChild(item) # type: ignore[union-attr] - if parent.childCount() == 0: # type: ignore[union-attr] + if item.configName is not None and item.configName in self.config: + del self.config[item.configName] + parent.removeChild(item) + if parent.childCount() == 0: self.takeTopLevelItem((self.indexOfTopLevelItem(parent))) else: for i in range(item.childCount()): child = item.child(i) - if child.configName in self.config: # type: ignore[union-attr] - del self.config[child.configName] # type: ignore[union-attr] + if child.configName in self.config: + del self.config[child.configName] self.takeTopLevelItem(self.indexOfTopLevelItem(item)) @@ -610,8 +609,8 @@ def __init__( else: self._guiConfig = guiConfig - self.stationServer = None - self.stationServerThread = None + self.stationServer: Optional[StationServer] = None + self.stationServerThread: Optional[QtCore.QThread] = None self.instrumentTabsOpen: dict[str, GenericInstrument] = {} @@ -667,31 +666,31 @@ def __init__( # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] + self.toolBar.addAction(self.refreshStationAction) # Parameter tools. - self.toolBar.addSeparator() # type: ignore[union-attr] - self.toolBar.addWidget(QtWidgets.QLabel("Params:")) # type: ignore[union-attr] + self.toolBar.addSeparator() + self.toolBar.addWidget(QtWidgets.QLabel("Params:")) self.loadParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/load.svg"), "Load from file", self ) self.loadParamsAction.triggered.connect(self.loadParamsFromFile) - self.toolBar.addAction(self.loadParamsAction) # type: ignore[union-attr] + self.toolBar.addAction(self.loadParamsAction) self.saveParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/save.svg"), "Save to file", self ) self.saveParamsAction.triggered.connect(self.saveParamsToFile) - self.toolBar.addAction(self.saveParamsAction) # type: ignore[union-attr] + self.toolBar.addAction(self.saveParamsAction) self.serverStatus.testButton.clicked.connect( lambda x: self.client.ask("Ping server.") @@ -710,7 +709,7 @@ def __init__( def log(self, message: str, level: LogLevels = LogLevels.info) -> None: log(logger, message, level) - def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: + def closeEvent(self, event: QtGui.QCloseEvent) -> None: for name, widget in list(self.instrumentTabsOpen.items()): try: widget.close() @@ -721,6 +720,7 @@ def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: if ( hasattr(self, "stationServerThread") and self.stationServerThread is not None + and self.stationServer is not None ): if self.stationServerThread.isRunning(): try: @@ -732,33 +732,39 @@ def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: self.client.disconnect() except Exception: pass - event.accept() # type: ignore[union-attr] + event.accept() def startServer(self) -> None: """Start the instrument server in a separate thread.""" - self.stationServer = StationServer(**self._serverKwargs) # type: ignore[assignment] - self.stationServerThread = QtCore.QThread() # type: ignore[assignment] - self.stationServer.moveToThread(self.stationServerThread) # type: ignore[attr-defined] - self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type,attr-defined] - self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServerThread.quit) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServer.deleteLater) # type: ignore[attr-defined] + self.stationServer = StationServer(**self._serverKwargs) + self.stationServerThread = QtCore.QThread() + assert self.stationServer is not None + assert self.stationServerThread is not None + self.stationServer.moveToThread(self.stationServerThread) + self.stationServerThread.started.connect(self.stationServer.startServer) + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) + self.stationServer.finished.connect(self.stationServerThread.quit) + self.stationServer.finished.connect(self.stationServer.deleteLater) # Connecting some additional things for messages. - self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.client.start) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.refreshStationComponents) # type: ignore[attr-defined] - self.stationServer.finished.connect( # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) + self.stationServer.serverStarted.connect(self.client.start) + self.stationServer.serverStarted.connect(self.refreshStationComponents) + self.stationServer.finished.connect( lambda: self.log("Server thread finished.", LogLevels.info) ) - self.stationServer.messageReceived.connect(self._messageReceived) # type: ignore[attr-defined] - self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) # type: ignore[attr-defined] - self.stationServer.funcCalled.connect(self.onFuncCalled) # type: ignore[attr-defined] + self.stationServer.messageReceived.connect(self._messageReceived) + self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) + self.stationServer.funcCalled.connect(self.onFuncCalled) - self.stationServerThread.start() # type: ignore[attr-defined] + self.stationServerThread.start() def getServerIfRunning(self) -> Optional["StationServer"]: - if self.stationServer is not None and self.stationServerThread.isRunning(): # type: ignore[union-attr] + if ( + self.stationServer is not None + and self.stationServerThread is not None + and self.stationServerThread.isRunning() + ): return self.stationServer else: return None @@ -866,7 +872,7 @@ def displayComponentInfo(self, name: Union[str, None]) -> None: bp = self._bluePrints[name] else: bp = None - self.stationObjInfo.setObject(bp) # type: ignore[arg-type] + self.stationObjInfo.setObject(bp) @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: @@ -894,7 +900,10 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: if "kwargs" in self._guiConfig[name]["gui"]: kwargs = self._guiConfig[name]["gui"]["kwargs"] - kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) # type: ignore[union-attr] + station_server = self.stationServer + if station_server is None: + raise RuntimeError("addInstrumentToGui called before server started") + kwargs["sub_port"] = kwargs.get("sub_port", station_server.port + 1) kwargs["shortcutManager"] = self.shortcutManager insWidget = widgetClass(ins, parent=self, **kwargs) index = self.tabs.addTab(insWidget, ins.name) @@ -953,15 +962,15 @@ def __init__(self, host: str = "localhost", port: int = 5555) -> None: # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] + self.toolBar.addAction(self.refreshStationAction) self.refreshStationComponents() From b618362ae8cae300d15e6c2c7848a9a4cb6e4234 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Mon, 18 May 2026 17:25:39 -0400 Subject: [PATCH 05/11] shortcuts now stored in server config file instead of separate JSON file --- src/instrumentserver/apps.py | 9 +++- src/instrumentserver/client/proxy.py | 4 +- src/instrumentserver/config.py | 10 +++- src/instrumentserver/gui/base_instrument.py | 2 +- src/instrumentserver/gui/shortcuts.py | 57 ++++++++------------- src/instrumentserver/server/application.py | 8 ++- 6 files changed, 47 insertions(+), 43 deletions(-) diff --git a/src/instrumentserver/apps.py b/src/instrumentserver/apps.py index 0d881d1..e624e42 100644 --- a/src/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -63,14 +63,15 @@ def serverScript() -> None: stationConfig, serverConfig, guiConfig, + shortcutConfig, tempFile, pollingRates, pollingThread, ipAddresses, - ) = None, None, None, None, None, None, None + ) = None, None, None, None, None, None, None, None if configPath != "": # Separates the corresponding settings into the 5 necessary parts - stationConfig, serverConfig, guiConfig, tempFile, pollingRates, ipAddresses = ( + stationConfig, serverConfig, guiConfig, shortcutConfig, tempFile, pollingRates, ipAddresses = ( loadConfig(configPath) ) if pollingRates is not None and pollingRates != {}: @@ -89,8 +90,10 @@ def serverScript() -> None: serverConfig=serverConfig, stationConfig=stationConfig, guiConfig=guiConfig, + shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, + configPath=configPath ) else: serverWithGui( @@ -100,8 +103,10 @@ def serverScript() -> None: serverConfig=serverConfig, stationConfig=stationConfig, guiConfig=guiConfig, + shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, + configPath=configPath ) # Close and delete the temporary files diff --git a/src/instrumentserver/client/proxy.py b/src/instrumentserver/client/proxy.py index 3c43d74..391542c 100644 --- a/src/instrumentserver/client/proxy.py +++ b/src/instrumentserver/client/proxy.py @@ -18,7 +18,7 @@ import qcodes as qc import zmq from qcodes import Instrument, Parameter -from qcodes.instrument.base import InstrumentBase +from qcodes.instrument import InstrumentBase from instrumentserver import DEFAULT_PORT, QtCore from instrumentserver.helpers import flat_to_nested_dict, flatten_dict, is_flat_dict @@ -859,7 +859,7 @@ def __init__( # Use config.py to parse server config format from instrumentserver.config import loadConfig - _, serverConfig, fullConfig, tempFile, _, _ = loadConfig(config_path) + _, serverConfig, fullConfig, _, tempFile, _, _ = loadConfig(config_path) tempFile.close() # Clean up temp file self.full_config = fullConfig diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index d177c87..2f8a2a2 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -18,7 +18,7 @@ GUIFIELD = {"type": "instrumentserver.gui.instruments.GenericInstrument", "kwargs": {}} -def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict, dict]: +def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes], dict, dict]: """ Loads the config for the instrumentserver. From 1 config file it splits the respective fields into 3 different objects: a serverConfig (the configurations for the server), a stationConfig(the qcodes station config file clean @@ -36,6 +36,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict serverConfig: dict = {} # Config for the server guiConfig = {} # Individual gui config of each instrument fullConfig = {} # serverConfig + guiConfig + any unfilled fields. Used for creating instruments from the gui + shortcutConfig = {} # Preferences for keyboard shortcuts pollingRates = {} # Polling rates for each parameter ipAddresses = {} # Dictionary of IP Addresses to send broadcasts to: # externalBroadcast: where to externally send parameter change broadcasts to, formatted like "tcp://address:port" @@ -149,6 +150,11 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict # Update fullConfig with merged GUI config fullConfig[instrumentName]["gui"] = guiConfig[instrumentName] + + # Gets all shortcuts different to REGISTRY defaults from the config file + if "shortcuts" in rawConfig: + shortcutConfig = rawConfig["shortcuts"] + rawConfig.pop("shortcuts") # Gets all of the broadcasting and listening addresses from the config file if "networking" in rawConfig: @@ -170,4 +176,4 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, IO[bytes], dict tempFilePath = tempFile.name # You need to return the tempFile itself so that the garbage collector doesn't touch it - return tempFilePath, serverConfig, fullConfig, tempFile, pollingRates, ipAddresses + return tempFilePath, serverConfig, fullConfig, shortcutConfig, tempFile, pollingRates, ipAddresses diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index fc15dcd..7b38b42 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -108,7 +108,7 @@ from instrumentserver import QtCore, QtGui, QtWidgets -from .shortcuts import KeyboardShortcutManager +from instrumentserver.gui.shortcuts import KeyboardShortcutManager class ItemBase(QtGui.QStandardItem): diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 376f1cc..8d33eb5 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -1,4 +1,4 @@ -import json +import yaml import logging import os from collections import defaultdict @@ -51,16 +51,22 @@ def __init__(self) -> None: self._shortcut_map: dict[str, QtWidgets.QShortcut] = {} self._action_map: dict[str, QtWidgets.QAction] = {} - def load(self, path: str) -> None: - """Override the current mapping with entries read from a JSON file.""" - with open(path) as f: - data = json.load(f) - self.mapping.update(data) + def load_from_dict(self, config) -> None: + """Override the current mapping with entries read from serverConfig file.""" + self.mapping.update(config) def save(self, path: str) -> None: - """Write the current mapping to a JSON file.""" + """Write the current mapping to the serverConfig file.""" + with open(path, "r") as f: + data = yaml.safe_load(f) or {} + + diffs = {k: v for k, v in self.mapping.items() if v != self.REGISTRY[k][0]} + if diffs: + data["shortcuts"] = diffs + elif "shortcuts" in data: + del data["shortcuts"] with open(path, "w") as f: - json.dump(self.mapping, f, indent=2) + yaml.dump(data, f, indent=2) def apply_to_action( self, action_id: str, qaction: Optional[QtWidgets.QAction] @@ -105,7 +111,7 @@ class ShortcutEditorWidget(QtWidgets.QWidget): Intended to be embedded as a tab in the server window. Changes made in the table are applied live to the manager (and therefore all registered shortcuts) - when Save is clicked. Use 'Save to file' / 'Load from file' to persist across sessions. + when Save is clicked. Use 'Save to file' to persist across sessions. Each row has a small colored indicator dot in the rightmost column: - white : saved and unique @@ -121,6 +127,7 @@ class ShortcutEditorWidget(QtWidgets.QWidget): def __init__( self, manager: KeyboardShortcutManager, + configPath: str, parent: Optional[QtWidgets.QWidget] = None, ) -> None: super().__init__(parent) @@ -145,8 +152,6 @@ def __init__( self._indicators: list[QtWidgets.QLabel] = [] self._populateTable() - btnLoad = QtWidgets.QPushButton("Load from file") - btnLoad.clicked.connect(self._loadFromFile) btnSaveFile = QtWidgets.QPushButton("Save to file") btnSaveFile.clicked.connect(self._saveToFile) btnReset = QtWidgets.QPushButton("Reset to defaults") @@ -155,7 +160,6 @@ def __init__( btnSave.clicked.connect(self._save) btnRow = QtWidgets.QHBoxLayout() - btnRow.addWidget(btnLoad) btnRow.addWidget(btnSaveFile) btnRow.addStretch() btnRow.addWidget(btnReset) @@ -166,6 +170,8 @@ def __init__( layout.addLayout(btnRow) self.setLayout(layout) + self.configPath = configPath + def _populateTable(self) -> None: self._indicators.clear() self._table.clearContents() @@ -281,34 +287,15 @@ def _save(self) -> None: self._updateAllIndicators() logger.info("Shortcuts saved locally") - @QtCore.Slot() - def _loadFromFile(self) -> None: - path, _ = QtWidgets.QFileDialog.getOpenFileName( - self, "Load Shortcuts", ".", "JSON Files (*.json);;All Files (*)" - ) - if path: - try: - self.manager.load(path) - self._populateTable() - logger.info(f"Loaded shortcuts from {path}") - except Exception as e: - logger.warning(f"Failed to load shortcuts from {path}: {e}") - @QtCore.Slot() def _saveToFile(self) -> None: self._save() - path, _ = QtWidgets.QFileDialog.getSaveFileName( - self, - "Save Shortcuts", - "shortcuts.json", - "JSON Files (*.json);;All Files (*)", - ) - if path: + if self.configPath: try: - self.manager.save(path) - logger.info(f"Saved shortcuts to {path}") + self.manager.save(self.configPath) + logger.info(f"Saved shortcuts to {self.configPath}") except Exception as e: - logger.warning(f"Failed to save shortcuts to {path}: {e}") + logger.warning(f"Failed to save shortcuts to {self.configPath}: {e}") @QtCore.Slot() def _resetDefaults(self) -> None: diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 4bf2a42..3bd56ce 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -609,6 +609,9 @@ def __init__( else: self._guiConfig = guiConfig + shortcutConfig = serverKwargs.pop("shortcutConfig", {}) + configPath = serverKwargs.pop("configPath", None) + self.stationServer: Optional[StationServer] = None self.stationServerThread: Optional[QtCore.QThread] = None @@ -661,7 +664,10 @@ def __init__( self.tabs.addUnclosableTab(self.serverStatus, "Server") self.shortcutManager = KeyboardShortcutManager() - self.shortcutEditor = ShortcutEditorWidget(self.shortcutManager) + if shortcutConfig: + self.shortcutManager.load_from_dict(shortcutConfig) + + self.shortcutEditor = ShortcutEditorWidget(self.shortcutManager, configPath) self.tabs.addUnclosableTab(self.shortcutEditor, "Shortcuts") # Toolbar. From d0ffee545759f904787340b7a14dca6f5761bdb8 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Tue, 19 May 2026 21:20:09 -0400 Subject: [PATCH 06/11] can now click right to jump focus to parameters value. addressed feedback --- src/instrumentserver/gui/base_instrument.py | 26 ++++++--------------- src/instrumentserver/gui/instruments.py | 25 ++++++++++++++++++++ src/instrumentserver/gui/shortcuts.py | 10 +++++--- src/instrumentserver/server/application.py | 6 ++--- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 7b38b42..aab971a 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -107,7 +107,6 @@ from typing import Any, Dict, List, Optional, cast from instrumentserver import QtCore, QtGui, QtWidgets - from instrumentserver.gui.shortcuts import KeyboardShortcutManager @@ -225,7 +224,7 @@ def __init__( self.loadingItems = False @staticmethod - def _matches_any_pattern(name: str, patterns: Optional[List[str]]) -> bool: + def _matches_any_pattern(name: str, patterns: List[str]) -> bool: """ Check if a name matches any glob pattern in the list. @@ -545,7 +544,7 @@ def __init__( super().__init__(parent=parent) # Indicates if a column is using delegates. - self.delegateColumns: List[int] = delegateColumns or [] + self.delegateColumns: List[int] = delegateColumns self.lastSelectedItem = None # Stores the last collapsed state before a change in filtering to restore it afterwards. # The keys are persistent indexes from the original model (not the proxy one) and the values a bool @@ -772,6 +771,7 @@ class InstrumentDisplayBase(QtWidgets.QWidget): :param proxyModelType: The type of proxy model that should be used. :param viewType: The type of view that should be used. :param callSignals: If False, the constructor will not call the method connectSignals + :param shortcutManager: Manager shared across the application so actions can be registered to shortcuts """ def __init__( @@ -783,7 +783,7 @@ def __init__( proxyModelType: type = InstrumentSortFilterProxyModel, viewType: type = InstrumentTreeViewBase, callSignals: bool = True, - shortcutManager: Optional[KeyboardShortcutManager] = None, + shortcutManager: type = KeyboardShortcutManager, parent: Optional[QtWidgets.QWidget] = None, **modelKwargs: Any, ) -> None: @@ -799,11 +799,7 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = ( - shortcutManager - if shortcutManager is not None - else KeyboardShortcutManager() - ) + self.shortcutManager = shortcutManager self.layout_ = QtWidgets.QVBoxLayout() @@ -882,14 +878,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) starAction.setCheckable(True) - starAction.triggered.connect(lambda x: self.promoteStar()) + starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) self.shortcutManager.apply_to_action("toggle_star", starAction) trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) trashAction.setCheckable(True) - trashAction.triggered.connect(lambda x: self.hideTrash()) + trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) self.shortcutManager.apply_to_action("toggle_trash", trashAction) # Debugging tools keep commented for commits. @@ -903,14 +899,6 @@ def makeToolbar(self) -> QtWidgets.QToolBar: return toolbar - @QtCore.Slot() - def hideTrash(self) -> None: - self.proxyModel.onToggleTrash() - - @QtCore.Slot() - def promoteStar(self) -> None: - self.proxyModel.onToggleStar() - @QtCore.Slot() def refreshAll(self) -> None: self.model.refreshAll() diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 6f2918c..6dbc151 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -466,6 +466,7 @@ def __init__( modelKwargs["sub_port"] = kwargs.pop("sub_port") shortcutManager = kwargs.pop("shortcutManager", None) + print(shortcutManager) super().__init__( instrument=instrument, @@ -486,6 +487,11 @@ def connectSignals(self) -> None: self.shortcutManager.register( "toggle_python", self._togglePythonCurrentItem, self ) + self.shortcutManager.register("edit_value", self._focusToParameterValue, self) + print( + "edit_value registered", + self.shortcutManager._shortcut_map.get("edit_value"), + ) @QtCore.Slot() def _refreshCurrentItem(self) -> None: @@ -515,6 +521,25 @@ def _togglePythonCurrentItem(self) -> None: if widget is not None and isinstance(widget.paramWidget, AnyInput): widget.paramWidget.doEval.toggle() + @QtCore.Slot() + def _focusToParameterValue(self) -> None: + logger.debug("test") + proxy_index = self.view.currentIndex() + if not proxy_index.isValid(): + return + source_index = self.proxyModel.mapToSource(proxy_index) + if source_index.column() != 0: + source_index = source_index.sibling(source_index.row(), 0) + item = self.model.itemFromIndex(source_index) + if isinstance(item, ItemBase): + widget = self.view.delegate.parameters.get(item.name) + if widget and hasattr(widget, "paramWidget"): + pw = widget.paramWidget + if isinstance(pw, AnyInput): + pw.input.setFocus() + else: + pw.setFocus() + # ----------------- Parameters Display Classes - Ending -------------------------------- diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 8d33eb5..163011c 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -1,9 +1,10 @@ -import yaml import logging import os from collections import defaultdict from typing import Callable, Optional +import yaml + from instrumentserver import QtCore, QtGui, QtWidgets, getInstrumentserverPath _ICON_DIR = getInstrumentserverPath("resource", "icons") @@ -44,6 +45,7 @@ class KeyboardShortcutManager: "save_items": ("Ctrl+S", "Save parameters to JSON file"), "fit_column": ("Ctrl+Shift+D", "Fits column width"), "sort_column": ("Ctrl+D", "Toggle sorting of selected column"), + "edit_value": ("Right", "Jump cursor to value field for selected parameter"), } def __init__(self) -> None: @@ -111,7 +113,7 @@ class ShortcutEditorWidget(QtWidgets.QWidget): Intended to be embedded as a tab in the server window. Changes made in the table are applied live to the manager (and therefore all registered shortcuts) - when Save is clicked. Use 'Save to file' to persist across sessions. + when Save is clicked. Use 'Save to file' to persist across sessions. Each row has a small colored indicator dot in the rightmost column: - white : saved and unique @@ -182,7 +184,9 @@ def _populateTable(self) -> None: id_item = QtWidgets.QTableWidgetItem(action_id) id_item.setFlags( - QtCore.Qt.ItemFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) + QtCore.Qt.ItemFlags( + id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable + ) ) desc_item = QtWidgets.QTableWidgetItem(description) desc_item.setFlags( diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index 3bd56ce..d574d52 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -906,11 +906,9 @@ def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: if "kwargs" in self._guiConfig[name]["gui"]: kwargs = self._guiConfig[name]["gui"]["kwargs"] - station_server = self.stationServer - if station_server is None: - raise RuntimeError("addInstrumentToGui called before server started") - kwargs["sub_port"] = kwargs.get("sub_port", station_server.port + 1) + kwargs["sub_port"] = kwargs.get("sub_port", self.stationServer.port + 1) # type: ignore[union-attr] kwargs["shortcutManager"] = self.shortcutManager + insWidget = widgetClass(ins, parent=self, **kwargs) index = self.tabs.addTab(insWidget, ins.name) self.instrumentTabsOpen[ins.name] = insWidget From e87e19ceea60674e76125f624b8b0151b54859c7 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 21:57:02 -0400 Subject: [PATCH 07/11] getting tests to pass part 1 --- src/instrumentserver/gui/base_instrument.py | 130 ++++++++++---------- src/instrumentserver/gui/instruments.py | 45 +++---- src/instrumentserver/gui/shortcuts.py | 17 +-- src/instrumentserver/server/application.py | 82 ++++++------ 4 files changed, 131 insertions(+), 143 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index aab971a..9dad22c 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -150,8 +150,8 @@ class DelegateBase(QtWidgets.QStyledItemDelegate): def getItem(cls, QModelIndex: QtCore.QModelIndex) -> QtGui.QStandardItem: proxyModel = QModelIndex.model() - model = proxyModel.sourceModel() - item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) + model = proxyModel.sourceModel() # type: ignore[union-attr] + item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] if item.column != 0: parent = item.parent() row = item.row() @@ -261,13 +261,13 @@ def loadItems(self, module: Any = None, prefix: Optional[str] = None) -> None: # constructor if prefix is not None: objectName = ".".join([prefix, objectName]) - if not self._matches_any_pattern(objectName, self.itemsHide): + if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] item = self.addItem( fullName=objectName, star=False, trash=False, element=obj ) - if self._matches_any_pattern(objectName, self.itemsTrash): + if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(item) - if self._matches_any_pattern(objectName, self.itemsStar): + if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(item) for submodName, submod in module.submodules.items(): @@ -334,20 +334,20 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: - if not self._matches_any_pattern(smName, self.itemsHide): - self.insertItemTo(parent, subModItem) - if self._matches_any_pattern(smName, self.itemsTrash): + if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(subModItem) - if self._matches_any_pattern(smName, self.itemsStar): + if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(subModItem) else: - self.insertItemTo(parent, subModItem) - parent = subModItem + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + parent = subModItem # type: ignore[assignment] else: - parent = items[0] + parent = items[0] # type: ignore[assignment] newItem = self.itemClass(name=fullName, **kwargs) - self.insertItemTo(parent, newItem) + self.insertItemTo(parent, newItem) # type: ignore[arg-type] return newItem @@ -467,7 +467,7 @@ def _isParentTrash(self, parent: Optional["ItemBase"]) -> bool: if parent.trash: return True - return self._isParentTrash(parent.parent()) + return self._isParentTrash(parent.parent()) # type: ignore[arg-type] def filterAcceptsRow( self, source_row: int, source_parent: QtCore.QModelIndex @@ -490,7 +490,7 @@ def filterAcceptsRow( # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr( + if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] item, "trash", False ): # item could be None when it's trashed and hidden return False @@ -512,15 +512,15 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: rightItem = model.itemFromIndex(right) if hasattr(leftItem, "star") and hasattr(rightItem, "star"): if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: - if rightItem.star and not leftItem.star: + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return True - elif not rightItem.star and leftItem.star: + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return False elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: - if rightItem.star and not leftItem.star: + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return False - elif not rightItem.star and leftItem.star: + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return True return super().lessThan(left, right) @@ -544,7 +544,7 @@ def __init__( super().__init__(parent=parent) # Indicates if a column is using delegates. - self.delegateColumns: List[int] = delegateColumns + self.delegateColumns = delegateColumns self.lastSelectedItem = None # Stores the last collapsed state before a change in filtering to restore it afterwards. # The keys are persistent indexes from the original model (not the proxy one) and the values a bool @@ -565,8 +565,8 @@ def __init__( self.setSortingEnabled(False) # The tree should not have anything to do with filtering itself since that is left for the proxy model. - self.header().setSortIndicatorShown(True) - self.header().setSectionsClickable(True) + self.header().setSortIndicatorShown(True) # type: ignore[union-attr] + self.header().setSectionsClickable(True) # type: ignore[union-attr] self.setAlternatingRowColors(True) @@ -603,8 +603,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(index) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if item.hasChildren(): - self.fillCollapsedDict(item) + if item.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(item) # type: ignore[arg-type] else: for i in range(parentItem.rowCount()): child = parentItem.child(i, 0) @@ -615,8 +615,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(childIndex) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if child.hasChildren(): - self.fillCollapsedDict(child) + if child.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(child) # type: ignore[arg-type] @QtCore.Slot() def restoreCollapsedDict(self) -> None: @@ -625,23 +625,23 @@ def restoreCollapsedDict(self) -> None: the persistent editors and triggers a resizing of delegates. """ for persistentIndex, state in self.collapsedState.items(): - modelIndex = self.modelActual.index( + modelIndex = self.modelActual.index( # type: ignore[union-attr] persistentIndex.row(), persistentIndex.column(), persistentIndex.parent(), ) - item = self.modelActual.itemFromIndex(modelIndex) - proxyIndex = self.model().mapFromSource(modelIndex) + item = self.modelActual.itemFromIndex(modelIndex) # type: ignore[union-attr] + proxyIndex = self.model().mapFromSource(modelIndex) # type: ignore[union-attr] self.setExpanded(proxyIndex, state) if item.showDelegate: delegateIndexes = [ - self.modelActual.index( + self.modelActual.index( # type: ignore[union-attr] persistentIndex.row(), x, persistentIndex.parent() ) - for x in self.delegateColumns + for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) for index in delegateIndexes + self.model().mapFromSource(index) for index in delegateIndexes # type: ignore[union-attr] ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) @@ -656,14 +656,14 @@ def setAllDelegatesPersistent( :param parentIndex: If None, start the process. if it's an item, it will go through the children """ if parentIndex is None: - for i in range(self.model().rowCount()): - for column in self.delegateColumns: - index = self.model().index(i, column) - index0 = self.model().index( + for i in range(self.model().rowCount()): # type: ignore[union-attr] + for column in self.delegateColumns: # type: ignore[union-attr] + index = self.model().index(i, column) # type: ignore[union-attr] + index0 = self.model().index( # type: ignore[union-attr] i, 0 ) # Only items at column 0 hold children and model info - item0 = self.modelActual.itemFromIndex( - self.model().mapToSource(index0) + item0 = self.modelActual.itemFromIndex( # type: ignore[union-attr] + self.model().mapToSource(index0) # type: ignore[union-attr] ) if item0.showDelegate: self.openPersistentEditor(index) @@ -671,18 +671,18 @@ def setAllDelegatesPersistent( self.setAllDelegatesPersistent(index0) else: - parentItem = self.modelActual.itemFromIndex( - self.model().mapToSource(parentIndex) + parentItem = self.modelActual.itemFromIndex( # type: ignore[union-attr] + self.model().mapToSource(parentIndex) # type: ignore[union-attr] ) for i in range(parentItem.rowCount()): - for column in self.delegateColumns: + for column in self.delegateColumns: # type: ignore[union-attr] item = parentItem.child(i, column) item0 = parentItem.child(i, 0) - index = self.model().mapFromSource( - self.modelActual.indexFromItem(item) + index = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item) # type: ignore[union-attr] ) - index0 = self.model().mapFromSource( - self.modelActual.indexFromItem(item0) + index0 = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(item0) # type: ignore[union-attr] ) if item0.showDelegate: self.openPersistentEditor(index) @@ -700,13 +700,13 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: if item.showDelegate: row = item.row() parent = item.parent() - for column in self.delegateColumns: + for column in self.delegateColumns: # type: ignore[union-attr] if parent is None: - sibling = self.modelActual.item(row, column) + sibling = self.modelActual.item(row, column) # type: ignore[union-attr] else: sibling = parent.child(row, column) - index = self.model().mapFromSource( - self.modelActual.indexFromItem(sibling) + index = self.model().mapFromSource( # type: ignore[union-attr] + self.modelActual.indexFromItem(sibling) # type: ignore[union-attr] ) self.openPersistentEditor(index) self.scheduleDelayedItemsLayout() @@ -715,9 +715,9 @@ def onCheckDelegate(self, item: Optional["ItemBase"]) -> None: def onContextMenuRequested(self, pos: QtCore.QPoint) -> None: # We get the item from the real model, not the proxy model - originalModel = self.model().sourceModel() + originalModel = self.model().sourceModel() # type: ignore[union-attr] proxyIndex = self.indexAt(pos) - index = self.model().mapToSource(proxyIndex) + index = self.model().mapToSource(proxyIndex) # type: ignore[union-attr] # catch the case if the user rightcliks on any other column if index.column() != 0: @@ -783,7 +783,7 @@ def __init__( proxyModelType: type = InstrumentSortFilterProxyModel, viewType: type = InstrumentTreeViewBase, callSignals: bool = True, - shortcutManager: type = KeyboardShortcutManager, + shortcutManager: Optional[KeyboardShortcutManager] = None, parent: Optional[QtWidgets.QWidget] = None, **modelKwargs: Any, ) -> None: @@ -799,7 +799,7 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = shortcutManager + self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() self.layout_ = QtWidgets.QVBoxLayout() @@ -853,7 +853,7 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/refresh.svg"), "refresh all items from the instrument", ) - refreshAction.triggered.connect(lambda x: self.refreshAll()) + refreshAction.triggered.connect(lambda x: self.refreshAll()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("refresh_all", refreshAction) toolbar.addSeparator() @@ -862,14 +862,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/expand.svg"), "expand tree", ) - expandAction.triggered.connect(lambda x: self.view.expandAll()) + expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("expand_all", expandAction) collapseAction = toolbar.addAction( QtGui.QIcon(":/icons/collapse.svg"), "collapse tree", ) - collapseAction.triggered.connect(lambda x: self.view.collapseAll()) + collapseAction.triggered.connect(lambda x: self.view.collapseAll()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("collapse_all", collapseAction) toolbar.addSeparator() @@ -877,15 +877,15 @@ def makeToolbar(self) -> QtWidgets.QToolBar: starAction = toolbar.addAction( QtGui.QIcon(":/icons/star.svg"), "Move Starred items to the top" ) - starAction.setCheckable(True) - starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) + starAction.setCheckable(True) # type: ignore[union-attr] + starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("toggle_star", starAction) trashAction = toolbar.addAction( QtGui.QIcon(":/icons/trash-crossed.svg"), "Hide trashed items" ) - trashAction.setCheckable(True) - trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) + trashAction.setCheckable(True) # type: ignore[union-attr] + trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("toggle_trash", trashAction) # Debugging tools keep commented for commits. @@ -957,13 +957,13 @@ def debuggingMethod(self) -> None: def fillChildren(parent: QtGui.QStandardItem) -> None: for i in range(parent.rowCount()): item = parent.child(i, 0) - items[item.name] = { + items[item.name] = { # type: ignore[union-attr] "item": item, - "star": item.star, - "trash": item.trash, + "star": item.star, # type: ignore[union-attr] + "trash": item.trash, # type: ignore[union-attr] } - if item.hasChildren(): - fillChildren(item) + if item.hasChildren(): # type: ignore[union-attr] + fillChildren(item) # type: ignore[arg-type] for i in range(self.model.rowCount()): item = self.model.item(i, 0) diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 6dbc151..7c5f545 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -158,7 +158,7 @@ def clear(self) -> None: self.unitEdit.setText("") if self.typeInput: self.typeSelect.setCurrentText( - parameterTypes[ParameterTypes.numeric]["name"] + parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] ) self.valsArgsEdit.setText("") @@ -287,7 +287,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, @@ -297,10 +297,10 @@ def createEditor( This is the function that is supposed to create the widget. It should return it. """ item = self.getItem(index) - element = item.element + element = item.element # type: ignore[attr-defined] ret = ParameterWidget(element, widget) - self.parameters[item.name] = ret + self.parameters[item.name] = ret # type: ignore[attr-defined] # Try to fetch and display current value immediately # ---- Chao: removed because the constructor of ParameterWidget object already calls parameter get ---- # if element.gettable: @@ -333,7 +333,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.subClient = SubClient([self.instrument.name], **subClientArgs) self.subClient.moveToThread(self.cliThread) - self.cliThread.started.connect(self.subClient.connect) + self.cliThread.started.connect(self.subClient.connect) # type: ignore[arg-type] self.subClient.update.connect(self.updateParameter) self.subClient.finished.connect(self.cliThread.quit) @@ -398,8 +398,8 @@ def insertItemTo( if item is not None: # A parameter might not have a unit unit = "" - if item.element is not None: - unit = item.element.unit + if item.element is not None: # type: ignore[attr-defined] + unit = item.element.unit # type: ignore[attr-defined] unitItem = QtGui.QStandardItem(unit) extraItem = QtGui.QStandardItem() @@ -488,10 +488,6 @@ def connectSignals(self) -> None: "toggle_python", self._togglePythonCurrentItem, self ) self.shortcutManager.register("edit_value", self._focusToParameterValue, self) - print( - "edit_value registered", - self.shortcutManager._shortcut_map.get("edit_value"), - ) @QtCore.Slot() def _refreshCurrentItem(self) -> None: @@ -523,7 +519,6 @@ def _togglePythonCurrentItem(self) -> None: @QtCore.Slot() def _focusToParameterValue(self) -> None: - logger.debug("test") proxy_index = self.view.currentIndex() if not proxy_index.isValid(): return @@ -551,18 +546,18 @@ class ParameterDeleteDelegate(ParameterDelegate): #: Emits the name of the parameter to be deleted when the user presses the delete button. removeParameter = QtCore.Signal(str) - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element - rw = self.makeRemoveWidget(item.name, widget) + element = item.element # type: ignore[attr-defined] + rw = self.makeRemoveWidget(item.name, widget) # type: ignore[attr-defined] ret = ParameterWidget(parameter=element, parent=widget, additionalWidgets=[rw]) - self.parameters[item.name] = ret + self.parameters[item.name] = ret # type: ignore[attr-defined] return ret @@ -610,7 +605,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.setEditable(False) - self.params = self.parent().instrument + self.params = self.parent().instrument # type: ignore[union-attr] self.refreshing = False loadingProfile = None @@ -702,14 +697,14 @@ def makeToolbar(self) -> QtWidgets.QToolBar: QtGui.QIcon(":/icons/load.svg"), "Load parameters from file", ) - loadParamAction.triggered.connect(lambda x: self.loadFromFile()) + loadParamAction.triggered.connect(lambda x: self.loadFromFile()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("load_items", loadParamAction) saveParamAction = toolbar.addAction( QtGui.QIcon(":/icons/save.svg"), "Save parameters to file", ) - saveParamAction.triggered.connect(lambda x: self.saveToFile()) + saveParamAction.triggered.connect(lambda x: self.saveToFile()) # type: ignore[union-attr] self.shortcutManager.apply_to_action("save_items", saveParamAction) return toolbar @@ -795,22 +790,22 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: self.methods: Dict[str, "MethodDisplay"] = {} - def createEditor( + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ) -> QtWidgets.QWidget: item = self.getItem(index) - element = item.element - ret = MethodDisplay(element, item.name, parent=widget) + element = item.element # type: ignore[attr-defined] + ret = MethodDisplay(element, item.name, parent=widget) # type: ignore[attr-defined] parent = self.parent() assert hasattr(parent, "clearAlertsAction") # connecting the widget with the clear alert signal - parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) + parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) # type: ignore[union-attr] - self.methods[item.name] = ret + self.methods[item.name] = ret # type: ignore[attr-defined] return ret @@ -910,7 +905,7 @@ def __init__( self.parametersList.view.resizeColumnToContents(1) self.methodsList.view.resizeColumnToContents(0) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # type: ignore[override] """Stop the parameter subscriber thread before destruction.""" model = getattr(self.parametersList, "model", None) if model is not None and hasattr(model, "stopListener"): diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 163011c..7af1a6e 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -53,7 +53,7 @@ def __init__(self) -> None: self._shortcut_map: dict[str, QtWidgets.QShortcut] = {} self._action_map: dict[str, QtWidgets.QAction] = {} - def load_from_dict(self, config) -> None: + def load_from_dict(self, config: dict[str, str]) -> None: """Override the current mapping with entries read from serverConfig file.""" self.mapping.update(config) @@ -138,6 +138,7 @@ def __init__( self._table = QtWidgets.QTableWidget(len(manager.REGISTRY), 4, self) self._table.setHorizontalHeaderLabels(["Action", "Description", "Shortcut", ""]) header = self._table.horizontalHeader() + assert header is not None header.setSectionResizeMode( 0, QtWidgets.QHeaderView.ResizeMode.ResizeToContents ) @@ -183,17 +184,11 @@ def _populateTable(self) -> None: current = self.manager.mapping.get(action_id, "") id_item = QtWidgets.QTableWidgetItem(action_id) - id_item.setFlags( - QtCore.Qt.ItemFlags( - id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable - ) - ) + id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + desc_item = QtWidgets.QTableWidgetItem(description) - desc_item.setFlags( - QtCore.Qt.ItemFlags( - desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable - ) - ) + desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index d574d52..c47caa8 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -61,7 +61,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] ) self.deleteAction.triggered.connect(self.onDeleteAction) self.itemSelectionChanged.connect(self._processSelection) @@ -130,7 +130,7 @@ class ServerStatus(QtWidgets.QWidget): def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: super().__init__(parent) - self.layout = QtWidgets.QVBoxLayout(self) + self.layout = QtWidgets.QVBoxLayout(self) # type: ignore[assignment,method-assign] # At the top: a status label, and a button for emitting a test message self.addressLabel = QtWidgets.QLabel() @@ -144,13 +144,13 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None: ) ) - self.layout.addLayout(self.statusLayout) + self.layout.addLayout(self.statusLayout) # type: ignore[attr-defined] # next row: a window for displaying the incoming messages. - self.layout.addWidget(QtWidgets.QLabel("Messages:")) + self.layout.addWidget(QtWidgets.QLabel("Messages:")) # type: ignore[attr-defined] self.messages = QtWidgets.QTextEdit() self.messages.setReadOnly(True) - self.layout.addWidget(self.messages) + self.layout.addWidget(self.messages) # type: ignore[attr-defined] @QtCore.Slot(str) def setListeningAddress(self, addr: str) -> None: @@ -328,7 +328,7 @@ def __init__(self, guiConfig: Optional[dict] = None, *args: Any) -> None: self.contextMenu.addSeparator() self.contextMenu.addAction(self.deletePossibleInstrumentAction) self.customContextMenuRequested.connect( - lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) + lambda x: self.contextMenu.exec_(self.mapToGlobal(x)) # type: ignore[arg-type] ) self.basedInstrumentAction.triggered.connect(self.onBasedInstrumentAction) @@ -432,16 +432,16 @@ def onRemoveInstrumentFromTree(self) -> None: for item in items: if item.childCount() == 0: parent = item.parent() - if item.configName is not None and item.configName in self.config: - del self.config[item.configName] - parent.removeChild(item) - if parent.childCount() == 0: + if item.configName is not None and item.configName in self.config: # type: ignore[attr-defined] + del self.config[item.configName] # type: ignore[attr-defined] + parent.removeChild(item) # type: ignore[union-attr] + if parent.childCount() == 0: # type: ignore[union-attr] self.takeTopLevelItem((self.indexOfTopLevelItem(parent))) else: for i in range(item.childCount()): child = item.child(i) - if child.configName in self.config: - del self.config[child.configName] + if child.configName in self.config: # type: ignore[union-attr] + del self.config[child.configName] # type: ignore[union-attr] self.takeTopLevelItem(self.indexOfTopLevelItem(item)) @@ -672,31 +672,31 @@ def __init__( # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) + self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] # Parameter tools. - self.toolBar.addSeparator() - self.toolBar.addWidget(QtWidgets.QLabel("Params:")) + self.toolBar.addSeparator() # type: ignore[union-attr] + self.toolBar.addWidget(QtWidgets.QLabel("Params:")) # type: ignore[union-attr] self.loadParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/load.svg"), "Load from file", self ) self.loadParamsAction.triggered.connect(self.loadParamsFromFile) - self.toolBar.addAction(self.loadParamsAction) + self.toolBar.addAction(self.loadParamsAction) # type: ignore[union-attr] self.saveParamsAction = QtWidgets.QAction( QtGui.QIcon(":/icons/save.svg"), "Save to file", self ) self.saveParamsAction.triggered.connect(self.saveParamsToFile) - self.toolBar.addAction(self.saveParamsAction) + self.toolBar.addAction(self.saveParamsAction) # type: ignore[union-attr] self.serverStatus.testButton.clicked.connect( lambda x: self.client.ask("Ping server.") @@ -715,7 +715,7 @@ def __init__( def log(self, message: str, level: LogLevels = LogLevels.info) -> None: log(logger, message, level) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: + def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: for name, widget in list(self.instrumentTabsOpen.items()): try: widget.close() @@ -738,32 +738,30 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: self.client.disconnect() except Exception: pass - event.accept() + event.accept() # type: ignore[union-attr] def startServer(self) -> None: """Start the instrument server in a separate thread.""" - self.stationServer = StationServer(**self._serverKwargs) - self.stationServerThread = QtCore.QThread() - assert self.stationServer is not None - assert self.stationServerThread is not None - self.stationServer.moveToThread(self.stationServerThread) - self.stationServerThread.started.connect(self.stationServer.startServer) - self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) - self.stationServer.finished.connect(self.stationServerThread.quit) - self.stationServer.finished.connect(self.stationServer.deleteLater) + self.stationServer = StationServer(**self._serverKwargs) # type: ignore[assignment] + self.stationServerThread = QtCore.QThread() # type: ignore[assignment] + self.stationServer.moveToThread(self.stationServerThread) # type: ignore[attr-defined] + self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type,attr-defined] + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) # type: ignore[attr-defined] + self.stationServer.finished.connect(self.stationServerThread.quit) # type: ignore[attr-defined] + self.stationServer.finished.connect(self.stationServer.deleteLater) # type: ignore[attr-defined] # Connecting some additional things for messages. - self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) - self.stationServer.serverStarted.connect(self.client.start) - self.stationServer.serverStarted.connect(self.refreshStationComponents) - self.stationServer.finished.connect( + self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.client.start) # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.refreshStationComponents) # type: ignore[attr-defined] + self.stationServer.finished.connect( # type: ignore[attr-defined] lambda: self.log("Server thread finished.", LogLevels.info) ) - self.stationServer.messageReceived.connect(self._messageReceived) - self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) - self.stationServer.funcCalled.connect(self.onFuncCalled) + self.stationServer.messageReceived.connect(self._messageReceived) # type: ignore[attr-defined] + self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) # type: ignore[attr-defined] + self.stationServer.funcCalled.connect(self.onFuncCalled) # type: ignore[attr-defined] - self.stationServerThread.start() + self.stationServerThread.start() # type: ignore[attr-defined] def getServerIfRunning(self) -> Optional["StationServer"]: if ( @@ -878,7 +876,7 @@ def displayComponentInfo(self, name: Union[str, None]) -> None: bp = self._bluePrints[name] else: bp = None - self.stationObjInfo.setObject(bp) + self.stationObjInfo.setObject(bp) # type: ignore[arg-type] @QtCore.Slot(QtWidgets.QTreeWidgetItem, int) def addInstrumentTab(self, item: QtWidgets.QTreeWidgetItem, index: int) -> None: @@ -966,15 +964,15 @@ def __init__(self, host: str = "localhost", port: int = 5555) -> None: # Toolbar. self.toolBar = self.addToolBar("Tools") - self.toolBar.setIconSize(QtCore.QSize(16, 16)) + self.toolBar.setIconSize(QtCore.QSize(16, 16)) # type: ignore[union-attr] # Station tools. - self.toolBar.addWidget(QtWidgets.QLabel("Station:")) + self.toolBar.addWidget(QtWidgets.QLabel("Station:")) # type: ignore[union-attr] self.refreshStationAction = QtWidgets.QAction( QtGui.QIcon(":/icons/refresh.svg"), "Refresh", self ) self.refreshStationAction.triggered.connect(self.refreshStationComponents) - self.toolBar.addAction(self.refreshStationAction) + self.toolBar.addAction(self.refreshStationAction) # type: ignore[union-attr] self.refreshStationComponents() From 741af7af0d6755e6cfba069429857e24f35cf741 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 22:11:26 -0400 Subject: [PATCH 08/11] getting tests to pass part 2 --- src/instrumentserver/gui/base_instrument.py | 57 +++++++++++---------- src/instrumentserver/gui/instruments.py | 4 +- src/instrumentserver/gui/shortcuts.py | 4 +- src/instrumentserver/server/application.py | 32 ++++++------ 4 files changed, 51 insertions(+), 46 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 9dad22c..8f302ef 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -150,8 +150,8 @@ class DelegateBase(QtWidgets.QStyledItemDelegate): def getItem(cls, QModelIndex: QtCore.QModelIndex) -> QtGui.QStandardItem: proxyModel = QModelIndex.model() - model = proxyModel.sourceModel() # type: ignore[union-attr] - item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] + model = proxyModel.sourceModel() # type: ignore[union-attr] + item = model.itemFromIndex(proxyModel.mapToSource(QModelIndex)) # type: ignore[union-attr] if item.column != 0: parent = item.parent() row = item.row() @@ -261,13 +261,13 @@ def loadItems(self, module: Any = None, prefix: Optional[str] = None) -> None: # constructor if prefix is not None: objectName = ".".join([prefix, objectName]) - if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] + if not self._matches_any_pattern(objectName, self.itemsHide): # type: ignore[arg-type] item = self.addItem( fullName=objectName, star=False, trash=False, element=obj ) - if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(item) - if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(objectName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(item) for submodName, submod in module.submodules.items(): @@ -334,20 +334,20 @@ def addItem(self, fullName: str, **kwargs: Any) -> "ItemBase": ) # submodules get directly added here and not in the load function, so need to have it here too. if self.loadingItems: - if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] + if not self._matches_any_pattern(smName, self.itemsHide): # type: ignore[arg-type] + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsTrash): # type: ignore[arg-type] self.onItemTrashToggle(subModItem) - if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] + if self._matches_any_pattern(smName, self.itemsStar): # type: ignore[arg-type] self.onItemStarToggle(subModItem) else: - self.insertItemTo(parent, subModItem) # type: ignore[arg-type] - parent = subModItem # type: ignore[assignment] + self.insertItemTo(parent, subModItem) # type: ignore[arg-type] + parent = subModItem # type: ignore[assignment] else: - parent = items[0] # type: ignore[assignment] + parent = items[0] # type: ignore[assignment] newItem = self.itemClass(name=fullName, **kwargs) - self.insertItemTo(parent, newItem) # type: ignore[arg-type] + self.insertItemTo(parent, newItem) # type: ignore[arg-type] return newItem @@ -467,7 +467,7 @@ def _isParentTrash(self, parent: Optional["ItemBase"]) -> bool: if parent.trash: return True - return self._isParentTrash(parent.parent()) # type: ignore[arg-type] + return self._isParentTrash(parent.parent()) # type: ignore[arg-type] def filterAcceptsRow( self, source_row: int, source_parent: QtCore.QModelIndex @@ -490,7 +490,7 @@ def filterAcceptsRow( # Assertion is there to satisfy mypy. item can be None, that is why we check before making the assertion if item is not None: assert isinstance(item, ItemBase) - if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] + if self._isParentTrash(parent) or getattr( # type: ignore[arg-type] item, "trash", False ): # item could be None when it's trashed and hidden return False @@ -512,15 +512,15 @@ def lessThan(self, left: QtCore.QModelIndex, right: QtCore.QModelIndex) -> bool: rightItem = model.itemFromIndex(right) if hasattr(leftItem, "star") and hasattr(rightItem, "star"): if self.sortOrder() == QtCore.Qt.SortOrder.DescendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return True - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return False elif self.sortOrder() == QtCore.Qt.SortOrder.AscendingOrder: - if rightItem.star and not leftItem.star: # type: ignore[union-attr] + if rightItem.star and not leftItem.star: # type: ignore[union-attr] return False - elif not rightItem.star and leftItem.star: # type: ignore[union-attr] + elif not rightItem.star and leftItem.star: # type: ignore[union-attr] return True return super().lessThan(left, right) @@ -565,8 +565,8 @@ def __init__( self.setSortingEnabled(False) # The tree should not have anything to do with filtering itself since that is left for the proxy model. - self.header().setSortIndicatorShown(True) # type: ignore[union-attr] - self.header().setSectionsClickable(True) # type: ignore[union-attr] + self.header().setSortIndicatorShown(True) # type: ignore[union-attr] + self.header().setSectionsClickable(True) # type: ignore[union-attr] self.setAlternatingRowColors(True) @@ -603,8 +603,8 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(index) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if item.hasChildren(): # type: ignore[union-attr] - self.fillCollapsedDict(item) # type: ignore[arg-type] + if item.hasChildren(): # type: ignore[union-attr] + self.fillCollapsedDict(item) # type: ignore[arg-type] else: for i in range(parentItem.rowCount()): child = parentItem.child(i, 0) @@ -615,7 +615,7 @@ def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None: proxyIndex = m.mapFromSource(childIndex) if proxyIndex.isValid(): self.collapsedState[persistentIndex] = self.isExpanded(proxyIndex) - if child.hasChildren(): # type: ignore[union-attr] + if child.hasChildren(): # type: ignore[union-attr] self.fillCollapsedDict(child) # type: ignore[arg-type] @QtCore.Slot() @@ -641,7 +641,8 @@ def restoreCollapsedDict(self) -> None: for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) for index in delegateIndexes # type: ignore[union-attr] + self.model().mapFromSource(index) + for index in delegateIndexes # type: ignore[union-attr] ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) @@ -799,7 +800,11 @@ def __init__( self.proxyModel = proxyModelType(self.model) self.view = viewType(self.proxyModel) - self.shortcutManager = shortcutManager if shortcutManager is not None else KeyboardShortcutManager() + self.shortcutManager = ( + shortcutManager + if shortcutManager is not None + else KeyboardShortcutManager() + ) self.layout_ = QtWidgets.QVBoxLayout() diff --git a/src/instrumentserver/gui/instruments.py b/src/instrumentserver/gui/instruments.py index 7c5f545..ab387c4 100644 --- a/src/instrumentserver/gui/instruments.py +++ b/src/instrumentserver/gui/instruments.py @@ -158,7 +158,7 @@ def clear(self) -> None: self.unitEdit.setText("") if self.typeInput: self.typeSelect.setCurrentText( - parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] + parameterTypes[ParameterTypes.numeric]["name"] # type: ignore[arg-type] ) self.valsArgsEdit.setText("") @@ -287,7 +287,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None: # used to keep a reference to the widget. self.parameters: Dict[str, QtWidgets.QWidget] = {} - def createEditor( # type: ignore[override] + def createEditor( # type: ignore[override] self, widget: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 7af1a6e..4b7a294 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -184,10 +184,10 @@ def _populateTable(self) -> None: current = self.manager.mapping.get(action_id, "") id_item = QtWidgets.QTableWidgetItem(action_id) - id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + id_item.setFlags(id_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] desc_item = QtWidgets.QTableWidgetItem(description) - desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] + desc_item.setFlags(desc_item.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable) # type: ignore[arg-type] self._table.setItem(row, 0, id_item) self._table.setItem(row, 1, desc_item) diff --git a/src/instrumentserver/server/application.py b/src/instrumentserver/server/application.py index c47caa8..608082b 100644 --- a/src/instrumentserver/server/application.py +++ b/src/instrumentserver/server/application.py @@ -738,30 +738,30 @@ def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: self.client.disconnect() except Exception: pass - event.accept() # type: ignore[union-attr] + event.accept() # type: ignore[union-attr] def startServer(self) -> None: """Start the instrument server in a separate thread.""" - self.stationServer = StationServer(**self._serverKwargs) # type: ignore[assignment] - self.stationServerThread = QtCore.QThread() # type: ignore[assignment] - self.stationServer.moveToThread(self.stationServerThread) # type: ignore[attr-defined] - self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type,attr-defined] - self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServerThread.quit) # type: ignore[attr-defined] - self.stationServer.finished.connect(self.stationServer.deleteLater) # type: ignore[attr-defined] + self.stationServer = StationServer(**self._serverKwargs) + self.stationServerThread = QtCore.QThread() + self.stationServer.moveToThread(self.stationServerThread) + self.stationServerThread.started.connect(self.stationServer.startServer) # type: ignore[arg-type] + self.stationServer.finished.connect(lambda: self.log("ZMQ server closed.")) + self.stationServer.finished.connect(self.stationServerThread.quit) + self.stationServer.finished.connect(self.stationServer.deleteLater) # Connecting some additional things for messages. - self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.client.start) # type: ignore[attr-defined] - self.stationServer.serverStarted.connect(self.refreshStationComponents) # type: ignore[attr-defined] - self.stationServer.finished.connect( # type: ignore[attr-defined] + self.stationServer.serverStarted.connect(self.serverStatus.setListeningAddress) + self.stationServer.serverStarted.connect(self.client.start) + self.stationServer.serverStarted.connect(self.refreshStationComponents) + self.stationServer.finished.connect( lambda: self.log("Server thread finished.", LogLevels.info) ) - self.stationServer.messageReceived.connect(self._messageReceived) # type: ignore[attr-defined] - self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) # type: ignore[attr-defined] - self.stationServer.funcCalled.connect(self.onFuncCalled) # type: ignore[attr-defined] + self.stationServer.messageReceived.connect(self._messageReceived) + self.stationServer.instrumentCreated.connect(self.addInstrumentToGui) + self.stationServer.funcCalled.connect(self.onFuncCalled) - self.stationServerThread.start() # type: ignore[attr-defined] + self.stationServerThread.start() def getServerIfRunning(self) -> Optional["StationServer"]: if ( From aaa8386aee0669843d7bac7616c3436ae5e97793 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 22:18:38 -0400 Subject: [PATCH 09/11] getting tests to pass part 3 --- src/instrumentserver/apps.py | 16 +++++++++++----- src/instrumentserver/config.py | 18 ++++++++++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/instrumentserver/apps.py b/src/instrumentserver/apps.py index e624e42..b3680e5 100644 --- a/src/instrumentserver/apps.py +++ b/src/instrumentserver/apps.py @@ -71,9 +71,15 @@ def serverScript() -> None: ) = None, None, None, None, None, None, None, None if configPath != "": # Separates the corresponding settings into the 5 necessary parts - stationConfig, serverConfig, guiConfig, shortcutConfig, tempFile, pollingRates, ipAddresses = ( - loadConfig(configPath) - ) + ( + stationConfig, + serverConfig, + guiConfig, + shortcutConfig, + tempFile, + pollingRates, + ipAddresses, + ) = loadConfig(configPath) if pollingRates is not None and pollingRates != {}: pollingThread = QtCore.QThread() pollWorker = PollingWorker(pollingRates=pollingRates) @@ -93,7 +99,7 @@ def serverScript() -> None: shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, - configPath=configPath + configPath=configPath, ) else: serverWithGui( @@ -106,7 +112,7 @@ def serverScript() -> None: shortcutConfig=shortcutConfig, pollingThread=pollingThread, ipAddresses=ipAddresses, - configPath=configPath + configPath=configPath, ) # Close and delete the temporary files diff --git a/src/instrumentserver/config.py b/src/instrumentserver/config.py index 2f8a2a2..f30d874 100644 --- a/src/instrumentserver/config.py +++ b/src/instrumentserver/config.py @@ -18,7 +18,9 @@ GUIFIELD = {"type": "instrumentserver.gui.instruments.GenericInstrument", "kwargs": {}} -def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes], dict, dict]: +def loadConfig( + configPath: str | Path, +) -> tuple[str, dict, dict, dict, IO[bytes], dict, dict]: """ Loads the config for the instrumentserver. From 1 config file it splits the respective fields into 3 different objects: a serverConfig (the configurations for the server), a stationConfig(the qcodes station config file clean @@ -36,7 +38,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes] serverConfig: dict = {} # Config for the server guiConfig = {} # Individual gui config of each instrument fullConfig = {} # serverConfig + guiConfig + any unfilled fields. Used for creating instruments from the gui - shortcutConfig = {} # Preferences for keyboard shortcuts + shortcutConfig = {} # Preferences for keyboard shortcuts pollingRates = {} # Polling rates for each parameter ipAddresses = {} # Dictionary of IP Addresses to send broadcasts to: # externalBroadcast: where to externally send parameter change broadcasts to, formatted like "tcp://address:port" @@ -150,7 +152,7 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes] # Update fullConfig with merged GUI config fullConfig[instrumentName]["gui"] = guiConfig[instrumentName] - + # Gets all shortcuts different to REGISTRY defaults from the config file if "shortcuts" in rawConfig: shortcutConfig = rawConfig["shortcuts"] @@ -176,4 +178,12 @@ def loadConfig(configPath: str | Path) -> tuple[str, dict, dict, dict, IO[bytes] tempFilePath = tempFile.name # You need to return the tempFile itself so that the garbage collector doesn't touch it - return tempFilePath, serverConfig, fullConfig, shortcutConfig, tempFile, pollingRates, ipAddresses + return ( + tempFilePath, + serverConfig, + fullConfig, + shortcutConfig, + tempFile, + pollingRates, + ipAddresses, + ) From b087eece0906de63bc5f00b45499498c5497c8e5 Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Wed, 20 May 2026 22:23:02 -0400 Subject: [PATCH 10/11] forgot to commit base instrument. part 4 --- src/instrumentserver/gui/base_instrument.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instrumentserver/gui/base_instrument.py b/src/instrumentserver/gui/base_instrument.py index 8f302ef..4057ef1 100644 --- a/src/instrumentserver/gui/base_instrument.py +++ b/src/instrumentserver/gui/base_instrument.py @@ -641,8 +641,8 @@ def restoreCollapsedDict(self) -> None: for x in self.delegateColumns # type: ignore[union-attr] ] proxyDelegateIndexes = [ - self.model().mapFromSource(index) - for index in delegateIndexes # type: ignore[union-attr] + self.model().mapFromSource(index) # type: ignore[union-attr] + for index in delegateIndexes ] for delegateIndex in proxyDelegateIndexes: self.openPersistentEditor(delegateIndex) From f9180104a49a37b2d904cd6ce7fec20536e3c92b Mon Sep 17 00:00:00 2001 From: olivers3uiuc Date: Sat, 23 May 2026 17:54:14 -0400 Subject: [PATCH 11/11] rewrote tests because loadConfig now returns 7 values instead of 6. also, removed 'Save' button, so only can save to file --- src/instrumentserver/gui/shortcuts.py | 11 ++--- test/pytest/test_config.py | 67 +++++++++++++++++++++------ 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/instrumentserver/gui/shortcuts.py b/src/instrumentserver/gui/shortcuts.py index 4b7a294..bfe0231 100644 --- a/src/instrumentserver/gui/shortcuts.py +++ b/src/instrumentserver/gui/shortcuts.py @@ -155,18 +155,15 @@ def __init__( self._indicators: list[QtWidgets.QLabel] = [] self._populateTable() - btnSaveFile = QtWidgets.QPushButton("Save to file") - btnSaveFile.clicked.connect(self._saveToFile) btnReset = QtWidgets.QPushButton("Reset to defaults") btnReset.clicked.connect(self._resetDefaults) - btnSave = QtWidgets.QPushButton("Save") - btnSave.clicked.connect(self._save) + btnSaveFile = QtWidgets.QPushButton("Save to file") + btnSaveFile.clicked.connect(self._saveToFile) btnRow = QtWidgets.QHBoxLayout() - btnRow.addWidget(btnSaveFile) btnRow.addStretch() btnRow.addWidget(btnReset) - btnRow.addWidget(btnSave) + btnRow.addWidget(btnSaveFile) layout = QtWidgets.QVBoxLayout() layout.addWidget(self._table) @@ -277,14 +274,12 @@ def _restoreAfterRevert( widget.setKeySequence(QtGui.QKeySequence(intended)) widget.blockSignals(False) - @QtCore.Slot() def _save(self) -> None: for row, action_id in enumerate(self.manager.REGISTRY): widget = self._table.cellWidget(row, 2) if isinstance(widget, QtWidgets.QKeySequenceEdit): self.manager.rebind(action_id, widget.keySequence().toString()) self._updateAllIndicators() - logger.info("Shortcuts saved locally") @QtCore.Slot() def _saveToFile(self) -> None: diff --git a/test/pytest/test_config.py b/test/pytest/test_config.py index fdbe570..1a4e6e1 100644 --- a/test/pytest/test_config.py +++ b/test/pytest/test_config.py @@ -27,13 +27,20 @@ def test_minimal_config(tmp_path): type: instrumentserver.testing.dummy_instruments.generic.DummyInstrumentWithSubmodule """, ) - path, serverConfig, fullConfig, tempFile, pollingRates, ipAddresses = loadConfig( - cfg - ) + ( + path, + serverConfig, + fullConfig, + shortcutConfig, + tempFile, + pollingRates, + ipAddresses, + ) = loadConfig(cfg) tempFile.close() assert "my_ins" in serverConfig assert "my_ins" in fullConfig + assert shortcutConfig == {} assert pollingRates == {} assert ipAddresses == {} # returned path is a string @@ -49,7 +56,7 @@ def test_temp_file_is_readable(tmp_path): type: some.Type """, ) - tempFilePath, _, _, tempFile, _, _ = loadConfig(cfg) + tempFilePath, _, _, _, tempFile, _, _ = loadConfig(cfg) tempFile.seek(0) content = tempFile.read() assert len(content) > 0 @@ -70,7 +77,7 @@ def test_initialize_defaults_to_true(tmp_path): type: some.Type """, ) - _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) + _, serverConfig, _, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert serverConfig["my_ins"]["initialize"] is True @@ -85,7 +92,7 @@ def test_initialize_explicit_false(tmp_path): initialize: false """, ) - _, serverConfig, _, tempFile, _, _ = loadConfig(cfg) + _, serverConfig, _, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert serverConfig["my_ins"]["initialize"] is False @@ -118,7 +125,7 @@ def test_gui_defaults_to_generic_instrument(tmp_path): type: some.Type """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert fullConfig["my_ins"]["gui"]["type"] == GUIFIELD["type"] @@ -134,7 +141,7 @@ def test_gui_generic_alias_maps_to_full_path(tmp_path): type: generic """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() assert fullConfig["my_ins"]["gui"]["type"] == GUIFIELD["type"] @@ -187,7 +194,7 @@ def test_polling_rate_parsed(tmp_path): param2: 200 """, ) - _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) + _, _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) tempFile.close() assert pollingRates == {"my_ins.param1": 100, "my_ins.param2": 200} @@ -202,7 +209,7 @@ def test_polling_rate_empty_is_ignored(tmp_path): pollingRate: """, ) - _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) + _, _, _, _, tempFile, pollingRates, _ = loadConfig(cfg) tempFile.close() assert pollingRates == {} @@ -224,7 +231,7 @@ def test_networking_parsed(tmp_path): listeningAddress: 192.168.1.1 """, ) - _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) + _, _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) tempFile.close() assert ipAddresses["externalBroadcast"] == "tcp://192.168.1.1:5556" assert ipAddresses["listeningAddress"] == "192.168.1.1" @@ -239,7 +246,7 @@ def test_no_networking_section_gives_empty_dict(tmp_path): type: some.Type """, ) - _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) + _, _, _, _, tempFile, _, ipAddresses = loadConfig(cfg) tempFile.close() assert ipAddresses == {} @@ -262,7 +269,7 @@ def test_gui_defaults_default_section(tmp_path): - IDN """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() kwargs = fullConfig["my_ins"]["gui"].get("kwargs", {}) assert "parameters-hide" in kwargs @@ -282,7 +289,7 @@ def test_gui_defaults_class_section(tmp_path): - power_level """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() kwargs = fullConfig["my_ins"]["gui"].get("kwargs", {}) assert "parameters-hide" in kwargs @@ -310,9 +317,39 @@ def test_gui_defaults_merging_order(tmp_path): - class_param """, ) - _, _, fullConfig, tempFile, _, _ = loadConfig(cfg) + _, _, fullConfig, _, tempFile, _, _ = loadConfig(cfg) tempFile.close() hide = fullConfig["my_ins"]["gui"]["kwargs"]["parameters-hide"] assert "default_param" in hide assert "class_param" in hide assert "instance_param" in hide + + +def test_shortcuts_parsed(tmp_path): + cfg = _write_config( + tmp_path, + """\ +instruments: + my_ins: + type: some.Type +shortcuts: + jump_filter: "Ctrl+G" +""", + ) + _, _, _, shortcutConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert shortcutConfig == {"jump_filter": "Ctrl+G"} + + +def test_no_shortcuts_gives_empty_dict(tmp_path): + cfg = _write_config( + tmp_path, + """\ +instruments: + my_ins: + type: some.Type +""", + ) + _, _, _, shortcutConfig, tempFile, _, _ = loadConfig(cfg) + tempFile.close() + assert shortcutConfig == {}