mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-10 18:50:55 +02:00
Compare commits
32 Commits
v2.18.0
...
docs/wavef
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f1526182b | |||
|
|
f10140e0f3 | ||
| 09c5a443aa | |||
| 3f5ab142a3 | |||
|
|
422d06d141 | ||
| 371bc485d0 | |||
|
|
70970ecf00 | ||
| 3d59c25aa9 | |||
|
|
70a06c5fd1 | ||
| 7ba8863d6a | |||
|
|
00ea8bb6c6 | ||
| e841468892 | |||
| 48a0e5831f | |||
| 1e9dd4cd25 | |||
| d10328cb5c | |||
|
|
6b248e93f5 | ||
| bc3085ab8c | |||
| 9cba696afd | |||
|
|
881b7a7e9d | ||
| 29a26b19f9 | |||
|
|
cba4d47f76 | ||
| 9f3dcc3ab3 | |||
| 57f75bd4d5 | |||
| 4456297beb | |||
|
|
ae26b43fb1 | ||
| 7484f5160c | |||
| 6421050116 | |||
|
|
5a137d1219 | ||
| d5a40dabc7 | |||
| f3da6e959e | |||
| 3a103410e7 | |||
| 3378051250 |
2
.github/workflows/formatter.yml
vendored
2
.github/workflows/formatter.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
isort --check --diff ./
|
||||
|
||||
- name: Check for disallowed imports from PySide
|
||||
run: '! grep -re "from PySide6\." bec_widgets/ | grep -v -e "PySide6.QtDesigner" -e "PySide6.scripts"'
|
||||
run: '! grep -re "from PySide6\." bec_widgets/ tests/ | grep -v -e "PySide6.QtDesigner" -e "PySide6.scripts"'
|
||||
|
||||
Pylint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
124
CHANGELOG.md
124
CHANGELOG.md
@@ -1,6 +1,130 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v2.21.2 (2025-06-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **waveform**: Fix waveform categorisation for aborted scans
|
||||
([`09c5a44`](https://github.com/bec-project/bec_widgets/commit/09c5a443aac675f02fa1e38179deb9863af152e2))
|
||||
|
||||
### Testing
|
||||
|
||||
- Assert config for equality, not identity
|
||||
([`3f5ab14`](https://github.com/bec-project/bec_widgets/commit/3f5ab142a3cb5446261c4faebdc7b13f10ef4a80))
|
||||
|
||||
|
||||
## v2.21.1 (2025-06-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **sbb monitor**: Add missing pyproject file
|
||||
([`371bc48`](https://github.com/bec-project/bec_widgets/commit/371bc485d060404433082c9e3e00780961ce6ae3))
|
||||
|
||||
|
||||
## v2.21.0 (2025-06-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **sbb monitor**: Add sbb monitor widget
|
||||
([`3d59c25`](https://github.com/bec-project/bec_widgets/commit/3d59c25aa93590a62ab4d31a4ab08589402bf407))
|
||||
|
||||
|
||||
## v2.20.1 (2025-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **signal input base**: Unregister callback to avoid accessing deleted qt objects
|
||||
([`7ba8863`](https://github.com/bec-project/bec_widgets/commit/7ba8863d6a0c21f772e4ef8a5d4180c2a7ab49cb))
|
||||
|
||||
|
||||
## v2.20.0 (2025-06-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **curve_settings**: Larger minimalWidth for the x device combobox selection
|
||||
([`48a0e58`](https://github.com/bec-project/bec_widgets/commit/48a0e5831feccd30f24218821bbc9d73f8c47933))
|
||||
|
||||
### Features
|
||||
|
||||
- **waveform**: Move x axis selection to a combobox
|
||||
([`d10328c`](https://github.com/bec-project/bec_widgets/commit/d10328cb5c775a9b7b40ed4e9f2889e63eb039ff))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **curve settings**: Move signal logic to SignalCombobox
|
||||
([`e841468`](https://github.com/bec-project/bec_widgets/commit/e84146889210165de1c4e63eb20b39f30cc5c623))
|
||||
|
||||
### Testing
|
||||
|
||||
- **curve settings**: Add curve tree elements to the dialog test
|
||||
([`1e9dd4c`](https://github.com/bec-project/bec_widgets/commit/1e9dd4cd2561d37bdda1cd86b511295c259b2831))
|
||||
|
||||
|
||||
## v2.19.4 (2025-06-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **curve tree**: Remove manual interception of the close event; call parent cleanup
|
||||
([`bc3085a`](https://github.com/bec-project/bec_widgets/commit/bc3085ab8cb6688da358df4a7c07fc213a99f2df))
|
||||
|
||||
- **waveform**: Curve tree elements must clean up signal combobox
|
||||
([`9cba696`](https://github.com/bec-project/bec_widgets/commit/9cba696afd3300a76678dfdc4226604696cc3696))
|
||||
|
||||
|
||||
## v2.19.3 (2025-06-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scan_control**: Safeguard against empty history; reversed history to fetch the newest scan
|
||||
([`29a26b1`](https://github.com/bec-project/bec_widgets/commit/29a26b19f9ab829b0d877c3233613a0936db0a12))
|
||||
|
||||
|
||||
## v2.19.2 (2025-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **scan_control**: Scan parameters fetched from the scan_history, fix #707
|
||||
([`4456297`](https://github.com/bec-project/bec_widgets/commit/4456297beb940b147882f96caee6fb19aaf93c73))
|
||||
|
||||
### Build System
|
||||
|
||||
- Bec_lib 3.44 required
|
||||
([`9f3dcc3`](https://github.com/bec-project/bec_widgets/commit/9f3dcc3ab30a2c238ffffa8d594735ccaf6f1ca4))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **scan_control**: Request_last_executed_scan_parameters logic adjusted
|
||||
([`57f75bd`](https://github.com/bec-project/bec_widgets/commit/57f75bd4d506ca4d8dc982f3051d0d4c29b0d41c))
|
||||
|
||||
|
||||
## v2.19.1 (2025-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **launch_window**: Number of remaining connections extended to 4
|
||||
([`7484f51`](https://github.com/bec-project/bec_widgets/commit/7484f5160c8c6d632fd27996035ff6c0dda2e657))
|
||||
|
||||
|
||||
## v2.19.0 (2025-06-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **ci**: Extend check for pyside import to tests
|
||||
([`d5a40da`](https://github.com/bec-project/bec_widgets/commit/d5a40dabc74753acad05e3eb6b121499fc1e03d7))
|
||||
|
||||
### Features
|
||||
|
||||
- (#494) add signal display to device browser
|
||||
([`f3da6e9`](https://github.com/bec-project/bec_widgets/commit/f3da6e959e0416827ee5d02e34e6ad0ecfc8e5e7))
|
||||
|
||||
- (#494) add tabbed layout for device item
|
||||
([`3378051`](https://github.com/bec-project/bec_widgets/commit/337805125098c3e028a17b74ef6d9ae4b9ba3d6d))
|
||||
|
||||
- (#494) display device signals
|
||||
([`3a10341`](https://github.com/bec-project/bec_widgets/commit/3a103410e7448256a56b59bb3276fee056ec42a0))
|
||||
|
||||
|
||||
## v2.18.0 (2025-06-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -542,7 +542,7 @@ class LaunchWindow(BECMainWindow):
|
||||
remaining_connections = [
|
||||
connection for connection in connections.values() if connection.parent_id != self.gui_id
|
||||
]
|
||||
return len(remaining_connections) <= 2
|
||||
return len(remaining_connections) <= 4
|
||||
|
||||
def _turn_off_the_lights(self, connections: dict):
|
||||
"""
|
||||
|
||||
@@ -49,6 +49,7 @@ _Widgets = {
|
||||
"ResetButton": "ResetButton",
|
||||
"ResumeButton": "ResumeButton",
|
||||
"RingProgressBar": "RingProgressBar",
|
||||
"SBBMonitor": "SBBMonitor",
|
||||
"ScanControl": "ScanControl",
|
||||
"ScatterWaveform": "ScatterWaveform",
|
||||
"SignalComboBox": "SignalComboBox",
|
||||
@@ -3249,6 +3250,12 @@ class RingProgressBar(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class SBBMonitor(RPCBase):
|
||||
"""A widget to display the SBB monitor website."""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class ScanControl(RPCBase):
|
||||
"""Widget to submit new scans to the queue."""
|
||||
|
||||
|
||||
@@ -169,6 +169,9 @@ class BECDockArea(BECWidget, QWidget):
|
||||
tooltip="Add LogPanel - Disabled",
|
||||
filled=True,
|
||||
),
|
||||
"sbb_monitor": MaterialIconAction(
|
||||
icon_name="train", tooltip="Add SBB Monitor", filled=True
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_2": SeparatorAction(),
|
||||
@@ -238,6 +241,9 @@ class BECDockArea(BECWidget, QWidget):
|
||||
# self.toolbar.widgets["menu_utils"].widgets["log_panel"].triggered.connect(
|
||||
# lambda: self._create_widget_from_toolbar(widget_name="LogPanel")
|
||||
# )
|
||||
self.toolbar.widgets["menu_utils"].widgets["sbb_monitor"].triggered.connect(
|
||||
lambda: self._create_widget_from_toolbar(widget_name="SBBMonitor")
|
||||
)
|
||||
|
||||
# Icons
|
||||
self.toolbar.widgets["attach_all"].action.triggered.connect(self.attach_all)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QPoint, Qt
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QProgressBar, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class WidgetTooltip(QWidget):
|
||||
"""Frameless, always-on-top window that behaves like a tooltip."""
|
||||
|
||||
def __init__(self, content: QWidget) -> None:
|
||||
super().__init__(None, Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating)
|
||||
self.setMouseTracking(True)
|
||||
self.content = content
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(6, 6, 6, 6)
|
||||
layout.addWidget(self.content)
|
||||
self.adjustSize()
|
||||
|
||||
def leaveEvent(self, _event) -> None:
|
||||
self.hide()
|
||||
|
||||
def show_above(self, global_pos: QPoint, offset: int = 8) -> None:
|
||||
self.adjustSize()
|
||||
screen = QApplication.screenAt(global_pos) or QApplication.primaryScreen()
|
||||
screen_geo = screen.availableGeometry()
|
||||
geom = self.geometry()
|
||||
|
||||
x = global_pos.x() - geom.width() // 2
|
||||
y = global_pos.y() - geom.height() - offset
|
||||
|
||||
x = max(screen_geo.left(), min(x, screen_geo.right() - geom.width()))
|
||||
y = max(screen_geo.top(), min(y, screen_geo.bottom() - geom.height()))
|
||||
|
||||
self.move(x, y)
|
||||
self.show()
|
||||
|
||||
|
||||
class HoverWidget(QWidget):
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, *, simple: QWidget, full: QWidget):
|
||||
super().__init__(parent)
|
||||
self._simple = simple
|
||||
self._full = full
|
||||
self._full.setVisible(False)
|
||||
self._tooltip = None
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.addWidget(simple)
|
||||
|
||||
def enterEvent(self, event):
|
||||
# suppress empty-label tooltips for labels
|
||||
if isinstance(self._full, QLabel) and not self._full.text():
|
||||
return
|
||||
|
||||
if self._tooltip is None: # first time only
|
||||
self._tooltip = WidgetTooltip(self._full)
|
||||
self._full.setVisible(True)
|
||||
|
||||
centre = self.mapToGlobal(self.rect().center())
|
||||
self._tooltip.show_above(centre)
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
if self._tooltip and self._tooltip.isVisible():
|
||||
self._tooltip.hide()
|
||||
super().leaveEvent(event)
|
||||
|
||||
def close(self):
|
||||
if self._tooltip:
|
||||
self._tooltip.close()
|
||||
self._tooltip.deleteLater()
|
||||
self._tooltip = None
|
||||
super().close()
|
||||
|
||||
|
||||
################################################################################
|
||||
# Demo
|
||||
# Just a simple example to show how the HoverWidget can be used to display
|
||||
# a tooltip with a full widget inside (two different widgets are used
|
||||
# for the simple and full versions).
|
||||
################################################################################
|
||||
|
||||
|
||||
class DemoSimpleWidget(QLabel): # pragma: no cover
|
||||
"""A simple widget to be used as a trigger for the tooltip."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setText("Hover me for a preview!")
|
||||
|
||||
|
||||
class DemoFullWidget(QProgressBar): # pragma: no cover
|
||||
"""A full widget to be shown in the tooltip."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setRange(0, 100)
|
||||
self.setValue(75)
|
||||
self.setFixedWidth(320)
|
||||
self.setFixedHeight(30)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
window = QWidget()
|
||||
window.layout = QHBoxLayout(window)
|
||||
hover_widget = HoverWidget(simple=DemoSimpleWidget(), full=DemoFullWidget())
|
||||
window.layout.addWidget(hover_widget)
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@@ -3,15 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import (
|
||||
QAbstractAnimation,
|
||||
QEasingCurve,
|
||||
QEvent,
|
||||
QPropertyAnimation,
|
||||
QSize,
|
||||
Qt,
|
||||
QTimer,
|
||||
)
|
||||
from qtpy.QtCore import QEasingCurve, QEvent, QPropertyAnimation, QSize, Qt, QTimer
|
||||
from qtpy.QtGui import QAction, QActionGroup, QIcon
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -30,6 +22,7 @@ from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.containers.main_window.addons.hover_widget import HoverWidget
|
||||
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
|
||||
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||||
from bec_widgets.widgets.progress.scan_progressbar.scan_progressbar import ScanProgressBar
|
||||
@@ -96,33 +89,60 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self._add_separator()
|
||||
|
||||
# Centre: Client‑info label (stretch=1 so it expands)
|
||||
self._client_info_label = ScrollLabel()
|
||||
self._client_info_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.status_bar.addWidget(self._client_info_label, 1)
|
||||
|
||||
# Timer to automatically clear client messages once they expire
|
||||
self._client_info_expire_timer = QTimer(self)
|
||||
self._client_info_expire_timer.setSingleShot(True)
|
||||
self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText(""))
|
||||
self._add_client_info_label()
|
||||
|
||||
# Add scan_progress bar with display logic
|
||||
self._add_scan_progress_bar()
|
||||
|
||||
################################################################################
|
||||
# Client message status bar widget helpers
|
||||
|
||||
def _add_client_info_label(self):
|
||||
"""
|
||||
Add a client info label to the status bar.
|
||||
This label will display messages from the BEC dispatcher.
|
||||
"""
|
||||
|
||||
# Scroll label for client info in Status Bar
|
||||
self._client_info_label = ScrollLabel(self)
|
||||
self._client_info_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
# Full label used in the hover widget
|
||||
self._client_info_label_full = QLabel(self)
|
||||
self._client_info_label_full.setAlignment(
|
||||
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
# Hover widget to show the full client info label
|
||||
self._client_info_hover = HoverWidget(
|
||||
self, simple=self._client_info_label, full=self._client_info_label_full
|
||||
)
|
||||
self.status_bar.addWidget(self._client_info_hover, 1)
|
||||
|
||||
# Timer to automatically clear client messages once they expire
|
||||
self._client_info_expire_timer = QTimer(self)
|
||||
self._client_info_expire_timer.setSingleShot(True)
|
||||
self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText(""))
|
||||
self._client_info_expire_timer.timeout.connect(
|
||||
lambda: self._client_info_label_full.setText("")
|
||||
)
|
||||
|
||||
################################################################################
|
||||
# Progress‑bar helpers
|
||||
def _add_scan_progress_bar(self):
|
||||
|
||||
# --- Progress bar -------------------------------------------------
|
||||
# Scan progress bar minimalistic design setup
|
||||
self._scan_progress_bar = ScanProgressBar(self, one_line_design=True)
|
||||
self._scan_progress_bar.show_elapsed_time = False
|
||||
self._scan_progress_bar.show_remaining_time = False
|
||||
self._scan_progress_bar.show_source_label = False
|
||||
self._scan_progress_bar.progressbar.label_template = ""
|
||||
self._scan_progress_bar.progressbar.setFixedHeight(8)
|
||||
self._scan_progress_bar.progressbar.setFixedWidth(80)
|
||||
# Setting HoverWidget for the scan progress bar - minimal and full version
|
||||
self._scan_progress_bar_simple = ScanProgressBar(self, one_line_design=True)
|
||||
self._scan_progress_bar_simple.show_elapsed_time = False
|
||||
self._scan_progress_bar_simple.show_remaining_time = False
|
||||
self._scan_progress_bar_simple.show_source_label = False
|
||||
self._scan_progress_bar_simple.progressbar.label_template = ""
|
||||
self._scan_progress_bar_simple.progressbar.setFixedHeight(8)
|
||||
self._scan_progress_bar_simple.progressbar.setFixedWidth(80)
|
||||
self._scan_progress_bar_full = ScanProgressBar(self)
|
||||
self._scan_progress_hover = HoverWidget(
|
||||
self, simple=self._scan_progress_bar_simple, full=self._scan_progress_bar_full
|
||||
)
|
||||
|
||||
# Bundle the progress bar with a separator
|
||||
separator = self._add_separator(separate_object=True)
|
||||
@@ -133,7 +153,7 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self._scan_progress_bar_with_separator.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._scan_progress_bar_with_separator.layout.setSpacing(0)
|
||||
self._scan_progress_bar_with_separator.layout.addWidget(separator)
|
||||
self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_bar)
|
||||
self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_hover)
|
||||
|
||||
# Set Size
|
||||
self._scan_progress_bar_target_width = self.SCAN_PROGRESS_WIDTH
|
||||
@@ -152,8 +172,8 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
self._scan_progress_hide_timer.timeout.connect(self._animate_hide_scan_progress_bar)
|
||||
|
||||
# Show / hide behaviour
|
||||
self._scan_progress_bar.progress_started.connect(self._show_scan_progress_bar)
|
||||
self._scan_progress_bar.progress_finished.connect(self._delay_hide_scan_progress_bar)
|
||||
self._scan_progress_bar_simple.progress_started.connect(self._show_scan_progress_bar)
|
||||
self._scan_progress_bar_simple.progress_finished.connect(self._delay_hide_scan_progress_bar)
|
||||
|
||||
def _show_scan_progress_bar(self):
|
||||
if self._scan_progress_hide_timer.isActive():
|
||||
@@ -342,10 +362,10 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
msg(dict): The message to display, should contain:
|
||||
meta(dict): Metadata about the message, usually empty.
|
||||
"""
|
||||
# self._client_info_label.setText("")
|
||||
message = msg.get("message", "")
|
||||
expiration = msg.get("expire", 0) # 0 → never expire
|
||||
self._client_info_label.setText(message)
|
||||
self._client_info_label_full.setText(message)
|
||||
|
||||
# Restart the expiration timer if necessary
|
||||
if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
|
||||
@@ -393,10 +413,20 @@ class BECMainWindow(BECWidget, QMainWindow):
|
||||
if hasattr(self, "_scan_progress_hide_timer") and self._scan_progress_hide_timer.isActive():
|
||||
self._scan_progress_hide_timer.stop()
|
||||
|
||||
########################################
|
||||
# Status bar widgets cleanup
|
||||
|
||||
# Client info label cleanup
|
||||
self._client_info_label.cleanup()
|
||||
self._scan_progress_bar.close()
|
||||
self._scan_progress_bar.deleteLater()
|
||||
self._client_info_hover.close()
|
||||
self._client_info_hover.deleteLater()
|
||||
# Scan progress bar cleanup
|
||||
self._scan_progress_bar_simple.close()
|
||||
self._scan_progress_bar_simple.deleteLater()
|
||||
self._scan_progress_bar_full.close()
|
||||
self._scan_progress_bar_full.deleteLater()
|
||||
self._scan_progress_hover.close()
|
||||
self._scan_progress_hover.deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class DeviceSignalInputBase(BECWidget):
|
||||
self._hinted_signals = []
|
||||
self._normal_signals = []
|
||||
self._config_signals = []
|
||||
self.bec_dispatcher.client.callbacks.register(
|
||||
self._device_update_register = self.bec_dispatcher.client.callbacks.register(
|
||||
EventType.DEVICE_UPDATE, self.update_signals_from_filters
|
||||
)
|
||||
|
||||
@@ -289,3 +289,10 @@ class DeviceSignalInputBase(BECWidget):
|
||||
if config is None:
|
||||
return DeviceSignalInputBaseConfig(widget_class=self.__class__.__name__)
|
||||
return DeviceSignalInputBaseConfig.model_validate(config)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the widget.
|
||||
"""
|
||||
self.bec_dispatcher.client.callbacks.remove(self._device_update_register)
|
||||
super().cleanup()
|
||||
|
||||
@@ -90,6 +90,36 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
|
||||
self.insertItem(0, "Hinted Signals")
|
||||
self.model().item(0).setEnabled(False)
|
||||
|
||||
def set_to_obj_name(self, obj_name: str) -> bool:
|
||||
"""
|
||||
Set the combobox to the object name of the signal.
|
||||
|
||||
Args:
|
||||
obj_name (str): Object name of the signal.
|
||||
|
||||
Returns:
|
||||
bool: True if the object name was found and set, False otherwise.
|
||||
"""
|
||||
for i in range(self.count()):
|
||||
signal_data = self.itemData(i)
|
||||
if signal_data and signal_data.get("obj_name") == obj_name:
|
||||
self.setCurrentIndex(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_to_first_enabled(self) -> bool:
|
||||
"""
|
||||
Set the combobox to the first enabled item.
|
||||
|
||||
Returns:
|
||||
bool: True if an enabled item was found and set, False otherwise.
|
||||
"""
|
||||
for i in range(self.count()):
|
||||
if self.model().item(i).isEnabled():
|
||||
self.setCurrentIndex(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
@SafeSlot()
|
||||
def reset_selection(self):
|
||||
"""Reset the selection of the combobox."""
|
||||
|
||||
@@ -203,35 +203,40 @@ class ScanControl(BECWidget, QWidget):
|
||||
"""
|
||||
Requests the last executed scan parameters from BEC and restores them to the scan control widget.
|
||||
"""
|
||||
enabled = self.toggle.checked
|
||||
current_scan = self.comboBox_scan_selection.currentText()
|
||||
if enabled:
|
||||
history = self.client.connector.lrange(MessageEndpoints.scan_queue_history(), 0, -1)
|
||||
self.last_scan_found = False
|
||||
if not self.toggle.checked:
|
||||
return
|
||||
|
||||
for scan in history:
|
||||
scan_name = scan.content["info"]["request_blocks"][-1]["msg"].content["scan_type"]
|
||||
if scan_name == current_scan:
|
||||
args_dict = scan.content["info"]["request_blocks"][-1]["msg"].content[
|
||||
"parameter"
|
||||
]["args"]
|
||||
args_list = []
|
||||
for key, value in args_dict.items():
|
||||
args_list.append(key)
|
||||
args_list.extend(value)
|
||||
if len(args_list) > 1 and self.arg_box is not None:
|
||||
self.arg_box.set_parameters(args_list)
|
||||
kwargs = scan.content["info"]["request_blocks"][-1]["msg"].content["parameter"][
|
||||
"kwargs"
|
||||
]
|
||||
if kwargs and self.kwarg_boxes:
|
||||
for box in self.kwarg_boxes:
|
||||
box.set_parameters(kwargs)
|
||||
self.last_scan_found = True
|
||||
break
|
||||
else:
|
||||
self.last_scan_found = False
|
||||
else:
|
||||
self.last_scan_found = False
|
||||
current_scan = self.comboBox_scan_selection.currentText()
|
||||
history = (
|
||||
self.client.connector.xread(
|
||||
MessageEndpoints.scan_history(), from_start=True, user_id=self.object_name
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
for scan in reversed(history):
|
||||
scan_data = scan.get("data")
|
||||
if not scan_data:
|
||||
continue
|
||||
|
||||
if scan_data.scan_name != current_scan:
|
||||
continue
|
||||
|
||||
ri = getattr(scan_data, "request_inputs", {}) or {}
|
||||
args_list = ri.get("arg_bundle", [])
|
||||
if args_list and self.arg_box:
|
||||
self.arg_box.set_parameters(args_list)
|
||||
|
||||
inputs = ri.get("inputs", {})
|
||||
kwargs = ri.get("kwargs", {})
|
||||
merged = {**inputs, **kwargs}
|
||||
if merged and self.kwarg_boxes:
|
||||
for box in self.kwarg_boxes:
|
||||
box.set_parameters(merged)
|
||||
|
||||
self.last_scan_found = True
|
||||
break
|
||||
|
||||
@SafeProperty(str)
|
||||
def current_scan(self):
|
||||
|
||||
0
bec_widgets/widgets/editors/sbb_monitor/__init__.py
Normal file
0
bec_widgets/widgets/editors/sbb_monitor/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.editors.sbb_monitor.sbb_monitor_plugin import SBBMonitorPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(SBBMonitorPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
15
bec_widgets/widgets/editors/sbb_monitor/sbb_monitor.py
Normal file
15
bec_widgets/widgets/editors/sbb_monitor/sbb_monitor.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from bec_widgets.widgets.editors.website.website import WebsiteWidget
|
||||
|
||||
|
||||
class SBBMonitor(WebsiteWidget):
|
||||
"""
|
||||
A widget to display the SBB monitor website.
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "train"
|
||||
USER_ACCESS = []
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
url = "https://free.oevplus.ch/monitor/?viewType=splitView&layout=1&showClock=true&showPerron=true&stationGroup1Title=Villigen%2C%20PSI%20West&stationGroup2Title=Siggenthal-Würenlingen&station_1_id=85%3A3592&station_1_name=Villigen%2C%20PSI%20West&station_1_quantity=5&station_1_group=1&station_2_id=85%3A3502&station_2_name=Siggenthal-Würenlingen&station_2_quantity=5&station_2_group=2"
|
||||
super().__init__(parent=parent, url=url, **kwargs)
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['sbb_monitor.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.sbb_monitor.sbb_monitor import SBBMonitor
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='SBBMonitor' name='sbb_monitor'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class SBBMonitorPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = SBBMonitor(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(SBBMonitor.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "sbb_monitor"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "SBBMonitor"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -2,13 +2,12 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtWidgets import (
|
||||
QComboBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
@@ -16,9 +15,8 @@ from qtpy.QtWidgets import (
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||
from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_tree import CurveTree
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
@@ -30,6 +28,7 @@ class CurveSetting(SettingWidget):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
self.setProperty("skip_settings", True)
|
||||
self.target_widget = target_widget
|
||||
self._x_settings_connected = False
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
|
||||
@@ -51,15 +50,23 @@ class CurveSetting(SettingWidget):
|
||||
self.mode_combo_label = QLabel("Mode")
|
||||
self.mode_combo = QComboBox()
|
||||
self.mode_combo.addItems(["auto", "index", "timestamp", "device"])
|
||||
self.mode_combo.setMinimumWidth(120)
|
||||
|
||||
self.spacer = QWidget()
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
self.device_x_label = QLabel("Device")
|
||||
self.device_x = DeviceLineEdit(parent=self)
|
||||
self.device_x = DeviceComboBox(parent=self)
|
||||
self.device_x.insertItem(0, "")
|
||||
self.device_x.setEditable(True)
|
||||
self.device_x.setMinimumWidth(180)
|
||||
|
||||
self.signal_x_label = QLabel("Signal")
|
||||
self.signal_x = QLineEdit()
|
||||
self.signal_x = SignalComboBox(parent=self)
|
||||
self.signal_x.include_config_signals = False
|
||||
self.signal_x.insertItem(0, "")
|
||||
self.signal_x.setEditable(True)
|
||||
self.signal_x.setMinimumWidth(180)
|
||||
|
||||
self._get_x_mode_from_waveform()
|
||||
self.switch_x_device_selection()
|
||||
@@ -85,11 +92,32 @@ class CurveSetting(SettingWidget):
|
||||
|
||||
def switch_x_device_selection(self):
|
||||
if self.mode_combo.currentText() == "device":
|
||||
self._x_settings_connected = True
|
||||
self.device_x.currentTextChanged.connect(self.signal_x.set_device)
|
||||
self.device_x.device_reset.connect(self.signal_x.reset_selection)
|
||||
|
||||
self.device_x.setEnabled(True)
|
||||
self.device_x.setText(self.target_widget.x_axis_mode["name"])
|
||||
self.signal_x.setText(self.target_widget.x_axis_mode["entry"])
|
||||
self.signal_x.setEnabled(True)
|
||||
item = self.device_x.findText(self.target_widget.x_axis_mode["name"])
|
||||
self.device_x.setCurrentIndex(item if item != -1 else 0)
|
||||
signal_x = self.target_widget.x_axis_mode.get("entry", "")
|
||||
if signal_x:
|
||||
self.signal_x.set_to_obj_name(signal_x)
|
||||
else:
|
||||
# If no match is found, set to the first enabled item
|
||||
if not self.signal_x.set_to_first_enabled():
|
||||
# If no enabled item is found, set to the first item
|
||||
self.signal_x.setCurrentIndex(0)
|
||||
else:
|
||||
self.device_x.setEnabled(False)
|
||||
self.signal_x.setEnabled(False)
|
||||
self.device_x.setCurrentIndex(0)
|
||||
self.signal_x.setCurrentIndex(0)
|
||||
|
||||
if self._x_settings_connected:
|
||||
self._x_settings_connected = False
|
||||
self.device_x.currentTextChanged.disconnect(self.signal_x.set_device)
|
||||
self.device_x.device_reset.disconnect(self.signal_x.reset_selection)
|
||||
|
||||
def _init_y_box(self):
|
||||
self.y_axis_box = QGroupBox("Y Axis")
|
||||
@@ -108,10 +136,11 @@ class CurveSetting(SettingWidget):
|
||||
Accepts the changes made in the settings widget and applies them to the target widget.
|
||||
"""
|
||||
if self.mode_combo.currentText() == "device":
|
||||
self.target_widget.x_mode = self.device_x.text()
|
||||
signal_x = self.signal_x.text()
|
||||
self.target_widget.x_mode = self.device_x.currentText()
|
||||
signal_x = self.signal_x.currentText()
|
||||
signal_data = self.signal_x.itemData(self.signal_x.currentIndex())
|
||||
if signal_x != "":
|
||||
self.target_widget.x_entry = signal_x
|
||||
self.target_widget.x_entry = signal_data.get("obj_name", signal_x)
|
||||
else:
|
||||
self.target_widget.x_mode = self.mode_combo.currentText()
|
||||
self.curve_manager.send_curve_json()
|
||||
@@ -126,5 +155,7 @@ class CurveSetting(SettingWidget):
|
||||
"""Cleanup the widget."""
|
||||
self.device_x.close()
|
||||
self.device_x.deleteLater()
|
||||
self.signal_x.close()
|
||||
self.signal_x.deleteLater()
|
||||
self.curve_manager.close()
|
||||
self.curve_manager.deleteLater()
|
||||
|
||||
@@ -142,20 +142,10 @@ class CurveRow(QTreeWidgetItem):
|
||||
# If the device name is not found, set the first enabled item
|
||||
self.device_edit.setCurrentIndex(0)
|
||||
|
||||
for i in range(self.entry_edit.count()):
|
||||
entry_data = self.entry_edit.itemData(i)
|
||||
if entry_data and entry_data.get("obj_name") == self.config.signal.entry:
|
||||
# If the device name matches an object name, set it
|
||||
self.entry_edit.setCurrentIndex(i)
|
||||
break
|
||||
else:
|
||||
# If no match found, set the first enabled item
|
||||
for i in range(self.entry_edit.count()):
|
||||
model = self.entry_edit.model()
|
||||
if model.flags(model.index(i, 0)) & Qt.ItemIsEnabled:
|
||||
self.entry_edit.setCurrentIndex(i)
|
||||
break
|
||||
else:
|
||||
if not self.entry_edit.set_to_obj_name(self.config.signal.entry):
|
||||
# If the entry is not found, try to set it to the first enabled item
|
||||
if not self.entry_edit.set_to_first_enabled():
|
||||
# If no enabled item is found, set to the first item
|
||||
self.entry_edit.setCurrentIndex(0)
|
||||
|
||||
self.tree.setItemWidget(self, 1, self.device_edit)
|
||||
@@ -263,6 +253,11 @@ class CurveRow(QTreeWidgetItem):
|
||||
self.device_edit.deleteLater()
|
||||
self.device_edit = None
|
||||
|
||||
if getattr(self, "entry_edit", None) is not None:
|
||||
self.entry_edit.close()
|
||||
self.entry_edit.deleteLater()
|
||||
self.entry_edit = None
|
||||
|
||||
if getattr(self, "dap_combo", None) is not None:
|
||||
self.dap_combo.close()
|
||||
self.dap_combo.deleteLater()
|
||||
@@ -575,7 +570,4 @@ class CurveTree(BECWidget, QWidget):
|
||||
all_items = list(self.all_items)
|
||||
for item in all_items:
|
||||
item.remove_self()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.cleanup()
|
||||
return super().closeEvent(event)
|
||||
super().cleanup()
|
||||
|
||||
@@ -1683,9 +1683,14 @@ class Waveform(PlotBase):
|
||||
return None
|
||||
|
||||
if hasattr(self.scan_item, "live_data"):
|
||||
readout_priority = self.scan_item.status_message.info["readout_priority"] # live data
|
||||
readout_priority = self.scan_item.status_message.info.get(
|
||||
"readout_priority"
|
||||
) # live data
|
||||
else:
|
||||
readout_priority = self.scan_item.metadata["bec"]["readout_priority"] # history
|
||||
readout_priority = self.scan_item.metadata["bec"].get("readout_priority") # history
|
||||
|
||||
if readout_priority is None:
|
||||
return None
|
||||
|
||||
# Reset sync/async curve lists
|
||||
self._async_curves.clear()
|
||||
|
||||
@@ -8,7 +8,7 @@ from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import QMimeData, QSize, Qt, Signal
|
||||
from qtpy.QtGui import QDrag
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QToolButton, QWidget
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QTabWidget, QToolButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
||||
@@ -18,6 +18,9 @@ from bec_widgets.widgets.services.device_browser.device_item.device_config_dialo
|
||||
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
||||
DeviceConfigForm,
|
||||
)
|
||||
from bec_widgets.widgets.services.device_browser.device_item.device_signal_display import (
|
||||
SignalDisplay,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtGui import QMouseEvent
|
||||
@@ -38,10 +41,25 @@ class DeviceItem(ExpandableGroupFrame):
|
||||
self._expanded_first_time = False
|
||||
self._data = None
|
||||
self.device = device
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.set_layout(layout)
|
||||
|
||||
self._layout = QHBoxLayout()
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._tab_widget = QTabWidget(tabShape=QTabWidget.TabShape.Rounded)
|
||||
self._tab_widget.setDocumentMode(True)
|
||||
self._layout.addWidget(self._tab_widget)
|
||||
|
||||
self.set_layout(self._layout)
|
||||
|
||||
self._form_page = QWidget()
|
||||
self._form_page_layout = QVBoxLayout()
|
||||
self._form_page.setLayout(self._form_page_layout)
|
||||
|
||||
self._signal_page = QWidget()
|
||||
self._signal_page_layout = QVBoxLayout()
|
||||
self._signal_page.setLayout(self._signal_page_layout)
|
||||
|
||||
self._tab_widget.addTab(self._form_page, "Configuration")
|
||||
self._tab_widget.addTab(self._signal_page, "Signals")
|
||||
self.adjustSize()
|
||||
|
||||
def _create_title_layout(self, title: str, icon: str):
|
||||
@@ -64,7 +82,9 @@ class DeviceItem(ExpandableGroupFrame):
|
||||
if not self.expanded and not self._expanded_first_time:
|
||||
self._expanded_first_time = True
|
||||
self.form = DeviceConfigForm(parent=self, pretty_display=True)
|
||||
self._contents.layout().addWidget(self.form)
|
||||
self._form_page_layout.addWidget(self.form)
|
||||
self.signals = SignalDisplay(parent=self, device=self.device)
|
||||
self._signal_page_layout.addWidget(self.signals)
|
||||
self._reload_config()
|
||||
self.broadcast_size_hint.emit(self.sizeHint())
|
||||
super().switch_expanded_state()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.widgets.containers.dock.dock import BECDock
|
||||
from bec_widgets.widgets.utility.signal_label.signal_label import SignalLabel
|
||||
|
||||
|
||||
class SignalDisplay(BECWidget, QWidget):
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client=None,
|
||||
device: str = "",
|
||||
config: ConnectionConfig = None,
|
||||
gui_id: str | None = None,
|
||||
theme_update: bool = False,
|
||||
parent_dock: BECDock | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""A widget to display all the signals from a given device, and allow getting
|
||||
a fresh reading."""
|
||||
super().__init__(client, config, gui_id, theme_update, parent_dock, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
self._layout = QVBoxLayout()
|
||||
self.setLayout(self._layout)
|
||||
self._content = QWidget()
|
||||
self._layout.addWidget(self._content)
|
||||
self._device = device
|
||||
self.device = device
|
||||
|
||||
@SafeSlot()
|
||||
def _refresh(self):
|
||||
if self.device in self.dev:
|
||||
self.dev.get(self.device).read(cached=False)
|
||||
self.dev.get(self.device).read_configuration(cached=False)
|
||||
|
||||
def _add_refresh_button(self):
|
||||
button_holder = QWidget()
|
||||
button_holder.setLayout(QHBoxLayout())
|
||||
button_holder.layout().setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
button_holder.layout().setContentsMargins(0, 0, 0, 0)
|
||||
refresh_button = QToolButton()
|
||||
refresh_button.setIcon(
|
||||
material_icon(icon_name="refresh", size=(20, 20), convert_to_pixmap=False)
|
||||
)
|
||||
refresh_button.clicked.connect(self._refresh)
|
||||
button_holder.layout().addWidget(refresh_button)
|
||||
self._content_layout.addWidget(button_holder)
|
||||
|
||||
def _populate(self):
|
||||
self._content.deleteLater()
|
||||
self._content = QWidget()
|
||||
self._layout.addWidget(self._content)
|
||||
self._content_layout = QVBoxLayout()
|
||||
self._content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._content.setLayout(self._content_layout)
|
||||
|
||||
self._add_refresh_button()
|
||||
|
||||
if self._device in self.dev:
|
||||
for sig in self.dev[self.device]._info.get("signals", {}).keys():
|
||||
self._content_layout.addWidget(
|
||||
SignalLabel(
|
||||
device=self._device,
|
||||
signal=sig,
|
||||
show_select_button=False,
|
||||
show_default_units=True,
|
||||
)
|
||||
)
|
||||
self._content_layout.addStretch(1)
|
||||
else:
|
||||
self._content_layout.addWidget(
|
||||
QLabel(f"Device {self.device} not found in device manager!")
|
||||
)
|
||||
|
||||
@SafeProperty(str)
|
||||
def device(self):
|
||||
return self._device
|
||||
|
||||
@device.setter
|
||||
def device(self, value: str):
|
||||
self._device = value
|
||||
self._populate()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("light")
|
||||
widget = SignalDisplay(device="samx")
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -16,7 +16,6 @@ from qtpy.QtWidgets import (
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
@@ -180,6 +179,7 @@ class SignalLabel(BECWidget, QWidget):
|
||||
self._custom_units: str = custom_units
|
||||
self._show_default_units: bool = show_default_units
|
||||
self._decimal_places = 3
|
||||
self._dtype = None
|
||||
|
||||
self._show_hinted_signals: bool = True
|
||||
self._show_normal_signals: bool = False
|
||||
@@ -241,8 +241,10 @@ class SignalLabel(BECWidget, QWidget):
|
||||
"""Subscribe to the Redis topic for the device to display"""
|
||||
if not self._connected and self._device and self._device in self.dev:
|
||||
self._connected = True
|
||||
self._readback_endpoint = MessageEndpoints.device_readback(self._device)
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, self._readback_endpoint)
|
||||
self._read_endpoint = MessageEndpoints.device_read(self._device)
|
||||
self._read_config_endpoint = MessageEndpoints.device_read_configuration(self._device)
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, self._read_endpoint)
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, self._read_config_endpoint)
|
||||
self._manual_read()
|
||||
self.set_display_value(self._value)
|
||||
|
||||
@@ -250,7 +252,8 @@ class SignalLabel(BECWidget, QWidget):
|
||||
"""Unsubscribe from the Redis topic for the device to display"""
|
||||
if self._connected:
|
||||
self._connected = False
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._readback_endpoint)
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._read_endpoint)
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._read_config_endpoint)
|
||||
|
||||
def _manual_read(self):
|
||||
if self._device is None or not isinstance(
|
||||
@@ -259,8 +262,13 @@ class SignalLabel(BECWidget, QWidget):
|
||||
self._units = ""
|
||||
self._value = "__"
|
||||
return
|
||||
signal: Signal = (
|
||||
getattr(device, self.signal, None) if isinstance(device, Device) else device
|
||||
signal, info = (
|
||||
(
|
||||
getattr(device, self.signal, None),
|
||||
device._info.get("signals", {}).get(self._signal, {}).get("describe", {}),
|
||||
)
|
||||
if isinstance(device, Device)
|
||||
else (device, device.describe().get(self._device))
|
||||
)
|
||||
if not isinstance(signal, Signal): # Avoid getting other attributes of device, e.g. methods
|
||||
signal = None
|
||||
@@ -269,7 +277,8 @@ class SignalLabel(BECWidget, QWidget):
|
||||
self._value = "__"
|
||||
return
|
||||
self._value = signal.get()
|
||||
self._units = signal.get_device_config().get("egu", "")
|
||||
self._units = info.get("egu", "")
|
||||
self._dtype = info.get("dtype", "float")
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_readback(self, msg: dict, metadata: dict) -> None:
|
||||
@@ -278,8 +287,10 @@ class SignalLabel(BECWidget, QWidget):
|
||||
"""
|
||||
try:
|
||||
signal_to_read = self._patch_hinted_signal()
|
||||
self._value = msg["signals"][signal_to_read]["value"]
|
||||
self.set_display_value(self._value)
|
||||
_value = msg["signals"].get(signal_to_read, {}).get("value")
|
||||
if _value is not None:
|
||||
self._value = _value
|
||||
self.set_display_value(self._value)
|
||||
except Exception as e:
|
||||
self._display.setText("ERROR!")
|
||||
self._display.setToolTip(
|
||||
@@ -401,7 +412,10 @@ class SignalLabel(BECWidget, QWidget):
|
||||
if self._decimal_places == 0:
|
||||
return value
|
||||
try:
|
||||
return f"{float(value):0.{self._decimal_places}f}"
|
||||
if self._dtype in ("integer", "float"):
|
||||
return f"{float(value):0.{self._decimal_places}f}"
|
||||
else:
|
||||
return str(value)
|
||||
except ValueError:
|
||||
return value
|
||||
|
||||
|
||||
94
docs/user/widgets/waveform/waveform_ruleset.md
Normal file
94
docs/user/widgets/waveform/waveform_ruleset.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Waveform Widget Plotting Ruleset
|
||||
|
||||
The Waveform widget allows plotting data from scans generated within the BEC framework. Each scan produces a **scan item
|
||||
**, which includes measurement data from various devices, as well as additional metadata (e.g., devices driving the
|
||||
scan, such as `motor_x`, `motor_y`, etc.).
|
||||
|
||||
The rules described below define how data from different devices and scans can be plotted together, depending on the
|
||||
selected `x_mode`.
|
||||
|
||||
---
|
||||
|
||||
## Types of Curves
|
||||
|
||||
- **Live Curves**
|
||||
- Continuously updated from the currently running scan.
|
||||
- Dynamically adapt based on live data.
|
||||
|
||||
- **History Curves**
|
||||
- Static curves representing data from a specific completed scan (e.g., scan 110).
|
||||
- Displayed only if compatible with the currently selected x-axis device.
|
||||
|
||||
---
|
||||
|
||||
## General Compatibility Rules
|
||||
|
||||
- Data from devices within **the same scan item** can always be plotted against each other, provided they have the same
|
||||
number of data points.
|
||||
- Example: plotting `detector_1` against `detector_2` from scan 110 to check signal correlation is valid.
|
||||
|
||||
- Data from **different scan items** cannot be plotted against each other.
|
||||
- Example: plotting x-data from `motor_x` in scan 110 against y-data from `detector_1` in scan 111 is not allowed.
|
||||
|
||||
---
|
||||
|
||||
## Specific Rules for Each `x_mode`
|
||||
|
||||
### 1. `x_mode='auto'` (default)
|
||||
|
||||
- Automatically determines the device for x-axis scaling from the currently running (live) scan.
|
||||
- The primary device used for scaling is the first device listed in the current scan's `scan_report_devices` (usually a
|
||||
positioner in the case of step scans).
|
||||
- **Live Curves**:
|
||||
- Always plotted against the selected x-axis device from the current scan.
|
||||
- **History Curves**:
|
||||
- Each history curve is linked to a specific scan item with scan ID.
|
||||
- Waveform widget checks compatibility with the currently selected x-axis device from the live scan.
|
||||
- If the selected x-axis device data exists in the history curve’s original scan item, the curve is displayed.
|
||||
- If the selected x-axis device data does not exist in the original scan item, the history curve remains **hidden**
|
||||
until a compatible device is selected again.
|
||||
|
||||
_Example Scenario_:
|
||||
|
||||
- The live scan currently uses `motor_x` as the x-axis device. Any history curve will only be displayed if its original
|
||||
scan item contains data for `motor_x`. If not, the history curve is hidden.
|
||||
|
||||
---
|
||||
|
||||
### 2. `x_mode='timestamp'`
|
||||
|
||||
- X-axis scaling is based on timestamps from each data point.
|
||||
- All curves, both live and history, are always compatible, as timestamps provide a universal and absolute reference for
|
||||
the x-axis.
|
||||
- Curves from different scan items can appear simultaneously, regardless of the devices measured.
|
||||
|
||||
---
|
||||
|
||||
### 3. `x_mode='index'`
|
||||
|
||||
- X-axis scaling uses data point indices (0, 1, 2, ..., N-1).
|
||||
- Allows plotting multiple curves of varying lengths in the same view.
|
||||
- All curves are always compatible, as indices represent relative positions, independent of device or timestamp, it is
|
||||
up to the user to interpret the x-axis.
|
||||
|
||||
---
|
||||
|
||||
### 4. `x_mode='device'`
|
||||
|
||||
- User explicitly selects a device to scale the x-axis.
|
||||
- The chosen device must exist in each curve’s respective scan item.
|
||||
- **Live Curves**:
|
||||
- Continuously displayed if the selected device data is being measured in the current scan.
|
||||
- **History Curves**:
|
||||
- Displayed only if the selected device exists in the scan item from which the history curve originates.
|
||||
- Remain **hidden** if the selected device is not present in the original scan item, reappearing only when a
|
||||
compatible device is chosen again.
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Points
|
||||
|
||||
- Each curve stores its own independent x and y data sets (as it is defined by `pg.PlotDataItem`, allowing simultaneous
|
||||
plotting of multiple curves with different data lengths.
|
||||
- Compatibility checks ensure that plotting meaningful comparisons is always possible, avoiding combinations of
|
||||
unrelated or non-compatible datasets.
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "2.18.0"
|
||||
version = "2.21.2"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
@@ -14,7 +14,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"bec_ipython_client>=3.42.4, <=4.0", # needed for jupyter console
|
||||
"bec_lib>=3.42.4, <=4.0",
|
||||
"bec_lib>=3.44, <=4.0",
|
||||
"bec_qthemes~=0.7, >=0.7",
|
||||
"black~=25.0", # needed for bw-generate-cli
|
||||
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_available_widgets(qtbot, connected_client_gui_obj):
|
||||
gui = connected_client_gui_obj
|
||||
dock_area = gui.bec
|
||||
# Number of top level widgets, should be 4
|
||||
top_level_widgets_count = 8
|
||||
top_level_widgets_count = 12
|
||||
assert len(gui._server_registry) == top_level_widgets_count
|
||||
# Number of widgets with parent_id == None, should be 2
|
||||
widgets = [
|
||||
|
||||
@@ -93,7 +93,7 @@ def test_curve_setting_switch_device_mode(curve_setting_fixture, qtbot):
|
||||
assert curve_setting.device_x.isEnabled()
|
||||
|
||||
# This line edit should reflect the waveform.x_axis_mode["name"], or be blank if none
|
||||
assert curve_setting.device_x.text() == wf.x_axis_mode["name"]
|
||||
assert curve_setting.device_x.currentText() == ""
|
||||
|
||||
|
||||
def test_curve_setting_refresh(curve_setting_fixture, qtbot):
|
||||
@@ -127,8 +127,8 @@ def test_change_device_from_target_widget(curve_setting_fixture, qtbot):
|
||||
|
||||
assert curve_setting.mode_combo.currentText() == "device"
|
||||
assert curve_setting.device_x.isEnabled()
|
||||
assert curve_setting.device_x.text() == wf.x_axis_mode["name"]
|
||||
assert curve_setting.signal_x.text() == wf.x_axis_mode["entry"]
|
||||
assert curve_setting.device_x.currentText() == wf.x_axis_mode["name"]
|
||||
assert curve_setting.signal_x.currentText() == f"{wf.x_axis_mode['entry']} (readback)"
|
||||
|
||||
|
||||
##################################################
|
||||
|
||||
@@ -3,6 +3,7 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
from qtpy.QtCore import QPoint, Qt
|
||||
from qtpy.QtWidgets import QTabWidget
|
||||
|
||||
from bec_widgets.widgets.services.device_browser.device_browser import DeviceBrowser
|
||||
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
||||
@@ -86,8 +87,13 @@ def test_device_item_expansion(device_browser, qtbot):
|
||||
device_item: QListWidgetItem = device_browser.ui.device_list.itemAt(0, 0)
|
||||
widget: DeviceItem = device_browser.ui.device_list.itemWidget(device_item)
|
||||
qtbot.mouseClick(widget._expansion_button, Qt.MouseButton.LeftButton)
|
||||
form = widget._contents.layout().itemAt(0).widget()
|
||||
qtbot.waitUntil(lambda: isinstance(form, DeviceConfigForm), timeout=500)
|
||||
tab_widget: QTabWidget = widget._contents.layout().itemAt(0).widget()
|
||||
qtbot.waitUntil(lambda: tab_widget.widget(0) is not None, timeout=100)
|
||||
qtbot.waitUntil(
|
||||
lambda: isinstance(tab_widget.widget(0).layout().itemAt(0).widget(), DeviceConfigForm),
|
||||
timeout=100,
|
||||
)
|
||||
form = tab_widget.widget(0).layout().itemAt(0).widget()
|
||||
assert widget.expanded
|
||||
assert (name_field := form.widget_dict.get("name")) is not None
|
||||
qtbot.waitUntil(lambda: name_field.getValue() == "samx", timeout=500)
|
||||
|
||||
@@ -144,3 +144,12 @@ def test_signal_lineedit(device_signal_line_edit):
|
||||
assert device_signal_line_edit._is_valid_input is True
|
||||
device_signal_line_edit.setText("invalid")
|
||||
assert device_signal_line_edit._is_valid_input is False
|
||||
|
||||
|
||||
def test_device_signal_input_base_cleanup(qtbot, mocked_client):
|
||||
|
||||
widget = DeviceInputWidget(client=mocked_client)
|
||||
widget.close()
|
||||
widget.deleteLater()
|
||||
|
||||
mocked_client.callbacks.remove.assert_called_once_with(widget._device_update_register)
|
||||
|
||||
@@ -109,6 +109,25 @@ def test_launch_window_launch_plugin_auto_update(bec_launch_window):
|
||||
"dock_area": mock.MagicMock(),
|
||||
"scan_progress": mock.MagicMock(),
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"launcher": mock.MagicMock(),
|
||||
"dock_area": mock.MagicMock(),
|
||||
"scan_progress_simple": mock.MagicMock(),
|
||||
"scan_progress_full": mock.MagicMock(),
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"launcher": mock.MagicMock(),
|
||||
"dock_area": mock.MagicMock(),
|
||||
"scan_progress_simple": mock.MagicMock(),
|
||||
"scan_progress_full": mock.MagicMock(),
|
||||
"hover_widget": mock.MagicMock(),
|
||||
},
|
||||
True,
|
||||
),
|
||||
],
|
||||
@@ -147,6 +166,25 @@ def test_gui_server_turns_off_the_lights(bec_launch_window, connections, hide):
|
||||
"dock_area": mock.MagicMock(),
|
||||
"scan_progress": mock.MagicMock(),
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
"launcher": mock.MagicMock(),
|
||||
"dock_area": mock.MagicMock(),
|
||||
"scan_progress_simple": mock.MagicMock(),
|
||||
"scan_progress_full": mock.MagicMock(),
|
||||
},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{
|
||||
"launcher": mock.MagicMock(),
|
||||
"dock_area": mock.MagicMock(),
|
||||
"scan_progress_simple": mock.MagicMock(),
|
||||
"scan_progress_full": mock.MagicMock(),
|
||||
"hover_widget": mock.MagicMock(),
|
||||
},
|
||||
False,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import webbrowser
|
||||
|
||||
import pytest
|
||||
from qtpy.QtWidgets import QFrame
|
||||
from qtpy.QtCore import QEvent, QPoint, QPointF
|
||||
from qtpy.QtGui import QEnterEvent
|
||||
from qtpy.QtWidgets import QApplication, QFrame, QLabel
|
||||
|
||||
from bec_widgets.widgets.containers.main_window.addons.hover_widget import (
|
||||
HoverWidget,
|
||||
WidgetTooltip,
|
||||
)
|
||||
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
|
||||
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
@@ -228,3 +234,60 @@ def test_scan_progress_bar_hide_animation(qtbot, bec_main_window):
|
||||
qtbot.waitUntil(lambda: container.maximumWidth() == 0, timeout=2000)
|
||||
|
||||
assert container.maximumWidth() == 0
|
||||
|
||||
|
||||
#################################################################
|
||||
# Tests for hover widget and tooltip behaviour
|
||||
|
||||
|
||||
def test_hover_widget_tooltip(qtbot):
|
||||
"""
|
||||
After a HoverWidget is closed, its WidgetTooltip must be gone.
|
||||
"""
|
||||
simple = QLabel("Hover me")
|
||||
full = QLabel("Full details")
|
||||
hover = create_widget(qtbot, HoverWidget, simple=simple, full=full)
|
||||
|
||||
assert hover._simple is simple
|
||||
assert hover._full is full
|
||||
assert hover._tooltip is None
|
||||
|
||||
|
||||
def test_widget_tooltip_show_and_hide(qtbot):
|
||||
"""
|
||||
WidgetTooltip should appear when show_above is called and hide on Leave.
|
||||
"""
|
||||
full_lbl = QLabel("Standalone tooltip content")
|
||||
tooltip = create_widget(qtbot, WidgetTooltip, content=full_lbl)
|
||||
|
||||
# Show above an arbitrary point
|
||||
pos = QPoint(200, 200)
|
||||
tooltip.show_above(pos)
|
||||
assert tooltip.isVisible()
|
||||
|
||||
# Send a synthetic Leave event
|
||||
QApplication.sendEvent(tooltip, QEvent(QEvent.Leave))
|
||||
qtbot.waitUntil(lambda: not tooltip.isVisible(), timeout=500)
|
||||
assert not tooltip.isVisible()
|
||||
|
||||
|
||||
def test_hover_widget_mouse_events(qtbot):
|
||||
"""
|
||||
Verify that HoverWidget responds correctly to Enter, MouseMove, and Leave
|
||||
events, keeping the tooltip visible only while the pointer is inside.
|
||||
"""
|
||||
simple = QLabel("Hover‑target")
|
||||
full = QLabel("Full‑view")
|
||||
hover = create_widget(qtbot, HoverWidget, simple=simple, full=full)
|
||||
|
||||
local = QPointF(hover.rect().center()) # inside widget
|
||||
scene = QPointF(hover.mapTo(hover.window(), local.toPoint()))
|
||||
global_ = QPointF(hover.mapToGlobal(local.toPoint()))
|
||||
|
||||
enter_event = QEnterEvent(local, scene, global_)
|
||||
hover.enterEvent(event=enter_event)
|
||||
qtbot.wait(200)
|
||||
|
||||
assert hover._tooltip is not None
|
||||
assert hover._tooltip.isVisible()
|
||||
assert hover._tooltip.content is full
|
||||
|
||||
@@ -29,4 +29,4 @@ def test_gui_server_get_service_config(gui_server):
|
||||
"""
|
||||
Test that the server is started with the correct arguments.
|
||||
"""
|
||||
assert gui_server._get_service_config().config is ServiceConfig().config
|
||||
assert gui_server._get_service_config().config == ServiceConfig().config
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.messages import AvailableResourceMessage, ScanQueueHistoryMessage, ScanQueueMessage
|
||||
from bec_lib.messages import AvailableResourceMessage, ScanHistoryMessage
|
||||
from qtpy.QtCore import QModelIndex, Qt
|
||||
|
||||
from bec_widgets.utils.forms_from_types.items import StrFormItem
|
||||
@@ -221,82 +221,36 @@ available_scans_message = AvailableResourceMessage(
|
||||
}
|
||||
)
|
||||
|
||||
scan_history = ScanQueueHistoryMessage(
|
||||
scan_history = ScanHistoryMessage(
|
||||
metadata={},
|
||||
status="COMPLETED",
|
||||
queue_id="94d7cb39-aa70-4060-92de-addcfb64e3c0",
|
||||
info={
|
||||
"queue_id": "94d7cb39-aa70-4060-92de-addcfb64e3c0",
|
||||
"scan_id": ["bc2aa11f-24f6-44d6-8717-95e97fb43015"],
|
||||
"is_scan": [True],
|
||||
"request_blocks": [
|
||||
{
|
||||
"msg": ScanQueueMessage(
|
||||
metadata={
|
||||
"file_suffix": None,
|
||||
"file_directory": None,
|
||||
"user_metadata": {},
|
||||
"RID": "99321ef7-00ac-4e0c-9120-ce689bd88a4d",
|
||||
},
|
||||
scan_type="line_scan",
|
||||
parameter={
|
||||
"args": {"samx": [0.0, 2.0]},
|
||||
"kwargs": {
|
||||
"steps": 10,
|
||||
"relative": False,
|
||||
"exp_time": 2.0,
|
||||
"burst_at_each_point": 1,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
},
|
||||
queue="primary",
|
||||
),
|
||||
"RID": "99321ef7-00ac-4e0c-9120-ce689bd88a4d",
|
||||
"scan_motors": ["samx"],
|
||||
"readout_priority": {
|
||||
"monitored": ["samx"],
|
||||
"baseline": [],
|
||||
"on_request": [],
|
||||
"async": [],
|
||||
},
|
||||
"is_scan": True,
|
||||
"scan_number": 176,
|
||||
"scan_id": "bc2aa11f-24f6-44d6-8717-95e97fb43015",
|
||||
"metadata": {
|
||||
"file_suffix": None,
|
||||
"file_directory": None,
|
||||
"user_metadata": {},
|
||||
"RID": "99321ef7-00ac-4e0c-9120-ce689bd88a4d",
|
||||
},
|
||||
"content": {
|
||||
"scan_type": "line_scan",
|
||||
"parameter": {
|
||||
"args": {"samx": [0.0, 2.0]},
|
||||
"kwargs": {
|
||||
"steps": 10,
|
||||
"relative": False,
|
||||
"exp_time": 2.0,
|
||||
"burst_at_each_point": 1,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
},
|
||||
"queue": "primary",
|
||||
},
|
||||
"report_instructions": [{"scan_progress": 10}],
|
||||
}
|
||||
],
|
||||
"scan_number": [176],
|
||||
"status": "COMPLETED",
|
||||
"active_request_block": None,
|
||||
scan_id="79cbef20-9ebe-45bb-a44c-f518be27a25c",
|
||||
scan_number=1,
|
||||
dataset_number=1,
|
||||
file_path="/somepath/scan_1.h5",
|
||||
exit_status="closed",
|
||||
start_time=1750618470.936856,
|
||||
end_time=1750618473.668227,
|
||||
scan_name="line_scan",
|
||||
num_points=100,
|
||||
request_inputs={
|
||||
"arg_bundle": ["samx", 0.0, 2.0],
|
||||
"inputs": {},
|
||||
"kwargs": {
|
||||
"steps": 10,
|
||||
"exp_time": 2,
|
||||
"relative": False,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
},
|
||||
queue="primary",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def scan_control(qtbot, mocked_client): # , mock_dev):
|
||||
mocked_client.connector.set(MessageEndpoints.available_scans(), available_scans_message)
|
||||
mocked_client.connector.lpush(MessageEndpoints.scan_queue_history(), scan_history)
|
||||
mocked_client.connector.xadd(
|
||||
topic=MessageEndpoints.scan_history(), msg_dict={"data": scan_history}
|
||||
)
|
||||
widget = ScanControl(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
|
||||
@@ -121,11 +121,13 @@ def test_custom_label(signal_label: SignalLabel, qtbot):
|
||||
|
||||
def test_units_in_display(signal_label: SignalLabel, qtbot):
|
||||
signal_label._value = "1.8"
|
||||
signal_label._dtype = "float"
|
||||
signal_label.custom_units = "Mfurlong μfortnight⁻¹"
|
||||
assert signal_label._display.text() == "1.800 Mfurlong μfortnight⁻¹"
|
||||
|
||||
|
||||
def test_decimal_places(signal_label: SignalLabel, qtbot):
|
||||
signal_label._dtype = "float"
|
||||
signal_label.decimal_places = 2
|
||||
signal_label.set_display_value("123.456")
|
||||
assert signal_label._display.text() == "123.46 m/s"
|
||||
@@ -226,6 +228,7 @@ def test_handle_readback(signal_label: SignalLabel, qtbot):
|
||||
signal_label.device = "samx"
|
||||
signal_label.signal = "readback"
|
||||
signal_label.custom_units = "μm"
|
||||
signal_label._dtype = "float"
|
||||
signal_label.on_device_readback({"random": {"stuff": "in", "corrupted": "reading"}}, {})
|
||||
assert signal_label._display.text() == "ERROR!"
|
||||
assert "Error processing incoming reading" in signal_label._display.toolTip()
|
||||
|
||||
@@ -806,6 +806,13 @@ def test_show_curve_settings_popup(qtbot, mocked_client):
|
||||
assert wf.curve_settings_dialog.isVisible()
|
||||
assert curve_action.isChecked()
|
||||
|
||||
# add a new row to the curve tree
|
||||
wf.curve_settings_dialog.widget.curve_manager.toolbar.widgets["add"].action.trigger()
|
||||
wf.curve_settings_dialog.widget.curve_manager.toolbar.widgets["add"].action.trigger()
|
||||
qtbot.wait(100)
|
||||
# Check that the new row is added
|
||||
assert wf.curve_settings_dialog.widget.curve_manager.tree.model().rowCount() == 2
|
||||
|
||||
wf.curve_settings_dialog.close()
|
||||
assert wf.curve_settings_dialog is None
|
||||
assert not curve_action.isChecked(), "Should be unchecked after closing dialog"
|
||||
|
||||
Reference in New Issue
Block a user