1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-18 06:15:37 +02:00

Compare commits

..

36 Commits

Author SHA1 Message Date
18c104fc12 fix(monaco_dock): update editor metadata handling and improve open_file method 2025-11-26 10:56:41 +01:00
499d0a5986 refactor(developer_widget): enhance documentation and add missing imports 2025-11-26 10:56:30 +01:00
487feebcbf feat(developer_widget): add signal connection for focused editor changes to disable run button for macro files 2025-11-26 10:56:14 +01:00
73528eef18 fix(collapsible_tree_section): fix typo in the styleSheet 2025-11-24 11:45:33 +01:00
c2780d629c feat(advanced_dock_area): floating docks restore with relative geometry 2025-11-24 10:55:17 +01:00
d090f8f7e5 refactor(developer widget): type hint improvements 2025-11-24 10:55:17 +01:00
f9c03f1daa refactor: improvements to enum access 2025-11-24 10:55:17 +01:00
78485d3df0 fix(widget_state_manager): omits QIcon properties to prevent segfault 2025-11-24 10:55:17 +01:00
dc30e6b022 fix(widget_state_manager): visibility managed by parent 2025-11-24 10:55:17 +01:00
fd2b8918f5 feat(advanced_dock_area): instance lock for multiple ads in same session 2025-11-24 10:55:17 +01:00
0f392efdce fix(widgets): removed isVisible from all SafeProperties 2025-11-24 10:55:17 +01:00
e708ee56f4 fix(widget_state_manager): IDEExplorer plugin not initialised in designer 2025-11-24 10:55:17 +01:00
e74eea199e fix(widget_state_manager): skip property listed introduced 2025-11-24 10:55:17 +01:00
1b299b9334 feat(widget_state_manager): can serialize from root 2025-11-24 10:55:17 +01:00
aed22c605b fix(widget_state_manager): always setting visible to true 2025-11-24 10:55:17 +01:00
557371f3ba fix(client): client regenerated 2025-11-24 10:55:17 +01:00
31d01abe18 fix(bec_widget): improved qt enums; grab safeguard 2025-11-24 10:55:17 +01:00
86a966f10f fix(widget_io): find ancestor returns correct type 2025-11-24 10:55:17 +01:00
a9039c5ee6 fix(qt_ads): pythons stubs match structure of PySide6QtAds 2025-11-24 10:55:17 +01:00
0119332bcf fix(ide_explorer): light mode fixed 2025-11-24 10:55:17 +01:00
22651f1c58 fix(widget_state_manager): visible is always true 2025-11-24 10:55:17 +01:00
bd2d47f18f refactor(main_app): adapted for DockAreaWidget changes 2025-11-24 10:55:17 +01:00
00e4786651 refactor(developer_view): changed to use DockAreaWidget 2025-11-24 10:55:17 +01:00
6c6408e87f refactor(monaco_dock): changed to use DockAreaWidget 2025-11-24 10:55:17 +01:00
d259de227d feat(advanced_dock_area): created DockAreaWidget base class; profile management through namespaces; dock area variants 2025-11-24 10:55:17 +01:00
28c0b3b18b fix(main_window): removed general forced cleanup 2025-11-24 10:55:17 +01:00
003fff7e4b fix(advanced_dock_area): disable developer mode switch 2025-11-24 10:55:17 +01:00
b9643691b9 feat(advanced_dock_area): UI/UX for profile management improved, saving directories logic adjusted 2025-11-24 10:55:17 +01:00
652bf2d00b fix(main_window): cleanup adjusted with shiboken6 2025-11-24 10:55:17 +01:00
a20d8b269d fix(dark_mode_button): skip settings added 2025-11-24 10:55:17 +01:00
1ab915468b fix(widget_state_manager): added shiboken check 2025-11-24 10:55:17 +01:00
578be47880 feat(bec_widget): save screenshot to bytes 2025-11-24 10:55:17 +01:00
5b364c5aa5 refactor: improve toolbar actions typing 2025-11-21 10:59:43 +01:00
38134ec849 WILL BE REMOVED AFTER REBASE fix(fakeredis): add support for additional args
(cherry picked from commit c9455672b5)
2025-11-19 18:15:44 +01:00
937bea5103 WILL BE REMOVED AFTER REBASE fix(test): removed duplicate test in crosshair
(cherry picked from commit d00d786399)
2025-11-18 16:51:52 +01:00
0ba14b2dbb WILL BE REMOVED AFTER REBASE build: pyqtgraph pin to 0.13.7
(cherry picked from commit a4c465dcaf)
2025-11-18 16:51:52 +01:00
58 changed files with 6276 additions and 1410 deletions

View File

@@ -1,8 +1,7 @@
import os
import sys
import PySide6QtAds as QtAds
import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot

View File

@@ -47,7 +47,10 @@ class BECMainApp(BECMainWindow):
def _add_views(self):
self.add_section("BEC Applications", "bec_apps")
self.ads = AdvancedDockArea(self)
self.ads = AdvancedDockArea(
self, profile_namespace="main_workspace", auto_profile_namespace=False
)
self.ads.setObjectName("MainWorkspace")
self.device_manager = DeviceManagerWidget(self)
self.developer_view = DeveloperView(self)

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from bec_qthemes import material_icon
from qtpy import QtWidgets
from qtpy.QtCore import QEasingCurve, QParallelAnimationGroup, QPropertyAnimation, Qt, Signal
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import (
QGraphicsOpacityEffect,
QHBoxLayout,
@@ -80,7 +79,7 @@ class SideBar(QScrollArea):
self.toggle = QToolButton(self)
self.toggle.setCheckable(False)
self.toggle.setIcon(material_icon("keyboard_arrow_right", icon_type=QIcon))
self.toggle.setIcon(material_icon("keyboard_arrow_right", convert_to_pixmap=False))
self.toggle.clicked.connect(self.on_expand)
self.toggle_row_layout.addWidget(self.title_label, 1, Qt.AlignLeft | Qt.AlignVCenter)
@@ -153,7 +152,7 @@ class SideBar(QScrollArea):
self.toggle.setIcon(
material_icon(
"keyboard_arrow_left" if self._is_expanded else "keyboard_arrow_right",
icon_type=QIcon,
convert_to_pixmap=False,
)
)
@@ -199,7 +198,7 @@ class SideBar(QScrollArea):
self.toggle.setIcon(
material_icon(
"keyboard_arrow_left" if self._is_expanded else "keyboard_arrow_right",
icon_type=QIcon,
convert_to_pixmap=False,
)
)
# Refresh each component that supports it

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
from qtpy.QtGui import QIcon
from bec_qthemes import material_icon
from qtpy import QtCore
from qtpy.QtCore import QEasingCurve, QPropertyAnimation, Qt
@@ -123,7 +121,7 @@ class NavigationItem(QWidget):
# Main Icon
self.icon_btn = QToolButton(self)
self.icon_btn.setIcon(material_icon(self._icon_name, filled=False, icon_type=QIcon))
self.icon_btn.setIcon(material_icon(self._icon_name, filled=False, convert_to_pixmap=False))
self.icon_btn.setAutoRaise(True)
self._icon_size_collapsed = QtCore.QSize(20, 20)
self._icon_size_expanded = QtCore.QSize(26, 26)
@@ -280,10 +278,12 @@ class NavigationItem(QWidget):
self._toggled = value
if value:
new_icon = material_icon(
self._icon_name, filled=True, color=get_on_primary(), icon_type=QIcon
self._icon_name, filled=True, color=get_on_primary(), convert_to_pixmap=False
)
else:
new_icon = material_icon(self._icon_name, filled=False, color=get_fg(), icon_type=QIcon)
new_icon = material_icon(
self._icon_name, filled=False, color=get_fg(), convert_to_pixmap=False
)
self.icon_btn.setIcon(new_icon)
# Re-polish so QSS applies correct colors to icon/labels
for w in (self, self.icon_btn, self.title_lbl, self.mini_lbl):
@@ -352,7 +352,7 @@ class DarkModeNavItem(NavigationItem):
self.mini_lbl.setText("Light" if is_dark else "Dark")
# Update icon
self.icon_btn.setIcon(
material_icon("light_mode" if is_dark else "dark_mode", icon_type=QIcon)
material_icon("light_mode" if is_dark else "dark_mode", convert_to_pixmap=False)
)
def refresh_theme(self):

View File

@@ -21,9 +21,6 @@ class DeveloperView(ViewBase):
self.developer_widget = DeveloperWidget(parent=self)
self.set_content(self.developer_widget)
# Apply stretch after the layout is done
self.set_default_view([2, 5, 3], [7, 3])
if __name__ == "__main__":
import sys

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import re
import markdown
@@ -5,18 +7,17 @@ from bec_lib.endpoints import MessageEndpoints
from bec_lib.script_executor import upload_script
from bec_qthemes import material_icon
from qtpy.QtGui import QKeySequence, QShortcut
from qtpy.QtWidgets import QTextEdit, QVBoxLayout, QWidget
from shiboken6 import isValid
from qtpy.QtWidgets import QTextEdit
import bec_widgets.widgets.containers.ads as QtAds
from bec_widgets import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.toolbars.actions import MaterialIconAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.widgets.containers.ads import CDockManager, CDockWidget
from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
from bec_widgets.widgets.containers.advanced_dock_area.basic_dock_area import DockAreaWidget
from bec_widgets.widgets.containers.qt_ads import CDockWidget
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
from bec_widgets.widgets.editors.web_console.web_console import WebConsole
from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
@@ -77,31 +78,38 @@ def markdown_to_html(md_text: str) -> str:
return css + html
class DeveloperWidget(BECWidget, QWidget):
class DeveloperWidget(DockAreaWidget):
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
super().__init__(parent=parent, variant="compact", **kwargs)
# Top-level layout hosting a toolbar and the dock manager
self._root_layout = QVBoxLayout(self)
self._root_layout.setContentsMargins(0, 0, 0, 0)
self._root_layout.setSpacing(0)
# Promote toolbar above the dock manager provided by the base class
self.toolbar = ModularToolBar(self)
self.init_developer_toolbar()
self._root_layout.addWidget(self.toolbar)
self.dock_manager = CDockManager(self)
self.dock_manager.setStyleSheet("")
self._root_layout.addWidget(self.dock_manager)
self._root_layout.insertWidget(0, self.toolbar)
# Initialize the widgets
self.explorer = IDEExplorer(self)
self.explorer.setObjectName("Explorer")
self.console = WebConsole(self)
self.console.setObjectName("Console")
self.terminal = WebConsole(self, startup_cmd="")
self.terminal.setObjectName("Terminal")
self.monaco = MonacoDock(self)
self.monaco.setObjectName("MonacoEditor")
self.monaco.save_enabled.connect(self._on_save_enabled_update)
self.plotting_ads = AdvancedDockArea(self, mode="plot", default_add_direction="bottom")
self.plotting_ads = AdvancedDockArea(
self,
mode="plot",
default_add_direction="bottom",
profile_namespace="developer_plotting",
auto_profile_namespace=False,
enable_profile_management=False,
variant="compact",
)
self.plotting_ads.setObjectName("PlottingArea")
self.signature_help = QTextEdit(self)
self.signature_help.setObjectName("Signature Help")
self.signature_help.setAcceptRichText(True)
self.signature_help.setReadOnly(True)
self.signature_help.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
@@ -112,58 +120,87 @@ class DeveloperWidget(BECWidget, QWidget):
lambda text: self.signature_help.setHtml(markdown_to_html(text))
)
self._current_script_id: str | None = None
self.script_editor_tab = None
# Create the dock widgets
self.explorer_dock = QtAds.CDockWidget("Explorer", self)
self.explorer_dock.setWidget(self.explorer)
self.console_dock = QtAds.CDockWidget("Console", self)
self.console_dock.setWidget(self.console)
self.monaco_dock = QtAds.CDockWidget("Monaco Editor", self)
self.monaco_dock.setWidget(self.monaco)
self.terminal_dock = QtAds.CDockWidget("Terminal", self)
self.terminal_dock.setWidget(self.terminal)
# Monaco will be central widget
self.dock_manager.setCentralWidget(self.monaco_dock)
# Add the dock widgets to the dock manager
area_bottom = self.dock_manager.addDockWidget(
QtAds.DockWidgetArea.BottomDockWidgetArea, self.console_dock
)
self.dock_manager.addDockWidgetTabToArea(self.terminal_dock, area_bottom)
area_left = self.dock_manager.addDockWidget(
QtAds.DockWidgetArea.LeftDockWidgetArea, self.explorer_dock
)
area_left.titleBar().setVisible(False)
for dock in self.dock_manager.dockWidgets():
# dock.setFeature(CDockWidget.DockWidgetDeleteOnClose, True)#TODO implement according to MonacoDock or AdvancedDockArea
# dock.setFeature(CDockWidget.CustomCloseHandling, True) #TODO same
dock.setFeature(CDockWidget.DockWidgetFeature.DockWidgetClosable, False)
dock.setFeature(CDockWidget.DockWidgetFeature.DockWidgetFloatable, False)
dock.setFeature(CDockWidget.DockWidgetFeature.DockWidgetMovable, False)
self.plotting_ads_dock = QtAds.CDockWidget("Plotting Area", self)
self.plotting_ads_dock.setWidget(self.plotting_ads)
self.signature_dock = QtAds.CDockWidget("Signature Help", self)
self.signature_dock.setWidget(self.signature_help)
area_right = self.dock_manager.addDockWidget(
QtAds.DockWidgetArea.RightDockWidgetArea, self.plotting_ads_dock
)
self.dock_manager.addDockWidgetTabToArea(self.signature_dock, area_right)
self._initialize_layout()
# Connect editor signals
self.explorer.file_open_requested.connect(self._open_new_file)
self.monaco.macro_file_updated.connect(self.explorer.refresh_macro_file)
self.monaco.focused_editor.connect(self._on_focused_editor_changed)
self.toolbar.show_bundles(["save", "execution", "settings"])
def _initialize_layout(self) -> None:
"""Create the default dock arrangement for the developer workspace."""
# Monaco editor as the central dock
self.monaco_dock = self.new(
self.monaco,
closable=False,
floatable=False,
movable=False,
return_dock=True,
show_title_bar=False,
show_settings_action=False,
title_buttons={"float": False, "close": False, "menu": False},
# promote_central=True,
)
# Explorer on the left without a title bar
self.explorer_dock = self.new(
self.explorer,
where="left",
closable=False,
floatable=False,
movable=False,
return_dock=True,
show_title_bar=False,
)
# Console and terminal tabbed along the bottom
self.console_dock = self.new(
self.console,
relative_to=self.monaco_dock,
where="bottom",
closable=False,
floatable=False,
movable=False,
return_dock=True,
title_buttons={"float": True, "close": False},
)
self.terminal_dock = self.new(
self.terminal,
closable=False,
floatable=False,
movable=False,
tab_with=self.console_dock,
return_dock=True,
title_buttons={"float": False, "close": False},
)
# Plotting area on the right with signature help tabbed alongside
self.plotting_ads_dock = self.new(
self.plotting_ads,
where="right",
closable=False,
floatable=False,
movable=False,
return_dock=True,
title_buttons={"float": True},
)
self.signature_dock = self.new(
self.signature_help,
closable=False,
floatable=False,
movable=False,
tab_with=self.plotting_ads_dock,
return_dock=True,
title_buttons={"float": False, "close": False},
)
self.set_layout_ratios(horizontal=[2, 5, 3], vertical=[7, 3])
def init_developer_toolbar(self):
"""Initialize the developer toolbar with necessary actions and widgets."""
save_button = MaterialIconAction(
@@ -247,14 +284,17 @@ class DeveloperWidget(BECWidget, QWidget):
@SafeSlot()
def on_save(self):
"""Save the currently focused file in the Monaco editor."""
self.monaco.save_file()
@SafeSlot()
def on_save_as(self):
"""Save the currently focused file in the Monaco editor with a 'Save As' dialog."""
self.monaco.save_file(force_save_as=True)
@SafeSlot()
def on_vim_triggered(self):
"""Toggle Vim mode in the Monaco editor."""
self.monaco.set_vim_mode(self.toolbar.components.get_action("vim").action.isChecked())
@SafeSlot(bool)
@@ -264,27 +304,39 @@ class DeveloperWidget(BECWidget, QWidget):
@SafeSlot()
def on_execute(self):
"""Upload and run the currently focused script in the Monaco editor."""
self.script_editor_tab = self.monaco.last_focused_editor
if not self.script_editor_tab:
return
self.current_script_id = upload_script(
self.client.connector, self.script_editor_tab.widget().get_text()
)
widget = self.script_editor_tab.widget()
if not isinstance(widget, MonacoWidget):
return
self.current_script_id = upload_script(self.client.connector, widget.get_text())
self.console.write(f'bec._run_script("{self.current_script_id}")')
print(f"Uploaded script with ID: {self.current_script_id}")
@SafeSlot()
def on_stop(self):
"""Stop the execution of the currently running script"""
if not self.current_script_id:
return
self.console.send_ctrl_c()
@property
def current_script_id(self):
"""Get the ID of the currently running script."""
return self._current_script_id
@current_script_id.setter
def current_script_id(self, value: str | None):
"""
Set the ID of the currently running script.
Args:
value (str | None): The script ID to set.
Raises:
ValueError: If the provided value is not a string or None.
"""
if value is not None and not isinstance(value, str):
raise ValueError("Script ID must be a string.")
old_script_id = self._current_script_id
@@ -301,31 +353,55 @@ class DeveloperWidget(BECWidget, QWidget):
self.on_script_execution_info, MessageEndpoints.script_execution_info(new_script_id)
)
@SafeSlot(CDockWidget)
def _on_focused_editor_changed(self, tab_widget: CDockWidget):
"""
Disable the run button if the focused editor is a macro file.
Args:
tab_widget: The currently focused tab widget in the Monaco editor.
"""
if not isinstance(tab_widget, CDockWidget):
return
widget = tab_widget.widget()
if not isinstance(widget, MonacoWidget):
return
file_scope = widget.metadata.get("scope", "")
run_action = self.toolbar.components.get_action("run")
stop_action = self.toolbar.components.get_action("stop")
if "macro" in file_scope:
run_action.action.setEnabled(False)
stop_action.action.setEnabled(False)
else:
run_action.action.setEnabled(True)
stop_action.action.setEnabled(True)
@SafeSlot(dict, dict)
def on_script_execution_info(self, content: dict, metadata: dict):
"""
Handle script execution info messages to update the editor highlights.
Args:
content (dict): The content of the message containing execution info.
metadata (dict): Additional metadata for the message.
"""
print(f"Script execution info: {content}")
current_lines = content.get("current_lines")
if self.script_editor_tab is None:
return
widget = self.script_editor_tab.widget()
if not isinstance(widget, MonacoWidget):
return
if not current_lines:
self.script_editor_tab.widget().clear_highlighted_lines()
widget.clear_highlighted_lines()
return
line_number = current_lines[0]
self.script_editor_tab.widget().clear_highlighted_lines()
self.script_editor_tab.widget().set_highlighted_lines(line_number, line_number)
widget.clear_highlighted_lines()
widget.set_highlighted_lines(line_number, line_number)
def cleanup(self):
for dock in self.dock_manager.dockWidgets():
self._delete_dock(dock)
"""Clean up resources used by the developer widget."""
self.delete_all()
return super().cleanup()
def _delete_dock(self, dock: CDockWidget) -> None:
w = dock.widget()
if w and isValid(w):
w.close()
w.deleteLater()
if isValid(dock):
dock.closeDockWidget()
dock.deleteDockWidget()
if __name__ == "__main__":
import sys

