From 3aa2f2225fba499b648d191ea27553b6db303c18 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Wed, 19 Feb 2025 17:30:49 +0100 Subject: [PATCH] fix(plot_base): ability to choose between popup or side panel gui mode --- bec_widgets/qt_utils/settings_dialog.py | 13 +- .../widgets/plots_next_gen/plot_base.py | 203 ++++++++++--- .../setting_menus/axis_settings.py | 63 +++- .../setting_menus/axis_settings_horizontal.ui | 270 ++++++++---------- 4 files changed, 363 insertions(+), 186 deletions(-) diff --git a/bec_widgets/qt_utils/settings_dialog.py b/bec_widgets/qt_utils/settings_dialog.py index 38a220f0..904db655 100644 --- a/bec_widgets/qt_utils/settings_dialog.py +++ b/bec_widgets/qt_utils/settings_dialog.py @@ -1,6 +1,6 @@ from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget -from bec_widgets.qt_utils.error_popups import SafeSlot as Slot +from bec_widgets.qt_utils.error_popups import SafeSlot class SettingWidget(QWidget): @@ -20,14 +20,14 @@ class SettingWidget(QWidget): def set_target_widget(self, target_widget: QWidget): self.target_widget = target_widget - @Slot() + @SafeSlot() def accept_changes(self): """ Accepts the changes made in the settings widget and applies them to the target widget. """ pass - @Slot(dict) + @SafeSlot(dict) def display_current_settings(self, config_dict: dict): """ Displays the current settings of the target widget in the settings widget. @@ -54,12 +54,13 @@ class SettingsDialog(QDialog): settings_widget: SettingWidget = None, window_title: str = "Settings", config: dict = None, + modal: bool = False, *args, **kwargs, ): super().__init__(parent, *args, **kwargs) - self.setModal(False) + self.setModal(modal) self.setWindowTitle(window_title) @@ -92,7 +93,7 @@ class SettingsDialog(QDialog): ok_button.setDefault(True) ok_button.setAutoDefault(True) - @Slot() + @SafeSlot() def accept(self): """ Accept the changes made in the settings widget and close the dialog. @@ -100,7 +101,7 @@ class SettingsDialog(QDialog): self.widget.accept_changes() super().accept() - @Slot() + @SafeSlot() def apply_changes(self): """ Apply the changes made in the settings widget without closing the dialog. diff --git a/bec_widgets/widgets/plots_next_gen/plot_base.py b/bec_widgets/widgets/plots_next_gen/plot_base.py index 6d720d0e..326d6d1a 100644 --- a/bec_widgets/widgets/plots_next_gen/plot_base.py +++ b/bec_widgets/widgets/plots_next_gen/plot_base.py @@ -1,14 +1,18 @@ from __future__ import annotations +from enum import Enum + +import numpy as np import pyqtgraph as pg from bec_lib import bec_logger from qtpy.QtCore import QPoint, QPointF, Qt, Signal -from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget +from qtpy.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot from bec_widgets.qt_utils.round_frame import RoundedFrame +from bec_widgets.qt_utils.settings_dialog import SettingsDialog from bec_widgets.qt_utils.side_panel import SidePanel -from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, SeparatorAction +from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, ToolbarBundle from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.fps_counter import FPSCounter @@ -20,7 +24,6 @@ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions impor ) from bec_widgets.widgets.plots_next_gen.toolbar_bundles.plot_export import PlotExportBundle from bec_widgets.widgets.plots_next_gen.toolbar_bundles.roi_bundle import ROIBundle -from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton logger = bec_logger.logger @@ -42,6 +45,12 @@ class BECViewBox(pg.ViewBox): self.update() +class UIMode(Enum): + NONE = 0 + POPUP = 1 + SIDE = 2 + + class PlotBase(BECWidget, QWidget): PLUGIN = False RPC = False @@ -59,6 +68,7 @@ class PlotBase(BECWidget, QWidget): config: ConnectionConfig | None = None, client=None, gui_id: str | None = None, + popups: bool = False, ) -> None: if config is None: config = ConnectionConfig(widget_class=self.__class__.__name__) @@ -74,6 +84,8 @@ class PlotBase(BECWidget, QWidget): self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) self.layout_manager = LayoutManagerWidget(parent=self) + self.layout_manager.layout.setContentsMargins(0, 0, 0, 0) + self.layout_manager.layout.setSpacing(0) # Property Manager self.state_manager = WidgetStateManager(self) @@ -82,12 +94,14 @@ class PlotBase(BECWidget, QWidget): self.entry_validator = EntryValidator(self.dev) # Base widgets elements + self._ui_mode = UIMode.POPUP if popups else UIMode.SIDE + self.axis_settings_dialog = None self.plot_widget = pg.GraphicsLayoutWidget(parent=self) self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True)) self.plot_widget.addItem(self.plot_item) self.side_panel = SidePanel(self, orientation="left", panel_max_width=280) self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal") - self.init_toolbar() + self._init_toolbar() # PlotItem Addons self.plot_item.addLegend() @@ -115,13 +129,14 @@ class PlotBase(BECWidget, QWidget): self.layout_manager.add_widget_relative(self.side_panel, self.round_plot_widget, "left") self.layout_manager.add_widget_relative(self.toolbar, self.fps_label, "top") - self.add_side_menus() + self.ui_mode = self._ui_mode # to initiate the first time # PlotItem ViewBox Signals self.plot_item.vb.sigStateChanged.connect(self.viewbox_state_changed) - def init_toolbar(self): - + def _init_toolbar(self): + self.popup_bundle = None + self.performance_bundle = ToolbarBundle("performance") self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self) self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self) # self.state_export_bundle = SaveStateBundle("state_export", target_widget=self) #TODO ATM disabled, cannot be used in DockArea, which is exposed to the user @@ -133,33 +148,130 @@ class PlotBase(BECWidget, QWidget): self.toolbar.add_bundle(self.mouse_bundle, target_widget=self) self.toolbar.add_bundle(self.roi_bundle, target_widget=self) - self.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self) - self.toolbar.add_action( + self.performance_bundle.add_action( "fps_monitor", - MaterialIconAction(icon_name="speed", tooltip="Show FPS Monitor", checkable=True), - target_widget=self, + MaterialIconAction( + icon_name="speed", tooltip="Show FPS Monitor", checkable=True, parent=self + ), ) - self.toolbar.addWidget(DarkModeButton(toolbar=True)) + self.toolbar.add_bundle(self.performance_bundle, target_widget=self) self.toolbar.widgets["fps_monitor"].action.toggled.connect( lambda checked: setattr(self, "enable_fps_monitor", checked) ) + # hide some options by default + self.toolbar.toggle_action_visibility("fps_monitor", False) + def add_side_menus(self): """Adds multiple menus to the side panel.""" # Setting Axis Widget - axis_setting = AxisSettings(target_widget=self) - self.side_panel.add_menu( - action_id="axis", - icon_name="settings", - tooltip="Show Axis Settings", - widget=axis_setting, - title="Axis Settings", + try: + axis_setting = AxisSettings(target_widget=self) + self.side_panel.add_menu( + action_id="axis", + icon_name="settings", + tooltip="Show Axis Settings", + widget=axis_setting, + title="Axis Settings", + ) + except ValueError: + return + + def add_popups(self): + """ + Add popups to the toolbar. + """ + self.popup_bundle = ToolbarBundle("popup_bundle") + settings = MaterialIconAction( + icon_name="settings", tooltip="Show Axis Settings", checkable=True, parent=self ) + self.popup_bundle.add_action("axis", settings) + self.toolbar.add_bundle(self.popup_bundle, target_widget=self) + self.toolbar.widgets["axis"].action.triggered.connect(self.show_axis_settings_popup) + + def show_axis_settings_popup(self): + """ + Show the axis settings dialog. + """ + settings_action = self.toolbar.widgets["axis"].action + if self.axis_settings_dialog is None or not self.axis_settings_dialog.isVisible(): + axis_setting = AxisSettings(target_widget=self, popup=True) + self.axis_settings_dialog = SettingsDialog( + self, settings_widget=axis_setting, window_title="Axis Settings", modal=False + ) + # When the dialog is closed, update the toolbar icon and clear the reference + self.axis_settings_dialog.finished.connect(self._axis_settings_closed) + self.axis_settings_dialog.show() + settings_action.setChecked(True) + else: + # If already open, bring it to the front + self.axis_settings_dialog.raise_() + self.axis_settings_dialog.activateWindow() + settings_action.setChecked(True) # keep it toggled + + def _axis_settings_closed(self): + """ + Slot for when the axis settings dialog is closed. + """ + self.axis_settings_dialog = None + self.toolbar.widgets["axis"].action.setChecked(False) ################################################################################ # Toggle UI Elements ################################################################################ + @property + def ui_mode(self) -> UIMode: + return self._ui_mode + + @ui_mode.setter + def ui_mode(self, mode: UIMode): + if not isinstance(mode, UIMode): + raise ValueError("ui_mode must be an instance of UIMode") + self._ui_mode = mode + + # First, clear both UI elements: + if self.popup_bundle is not None: + for action_id in self.toolbar.bundles["popup_bundle"]: + self.toolbar.widgets[action_id].action.setVisible(False) + if self.axis_settings_dialog is not None and self.axis_settings_dialog.isVisible(): + self.axis_settings_dialog.close() + self.side_panel.hide() + + # Now, apply the new mode: + if mode == UIMode.POPUP: + if self.popup_bundle is None: + self.add_popups() + else: + for action_id in self.toolbar.bundles["popup_bundle"]: + self.toolbar.widgets[action_id].action.setVisible(True) + elif mode == UIMode.SIDE: + self.add_side_menus() + self.side_panel.show() + + @SafeProperty(bool, doc="Enable popups setting dialogs for the plot widget.") + def enable_popups(self): + return self.ui_mode == UIMode.POPUP + + @enable_popups.setter + def enable_popups(self, value: bool): + if value: + self.ui_mode = UIMode.POPUP + else: + if self.ui_mode == UIMode.POPUP: + self.ui_mode = UIMode.NONE + + @SafeProperty(bool, doc="Show Side Panel") + def enable_side_panel(self) -> bool: + return self.ui_mode == UIMode.SIDE + + @enable_side_panel.setter + def enable_side_panel(self, value: bool): + if value: + self.ui_mode = UIMode.SIDE + else: + if self.ui_mode == UIMode.SIDE: + self.ui_mode = UIMode.NONE @SafeProperty(bool, doc="Show Toolbar") def enable_toolbar(self) -> bool: @@ -167,15 +279,22 @@ class PlotBase(BECWidget, QWidget): @enable_toolbar.setter def enable_toolbar(self, value: bool): - self.toolbar.setVisible(value) - - @SafeProperty(bool, doc="Show Side Panel") - def enable_side_panel(self) -> bool: - return self.side_panel.isVisible() - - @enable_side_panel.setter - def enable_side_panel(self, value: bool): - self.side_panel.setVisible(value) + if value: + # Disable popup mode + if self._popups: + # Directly update the internal flag to avoid recursion + self._popups = False + # Hide the popup bundle if it exists and close any open dialogs + if self.popup_bundle is not None: + for action in self.toolbar.bundles["popup_bundle"].actions: + action.setVisible(False) + if self.axis_settings_dialog is not None and self.axis_settings_dialog.isVisible(): + self.axis_settings_dialog.close() + self.side_panel.show() + # Add side menus if not already added + self.add_side_menus() + else: + self.side_panel.hide() @SafeProperty(bool, doc="Enable the FPS monitor.") def enable_fps_monitor(self) -> bool: @@ -591,16 +710,34 @@ class PlotBase(BECWidget, QWidget): item.ctrlMenu.deleteLater() +class DemoPlotBase(QMainWindow): # pragma: no cover: + def __init__(self): + super().__init__() + self.main_widget = QWidget() + self.setCentralWidget(self.main_widget) + self.main_widget.layout = QHBoxLayout(self.main_widget) + + self.plot_popup = PlotBase(popups=True) + self.plot_popup.title = "PlotBase with popups" + self.plot_side_panels = PlotBase(popups=False) + self.plot_side_panels.title = "PlotBase with side panels" + + self.plot_popup.plot_item.plot(np.random.rand(100), pen=(255, 0, 0)) + self.plot_side_panels.plot_item.plot(np.random.rand(100), pen=(0, 255, 0)) + + self.main_widget.layout.addWidget(self.plot_side_panels) + self.main_widget.layout.addWidget(self.plot_popup) + + self.resize(1400, 600) + + if __name__ == "__main__": # pragma: no cover: import sys from qtpy.QtWidgets import QApplication app = QApplication(sys.argv) - widget = PlotBase() - widget.show() - # Just some example data and parameters to test - widget.y_grid = True - widget.plot_item.plot([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]) + window = DemoPlotBase() + window.show() sys.exit(app.exec_()) diff --git a/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py b/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py index 6fe2cb84..3cdec0d1 100644 --- a/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py +++ b/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings.py @@ -9,7 +9,7 @@ from bec_widgets.utils.widget_io import WidgetIO class AxisSettings(SettingWidget): - def __init__(self, parent=None, target_widget=None, *args, **kwargs): + def __init__(self, parent=None, target_widget=None, popup=False, *args, **kwargs): super().__init__(parent=parent, *args, **kwargs) # This is a settings widget that depends on the target widget @@ -18,9 +18,15 @@ class AxisSettings(SettingWidget): self.setProperty("skip_settings", True) self.setObjectName("AxisSettings") current_path = os.path.dirname(__file__) - form = UILoader().load_ui(os.path.join(current_path, "axis_settings_vertical.ui"), self) + if popup: + form = UILoader().load_ui( + os.path.join(current_path, "axis_settings_horizontal.ui"), self + ) + else: + form = UILoader().load_ui(os.path.join(current_path, "axis_settings_vertical.ui"), self) self.target_widget = target_widget + self.popup = popup # # Scroll area self.scroll_area = QScrollArea(self) @@ -34,10 +40,13 @@ class AxisSettings(SettingWidget): # self.layout.addWidget(self.ui) self.ui = form - self.connect_all_signals() - if self.target_widget is not None: + if self.target_widget is not None and self.popup is False: + self.connect_all_signals() self.target_widget.property_changed.connect(self.update_property) + if self.popup is True: + self.fetch_all_properties() + def connect_all_signals(self): for widget in [ self.ui.title, @@ -93,3 +102,49 @@ class AxisSettings(SettingWidget): was_blocked = widget_to_set.blockSignals(True) WidgetIO.set_value(widget_to_set, value) widget_to_set.blockSignals(was_blocked) + + def fetch_all_properties(self): + """ + Fetch all properties from the target widget and update the settings widget. + """ + for widget in [ + self.ui.title, + self.ui.inner_axes, + self.ui.outer_axes, + self.ui.x_label, + self.ui.x_min, + self.ui.x_max, + self.ui.x_log, + self.ui.x_grid, + self.ui.y_label, + self.ui.y_min, + self.ui.y_max, + self.ui.y_log, + self.ui.y_grid, + ]: + property_name = widget.objectName() + value = getattr(self.target_widget, property_name) + WidgetIO.set_value(widget, value) + + def accept_changes(self): + """ + Apply all properties from the settings widget to the target widget. + """ + for widget in [ + self.ui.title, + self.ui.inner_axes, + self.ui.outer_axes, + self.ui.x_label, + self.ui.x_min, + self.ui.x_max, + self.ui.x_log, + self.ui.x_grid, + self.ui.y_label, + self.ui.y_min, + self.ui.y_max, + self.ui.y_log, + self.ui.y_grid, + ]: + property_name = widget.objectName() + value = WidgetIO.get_value(widget) + setattr(self.target_widget, property_name, value) diff --git a/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui b/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui index dae3a82a..323f99c7 100644 --- a/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui +++ b/bec_widgets/widgets/plots_next_gen/setting_menus/axis_settings_horizontal.ui @@ -6,67 +6,131 @@ 0 0 - 427 - 270 + 486 + 300 - - - 0 - 250 - - - - - 16777215 - 278 - - Form - - - - - - Plot Title - - - - - - - - + + + Inner Axes + + + + + + + Outer Axes - + + + + false + + + + + + + X Axis + + + + + + Grid + + + + + + + false + + + + + + + Qt::AlignmentFlag::AlignCenter + + + -9999.000000000000000 + + + 9999.000000000000000 + + + + + + + Max + + + + + + + false + + + + + + + Qt::AlignmentFlag::AlignCenter + + + -9999.000000000000000 + + + 9999.000000000000000 + + + + + + + Log + + + + + + + Min + + + + + + + Label + + + + + + + + + + Y Axis - - - - - linear - - - - - log - - - - @@ -106,7 +170,7 @@ - Scale + Log @@ -124,13 +188,6 @@ - - - - - - - @@ -138,109 +195,36 @@ - - - - - - - X Axis - - - - - - Scale - - - - - - - Qt::AlignmentFlag::AlignCenter - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - Min - - - - - - - Qt::AlignmentFlag::AlignCenter - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - linear - - - - - log - - - - - - - - Max - - - - - - - - - - Label + + + false - - - - - - - - - - Grid + + + false - - - - false - - + + + + + + Plot Title + + + + + + +