Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions src/instrumentserver/gui/base_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,14 @@ class InstrumentTreeViewBase(QtWidgets.QTreeView):
#: emitted when this item got its star action triggered.
itemStarToggle = QtCore.Signal(ItemBase)

#: Signal()
#: emitted when the user presses Enter, F2, or Right to enter edit mode on the selected parameter.
editCurrentParameter = QtCore.Signal()

#: Signal()
#: emitted when the user presses Delete to clear the selected parameter's value.
clearCurrentParameter = QtCore.Signal()

def __init__(
self,
model: QtCore.QAbstractItemModel,
Expand Down Expand Up @@ -586,6 +594,15 @@ def __init__(
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.onContextMenuRequested)

for key in ("Return", "Enter", "F2", "Right"):
sc = QtWidgets.QShortcut(QtGui.QKeySequence(key), self)
sc.setContext(QtCore.Qt.ShortcutContext.WidgetShortcut)
sc.activated.connect(self.editCurrentParameter)

sc = QtWidgets.QShortcut(QtGui.QKeySequence("Backspace"), self)
sc.setContext(QtCore.Qt.ShortcutContext.WidgetShortcut)
sc.activated.connect(self.clearCurrentParameter)

@QtCore.Slot()
def fillCollapsedDict(self, parentItem: Optional[ItemBase] = None) -> None:
"""
Expand Down Expand Up @@ -749,6 +766,15 @@ def onContextMenuRequested(self, pos: QtCore.QPoint) -> None:

self.contextMenu.exec_(self.mapToGlobal(pos))

def focusNextPrevChild(self, next: bool) -> bool:
current = self.currentIndex()
if current.isValid():
next_idx = self.indexBelow(current) if next else self.indexAbove(current)
if next_idx.isValid():
self.setCurrentIndex(next_idx)
return True
return super().focusNextPrevChild(next)

@QtCore.Slot()
def onStarActionTrigger(self) -> None:
self.itemStarToggle.emit(self.lastSelectedItem)
Expand Down
130 changes: 128 additions & 2 deletions src/instrumentserver/gui/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ class MethodDisplay(QtWidgets.QWidget):
#: emitted when the widget runs a function and is successful. Emits the return value as a string.
runSuccessful = QtCore.Signal(str)

#: Signal() --
#: emitted when the user commits via Return/Enter, regardless of success or failure
valueCommitted = QtCore.Signal()

def __init__(
self,
fun: Callable,
Expand Down Expand Up @@ -254,6 +258,8 @@ def runFun(self) -> None:
except Exception as e:
self.runFailed.emit(str(e))
logger.warning(f"'{self.fullName}' Raised the following execution: {e}")
finally:
self.valueCommitted.emit()

@classmethod
def getTooltipFromFun(cls, fun: Callable) -> str:
Expand Down Expand Up @@ -286,6 +292,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None:
# Stores as key the name of the item and as value the widget that the delegate creates.
# used to keep a reference to the widget.
self.parameters: Dict[str, QtWidgets.QWidget] = {}
self.navFilter: Optional["ValueCellNavigationFilter"] = None

def createEditor( # type: ignore[override]
self,
Expand All @@ -301,6 +308,16 @@ def createEditor( # type: ignore[override]

ret = ParameterWidget(element, widget)
self.parameters[item.name] = ret # type: ignore[attr-defined]
ret.valueCommitted.connect(self.parent().setFocus) # type: ignore[union-attr]

if self.navFilter is not None:
if isinstance(ret.paramWidget, AnyInput):
input_widget = ret.paramWidget.input
else:
input_widget = ret.paramWidget
input_widget.installEventFilter(self.navFilter)
self.navFilter.registerWidget(input_widget, index)

# Try to fetch and display current value immediately
# ---- Chao: removed because the constructor of ParameterWidget object already calls parameter get ----
# if element.gettable:
Expand All @@ -312,6 +329,76 @@ def createEditor( # type: ignore[override]
return ret


class ValueCellNavigationFilter(QtCore.QObject):
"""Event filter installed on value input widgets to handle Escape, Tab, and Shift+Tab."""

def __init__(self, treeView: InstrumentTreeViewBase) -> None:
super().__init__(treeView)
self._treeView = treeView
self._widgetIndex: Dict[QtCore.QObject, QtCore.QPersistentModelIndex] = {}

def registerWidget(self, widget: QtCore.QObject, index: QtCore.QModelIndex) -> None:
self._widgetIndex[widget] = QtCore.QPersistentModelIndex(index)

def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: # type: ignore[override]
if event.type() == QtCore.QEvent.Type.FocusIn:
if obj in self._widgetIndex:
idx = self._widgetIndex[obj]
if idx.isValid():
self._treeView.setCurrentIndex(QtCore.QModelIndex(idx))
return False

if event.type() == QtCore.QEvent.Type.KeyPress:
assert isinstance(event, QtGui.QKeyEvent)
key = QtGui.QKeySequence(event.key()).toString()

if key == "Esc":
host = self._findHostWidget(obj)
if isinstance(host, ParameterWidget):
host.setWidgetFromParameter()
elif isinstance(host, MethodDisplay):
host.anyInput.input.clear()
self._treeView.setFocus()
return True

elif key == "Tab":
self._commitAndMove(obj, 1)
return True

elif key == "Backtab":
self._commitAndMove(obj, -1)
return True

return super().eventFilter(obj, event)

def _findHostWidget(
self, obj: QtCore.QObject
) -> Optional[Union[ParameterWidget, MethodDisplay]]:
parent = obj.parent()
while parent is not None:
if isinstance(parent, (ParameterWidget, MethodDisplay)):
return parent
parent = parent.parent()
return None

def _commitAndMove(self, obj: QtCore.QObject, direction: int) -> None:
host = self._findHostWidget(obj)
if isinstance(host, ParameterWidget):
host.setButton.click()
elif isinstance(host, MethodDisplay):
host.runFun()
current = self._treeView.currentIndex()
if current.isValid():
next_idx = (
self._treeView.indexBelow(current)
if direction > 0
else self._treeView.indexAbove(current)
)
if next_idx.isValid():
self._treeView.setCurrentIndex(next_idx)
self._treeView.setFocus()


class ModelParameters(InstrumentModelBase):
# : Signal(item, object) : Emitted when an item in the model has received a new value, first object is the item's
# name, second object is its new value
Expand Down Expand Up @@ -424,6 +511,7 @@ def __init__(
super().__init__(model, [2], *args, **kwargs)

self.delegate = ParameterDelegate(self)
self.delegate.navFilter = ValueCellNavigationFilter(self)

self.setItemDelegateForColumn(2, self.delegate)
self.setAllDelegatesPersistent()
Expand Down Expand Up @@ -479,14 +567,16 @@ def __init__(
**modelKwargs,
)

self.view.editCurrentParameter.connect(self._focusToParameterValue)
self.view.clearCurrentParameter.connect(self._clearCurrentParameter)

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("edit_value", self._focusToParameterValue, self)

def _withCurrentParameter(
self, callback: Callable[["ParameterWidget"], None]
Expand Down Expand Up @@ -521,6 +611,18 @@ def _focusToParameterValue(self) -> None:
)
)

@QtCore.Slot()
def _clearCurrentParameter(self) -> None:
self._withCurrentParameter(
lambda w: (
w.paramWidget.input.clear()
if isinstance(w.paramWidget, AnyInput)
else w.paramWidget.clear()
if hasattr(w.paramWidget, "clear")
else None
)
)


# ----------------- Parameters Display Classes - Ending --------------------------------

Expand All @@ -544,6 +646,15 @@ def createEditor( # type: ignore[override]

ret = ParameterWidget(parameter=element, parent=widget, additionalWidgets=[rw])
self.parameters[item.name] = ret # type: ignore[attr-defined]
ret.valueCommitted.connect(self.parent().setFocus) # type: ignore[union-attr]

if self.navFilter is not None:
if isinstance(ret.paramWidget, AnyInput):
input_widget = ret.paramWidget.input
else:
input_widget = ret.paramWidget
input_widget.installEventFilter(self.navFilter)
self.navFilter.registerWidget(input_widget, index)

return ret

Expand Down Expand Up @@ -572,6 +683,7 @@ def __init__(
super().__init__(model, [2], *args, **kwargs)

self.delegate = ParameterDeleteDelegate(self)
self.delegate.navFilter = ValueCellNavigationFilter(self)

self.setItemDelegateForColumn(2, self.delegate)
self.setAllDelegatesPersistent()
Expand Down Expand Up @@ -769,6 +881,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None) -> None:
super().__init__(parent=parent)

self.methods: Dict[str, "MethodDisplay"] = {}
self.navFilter: Optional[ValueCellNavigationFilter] = None

def createEditor( # type: ignore[override]
self,
Expand All @@ -786,6 +899,12 @@ def createEditor( # type: ignore[override]
parent.clearAlertsAction.triggered.connect(ret.alertLabel.clearAlert) # type: ignore[union-attr]

self.methods[item.name] = ret # type: ignore[attr-defined]
ret.valueCommitted.connect(self.parent().setFocus) # type: ignore[union-attr]

if self.navFilter is not None:
ret.anyInput.input.installEventFilter(self.navFilter)
self.navFilter.registerWidget(ret.anyInput.input, index)

return ret


Expand All @@ -804,6 +923,7 @@ def __init__(
self.contextMenu.addAction(self.clearAlertsAction)

self.delegate = MethodsDelegate(self)
self.delegate.navFilter = ValueCellNavigationFilter(self)
self.setItemDelegateForColumn(1, self.delegate)
self.setAllDelegatesPersistent()

Expand Down Expand Up @@ -832,12 +952,14 @@ def __init__(self, instrument: Any, **kwargs: Any) -> None:
**modelKwargs,
)

self.view.editCurrentParameter.connect(self._focusToMethodValue)
self.view.clearCurrentParameter.connect(self._clearCurrentMethod)

def connectSignals(self) -> None:
super().connectSignals()
self.shortcutManager.register(
"toggle_python", self._togglePythonCurrentItem, self
)
self.shortcutManager.register("edit_value", self._focusToMethodValue, self)
self.shortcutManager.register("run_method", self._runCurrentMethod, self)

def _withCurrentMethod(self, callback: Callable[["MethodDisplay"], None]) -> None:
Expand All @@ -855,6 +977,10 @@ def _togglePythonCurrentItem(self) -> None:
def _focusToMethodValue(self) -> None:
self._withCurrentMethod(lambda w: w.anyInput.input.setFocus())

@QtCore.Slot()
def _clearCurrentMethod(self) -> None:
self._withCurrentMethod(lambda w: w.anyInput.input.clear())

@QtCore.Slot()
def _runCurrentMethod(self) -> None:
self._withCurrentMethod(lambda w: w.runFun())
Expand Down
6 changes: 5 additions & 1 deletion src/instrumentserver/gui/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class ParameterWidget(QtWidgets.QWidget):
#: Signal(Any) --
_valueFromWidget = QtCore.Signal(object)

#: Signal() --
#: emitted when the user commits a value via Return/Enter
valueCommitted = QtCore.Signal()

def __init__(
self,
parameter: Parameter,
Expand Down Expand Up @@ -181,7 +185,7 @@ def onReturnPressed(self) -> None:
"""Activates the setButton when the input is selected and enter is pressed."""
self.setButton.click()
self.paramWidget.input.deselect()
self.setButton.setFocus()
self.valueCommitted.emit()

def setParameter(self, value: Any) -> None:
try:
Expand Down
1 change: 0 additions & 1 deletion src/instrumentserver/gui/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class KeyboardShortcutManager:
"save_items": ("Ctrl+Shift+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:
Expand Down
Loading