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
+
+
+
+ -
+
+
+