Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ jobs:
libgl1 \
libegl1 \
libxkbcommon0 \
libdbus-1-3
libdbus-1-3 \
libfontconfig1 \
fonts-dejavu-core
- run: uv sync --locked --group dev
- run: uv run pytest test/ -v

Expand Down
17 changes: 13 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 Down Expand Up @@ -100,8 +107,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
2 changes: 1 addition & 1 deletion src/instrumentserver/client/proxy.py
Original file line number Diff line number Diff line change
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,
)
87 changes: 74 additions & 13 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,7 @@ 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
"""

for pattern in patterns:
if fnmatch.fnmatch(name, pattern):
return True
Expand Down Expand Up @@ -481,7 +483,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 +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__(
Expand All @@ -780,6 +783,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 +799,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 +840,17 @@ 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)
self.shortcutManager.register("refresh_all", self.refreshAll, self)
self.shortcutManager.register("expand_all", self.view.expandAll, self)
self.shortcutManager.register("collapse_all", self.view.collapseAll, self)
self.shortcutManager.register("toggle_star", self._starAction.trigger, self) # type: ignore[union-attr]
self.shortcutManager.register("toggle_trash", self._trashAction.trigger, self) # type: ignore[union-attr]

def makeToolbar(self) -> QtWidgets.QToolBar:
"""
Creates the toolbar, override to add more buttons to the toolbar.
Expand All @@ -842,6 +863,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.register_tooltip("refresh_all", refreshAction)

toolbar.addSeparator()

Expand All @@ -850,26 +872,30 @@ def makeToolbar(self) -> QtWidgets.QToolBar:
"expand tree",
)
expandAction.triggered.connect(lambda x: self.view.expandAll()) # type: ignore[union-attr]
self.shortcutManager.register_tooltip("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.register_tooltip("collapse_all", collapseAction)

toolbar.addSeparator()

starAction = toolbar.addAction(
self._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._starAction.setCheckable(True) # type: ignore[union-attr]
self._starAction.triggered.connect(lambda x: self.proxyModel.onToggleStar()) # type: ignore[union-attr]
self.shortcutManager.register_tooltip("toggle_star", self._starAction)

trashAction = toolbar.addAction(
self._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._trashAction.setCheckable(True) # type: ignore[union-attr]
self._trashAction.triggered.connect(lambda x: self.proxyModel.onToggleTrash()) # type: ignore[union-attr]
self.shortcutManager.register_tooltip("toggle_trash", self._trashAction)

# Debugging tools keep commented for commits.
# printAction = toolbar.addAction(
Expand All @@ -883,16 +909,51 @@ def makeToolbar(self) -> QtWidgets.QToolBar:
return toolbar

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

def _getCurrentItem(self) -> Optional[ItemBase]:
proxy_index = self.view.currentIndex()
if not proxy_index.isValid():
return 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)
return item if isinstance(item, ItemBase) else None

def _toggleCurrentItem(self, signal: QtCore.SignalInstance) -> None:
item = self._getCurrentItem()
if item is not None:
self.view.lastSelectedItem = item
signal.emit(item)

@QtCore.Slot()
def promoteStar(self) -> None:
self.proxyModel.onToggleStar()
def _starCurrentItem(self) -> None:
self._toggleCurrentItem(self.view.itemStarToggle)

@QtCore.Slot()
def refreshAll(self) -> None:
self.model.refreshAll()
def _trashCurrentItem(self) -> None:
self._toggleCurrentItem(self.view.itemTrashToggle)

@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:
"""
Expand Down
Loading
Loading