mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-16 10:03:03 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c40ce53955 | |||
| 2b22a7065c | |||
| 9146c5194e | |||
| 5fde0c6efc | |||
| 68903fc6ae |
@@ -1,6 +1,22 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v3.16.0 (2026-06-15)
|
||||
|
||||
### Features
|
||||
|
||||
- **screenshot**: Add option to upload to SciLog
|
||||
([`9146c51`](https://github.com/bec-project/bec_widgets/commit/9146c5194e0871b6f99a1b985a6d6b2e3fdd409c))
|
||||
|
||||
|
||||
## v3.15.1 (2026-06-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **curve_tree**: Update header labels to reflect device and signal columns
|
||||
([`68903fc`](https://github.com/bec-project/bec_widgets/commit/68903fc6ae2ffd3ac7b5394ba6cf9a4b2ce745e5))
|
||||
|
||||
|
||||
## v3.15.0 (2026-06-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -304,6 +306,49 @@ class BECWidget(BECConnector):
|
||||
buf.close()
|
||||
return ba
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
@rpc_timeout(None)
|
||||
def screenshot_to_scilog(self) -> None:
|
||||
"""
|
||||
Take a screenshot of the widget and send it to SciLog through BEC messaging services.
|
||||
"""
|
||||
if not isinstance(self, QWidget):
|
||||
raise RuntimeError("Cannot take screenshot of non-QWidget instance")
|
||||
|
||||
messaging = getattr(self.client, "messaging", None)
|
||||
if messaging is None:
|
||||
raise RuntimeError("BEC messaging services are not available on the current client.")
|
||||
|
||||
scilog = messaging.scilog
|
||||
if not getattr(scilog, "_enabled", False):
|
||||
# We currently don't expose a public method to check if SciLog is enabled,
|
||||
# so we play defensive and check for an internal _enabled attribute that
|
||||
# should be True when SciLog is enabled.
|
||||
raise RuntimeError(
|
||||
"SciLog is not enabled for the current client, cannot send screenshot."
|
||||
)
|
||||
|
||||
pixmap: QPixmap = self.grab()
|
||||
if pixmap.isNull():
|
||||
raise RuntimeError("Failed to capture screenshot.")
|
||||
|
||||
tmp_name: str | None = None
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_file:
|
||||
tmp_name = tmp_file.name
|
||||
|
||||
if not pixmap.save(tmp_name, "PNG"):
|
||||
raise RuntimeError("Failed to save screenshot to a temporary file.")
|
||||
|
||||
msg = messaging.scilog.new()
|
||||
msg.add_attachment(tmp_name)
|
||||
msg.send()
|
||||
logger.info("Screenshot sent to SciLog")
|
||||
finally:
|
||||
if tmp_name and os.path.exists(tmp_name):
|
||||
os.unlink(tmp_name)
|
||||
|
||||
def attach(self):
|
||||
dock = WidgetHierarchy.find_ancestor(self, QtAds.CDockWidget)
|
||||
if dock is None:
|
||||
|
||||
@@ -32,6 +32,7 @@ from bec_widgets.utils.rpc_widget_handler import widget_handler
|
||||
from bec_widgets.utils.toolbars.actions import (
|
||||
ExpandableMenuAction,
|
||||
MaterialIconAction,
|
||||
SwitchableToolBarAction,
|
||||
WidgetAction,
|
||||
)
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||
@@ -470,10 +471,34 @@ class BECDockArea(DockAreaWidget):
|
||||
self._profile_management_enabled
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"screenshot",
|
||||
"screenshot_save",
|
||||
MaterialIconAction(icon_name="photo_camera", tooltip="Take Screenshot", parent=self),
|
||||
)
|
||||
self.toolbar.components.get_action("screenshot").action.setVisible(
|
||||
self.toolbar.components.add_safe(
|
||||
"screenshot_to_scilog",
|
||||
MaterialIconAction(
|
||||
icon_name="add_a_photo", tooltip="Send Screenshot to SciLog", parent=self
|
||||
),
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"screenshot",
|
||||
SwitchableToolBarAction(
|
||||
actions={
|
||||
"save": self.toolbar.components.get_action_reference("screenshot_save")(),
|
||||
"scilog": self.toolbar.components.get_action_reference(
|
||||
"screenshot_to_scilog"
|
||||
)(),
|
||||
},
|
||||
initial_action="save",
|
||||
tooltip="Screenshot Actions",
|
||||
checkable=False,
|
||||
parent=self.toolbar,
|
||||
),
|
||||
)
|
||||
self.toolbar.components.get_action("screenshot_save").action.setVisible(
|
||||
self._profile_management_enabled
|
||||
)
|
||||
self.toolbar.components.get_action("screenshot_to_scilog").action.setVisible(
|
||||
self._profile_management_enabled
|
||||
)
|
||||
dark_mode_action = WidgetAction(
|
||||
@@ -542,7 +567,12 @@ class BECDockArea(DockAreaWidget):
|
||||
_connect_flat_actions(self._ACTION_MAPPINGS["menu_utils"])
|
||||
|
||||
self.toolbar.components.get_action("attach_all").action.triggered.connect(self.attach_all)
|
||||
self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot)
|
||||
self.toolbar.components.get_action("screenshot_save").action.triggered.connect(
|
||||
self.screenshot
|
||||
)
|
||||
self.toolbar.components.get_action("screenshot_to_scilog").action.triggered.connect(
|
||||
self.screenshot_to_scilog
|
||||
)
|
||||
|
||||
def _new_plugin_widget(self, widget_type: str, toolbar_action: MaterialIconAction) -> None:
|
||||
# Created as helper method for simple tests
|
||||
|
||||
@@ -12,7 +12,7 @@ logger = bec_logger.logger
|
||||
|
||||
|
||||
class AbortButton(BECWidget, QWidget):
|
||||
"""A button that abort the scan."""
|
||||
"""A button that abort the request."""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "cancel"
|
||||
@@ -25,11 +25,12 @@ class AbortButton(BECWidget, QWidget):
|
||||
config=None,
|
||||
gui_id=None,
|
||||
toolbar=False,
|
||||
scan_id=None,
|
||||
request_id=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
self.request_id = request_id
|
||||
|
||||
self.layout = QHBoxLayout(self)
|
||||
self.layout.setSpacing(0)
|
||||
@@ -39,7 +40,7 @@ class AbortButton(BECWidget, QWidget):
|
||||
if toolbar:
|
||||
icon = material_icon("cancel", color="#666666", filled=True)
|
||||
self.button = QToolButton(icon=icon)
|
||||
self.button.setToolTip("Abort the scan")
|
||||
self.button.setToolTip("Abort the request")
|
||||
else:
|
||||
self.button = QPushButton()
|
||||
self.button.setText("Abort")
|
||||
@@ -47,8 +48,6 @@ class AbortButton(BECWidget, QWidget):
|
||||
|
||||
self.layout.addWidget(self.button)
|
||||
|
||||
self.scan_id = scan_id
|
||||
|
||||
@SafeSlot()
|
||||
def abort_scan(
|
||||
self,
|
||||
@@ -57,10 +56,10 @@ class AbortButton(BECWidget, QWidget):
|
||||
Abort the scan.
|
||||
|
||||
Args:
|
||||
scan_id(str|None): The scan id to abort. If None, the current scan will be aborted.
|
||||
request_id(str|None): The request id to abort. If None, the current scan will be aborted.
|
||||
"""
|
||||
if self.scan_id is not None:
|
||||
logger.info(f"Aborting scan with scan_id: {self.scan_id}")
|
||||
self.queue.request_scan_abortion(scan_id=self.scan_id)
|
||||
if self.request_id is not None:
|
||||
logger.info(f"Aborting scan with request_id: {self.request_id}")
|
||||
self.queue.request_scan_abortion(request_id=self.request_id)
|
||||
else:
|
||||
self.queue.request_scan_abortion()
|
||||
|
||||
@@ -552,7 +552,7 @@ class CurveTree(BECWidget, QWidget):
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setColumnCount(8)
|
||||
self.tree.setHeaderLabels(
|
||||
["Actions", "Name", "Entry", "Scan #", "Color", "Style", "Width", "Symbol"]
|
||||
["Actions", "Device", "Signal", "Scan #", "Color", "Style", "Width", "Symbol"]
|
||||
)
|
||||
|
||||
header = self.tree.header()
|
||||
|
||||
@@ -176,6 +176,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
scan_names = []
|
||||
scan_ids = []
|
||||
user_metadatas = []
|
||||
request_ids = []
|
||||
status = item.status
|
||||
for request_block in blocks:
|
||||
scan_type = request_block.msg.scan_type
|
||||
@@ -193,6 +194,8 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
scan_id = request_block.scan_id
|
||||
if scan_id:
|
||||
scan_ids.append(scan_id)
|
||||
if request_block.RID:
|
||||
request_ids.append(request_block.RID)
|
||||
if scan_types:
|
||||
scan_types = ", ".join(scan_types)
|
||||
if scan_numbers:
|
||||
@@ -208,7 +211,15 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
tooltip = json.dumps(user_metadatas, indent=2)
|
||||
if scan_ids:
|
||||
scan_ids = ", ".join(scan_ids)
|
||||
self.set_row(index, scan_numbers, scan_names, scan_types, status, scan_ids, tooltip)
|
||||
self.set_row(
|
||||
index,
|
||||
scan_numbers,
|
||||
scan_names,
|
||||
scan_types,
|
||||
status,
|
||||
tooltip,
|
||||
abort_request_id=request_ids[0] if request_ids else None,
|
||||
)
|
||||
busy = (
|
||||
False
|
||||
if all(item.status in ("STOPPED", "COMPLETED", "IDLE") for item in queue_info)
|
||||
@@ -249,8 +260,8 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
scan_name: str,
|
||||
scan_type: str,
|
||||
status: str,
|
||||
scan_id: str,
|
||||
tooltip: str = "",
|
||||
abort_request_id: str = "",
|
||||
):
|
||||
"""
|
||||
Set the row of the table.
|
||||
@@ -261,10 +272,10 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
scan_name (str): The scan name.
|
||||
scan_type (str): The scan type.
|
||||
status (str): The status.
|
||||
scan_id (str): The scan id.
|
||||
tooltip (str): Optional tooltip to display (pretty-printed user metadata).
|
||||
abort_request_id (str): request id to abort.
|
||||
"""
|
||||
abort_button = self._create_abort_button(scan_id)
|
||||
abort_button = self._create_abort_button(abort_request_id)
|
||||
abort_button.button.clicked.connect(self.delete_selected_row)
|
||||
|
||||
self.table.setItem(index, 0, self.format_item(scan_number, tooltip=tooltip))
|
||||
@@ -273,17 +284,17 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
self.table.setItem(index, 3, self.format_item(status, status=True, tooltip=tooltip))
|
||||
self.table.setCellWidget(index, 4, abort_button)
|
||||
|
||||
def _create_abort_button(self, scan_id: str) -> AbortButton:
|
||||
def _create_abort_button(self, request_id: str) -> AbortButton:
|
||||
"""
|
||||
Create an abort button with styling for BEC Queue widget for certain scan_id.
|
||||
Create an abort button with styling for BEC Queue widget for certain request_id.
|
||||
|
||||
Args:
|
||||
scan_id(str): The scan id to abort.
|
||||
request_id(str): The request id to abort.
|
||||
|
||||
Returns:
|
||||
AbortButton: The abort button.
|
||||
"""
|
||||
abort_button = AbortButton(parent=self, scan_id=scan_id)
|
||||
abort_button = AbortButton(parent=self, request_id=request_id)
|
||||
|
||||
abort_button.button.setText("")
|
||||
abort_button.button.setIcon(
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "3.15.0"
|
||||
version = "3.16.0"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.11"
|
||||
classifiers = [
|
||||
@@ -76,6 +76,8 @@ qtermwidget = ["pyside6_qtermwidget"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from qtpy.QtCore import QSettings, Qt, QTimer
|
||||
from qtpy.QtGui import QPixmap
|
||||
from qtpy.QtWidgets import QDialog, QMessageBox, QWidget
|
||||
from qtpy.QtWidgets import QDialog, QMessageBox, QToolButton, QWidget
|
||||
|
||||
import bec_widgets.widgets.containers.dock_area.basic_dock_area as basic_dock_module
|
||||
import bec_widgets.widgets.containers.dock_area.dock_area as dock_area_module
|
||||
@@ -987,9 +987,9 @@ class TestToolbarFunctionality:
|
||||
mock_screenshot = mock.MagicMock()
|
||||
mock_grab.return_value = mock_screenshot
|
||||
|
||||
# Trigger the screenshot action
|
||||
action = advanced_dock_area.toolbar.components.get_action("screenshot").action
|
||||
action.trigger()
|
||||
# Trigger the screenshot main button
|
||||
action = advanced_dock_area.toolbar.components.get_action("screenshot")
|
||||
action.main_button.click()
|
||||
|
||||
# Verify the dialog was called
|
||||
mock_dialog.assert_called_once()
|
||||
@@ -1000,6 +1000,41 @@ class TestToolbarFunctionality:
|
||||
# Verify save was called with the filename
|
||||
mock_screenshot.save.assert_called_once_with(str(screenshot_path))
|
||||
|
||||
def test_screenshot_button_has_scilog_dropdown(self, advanced_dock_area):
|
||||
"""Test screenshot toolbar button exposes a SciLog dropdown option."""
|
||||
action = advanced_dock_area.toolbar.components.get_action("screenshot")
|
||||
button = action.main_button
|
||||
|
||||
assert isinstance(button, QToolButton)
|
||||
assert button.popupMode() == QToolButton.ToolButtonPopupMode.MenuButtonPopup
|
||||
assert button.menu() is not None
|
||||
assert [menu_action.text() for menu_action in button.menu().actions()] == [
|
||||
"Take Screenshot",
|
||||
"Send Screenshot to SciLog",
|
||||
]
|
||||
|
||||
def test_screenshot_to_scilog_action(self, advanced_dock_area):
|
||||
"""Test sending a screenshot through the BEC SciLog messaging service."""
|
||||
mock_message = mock.MagicMock()
|
||||
mock_message.add_attachment.return_value = mock_message
|
||||
advanced_dock_area.client.messaging.scilog.new = mock.MagicMock(return_value=mock_message)
|
||||
advanced_dock_area.client.messaging.scilog._enabled = True
|
||||
|
||||
with mock.patch.object(advanced_dock_area, "grab") as mock_grab:
|
||||
mock_screenshot = mock.MagicMock()
|
||||
mock_screenshot.isNull.return_value = False
|
||||
mock_screenshot.save.return_value = True
|
||||
mock_grab.return_value = mock_screenshot
|
||||
|
||||
action = advanced_dock_area.toolbar.components.get_action("screenshot")
|
||||
assert action.main_button is not None
|
||||
scilog_menu_action = action.main_button.menu().actions()[1]
|
||||
scilog_menu_action.trigger()
|
||||
|
||||
advanced_dock_area.client.messaging.scilog.new.assert_called_once_with()
|
||||
mock_message.add_attachment.assert_called_once()
|
||||
mock_message.send.assert_called_once_with()
|
||||
|
||||
def test_plugin_toolbar_actions_empty_when_no_plugins(self, clear_plugin_toolbar_actions_cache):
|
||||
"""Test that no plugin toolbar actions are produced when no plugin widgets exist."""
|
||||
with patch(
|
||||
|
||||
Reference in New Issue
Block a user