mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-15 17:40:57 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from qtpy.QtCore import QUrl
|
||||
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer
|
||||
from qtpy.QtWidgets import QApplication, QComboBox, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class SoundPlayerWidget(QWidget):
|
||||
"""Simple widget to preview bundled sound assets."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setWindowTitle("Sound Player")
|
||||
|
||||
self._sounds_dir = Path(__file__).resolve().parent.parent / "assets" / "sounds"
|
||||
self._player = QMediaPlayer(self)
|
||||
self._audio_output = QAudioOutput(self)
|
||||
self._player.setAudioOutput(self._audio_output)
|
||||
|
||||
self.sound_combo_box = QComboBox(self)
|
||||
self.play_button = QPushButton("Play", self)
|
||||
|
||||
self._populate_sounds()
|
||||
self.play_button.clicked.connect(self.play_selected_sound)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.sound_combo_box)
|
||||
layout.addWidget(self.play_button)
|
||||
|
||||
self.resize(420, 100)
|
||||
|
||||
def _populate_sounds(self) -> None:
|
||||
"""Load bundled sound assets into the combo box."""
|
||||
sound_files = sorted(self._sounds_dir.glob("*.mp3"))
|
||||
for sound_file in sound_files:
|
||||
self.sound_combo_box.addItem(sound_file.stem, str(sound_file))
|
||||
|
||||
self.play_button.setEnabled(bool(sound_files))
|
||||
if not sound_files:
|
||||
self.sound_combo_box.addItem("No sounds found")
|
||||
|
||||
def play_selected_sound(self) -> None:
|
||||
"""Play the currently selected sound asset."""
|
||||
sound_path = self.sound_combo_box.currentData()
|
||||
if not sound_path:
|
||||
return
|
||||
|
||||
self._player.setSource(QUrl.fromLocalFile(sound_path))
|
||||
self._player.stop()
|
||||
self._player.play()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from bec_qthemes import apply_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("light")
|
||||
|
||||
widget = SoundPlayerWidget()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -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
@@ -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