From b8a05f37d1196583449f06cf333269ffccbf2fac Mon Sep 17 00:00:00 2001 From: Talley Lambert <talley.lambert@gmail.com> Date: Sun, 3 Sep 2023 14:09:20 -0400 Subject: [PATCH] refactor: changing toolbar structure (#281) * changing toolbar structure * Update src/napari_micromanager/_gui_objects/_toolbar.py Co-authored-by: federico gasparoli <70725613+fdrgsp@users.noreply.github.com> * Update src/napari_micromanager/_gui_objects/_toolbar.py Co-authored-by: federico gasparoli <70725613+fdrgsp@users.noreply.github.com> * bump version * layout tweaks * fix width issue --------- Co-authored-by: federico gasparoli <70725613+fdrgsp@users.noreply.github.com> --- pyproject.toml | 2 +- .../_gui_objects/_mda_widget.py | 1 - .../_gui_objects/_toolbar.py | 366 +++++++----------- src/napari_micromanager/main_window.py | 3 +- 4 files changed, 140 insertions(+), 232 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1034c436..19b8991c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "fonticon-materialdesignicons6", "napari >=0.4.13", "pymmcore-plus >=0.8.0", - "pymmcore-widgets >=0.4.1", + "pymmcore-widgets >=0.4.2", "superqt >=0.5.1", "tifffile", "useq-schema >=0.4.1", diff --git a/src/napari_micromanager/_gui_objects/_mda_widget.py b/src/napari_micromanager/_gui_objects/_mda_widget.py index 7fbc41e6..1c535fea 100644 --- a/src/napari_micromanager/_gui_objects/_mda_widget.py +++ b/src/napari_micromanager/_gui_objects/_mda_widget.py @@ -31,7 +31,6 @@ def __init__( self, *, parent: QWidget | None = None, mmcore: CMMCorePlus | None = None ) -> None: super().__init__(include_run_button=True, parent=parent, mmcore=mmcore) - # add save widget v_layout = cast(QVBoxLayout, self._central_widget.layout()) self._save_groupbox = SaveWidget() diff --git a/src/napari_micromanager/_gui_objects/_toolbar.py b/src/napari_micromanager/_gui_objects/_toolbar.py index ed0b9237..bf53d454 100644 --- a/src/napari_micromanager/_gui_objects/_toolbar.py +++ b/src/napari_micromanager/_gui_objects/_toolbar.py @@ -21,7 +21,7 @@ from qtpy.QtCore import QEvent, QObject, QSize, Qt from qtpy.QtWidgets import ( QDockWidget, - QGroupBox, + QFrame, QHBoxLayout, QLabel, QMainWindow, @@ -42,9 +42,7 @@ if TYPE_CHECKING: import napari.viewer -TOOLBAR_SIZE = 45 TOOL_SIZE = 35 -GROUPBOX_STYLE = "QGroupBox { border-radius: 3px; }" # Dict for QObject and its QPushButton icon @@ -87,15 +85,14 @@ def __init__(self, viewer: napari.viewer.Viewer) -> None: self._dock_widgets: dict[str, QDockWidget] = {} # add toolbar items toolbar_items = [ - self._add_cfg(), - self._add_objective(), - self._add_channels(), - self._add_exposure(), - self._add_snap_live_toolbar(), - self._add_tools_toolsbar(), - self._add_plugins_toolbar(), - "", - self._add_shutter_toolbar(), + ConfigToolBar(self), + ChannelsToolBar(self), + ObjectivesToolBar(self), + None, + ShuttersToolBar(self), + SnapLiveToolBar(self), + ExposureToolBar(self), + ToolsToolBar(self), ] for item in toolbar_items: if item: @@ -111,7 +108,6 @@ def _initialize(self) -> None: win := getattr(self.viewer.window, "_qt_window", None) ): return - win = cast(QMainWindow, win) if ( isinstance(dw := self.parent(), QDockWidget) @@ -125,7 +121,7 @@ def _initialize(self) -> None: dw.setVisible(was_visible) # necessary after using removeDockWidget self.removeEventFilter(self) - def eventFilter(self, obj: QObject, event: QEvent) -> bool: + def eventFilter(self, obj: QObject | None, event: QEvent | None) -> bool: """Event filter that ensures that this widget is shown at the top. npe2 plugins don't have a way to specify where they should be added, so this @@ -135,7 +131,8 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool: # the move event is one of the first events that is fired when the widget is # docked, so we use it to re-dock this widget at the top if ( - event.type() == QEvent.Type.Move + event + and event.type() == QEvent.Type.Move and obj is self and not self._is_initialized ): @@ -143,219 +140,6 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool: return False - def _add_cfg(self) -> QToolBar: - """Create a QToolBar with the `ConfigurationWidget`.""" - cfg_toolbar = QToolBar("Configuration", self) - cfg_toolbar.setObjectName("MM-Configuration") - cfg_toolbar.setMinimumHeight(TOOLBAR_SIZE) - cfg_toolbar.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed - ) - cfg_wdg = ConfigurationWidget() - cfg_wdg.setStyleSheet(GROUPBOX_STYLE) - cfg_wdg.setTitle("") - cfg_wdg.layout().setContentsMargins(5, 0, 5, 0) - cfg_toolbar.addWidget(cfg_wdg) - - return cfg_toolbar - - def _create_groupbox(self) -> QGroupBox: - wdg = QGroupBox() - wdg.setLayout(QHBoxLayout()) - wdg.layout().setContentsMargins(5, 0, 5, 0) - wdg.layout().setSpacing(0) - return wdg - - def _add_objective(self) -> QToolBar: - """Create a QToolBar with the `ObjectivesWidget`.""" - obj_toolbar = QToolBar("Objectives", self) - obj_toolbar.setObjectName("MM-Objectives") - obj_toolbar.setMinimumHeight(TOOLBAR_SIZE) - - wdg = self._create_groupbox() - wdg.setStyleSheet(GROUPBOX_STYLE) - # TODO: add this directly to ObjectivesWidget - self.obj_wdg = ObjectivesWidget() - self.obj_wdg.setMinimumWidth(0) - self.obj_wdg._mmc.events.systemConfigurationLoaded.connect(self._resize_obj) - self.obj_wdg._combo.setSizePolicy( - QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed - ) - wdg.layout().addWidget(self.obj_wdg) - obj_toolbar.addWidget(wdg) - - return obj_toolbar - - # TODO: add this directly to ObjectivesWidget - def _resize_obj(self) -> None: - self.obj_wdg._combo.setSizePolicy( - QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed - ) - self.obj_wdg.setMinimumWidth(0) - self.obj_wdg._combo.adjustSize() - - def _add_snap_live_toolbar(self) -> QToolBar: - """Create a QToolBar with the `SnapButton` and `LiveButton`.""" - snap_live_toolbar = QToolBar("Snap Live", self) - snap_live_toolbar.setObjectName("MM-Snap") - snap_live_toolbar.setMinimumHeight(TOOLBAR_SIZE) - - wdg = self._create_groupbox() - wdg.layout().setSpacing(3) - wdg.setStyleSheet("border: 0px;") - - snap_btn = SnapButton() - snap_btn.setText("") - snap_btn.setToolTip("Snap") - snap_btn.setFixedSize(TOOL_SIZE, TOOL_SIZE) - wdg.layout().addWidget(snap_btn) - - live_btn = LiveButton() - live_btn.setText("") - live_btn.setToolTip("Live Mode") - live_btn.button_text_off = "" - live_btn.button_text_on = "" - live_btn.setFixedSize(TOOL_SIZE, TOOL_SIZE) - wdg.layout().addWidget(live_btn) - - snap_live_toolbar.addWidget(wdg) - - return snap_live_toolbar - - def _add_channels(self) -> QToolBar: - """Create a QToolBar with the `ChannelWidget`.""" - ch_toolbar = QToolBar("Channels", self) - ch_toolbar.setObjectName("MM-Channels") - ch_toolbar.setMinimumHeight(TOOLBAR_SIZE) - - wdg = self._create_groupbox() - wdg.layout().setSpacing(5) - wdg.setStyleSheet(GROUPBOX_STYLE) - - ch_lbl = QLabel(text="Channel:") - wdg.layout().addWidget(ch_lbl) - wdg.layout().addWidget(ChannelGroupWidget()) - wdg.layout().addWidget(ChannelWidget()) - - ch_toolbar.addWidget(wdg) - - return ch_toolbar - - def _add_exposure(self) -> QToolBar: - """Create a QToolBar with the `DefaultCameraExposureWidget`.""" - exp_toolbar = QToolBar("Exposure", self) - exp_toolbar.setObjectName("MM-Exposure") - exp_toolbar.setMinimumHeight(TOOLBAR_SIZE) - - wdg = self._create_groupbox() - wdg.setStyleSheet(GROUPBOX_STYLE) - - exposure_wdg = DefaultCameraExposureWidget() - exp_lbl = QLabel(text="Exposure:") - exp_layout = cast(QHBoxLayout, exposure_wdg.layout()) - exp_layout.setContentsMargins(0, 0, 0, 0) - exp_layout.setSpacing(3) - exp_layout.insertWidget(0, exp_lbl) - wdg.layout().addWidget(exposure_wdg) - - exp_toolbar.addWidget(wdg) - - return exp_toolbar - - def _add_shutter_toolbar(self) -> QToolBar: - """Create a QToolBar with the `MMShuttersWidget`.""" - shutters_toolbar = QToolBar("Shutters", self) - shutters_toolbar.setObjectName("MM-Shutters") - shutters_toolbar.setMinimumHeight(TOOLBAR_SIZE) - - wdg = self._create_groupbox() - wdg.layout().setSpacing(3) - wdg.setStyleSheet("border: 0px;") - - shutter_wdg = MMShuttersWidget() - shutter_wdg.setMinimumHeight(TOOL_SIZE) - wdg.layout().addWidget(shutter_wdg) - - shutters_toolbar.addWidget(wdg) - - return shutters_toolbar - - def _add_tools_toolsbar(self) -> QToolBar: - """Add a QToolBar containing QPushButtons for pymmcore-widgets. - - e.g. Property Browser, GroupPresetTableWidget, ... - - QPushButtons are connected to the `_show_dock_widget` method. - - The QPushButton.whatsThis() property is used to store the key that - will be used by the `_show_dock_widget` method. - """ - tools_toolbar = QToolBar("Tools", self) - tools_toolbar.setObjectName("MM-Tools") - tools_toolbar.setMinimumHeight(TOOLBAR_SIZE) - - wdg = self._create_groupbox() - wdg.layout().setSpacing(3) - wdg.setStyleSheet("border: 0px;") - - for key in DOCK_WIDGETS: - btn_icon = DOCK_WIDGETS[key][1] - if btn_icon is None: - continue - btn = self._make_tool_button(key, btn_icon) - btn.setWhatsThis(key) - btn.clicked.connect(self._show_dock_widget) - wdg.layout().addWidget(btn) - - tools_toolbar.addWidget(wdg) - - return tools_toolbar - - def _make_tool_button(self, tooltip: str, btn_icon: str) -> QPushButton: - """Create the QPushbutton for the tools QToolBar.""" - btn = QPushButton() - btn.setToolTip(tooltip) - btn.setFixedSize(TOOL_SIZE, TOOL_SIZE) - btn.setIcon(icon(btn_icon, color=(0, 255, 0))) - btn.setIconSize(QSize(30, 30)) - return btn - - def _add_plugins_toolbar(self) -> QToolBar: - """Add a QToolBar containing plugins QPushButtons. - - e.g. MDA, ... - - QPushButtons are connected to the `_show_dock_widget` method. - - The QPushButton.whatsThis() property is used to store the key that - will be used by the `_show_dock_widget` method. - """ - plgs_toolbar = QToolBar("Plugins") - plgs_toolbar.setObjectName("MM-Plugins") - plgs_toolbar.setMinimumHeight(TOOLBAR_SIZE) - - wdg = QGroupBox() - wdg.setLayout(QHBoxLayout()) - wdg.layout().setContentsMargins(5, 0, 5, 0) - wdg.layout().setSpacing(3) - wdg.setStyleSheet("border: 0px;") - - mda = self._make_plugin_button("MDA", "MultiDimensional Acquisition") - wdg.layout().addWidget(mda) - - plgs_toolbar.addWidget(wdg) - - return plgs_toolbar - - def _make_plugin_button(self, btn_text: str, tooltip: str) -> QPushButton: - """Create the QPushButton for the plugins QToolBar.""" - btn = QPushButton(text=btn_text) - btn.setToolTip(tooltip) - btn.setMinimumHeight(TOOL_SIZE) - btn.setWhatsThis(btn_text) - btn.clicked.connect(self._show_dock_widget) - return btn - def _show_dock_widget(self, key: str = "") -> None: """Look up widget class in DOCK_WIDGETS and add/create or show/raise. @@ -396,7 +180,6 @@ def _show_dock_widget(self, key: str = "") -> None: ) floating = True tabify = False - dock_wdg = self._add_dock_widget(wdg, key, floating=floating, tabify=tabify) self._dock_widgets[key] = dock_wdg @@ -410,7 +193,132 @@ def _add_dock_widget( area="right", tabify=tabify, ) + # fix napari bug that makes dock widgets too large + with contextlib.suppress(AttributeError): + self.viewer.window._qt_window.resizeDocks( + [dock_wdg], [widget.sizeHint().width() + 20], Qt.Orientation.Horizontal + ) with contextlib.suppress(AttributeError): dock_wdg._close_btn = False dock_wdg.setFloating(floating) return dock_wdg + + +# -------------- Toolbars -------------------- + + +class MMToolBar(QToolBar): + def __init__(self, title: str, parent: QWidget = None) -> None: + super().__init__(title, parent) + self.setMinimumHeight(48) + self.setObjectName(f"MM-{title}") + + self.frame = QFrame() + gb_layout = QHBoxLayout(self.frame) + gb_layout.setContentsMargins(0, 0, 0, 0) + gb_layout.setSpacing(2) + self.addWidget(self.frame) + + def addSubWidget(self, wdg: QWidget) -> None: + cast("QHBoxLayout", self.frame.layout()).addWidget(wdg) + + +class ConfigToolBar(MMToolBar): + def __init__(self, parent: QWidget) -> None: + super().__init__("Configuration", parent) + self.addSubWidget(ConfigurationWidget()) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + +class ObjectivesToolBar(MMToolBar): + def __init__(self, parent: QWidget) -> None: + super().__init__("Objectives", parent=parent) + self._wdg = ObjectivesWidget() + self._wdg._mmc.events.systemConfigurationLoaded.connect(self._resize_obj) + self.addSubWidget(self._wdg) + self._resize_obj() + + def _resize_obj(self) -> None: + self._wdg._combo.setSizePolicy( + QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed + ) + self._wdg.setMinimumWidth(0) + self._wdg._combo.adjustSize() + + +class ChannelsToolBar(MMToolBar): + def __init__(self, parent: QWidget) -> None: + super().__init__("Channels", parent) + self.addSubWidget(QLabel(text="Channel:")) + self.addSubWidget(ChannelGroupWidget()) + self.addSubWidget(ChannelWidget()) + + +class ExposureToolBar(MMToolBar): + def __init__(self, parent: QWidget) -> None: + super().__init__("Exposure", parent) + self.addSubWidget(QLabel(text="Exposure:")) + self.addSubWidget(DefaultCameraExposureWidget()) + + +class SnapLiveToolBar(MMToolBar): + def __init__(self, parent: QWidget) -> None: + super().__init__("Snap Live", parent) + snap_btn = SnapButton() + snap_btn.setText("") + snap_btn.setToolTip("Snap") + snap_btn.setFixedSize(TOOL_SIZE, TOOL_SIZE) + self.addSubWidget(snap_btn) + + live_btn = LiveButton() + live_btn.setText("") + live_btn.setToolTip("Live Mode") + live_btn.button_text_off = "" + live_btn.button_text_on = "" + live_btn.setFixedSize(TOOL_SIZE, TOOL_SIZE) + self.addSubWidget(live_btn) + + +class ToolsToolBar(MMToolBar): + """A QToolBar containing QPushButtons for pymmcore-widgets. + + e.g. Property Browser, GroupPresetTableWidget, ... + + QPushButtons are connected to the `_show_dock_widget` method. + + The QPushButton.whatsThis() property is used to store the key that + will be used by the `_show_dock_widget` method. + """ + + def __init__(self, parent: MicroManagerToolbar) -> None: + super().__init__("Tools", parent) + + if not isinstance(parent, MicroManagerToolbar): + raise TypeError("parent must be a MicroManagerToolbar instance.") + + for key in DOCK_WIDGETS: + btn_icon = DOCK_WIDGETS[key][1] + if btn_icon is None: + continue + + btn = QPushButton() + btn.setToolTip(key) + btn.setFixedSize(TOOL_SIZE, TOOL_SIZE) + btn.setIcon(icon(btn_icon, color=(0, 255, 0))) + btn.setIconSize(QSize(30, 30)) + btn.setWhatsThis(key) + btn.clicked.connect(parent._show_dock_widget) + self.addSubWidget(btn) + + btn = QPushButton("MDA") + btn.setToolTip("MultiDimensional Acquisition") + btn.setFixedSize(TOOL_SIZE, TOOL_SIZE) + btn.setWhatsThis("MDA") + btn.clicked.connect(parent._show_dock_widget) + self.addSubWidget(btn) + + +class ShuttersToolBar(MMToolBar): + def __init__(self, parent: QWidget) -> None: + super().__init__("Shutters", parent) + self.addSubWidget(MMShuttersWidget()) diff --git a/src/napari_micromanager/main_window.py b/src/napari_micromanager/main_window.py index 39665c30..02f40aba 100644 --- a/src/napari_micromanager/main_window.py +++ b/src/napari_micromanager/main_window.py @@ -56,7 +56,8 @@ def __init__(self, viewer: napari.viewer.Viewer) -> None: signal.connect(slot) # add minmax dockwidget - self.viewer.window.add_dock_widget(self.minmax, name="MinMax", area="left") + if "MinMax" not in getattr(self.viewer.window, "_dock_widgets", []): + self.viewer.window.add_dock_widget(self.minmax, name="MinMax", area="left") # queue cleanup self.destroyed.connect(self._cleanup)