View File

@@ -4,7 +4,6 @@ import os
from functools import partial
from typing import List, Literal
import PySide6QtAds as QtAds
import yaml
from bec_lib import config_helper
from bec_lib.bec_yaml_loader import yaml_load
@@ -12,7 +11,6 @@ from bec_lib.file_utils import DeviceConfigWriter
from bec_lib.logger import bec_logger
from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
from bec_qthemes import apply_theme
from PySide6QtAds import CDockManager, CDockWidget
from qtpy.QtCore import Qt, QThreadPool, QTimer
from qtpy.QtWidgets import (
QDialog,
@@ -28,6 +26,7 @@ from qtpy.QtWidgets import (
QWidget,
)
import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.help_inspector.help_inspector import HelpInspector
@@ -158,7 +157,7 @@ class DeviceManagerView(BECWidget, QWidget):
self._root_layout = QVBoxLayout(self)
self._root_layout.setContentsMargins(0, 0, 0, 0)
self._root_layout.setSpacing(0)
self.dock_manager = CDockManager(self)
self.dock_manager = QtAds.CDockManager(self)
self.dock_manager.setStyleSheet("")
self._root_layout.addWidget(self.dock_manager)
@@ -237,9 +236,9 @@ class DeviceManagerView(BECWidget, QWidget):
self.dock_manager.addDockWidgetTabToArea(self.error_logs_dock, area)
for dock in self.dock_manager.dockWidgets():
dock.setFeature(CDockWidget.DockWidgetClosable, False)
dock.setFeature(CDockWidget.DockWidgetFloatable, False)
dock.setFeature(CDockWidget.DockWidgetMovable, False)
dock.setFeature(QtAds.CDockWidget.DockWidgetClosable, False)
dock.setFeature(QtAds.CDockWidget.DockWidgetFloatable, False)
dock.setFeature(QtAds.CDockWidget.DockWidgetMovable, False)
# Apply stretch after the layout is done
self.set_default_view([2, 8, 2], [7, 3])

View File

@@ -8,7 +8,6 @@ from bec_lib.bec_yaml_loader import yaml_load
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from qtpy import QtCore, QtWidgets
from qtpy.QtGui import QIcon
from bec_widgets.applications.views.device_manager_view.device_manager_view import DeviceManagerView
from bec_widgets.utils.bec_widget import BECWidget
@@ -47,13 +46,13 @@ class DeviceManagerWidget(BECWidget, QtWidgets.QWidget):
)
# Load current config
self.button_load_current_config = QtWidgets.QPushButton("Load Current Config")
icon = material_icon(icon_name="database", size=(24, 24), icon_type=QIcon)
icon = material_icon(icon_name="database", size=(24, 24), convert_to_pixmap=False)
self.button_load_current_config.setIcon(icon)
self._overlay_layout.addWidget(self.button_load_current_config)
self.button_load_current_config.clicked.connect(self._load_config_clicked)
# Load config from disk
self.button_load_config_from_file = QtWidgets.QPushButton("Load Config From File")
icon = material_icon(icon_name="folder", size=(24, 24), icon_type=QIcon)
icon = material_icon(icon_name="folder", size=(24, 24), convert_to_pixmap=False)
self.button_load_config_from_file.setIcon(icon)
self._overlay_layout.addWidget(self.button_load_config_from_file)
self.button_load_config_from_file.clicked.connect(self._load_config_from_file_clicked)

View File

