from abc import abstractmethod
from typing import TYPE_CHECKING
from qtpy.QtWidgets import QComboBox
from pint import DimensionalityError
from qtpy import QtWidgets, QtCore, QtGui
from typing import Union, List
from pymodaq.utils.shared_ui import SharedUI
from pymodaq_gui.qt_utils import mkQApp
from pymodaq_gui.utils.widgets.widget_with_label_title import WidgetWithLabelTitle
from pymodaq_gui.utils.widgets.window import make_window
from pymodaq_utils.utils import ThreadCommand
from pymodaq_utils.config import GlobalConfig as Config
from pymodaq.control_modules.thread_commands import UiToMainMove
from pymodaq.control_modules.ui_utils import ControlModuleUI
from pymodaq.utils.data import DataActuator
from pymodaq_data import Q_
from pymodaq_data import DataToExport
from pymodaq_gui.plotting.data_viewers import ViewerDispatcher
from pymodaq_gui.utils import (DockArea, QSpinBoxWithShortcut,
PushButtonIcon, QSpinBox_ro,
Dock)
from pymodaq_gui.utils.widgets import LabelWithFont, MultistateLED, StatusPalette, Status
from pymodaq_gui.plotting.utils.plot_utils import DetachablePanel
from pymodaq_utils.enums import StrEnum
from pymodaq_utils.utils import ThreadCommand
from pymodaq.control_modules.daq_move_ui.utils import UiType
config = Config()
class MoveLedState(StrEnum):
"""States of the DAQ_Move status LED."""
UNINITIALIZED = 'uninitialized'
IDLE = 'idle'
MOVING = 'moving'
ERROR = 'error'
[docs]
class DAQMoveUI(ControlModuleUI):
"""DAQ_Move user interface.
This class manages the UI and emit dedicated signals depending on actions from the user
Attributes
----------
command_sig: Signal[Threadcommand]
This signal is emitted whenever some actions done by the user has to be
applied on the main module. Possible commands are:
* init
* get_value
* loop_get_value
* find_home
* stop
* move_abs
* move_rel
* actuator_changed
* rel_value
* show_plugin_config
Methods
-------
display_value(value: float)
Update the display of the actuator's value on the UI
do_init()
Programmatic init
See Also
--------
pymodaq.utils.daq_utils.ThreadCommand
"""
is_compact = False
def __init__(self, app, parent: Union[DockArea, QtWidgets.QWidget],
title="DAQ_Move",
controls_dock: Dock = None,
settings_dock: Dock = None,):
super().__init__(app,
parent,
title=title,
settings_dock=settings_dock,)
self.controls_dock = controls_dock
self.title = title
self._unit = ''
self.actuators_combo: QComboBox = None
self.abs_value_sb: QSpinBoxWithShortcut = None
self.abs_value_sb_green: QSpinBoxWithShortcut = None
self.abs_value_sb_2: QSpinBoxWithShortcut = None
self.abs_value_sb_red: QSpinBoxWithShortcut = None
self.abs_value_sb_bis: QSpinBoxWithShortcut = None
self.status_led: MultistateLED = None
self.current_value_sb: QSpinBox_ro = None
self.find_home_pb: PushButtonIcon = None
self.move_rel_plus_pb: PushButtonIcon = None
self.move_abs_pb: PushButtonIcon = None
self.rel_value_sb: QSpinBoxWithShortcut = None
self.move_rel_minus_pb: PushButtonIcon = None
self.stop_pb: PushButtonIcon = None
self.get_value_pb: PushButtonIcon = None
self.control_widget: WidgetWithLabelTitle = None
self._controls_panel: DetachablePanel = None
self.graph_widget: QtWidgets.QWidget = None
self.viewer: ViewerDispatcher = None
self.setup_ui()
self.enable_move_buttons(False)
# -------------------------------------------------------------------------
# Properties
# -------------------------------------------------------------------------
@property
def actuator_init(self):
return self._ini_state
@actuator_init.setter
def actuator_init(self, status):
self._ini_state = status
self.status_led.set_state(MoveLedState.IDLE if status else MoveLedState.UNINITIALIZED)
self.enable_move_buttons(status)
self.update_init_icon(status, 'ini_actuator')
if self.has_action('ini_actuator'):
self.set_action_checked('ini_actuator', status)
@property
def actuator(self):
return self.actuators_combo.currentText()
@actuator.setter
def actuator(self, act_name: str):
self.actuators_combo.setCurrentText(act_name)
@property
def actuators(self):
return [self.actuators_combo.itemText(ind) for ind in range(self.actuators_combo.count())]
@actuators.setter
def actuators(self, actuators: List[str]):
self.actuators_combo.clear()
self.actuators_combo.addItems(actuators)
@property
def move_done(self):
"""bool: True when the actuator is idle (not moving)."""
return self.status_led.get_state() != MoveLedState.MOVING
@move_done.setter
def move_done(self, status):
self.status_led.set_state(MoveLedState.IDLE if status else MoveLedState.MOVING)
def quit_fun(self) -> bool | None:
return self.app.quit_fun()
# -------------------------------------------------------------------------
# UI Construction
# -------------------------------------------------------------------------
def setup_docks_and_widgets(self):
self.parent.setLayout(QtWidgets.QHBoxLayout())
self.control_widget = WidgetWithLabelTitle(self.title, closable=True, attachable=True)
self.control_widget.sig_close.connect(lambda: self.get_action('show_controls').trigger())
self.control_widget.closeEvent = lambda event: self.set_action_checked('show_controls', False)
self._controls_panel = DetachablePanel(
self.control_widget, self.controls_dock, f'{self.title} controls',
detached=self.config('pymodaq', 'actuator', 'controls_as_popup'),
orientation=QtCore.Qt.Orientation.Vertical,
layout_config_path=('pymodaq', 'actuator', 'controls_dock_layout'),
is_shown=lambda: self.is_action_checked('show_controls'))
self.parent.layout().setContentsMargins(0, 0, 0, 0)
self.actuators_combo = QComboBox()
self.abs_value_sb = QSpinBoxWithShortcut(step=0.1, dec=True, siPrefix=config('pymodaq', 'actuator', 'siprefix'))
self.abs_value_sb.setStyleSheet("background-color : lightgreen; color: black")
self.abs_value_sb.setValue(config('pymodaq', 'actuator', 'default_value_green'))
self.abs_value_sb_green = self.abs_value_sb
self.abs_value_sb_2 = QSpinBoxWithShortcut(step=0.1, dec=True, siPrefix=config('pymodaq', 'actuator', 'siprefix'))
self.abs_value_sb_2.setStyleSheet("background-color : lightcoral; color: black")
self.abs_value_sb_2.setValue(config('pymodaq', 'actuator', 'default_value_red'))
self.abs_value_sb_red = self.abs_value_sb_2
self.abs_value_sb_bis = QSpinBoxWithShortcut(step=0.1, dec=True, siPrefix=config('pymodaq', 'actuator', 'siprefix'))
self.status_led = MultistateLED(
states=[
(MoveLedState.UNINITIALIZED, StatusPalette.color(Status.OFF)),
(MoveLedState.IDLE, StatusPalette.color(Status.IDLE)),
(MoveLedState.MOVING, StatusPalette.color(Status.RUNNING)),
(MoveLedState.ERROR, StatusPalette.color(Status.CRITICAL)),
],
readonly=True,
)
self.current_value_sb = QSpinBox_ro(font_size=10, min_height=20,
siPrefix=config('pymodaq', 'actuator', 'siprefix'),
)
self.current_value_sb.setMinimumWidth(80)
self.abs_value_sb_2.setMinimumWidth(80)
self.abs_value_sb.setMinimumWidth(80)
self.current_value_sb.set_font_size(10)
self.current_value_sb.setMinimumHeight(20)
self.current_value_sb.setMinimumWidth(80)
self.find_home_pb = PushButtonIcon('home', 'Find Home', icon_color=self.get_theme().magenta)
self.move_rel_plus_pb = PushButtonIcon('step_out', 'Set Rel. (+)', icon_color=self.get_theme().yellow)
self.move_abs_pb = PushButtonIcon('step', 'Set Abs.',
tip='Set the value of the actuator to the set absolute value',
icon_color=self.get_theme().green)
self.rel_value_sb = QSpinBoxWithShortcut(step=0.1, dec=True, siPrefix=config('pymodaq', 'actuator', 'siprefix'),
key_sequences=("Ctrl+E","Ctrl+Shift+E"),)
self.move_rel_minus_pb = PushButtonIcon('step_into', 'Set Rel. (-)', icon_color=self.get_theme().blue)
self.stop_pb = PushButtonIcon('stop_circle', 'Stop', icon_color=self.get_theme().red)
self.get_value_pb = PushButtonIcon('looks_one', 'Update Value', icon_color=self.get_theme().cyan)
self.statusbar.setMaximumHeight(30)
self.graph_widget = QtWidgets.QWidget()
self.graph_widget.setLayout(QtWidgets.QHBoxLayout())
self.graph_widget.layout().setContentsMargins(0, 0, 0, 0)
dockarea = DockArea()
self.graph_widget.layout().addWidget(dockarea)
self.viewer = ViewerDispatcher(dockarea)
self.actuator_init = False
self.populate_control_ui(self.control_widget)
def setup_menus_and_toolbars(self, menubar: QtWidgets.QMenuBar = None):
self.add_toolbar('move', 'DAQMove')
self.parent.layout().insertWidget(0, self.get_toolbar('move'))
def populate_control_ui(self, widget: WidgetWithLabelTitle):
container_widget = QtWidgets.QWidget()
widget.insert_widget(container_widget)
container_widget.setLayout(QtWidgets.QGridLayout())
container_widget.layout().addWidget(LabelWithFont('Abs. Value'), 0, 0)
container_widget.layout().addWidget(self.find_home_pb, 0, 1)
container_widget.layout().addWidget(self.abs_value_sb_bis, 1, 0)
container_widget.layout().addWidget(self.move_abs_pb, 1, 1)
container_widget.layout().addWidget(LabelWithFont('Rel. Increment'), 2, 0)
container_widget.layout().addWidget(self.move_rel_plus_pb, 2, 1)
container_widget.layout().addWidget(self.rel_value_sb, 3, 0)
container_widget.layout().addWidget(self.move_rel_minus_pb, 3, 1)
container_widget.layout().addWidget(self.stop_pb, 4, 0)
container_widget.layout().addWidget(self.get_value_pb, 4, 1)
container_widget.layout().setContentsMargins(0, 0, 0, 0)
widget.setVisible(False)
def setup_actions(self, toolbar: QtWidgets.QToolBar = None):
if toolbar is None:
toolbar = self.toolbar
self._setup_name_widget(toolbar=toolbar)
self.add_widget('actuators_combo', self.actuators_combo, toolbar=toolbar)
self._setup_init_action(toolbar=toolbar, action_name='ini_actuator',
display_name='Ini. Actuator', tip='Connect to selected actuator')
self._setup_settings_action(toolbar=toolbar)
toolbar.addSeparator()
self.add_widget('current', self.current_value_sb, toolbar=toolbar)
self.add_widget('move_done', self.status_led, toolbar=toolbar)
self.add_action('stop', 'Stop', 'stop_circle', "Stop Motion",
toolbar=toolbar, icon_color=self.get_theme().red)
toolbar.addSeparator()
self.add_action('show_controls', 'Show Controls',
'discover_tune', "Show more controls",
icon_checked_color=self.get_theme().green,
checkable=True,
toolbar=toolbar)
self.add_action('show_graph', 'Show Graph', 'bid_landscape', 'Show/Hide the Graph Widget',
checkable=True, checked=True, icon_checked='bid_landscape_disabled',
icon_color=self.get_theme().green, icon_checked_color=self.get_theme().red,
toolbar=toolbar)
self.add_action('refresh_value', 'Refresh', 'repeat',
"Refresh Value Continuously", checkable=True,
toolbar=toolbar, icon_checked='repeat_on',
icon_checked_color=self.get_theme().green)
self.add_widget('status', self.statusbar, toolbar=toolbar)
def setup_absolute_spinbox_actions(self, toolbar: QtWidgets.QToolBar = None):
if toolbar is None:
toolbar = self.toolbar
self.add_widget('abs_green', self.abs_value_sb, toolbar=toolbar,
before='stop')
self.add_widget('abs_red', self.abs_value_sb_2, toolbar=toolbar,
before='stop')
def remove_absolute_spinbox_actions(self, toolbar: QtWidgets.QToolBar = None):
self.remove_action(toolbar, action='abs_green')
self.remove_action(toolbar, action='abs_red')
def setup_absolute_actions(self, toolbar: QtWidgets.QToolBar = None):
if toolbar is None:
toolbar = self.toolbar
self.add_action('move_abs_green', 'Move Abs', 'step',
"Move to the set absolute value",
icon_color=self.get_theme().green,
toolbar=toolbar,
before='stop')
self.add_action('move_abs_red', 'Move Abs', 'step',
"Move to the other set absolute value",
icon_color=self.get_theme().red,
toolbar=toolbar,
before='stop')
def remove_absolute_actions(self, toolbar: QtWidgets.QToolBar = None):
self.remove_action(toolbar, action='move_abs_green')
self.remove_action(toolbar, action='move_abs_red')
def setup_relative_actions(self, toolbar: QtWidgets.QToolBar = None):
if toolbar is None:
toolbar = self.toolbar
self.add_widget('rel_move', self.rel_value_sb, toolbar=toolbar,
before='stop')
self.add_action('move_rel_plus', 'Set Rel. (+)', 'step_out',
toolbar=toolbar,
icon_color=self.get_theme().yellow,
before='stop')
self.add_action('move_rel_minus', 'Set Rel. (-)', 'step_into',
toolbar=toolbar,
icon_color=self.get_theme().blue,
before='stop')
self.add_action('reset_value', 'Reset Internal Value (no encoder)', 'restart_alt',
toolbar=toolbar,
before='stop')
def remove_relative_actions(self, toolbar: QtWidgets.QToolBar = None):
self.remove_action(toolbar, action='rel_move')
self.remove_action(toolbar, action='move_rel_plus')
self.remove_action(toolbar, action='move_rel_minus')
self.remove_action(toolbar, action='reset_value')
[docs]
def set_ui_type(self, ui_type: UiType):
""" Called after initialization to finalize the UI setup given the
chosen UI type"""
from pymodaq import ActuatorUIFactory
if ui_type == UiType.NONE:
ui_type = config('pymodaq', 'actuator', 'ui')[0]
custom_ui = ActuatorUIFactory.get(ui_type)(self)
custom_ui.setup_docks_and_widgets()
custom_ui.setup_move_actions()
self.connect_move_actions()
custom_ui.setup_action_visibility()
def cleanup_ui(self):
try:
self.actuators_combo.currentTextChanged.disconnect()
except TypeError:
pass
if 'move_abs_green' in self.actions_names:
self.connect_action('move_abs_green', connect=False)
if 'move_abs_red' in self.actions_names:
self.connect_action('move_abs_red', connect=False)
try:
self.move_abs_pb.clicked.disconnect()
except TypeError:
pass
try:
self.move_rel_plus_pb.clicked.disconnect()
except TypeError:
pass
try:
self.move_rel_minus_pb.clicked.disconnect()
except TypeError:
pass
self.remove_absolute_spinbox_actions(self.toolbar)
self.remove_absolute_actions(self.toolbar)
self.remove_relative_actions(self.toolbar)
def connect_common_move_actions(self):
if 'show_controls' in self.actions_names:
self.connect_action('show_controls', self.show_controls)
if 'show_graph' in self.actions_names:
self.connect_action('show_graph', lambda checked: self.show_graph(not checked))
if 'stop' in self.actions_names:
self.connect_action('stop', lambda: self.command_sig.emit(ThreadCommand(UiToMainMove.STOP, )))
if 'show_config' in self.actions_names:
self.connect_action('show_config', lambda: self.command_sig.emit(ThreadCommand(UiToMainMove.SHOW_CONFIG, )))
if 'refresh_value' in self.actions_names:
self.connect_action('refresh_value',
lambda do_refresh: self.command_sig.emit(ThreadCommand(UiToMainMove.LOOP_GET_VALUE,
do_refresh)))
def connect_things(self):
self._connect_common_actions()
self.connect_common_move_actions()
def connect_move_actions(self):
if 'move_abs_green' in self.actions_names:
self.connect_action('move_abs_green', lambda: self.emit_move_abs(self.abs_value_sb))
if 'move_abs_red' in self.actions_names:
self.connect_action('move_abs_red', lambda: self.emit_move_abs(self.abs_value_sb_2))
if 'reset_value' in self.actions_names:
self.connect_action('reset_value',
lambda: self.command_sig.emit(ThreadCommand(UiToMainMove.RESET_VALUE, )))
self.move_abs_pb.clicked.connect(lambda: self.emit_move_abs(self.abs_value_sb_bis))
self.abs_value_sb.shortcut["Ctrl+E"].activated.connect(lambda: self.emit_move_abs(self.abs_value_sb))
self.abs_value_sb_2.shortcut["Ctrl+E"].activated.connect(lambda: self.emit_move_abs(self.abs_value_sb_2))
self.abs_value_sb_bis.shortcut["Ctrl+E"].activated.connect(lambda: self.emit_move_abs(self.abs_value_sb_bis))
self.rel_value_sb.valueChanged.connect(lambda: self.command_sig.emit(
ThreadCommand(UiToMainMove.REL_VALUE, self.rel_value_sb.value())))
self.move_rel_plus_pb.clicked.connect(lambda: self.emit_move_rel('+'))
self.move_rel_minus_pb.clicked.connect(lambda: self.emit_move_rel('-'))
self.rel_value_sb.shortcut["Ctrl+E"].activated.connect(lambda: self.emit_move_rel('+'))
self.rel_value_sb.shortcut["Ctrl+Shift+E"].activated.connect(lambda: self.emit_move_rel('-'))
self.find_home_pb.clicked.connect(lambda: self.command_sig.emit(ThreadCommand(UiToMainMove.FIND_HOME, )))
self.stop_pb.clicked.connect(lambda: self.command_sig.emit(ThreadCommand(UiToMainMove.STOP, )))
self.get_value_pb.clicked.connect(lambda: self.command_sig.emit(ThreadCommand(UiToMainMove.GET_VALUE, )))
self.actuators_combo.currentTextChanged.connect(
lambda act: self.command_sig.emit(ThreadCommand(UiToMainMove.ACTUATOR_CHANGED, act)))
# -------------------------------------------------------------------------
# Public API
# -------------------------------------------------------------------------
[docs]
def do_init(self, do_init=True):
"""Programmatically press the Init button
API entry
Parameters
----------
do_init: bool
will fire the Init button depending on the argument value and the button check state
"""
if do_init is not self.is_action_checked('ini_actuator'):
self.get_action('ini_actuator').trigger()
def show_data(self, data: DataToExport):
self.viewer.show_data(data)
def display_value(self, value: DataActuator):
try:
self.current_value_sb.setValue(value.value(self._unit))
except DimensionalityError as e:
value.force_units(self._unit)
self.current_value_sb.setValue(value.value())
def enable_move_buttons(self, status):
self.abs_value_sb.setEnabled(status)
self.abs_value_sb_2.setEnabled(status)
for action_name in ('move_abs_green', 'move_abs_red', 'move_rel'):
if action_name in self.actions_names:
self.get_action(action_name).setEnabled(status)
self.control_widget.setEnabled(status)
[docs]
def set_abs_spinbox_properties(self, **properties):
""" Change the Spinbox properties
Parameters
--------
properties: dict or named parameters
possible keys are :
* decimals: to set the number of displayed decimals
* 'minimum': to set the minimum value
* 'maximum': to set the maximum value
* 'step': to set the step value
"""
if 'decimals' in properties:
self.abs_value_sb.setDecimals(properties['decimals'])
self.abs_value_sb_2.setDecimals(properties['decimals'])
self.abs_value_sb_bis.setDecimals(properties['decimals'])
if 'minimum' in properties:
self.abs_value_sb.setMinimum(properties['minimum'])
self.abs_value_sb_2.setMinimum(properties['minimum'])
self.abs_value_sb_bis.setMinimum(properties['minimum'])
if 'maximum' in properties:
self.abs_value_sb.setMaximum(properties['maximum'])
self.abs_value_sb_2.setMaximum(properties['maximum'])
self.abs_value_sb_bis.setMaximum(properties['maximum'])
if 'step' in properties:
self.abs_value_sb.setSingleStep(properties['step'])
self.abs_value_sb_2.setSingleStep(properties['step'])
self.abs_value_sb_bis.setSingleStep(properties['step'])
def set_abs_value_red(self, value: Q_):
self.abs_value_sb_2.setValue(value.m_as(self._unit))
def set_abs_value_green(self, value: Q_):
self.abs_value_sb.setValue(value.m_as(self._unit))
def set_abs_value(self, value: Q_):
self.abs_value_sb_bis.setValue(value.m_as(self._unit))
def set_relative_value(self, value: Q_):
self.rel_value_sb.setValue(value.m_as(self._unit))
def set_rel_value(self, value: Q_):
self.rel_value_sb.setValue(value.m_as(self._unit))
[docs]
def set_unit_as_suffix(self, unit: str):
"""Will append the actuator units in the value display"""
self._unit = unit
self.current_value_sb.setOpts(suffix=unit)
self.abs_value_sb_bis.setOpts(suffix=unit)
self.abs_value_sb.setOpts(suffix=unit)
self.abs_value_sb_2.setOpts(suffix=unit)
self.rel_value_sb.setOpts(suffix=unit)
[docs]
def set_unit_prefix(self, show=True):
""" Change the display status of the spinbox SI prefix"""
self.current_value_sb.setOpts(siPrefix=show)
self.abs_value_sb_bis.setOpts(siPrefix=show)
self.abs_value_sb.setOpts(siPrefix=show)
self.abs_value_sb_2.setOpts(siPrefix=show)
self.rel_value_sb.setOpts(siPrefix=show)
def set_settings_tree(self, tree):
self._settings_widget = tree
# -------------------------------------------------------------------------
# Slots / Event Handlers
# -------------------------------------------------------------------------
def send_init(self, checked: bool):
self.actuators_combo.setEnabled(not checked)
self.command_sig.emit(ThreadCommand(UiToMainMove.INIT, [checked,
self.actuators_combo.currentText()]))
def emit_move_abs(self, spinbox):
spinbox.editingFinished.emit()
self.command_sig.emit(ThreadCommand(UiToMainMove.MOVE_ABS, DataActuator(data=spinbox.value(),
units=self._unit)))
def emit_move_rel(self, sign):
self.rel_value_sb.editingFinished.emit()
self.command_sig.emit(ThreadCommand(
UiToMainMove.MOVE_REL,
DataActuator(data=self.rel_value_sb.value() * (1 if sign == '+' else -1),
units=self._unit)))
# -------------------------------------------------------------------------
# Visibility / Lifecycle
# -------------------------------------------------------------------------
def show_controls(self, show: bool = True):
self._controls_panel.show(show)
def show_graph(self, show: bool = True):
self.graph_widget.setWindowTitle(f'{self.title} graph')
self.graph_widget.setVisible(show)
self.graph_widget.closeEvent = lambda event: self.set_action_checked('show_graph', False)
def close(self):
self.parent.close()
self.graph_widget.close()
self.control_widget.close()
super().close()
if __name__ == '__main__':
app = mkQApp('DAQ_Move')
win, area = make_window(area=False, title='DAQ_Move')
widget = QtWidgets.QWidget()
daq_move = DAQMoveUI(widget)
win.setCentralWidget(widget)
shared_ui = SharedUI(win)
shared_ui.affect_application(daq_move)
shared_ui.add_toolbar('move_toolbar', 'Move', win,
toolbar=daq_move.toolbar,
add_break=False)
daq_move.settings_tree.setVisible(False)
widget.layout().addWidget(daq_move.control_widget)
widget.layout().addWidget(daq_move.settings_tree)
widget.layout().addWidget(daq_move.graph_widget)
app.exec()