From 1cda2b70d289e62ec1863ed17910a71e1cb751c4 Mon Sep 17 00:00:00 2001 From: federico gasparoli <70725613+fdrgsp@users.noreply.github.com> Date: Tue, 12 Nov 2024 07:16:02 -0500 Subject: [PATCH] feat: use QRadioButtons to select z_plan (#385) * feat: use radiobuttons * tweak * fix: fix setMode --------- Co-authored-by: Talley Lambert --- src/pymmcore_widgets/mda/_core_z.py | 2 +- src/pymmcore_widgets/useq_widgets/_z.py | 117 +++++++++++------------- tests/useq_widgets/test_useq_widgets.py | 7 +- 3 files changed, 61 insertions(+), 65 deletions(-) diff --git a/src/pymmcore_widgets/mda/_core_z.py b/src/pymmcore_widgets/mda/_core_z.py index 97f529f56..b2e988b20 100644 --- a/src/pymmcore_widgets/mda/_core_z.py +++ b/src/pymmcore_widgets/mda/_core_z.py @@ -51,7 +51,7 @@ def __init__( def setMode( self, - mode: Mode | Literal["top_bottom", "range_around", "above_below"] | None = None, + mode: Mode | Literal["top_bottom", "range_around", "above_below"], ) -> None: super().setMode(mode) self.bottom_btn.setVisible(self._mode == Mode.TOP_BOTTOM) diff --git a/src/pymmcore_widgets/useq_widgets/_z.py b/src/pymmcore_widgets/useq_widgets/_z.py index b0bb85c6d..40c872401 100644 --- a/src/pymmcore_widgets/useq_widgets/_z.py +++ b/src/pymmcore_widgets/useq_widgets/_z.py @@ -1,12 +1,13 @@ from __future__ import annotations import enum -from typing import TYPE_CHECKING, Final, Literal, cast +from typing import Final, Literal import useq from fonticon_mdi6 import MDI6 from qtpy.QtCore import Qt, Signal from qtpy.QtWidgets import ( + QAbstractButton, QButtonGroup, QDoubleSpinBox, QGridLayout, @@ -15,7 +16,6 @@ QPushButton, QRadioButton, QSpinBox, - QToolButton, QVBoxLayout, QWidget, ) @@ -24,11 +24,6 @@ from pymmcore_widgets._util import SeparatorWidget -if TYPE_CHECKING: - from PyQt6.QtGui import QAction, QActionGroup -else: - from qtpy.QtGui import QAction, QActionGroup - class Mode(enum.Enum): """Recognized ZPlanWidget modes.""" @@ -67,52 +62,44 @@ def __init__(self, parent: QWidget | None = None) -> None: # to store a "suggested" step size self._suggested: float | None = None - # #################### Mode Buttons #################### - - # ------------------- actions ---------- + self._mode: Mode = Mode.TOP_BOTTOM - self._mode_top_bot = QAction( - icon(MDI6.arrow_expand_vertical, scale_factor=1), "Mark top and bottom." - ) - self._mode_top_bot.setCheckable(True) - self._mode_top_bot.setData(Mode.TOP_BOTTOM) - self._mode_top_bot.triggered.connect(self.setMode) + # #################### Mode Buttons #################### - self._mode_range = QAction( - icon(MDI6.arrow_split_horizontal, scale_factor=1), - "Range symmetric around reference.", + self._btn_top_bot = QRadioButton("Top/Bottom") + self._btn_top_bot.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self._btn_top_bot.setIcon(icon(MDI6.arrow_expand_vertical)) + self._btn_top_bot.setToolTip("Mark top and bottom.") + self._btn_range = QRadioButton("Range Around (Symmetric)") + self._btn_range.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self._btn_range.setIcon(icon(MDI6.arrow_split_horizontal)) + self._btn_range.setToolTip("Range symmetric around reference.") + self._button_above_below = QRadioButton("Range Asymmetric") + self._button_above_below.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self._button_above_below.setIcon(icon(MDI6.arrow_expand_up)) + self._button_above_below.setToolTip( + "Range asymmetrically above/below reference." ) - self._mode_range.setCheckable(True) - self._mode_range.setData(Mode.RANGE_AROUND) - self._mode_range.triggered.connect(self.setMode) - self._mode_above_below = QAction( - icon(MDI6.arrow_expand_up, scale_factor=1), - "Range asymmetrically above/below reference.", - ) - self._mode_above_below.setCheckable(True) - self._mode_above_below.setData(Mode.ABOVE_BELOW) - self._mode_above_below.triggered.connect(self.setMode) - - self._mode_group = QActionGroup(self) - self._mode_group.addAction(self._mode_top_bot) - self._mode_group.addAction(self._mode_range) - self._mode_group.addAction(self._mode_above_below) - - # ------------------- - - btn_top_bot = QToolButton() - btn_top_bot.setDefaultAction(self._mode_top_bot) - btn_range = QToolButton() - btn_range.setDefaultAction(self._mode_range) - button_above_below = QToolButton() - button_above_below.setDefaultAction(self._mode_above_below) - - btn_layout = QHBoxLayout() - btn_layout.addWidget(btn_top_bot) - btn_layout.addWidget(btn_range) - btn_layout.addWidget(button_above_below) - btn_layout.addStretch() + self._mode_btn_group = QButtonGroup() + self._mode_btn_group.addButton(self._btn_top_bot) + self._mode_btn_group.addButton(self._btn_range) + self._mode_btn_group.addButton(self._button_above_below) + self._mode_btn_group.buttonToggled.connect(self.setMode) + + # radio buttons on the top row + btn_wdg = QWidget() + btn_layout = QHBoxLayout(btn_wdg) + btn_layout.setContentsMargins(0, 0, 0, 0) + btn_layout.addWidget(self._btn_top_bot, 0) + btn_layout.addWidget(self._btn_range, 0) + btn_layout.addWidget(self._button_above_below, 1) + + # FIXME: On Windows 11, buttons within an inner widget of a ScrollArea + # are filled in with the accent color, making it very difficult to see + # which radio button is checked. This HACK solves the issue. It's + # likely future Qt versions will fix this. + btn_wdg.setStyleSheet("QRadioButton {color: none}") # #################### Value Widgets #################### @@ -248,7 +235,6 @@ def __init__(self, parent: QWidget | None = None) -> None: left_half = QVBoxLayout() left_half.addWidget(self._range_readout) - # left_half.addWidget(self.leave_shutter_open) right_half = QVBoxLayout() right_half.addWidget(self._bottom_to_top) @@ -261,7 +247,7 @@ def __init__(self, parent: QWidget | None = None) -> None: below_grid.addLayout(right_half) layout = QVBoxLayout(self) - layout.addLayout(btn_layout) + layout.addWidget(btn_wdg) layout.addWidget(SeparatorWidget()) layout.addLayout(self._grid_layout) layout.addStretch() @@ -270,13 +256,12 @@ def __init__(self, parent: QWidget | None = None) -> None: # #################### Defaults #################### self.setMode(Mode.TOP_BOTTOM) - # self.setSuggestedStep(1) # ------------------------- Public API ------------------------- def setMode( self, - mode: Mode | Literal["top_bottom", "range_around", "above_below", None] = None, + mode: Mode | Literal["top_bottom", "range_around", "above_below"], ) -> None: """Set the current mode. @@ -288,27 +273,35 @@ def setMode( The mode to set. By default, None. If None, the mode is determined by the sender().data(), for internal usage. """ - if isinstance(mode, str): - mode = Mode(mode) - elif isinstance(mode, (bool, type(None))): - mode = cast("QAction", self.sender()).data() - - self._mode = cast(Mode, mode) + if isinstance(mode, QRadioButton): + btn_map: dict[QAbstractButton, Mode] = { + self._btn_top_bot: Mode.TOP_BOTTOM, + self._btn_range: Mode.RANGE_AROUND, + self._button_above_below: Mode.ABOVE_BELOW, + } + self._mode = btn_map[mode] + elif isinstance(mode, str): + self._mode = Mode(mode) + else: + self._mode = mode if self._mode is Mode.TOP_BOTTOM: - self._mode_top_bot.setChecked(True) + with signals_blocked(self._mode_btn_group): + self._btn_top_bot.setChecked(True) self._set_row_visible(ROW_RANGE_AROUND, False) self._set_row_visible(ROW_ABOVE_BELOW, False) self._set_row_visible(ROW_TOP_BOTTOM, True) elif self._mode is Mode.RANGE_AROUND: - self._mode_range.setChecked(True) + with signals_blocked(self._mode_btn_group): + self._btn_range.setChecked(True) self._set_row_visible(ROW_TOP_BOTTOM, False) self._set_row_visible(ROW_ABOVE_BELOW, False) self._set_row_visible(ROW_RANGE_AROUND, True) elif self._mode is Mode.ABOVE_BELOW: - self._mode_above_below.setChecked(True) + with signals_blocked(self._mode_btn_group): + self._button_above_below.setChecked(True) self._set_row_visible(ROW_RANGE_AROUND, False) self._set_row_visible(ROW_TOP_BOTTOM, False) self._set_row_visible(ROW_ABOVE_BELOW, True) diff --git a/tests/useq_widgets/test_useq_widgets.py b/tests/useq_widgets/test_useq_widgets.py index bd93a9620..fd704ff9c 100644 --- a/tests/useq_widgets/test_useq_widgets.py +++ b/tests/useq_widgets/test_useq_widgets.py @@ -358,12 +358,15 @@ def test_z_plan_widget(qtbot: QtBot) -> None: assert wdg.mode() == _z.Mode.TOP_BOTTOM assert wdg.top.isVisible() assert not wdg.above.isVisible() - wdg._mode_range.trigger() + assert wdg._btn_top_bot.isChecked() + wdg.setMode(_z.Mode.RANGE_AROUND) assert wdg.range.isVisible() assert not wdg.top.isVisible() - wdg._mode_above_below.trigger() + assert wdg._btn_range.isChecked() + wdg.setMode(_z.Mode.ABOVE_BELOW) assert wdg.above.isVisible() assert not wdg.range.isVisible() + assert wdg._button_above_below.isChecked() assert wdg.step.value() == 1 wdg.setSuggestedStep(0.5)