@@ -98,46 +98,51 @@ class AdvancedDockArea(RPCBase):
@rpc_call
def new(
self,
widget: "BECWidget | str",
widget: "QWidget | str",
*,
closable: "bool" = True,
floatable: "bool" = True,
movable: "bool" = True,
start_floating: "bool" = False,
where: "Literal['left', 'right', 'top', 'bottom'] | None" = None,
**kwargs,
) -> "BECWidget":
on_close: "Callable[[CDockWidget, QWidget], None] | None" = None,
tab_with: "CDockWidget | QWidget | str | None" = None,
relative_to: "CDockWidget | QWidget | str | None" = None,
return_dock: "bool" = False,
show_title_bar: "bool | None" = None,
title_buttons: "Mapping[str, bool] | Sequence[str] | str | None" = None,
show_settings_action: "bool | None" = None,
promote_central: "bool" = False,
**widget_kwargs,
) -> "QWidget | CDockWidget | BECWidget":
"""
Create a new widget (or reuse an instance) and add it as a dock.
Override the base helper so dock settings are available by default.
Args:
widget: Widget instance or a string widget type (factory-created).
closable: Whether the dock is closable.
floatable: Whether the dock is floatable.
movable: Whether the dock is movable.
start_floating: Start the dock in a floating state.
where: Preferred area to add the dock: "left" | "right" | "top" | "bottom".
If None, uses the instance default passed at construction time.
**kwargs: The keyword arguments for the widget.
Returns:
The widget instance.
The flag remains user-configurable (pass ``False`` to hide the action).
"""
@rpc_call
def dock_map(self) -> "dict[str, CDockWidget]":
"""
Return the dock widgets map as dictionary with names as keys.
"""
@rpc_call
def dock_list(self) -> "list[CDockWidget]":
"""
Return the list of dock widgets.
"""
@rpc_call
def widget_map(self) -> "dict[str, QWidget]":
"""
Return a dictionary mapping widget names to their corresponding BECWidget instances.
Returns:
dict: A dictionary mapping widget names to BECWidget instances.
Return a dictionary mapping widget names to their corresponding widgets.
"""
@rpc_call
def widget_list(self) -> "list[QWidget]":
"""
Return a list of all BECWidget instances in the dock area.
Returns:
list: A list of all BECWidget instances in the dock area.
Return a list of all widgets contained in the dock area.
"""
@property
@@ -153,13 +158,58 @@ class AdvancedDockArea(RPCBase):
@rpc_call
def attach_all(self):
"""
Return all floating docks to the dock area, preserving tab groups within each floating container.
Re-attach floating docks back into the dock manager.
"""
@rpc_call
def delete_all(self):
"""
Delete all docks and widgets.
Delete all docks and their associated widgets.
"""
@rpc_call
def set_layout_ratios(
self,
*,
horizontal: "Sequence[float] | Mapping[int | str, float] | None" = None,
vertical: "Sequence[float] | Mapping[int | str, float] | None" = None,
splitter_overrides: "Mapping[int | str | Sequence[int], Sequence[float] | Mapping[int | str, float]] | None" = None,
) -> "None":
"""
Adjust splitter ratios in the dock layout.
Args:
horizontal: Weights applied to every horizontal splitter encountered.
vertical: Weights applied to every vertical splitter encountered.
splitter_overrides: Optional overrides targeting specific splitters identified
by their index path (e.g. ``{0: [1, 2], (1, 0): [3, 5]}``). Paths are zero-based
indices following the splitter hierarchy, starting from the root splitter.
Example:
To build three columns with custom per-column ratios::
area.set_layout_ratios(
horizontal=[1, 2, 1], # column widths
splitter_overrides={
0: [1, 2], # column 0 (two rows)
1: [3, 2, 1], # column 1 (three rows)
2: [1], # column 2 (single row)
},
)
"""
@rpc_call
def describe_layout(self) -> "list[dict[str, Any]]":
"""
Return metadata describing splitter paths, orientations, and contained docks.
Useful for determining the keys to use in `set_layout_ratios(splitter_overrides=...)`.
"""
@rpc_call
def print_layout_structure(self) -> "None":
"""
Pretty-print the current splitter paths to stdout.
"""
@property
@@ -1290,6 +1340,161 @@ class DeviceLineEdit(RPCBase):
"""
class DockAreaWidget(RPCBase):
"""Lightweight dock area that exposes the core Qt ADS docking helpers without any"""
@rpc_call
def new(
self,
widget: "QWidget | str",
*,
closable: "bool" = True,
floatable: "bool" = True,
movable: "bool" = True,
start_floating: "bool" = False,
floating_state: "Mapping[str, object] | None" = None,
where: "Literal['left', 'right', 'top', 'bottom'] | None" = None,
on_close: "Callable[[CDockWidget, QWidget], None] | None" = None,
tab_with: "CDockWidget | QWidget | str | None" = None,
relative_to: "CDockWidget | QWidget | str | None" = None,
return_dock: "bool" = False,
show_title_bar: "bool | None" = None,
title_buttons: "Mapping[str, bool] | Sequence[str] | str | None" = None,
show_settings_action: "bool | None" = False,
promote_central: "bool" = False,
dock_icon: "QIcon | None" = None,
apply_widget_icon: "bool" = True,
**widget_kwargs,
) -> "QWidget | CDockWidget | BECWidget":
"""
Create a new widget (or reuse an instance) and add it as a dock.
Args:
widget(QWidget | str): Instance or registered widget type string.
closable(bool): Whether the dock is closable.
floatable(bool): Whether the dock is floatable.
movable(bool): Whether the dock is movable.
start_floating(bool): Whether to start the dock floating.
floating_state(Mapping | None): Optional floating geometry metadata to apply when floating.
where(Literal["left", "right", "top", "bottom"] | None): Dock placement hint relative to the dock area (ignored when
``relative_to`` is provided without an explicit value).
on_close(Callable[[CDockWidget, QWidget], None] | None): Optional custom close handler accepting (dock, widget).
tab_with(CDockWidget | QWidget | str | None): Existing dock (or widget/name) to tab the new dock alongside.
relative_to(CDockWidget | QWidget | str | None): Existing dock (or widget/name) used as the positional anchor.
When supplied and ``where`` is ``None``, the new dock inherits the
anchor's current dock area.
return_dock(bool): When True, return the created dock instead of the widget.
show_title_bar(bool | None): Explicitly show or hide the dock area's title bar.
title_buttons(Mapping[str, bool] | Sequence[str] | str | None): Mapping or iterable describing which title bar buttons should
remain visible. Provide a mapping of button names (``"float"``,
``"close"``, ``"menu"``, ``"auto_hide"``, ``"minimize"``) to booleans,
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
dock_icon(QIcon | None): Optional icon applied to the dock via ``CDockWidget.setIcon``.
Provide a `QIcon` (e.g. from ``material_icon``). When ``None`` (default),
the widget's ``ICON_NAME`` attribute is used when available.
apply_widget_icon(bool): When False, skip automatically resolving the icon from
the widget's ``ICON_NAME`` (useful for callers who want no icon and do not pass one explicitly).
Returns:
The widget instance by default, or the created `CDockWidget` when `return_dock` is True.
"""
@rpc_call
def dock_map(self) -> "dict[str, CDockWidget]":
"""
Return the dock widgets map as dictionary with names as keys.
"""
@rpc_call
def dock_list(self) -> "list[CDockWidget]":
"""
Return the list of dock widgets.
"""
@rpc_call
def widget_map(self) -> "dict[str, QWidget]":
"""
Return a dictionary mapping widget names to their corresponding widgets.
"""
@rpc_call
def widget_list(self) -> "list[QWidget]":
"""
Return a list of all widgets contained in the dock area.
"""
@rpc_call
def attach_all(self):
"""
Re-attach floating docks back into the dock manager.
"""
@rpc_call
def delete_all(self):
"""
Delete all docks and their associated widgets.
"""
@rpc_call
def set_layout_ratios(
self,
*,
horizontal: "Sequence[float] | Mapping[int | str, float] | None" = None,
vertical: "Sequence[float] | Mapping[int | str, float] | None" = None,
splitter_overrides: "Mapping[int | str | Sequence[int], Sequence[float] | Mapping[int | str, float]] | None" = None,
) -> "None":
"""
Adjust splitter ratios in the dock layout.
Args:
horizontal: Weights applied to every horizontal splitter encountered.
vertical: Weights applied to every vertical splitter encountered.
splitter_overrides: Optional overrides targeting specific splitters identified
by their index path (e.g. ``{0: [1, 2], (1, 0): [3, 5]}``). Paths are zero-based
indices following the splitter hierarchy, starting from the root splitter.
Example:
To build three columns with custom per-column ratios::
area.set_layout_ratios(
horizontal=[1, 2, 1], # column widths
splitter_overrides={
0: [1, 2], # column 0 (two rows)
1: [3, 2, 1], # column 1 (three rows)
2: [1], # column 2 (single row)
},
)
"""
@rpc_call
def describe_layout(self) -> "list[dict[str, Any]]":
"""
Return metadata describing splitter paths, orientations, and contained docks.
Useful for determining the keys to use in `set_layout_ratios(splitter_overrides=...)`.
"""
@rpc_call
def print_layout_structure(self) -> "None":
"""
Pretty-print the current splitter paths to stdout.
"""
@rpc_call
def set_central_dock(self, dock: "CDockWidget | QWidget | str") -> "None":
"""
Promote an existing dock to be the dock manager's central widget.
Args:
dock(CDockWidget | QWidget | str): Dock reference to promote.
"""
class EllipticalROI(RPCBase):
"""Elliptical Region of Interest with centre/width/height tracking and auto-labelling."""
@@ -2690,21 +2895,154 @@ class MonacoDock(RPCBase):
"""MonacoDock is a dock widget that contains Monaco editor instances."""
@rpc_call
def remove(self):
def new(
self,
widget: "QWidget | str",
*,
closable: "bool" = True,
floatable: "bool" = True,
movable: "bool" = True,
start_floating: "bool" = False,
floating_state: "Mapping[str, object] | None" = None,
where: "Literal['left', 'right', 'top', 'bottom'] | None" = None,
on_close: "Callable[[CDockWidget, QWidget], None] | None" = None,
tab_with: "CDockWidget | QWidget | str | None" = None,
relative_to: "CDockWidget | QWidget | str | None" = None,
return_dock: "bool" = False,
show_title_bar: "bool | None" = None,
title_buttons: "Mapping[str, bool] | Sequence[str] | str | None" = None,
show_settings_action: "bool | None" = False,
promote_central: "bool" = False,
dock_icon: "QIcon | None" = None,
apply_widget_icon: "bool" = True,
**widget_kwargs,
) -> "QWidget | CDockWidget | BECWidget":
"""
Cleanup the BECConnector
Create a new widget (or reuse an instance) and add it as a dock.
Args:
widget(QWidget | str): Instance or registered widget type string.
closable(bool): Whether the dock is closable.
floatable(bool): Whether the dock is floatable.
movable(bool): Whether the dock is movable.
start_floating(bool): Whether to start the dock floating.
floating_state(Mapping | None): Optional floating geometry metadata to apply when floating.
where(Literal["left", "right", "top", "bottom"] | None): Dock placement hint relative to the dock area (ignored when
``relative_to`` is provided without an explicit value).
on_close(Callable[[CDockWidget, QWidget], None] | None): Optional custom close handler accepting (dock, widget).
tab_with(CDockWidget | QWidget | str | None): Existing dock (or widget/name) to tab the new dock alongside.
relative_to(CDockWidget | QWidget | str | None): Existing dock (or widget/name) used as the positional anchor.
When supplied and ``where`` is ``None``, the new dock inherits the
anchor's current dock area.
return_dock(bool): When True, return the created dock instead of the widget.
show_title_bar(bool | None): Explicitly show or hide the dock area's title bar.
title_buttons(Mapping[str, bool] | Sequence[str] | str | None): Mapping or iterable describing which title bar buttons should
remain visible. Provide a mapping of button names (``"float"``,
``"close"``, ``"menu"``, ``"auto_hide"``, ``"minimize"``) to booleans,
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `AdvancedDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
dock_icon(QIcon | None): Optional icon applied to the dock via ``CDockWidget.setIcon``.
Provide a `QIcon` (e.g. from ``material_icon``). When ``None`` (default),
the widget's ``ICON_NAME`` attribute is used when available.
apply_widget_icon(bool): When False, skip automatically resolving the icon from
the widget's ``ICON_NAME`` (useful for callers who want no icon and do not pass one explicitly).
Returns:
The widget instance by default, or the created `CDockWidget` when `return_dock` is True.
"""
@rpc_call
def attach(self):
def dock_map(self) -> "dict[str, CDockWidget]":
"""
None
Return the dock widgets map as dictionary with names as keys.
"""
@rpc_call
def detach(self):
def dock_list(self) -> "list[CDockWidget]":
"""
Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget.
Return the list of dock widgets.
"""
@rpc_call
def widget_map(self) -> "dict[str, QWidget]":
"""
Return a dictionary mapping widget names to their corresponding widgets.
"""
@rpc_call
def widget_list(self) -> "list[QWidget]":
"""
Return a list of all widgets contained in the dock area.
"""
@rpc_call
def attach_all(self):
"""
Re-attach floating docks back into the dock manager.
"""
@rpc_call
def delete_all(self):
"""
Delete all docks and their associated widgets.
"""
@rpc_call
def set_layout_ratios(
self,
*,
horizontal: "Sequence[float] | Mapping[int | str, float] | None" = None,
vertical: "Sequence[float] | Mapping[int | str, float] | None" = None,
splitter_overrides: "Mapping[int | str | Sequence[int], Sequence[float] | Mapping[int | str, float]] | None" = None,
) -> "None":
"""
Adjust splitter ratios in the dock layout.
Args:
horizontal: Weights applied to every horizontal splitter encountered.
vertical: Weights applied to every vertical splitter encountered.
splitter_overrides: Optional overrides targeting specific splitters identified
by their index path (e.g. ``{0: [1, 2], (1, 0): [3, 5]}``). Paths are zero-based
indices following the splitter hierarchy, starting from the root splitter.
Example:
To build three columns with custom per-column ratios::
area.set_layout_ratios(
horizontal=[1, 2, 1], # column widths
splitter_overrides={
0: [1, 2], # column 0 (two rows)
1: [3, 2, 1], # column 1 (three rows)
2: [1], # column 2 (single row)
},
)
"""
@rpc_call
def describe_layout(self) -> "list[dict[str, Any]]":
"""
Return metadata describing splitter paths, orientations, and contained docks.
Useful for determining the keys to use in `set_layout_ratios(splitter_overrides=...)`.
"""
@rpc_call
def print_layout_structure(self) -> "None":
"""
Pretty-print the current splitter paths to stdout.
"""
@rpc_call
def set_central_dock(self, dock: "CDockWidget | QWidget | str") -> "None":
"""
Promote an existing dock to be the dock manager's central widget.
Args:
dock(CDockWidget | QWidget | str): Dock reference to promote.
"""

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from bec_qthemes import material_icon
from qtpy import PYSIDE6
from qtpy.QtGui import QIcon, QPixmap
from qtpy.QtGui import QIcon
from bec_widgets.utils.bec_plugin_helper import user_widget_plugin
@@ -35,7 +35,7 @@ def designer_material_icon(icon_name: str) -> QIcon:
Returns:
QIcon: The QIcon for the material icon.
"""
return QIcon(material_icon(icon_name, filled=True, icon_type=QPixmap))
return QIcon(material_icon(icon_name, filled=True, convert_to_pixmap=True))
def list_editable_packages() -> set[str]:

View File

@@ -3,12 +3,13 @@ from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
import PySide6QtAds as QtAds
import shiboken6
from bec_lib.logger import bec_logger
from qtpy.QtCore import QObject
from qtpy.QtCore import QBuffer, QByteArray, QIODevice, QObject, Qt
from qtpy.QtGui import QPixmap
from qtpy.QtWidgets import QApplication, QFileDialog, QWidget
import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.error_popups import SafeConnect, SafeSlot
@@ -57,7 +58,6 @@ class BECWidget(BECConnector):
theme_update(bool, optional): Whether to subscribe to theme updates. Defaults to False. When set to True, the
widget's apply_theme method will be called when the theme changes.
"""
super().__init__(
client=client, config=config, gui_id=gui_id, parent_dock=parent_dock, **kwargs
)
@@ -218,6 +218,50 @@ class BECWidget(BECConnector):
screenshot.save(file_name)
logger.info(f"Screenshot saved to {file_name}")
def screenshot_bytes(
self,
*,
max_width: int | None = None,
max_height: int | None = None,
fmt: str = "PNG",
quality: int = -1,
) -> QByteArray:
"""
Grab this widget, optionally scale to a max size, and return encoded image bytes.
If max_width/max_height are omitted (the default), capture at full resolution.
Args:
max_width(int, optional): Maximum width of the screenshot.
max_height(int, optional): Maximum height of the screenshot.
fmt(str, optional): Image format (e.g., "PNG", "JPEG").
quality(int, optional): Image quality (0-100), -1 for default.
Returns:
QByteArray: The screenshot image bytes.
"""
if not isinstance(self, QWidget):
return QByteArray()
if not hasattr(self, "grab"):
raise RuntimeError(f"Cannot take screenshot of non-QWidget instance: {repr(self)}")
pixmap: QPixmap = self.grab()
if pixmap.isNull():
return QByteArray()
if max_width is not None or max_height is not None:
w = max_width if max_width is not None else pixmap.width()
h = max_height if max_height is not None else pixmap.height()
pixmap = pixmap.scaled(
w, h, Qt.AspectRatioMode.KeepAspectRatio, Qt.QSmoothTransformation
)
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QIODevice.OpenModeFlag.WriteOnly)
pixmap.save(buf, fmt, quality)
buf.close()
return ba
def attach(self):
dock = WidgetHierarchy.find_ancestor(self, QtAds.CDockWidget)
if dock is None:

View File

@@ -3,7 +3,7 @@ from types import SimpleNamespace
from bec_qthemes import material_icon
from qtpy.QtCore import Property, Qt, Signal
from qtpy.QtGui import QColor, QIcon
from qtpy.QtGui import QColor
from qtpy.QtWidgets import (
QDialog,
QHBoxLayout,
@@ -132,7 +132,7 @@ class CompactPopupWidget(QWidget):
self.compact_status = LedLabel(self.compact_view_widget)
self.compact_show_popup = QToolButton(self.compact_view_widget)
self.compact_show_popup.setIcon(
material_icon(icon_name="expand_content", size=(10, 10), icon_type=QIcon)
material_icon(icon_name="expand_content", size=(10, 10), convert_to_pixmap=False)
)
self.compact_view_widget.layout().addWidget(self.compact_label)
self.compact_view_widget.layout().addWidget(self.compact_status)
@@ -144,6 +144,7 @@ class CompactPopupWidget(QWidget):
self.container.setVisible(True)
layout(self.container)
self.layout = self.container.layout()
self._compact_view = False
self.compact_show_popup.clicked.connect(self.show_popup)
@@ -171,7 +172,9 @@ class CompactPopupWidget(QWidget):
self.compact_label.setVisible(False)
self.compact_status.setVisible(False)
self.compact_show_popup.setIcon(
material_icon(icon_name="collapse_content", size=(10, 10), icon_type=QIcon)
material_icon(
icon_name="collapse_content", size=(10, 10), convert_to_pixmap=False
)
)
self.expand.emit(True)
else:
@@ -179,7 +182,9 @@ class CompactPopupWidget(QWidget):
self.compact_label.setVisible(True)
self.compact_status.setVisible(True)
self.compact_show_popup.setIcon(
material_icon(icon_name="expand_content", size=(10, 10), icon_type=QIcon)
material_icon(
icon_name="expand_content", size=(10, 10), convert_to_pixmap=False
)
)
self.compact_view = True
self.expand.emit(False)
@@ -206,7 +211,7 @@ class CompactPopupWidget(QWidget):
@Property(bool)
def compact_view(self):
return self.compact_label.isVisible()
return self._compact_view
@compact_view.setter
def compact_view(self, set_compact: bool):
@@ -216,6 +221,7 @@ class CompactPopupWidget(QWidget):
the full view is displayed. This is handled by toggling visibility of
the container widget or the compact view widget.
"""
self._compact_view = set_compact
if set_compact:
self.compact_view_widget.setVisible(True)
self.container.setVisible(False)

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
from bec_qthemes import material_icon
from qtpy.QtCore import QSize, Signal
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import (
QApplication,
QFrame,
@@ -96,9 +95,11 @@ class ExpandableGroupFrame(QFrame):
def _update_expansion_icon(self):
self._expansion_button.setIcon(
material_icon(icon_name=self.EXPANDED_ICON_NAME, size=(10, 10), icon_type=QIcon)
material_icon(icon_name=self.EXPANDED_ICON_NAME, size=(10, 10), convert_to_pixmap=False)
if self.expanded
else material_icon(icon_name=self.COLLAPSED_ICON_NAME, size=(10, 10), icon_type=QIcon)
else material_icon(
icon_name=self.COLLAPSED_ICON_NAME, size=(10, 10), convert_to_pixmap=False
)
)
@SafeProperty(str)
@@ -114,7 +115,7 @@ class ExpandableGroupFrame(QFrame):
if icon_name:
self._title_icon.setVisible(True)
self._title_icon.setPixmap(
material_icon(icon_name=icon_name, size=(20, 20), icon_type=QPixmap)
material_icon(icon_name=icon_name, size=(20, 20), convert_to_pixmap=True)
)
else:
self._title_icon.setVisible(False)

View File

@@ -7,7 +7,6 @@ from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from pydantic import BaseModel, ValidationError
from qtpy.QtCore import Signal # type: ignore
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QGridLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget
from bec_widgets.utils.bec_widget import BECWidget
@@ -208,7 +207,7 @@ class PydanticModelForm(TypedForm):
self._validity.compact_view = True # type: ignore
self._validity.label = "Validity" # type: ignore
self._validity.compact_show_popup.setIcon(
material_icon(icon_name="info", size=(10, 10), icon_type=QIcon)
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
)
self._validity_message = QLabel("Not yet validated")
self._validity.addWidget(self._validity_message)

View File

@@ -26,7 +26,7 @@ from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined
from qtpy import QtCore
from qtpy.QtCore import QSize, Qt, Signal # type: ignore
from qtpy.QtGui import QFontMetrics, QIcon
from qtpy.QtGui import QFontMetrics
from qtpy.QtWidgets import (
QApplication,
QButtonGroup,
@@ -203,7 +203,9 @@ class DynamicFormItem(QWidget):
def _add_clear_button(self):
self._clear_button = QToolButton()
self._clear_button.setIcon(material_icon(icon_name="close", size=(10, 10), icon_type=QIcon))
self._clear_button.setIcon(
material_icon(icon_name="close", size=(10, 10), convert_to_pixmap=False)
)
self._layout.addWidget(self._clear_button)
# the widget added in _add_main_widget must implement .clear() if value is not required
self._clear_button.setToolTip("Clear value or reset to default.")

View File

@@ -11,7 +11,7 @@ from bec_lib.device import ReadoutPriority
from bec_lib.logger import bec_logger
from bec_qthemes._icon.material_icons import material_icon
from qtpy.QtCore import QSize, Qt, QTimer
from qtpy.QtGui import QAction, QColor, QIcon
from qtpy.QtGui import QAction, QColor, QIcon # type: ignore
from qtpy.QtWidgets import (
QApplication,
QComboBox,
@@ -53,9 +53,9 @@ def create_action_with_text(toolbar_action, toolbar: QToolBar):
btn.setDefaultAction(toolbar_action.action)
btn.setAutoRaise(True)
if toolbar_action.text_position == "beside":
btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
else:
btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
btn.setText(toolbar_action.label_text)
toolbar.addWidget(btn)
@@ -66,7 +66,7 @@ class NoCheckDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
# Remove any check indicator
option.checkState = Qt.Unchecked
option.checkState = Qt.CheckState.Unchecked
class LongPressToolButton(QToolButton):
@@ -111,13 +111,15 @@ class ToolBarAction(ABC):
checkable (bool, optional): Whether the action is checkable. Defaults to False.
"""
def __init__(self, icon_path: str = None, tooltip: str = None, checkable: bool = False):
def __init__(
self, icon_path: str | None = None, tooltip: str | None = None, checkable: bool = False
):
self.icon_path = (
os.path.join(MODULE_PATH, "assets", "toolbar_icons", icon_path) if icon_path else None
)
self.tooltip = tooltip
self.checkable = checkable
self.action = None
self.tooltip: str = tooltip or ""
self.checkable: bool = checkable
self.action: QAction
@abstractmethod
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
@@ -133,6 +135,11 @@ class ToolBarAction(ABC):
pass
class IconAction(ToolBarAction):
@abstractmethod
def get_icon(self) -> QIcon: ...
class SeparatorAction(ToolBarAction):
"""Separator action for the toolbar."""
@@ -140,7 +147,7 @@ class SeparatorAction(ToolBarAction):
toolbar.addSeparator()
class QtIconAction(ToolBarAction):
class QtIconAction(IconAction):
def __init__(
self,
standard_icon,
@@ -179,13 +186,13 @@ class QtIconAction(ToolBarAction):
return self.icon
class MaterialIconAction(ToolBarAction):
class MaterialIconAction(IconAction):
"""
Action with a Material icon for the toolbar.
Args:
icon_name (str, optional): The name of the Material icon. Defaults to None.
tooltip (str, optional): The tooltip for the action. Defaults to None.
icon_name (str): The name of the Material icon.
tooltip (str): The tooltip for the action.
checkable (bool, optional): Whether the action is checkable. Defaults to False.
filled (bool, optional): Whether the icon is filled. Defaults to False.
color (str | tuple | QColor | dict[Literal["dark", "light"], str] | None, optional): The color of the icon.
@@ -197,8 +204,9 @@ class MaterialIconAction(ToolBarAction):
def __init__(
self,
icon_name: str = None,
tooltip: str = None,
icon_name: str,
tooltip: str,
*,
checkable: bool = False,
filled: bool = False,
color: str | tuple | QColor | dict[Literal["dark", "light"], str] | None = None,
@@ -217,9 +225,13 @@ class MaterialIconAction(ToolBarAction):
self.label_text = label_text
self.text_position = text_position
# Generate the icon using the material_icon helper
self.icon = material_icon(
self.icon_name, size=(20, 20), icon_type=QIcon, filled=self.filled, color=self.color
)
self.icon: QIcon = material_icon(
self.icon_name,
size=(20, 20),
convert_to_pixmap=False,
filled=self.filled,
color=self.color,
) # type: ignore
if parent is None:
logger.warning(
"MaterialIconAction was created without a parent. Please consider adding one. Using None as parent may cause issues."
@@ -255,11 +267,11 @@ class DeviceSelectionAction(ToolBarAction):
Action for selecting a device in a combobox.
Args:
label (str): The label for the combobox.
device_combobox (DeviceComboBox): The combobox for selecting the device.
label (str): The label for the combobox.
"""
def __init__(self, label: str | None = None, device_combobox=None):
def __init__(self, /, device_combobox: DeviceComboBox, label: str | None = None):
super().__init__()
self.label = label
self.device_combobox = device_combobox
@@ -281,7 +293,7 @@ class DeviceSelectionAction(ToolBarAction):
self.device_combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
class SwitchableToolBarAction(ToolBarAction):
class SwitchableToolBarAction(IconAction):
"""
A split toolbar action that combines a main action and a drop-down menu for additional actions.
@@ -301,9 +313,9 @@ class SwitchableToolBarAction(ToolBarAction):
def __init__(
self,
actions: Dict[str, ToolBarAction],
initial_action: str = None,
tooltip: str = None,
actions: Dict[str, IconAction],
initial_action: str | None = None,
tooltip: str | None = None,
checkable: bool = True,
default_state_checked: bool = False,
parent=None,
@@ -326,11 +338,11 @@ class SwitchableToolBarAction(ToolBarAction):
target (QWidget): The target widget for the action.
"""
self.main_button = LongPressToolButton(toolbar)
self.main_button.setPopupMode(QToolButton.MenuButtonPopup)
self.main_button.setPopupMode(QToolButton.ToolButtonPopupMode.MenuButtonPopup)
self.main_button.setCheckable(self.checkable)
default_action = self.actions[self.current_key]
self.main_button.setIcon(default_action.get_icon())
self.main_button.setToolTip(default_action.tooltip)
self.main_button.setToolTip(default_action.tooltip or "")
self.main_button.clicked.connect(self._trigger_current_action)
menu = QMenu(self.main_button)
for key, action_obj in self.actions.items():
@@ -428,11 +440,7 @@ class WidgetAction(ToolBarAction):
"""
def __init__(
self,
label: str | None = None,
widget: QWidget = None,
adjust_size: bool = True,
parent=None,
self, *, widget: QWidget, label: str | None = None, adjust_size: bool = True, parent=None
):
super().__init__(icon_path=None, tooltip=label, checkable=False)
self.label = label
@@ -455,14 +463,14 @@ class WidgetAction(ToolBarAction):
if self.label is not None:
label_widget = QLabel(text=f"{self.label}", parent=target)
label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
label_widget.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
label_widget.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight)
layout.addWidget(label_widget)
if isinstance(self.widget, QComboBox) and self.adjust_size:
self.widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
self.widget.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents)
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
size_policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.widget.setSizePolicy(size_policy)
self.widget.setMinimumWidth(self.calculate_minimum_width(self.widget))
@@ -471,7 +479,7 @@ class WidgetAction(ToolBarAction):
toolbar.addWidget(self.container)
# Store the container as the action to allow toggling visibility.
self.action = self.container
self.action = self.container # type: ignore
def cleanup(self):
"""
@@ -486,7 +494,7 @@ class WidgetAction(ToolBarAction):
@staticmethod
def calculate_minimum_width(combo_box: QComboBox) -> int:
font_metrics = combo_box.fontMetrics()
max_width = max(font_metrics.width(combo_box.itemText(i)) for i in range(combo_box.count()))
max_width = max(font_metrics.width(combo_box.itemText(i)) for i in range(combo_box.count())) # type: ignore
return max_width + 60
@@ -500,7 +508,7 @@ class ExpandableMenuAction(ToolBarAction):
icon_path (str, optional): The path to the icon file. Defaults to None.
"""
def __init__(self, label: str, actions: dict, icon_path: str = None):
def __init__(self, label: str, actions: dict[str, IconAction], icon_path: str | None = None):
super().__init__(icon_path, label)
self.actions = actions
self._button_ref: weakref.ReferenceType[QToolButton] | None = None
@@ -513,7 +521,7 @@ class ExpandableMenuAction(ToolBarAction):
if self.icon_path:
button.setIcon(QIcon(self.icon_path))
button.setText(self.tooltip)
button.setPopupMode(QToolButton.InstantPopup)
button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
button.setStyleSheet(
"""
QToolButton {
@@ -639,7 +647,7 @@ class TutorialAction(MaterialIconAction):
Returns:
str: Unique ID for the registered widget.
"""
return self.guided_help.register_widget(widget, text, widget_name)
return self.guided_help.register_widget(widget=widget, text=text, title=widget_name)
def start_tour(self):
"""Start the guided tour with all registered widgets."""

View File

@@ -2,8 +2,10 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Type, TypeVar, cast
import shiboken6 as shb
from bec_lib import bec_logger
from qtpy.QtWidgets import (
QApplication,
QCheckBox,
@@ -21,6 +23,13 @@ from qtpy.QtWidgets import (
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils import BECConnector
logger = bec_logger.logger
TAncestor = TypeVar("TAncestor", bound=QWidget)
class WidgetHandler(ABC):
"""Abstract base class for all widget handlers."""
@@ -576,44 +585,50 @@ class WidgetHierarchy:
return connectors
@staticmethod
def find_ancestor(widget, ancestor_class) -> QWidget | None:
def find_ancestor(
widget: QWidget | BECConnector, ancestor_class: Type[TAncestor] | str
) -> TAncestor | None:
"""
Traverse up the parent chain to find the nearest ancestor matching ancestor_class.
ancestor_class may be a class or a class-name string.
Returns the matching ancestor, or None if none is found.
Find the closest ancestor of the specified class (or class-name string).
Args:
widget(QWidget): The starting widget.
ancestor_class(Type[TAncestor] | str): The ancestor class or class-name string to search for.
Returns:
TAncestor | None: The closest ancestor of the specified class, or None if not found.
"""
# Guard against deleted/invalid Qt wrappers
if not shb.isValid(widget):
if widget is None or not shb.isValid(widget):
return None
# If searching for BECConnector specifically, reuse the dedicated helper
try:
from bec_widgets.utils import BECConnector # local import to avoid cycles
if ancestor_class is BECConnector or (
isinstance(ancestor_class, str) and ancestor_class == "BECConnector"
):
return WidgetHierarchy._get_becwidget_ancestor(widget)
except Exception:
# If import fails, fall back to generic traversal below
pass
is_bec_target = False
if isinstance(ancestor_class, str):
is_bec_target = ancestor_class == "BECConnector"
elif isinstance(ancestor_class, type):
is_bec_target = issubclass(ancestor_class, BECConnector)
# Generic traversal across QObject parent chain
parent = getattr(widget, "parent", None)
if callable(parent):
parent = parent()
if is_bec_target:
ancestor = WidgetHierarchy._get_becwidget_ancestor(widget)
return cast(TAncestor, ancestor)
except Exception as e:
logger.error(f"Error importing BECConnector: {e}")
parent = widget.parent() if hasattr(widget, "parent") else None
while parent is not None:
if not shb.isValid(parent):
return None
try:
if isinstance(ancestor_class, str):
if parent.__class__.__name__ == ancestor_class:
return parent
return cast(TAncestor, parent)
else:
if isinstance(parent, ancestor_class):
return parent
except Exception:
pass
return cast(TAncestor, parent)
except Exception as e:
logger.error(f"Error checking ancestor class: {e}")
parent = parent.parent() if hasattr(parent, "parent") else None
return None

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
import shiboken6
from bec_lib import bec_logger
from qtpy.QtCore import QSettings
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import (
QApplication,
QCheckBox,
@@ -19,17 +21,28 @@ from bec_widgets.utils.widget_io import WidgetHierarchy
logger = bec_logger.logger
PROPERTY_TO_SKIP = ["palette", "font", "windowIcon", "windowIconText", "locale", "styleSheet"]
class WidgetStateManager:
"""
A class to manage the state of a widget by saving and loading the state to and from a INI file.
Manage saving and loading widget state to/from an INI file.
Args:
widget(QWidget): The widget to manage the state for.
widget (QWidget): Root widget whose subtree will be serialized.
serialize_from_root (bool): When True, build group names relative to
this root and ignore parents above it. This keeps profiles portable
between different host window hierarchies.
root_id (str | None): Optional stable label to use for the root in
the settings key path. When omitted and `serialize_from_root` is
True, the class name of `widget` is used, falling back to its
objectName and finally to "root".
"""
def __init__(self, widget):
def __init__(self, widget, *, serialize_from_root: bool = False, root_id: str | None = None):
self.widget = widget
self._serialize_from_root = bool(serialize_from_root)
self._root_id = root_id
def save_state(self, filename: str | None = None, settings: QSettings | None = None):
"""
@@ -84,6 +97,9 @@ class WidgetStateManager:
settings(QSettings): The QSettings object to save the state to.
recursive(bool): Whether to recursively save the state of child widgets.
"""
if widget is None or not shiboken6.isValid(widget):
return
if widget.property("skip_settings") is True:
return
@@ -93,15 +109,28 @@ class WidgetStateManager:
for i in range(meta.propertyCount()):
prop = meta.property(i)
name = prop.name()
# Skip persisting QWidget visibility because container widgets (e.g. tab
# stacks, dock managers) manage that state themselves. Restoring a saved
# False can permanently hide a widget, while forcing True makes hidden
# tabs show on top. Leave the property to the parent widget instead.
if name == "visible":
continue
if (
name == "objectName"
or name in PROPERTY_TO_SKIP
or not prop.isReadable()
or not prop.isWritable()
or not prop.isStored() # can be extended to fine filter
):
continue
value = widget.property(name)
if isinstance(value, QIcon):
continue
settings.setValue(name, value)
settings.endGroup()
# Recursively process children (only if they aren't skipped)
@@ -115,11 +144,14 @@ class WidgetStateManager:
) # to avoid duplicates
for child in all_children:
if (
child.objectName()
child
and shiboken6.isValid(child)
and child.objectName()
and child.property("skip_settings") is not True
and not isinstance(child, QLabel)
):
self._save_widget_state_qsettings(child, settings, False)
logger.info(f"Saved state for widget '{widget_name}'")
def _load_widget_state_qsettings(
self, widget: QWidget, settings: QSettings, recursive: bool = True
@@ -132,6 +164,9 @@ class WidgetStateManager:
settings(QSettings): The QSettings object to load the state from.
recursive(bool): Whether to recursively load the state of child widgets.
"""
if widget is None or not shiboken6.isValid(widget):
return
if widget.property("skip_settings") is True:
return
@@ -141,6 +176,8 @@ class WidgetStateManager:
for i in range(meta.propertyCount()):
prop = meta.property(i)
name = prop.name()
if name == "visible":
continue
if settings.contains(name):
value = settings.value(name)
widget.setProperty(name, value)
@@ -156,29 +193,59 @@ class WidgetStateManager:
) # to avoid duplicates
for child in all_children:
if (
child.objectName()
child
and shiboken6.isValid(child)
and child.objectName()
and child.property("skip_settings") is not True
and not isinstance(child, QLabel)
):
self._load_widget_state_qsettings(child, settings, False)
def _get_full_widget_name(self, widget: QWidget):
def _get_full_widget_name(self, widget: QWidget) -> str:
"""
Get the full name of the widget including its parent names.
Build a group key for *widget*.
When `serialize_from_root` is False (default), this preserves the original
behavior and walks all parents up to the top-level widget.
When `serialize_from_root` is True, the key is built relative to
`self.widget` and parents above the managed root are ignored. The first
path segment is either `root_id` (when provided) or a stable label derived
from the root widget (class name, then objectName, then "root").
Args:
widget(QWidget): The widget to get the full name for.
Returns:
str: The full name of the widget.
widget (QWidget): The widget to build the key for.
"""
name = widget.objectName()
parent = widget.parent()
while parent:
obj_name = parent.objectName() or parent.metaObject().className()
name = obj_name + "." + name
parent = parent.parent()
return name
# Backwards-compatible behavior: include the entire parent chain.
if not getattr(self, "_serialize_from_root", False):
name = widget.objectName()
parent = widget.parent()
while parent:
obj_name = parent.objectName() or parent.metaObject().className()
name = obj_name + "." + name
parent = parent.parent()
return name
parts: list[str] = []
current: QWidget | None = widget
while current is not None:
if current is self.widget:
# Reached the serialization root.
root_label = self._root_id
if not root_label:
meta = current.metaObject() if hasattr(current, "metaObject") else None
class_name = meta.className() if meta is not None else ""
root_label = class_name or current.objectName() or "root"
parts.append(str(root_label))
break
obj_name = current.objectName() or current.metaObject().className()
parts.append(obj_name)
current = current.parent()
parts.reverse()
return ".".join(parts)
class ExampleApp(QWidget): # pragma: no cover:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,330 @@
from __future__ import annotations
from typing import Callable, Literal
from qtpy.QtCore import Qt
from qtpy.QtGui import QPixmap
from qtpy.QtWidgets import (
QCheckBox,
QDialog,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from bec_widgets import SafeSlot
class SaveProfileDialog(QDialog):
"""Dialog for saving workspace profiles with quick select option."""
def __init__(
self,
parent: QWidget | None = None,
current_name: str = "",
current_profile_name: str = "",
*,
name_exists: Callable[[str], bool] | None = None,
profile_origin: (
Callable[[str], Literal["module", "plugin", "settings", "unknown"]] | None
) = None,
origin_label: Callable[[str], str | None] | None = None,
quick_select_checked: bool = False,
):
super().__init__(parent)
self.setWindowTitle("Save Workspace Profile")
self.setModal(True)
self.resize(400, 160)
self._name_exists = name_exists or (lambda _: False)
self._profile_origin = profile_origin or (lambda _: "unknown")
self._origin_label = origin_label or (lambda _: None)
self._current_profile_name = current_profile_name.strip()
self._previous_name_before_overwrite = current_name
self._block_name_signals = False
self._block_checkbox_signals = False
self.overwrite_existing = False
layout = QVBoxLayout(self)
# Name input
name_row = QHBoxLayout()
name_row.addWidget(QLabel("Profile Name:"))
self.name_edit = QLineEdit(current_name)
self.name_edit.setPlaceholderText("Enter profile name...")
name_row.addWidget(self.name_edit)
layout.addLayout(name_row)
# Overwrite checkbox
self.overwrite_checkbox = QCheckBox("Overwrite current profile")
self.overwrite_checkbox.setEnabled(bool(self._current_profile_name))
self.overwrite_checkbox.toggled.connect(self._on_overwrite_toggled)
layout.addWidget(self.overwrite_checkbox)
# Quick-select checkbox
self.quick_select_checkbox = QCheckBox("Include in quick selection.")
self.quick_select_checkbox.setChecked(quick_select_checked)
layout.addWidget(self.quick_select_checkbox)
# Buttons
btn_row = QHBoxLayout()
btn_row.addStretch(1)
self.save_btn = QPushButton("Save")
self.save_btn.setDefault(True)
cancel_btn = QPushButton("Cancel")
self.save_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(self.save_btn)
btn_row.addWidget(cancel_btn)
layout.addLayout(btn_row)
# Enable/disable save button based on name input
self.name_edit.textChanged.connect(self._on_name_changed)
self._update_save_button()
@SafeSlot(bool)
def _on_overwrite_toggled(self, checked: bool):
if self._block_checkbox_signals:
return
if not self._current_profile_name:
return
self._block_name_signals = True
if checked:
self._previous_name_before_overwrite = self.name_edit.text()
self.name_edit.setText(self._current_profile_name)
self.name_edit.selectAll()
else:
if self.name_edit.text().strip() == self._current_profile_name:
self.name_edit.setText(self._previous_name_before_overwrite or "")
self._block_name_signals = False
self._update_save_button()
@SafeSlot(str)
def _on_name_changed(self, _: str):
if self._block_name_signals:
return
text = self.name_edit.text().strip()
if self.overwrite_checkbox.isChecked() and text != self._current_profile_name:
self._block_checkbox_signals = True
self.overwrite_checkbox.setChecked(False)
self._block_checkbox_signals = False
self._update_save_button()
def _update_save_button(self):
"""Enable save button only when name is not empty."""
self.save_btn.setEnabled(bool(self.name_edit.text().strip()))
def get_profile_name(self) -> str:
"""Return the entered profile name."""
return self.name_edit.text().strip()
def is_quick_select(self) -> bool:
"""Return whether the profile should appear in quick select."""
return self.quick_select_checkbox.isChecked()
def _generate_unique_name(self, base: str) -> str:
candidate_base = base.strip() or "profile"
suffix = "_custom"
candidate = f"{candidate_base}{suffix}"
counter = 1
while self._name_exists(candidate) or self._profile_origin(candidate) != "unknown":
candidate = f"{candidate_base}{suffix}_{counter}"
counter += 1
return candidate
def accept(self):
name = self.get_profile_name()
if not name:
return
self.overwrite_existing = False
origin = self._profile_origin(name)
if origin in {"module", "plugin"}:
source_label = self._origin_label(name)
if origin == "module":
provider = source_label or "BEC Widgets"
else:
provider = (
f"the {source_label} plugin repository"
if source_label
else "the plugin repository"
)
QMessageBox.information(
self,
"Read-only profile",
(
f"'{name}' is a default profile provided by {provider} and cannot be overwritten.\n"
"Please choose a different name."
),
)
suggestion = self._generate_unique_name(name)
self._block_name_signals = True
self.name_edit.setText(suggestion)
self.name_edit.selectAll()
self._block_name_signals = False
self._block_checkbox_signals = True
self.overwrite_checkbox.setChecked(False)
self._block_checkbox_signals = False
return
if origin == "settings":
reply = QMessageBox.question(
self,
"Overwrite profile",
(
f"A profile named '{name}' already exists.\n\n"
"Overwriting will update both the saved profile and its restore default.\n"
"Do you want to continue?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
suggestion = self._generate_unique_name(name)
self._block_name_signals = True
self.name_edit.setText(suggestion)
self.name_edit.selectAll()
self._block_name_signals = False
self._block_checkbox_signals = True
self.overwrite_checkbox.setChecked(False)
self._block_checkbox_signals = False
return
self.overwrite_existing = True
super().accept()
class PreviewPanel(QGroupBox):
"""Resizable preview pane that scales its pixmap with aspect ratio preserved."""
def __init__(self, title: str, pixmap: QPixmap | None, parent: QWidget | None = None):
super().__init__(title, parent)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self._original: QPixmap | None = pixmap if (pixmap and not pixmap.isNull()) else None
layout = QVBoxLayout(self)
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.image_label.setMinimumSize(360, 240)
self.image_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
layout.addWidget(self.image_label, 1)
if self._original:
self._update_scaled_pixmap()
else:
self.image_label.setText("No preview available")
self.image_label.setStyleSheet(
self.image_label.styleSheet() + "color: rgba(255,255,255,0.6); font-style: italic;"
)
def setPixmap(self, pixmap: QPixmap | None):
"""
Set the pixmap to display in the preview panel.
Args:
pixmap(QPixmap | None): The pixmap to display. If None or null, clears the preview.
"""
self._original = pixmap if (pixmap and not pixmap.isNull()) else None
if self._original:
self.image_label.setText("")
self._update_scaled_pixmap()
else:
self.image_label.setPixmap(QPixmap())
self.image_label.setText("No preview available")
def resizeEvent(self, event):
super().resizeEvent(event)
if self._original:
self._update_scaled_pixmap()
def _update_scaled_pixmap(self):
if not self._original:
return
size = self.image_label.size()
if size.width() <= 0 or size.height() <= 0:
return
scaled = self._original.scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.image_label.setPixmap(scaled)
class RestoreProfileDialog(QDialog):
"""
Confirmation dialog that previews the current profile screenshot against the default baseline.
"""
def __init__(
self, parent: QWidget | None, current_pixmap: QPixmap | None, default_pixmap: QPixmap | None
):
super().__init__(parent)
self.setWindowTitle("Restore Profile to Default")
self.setModal(True)
self.resize(880, 480)
layout = QVBoxLayout(self)
info_label = QLabel(
"Restoring will discard your custom layout and replace it with the default profile."
)
info_label.setWordWrap(True)
layout.addWidget(info_label)
preview_row = QHBoxLayout()
layout.addLayout(preview_row)
current_preview = PreviewPanel("Current", current_pixmap, self)
default_preview = PreviewPanel("Default", default_pixmap, self)
# Equal expansion left/right
preview_row.addWidget(current_preview, 1)
arrow_label = QLabel("\u2192")
arrow_label.setAlignment(Qt.AlignCenter)
arrow_label.setStyleSheet("font-size: 32px; padding: 0 16px;")
arrow_label.setMinimumWidth(40)
arrow_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
preview_row.addWidget(arrow_label)
preview_row.addWidget(default_preview, 1)
# Enforce equal stretch for both previews
preview_row.setStretch(0, 1)
preview_row.setStretch(1, 0)
preview_row.setStretch(2, 1)
warn_label = QLabel(
"This action cannot be undone. Do you want to restore the default layout now?"
)
warn_label.setWordWrap(True)
layout.addWidget(warn_label)
btn_row = QHBoxLayout()
btn_row.addStretch(1)
restore_btn = QPushButton("Restore")
restore_btn.setDefault(True)
cancel_btn = QPushButton("Cancel")
restore_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(restore_btn)
btn_row.addWidget(cancel_btn)
layout.addLayout(btn_row)
# Make the previews take most of the vertical space on resize
layout.setStretch(0, 0) # info label
layout.setStretch(1, 1) # preview row
layout.setStretch(2, 0) # warning label
layout.setStretch(3, 0) # buttons
@staticmethod
def confirm(
parent: QWidget | None, current_pixmap: QPixmap | None, default_pixmap: QPixmap | None
) -> bool:
dialog = RestoreProfileDialog(parent, current_pixmap, default_pixmap)
return dialog.exec() == QDialog.Accepted

View File

@@ -0,0 +1,408 @@
from __future__ import annotations
from functools import partial
from bec_lib import bec_logger
from bec_qthemes import material_icon
from qtpy.QtCore import Qt
from qtpy.QtGui import QPixmap
from qtpy.QtWidgets import (
QAbstractItemView,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QMessageBox,
QPushButton,
QSizePolicy,
QSplitter,
QStyledItemDelegate,
QTableWidget,
QTableWidgetItem,
QToolButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from bec_widgets import BECWidget, SafeSlot
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
delete_profile_files,
get_profile_info,
is_quick_select,
list_profiles,
load_profile_screenshot,
set_quick_select,
)
logger = bec_logger.logger
class WorkSpaceManager(BECWidget, QWidget):
RPC = False
PLUGIN = False
COL_ACTIONS = 0
COL_NAME = 1
COL_AUTHOR = 2
HEADERS = ["Actions", "Profile", "Author"]
def __init__(
self, parent=None, target_widget=None, default_profile: str | None = None, **kwargs
):
super().__init__(parent=parent, **kwargs)
self.target_widget = target_widget
self.profile_namespace = (
getattr(target_widget, "profile_namespace", None) if target_widget else None
)
self.accent_colors = get_accent_colors()
self._init_ui()
if self.target_widget is not None and hasattr(self.target_widget, "profile_changed"):
self.target_widget.profile_changed.connect(self.on_profile_changed)
if default_profile is not None:
self._select_by_name(default_profile)
self._show_profile_details(default_profile)
def _init_ui(self):
self.root_layout = QHBoxLayout(self)
self.splitter = QSplitter(Qt.Horizontal, self)
self.root_layout.addWidget(self.splitter)
# Init components
self._init_profile_table()
self._init_profile_details_tree()
self._init_screenshot_preview()
# Build two-column layout
left_col = QVBoxLayout()
left_col.addWidget(self.profile_table, 1)
left_col.addWidget(self.profile_details_tree, 0)
self.save_profile_button = QPushButton("Save current layout as new profile", self)
self.save_profile_button.clicked.connect(self.save_current_as_profile)
left_col.addWidget(self.save_profile_button)
self.save_profile_button.setEnabled(self.target_widget is not None)
# Wrap left widgets into a panel that participates in splitter sizing
left_panel = QWidget(self)
left_panel.setLayout(left_col)
left_panel.setMinimumWidth(220)
# Make the screenshot preview expand to fill remaining space
self.screenshot_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.right_box = QGroupBox("Profile Screenshot Preview", self)
right_col = QVBoxLayout(self.right_box)
right_col.addWidget(self.screenshot_label, 1)
self.splitter.addWidget(left_panel)
self.splitter.addWidget(self.right_box)
self.splitter.setStretchFactor(0, 0)
self.splitter.setStretchFactor(1, 1)
self.splitter.setSizes([350, 650])
def _init_profile_table(self):
self.profile_table = QTableWidget(self)
self.profile_table.setColumnCount(len(self.HEADERS))
self.profile_table.setHorizontalHeaderLabels(self.HEADERS)
self.profile_table.setAlternatingRowColors(True)
self.profile_table.verticalHeader().setVisible(False)
# Enforce row selection, single-select, and disable edits
self.profile_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.profile_table.setSelectionMode(QAbstractItemView.SingleSelection)
self.profile_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
# Ensure the table expands to use vertical space in the left panel
self.profile_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
header = self.profile_table.horizontalHeader()
header.setStretchLastSection(False)
header.setDefaultAlignment(Qt.AlignCenter)
class _CenterDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
option.displayAlignment = Qt.AlignCenter
self.profile_table.setItemDelegate(_CenterDelegate(self.profile_table))
header.setSectionResizeMode(self.COL_ACTIONS, QHeaderView.ResizeToContents)
header.setSectionResizeMode(self.COL_NAME, QHeaderView.Stretch)
header.setSectionResizeMode(self.COL_AUTHOR, QHeaderView.ResizeToContents)
self.render_table()
self.profile_table.itemSelectionChanged.connect(self._on_table_selection_changed)
self.profile_table.cellClicked.connect(self._on_cell_clicked)
def _init_profile_details_tree(self):
self.profile_details_tree = QTreeWidget(self)
self.profile_details_tree.setHeaderLabels(["Field", "Value"])
# Keep details compact so the table can expand
self.profile_details_tree.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
def _init_screenshot_preview(self):
self.screenshot_label = QLabel(self)
self.screenshot_label.setMinimumHeight(160)
self.screenshot_label.setAlignment(Qt.AlignCenter)
def render_table(self):
self.profile_table.setRowCount(0)
for profile in list_profiles(namespace=self.profile_namespace):
self._add_profile_row(profile)
def _add_profile_row(self, name: str):
row = self.profile_table.rowCount()
self.profile_table.insertRow(row)
actions_items = QWidget(self)
actions_items.profile_name = name
actions_items_layout = QHBoxLayout(actions_items)
actions_items_layout.setContentsMargins(0, 0, 0, 0)
info = get_profile_info(name, namespace=self.profile_namespace)
# Flags
is_active = (
self.target_widget is not None
and getattr(self.target_widget, "_current_profile_name", None) == name
)
quick = info.is_quick_select
is_read_only = info.is_read_only
# Play (green if active)
self._make_action_button(
actions_items,
"play_circle",
"Switch to this profile",
self.switch_profile,
filled=is_active,
color=(self.accent_colors.success if is_active else None),
)
# Quick-select (yellow if enabled)
self._make_action_button(
actions_items,
"star",
"Include in quick selection",
self.toggle_quick_select,
filled=quick,
color=(self.accent_colors.warning if quick else None),
)
# Delete (red, disabled when read-only)
delete_button = self._make_action_button(
actions_items,
"delete",
"Delete this profile",
self.delete_profile,
color=self.accent_colors.emergency,
)
if is_read_only:
delete_button.setEnabled(False)
delete_button.setToolTip("Bundled profiles are read-only and cannot be deleted.")
actions_items_layout.addStretch()
self.profile_table.setCellWidget(row, self.COL_ACTIONS, actions_items)
self.profile_table.setItem(row, self.COL_NAME, QTableWidgetItem(name))
self.profile_table.setItem(row, self.COL_AUTHOR, QTableWidgetItem(info.author))
def _make_action_button(
self,
parent: QWidget,
icon_name: str,
tooltip: str,
slot: callable,
*,
filled: bool = False,
color: str | None = None,
):
button = QToolButton(parent=parent)
button.setIcon(material_icon(icon_name, filled=filled, color=color))
button.setToolTip(tooltip)
button.clicked.connect(partial(slot, parent.profile_name))
parent.layout().addWidget(button)
return button
def _select_by_name(self, name: str) -> None:
for row in range(self.profile_table.rowCount()):
item = self.profile_table.item(row, self.COL_NAME)
if item and item.text() == name:
self.profile_table.selectRow(row)
break
def _current_selected_profile(self) -> str | None:
rows = self.profile_table.selectionModel().selectedRows()
if not rows:
return None
row = rows[0].row()
item = self.profile_table.item(row, self.COL_NAME)
return item.text() if item else None
def _show_profile_details(self, name: str) -> None:
info = get_profile_info(name, namespace=self.profile_namespace)
self.profile_details_tree.clear()
entries = [
("Name", info.name),
("Author", info.author or ""),
("Created", info.created or ""),
("Modified", info.modified or ""),
("Quick select", "Yes" if info.is_quick_select else "No"),
("Widgets", str(info.widget_count)),
("Size (KB)", str(info.size_kb)),
("User path", info.user_path or ""),
("Default path", info.default_path or ""),
]
for k, v in entries:
self.profile_details_tree.addTopLevelItem(QTreeWidgetItem([k, v]))
self.profile_details_tree.expandAll()
# Render screenshot preview from profile INI
pm = load_profile_screenshot(name, namespace=self.profile_namespace)
if pm is not None and not pm.isNull():
scaled = pm.scaled(
self.screenshot_label.width() or 800,
self.screenshot_label.height() or 450,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
self.screenshot_label.setPixmap(scaled)
else:
self.screenshot_label.setPixmap(QPixmap())
@SafeSlot()
def _on_table_selection_changed(self):
name = self._current_selected_profile()
if name:
self._show_profile_details(name)
@SafeSlot(int, int)
def _on_cell_clicked(self, row: int, column: int):
item = self.profile_table.item(row, self.COL_NAME)
if item:
self._show_profile_details(item.text())
##################################################
# Public Slots
##################################################
@SafeSlot(str)
def on_profile_changed(self, name: str):
"""Keep the manager in sync without forcing selection to the active profile."""
selected = self._current_selected_profile()
self.render_table()
if selected:
self._select_by_name(selected)
self._show_profile_details(selected)
@SafeSlot(str)
def switch_profile(self, profile_name: str):
self.target_widget.load_profile(profile_name)
try:
self.target_widget.toolbar.components.get_action(
"workspace_combo"
).widget.setCurrentText(profile_name)
except Exception as e:
logger.warning(f"Warning: Could not update workspace combo box. {e}")
self.render_table()
self._select_by_name(profile_name)
self._show_profile_details(profile_name)
@SafeSlot(str)
def toggle_quick_select(self, profile_name: str):
enabled = is_quick_select(profile_name, namespace=self.profile_namespace)
set_quick_select(profile_name, not enabled, namespace=self.profile_namespace)
self.render_table()
if self.target_widget is not None:
self.target_widget._refresh_workspace_list()
name = self._current_selected_profile()
if name:
self._show_profile_details(name)
@SafeSlot()
def save_current_as_profile(self):
if self.target_widget is None:
QMessageBox.information(
self,
"Save Profile",
"No workspace is associated with this manager. Attach a workspace to save profiles.",
)
return
self.target_widget.save_profile()
# AdvancedDockArea will emit profile_changed which will trigger table refresh,
# but ensure the UI stays in sync even if the signal is delayed.
self.render_table()
current = getattr(self.target_widget, "_current_profile_name", None)
if current:
self._select_by_name(current)
self._show_profile_details(current)
@SafeSlot(str)
def delete_profile(self, profile_name: str):
info = get_profile_info(profile_name, namespace=self.profile_namespace)
if info.is_read_only:
QMessageBox.information(
self, "Delete Profile", "This profile is read-only and cannot be deleted."
)
return
reply = QMessageBox.question(
self,
"Delete Profile",
(
f"Delete the profile '{profile_name}'?\n\n"
"This will remove both the user and default copies."
),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
try:
removed = delete_profile_files(profile_name, namespace=self.profile_namespace)
except OSError as exc:
QMessageBox.warning(
self, "Delete Profile", f"Failed to delete profile '{profile_name}': {exc}"
)
return
if not removed:
QMessageBox.information(
self, "Delete Profile", "No writable profile files were found to delete."
)
return
if self.target_widget is not None:
if getattr(self.target_widget, "_current_profile_name", None) == profile_name:
self.target_widget._current_profile_name = None
if hasattr(self.target_widget, "_refresh_workspace_list"):
self.target_widget._refresh_workspace_list()
self.render_table()
remaining_profiles = list_profiles(namespace=self.profile_namespace)
if remaining_profiles:
next_profile = remaining_profiles[0]
self._select_by_name(next_profile)
self._show_profile_details(next_profile)
else:
self.profile_details_tree.clear()
self.screenshot_label.setPixmap(QPixmap())
def resizeEvent(self, event):
super().resizeEvent(event)
name = self._current_selected_profile()
if not name:
return
pm = load_profile_screenshot(name, namespace=self.profile_namespace)
if pm is None or pm.isNull():
return
scaled = pm.scaled(
self.screenshot_label.width() or 800,
self.screenshot_label.height() or 450,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
self.screenshot_label.setPixmap(scaled)

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +1,16 @@
from __future__ import annotations
from bec_qthemes import material_icon
from typing import Callable
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QComboBox, QSizePolicy, QWidget
from qtpy.QtGui import QFont
from qtpy.QtWidgets import QComboBox, QSizePolicy
from bec_widgets import SafeSlot
from bec_widgets.utils.toolbars.actions import MaterialIconAction, WidgetAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents
from bec_widgets.utils.toolbars.connections import BundleConnection
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import (
is_profile_readonly,
list_profiles,
)
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import list_quick_profiles
class ProfileComboBox(QComboBox):
@@ -20,24 +18,55 @@ class ProfileComboBox(QComboBox):
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self._quick_provider: Callable[[], list[str]] = list_quick_profiles
def refresh_profiles(self):
"""Refresh the profile list with appropriate icons."""
def set_quick_profile_provider(self, provider: Callable[[], list[str]]) -> None:
self._quick_provider = provider
current_text = self.currentText()
def refresh_profiles(self, active_profile: str | None = None):
"""
Refresh the profile list and ensure the active profile is visible.
Args:
active_profile(str | None): The currently active profile name.
"""
current_text = active_profile or self.currentText()
self.blockSignals(True)
self.clear()
lock_icon = material_icon("edit_off", size=(16, 16), icon_type=QIcon)
quick_profiles = self._quick_provider()
quick_set = set(quick_profiles)
for profile in list_profiles():
if is_profile_readonly(profile):
self.addItem(lock_icon, f"{profile}")
# Set tooltip for read-only profiles
self.setItemData(self.count() - 1, "Read-only profile", Qt.ToolTipRole)
else:
self.addItem(profile)
items = list(quick_profiles)
if active_profile and active_profile not in quick_set:
items.insert(0, active_profile)
for profile in items:
self.addItem(profile)
idx = self.count() - 1
# Reset any custom styling
self.setItemData(idx, None, Qt.ItemDataRole.FontRole)
self.setItemData(idx, None, Qt.ItemDataRole.ToolTipRole)
self.setItemData(idx, None, Qt.ItemDataRole.ForegroundRole)
if active_profile and profile == active_profile:
tooltip = "Active workspace profile"
if profile not in quick_set:
font = QFont(self.font())
font.setItalic(True)
font.setBold(True)
self.setItemData(idx, font, Qt.ItemDataRole.FontRole)
self.setItemData(
idx, self.palette().highlight().color(), Qt.ItemDataRole.ForegroundRole
)
tooltip = "Active profile (not in quick select)"
self.setItemData(idx, tooltip, Qt.ItemDataRole.ToolTipRole)
self.setCurrentIndex(idx)
elif profile not in quick_set:
self.setItemData(idx, "Not in quick select", Qt.ItemDataRole.ToolTipRole)
# Restore selection if possible
index = self.findText(current_text)
@@ -45,9 +74,17 @@ class ProfileComboBox(QComboBox):
self.setCurrentIndex(index)
self.blockSignals(False)
if active_profile and self.currentText() != active_profile:
idx = self.findText(active_profile)
if idx >= 0:
self.setCurrentIndex(idx)
if active_profile and active_profile not in quick_set:
self.setToolTip("Active profile is not in quick select")
else:
self.setToolTip("")
def workspace_bundle(components: ToolbarComponents) -> ToolbarBundle:
def workspace_bundle(components: ToolbarComponents, enable_tools: bool = True) -> ToolbarBundle:
"""
Creates a workspace toolbar bundle for AdvancedDockArea.
@@ -57,22 +94,11 @@ def workspace_bundle(components: ToolbarComponents) -> ToolbarBundle:
Returns:
ToolbarBundle: The workspace toolbar bundle.
"""
# Lock icon action
components.add_safe(
"lock",
MaterialIconAction(
icon_name="lock_open_right",
tooltip="Lock Workspace",
checkable=True,
parent=components.toolbar,
),
)
# Workspace combo
combo = ProfileComboBox(parent=components.toolbar)
combo.setVisible(enable_tools)
components.add_safe("workspace_combo", WidgetAction(widget=combo, adjust_size=False))
# Save the current workspace icon
components.add_safe(
"save_workspace",
MaterialIconAction(
@@ -82,33 +108,32 @@ def workspace_bundle(components: ToolbarComponents) -> ToolbarBundle:
parent=components.toolbar,
),
)
# Delete workspace icon
components.get_action("save_workspace").action.setVisible(enable_tools)
components.add_safe(
"refresh_workspace",
"reset_default_workspace",
MaterialIconAction(
icon_name="refresh",
icon_name="undo",
tooltip="Refresh Current Workspace",
checkable=False,
parent=components.toolbar,
),
)
# Delete workspace icon
components.get_action("reset_default_workspace").action.setVisible(enable_tools)
components.add_safe(
"delete_workspace",
"manage_workspaces",
MaterialIconAction(
icon_name="delete",
tooltip="Delete Current Workspace",
checkable=False,
parent=components.toolbar,
icon_name="manage_accounts", tooltip="Manage", checkable=True, parent=components.toolbar
),
)
components.get_action("manage_workspaces").action.setVisible(enable_tools)
bundle = ToolbarBundle("workspace", components)
bundle.add_action("lock")
bundle.add_action("workspace_combo")
bundle.add_action("save_workspace")
bundle.add_action("refresh_workspace")
bundle.add_action("delete_workspace")
bundle.add_action("reset_default_workspace")
bundle.add_action("manage_workspaces")
return bundle
@@ -129,54 +154,45 @@ class WorkspaceConnection(BundleConnection):
def connect(self):
self._connected = True
# Connect the action to the target widget's method
self.components.get_action("lock").action.toggled.connect(self._lock_workspace)
self.components.get_action("save_workspace").action.triggered.connect(
self.target_widget.save_profile
)
save_action = self.components.get_action("save_workspace").action
if save_action.isVisible():
save_action.triggered.connect(self.target_widget.save_profile)
self.components.get_action("workspace_combo").widget.currentTextChanged.connect(
self.target_widget.load_profile
)
self.components.get_action("refresh_workspace").action.triggered.connect(
self._refresh_workspace
)
self.components.get_action("delete_workspace").action.triggered.connect(
self.target_widget.delete_profile
)
reset_action = self.components.get_action("reset_default_workspace").action
if reset_action.isVisible():
reset_action.triggered.connect(self._reset_workspace_to_default)
manage_action = self.components.get_action("manage_workspaces").action
if manage_action.isVisible():
manage_action.triggered.connect(self.target_widget.show_workspace_manager)
def disconnect(self):
if not self._connected:
return
# Disconnect the action from the target widget's method
self.components.get_action("lock").action.toggled.disconnect(self._lock_workspace)
self.components.get_action("save_workspace").action.triggered.disconnect(
self.target_widget.save_profile
)
save_action = self.components.get_action("save_workspace").action
if save_action.isVisible():
save_action.triggered.disconnect(self.target_widget.save_profile)
self.components.get_action("workspace_combo").widget.currentTextChanged.disconnect(
self.target_widget.load_profile
)
self.components.get_action("refresh_workspace").action.triggered.disconnect(
self._refresh_workspace
)
self.components.get_action("delete_workspace").action.triggered.disconnect(
self.target_widget.delete_profile
)
reset_action = self.components.get_action("reset_default_workspace").action
if reset_action.isVisible():
reset_action.triggered.disconnect(self._reset_workspace_to_default)
manage_action = self.components.get_action("manage_workspaces").action
if manage_action.isVisible():
manage_action.triggered.disconnect(self.target_widget.show_workspace_manager)
self._connected = False
@SafeSlot(bool)
def _lock_workspace(self, value: bool):
"""
Switches the workspace lock state and change the icon accordingly.
"""
setattr(self.target_widget, "lock_workspace", value)
self.components.get_action("lock").action.setChecked(value)
icon = material_icon("lock" if value else "lock_open_right", size=(20, 20), icon_type=QIcon)
self.components.get_action("lock").action.setIcon(icon)
@SafeSlot()
def _refresh_workspace(self):
def _reset_workspace_to_default(self):
"""
Refreshes the current workspace.
"""
combo = self.components.get_action("workspace_combo").widget
current_workspace = combo.currentText()
self.target_widget.load_profile(current_workspace)
self.target_widget.restore_user_profile_from_default()

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from bec_qthemes import material_icon
from qtpy.QtCore import QMimeData, Qt, Signal
from qtpy.QtGui import QDrag, QIcon
from qtpy.QtGui import QDrag
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QSizePolicy, QToolButton, QVBoxLayout, QWidget
from bec_widgets.utils.colors import get_theme_palette
@@ -72,10 +72,10 @@ class CollapsibleSection(QWidget):
self.header_add_button.setFixedSize(28, 28)
self.header_add_button.setToolTip("Add item")
self.header_add_button.setVisible(show_add_button)
self.header_add_button.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.header_add_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.header_add_button.setAutoRaise(True)
self.header_add_button.setIcon(material_icon("add", size=(28, 28)))
self.header_add_button.setIcon(material_icon("add", size=(28, 28), convert_to_pixmap=False))
header_layout.addWidget(self.header_add_button)
self.main_layout.addLayout(header_layout)
@@ -100,27 +100,25 @@ class CollapsibleSection(QWidget):
"""Update the header button appearance based on expanded state"""
# Use material icons with consistent sizing to match tree items
icon_name = "keyboard_arrow_down" if self.expanded else "keyboard_arrow_right"
icon = material_icon(icon_name=icon_name, size=(20, 20), icon_type=QIcon)
icon = material_icon(icon_name=icon_name, size=(20, 20), convert_to_pixmap=False)
self.header_button.setIcon(icon)
self.header_button.setText(self.title)
# Get theme colors
palette = get_theme_palette()
text_color = palette.text().color().name()
self.header_button.setStyleSheet(
f"""
QPushButton {{
"""
QPushButton {
font-weight: bold;
text-align: left;
margin: 0;
padding: 0px;
border: none;
background: transparent;
color: {text_color};
icon-size: 20px 20px;
}}
}
"""
)

View File

@@ -119,7 +119,7 @@ class NotificationToast(QFrame):
color=SEVERITY[self._kind.value]["color"],
filled=True,
size=(24, 24),
icon_type=QtGui.QIcon,
convert_to_pixmap=False,
)
)
icon_btn.setIconSize(QtCore.QSize(24, 24))
@@ -901,7 +901,7 @@ class NotificationIndicator(QWidget):
color=SEVERITY[sev.value]["color"],
filled=True,
size=(20, 20),
icon_type=QIcon,
convert_to_pixmap=False,
)
b.setIcon(icon)
b.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import QEasingCurve, QEvent, QPropertyAnimation, QSize, Qt, QTimer
@@ -21,7 +22,6 @@ from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.widgets.containers.main_window.addons.hover_widget import HoverWidget
from bec_widgets.widgets.containers.main_window.addons.notification_center.notification_banner import (
BECNotificationBroker,
@@ -35,7 +35,7 @@ from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanP
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
# Ensure the application does not use the native menu bar on macOS to be consistent with linux development.
QApplication.setAttribute(Qt.AA_DontUseNativeMenuBar, True)
QApplication.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeMenuBar, True)
class BECMainWindow(BECWidget, QMainWindow):
@@ -44,16 +44,8 @@ class BECMainWindow(BECWidget, QMainWindow):
SCAN_PROGRESS_WIDTH = 100 # px
STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds
def __init__(
self,
parent=None,
gui_id: str = None,
client=None,
window_title: str = "BEC",
*args,
**kwargs,
):
super().__init__(parent=parent, gui_id=gui_id, **kwargs)
def __init__(self, parent=None, window_title: str = "BEC", **kwargs):
super().__init__(parent=parent, **kwargs)
self.app = QApplication.instance()
self.status_bar = self.statusBar()
@@ -385,8 +377,8 @@ class BECMainWindow(BECWidget, QMainWindow):
# Help menu
help_menu = menu_bar.addMenu("Help")
help_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxQuestion)
bug_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation)
help_icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxQuestion)
bug_icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxInformation)
bec_docs = QAction("BEC Docs", self)
bec_docs.setIcon(help_icon)
@@ -457,21 +449,6 @@ class BECMainWindow(BECWidget, QMainWindow):
return super().event(event)
def cleanup(self):
central_widget = self.centralWidget()
if central_widget is not None:
central_widget.close()
central_widget.deleteLater()
if not isinstance(central_widget, BECWidget):
# if the central widget is not a BECWidget, we need to call the cleanup method
# of all widgets whose parent is the current BECMainWindow
children = self.findChildren(BECWidget)
for child in children:
ancestor = WidgetHierarchy._get_becwidget_ancestor(child)
if ancestor is self:
child.cleanup()
child.close()
child.deleteLater()
# Timer cleanup
if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
self._client_info_expire_timer.stop()

View File

@@ -7,61 +7,11 @@ import typing
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import Signal
from bec_widgets.widgets.containers.qt_ads import ads
from bec_widgets.widgets.containers.qt_ads.ads import *
# pylint: disable=unused-argument,invalid-name, missing-function-docstring, super-init-not-called
class SideBarLocation(enum.Enum):
SideBarTop = ...
SideBarLeft = ...
SideBarRight = ...
SideBarBottom = ...
SideBarNone = ...
class eBitwiseOperator(enum.Enum):
BitwiseAnd = ...
BitwiseOr = ...
class eIcon(enum.Enum):
TabCloseIcon = ...
AutoHideIcon = ...
DockAreaMenuIcon = ...
DockAreaUndockIcon = ...
DockAreaCloseIcon = ...
DockAreaMinimizeIcon = ...
IconCount = ...
class eDragState(enum.Enum):
DraggingInactive = ...
DraggingMousePressed = ...
DraggingTab = ...
DraggingFloatingWidget = ...
class TitleBarButton(enum.Enum):
TitleBarButtonTabsMenu = ...
TitleBarButtonUndock = ...
TitleBarButtonClose = ...
TitleBarButtonAutoHide = ...
TitleBarButtonMinimize = ...
class eTabIndex(enum.Enum):
TabDefaultInsertIndex = ...
TabInvalidIndex = ...
class DockWidgetArea(enum.Enum):
NoDockWidgetArea = ...
LeftDockWidgetArea = ...
RightDockWidgetArea = ...
TopDockWidgetArea = ...
BottomDockWidgetArea = ...
CenterDockWidgetArea = ...
LeftAutoHideArea = ...
RightAutoHideArea = ...
TopAutoHideArea = ...
BottomAutoHideArea = ...
InvalidDockWidgetArea = ...
OuterDockAreas = ...
AutoHideDockAreas = ...
AllDockAreas = ...
class CAutoHideDockContainer(QtWidgets.QFrame):
def __init__(
self,

View File

@@ -0,0 +1,58 @@
from __future__ import annotations
import enum
# pylint: disable=unused-argument,invalid-name, missing-function-docstring, super-init-not-called
class SideBarLocation(enum.Enum):
SideBarTop = ...
SideBarLeft = ...
SideBarRight = ...
SideBarBottom = ...
SideBarNone = ...
class eBitwiseOperator(enum.Enum):
BitwiseAnd = ...
BitwiseOr = ...
class eIcon(enum.Enum):
TabCloseIcon = ...
AutoHideIcon = ...
DockAreaMenuIcon = ...
DockAreaUndockIcon = ...
DockAreaCloseIcon = ...
DockAreaMinimizeIcon = ...
IconCount = ...
class eDragState(enum.Enum):
DraggingInactive = ...
DraggingMousePressed = ...
DraggingTab = ...
DraggingFloatingWidget = ...
class TitleBarButton(enum.Enum):
TitleBarButtonTabsMenu = ...
TitleBarButtonUndock = ...
TitleBarButtonClose = ...
TitleBarButtonAutoHide = ...
TitleBarButtonMinimize = ...
class eTabIndex(enum.Enum):
TabDefaultInsertIndex = ...
TabInvalidIndex = ...
class DockWidgetArea(enum.Enum):
NoDockWidgetArea = ...
LeftDockWidgetArea = ...
RightDockWidgetArea = ...
TopDockWidgetArea = ...
BottomDockWidgetArea = ...
CenterDockWidgetArea = ...
LeftAutoHideArea = ...
RightAutoHideArea = ...
TopAutoHideArea = ...
BottomAutoHideArea = ...
InvalidDockWidgetArea = ...
OuterDockAreas = ...
AutoHideDockAreas = ...
AllDockAreas = ...

View File

@@ -1,6 +1,5 @@
from bec_qthemes import material_icon
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QHBoxLayout, QMessageBox, QPushButton, QToolButton, QWidget
from bec_widgets.utils.bec_widget import BECWidget
@@ -24,7 +23,9 @@ class ResetButton(BECWidget, QWidget):
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
if toolbar:
icon = material_icon("restart_alt", color="#F19E39", filled=True, icon_type=QIcon)
icon = material_icon(
"restart_alt", color="#F19E39", filled=True, convert_to_pixmap=False
)
self.button = QToolButton(icon=icon)
self.button.setToolTip("Reset the scan queue")
else:

View File

@@ -1,6 +1,5 @@
from bec_qthemes import material_icon
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QToolButton, QWidget
from bec_widgets.utils.bec_widget import BECWidget
@@ -25,7 +24,7 @@ class ResumeButton(BECWidget, QWidget):
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
if toolbar:
icon = material_icon("resume", color="#2793e8", filled=True, icon_type=QIcon)
icon = material_icon("resume", color="#2793e8", filled=True, convert_to_pixmap=False)
self.button = QToolButton(icon=icon)
self.button.setToolTip("Resume the scan queue")
else:

View File

@@ -1,6 +1,5 @@
from bec_qthemes import material_icon
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QSizePolicy, QToolButton, QWidget
from bec_widgets.utils.bec_widget import BECWidget
@@ -25,7 +24,7 @@ class StopButton(BECWidget, QWidget):
self.layout.setAlignment(Qt.AlignmentFlag.AlignVCenter)
if toolbar:
icon = material_icon("stop", color="#cc181e", filled=True, icon_type=QIcon)
icon = material_icon("stop", color="#cc181e", filled=True, convert_to_pixmap=False)
self.button = QToolButton(icon=icon)
self.button.setToolTip("Stop the scan queue")
else:

View File

@@ -8,7 +8,7 @@ from bec_lib.device import Positioner
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from qtpy.QtCore import Signal
from qtpy.QtGui import QDoubleValidator, QIcon
from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDoubleSpinBox
from bec_widgets.utils import UILoader
@@ -49,6 +49,7 @@ class PositionerBox(PositionerBoxBase):
self._device = ""
self._limits = None
self._hide_device_selection = False
if self.current_path == "":
self.current_path = os.path.dirname(__file__)
@@ -87,7 +88,7 @@ class PositionerBox(PositionerBoxBase):
self.ui.setpoint.setValidator(self.setpoint_validator)
self.ui.spinner_widget.start()
self.ui.tool_button.clicked.connect(self._open_dialog_selection(self.set_positioner))
icon = material_icon(icon_name="edit_note", size=(16, 16), icon_type=QIcon)
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
self.ui.tool_button.setIcon(icon)
def force_update_readback(self):
@@ -114,11 +115,12 @@ class PositionerBox(PositionerBoxBase):
@SafeProperty(bool)
def hide_device_selection(self):
"""Hide the device selection"""
return not self.ui.tool_button.isVisible()
return self._hide_device_selection
@hide_device_selection.setter
def hide_device_selection(self, value: bool):
"""Set the device selection visibility"""
self._hide_device_selection = value
self.ui.tool_button.setVisible(not value)
@SafeSlot(bool)

View File

@@ -9,7 +9,7 @@ from bec_lib.device import Positioner
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from qtpy.QtCore import Signal
from qtpy.QtGui import QDoubleValidator, QIcon
from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDoubleSpinBox
from bec_widgets.utils import UILoader
@@ -63,6 +63,8 @@ class PositionerBox2D(PositionerBoxBase):
self._limits_hor = None
self._limits_ver = None
self._dialog = None
self._hide_device_selection = False
self._hide_device_boxes = False
if self.current_path == "":
self.current_path = os.path.dirname(__file__)
self.init_ui()
@@ -121,7 +123,7 @@ class PositionerBox2D(PositionerBoxBase):
self.ui.tool_button_ver.clicked.connect(
self._open_dialog_selection(self.set_positioner_ver)
)
icon = material_icon(icon_name="edit_note", size=(16, 16), icon_type=QIcon)
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
self.ui.tool_button_hor.setIcon(icon)
self.ui.tool_button_ver.setIcon(icon)
@@ -213,22 +215,24 @@ class PositionerBox2D(PositionerBoxBase):
@SafeProperty(bool)
def hide_device_selection(self):
"""Hide the device selection"""
return not self.ui.tool_button_hor.isVisible()
return self._hide_device_selection
@hide_device_selection.setter
def hide_device_selection(self, value: bool):
"""Set the device selection visibility"""
self._hide_device_selection = value
self.ui.tool_button_hor.setVisible(not value)
self.ui.tool_button_ver.setVisible(not value)
@SafeProperty(bool)
def hide_device_boxes(self):
"""Hide the device selection"""
return not self.ui.device_box_hor.isVisible()
return self._hide_device_boxes
@hide_device_boxes.setter
def hide_device_boxes(self, value: bool):
"""Set the device selection visibility"""
self._hide_device_boxes = value
self.ui.device_box_hor.setVisible(not value)
self.ui.device_box_ver.setVisible(not value)
@@ -315,7 +319,7 @@ class PositionerBox2D(PositionerBoxBase):
"tweak_decrease": self.ui.tweak_decrease_hor,
"units": self.ui.units_hor,
}
elif device == "vertical":
if device == "vertical":
return {
"spinner": self.ui.spinner_widget_ver,
"position_indicator": self.ui.position_indicator_ver,
@@ -328,8 +332,7 @@ class PositionerBox2D(PositionerBoxBase):
"tweak_decrease": self.ui.tweak_decrease_ver,
"units": self.ui.units_ver,
}
else:
raise ValueError(f"Device {device} is not represented by this UI")
raise ValueError(f"Device {device} is not represented by this UI")
def _device_ui_components(self, device: str):
if device == self.device_hor:

View File

@@ -91,6 +91,11 @@ class ScanControl(BECWidget, QWidget):
self._scan_metadata: dict | None = None
self._metadata_form = ScanMetadata(parent=self)
self._hide_arg_box = False
self._hide_kwarg_boxes = False
self._hide_scan_control_buttons = False
self._hide_metadata = False
self._hide_scan_selection_combobox = False
# Create and set main layout
self._init_UI()
@@ -120,7 +125,7 @@ class ScanControl(BECWidget, QWidget):
# Label to reload the last scan parameters within scan selection group box
self.toggle_layout = QHBoxLayout()
self.toggle_layout.addSpacerItem(
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed)
QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
)
self.last_scan_label = QLabel("Restore last scan parameters", self.scan_selection_group)
self.toggle = ToggleSwitch(parent=self.scan_selection_group, checked=False)
@@ -128,12 +133,16 @@ class ScanControl(BECWidget, QWidget):
self.toggle_layout.addWidget(self.last_scan_label)
self.toggle_layout.addWidget(self.toggle)
self.scan_selection_group.layout().addLayout(self.toggle_layout)
self.scan_selection_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.scan_selection_group.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed
)
self.layout.addWidget(self.scan_selection_group)
# Scan control (Run/Stop) buttons
self.scan_control_group = QWidget(self)
self.scan_control_group.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.scan_control_group.setSizePolicy(
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed
)
self.button_layout = QHBoxLayout(self.scan_control_group)
self.button_run_scan = QPushButton("Start", self.scan_control_group)
self.button_run_scan.setProperty("variant", "success")
@@ -262,9 +271,7 @@ class ScanControl(BECWidget, QWidget):
@SafeProperty(bool)
def hide_arg_box(self):
"""Property to hide the argument box."""
if self.arg_box is None:
return True
return not self.arg_box.isVisible()
return self._hide_arg_box
@hide_arg_box.setter
def hide_arg_box(self, hide: bool):
@@ -273,18 +280,14 @@ class ScanControl(BECWidget, QWidget):
Args:
hide(bool): Hide or show the argument box.
"""
self._hide_arg_box = hide
if self.arg_box is not None:
self.arg_box.setVisible(not hide)
@SafeProperty(bool)
def hide_kwarg_boxes(self):
"""Property to hide the keyword argument boxes."""
if len(self.kwarg_boxes) == 0:
return True
for box in self.kwarg_boxes:
if box is not None:
return not box.isVisible()
return self._hide_kwarg_boxes
@hide_kwarg_boxes.setter
def hide_kwarg_boxes(self, hide: bool):
@@ -293,6 +296,7 @@ class ScanControl(BECWidget, QWidget):
Args:
hide(bool): Hide or show the keyword argument boxes.
"""
self._hide_kwarg_boxes = hide
if len(self.kwarg_boxes) > 0:
for box in self.kwarg_boxes:
box.setVisible(not hide)
@@ -300,7 +304,7 @@ class ScanControl(BECWidget, QWidget):
@SafeProperty(bool)
def hide_scan_control_buttons(self):
"""Property to hide the scan control buttons."""
return not self.button_run_scan.isVisible()
return self._hide_scan_control_buttons
@hide_scan_control_buttons.setter
def hide_scan_control_buttons(self, hide: bool):
@@ -309,12 +313,13 @@ class ScanControl(BECWidget, QWidget):
Args:
hide(bool): Hide or show the scan control buttons.
"""
self._hide_scan_control_buttons = hide
self.show_scan_control_buttons(not hide)
@SafeProperty(bool)
def hide_metadata(self):
"""Property to hide the metadata form."""
return not self._metadata_form.isVisible()
return self._hide_metadata
@hide_metadata.setter
def hide_metadata(self, hide: bool):
@@ -323,6 +328,7 @@ class ScanControl(BECWidget, QWidget):
Args:
hide(bool): Hide or show the metadata form.
"""
self._hide_metadata = hide
self._metadata_form.setVisible(not hide)
@SafeProperty(bool)
@@ -342,12 +348,13 @@ class ScanControl(BECWidget, QWidget):
@SafeSlot(bool)
def show_scan_control_buttons(self, show: bool):
"""Shows or hides the scan control buttons."""
self._hide_scan_control_buttons = not show
self.scan_control_group.setVisible(show)
@SafeProperty(bool)
def hide_scan_selection_combobox(self):
"""Property to hide the scan selection combobox."""
return not self.comboBox_scan_selection.isVisible()
return self._hide_scan_selection_combobox
@hide_scan_selection_combobox.setter
def hide_scan_selection_combobox(self, hide: bool):
@@ -356,11 +363,13 @@ class ScanControl(BECWidget, QWidget):
Args:
hide(bool): Hide or show the scan selection combobox.
"""
self._hide_scan_selection_combobox = hide
self.show_scan_selection_combobox(not hide)
@SafeSlot(bool)
def show_scan_selection_combobox(self, show: bool):
"""Shows or hides the scan selection combobox."""
self._hide_scan_selection_combobox = not show
self.scan_selection_group.setVisible(show)
@SafeSlot(str)
@@ -412,9 +421,10 @@ class ScanControl(BECWidget, QWidget):
position = self.ARG_BOX_POSITION + (1 if self.arg_box is not None else 0)
for group in groups:
box = ScanGroupBox(box_type="kwargs", config=group)
box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
box.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
self.layout.insertWidget(position + len(self.kwarg_boxes), box)
self.kwarg_boxes.append(box)
box.setVisible(not self._hide_kwarg_boxes)
def add_arg_group(self, group: dict):
"""
@@ -424,9 +434,10 @@ class ScanControl(BECWidget, QWidget):
"""
self.arg_box = ScanGroupBox(box_type="args", config=group)
self.arg_box.device_selected.connect(self.emit_device_selected)
self.arg_box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
self.arg_box.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
self.arg_box.hide_add_remove_buttons = self._hide_add_remove_buttons
self.layout.insertWidget(self.ARG_BOX_POSITION, self.arg_box)
self.arg_box.setVisible(not self._hide_arg_box)
@SafeSlot(str)
def emit_device_selected(self, dev_names):

View File

@@ -3,7 +3,6 @@ from typing import Literal, Sequence
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from qtpy.QtCore import Property, Qt, Signal, Slot
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import (
QCheckBox,
QComboBox,
@@ -198,12 +197,12 @@ class ScanGroupBox(QGroupBox):
# Add bundle button
self.button_add_bundle = QPushButton(self)
self.button_add_bundle.setIcon(
material_icon(icon_name="add", size=(15, 15), icon_type=QIcon)
material_icon(icon_name="add", size=(15, 15), convert_to_pixmap=False)
)
# Remove bundle button
self.button_remove_bundle = QPushButton(self)
self.button_remove_bundle.setIcon(
material_icon(icon_name="remove", size=(15, 15), icon_type=QIcon)
material_icon(icon_name="remove", size=(15, 15), convert_to_pixmap=False)
)
hbox_layout.addWidget(self.button_add_bundle)
hbox_layout.addWidget(self.button_remove_bundle)

View File

@@ -65,6 +65,9 @@ class LMFitDialog(BECWidget, QWidget):
self._move_buttons = []
self._accent_colors = get_accent_colors()
self.action_buttons = {}
self._hide_curve_selection = False
self._hide_summary = False
self._hide_parameters = False
@property
def enable_actions(self) -> bool:
@@ -108,7 +111,7 @@ class LMFitDialog(BECWidget, QWidget):
@SafeProperty(bool)
def hide_curve_selection(self):
"""SafeProperty for showing the curve selection."""
return not self.ui.group_curve_selection.isVisible()
return self._hide_curve_selection
@hide_curve_selection.setter
def hide_curve_selection(self, show: bool):
@@ -117,12 +120,13 @@ class LMFitDialog(BECWidget, QWidget):
Args:
show (bool): Whether to show the curve selection.
"""
self._hide_curve_selection = show
self.ui.group_curve_selection.setVisible(not show)
@SafeProperty(bool)
def hide_summary(self) -> bool:
"""SafeProperty for showing the summary."""
return not self.ui.group_summary.isVisible()
return self._hide_summary
@hide_summary.setter
def hide_summary(self, show: bool):
@@ -131,12 +135,13 @@ class LMFitDialog(BECWidget, QWidget):
Args:
show (bool): Whether to show the summary.
"""
self._hide_summary = show
self.ui.group_summary.setVisible(not show)
@SafeProperty(bool)
def hide_parameters(self) -> bool:
"""SafeProperty for showing the parameters."""
return not self.ui.group_parameters.isVisible()
return self._hide_parameters
@hide_parameters.setter
def hide_parameters(self, show: bool):
@@ -145,6 +150,7 @@ class LMFitDialog(BECWidget, QWidget):
Args:
show (bool): Whether to show the parameters.
"""
self._hide_parameters = show
self.ui.group_parameters.setVisible(not show)
@property

View File

@@ -7,17 +7,16 @@ from typing import Any, cast
from bec_lib.logger import bec_logger
from bec_lib.macro_update_handler import has_executable_code
from qtpy.QtCore import QEvent, QTimer, Signal
from qtpy.QtWidgets import QFileDialog, QMessageBox, QToolButton, QVBoxLayout, QWidget
from qtpy.QtWidgets import QFileDialog, QMessageBox, QToolButton, QWidget
import bec_widgets.widgets.containers.ads as QtAds
from bec_widgets import BECWidget
from bec_widgets.widgets.containers.ads import CDockAreaWidget, CDockWidget
from bec_widgets.widgets.containers.advanced_dock_area.basic_dock_area import DockAreaWidget
from bec_widgets.widgets.containers.qt_ads import CDockAreaWidget, CDockWidget
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
logger = bec_logger.logger
class MonacoDock(BECWidget, QWidget):
class MonacoDock(DockAreaWidget):
"""
MonacoDock is a dock widget that contains Monaco editor instances.
It is used to manage multiple Monaco editors in a dockable interface.
@@ -29,55 +28,34 @@ class MonacoDock(BECWidget, QWidget):
macro_file_updated = Signal(str) # Emitted when a macro file is saved
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
# Top-level layout hosting a toolbar and the dock manager
self._root_layout = QVBoxLayout(self)
self._root_layout.setContentsMargins(0, 0, 0, 0)
self._root_layout.setSpacing(0)
self.dock_manager = QtAds.CDockManager(self)
self.dock_manager.setStyleSheet("")
super().__init__(
parent=parent,
variant="compact",
title="Monaco Editors",
default_add_direction="top",
**kwargs,
)
self.dock_manager.focusedDockWidgetChanged.connect(self._on_focus_event)
self._root_layout.addWidget(self.dock_manager)
self.dock_manager.installEventFilter(self)
self._last_focused_editor: CDockWidget | None = None
self.focused_editor.connect(self._on_last_focused_editor_changed)
self.add_editor()
self._open_files = {}
initial_editor = self.add_editor()
if isinstance(initial_editor, CDockWidget):
self.last_focused_editor = initial_editor
def _create_editor(self):
def _create_editor_widget(self) -> MonacoWidget:
"""Create a configured Monaco editor widget."""
init_lsp = len(self.dock_manager.dockWidgets()) == 0
widget = MonacoWidget(self, init_lsp=init_lsp)
widget.save_enabled.connect(self.save_enabled.emit)
widget.editor.signature_help_triggered.connect(self._on_signature_change)
count = len(self.dock_manager.dockWidgets())
dock = CDockWidget(f"Untitled_{count + 1}")
dock.setWidget(widget)
# Connect to modification status changes to update tab titles
widget.save_enabled.connect(
lambda modified: self._update_tab_title_for_modification(dock, modified)
)
dock.setFeature(CDockWidget.DockWidgetFeature.DockWidgetDeleteOnClose, True)
dock.setFeature(CDockWidget.DockWidgetFeature.CustomCloseHandling, True)
dock.setFeature(CDockWidget.DockWidgetFeature.DockWidgetClosable, True)
dock.setFeature(CDockWidget.DockWidgetFeature.DockWidgetFloatable, False)
dock.setFeature(CDockWidget.DockWidgetFeature.DockWidgetMovable, True)
dock.closeRequested.connect(lambda: self._on_editor_close_requested(dock, widget))
return dock
return widget
@property
def last_focused_editor(self) -> CDockWidget | None:
"""
Get the last focused editor.
"""
dock_widget = self.dock_manager.focusedDockWidget()
if dock_widget is not None and isinstance(dock_widget.widget(), MonacoWidget):
self.last_focused_editor = dock_widget
return self._last_focused_editor
@last_focused_editor.setter
@@ -164,9 +142,12 @@ class MonacoDock(BECWidget, QWidget):
# Temporarily disable read-only mode if the editor is read-only
# so we can clear the content for reuse
monaco_widget.set_readonly(False)
monaco_widget.set_text("")
monaco_widget.set_text("", reset=True)
dock.setWindowTitle("Untitled")
dock.setTabToolTip("Untitled")
monaco_widget.metadata["scope"] = ""
icon = self._resolve_dock_icon(monaco_widget, dock_icon=None, apply_widget_icon=True)
dock.setIcon(icon)
return
# Otherwise, proceed to close and delete the dock
@@ -221,37 +202,71 @@ class MonacoDock(BECWidget, QWidget):
def add_editor(
self, area: Any | None = None, title: str | None = None, tooltip: str | None = None
): # Any as qt ads does not return a proper type
) -> CDockWidget:
"""
Adds a new Monaco editor dock widget to the dock manager.
Add a new Monaco editor dock to the specified area.
Args:
area(Any | None): The area to add the editor to. If None, adds to the main area.
title(str | None): The title of the editor tab. If None, a default title is used.
tooltip(str | None): The tooltip for the editor tab. If None, no tooltip is set.
Returns:
CDockWidget: The created dock widget containing the Monaco editor.
"""
new_dock = self._create_editor()
if title is not None:
new_dock.setWindowTitle(title)
widget = self._create_editor_widget()
existing_count = len(self.dock_manager.dockWidgets())
default_title = title or f"Untitled_{existing_count + 1}"
tab_target: CDockWidget | None = None
if isinstance(area, CDockAreaWidget):
tab_target = area.currentDockWidget()
if tab_target is None:
docks = area.dockWidgets()
tab_target = docks[0] if docks else None
dock = self.new(
widget,
closable=True,
floatable=False,
movable=True,
tab_with=tab_target,
return_dock=True,
on_close=self._on_editor_close_requested,
title_buttons={"float": False},
where="right",
)
dock.setWindowTitle(default_title)
if tooltip is not None:
new_dock.setTabToolTip(tooltip)
if area is None:
area_obj = self.dock_manager.addDockWidgetTab(
QtAds.DockWidgetArea.TopDockWidgetArea, new_dock
)
self._ensure_area_plus(area_obj)
else:
# If an area is provided, add the dock to that area
self.dock_manager.addDockWidgetTabToArea(new_dock, area)
self._ensure_area_plus(area)
dock.setTabToolTip(tooltip)
widget.save_enabled.connect(
lambda modified, target=dock: self._update_tab_title_for_modification(target, modified)
)
area_widget = dock.dockAreaWidget()
if area_widget is not None:
self._ensure_area_plus(area_widget)
QTimer.singleShot(0, self._scan_and_fix_areas)
return new_dock
self.last_focused_editor = dock
return dock
def open_file(self, file_name: str, scope: str | None = None) -> None:
def open_file(self, file_name: str, scope: str = "") -> None:
"""
Open a file in the specified area. If the file is already open, activate it.
Args:
file_name (str): The path to the file to open.
scope (str): The scope to set for the editor metadata.
"""
open_files = self._get_open_files()
if file_name in open_files:
dock = self._get_editor_dock(file_name)
if dock is not None:
dock.setAsCurrentTab()
self.last_focused_editor = dock
return
file = os.path.basename(file_name)
@@ -274,17 +289,17 @@ class MonacoDock(BECWidget, QWidget):
editor_dock.setWindowTitle(file)
editor_dock.setTabToolTip(file_name)
editor_widget.open_file(file_name)
if scope is not None:
editor_widget.metadata["scope"] = scope
editor_widget.metadata["scope"] = scope
self.last_focused_editor = editor_dock
return
# File is not open, create a new editor
editor_dock = self.add_editor(title=file, tooltip=file_name)
widget = cast(MonacoWidget, editor_dock.widget())
widget.open_file(file_name)
if scope is not None:
widget.metadata["scope"] = scope
widget.metadata["scope"] = scope
editor_dock.setAsCurrentTab()
self.last_focused_editor = editor_dock
def save_file(
self, widget: MonacoWidget | None = None, force_save_as: bool = False, format_on_save=True
@@ -415,7 +430,7 @@ class MonacoDock(BECWidget, QWidget):
open_files.append(editor_widget.current_file)
return open_files
def _get_editor_dock(self, file_name: str) -> QtAds.CDockWidget | None:
def _get_editor_dock(self, file_name: str) -> CDockWidget | None:
for widget in self.dock_manager.dockWidgets():
editor_widget = cast(MonacoWidget, widget.widget())
if editor_widget.current_file == file_name:

View File

@@ -49,6 +49,7 @@ class ScanMetadata(PydanticModelForm):
self._scan_name = scan_name or ""
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
self._additional_metadata.data_changed.connect(self.validate_form)
self._hide_optional_metadata = False
super().__init__(parent=parent, data_model=self._md_schema, client=client, **kwargs)
@@ -63,7 +64,7 @@ class ScanMetadata(PydanticModelForm):
@SafeProperty(bool)
def hide_optional_metadata(self): # type: ignore
"""Property to hide the optional metadata table."""
return not self._additional_md_box.isVisible()
return self._hide_optional_metadata
@hide_optional_metadata.setter
def hide_optional_metadata(self, hide: bool):
@@ -72,6 +73,7 @@ class ScanMetadata(PydanticModelForm):
Args:
hide(bool): Hide or show the optional metadata table.
"""
self._hide_optional_metadata = hide
self._additional_md_box.setVisible(not hide)
def get_form_data(self):

View File

@@ -4,7 +4,7 @@ import time
from bec_qthemes import material_icon
from qtpy.QtCore import QSize, Qt, QTimer, Signal, Slot
from qtpy.QtGui import QBrush, QColor, QPainter, QPen, QPixmap
from qtpy.QtGui import QBrush, QColor, QPainter, QPen
from qtpy.QtWidgets import (
QApplication,
QComboBox,
@@ -87,7 +87,7 @@ class Pos(QWidget):
if self.is_revealed:
if self.is_mine:
p.drawPixmap(r, material_icon("experiment", icon_type=QPixmap, filled=True))
p.drawPixmap(r, material_icon("experiment", convert_to_pixmap=True, filled=True))
elif self.adjacent_n > 0:
pen = QPen(NUM_COLORS[self.adjacent_n])
@@ -103,7 +103,7 @@ class Pos(QWidget):
material_icon(
"flag",
size=(50, 50),
icon_type=QPixmap,
convert_to_pixmap=True,
filled=True,
color=self.palette().base().color(),
),
@@ -376,13 +376,13 @@ class Minesweeper(BECWidget, QWidget):
self.status = status
match status:
case GameStatus.READY:
icon = material_icon(icon_name="add", icon_type=QIcon)
icon = material_icon(icon_name="add", convert_to_pixmap=False)
case GameStatus.PLAYING:
icon = material_icon(icon_name="smart_toy", icon_type=QIcon)
icon = material_icon(icon_name="smart_toy", convert_to_pixmap=False)
case GameStatus.FAILED:
icon = material_icon(icon_name="error", icon_type=QIcon)
icon = material_icon(icon_name="error", convert_to_pixmap=False)
case GameStatus.SUCCESS:
icon = material_icon(icon_name="celebration", icon_type=QIcon)
icon = material_icon(icon_name="celebration", convert_to_pixmap=False)
self.reset_button.setIcon(icon)
def update_timer(self):

View File

@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Literal
from bec_lib import bec_logger
from bec_qthemes import material_icon
from qtpy.QtCore import QEvent, Qt
from qtpy.QtGui import QColor, QIcon
from qtpy.QtGui import QColor
from qtpy.QtWidgets import (
QColorDialog,
QHBoxLayout,
@@ -62,7 +62,7 @@ class ROILockButton(QToolButton):
movable = self._roi.movable
self.setChecked(not movable)
icon = "lock_open_right" if movable else "lock"
self.setIcon(material_icon(icon, size=(20, 20), icon_type=QIcon))
self.setIcon(material_icon(icon, size=(20, 20), convert_to_pixmap=False))
class ROIPropertyTree(BECWidget, QWidget):
@@ -209,11 +209,11 @@ class ROIPropertyTree(BECWidget, QWidget):
if on:
# switched to expanded state
self.tree.expandAll()
new_icon = material_icon("unfold_less", size=(20, 20), icon_type=QIcon)
new_icon = material_icon("unfold_less", size=(20, 20), convert_to_pixmap=False)
else:
# collapsed state
self.tree.collapseAll()
new_icon = material_icon("unfold_more", size=(20, 20), icon_type=QIcon)
new_icon = material_icon("unfold_more", size=(20, 20), convert_to_pixmap=False)
self.expand_toggle.action.setIcon(new_icon)
self.expand_toggle.action.toggled.connect(_exp_toggled)
@@ -231,7 +231,7 @@ class ROIPropertyTree(BECWidget, QWidget):
for r in self.controller.rois:
r.movable = not checked
new_icon = material_icon(
"lock" if checked else "lock_open_right", size=(20, 20), icon_type=QIcon
"lock" if checked else "lock_open_right", size=(20, 20), convert_to_pixmap=False
)
self.lock_all_action.action.setIcon(new_icon)
@@ -402,7 +402,11 @@ class ROIPropertyTree(BECWidget, QWidget):
# delete button
del_btn = QToolButton()
delete_icon = material_icon(
"delete", size=(20, 20), icon_type=QIcon, filled=False, color=self.DELETE_BUTTON_COLOR
"delete",
size=(20, 20),
convert_to_pixmap=False,
filled=False,
color=self.DELETE_BUTTON_COLOR,
)
del_btn.setIcon(delete_icon)
del_btn.clicked.connect(lambda _=None, r=roi: self._delete_roi(r))

View File

@@ -129,6 +129,12 @@ class PlotBase(BECWidget, QWidget):
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
# Visibility States
self._toolbar_visible = True
self._enable_fps_monitor = False
self._outer_axes_visible = self.plot_item.getAxis("top").isVisible()
self._inner_axes_visible = self.plot_item.getAxis("bottom").isVisible()
self.toolbar = ModularToolBar(parent=self, orientation="horizontal")
self._init_toolbar()
@@ -294,7 +300,7 @@ class PlotBase(BECWidget, QWidget):
"""
Show Toolbar.
"""
return self.toolbar.isVisible()
return self._toolbar_visible
@enable_toolbar.setter
def enable_toolbar(self, value: bool):
@@ -304,6 +310,7 @@ class PlotBase(BECWidget, QWidget):
Args:
value(bool): The value to set.
"""
self._toolbar_visible = value
self.toolbar.setVisible(value)
@SafeProperty(bool, doc="Enable the FPS monitor.")
@@ -311,7 +318,7 @@ class PlotBase(BECWidget, QWidget):
"""
Enable the FPS monitor.
"""
return self.fps_label.isVisible()
return self._enable_fps_monitor
@enable_fps_monitor.setter
def enable_fps_monitor(self, value: bool):
@@ -321,9 +328,11 @@ class PlotBase(BECWidget, QWidget):
Args:
value(bool): The value to set.
"""
if value and self.fps_monitor is None:
if value == self._enable_fps_monitor:
return
if value:
self.hook_fps_monitor()
elif not value and self.fps_monitor is not None:
else:
self.unhook_fps_monitor()
################################################################################
@@ -796,7 +805,7 @@ class PlotBase(BECWidget, QWidget):
"""
Show the outer axes of the plot widget.
"""
return self.plot_item.getAxis("top").isVisible()
return self._outer_axes_visible
@outer_axes.setter
def outer_axes(self, value: bool):
@@ -809,6 +818,7 @@ class PlotBase(BECWidget, QWidget):
self.plot_item.showAxis("top", value)
self.plot_item.showAxis("right", value)
self._outer_axes_visible = value
self.property_changed.emit("outer_axes", value)
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
@@ -816,7 +826,7 @@ class PlotBase(BECWidget, QWidget):
"""
Show inner axes of the plot widget.
"""
return self.plot_item.getAxis("bottom").isVisible()
return self._inner_axes_visible
@inner_axes.setter
def inner_axes(self, value: bool):
@@ -829,6 +839,7 @@ class PlotBase(BECWidget, QWidget):
self.plot_item.showAxis("bottom", value)
self.plot_item.showAxis("left", value)
self._inner_axes_visible = value
self._apply_x_label()
self._apply_y_label()
self.property_changed.emit("inner_axes", value)
@@ -969,6 +980,7 @@ class PlotBase(BECWidget, QWidget):
self.fps_monitor.sigFpsUpdate.connect(self.update_fps_label)
self.update_fps_label(0)
self._enable_fps_monitor = True
def unhook_fps_monitor(self, delete_label=True):
"""Unhook the FPS monitor from the plot."""
@@ -980,6 +992,7 @@ class PlotBase(BECWidget, QWidget):
if self.fps_label is not None:
# Hide Label
self.fps_label.hide()
self._enable_fps_monitor = False
################################################################################
# Crosshair

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from bec_qthemes._icon.material_icons import material_icon
from qtpy.QtGui import QIcon, QValidator
from qtpy.QtGui import QValidator
class ScanIndexValidator(QValidator):
@@ -184,7 +184,11 @@ class CurveRow(QTreeWidgetItem):
# Delete button
self.delete_button = QToolButton()
delete_icon = material_icon(
"delete", size=(20, 20), icon_type=QIcon, filled=False, color=self.DELETE_BUTTON_COLOR
"delete",
size=(20, 20),
convert_to_pixmap=False,
filled=False,
color=self.DELETE_BUTTON_COLOR,
)
self.delete_button.setIcon(delete_icon)
self.delete_button.clicked.connect(lambda: self.remove_self())
@@ -196,7 +200,7 @@ class CurveRow(QTreeWidgetItem):
analysis_icon = material_icon(
"monitoring",
size=(20, 20),
icon_type=QIcon,
convert_to_pixmap=False,
filled=False,
color=self.app.theme.colors["FG"].toTuple(),
)

View File

@@ -145,6 +145,9 @@ class ScanProgressBar(BECWidget, QWidget):
self.layout.addWidget(self.ui)
self.setLayout(self.layout)
self.progressbar = self.ui.progressbar
self._show_elapsed_time = self.ui.elapsed_time_label.isVisible()
self._show_remaining_time = self.ui.remaining_time_label.isVisible()
self._show_source_label = self.ui.source_label.isVisible()
self.connect_to_queue()
self._progress_source = None
@@ -221,30 +224,33 @@ class ScanProgressBar(BECWidget, QWidget):
@SafeProperty(bool)
def show_elapsed_time(self):
return self.ui.elapsed_time_label.isVisible()
return self._show_elapsed_time
@show_elapsed_time.setter
def show_elapsed_time(self, value):
self._show_elapsed_time = value
self.ui.elapsed_time_label.setVisible(value)
if hasattr(self.ui, "dash"):
self.ui.dash.setVisible(value)
@SafeProperty(bool)
def show_remaining_time(self):
return self.ui.remaining_time_label.isVisible()
return self._show_remaining_time
@show_remaining_time.setter
def show_remaining_time(self, value):
self._show_remaining_time = value
self.ui.remaining_time_label.setVisible(value)
if hasattr(self.ui, "dash"):
self.ui.dash.setVisible(value)
@SafeProperty(bool)
def show_source_label(self):
return self.ui.source_label.isVisible()
return self._show_source_label
@show_source_label.setter
def show_source_label(self, value):
self._show_source_label = value
self.ui.source_label.setVisible(value)
def update_labels(self):

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from bec_lib.endpoints import MessageEndpoints
from bec_qthemes import material_icon
from qtpy.QtCore import Property, Qt, Signal, Slot
from qtpy.QtGui import QColor, QIcon
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QHeaderView, QLabel, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
@@ -51,6 +51,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
self._toolbar_hidden = False
# Set up the toolbar
self.set_toolbar()
@@ -104,7 +105,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
@Property(bool)
def hide_toolbar(self):
"""Property to hide the BEC Queue toolbar."""
return not self.toolbar.isVisible()
return self._toolbar_hidden
@hide_toolbar.setter
def hide_toolbar(self, hide: bool):
@@ -123,6 +124,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
Args:
hide(bool): Whether to hide the toolbar.
"""
self._toolbar_hidden = hide
self.toolbar.setVisible(not hide)
def refresh_queue(self):
@@ -197,7 +199,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
if not content or not isinstance(content, str):
content = ""
item = QTableWidgetItem(content)
item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
# item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable)
if status:
@@ -240,7 +242,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
abort_button.button.setText("")
abort_button.button.setIcon(
material_icon("cancel", color="#cc181e", filled=True, icon_type=QIcon)
material_icon("cancel", color="#cc181e", filled=True, convert_to_pixmap=False)
)
abort_button.setStyleSheet(
"""

View File

@@ -10,7 +10,6 @@ from bec_lib.messages import ConfigAction, ScanStatusMessage
from bec_qthemes import material_icon
from pyqtgraph import SignalProxy
from qtpy.QtCore import QThreadPool, Signal
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QFileDialog, QListWidget, QToolButton, QVBoxLayout, QWidget
from bec_widgets.cli.rpc.rpc_register import RPCRegister
@@ -98,7 +97,7 @@ class DeviceBrowser(BECWidget, QWidget):
def init_tool_buttons(self):
def _setup_button(button: QToolButton, icon: str, slot: Callable, tooltip: str = ""):
button.clicked.connect(slot)
button.setIcon(material_icon(icon, size=(20, 20), icon_type=QIcon))
button.setIcon(material_icon(icon, size=(20, 20), convert_to_pixmap=False))
button.setToolTip(tooltip)
_setup_button(self.ui.add_button, "add", self._create_add_dialog, "add new device")

View File

@@ -1,7 +1,6 @@
from bec_lib.device import Device
from bec_qthemes import material_icon
from qtpy.QtCore import Qt
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
@@ -48,7 +47,9 @@ class SignalDisplay(BECWidget, QWidget):
button_holder.layout().setAlignment(Qt.AlignmentFlag.AlignRight)
button_holder.layout().setContentsMargins(0, 0, 0, 0)
refresh_button = QToolButton()
refresh_button.setIcon(material_icon(icon_name="refresh", size=(20, 20), icon_type=QIcon))
refresh_button.setIcon(
material_icon(icon_name="refresh", size=(20, 20), convert_to_pixmap=False)
)
refresh_button.clicked.connect(self._refresh)
button_holder.layout().addWidget(refresh_button)
self._content_layout.addWidget(button_holder)

View File

@@ -117,7 +117,9 @@ class IDEExplorer(BECWidget, QWidget):
show_add_button=True,
tooltip="Macros are reusable functions that can be called from scripts or the console.",
)
section.header_add_button.setIcon(material_icon("refresh", size=(20, 20)))
section.header_add_button.setIcon(
material_icon("refresh", size=(20, 20), convert_to_pixmap=False)
)
section.header_add_button.setToolTip("Reload all macros")
section.header_add_button.clicked.connect(self._reload_macros)

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
@@ -20,6 +20,8 @@ class IDEExplorerPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = IDEExplorer(parent)
return t

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
from bec_qthemes import material_icon
from qtpy.QtCore import Property, Qt, Slot
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QHBoxLayout, QPushButton, QToolButton, QWidget
from bec_widgets.utils.bec_widget import BECWidget
@@ -24,6 +23,7 @@ class DarkModeButton(BECWidget, QWidget):
**kwargs,
) -> None:
super().__init__(parent=parent, client=client, gui_id=gui_id, theme_update=True, **kwargs)
self.setProperty("skip_settings", True)
self._dark_mode_enabled = False
self.layout = QHBoxLayout(self)
@@ -90,7 +90,9 @@ class DarkModeButton(BECWidget, QWidget):
def update_mode_button(self):
icon = material_icon(
"light_mode" if self.dark_mode_enabled else "dark_mode", size=(20, 20), icon_type=QIcon
"light_mode" if self.dark_mode_enabled else "dark_mode",
size=(20, 20),
convert_to_pixmap=False,
)
self.mode_button.setIcon(icon)
self.mode_button.setToolTip("Set Light Mode" if self.dark_mode_enabled else "Set Dark Mode")

View File

@@ -19,7 +19,7 @@ dependencies = [
"black~=25.0", # needed for bw-generate-cli
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
"pydantic~=2.0",
"pyqtgraph~=0.13",
"pyqtgraph==0.13.7",
"PySide6==6.9.0",
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
"qtpy~=2.4",

View File

@@ -12,7 +12,7 @@ from bec_lib.scan_history import ScanHistory
from bec_widgets.tests.utils import DEVICES, DMMock, FakePositioner, Positioner
def fake_redis_server(host, port):
def fake_redis_server(host, port, **kwargs):
redis = fakeredis.FakeRedis()
return redis

File diff suppressed because it is too large Load Diff

View File

@@ -193,21 +193,6 @@ def test_crosshair_changed_signal(plot_widget_with_crosshair):
assert np.isclose(y, 5)
def test_marker_positions_after_mouse_move(plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair
pos_in_view = QPointF(2, 5)
pos_in_scene = plot_item.vb.mapViewToScene(pos_in_view)
event_mock = [pos_in_scene]
crosshair.mouse_moved(event_mock)
marker = crosshair.marker_moved_1d["Curve 1"]
marker_x, marker_y = marker.getData()
assert marker_x == [2]
assert marker_y == [5]
def test_crosshair_clicked_signal(qtbot, plot_widget_with_crosshair):
crosshair, plot_item = plot_widget_with_crosshair