From d4ff0bfac423505ee1163b5b1e2ca09d576af4c1 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Sat, 13 Jun 2026 12:21:37 +0200 Subject: [PATCH] feat(screenshot): add option to upload to SciLog --- bec_widgets/utils/bec_widget.py | 45 +++++++++++++++++++ .../widgets/containers/dock_area/dock_area.py | 36 +++++++++++++-- tests/unit_tests/test_dock_area.py | 43 ++++++++++++++++-- 3 files changed, 117 insertions(+), 7 deletions(-) diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index dc831c94..98d4f396 100644 --- a/bec_widgets/utils/bec_widget.py +++ b/bec_widgets/utils/bec_widget.py @@ -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, width="80%") + 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: diff --git a/bec_widgets/widgets/containers/dock_area/dock_area.py b/bec_widgets/widgets/containers/dock_area/dock_area.py index acd03392..8bfa9a37 100644 --- a/bec_widgets/widgets/containers/dock_area/dock_area.py +++ b/bec_widgets/widgets/containers/dock_area/dock_area.py @@ -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 diff --git a/tests/unit_tests/test_dock_area.py b/tests/unit_tests/test_dock_area.py index 10334ede..5739fa22 100644 --- a/tests/unit_tests/test_dock_area.py +++ b/tests/unit_tests/test_dock_area.py @@ -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(