Compare commits

...

3 Commits

6 changed files with 67 additions and 189 deletions
+25 -3
View File
@@ -5,28 +5,31 @@ import xml.etree.ElementTree as ET
from typing import TYPE_CHECKING, Callable
from bec_lib.logger import bec_logger
from bec_qthemes import enable_hover_gradient
from qtpy.QtCore import Qt, Signal # type: ignore
from qtpy.QtGui import QFontMetrics, QPainter, QPainterPath, QPixmap
from qtpy.QtWidgets import (
QApplication,
QComboBox,
QFileDialog,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QSpacerItem,
QVBoxLayout,
QWidget,
)
import bec_widgets
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
from bec_widgets.utils.colors import apply_theme
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.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
@@ -43,7 +46,7 @@ logger = bec_logger.logger
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class LaunchTile(RoundedFrame):
class LaunchTile(QFrame):
DEFAULT_SIZE = (250, 300)
open_signal = Signal()
@@ -56,8 +59,14 @@ class LaunchTile(RoundedFrame):
description: str | None = None,
show_selector: bool = False,
tile_size: tuple[int, int] | None = None,
gradient: list[str] | None = None,
):
super().__init__(parent=parent, orientation="vertical")
super().__init__(parent=parent)
self.setProperty("skip_settings", True)
self.setProperty("variant", "tile")
self.setAttribute(Qt.WA_StyledBackground, True)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(5, 5, 5, 5)
# Provide a perinstance TILE_SIZE so the class can compute layout
if tile_size is None:
@@ -153,6 +162,12 @@ class LaunchTile(RoundedFrame):
"""
)
self.layout.addWidget(self.action_button, alignment=Qt.AlignCenter)
if gradient is not None:
enable_hover_gradient(self, colours=gradient)
def apply_theme(self, theme: str):
"""Allow tiles to be theme-aware without custom styling logic."""
self.update()
def _fit_label_to_width(self, label: QLabel, max_width: int, min_pt: int = 10):
"""
@@ -215,6 +230,7 @@ class LaunchWindow(BECMainWindow):
description="Highly flexible and customizable dock area application with modular widgets.",
action_button=lambda: self.launch("dock_area"),
show_selector=False,
gradient=["#B73665", "#232770"],
)
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
@@ -229,6 +245,7 @@ class LaunchWindow(BECMainWindow):
action_button=self._open_auto_update,
show_selector=True,
selector_items=list(self.available_auto_updates.keys()) + ["Default"],
gradient=["#EE0678", "#FF6A00"],
)
self.register_tile(
@@ -239,6 +256,7 @@ class LaunchWindow(BECMainWindow):
description="GUI application with custom UI file.",
action_button=self._open_custom_ui_file,
show_selector=False,
gradient=["#155799", "#179655"],
)
# plugin widgets
@@ -257,6 +275,7 @@ class LaunchWindow(BECMainWindow):
action_button=self._open_widget,
show_selector=True,
selector_items=list(self.available_widgets.keys()),
gradient=["#000046", "#1CB5E0"],
)
self._update_theme()
@@ -275,6 +294,7 @@ class LaunchWindow(BECMainWindow):
action_button: Callable | None = None,
show_selector: bool = False,
selector_items: list[str] | None = None,
gradient: list[str] | None = None,
):
"""
Register a tile in the launcher window.
@@ -297,6 +317,7 @@ class LaunchWindow(BECMainWindow):
description=description,
show_selector=show_selector,
tile_size=self.TILE_SIZE,
gradient=gradient,
)
tile.setFixedWidth(self.TILE_SIZE[0])
tile.setMinimumHeight(self.TILE_SIZE[1])
@@ -585,6 +606,7 @@ if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
apply_theme("dark")
launcher = LaunchWindow()
launcher.show()
sys.exit(app.exec())
-126
View File
@@ -1,126 +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()
@@ -1,16 +1,24 @@
import pyqtgraph as pg
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QFrame, QVBoxLayout
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.setAttribute(Qt.WA_StyledBackground, True)
self.setProperty("variant", "plot_background")
self.setProperty("frameless", True)
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(5, 5, 5, 5)
self.layout.setSpacing(0)
self.content_widget = pg.GraphicsLayoutWidget(self)
self.layout.addWidget(self.content_widget)
@@ -27,7 +35,15 @@ class ImageROIPlot(RoundedFrame):
self.curve_color = "k"
for curve in self.plot_item.curves:
curve.setPen(pg.mkPen(self.curve_color, width=3))
super().apply_theme(theme)
self.apply_plot_widget_style()
def apply_plot_widget_style(self, border: str = "none"):
"""Keep pyqtgraph widgets styled by QSS/themes."""
if border != "none":
self.content_widget.setStyleSheet(f"border: {border};")
else:
self.content_widget.setStyleSheet("")
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""
+21 -4
View File
@@ -6,14 +6,13 @@ 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 QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget
from qtpy.QtWidgets import QFrame, QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget
from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
from bec_widgets.utils.bec_widget import BECWidget
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.performance import PerformanceConnection, performance_bundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
@@ -144,25 +143,43 @@ class PlotBase(BECWidget, QWidget):
self._update_theme(None)
def apply_theme(self, theme: str):
self.round_plot_widget.apply_theme(theme)
self.apply_plot_widget_style()
super().apply_theme(theme)
def _init_ui(self):
self.layout.addWidget(self.layout_manager)
self.round_plot_widget = RoundedFrame(parent=self, content_widget=self.plot_widget)
self.round_plot_widget = QFrame(parent=self)
self.round_plot_widget.setAttribute(Qt.WA_StyledBackground, True)
self.round_plot_widget.setProperty("variant", "plot_background")
self.round_plot_widget.setProperty("frameless", True)
plot_frame_layout = QVBoxLayout(self.round_plot_widget)
plot_frame_layout.setContentsMargins(5, 5, 5, 5)
plot_frame_layout.setSpacing(0)
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")
self.fps_label.hide()
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.apply_plot_widget_style()
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 apply_plot_widget_style(self, border: str = "none"):
"""Let theme/QSS style the plot widget; keep custom overrides minimal."""
if not isinstance(self.plot_widget, pg.GraphicsLayoutWidget):
return
if border != "none":
self.plot_widget.setStyleSheet(f"border: {border};")
else:
self.plot_widget.setStyleSheet("")
def _init_toolbar(self):
self.toolbar.add_bundle(performance_bundle(self.toolbar.components))
self.toolbar.add_bundle(plot_export_bundle(self.toolbar.components))
@@ -575,7 +575,7 @@ 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.apply_plot_widget_style() # To keep the correct theme
@SafeProperty(str)
def x_entry(self) -> str | None:
@@ -604,7 +604,7 @@ 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.apply_plot_widget_style()
@SafeProperty(str)
def color_palette(self) -> str:
-51
View File
@@ -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)