Compare commits

...

4 Commits

Author SHA1 Message Date
semantic-release 2b22a7065c 3.16.0
Automatically generated by python-semantic-release
2026-06-15 09:25:01 +00:00
wakonig_k 9146c5194e feat(screenshot): add option to upload to SciLog 2026-06-15 11:24:10 +02:00
semantic-release 5fde0c6efc 3.15.1
Automatically generated by python-semantic-release
2026-06-13 14:37:22 +00:00
wakonig_k 68903fc6ae fix(curve_tree): update header labels to reflect device and signal columns 2026-06-13 16:36:36 +02:00
6 changed files with 137 additions and 9 deletions
+16
View File
@@ -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
+45
View File
@@ -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
@@ -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()
+3 -1
View File
@@ -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"
+39 -4
View File
@@ -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(