Compare commits

..

1 Commits

Author SHA1 Message Date
wakonig_k 9213360c44 feat: add sound assets 2026-06-12 21:03:05 +02:00
55 changed files with 89 additions and 164 deletions
-16
View File
@@ -1,22 +1,6 @@
# 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.
-45
View File
@@ -1,7 +1,5 @@
from __future__ import annotations
import os
import tempfile
from datetime import datetime
from typing import TYPE_CHECKING
@@ -306,49 +304,6 @@ 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:
+63
View File
@@ -0,0 +1,63 @@
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,7 +32,6 @@ 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
@@ -471,34 +470,10 @@ class BECDockArea(DockAreaWidget):
self._profile_management_enabled
)
self.toolbar.components.add_safe(
"screenshot_save",
"screenshot",
MaterialIconAction(icon_name="photo_camera", tooltip="Take Screenshot", parent=self),
)
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.toolbar.components.get_action("screenshot").action.setVisible(
self._profile_management_enabled
)
dark_mode_action = WidgetAction(
@@ -567,12 +542,7 @@ 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_save").action.triggered.connect(
self.screenshot
)
self.toolbar.components.get_action("screenshot_to_scilog").action.triggered.connect(
self.screenshot_to_scilog
)
self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot)
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 request."""
"""A button that abort the scan."""
PLUGIN = True
ICON_NAME = "cancel"
@@ -25,12 +25,11 @@ class AbortButton(BECWidget, QWidget):
config=None,
gui_id=None,
toolbar=False,
request_id=None,
scan_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)
@@ -40,7 +39,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 request")
self.button.setToolTip("Abort the scan")
else:
self.button = QPushButton()
self.button.setText("Abort")
@@ -48,6 +47,8 @@ class AbortButton(BECWidget, QWidget):
self.layout.addWidget(self.button)
self.scan_id = scan_id
@SafeSlot()
def abort_scan(
self,
@@ -56,10 +57,10 @@ class AbortButton(BECWidget, QWidget):
Abort the scan.
Args:
request_id(str|None): The request id to abort. If None, the current scan will be aborted.
scan_id(str|None): The scan id to abort. If None, the current scan will be aborted.
"""
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)
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)
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", "Device", "Signal", "Scan #", "Color", "Style", "Width", "Symbol"]
["Actions", "Name", "Entry", "Scan #", "Color", "Style", "Width", "Symbol"]
)
header = self.tree.header()
@@ -176,7 +176,6 @@ 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
@@ -194,8 +193,6 @@ 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:
@@ -211,15 +208,7 @@ 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,
tooltip,
abort_request_id=request_ids[0] if request_ids else None,
)
self.set_row(index, scan_numbers, scan_names, scan_types, status, scan_ids, tooltip)
busy = (
False
if all(item.status in ("STOPPED", "COMPLETED", "IDLE") for item in queue_info)
@@ -260,8 +249,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.
@@ -272,10 +261,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(abort_request_id)
abort_button = self._create_abort_button(scan_id)
abort_button.button.clicked.connect(self.delete_selected_row)
self.table.setItem(index, 0, self.format_item(scan_number, tooltip=tooltip))
@@ -284,17 +273,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, request_id: str) -> AbortButton:
def _create_abort_button(self, scan_id: str) -> AbortButton:
"""
Create an abort button with styling for BEC Queue widget for certain request_id.
Create an abort button with styling for BEC Queue widget for certain scan_id.
Args:
request_id(str): The request id to abort.
scan_id(str): The scan id to abort.
Returns:
AbortButton: The abort button.
"""
abort_button = AbortButton(parent=self, request_id=request_id)
abort_button = AbortButton(parent=self, scan_id=scan_id)
abort_button.button.setText("")
abort_button.button.setIcon(
+1 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "bec_widgets"
version = "3.16.0"
version = "3.15.0"
description = "BEC Widgets"
requires-python = ">=3.11"
classifiers = [
@@ -76,8 +76,6 @@ qtermwidget = ["pyside6_qtermwidget"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+4 -39
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, QToolButton, QWidget
from qtpy.QtWidgets import QDialog, QMessageBox, 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 main button
action = advanced_dock_area.toolbar.components.get_action("screenshot")
action.main_button.click()
# Trigger the screenshot action
action = advanced_dock_area.toolbar.components.get_action("screenshot").action
action.trigger()
# Verify the dialog was called
mock_dialog.assert_called_once()
@@ -1000,41 +1000,6 @@ 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(