Skip to content
Open
19 changes: 15 additions & 4 deletions src/instrumentserver/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,23 @@ 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 = (
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)
Expand All @@ -89,8 +96,10 @@ def serverScript() -> None:
serverConfig=serverConfig,
stationConfig=stationConfig,
guiConfig=guiConfig,
shortcutConfig=shortcutConfig,
pollingThread=pollingThread,
ipAddresses=ipAddresses,
configPath=configPath,
)
else:
serverWithGui(
Expand All @@ -100,8 +109,10 @@ def serverScript() -> None:
serverConfig=serverConfig,
stationConfig=stationConfig,
guiConfig=guiConfig,
shortcutConfig=shortcutConfig,
pollingThread=pollingThread,
ipAddresses=ipAddresses,
configPath=configPath,
)

# Close and delete the temporary files
Expand Down
4 changes: 2 additions & 2 deletions src/instrumentserver/client/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions src/instrumentserver/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
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
Expand All @@ -36,6 +38,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"
Expand Down Expand Up @@ -150,6 +153,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:
addressDict = rawConfig["networking"]
Expand All @@ -170,4 +178,12 @@ 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,
)
77 changes: 68 additions & 9 deletions src/instrumentserver/gui/base_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
from typing import Any, Dict, List, Optional, cast

from instrumentserver import QtCore, QtGui, QtWidgets
from instrumentserver.gui.shortcuts import KeyboardShortcutManager


class ItemBase(QtGui.QStandardItem):
Expand Down Expand Up @@ -237,6 +238,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
Expand Down Expand Up @@ -481,7 +484,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
Expand Down Expand Up @@ -769,6 +772,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__(
Expand All @@ -780,6 +784,7 @@ def __init__(
proxyModelType: type = InstrumentSortFilterProxyModel,
viewType: type = InstrumentTreeViewBase,
callSignals: bool = True,
shortcutManager: Optional[KeyboardShortcutManager] = None,
parent: Optional[QtWidgets.QWidget] = None,
**modelKwargs: Any,
) -> None:
Expand All @@ -795,6 +800,12 @@ 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)
Expand Down Expand Up @@ -830,6 +841,12 @@ def connectSignals(self) -> None:
self.proxyModel.onSortingIndicatorChanged
)

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:
"""
Creates the toolbar, override to add more buttons to the toolbar.
Expand All @@ -842,6 +859,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)

toolbar.addSeparator()

Expand All @@ -850,26 +868,30 @@ 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)

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)

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]
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) # type: ignore[union-attr]
trashAction.triggered.connect(lambda x: self.hideTrash()) # 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.
# printAction = toolbar.addAction(
Expand All @@ -883,16 +905,53 @@ def makeToolbar(self) -> QtWidgets.QToolBar:
return toolbar

@QtCore.Slot()
def hideTrash(self) -> None:
self.proxyModel.onToggleTrash()
def refreshAll(self) -> None:
self.model.refreshAll()

@QtCore.Slot()
def promoteStar(self) -> None:
self.proxyModel.onToggleStar()
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)
if isinstance(item, ItemBase):
self.view.lastSelectedItem = item
self.view.itemStarToggle.emit(item)

@QtCore.Slot()
def refreshAll(self) -> None:
self.model.refreshAll()
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)
if isinstance(item, ItemBase):
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need all these new slots?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the first three and refactored the code accordingly. As for the final four, they act as thin wrappers. They need to resolve what the currently selected item is to execute, something the right-click (for trash/star item) implicitly does

def debuggingMethod(self) -> None:
"""
Expand Down
Loading
Loading