diff --git a/bec_widgets/applications/launch_window.py b/bec_widgets/applications/launch_window.py index cc8274fd..8b5f7efc 100644 --- a/bec_widgets/applications/launch_window.py +++ b/bec_widgets/applications/launch_window.py @@ -11,11 +11,13 @@ from qtpy.QtWidgets import ( QApplication, QComboBox, QFileDialog, + QFrame, QHBoxLayout, QLabel, QPushButton, QSizePolicy, QSpacerItem, + QVBoxLayout, QWidget, ) @@ -25,7 +27,6 @@ from bec_widgets.utils.container_utils import WidgetContainerUtils from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.utils.name_utils import pascal_to_snake from bec_widgets.utils.plugin_utils import get_plugin_auto_updates -from bec_widgets.utils.round_frame import RoundedFrame from bec_widgets.utils.rpc_register import RPCRegister from bec_widgets.utils.screen_utils import apply_window_geometry, centered_geometry_for_app from bec_widgets.utils.toolbars.toolbar import ModularToolBar @@ -46,7 +47,7 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__) START_EMPTY_PROFILE_OPTION = "Start Empty (No Profile)" -class LaunchTile(RoundedFrame): +class LaunchTile(QFrame): DEFAULT_SIZE = (250, 300) open_signal = Signal() @@ -60,7 +61,11 @@ class LaunchTile(RoundedFrame): show_selector: bool = False, tile_size: tuple[int, int] | None = None, ): - super().__init__(parent=parent, orientation="vertical") + super().__init__(parent=parent) + self.setProperty("skip_settings", True) + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + self.layout = QVBoxLayout(self) + self.layout.setContentsMargins(5, 5, 5, 5) # Provide a per‑instance TILE_SIZE so the class can compute layout if tile_size is None: @@ -574,15 +579,6 @@ class LaunchWindow(BECMainWindow): window.show() return window - def apply_theme(self, theme: str): - """ - Change the theme of the application. - """ - for tile in self.tiles.values(): - tile.apply_theme(theme) - - super().apply_theme(theme) - def _open_auto_update(self): """ Open the auto update window. diff --git a/bec_widgets/utils/round_frame.py b/bec_widgets/utils/round_frame.py deleted file mode 100644 index 14722b85..00000000 --- a/bec_widgets/utils/round_frame.py +++ /dev/null @@ -1,124 +0,0 @@ -import pyqtgraph as pg -from qtpy.QtCore import Property, Qt -from qtpy.QtWidgets import QApplication, QFrame, QHBoxLayout, QVBoxLayout, QWidget - -from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton - - -class RoundedFrame(QFrame): - # TODO this should be removed completely in favor of QSS styling, no time now - """ - A custom QFrame with rounded corners and optional theme updates. - The frame can contain any QWidget, however it is mainly designed to wrap PlotWidgets to provide a consistent look and feel with other BEC Widgets. - """ - - def __init__( - self, - parent=None, - content_widget: QWidget = None, - background_color: str = None, - orientation: str = "horizontal", - radius: int = 10, - ): - QFrame.__init__(self, parent) - - self.background_color = background_color - self._radius = radius - - # Apply rounded frame styling - self.setProperty("skip_settings", True) - self.setObjectName("roundedFrame") - - # Ensure QSS can paint background/border on this widget - self.setAttribute(Qt.WA_StyledBackground, True) - - # Create a layout for the frame - if orientation == "vertical": - self.layout = QVBoxLayout(self) - self.layout.setContentsMargins(5, 5, 5, 5) - else: - self.layout = QHBoxLayout(self) - self.layout.setContentsMargins(5, 5, 5, 5) # Set 5px margin - - # Add the content widget to the layout - if content_widget: - self.layout.addWidget(content_widget) - - # Store reference to the content widget - self.content_widget = content_widget - - # Automatically apply initial styles to the GraphicalLayoutWidget if applicable - self.apply_plot_widget_style() - self.update_style() - - def apply_theme(self, theme: str): - """Deprecated: RoundedFrame no longer handles theme; styling is QSS-driven.""" - self.update_style() - - @Property(int) - def radius(self): - """Radius of the rounded corners.""" - return self._radius - - @radius.setter - def radius(self, value: int): - self._radius = value - self.update_style() - - def update_style(self): - """ - Update the style of the frame based on the background color. - """ - self.setStyleSheet(f""" - QFrame#roundedFrame {{ - border-radius: {self._radius}px; - }} - """) - self.apply_plot_widget_style() - - def apply_plot_widget_style(self, border: str = "none"): - """ - Let QSS/pyqtgraph handle plot styling; avoid overriding here. - """ - if isinstance(self.content_widget, pg.GraphicsLayoutWidget): - self.content_widget.setStyleSheet("") - - -class ExampleApp(QWidget): # pragma: no cover - def __init__(self): - super().__init__() - self.setWindowTitle("Rounded Plots Example") - - # Main layout - layout = QVBoxLayout(self) - - dark_button = DarkModeButton() - - # Create PlotWidgets - plot1 = pg.GraphicsLayoutWidget() - plot_item_1 = pg.PlotItem() - plot_item_1.plot([1, 3, 2, 4, 6, 5], pen="r") - plot1.plot_item = plot_item_1 - - plot2 = pg.GraphicsLayoutWidget() - plot_item_2 = pg.PlotItem() - plot_item_2.plot([1, 2, 4, 8, 16, 32], pen="r") - plot2.plot_item = plot_item_2 - - # Add to layout (no RoundedFrame wrapper; QSS styles plots) - layout.addWidget(dark_button) - layout.addWidget(plot1) - layout.addWidget(plot2) - - self.setLayout(layout) - - # Theme flip demo removed; global theming applies automatically - - -if __name__ == "__main__": # pragma: no cover - app = QApplication([]) - - window = ExampleApp() - window.show() - - app.exec() diff --git a/bec_widgets/widgets/plots/image/image_roi_plot.py b/bec_widgets/widgets/plots/image/image_roi_plot.py index 629d5e20..8cf8a587 100644 --- a/bec_widgets/widgets/plots/image/image_roi_plot.py +++ b/bec_widgets/widgets/plots/image/image_roi_plot.py @@ -1,16 +1,21 @@ import pyqtgraph as pg +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QFrame, QHBoxLayout -from bec_widgets.utils.round_frame import RoundedFrame from bec_widgets.widgets.plots.plot_base import BECViewBox -class ImageROIPlot(RoundedFrame): +class ImageROIPlot(QFrame): """ A widget for displaying an image with a region of interest (ROI) overlay. """ def __init__(self, parent=None): super().__init__(parent=parent) + self.setProperty("skip_settings", True) + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + self.layout = QHBoxLayout(self) + self.layout.setContentsMargins(5, 5, 5, 5) self.content_widget = pg.GraphicsLayoutWidget(self) self.layout.addWidget(self.content_widget) @@ -18,9 +23,13 @@ class ImageROIPlot(RoundedFrame): self.content_widget.addItem(self.plot_item) self.curve_color = "w" - self.apply_plot_widget_style() - def apply_theme(self, theme: str): + """ + Recolor the live profile curves for the given theme. + + Args: + theme(str): "dark" or "light". + """ if theme == "dark": self.curve_color = "w" else: @@ -30,7 +39,6 @@ class ImageROIPlot(RoundedFrame): # Frozen reference profiles keep their own (dashed) styling. continue curve.setPen(pg.mkPen(self.curve_color, width=3)) - super().apply_theme(theme) def cleanup_pyqtgraph(self): """Cleanup pyqtgraph items.""" diff --git a/bec_widgets/widgets/plots/plot_base.py b/bec_widgets/widgets/plots/plot_base.py index 6c026073..de902f47 100644 --- a/bec_widgets/widgets/plots/plot_base.py +++ b/bec_widgets/widgets/plots/plot_base.py @@ -10,7 +10,7 @@ from bec_lib import bec_logger from bec_lib.scan_data_container import ScanDataContainer from bec_lib.scan_items import ScanItem from qtpy.QtCore import QPoint, QPointF, Qt, Signal -from qtpy.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget +from qtpy.QtWidgets import QFrame, QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget from bec_widgets.utils.bec_connector import ConnectionConfig from bec_widgets.utils.bec_widget import BECWidget @@ -19,7 +19,6 @@ from bec_widgets.utils.entry_validator import EntryValidator from bec_widgets.utils.error_popups import SafeProperty, SafeSlot from bec_widgets.utils.fps_counter import FPSCounter from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem -from bec_widgets.utils.round_frame import RoundedFrame from bec_widgets.utils.side_panel import SidePanel from bec_widgets.utils.toolbars.actions import MaterialIconAction from bec_widgets.utils.toolbars.performance import PerformanceConnection, performance_bundle @@ -210,15 +209,20 @@ class PlotBase(BECWidget, QWidget): self._update_theme(None) def apply_theme(self, theme: str): - self.round_plot_widget.apply_theme(theme) if hasattr(self, "info_label"): self.info_label.set_theme(theme) def _init_ui(self): self.layout.addWidget(self.layout_manager) - self.round_plot_widget = RoundedFrame(parent=self, content_widget=self.plot_widget) + # Rounded plot background, painted by the theme QSS (QFrame[variant="plot_background"]) + self.round_plot_widget = QFrame(parent=self) self.round_plot_widget.setProperty("variant", "plot_background") self.round_plot_widget.setProperty("frameless", True) + self.round_plot_widget.setProperty("skip_settings", True) + self.round_plot_widget.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + plot_frame_layout = QHBoxLayout(self.round_plot_widget) + plot_frame_layout.setContentsMargins(5, 5, 5, 5) + plot_frame_layout.addWidget(self.plot_widget) self.layout_manager.add_widget(self.round_plot_widget) self.layout_manager.add_widget_relative(self.fps_label, self.round_plot_widget, "top") diff --git a/bec_widgets/widgets/plots/waveform/waveform.py b/bec_widgets/widgets/plots/waveform/waveform.py index b8b689f2..2a8459d2 100644 --- a/bec_widgets/widgets/plots/waveform/waveform.py +++ b/bec_widgets/widgets/plots/waveform/waveform.py @@ -720,7 +720,6 @@ class Waveform(PlotBase): self.async_signal_update.emit() self.sync_signal_update.emit() self.plot_item.enableAutoRange(x=True) - self.round_plot_widget.apply_plot_widget_style() # To keep the correct theme self._refresh_alignment_state(force_readback=True) @SafeProperty(str) @@ -750,7 +749,6 @@ class Waveform(PlotBase): self.async_signal_update.emit() self.sync_signal_update.emit() self.plot_item.enableAutoRange(x=True) - self.round_plot_widget.apply_plot_widget_style() self._refresh_alignment_state(force_readback=True) @SafeProperty(str) diff --git a/bec_widgets/widgets/services/bec_atlas_admin_view/experiment_selection/experiment_mat_card.py b/bec_widgets/widgets/services/bec_atlas_admin_view/experiment_selection/experiment_mat_card.py index 86494ce0..83dd654d 100644 --- a/bec_widgets/widgets/services/bec_atlas_admin_view/experiment_selection/experiment_mat_card.py +++ b/bec_widgets/widgets/services/bec_atlas_admin_view/experiment_selection/experiment_mat_card.py @@ -17,7 +17,6 @@ from qtpy.QtWidgets import ( from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.colors import get_theme_palette -from bec_widgets.utils.round_frame import RoundedFrame from bec_widgets.widgets.services.bec_atlas_admin_view.experiment_selection.utils import ( format_name, format_schedule, diff --git a/tests/unit_tests/test_launch_window.py b/tests/unit_tests/test_launch_window.py index 2836cb3d..73d67877 100644 --- a/tests/unit_tests/test_launch_window.py +++ b/tests/unit_tests/test_launch_window.py @@ -292,6 +292,14 @@ def test_main_label_point_size_uniform(bec_launch_window): assert len(point_sizes) == 1, f"Non-uniform main-label point sizes: {point_sizes}" +@pytest.mark.parametrize("theme", ["light", "dark"]) +def test_launch_window_theme_update(bec_launch_window, theme): + """A theme change on the launcher must not log a slot error.""" + with mock.patch("bec_widgets.utils.error_popups.logger") as error_logger: + bec_launch_window._update_theme(theme) + error_logger.error.assert_not_called() + + def test_launch_window_cleanup_deregisters_registry_callback(bec_launch_window): """The turn-off-the-lights callback must not survive the launcher's cleanup: a later registry broadcast would call into a dying window (shutdown scenario).""" diff --git a/tests/unit_tests/test_round_frame.py b/tests/unit_tests/test_round_frame.py deleted file mode 100644 index 18c7152e..00000000 --- a/tests/unit_tests/test_round_frame.py +++ /dev/null @@ -1,51 +0,0 @@ -import pyqtgraph as pg -import pytest - -from bec_widgets.utils.round_frame import RoundedFrame - - -def cleanup_pyqtgraph(plot_widget): - item = plot_widget.getPlotItem() - item.vb.menu.close() - item.vb.menu.deleteLater() - item.ctrlMenu.close() - item.ctrlMenu.deleteLater() - - -@pytest.fixture -def basic_rounded_frame(qtbot): - frame = RoundedFrame() - qtbot.addWidget(frame) - qtbot.waitExposed(frame) - yield frame - - -@pytest.fixture -def plot_rounded_frame(qtbot): - plot_widget = pg.PlotWidget() - plot_widget.plot([0, 1, 2], [2, 1, 0]) - frame = RoundedFrame(content_widget=plot_widget) - qtbot.addWidget(frame) - qtbot.waitExposed(frame) - yield frame - cleanup_pyqtgraph(plot_widget) - - -def test_basic_rounded_frame_initialization(basic_rounded_frame): - assert basic_rounded_frame.radius == 10 - assert basic_rounded_frame.content_widget is None - assert basic_rounded_frame.background_color is None - - -def test_set_radius(basic_rounded_frame): - basic_rounded_frame.radius = 20 - assert basic_rounded_frame.radius == 20 - - -def test_apply_plot_widget_style(plot_rounded_frame): - # Verify that a PlotWidget can have its style applied - plot_rounded_frame.apply_plot_widget_style(border="1px solid red") - - # Ensure style application did not break anything - assert plot_rounded_frame.content_widget is not None - assert isinstance(plot_rounded_frame.content_widget, pg.PlotWidget)