def __init__(self, parent: QWidget): super(LogWidget, self).__init__(parent) self.setAttribute( Qt.WA_DeleteOnClose) # This is required to stop background timers! self._clear_button = make_button(self, 'Clear', 'delete-document', on_clicked=self._do_clear) self._status_display = QLabel(self) self._model = _TableModel(self) self._model.layoutChanged.connect(self._on_model_changed) self._table_view = _TableView(self, self._model) # View setup controls_layout = QHBoxLayout() controls_layout.addWidget(self._clear_button) controls_layout.addStretch(1) controls_layout.addWidget(self._status_display) layout = QVBoxLayout() layout.addLayout(controls_layout) layout.addWidget(self._table_view, 1) self.setLayout(layout)
def __init__(self, parent: QWidget, commander: Commander): super(Widget, self).__init__(parent) self._commander = commander self._event_suppression_depth = 0 self._sync_checkbox = QCheckBox(self) self._sync_checkbox.setIcon(get_icon('link')) self._sync_checkbox.setChecked(True) self._sync_checkbox.stateChanged.connect( self._on_sync_checkbox_changed) self._sync_checkbox.setToolTip('Always same value for all phases') self._sync_checkbox.setStatusTip(self._sync_checkbox.toolTip()) self._send_button = \ make_button(self, text='Execute', icon_name='send-up', tool_tip='Sends the command to the device; also, while this button is checked (pressed), ' 'commands will be sent automatically every time the controls are changed by the user.', checkable=True, checked=False, on_clicked=self._on_send_button_changed) self._phase_controls = [ SpinboxLinkedWithSlider(self, minimum=0.0, maximum=100.0, step=1.0, slider_orientation=SpinboxLinkedWithSlider. SliderOrientation.HORIZONTAL) for _ in range(3) ] for pc in self._phase_controls: pc.spinbox_suffix = ' %' pc.value_change_event.connect(self._on_any_control_changed) def make_fat_label(text: str) -> QLabel: lbl = QLabel(text, self) font: QFont = lbl.font() font.setBold(True) lbl.setFont(font) return lbl top_layout_items = [] for index, pc in enumerate(self._phase_controls): top_layout_items.append( lay_out_horizontally( make_fat_label('ABC'[index]), pc.spinbox, (pc.slider, 1), )) self.setLayout( lay_out_horizontally(*(top_layout_items + [self._sync_checkbox, self._send_button])))
def __init__(self, parent: QWidget, commander: Commander): super(MiscControlWidget, self).__init__(parent) self._commander = commander self._performer_should_stop = True self._frequency_input = QDoubleSpinBox(self) self._frequency_input.setRange(100, 15000) self._frequency_input.setValue(3000) self._frequency_input.setSuffix(" Hz") self._frequency_input.setToolTip("Beep frequency, in hertz") self._frequency_input.setStatusTip(self._frequency_input.toolTip()) self._duration_input = QDoubleSpinBox(self) self._duration_input.setRange(0.01, 3) self._duration_input.setValue(0.5) self._duration_input.setSuffix(" s") self._duration_input.setToolTip("Beep duration, in seconds") self._duration_input.setStatusTip(self._duration_input.toolTip()) self._go_button = make_button( self, text="Beep", icon_name="speaker", tool_tip="Sends a beep command to the device once", on_clicked=self._beep_once, ) def stealthy( icon_name: str, music_factory: typing.Callable[[], typing.Iterable["_Note"]] ) -> QWidget: b = make_button( self, icon_name=icon_name, on_clicked=lambda: self._begin_performance(music_factory()), ) b.setFlat(True) b.setFixedSize(4, 4) return b self.setLayout( lay_out_vertically( lay_out_horizontally( QLabel("Frequency", self), self._frequency_input, QLabel("Duration", self), self._duration_input, self._go_button, (None, 1), ), (None, 1), lay_out_horizontally( (None, 1), stealthy("darth-vader", _get_imperial_march), ), ))
def stealthy( icon_name: str, music_factory: typing.Callable[[], typing.Iterable['_Note']] ) -> QWidget: b = make_button( self, icon_name=icon_name, on_clicked=lambda: self._begin_performance(music_factory())) b.setFlat(True) b.setFixedSize(4, 4) return b
def __init__(self, parent: QWidget, commander: Commander): super(Widget, self).__init__(parent) self._commander = commander self.setLayout( lay_out_vertically( lay_out_horizontally( QLabel("Calibrate the VSI hardware", self), make_button( self, text="Calibrate", icon_name="scales", on_clicked=self._execute, ), (None, 1), ), (None, 1), ) )
def __init__(self, parent: QWidget, commander: Commander): super(HardwareTestControlWidget, self).__init__(parent) self._commander = commander self.setLayout( lay_out_vertically( QLabel( "The motor must be connected in order for the self test to succeed.", self, ), lay_out_horizontally( (None, 1), make_button( self, text="Run self-test", icon_name="play", on_clicked=self._execute, ), (None, 1), ), (None, 1), ))
def __init__(self, parent: QWidget, commander: Commander): super(Widget, self).__init__(parent) self._commander = commander self._event_suppression_depth = 0 self._send_button = make_button( self, text="Execute", icon_name="send-up", tool_tip= "Sends the command to the device; also, while this button is checked (pressed), " "commands will be sent automatically every time the controls are changed by the user.", checkable=True, checked=False, on_clicked=self._on_send_button_changed, ) self._volt_per_hertz_control = SpinboxLinkedWithSlider( self, minimum=0.001, maximum=1.0, step=0.001, slider_orientation=SpinboxLinkedWithSlider.SliderOrientation. HORIZONTAL, ) self._volt_per_hertz_control.spinbox_suffix = " V/Hz" self._volt_per_hertz_control.num_decimals = 3 self._volt_per_hertz_control.tool_tip = "Amplitude, volt per hertz" self._volt_per_hertz_control.status_tip = self._volt_per_hertz_control.tool_tip self._volt_per_hertz_control.slider_visible = False self._target_frequency_control = SpinboxLinkedWithSlider( self, minimum=-9999.0, maximum=9999.0, step=10.0, slider_orientation=SpinboxLinkedWithSlider.SliderOrientation. HORIZONTAL, ) self._target_frequency_control.spinbox_suffix = " Hz" self._target_frequency_control.tool_tip = "Target frequency, hertz" self._target_frequency_control.status_tip = ( self._target_frequency_control.tool_tip) self._frequency_gradient_control = SpinboxLinkedWithSlider( self, minimum=0.0, maximum=999.0, step=1.0, slider_orientation=SpinboxLinkedWithSlider.SliderOrientation. HORIZONTAL, ) self._frequency_gradient_control.spinbox_suffix = " Hz/s" self._frequency_gradient_control.tool_tip = ( "Frequency gradient, hertz per second") self._frequency_gradient_control.status_tip = ( self._frequency_gradient_control.tool_tip) self._frequency_gradient_control.slider_visible = False # Initial values self._volt_per_hertz_control.value = 0.01 self._target_frequency_control.value = 0.0 self._frequency_gradient_control.value = 20.0 # Having configured the values, connecting the events self._volt_per_hertz_control.value_change_event.connect( self._on_any_control_changed) self._target_frequency_control.value_change_event.connect( self._on_any_control_changed) self._frequency_gradient_control.value_change_event.connect( self._on_any_control_changed) self.setLayout( lay_out_horizontally( self._volt_per_hertz_control.spinbox, self._frequency_gradient_control.spinbox, self._target_frequency_control.spinbox, make_button( self, icon_name="clear-symbol", tool_tip="Reset the target frequency to zero", on_clicked=self._on_target_frequency_clear_button_clicked, ), self._target_frequency_control.slider, self._send_button, ))
def __init__( self, parent: typing.Optional[QWidget], on_connection_request: ConnectionRequestCallback, on_disconnection_request: DisconnectionRequestCallback, ): super(DeviceManagementWidget, self).__init__(parent) self.setAttribute( Qt.WA_DeleteOnClose) # This is required to stop background timers! self._port_discoverer = PortDiscoverer() self._port_mapping: typing.Dict[str:str] = {} self._port_combo = QComboBox(self) self._port_combo.setEditable(True) self._port_combo.setInsertPolicy(QComboBox.NoInsert) self._port_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents) self._port_combo.setFont(get_monospace_font()) self._port_combo.lineEdit().returnPressed.connect( self._on_confirmation) self._connect_button = make_button(self, "Connect", "disconnected", on_clicked=self._on_confirmation) self._connect_button.setEnabled( False) # Empty by default, therefore disabled self._port_combo.currentTextChanged.connect( lambda: self._connect_button.setEnabled( bool(self._port_combo.currentText().strip()))) self._status_text = QLabel(self) self._status_text.setText(_STATUS_WHEN_NOT_CONNECTED) self._status_text.setWordWrap(True) self._device_info_widget = LittleBobbyTablesWidget(self) combo_completer = QCompleter() combo_completer.setCaseSensitivity(Qt.CaseInsensitive) combo_completer.setModel(self._port_combo.model()) self._port_combo.setCompleter(combo_completer) self._update_timer = QTimer(self) self._update_timer.timeout.connect(self._update_ports) self._update_timer.start(2000) self._connection_progress_bar = QProgressBar(self) self._connection_progress_bar.setMinimum(0) self._connection_progress_bar.setMaximum(100) self._connection_established = False self._last_task: typing.Optional[asyncio.Task] = None self._connection_request_callback: ConnectionRequestCallback = ( on_connection_request) self._disconnection_request_callback: DisconnectionRequestCallback = ( on_disconnection_request) # Layout self._overlay = QStackedLayout(self) self._init_overlay_widgets() self.setLayout(self._overlay) # Initialization self._update_ports()
def __init__(self, parent: QWidget): super(RegisterViewWidget, self).__init__(parent) self._registers = [] self._running_task: asyncio.Task = None self._visibility_selector = QComboBox(self) self._visibility_selector.addItem("Show all registers", lambda _: True) self._visibility_selector.addItem("Only configuration parameters", lambda r: r.mutable and r.persistent) # noinspection PyUnresolvedReferences self._visibility_selector.currentIndexChanged.connect( lambda _: self._on_visibility_changed()) self._reset_selected_button = make_button( self, "Reset selected", icon_name="clear-symbol", tool_tip=f"Reset the currently selected registers to their default " f"values. The restored values will be committed " f"immediately. This function is available only if a " f"default value is defined. [{RESET_SELECTED_SHORTCUT}]", on_clicked=self._do_reset_selected, ) self._reset_all_button = make_button( self, "Reset all", icon_name="skull-crossbones", tool_tip=f"Reset the all registers to their default " f"values. The restored values will be committed " f"immediately.", on_clicked=self._do_reset_all, ) self._read_selected_button = make_button( self, "Read selected", icon_name="process", tool_tip=f"Read the currently selected registers only " f"[{READ_SELECTED_SHORTCUT}]", on_clicked=self._do_read_selected, ) self._read_all_button = make_button( self, "Read all", icon_name="process-plus", tool_tip="Read all registers from the device", on_clicked=self._do_read_all, ) self._export_button = make_button( self, "Export", icon_name="export", tool_tip="Export configuration parameters", on_clicked=self._do_export, ) self._import_button = make_button( self, "Import", icon_name="import", tool_tip="Import configuration parameters", on_clicked=self._do_import, ) self._expand_all_button = make_button( self, "", icon_name="expand-arrow", tool_tip="Expand all namespaces", on_clicked=lambda: self._tree.expandAll(), ) self._collapse_all_button = make_button( self, "", icon_name="collapse-arrow", tool_tip="Collapse all namespaces", on_clicked=lambda: self._tree.collapseAll(), ) self._status_display = QLabel(self) self._status_display.setWordWrap(True) self._reset_selected_button.setEnabled(False) self._reset_all_button.setEnabled(False) self._read_selected_button.setEnabled(False) self._read_all_button.setEnabled(False) self._export_button.setEnabled(False) self._import_button.setEnabled(False) self._tree = QTreeView(self) self._tree.setVerticalScrollMode(QTreeView.ScrollPerPixel) self._tree.setHorizontalScrollMode(QTreeView.ScrollPerPixel) self._tree.setAnimated(True) self._tree.setSelectionMode(QAbstractItemView.ExtendedSelection) self._tree.setAlternatingRowColors(True) self._tree.setContextMenuPolicy(Qt.ActionsContextMenu) # Not sure about this one. This hardcoded value may look bad on some platforms. self._tree.setIndentation(20) def add_action( callback: typing.Callable[[], None], icon_name: str, name: str, shortcut: typing.Optional[str] = None, ): action = QAction(get_icon(icon_name), name, self) # noinspection PyUnresolvedReferences action.triggered.connect(callback) if shortcut: action.setShortcut(shortcut) action.setAutoRepeat(False) try: action.setShortcutVisibleInContextMenu(True) except AttributeError: pass # This feature is not available in PyQt before 5.10 self._tree.addAction(action) add_action(self._do_read_all, "process-plus", "Read all registers") add_action( self._do_read_selected, "process", "Read selected registers", READ_SELECTED_SHORTCUT, ) add_action( self._do_reset_selected, "clear-symbol", "Reset selected to default", RESET_SELECTED_SHORTCUT, ) self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.VALUE), EditorDelegate(self._tree, self._display_status), ) # It doesn't seem to be explicitly documented, but it seems to be necessary to select either top or bottom # decoration position in order to be able to use center alignment. Left or right positions do not work here. self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.FLAGS), StyleOptionModifyingDelegate( self._tree, decoration_position=QStyleOptionViewItem.Top, # Important decoration_alignment=Qt.AlignCenter, ), ) header: QHeaderView = self._tree.header() header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setStretchLastSection( False) # Horizontal scroll bar doesn't work if this is enabled buttons_layout = QGridLayout() buttons_layout.addWidget(self._read_selected_button, 0, 0) buttons_layout.addWidget(self._reset_selected_button, 0, 2) buttons_layout.addWidget(self._read_all_button, 1, 0) buttons_layout.addWidget(self._reset_all_button, 1, 2) buttons_layout.addWidget(self._import_button, 2, 0) buttons_layout.addWidget(self._export_button, 2, 2) for col in range(3): buttons_layout.setColumnStretch(col, 1) layout = lay_out_vertically( (self._tree, 1), buttons_layout, lay_out_horizontally( self._visibility_selector, (None, 1), self._expand_all_button, self._collapse_all_button, ), self._status_display, ) self.setLayout(layout)
def __init__(self, parent: QWidget, commander: Commander): super(ControlWidget, self).__init__(parent, 'Controls', 'adjust') self._commander: Commander = commander self._last_seen_timestamped_general_status: typing.Optional[ typing.Tuple[float, GeneralStatusView]] = None self._run_widget = RunControlWidget(self, commander) self._motor_identification_widget = MotorIdentificationControlWidget( self, commander) self._hardware_test_widget = HardwareTestControlWidget(self, commander) self._misc_widget = MiscControlWidget(self, commander) self._low_level_manipulation_widget = LowLevelManipulationControlWidget( self, commander) self._panel = QTabWidget(self) self._panel.addTab(self._run_widget, get_icon('running'), 'Run') self._panel.addTab(self._motor_identification_widget, get_icon('caliper'), 'Motor identification') self._panel.addTab(self._hardware_test_widget, get_icon('pass-fail'), 'Self-test') self._panel.addTab(self._misc_widget, get_icon('ellipsis'), 'Miscellaneous') self._panel.addTab(self._low_level_manipulation_widget, get_icon('hand-button'), 'Low-level manipulation') self._current_widget: SpecializedControlWidgetBase = self._hardware_test_widget # Selecting which widget to show by default - let it be Run widget, it's easy to understand and it's first self._panel.setCurrentWidget(self._run_widget) # Shared buttons self._stop_button =\ make_button(self, text='Stop', icon_name='stop', tool_tip=f'Sends a regular stop command which instructs the controller to abandon the current' f'task and activate the Idle task [{STOP_SHORTCUT}]', on_clicked=self._do_regular_stop) self._stop_button.setSizePolicy(QSizePolicy().MinimumExpanding, QSizePolicy().MinimumExpanding) self._emergency_button =\ make_button(self, text='EMERGENCY\nSHUTDOWN', tool_tip=f'Unconditionally disables and locks down the VSI until restarted ' f'[{EMERGENCY_SHORTCUT}]', on_clicked=self._do_emergency_stop) self._emergency_button.setSizePolicy(QSizePolicy().MinimumExpanding, QSizePolicy().MinimumExpanding) small_font = QFont() small_font.setPointSize(round(small_font.pointSize() * 0.8)) self._emergency_button.setFont(small_font) # Observe that the shortcuts are children of the window! This is needed to make them global. self._stop_shortcut = QShortcut(QKeySequence(STOP_SHORTCUT), self.window()) self._stop_shortcut.setAutoRepeat(False) self._emergency_shortcut = QShortcut(QKeySequence(EMERGENCY_SHORTCUT), self.window()) self._emergency_shortcut.setAutoRepeat(False) self.setEnabled(False) # Layout def make_tiny_label(text: str, alignment: int) -> QLabel: lbl = QLabel(text, self) lbl.setAlignment(alignment | Qt.AlignHCenter) font: QFont = lbl.font() font.setPointSize(round(font.pointSize() * 0.7)) lbl.setFont(font) return lbl self.setLayout( lay_out_horizontally( (self._panel, 1), lay_out_vertically( (self._stop_button, 1), make_tiny_label(f'\u2191 {STOP_SHORTCUT} \u2191', Qt.AlignTop), make_tiny_label(f'\u2193 {EMERGENCY_SHORTCUT} \u2193', Qt.AlignBottom), (self._emergency_button, 1), ))) # Configuring the event handler in the last order, because it might fire while we're configuring the widgets! self._panel.currentChanged.connect(self._on_current_widget_changed) # Invoking the handler to complete initialization self._on_current_widget_changed(self._panel.currentIndex())