1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-10 18:50:55 +02:00

Compare commits

..

29 Commits

Author SHA1 Message Date
302ae90139 docs: added video tutorial section with BSEG YT video 2024-08-06 17:42:15 +02:00
semantic-release
1405068925 0.93.0
Automatically generated by python-semantic-release
2024-08-05 14:11:40 +00:00
5aad401ef8 feat(themes): moved themes to bec_qthemes
This reverts commit fd6ae91993
2024-08-05 14:24:05 +02:00
semantic-release
885dcfda89 0.92.5
Automatically generated by python-semantic-release
2024-08-05 12:20:01 +00:00
30fef929cf fix(spinner): stop timer on close event 2024-08-05 13:54:20 +02:00
1f30dd73a9 fix(status_box): fix cleanup of status box 2024-08-05 13:54:20 +02:00
73cd11e472 test: register all widgets with qtbot and close them 2024-08-05 13:54:20 +02:00
7616ca0e14 refactor(queue): refactored bec queue to inherit only from qwidget 2024-08-05 13:54:20 +02:00
semantic-release
ca29a69779 0.92.4
Automatically generated by python-semantic-release
2024-07-31 07:24:44 +00:00
dcc5fd71ee fix: fix missmatch of signal/slot in image and motormap 2024-07-29 16:05:21 +02:00
semantic-release
fee4901657 0.92.3
Automatically generated by python-semantic-release
2024-07-28 10:05:23 +00:00
71873ddf35 fix(docs): moved to pyside6 2024-07-28 11:17:17 +02:00
semantic-release
f8552ca551 0.92.2
Automatically generated by python-semantic-release
2024-07-28 08:53:31 +00:00
995a795060 fix(widgets): fixed import for tictactoe example 2024-07-28 10:42:32 +02:00
semantic-release
7ab81c5797 0.92.1
Automatically generated by python-semantic-release
2024-07-28 07:04:29 +00:00
bc1e23944c fix: use SafeSlot instead of Slot 2024-07-28 08:54:24 +02:00
a3fe20500a fix: linting 2024-07-28 08:54:24 +02:00
61a4e32deb fix: always add a QApplication for tests 2024-07-28 08:54:24 +02:00
3d681f77e1 fix: add xvfb to draw offscreen 2024-07-28 08:54:24 +02:00
5a9ccfd1f6 fix: reset ErrorPopup singleton between tests 2024-07-26 11:58:07 +02:00
fc57b7a126 fix: metaclass + QObject segfaults PyQt(cpp bindings) 2024-07-26 11:58:07 +02:00
06205e0790 build(ci): install ophyd_devices in editable mode for pipelines 2024-07-25 09:46:58 +02:00
4be6fd6b83 refactor: renamed DeviceMonitor2DMessage 2024-07-25 09:46:58 +02:00
714e1e139e refactor: rename device_monitor to device_monitor_2d 2024-07-25 09:46:58 +02:00
semantic-release
01c0e0b1df 0.92.0
Automatically generated by python-semantic-release
2024-07-24 18:52:56 +00:00
4457ef2147 fix(dock): custom label can be created closable 2024-07-23 22:22:16 +02:00
8ca60d54b3 feat(dock): dock style sheets updated 2024-07-23 22:22:16 +02:00
5696c993dc feat(general_gui): general gui added 2024-07-23 22:22:16 +02:00
1206e15309 fix(device_combobox): set minimum size to 125px 2024-07-23 22:22:16 +02:00
52 changed files with 742 additions and 246 deletions

View File

@@ -43,13 +43,20 @@ stages:
- export QTWEBENGINE_DISABLE_SANDBOX=1
.clone-repos: &clone-repos
- echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
- echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
.install-repos: &install-repos
- pip install -e ./ophyd_devices
- pip install -e ./bec/bec_lib[dev]
- pip install -e ./bec/bec_ipython_client
.install-os-packages: &install-os-packages
- apt-get update
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
- *install-qt-webengine-deps
before_script:
@@ -131,8 +138,7 @@ tests:
script:
- *clone-repos
- *install-os-packages
- pip install -e ./bec/bec_lib[dev]
- pip install -e ./bec/bec_ipython_client
- *install-repos
- pip install -e .[dev,pyqt6]
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
- coverage report
@@ -169,8 +175,7 @@ test-matrix:
script:
- *clone-repos
- *install-os-packages
- pip install -e ./bec/bec_lib[dev]
- pip install -e ./bec/bec_ipython_client
- *install-repos
- pip install -e .[dev,$QT_PCKG]
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
allow_failure: true
@@ -195,10 +200,9 @@ end-2-end-conda:
- cd ./bec
- source ./bin/install_bec_dev.sh -t
- pip install -e ./bec_lib[dev]
- pip install -e ./bec_ipython_client[dev]
- cd ../
- pip install -e ./ophyd_devices
- pip install -e .[dev,pyqt6]
- cd ./tests/end-2-end
- pytest -v --start-servers --flush-redis --random-order

View File

@@ -1,5 +1,87 @@
# CHANGELOG
## v0.93.0 (2024-08-05)
### Feature
* feat(themes): moved themes to bec_qthemes
This reverts commit fd6ae91993a23a7b8dbb2cf3c4b7c3eda6d2b0f6 ([`5aad401`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5aad401ef8774c7330784f72cd3b9d8c253e2b6a))
## v0.92.5 (2024-08-05)
### Fix
* fix(spinner): stop timer on close event ([`30fef92`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30fef929cf6fb4b73f48151c92a0ee54c734031d))
* fix(status_box): fix cleanup of status box ([`1f30dd7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f30dd73a9c1e3135087a5eef92c7329f54a604e))
### Refactor
* refactor(queue): refactored bec queue to inherit only from qwidget ([`7616ca0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7616ca0e145e233ccb48029a8c0b54b54b5b4194))
### Test
* test: register all widgets with qtbot and close them ([`73cd11e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/73cd11e47277e4437554b785a9551b28a572094f))
## v0.92.4 (2024-07-31)
### Fix
* fix: fix missmatch of signal/slot in image and motormap ([`dcc5fd7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dcc5fd71ee9f51767a7b2b1ed6200e89d1ef754c))
## v0.92.3 (2024-07-28)
### Fix
* fix(docs): moved to pyside6 ([`71873dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/71873ddf359516ded8f74f4d2f73df4156aa1368))
## v0.92.2 (2024-07-28)
### Fix
* fix(widgets): fixed import for tictactoe example ([`995a795`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/995a795060bebe25c17108d80ae0fa30463f03b1))
## v0.92.1 (2024-07-28)
### Build
* build(ci): install ophyd_devices in editable mode for pipelines ([`06205e0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06205e07903d93accf40abab153f440059f236ed))
### Fix
* fix: use SafeSlot instead of Slot ([`bc1e239`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bc1e23944cc0e5a861e3d0b4dc5b4ac6292d5269))
* fix: linting ([`a3fe205`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3fe20500ae2ac03dcde07432f7e21ce5262ce46))
* fix: always add a QApplication for tests ([`61a4e32`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61a4e32deb337ed27f2f43358b88b7266413b58e))
* fix: add xvfb to draw offscreen ([`3d681f7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3d681f77e144e74138fc5fa65630004d7c166878))
* fix: reset ErrorPopup singleton between tests ([`5a9ccfd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5a9ccfd1f6d2aacd5d86c1a34f74163b272d1ae4))
* fix: metaclass + QObject segfaults PyQt(cpp bindings) ([`fc57b7a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fc57b7a1262031a2df9e6a99493db87e766b779a))
### Refactor
* refactor: renamed DeviceMonitor2DMessage ([`4be6fd6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4be6fd6b83ea1048f16310f7d2bbe777b13b245e))
* refactor: rename device_monitor to device_monitor_2d ([`714e1e1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/714e1e139e0033d2725fefb636c419ca137a68c6))
## v0.92.0 (2024-07-24)
### Feature
* feat(dock): dock style sheets updated ([`8ca60d5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ca60d54b3cfa621172ce097fc1ba514c47ebac7))
* feat(general_gui): general gui added ([`5696c99`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5696c993dc1c0da40ff3e99f754c246cc017ea32))
### Fix
* fix(dock): custom label can be created closable ([`4457ef2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4457ef2147e21b856c9dcaf63c81ba98002dcaf1))
* fix(device_combobox): set minimum size to 125px ([`1206e15`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1206e153094cd8505badf69a1461572a76b4c5ad))
## v0.91.0 (2024-07-23)
### Feature
@@ -64,86 +146,6 @@ This reverts commit 3798714369adf4023f833b7749d2f46a0ec74eee ([`fd6ae91`](https:
## v0.88.1 (2024-07-22)
### Documentation
* docs: readthedocs icon path fixed ([`2bcaa42`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2bcaa4256d6daaefacb3ead8c72458d7b1498e29))
### Fix
* fix(plot_base): set_xy autorange moved to plotbase from waveform ([`a3dff7d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3dff7decc16115c12dc6b4ef1572552368da309))
### Refactor
* refactor(toolbar): generalizations of the ToolBarAction ([`ad112d1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ad112d1f08157f6987edd48a0bacf9f669ef1997))
## v0.88.0 (2024-07-19)
### Feature
* feat(waveform_widget): designer plugin added ([`1f8ef52`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1f8ef52b606283038052640849094f515a463403))
* feat(waveform_widget): switch between drag and rectangle mode ([`2be009c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2be009c6477ba26c5cfb4d827534c5d5eb428999))
* feat(waveform_widget): autorange button ([`8df6b00`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8df6b003e5c6a942fa2e875d9790e492c087bf26))
* feat(waveform_widget): dap parameter window ([`1e551d6`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1e551d6e9696f79ea2e0a179d13a4fc6c2a128b2))
* feat(waveform): export to matplotlib window of current scene ([`8d93405`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d9340539967b06b1e15f21a2106a39d5c740f31))
* feat(figure): export dialog can be launched from CLI and from toolbar ([`6ff6111`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ff611109153b9412dce37c527b19e839d99bba7))
* feat(waveform_widget): added error handle utility ([`a8ff1d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8ff1d4cd09cae5eaeb4bd0ea90fdd102e32f3a3))
* feat(curve_dialog): add DAP functionality ([`e830565`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e8305652fde384da037242cf8f7e3606f22bcfb6))
* feat(curve_dialog): curves can be added ([`c926a75`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c926a75a7927d672c044ea8f68771209ae5accc6))
* feat(waveform_widget): BECWaveformWidget toolbar added import/export config ([`fa9b171`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fa9b17191ddbb4043a658dae9aa0801e1dc22b84))
* feat(waveform_widget): BECWaveformWidget added with toolbar ([`755b394`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/755b394c1c4d7c443c442d89c630d08ce5415554))
### Fix
* fix(waveform_widget): plot API unified with BECFigure ([`2c8764a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2c8764a27de89b39b717032b58465e120ec57fbc))
* fix(colormap_selector): compatibility for PyQt6 when using designer fixed ([`50135b5`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/50135b5fe90a88618291e9357f180cb19251dace))
* fix(waveform_widget): adapted for BECWidget base class ([`6eb313f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6eb313fa76e559d62ecd8fa8849142b83817e47c))
* fix(waveform_widget): temporary disabled save/load config ([`7089cf3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7089cf356a43d805241d5621952e544d690e65e0))
* fix(waveform_widget): use @SafeSlot decorator for automatic error message ([`8e588d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8e588d79c86e950f6915e89c08fa9415c4bd8033))
* fix(waveform): colormaps of curves can be changed and normalised
feat(waveform): colormap can be changed from curve dialog
fix(curve_dialog): default dialog parameters fixed
curve Dialog colormap WIP ([`33495cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33495cfe03b363f18db61d8af2983f49027b7a43))
* fix(waveform_widget): adapted for changes from improved scan logic from waveform widget ([`8ac35d7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ac35d7280b1ff007c10612228d163cc0c5d1a99))
### Refactor
* refactor(icons): icons moved to the assets directory ([`a8b6ef2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8b6ef20cccae87515b10f054d0ed5b10e152769))
* refactor(waveform_widget): removed PYSIDE6 check ([`47fcb9e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/47fcb9ebfe35ae600cced95a1edc68f6f6e37a04))
### Test
* test(waveform_widget): test added ([`8d764e2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8d764e2d46a1e017dadc3c4630648c1ca708afc2))
## v0.87.1 (2024-07-18)
### Fix
* fix(dock): added hasattr to cleanup method for widgets ([`d75c55b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d75c55b2b1ccf156fb789c7813f1c5bdf256f860))
* fix: add missing close() call, ensure jupyter console client.shutdown() is called in closeEvent ([`e52ee26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e52ee2604cb35096f1bd833ca9516d8a34197d35))
### Refactor
* refactor: BECWidget is a mixin based on BECConnector, for each QWidget in BEC
Handles closeEvent() and RPC registering/unregistering ([`c7feb69`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c7feb6952d590b569f7b0cba3b019a9af0ce0c93))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@@ -0,0 +1,92 @@
import os
import sys
from qtpy.QtCore import QSize
from qtpy.QtGui import QActionGroup, QIcon
from qtpy.QtWidgets import QApplication, QMainWindow, QStyle
import bec_widgets
from bec_widgets.examples.general_app.web_links import BECWebLinksMixin
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.ui_loader import UILoader
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class BECGeneralApp(QMainWindow):
def __init__(self, parent=None):
super(BECGeneralApp, self).__init__(parent)
ui_file_path = os.path.join(os.path.dirname(__file__), "general_app.ui")
self.load_ui(ui_file_path)
self.resize(1280, 720)
self.ini_ui()
def ini_ui(self):
self._setup_icons()
self._hook_menubar_docs()
self._hook_theme_bar()
def load_ui(self, ui_file):
loader = UILoader(self)
self.ui = loader.loader(ui_file)
self.setCentralWidget(self.ui)
def _hook_menubar_docs(self):
# BEC Docs
self.ui.action_BEC_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
# BEC Widgets Docs
self.ui.action_BEC_widgets_docs.triggered.connect(BECWebLinksMixin.open_bec_widgets_docs)
# Bug report
self.ui.action_bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
def change_theme(self, theme):
apply_theme(theme)
def _setup_icons(self):
help_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxQuestion)
bug_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation)
computer_icon = QIcon.fromTheme("computer")
widget_icon = QIcon(os.path.join(MODULE_PATH, "assets", "designer_icons", "dock_area.png"))
self.ui.action_BEC_docs.setIcon(help_icon)
self.ui.action_BEC_widgets_docs.setIcon(help_icon)
self.ui.action_bug_report.setIcon(bug_icon)
self.ui.central_tab.setTabIcon(0, widget_icon)
self.ui.central_tab.setTabIcon(1, computer_icon)
def _hook_theme_bar(self):
self.ui.action_light.setCheckable(True)
self.ui.action_dark.setCheckable(True)
# Create an action group to make sure only one can be checked at a time
theme_group = QActionGroup(self)
theme_group.addAction(self.ui.action_light)
theme_group.addAction(self.ui.action_dark)
theme_group.setExclusive(True)
# Connect the actions to the theme change method
self.ui.action_light.triggered.connect(lambda: self.change_theme("light"))
self.ui.action_dark.triggered.connect(lambda: self.change_theme("dark"))
self.ui.action_dark.trigger()
def main(): # pragma: no cover
app = QApplication(sys.argv)
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "app_icons", "BEC-Dark.png"), size=QSize(48, 48)
)
app.setWindowIcon(icon)
main_window = BECGeneralApp()
main_window.show()
sys.exit(app.exec_())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -0,0 +1,262 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1718</width>
<height>1139</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<property name="tabShape">
<enum>QTabWidget::TabShape::Rounded</enum>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTabWidget" name="central_tab">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="dock_area_tab">
<attribute name="title">
<string>Dock Area</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="BECDockArea" name="dock_area"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="vscode_tab">
<attribute name="icon">
<iconset theme="QIcon::ThemeIcon::Computer"/>
</attribute>
<attribute name="title">
<string>Visual Studio Code</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<widget class="VSCodeEditor" name="vscode"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1718</width>
<height>31</height>
</rect>
</property>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="action_BEC_docs"/>
<addaction name="action_BEC_widgets_docs"/>
<addaction name="action_bug_report"/>
</widget>
<widget class="QMenu" name="menuTheme">
<property name="title">
<string>Theme</string>
</property>
<addaction name="action_light"/>
<addaction name="action_dark"/>
</widget>
<addaction name="menuTheme"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="dock_scan_control">
<property name="windowTitle">
<string>Scan Control</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_2">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ScanControl" name="scan_control"/>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dock_status_2">
<property name="windowTitle">
<string>BEC Service Status</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_3">
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="BECStatusBox" name="bec_status_box_2"/>
</item>
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="dock_queue">
<property name="windowTitle">
<string>Scan Queue</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_4">
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="BECQueue" name="bec_queue">
<row/>
<column/>
<column/>
<column/>
<item row="0" column="0"/>
<item row="0" column="1"/>
<item row="0" column="2"/>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="action_BEC_docs">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
</property>
<property name="text">
<string>BEC Docs</string>
</property>
</action>
<action name="action_BEC_widgets_docs">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
</property>
<property name="text">
<string>BEC Widgets Docs</string>
</property>
</action>
<action name="action_bug_report">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DialogError"/>
</property>
<property name="text">
<string>Bug Report</string>
</property>
</action>
<action name="action_light">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Light</string>
</property>
</action>
<action name="action_dark">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Dark</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>WebsiteWidget</class>
<extends>QWebEngineView</extends>
<header>website_widget</header>
</customwidget>
<customwidget>
<class>BECQueue</class>
<extends>QTableWidget</extends>
<header>bec_queue</header>
</customwidget>
<customwidget>
<class>ScanControl</class>
<extends>QWidget</extends>
<header>scan_control</header>
</customwidget>
<customwidget>
<class>VSCodeEditor</class>
<extends>WebsiteWidget</extends>
<header>vs_code_editor</header>
</customwidget>
<customwidget>
<class>BECStatusBox</class>
<extends>QWidget</extends>
<header>bec_status_box</header>
</customwidget>
<customwidget>
<class>BECDockArea</class>
<extends>QWidget</extends>
<header>dock_area</header>
</customwidget>
<customwidget>
<class>QWebEngineView</class>
<extends></extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,15 @@
import webbrowser
class BECWebLinksMixin:
@staticmethod
def open_bec_docs():
webbrowser.open("https://beamline-experiment-control.readthedocs.io/en/latest/")
@staticmethod
def open_bec_widgets_docs():
webbrowser.open("https://bec.readthedocs.io/projects/bec-widgets/en/latest/")
@staticmethod
def open_bec_bug_report():
webbrowser.open("https://gitlab.psi.ch/groups/bec/-/issues/")

View File

@@ -7,7 +7,8 @@ import sys
from bec_ipython_client.main import BECIPythonClient
from qtpy.QtWidgets import QApplication
from tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)

View File

@@ -2,8 +2,9 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QPyDesignerCustomWidgetCollection
from tictactoe import TicTacToe
from tictactoeplugin import TicTacToePlugin
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoeplugin import TicTacToePlugin
# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin

View File

@@ -4,10 +4,10 @@ import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from tictactoe import TicTacToe
from tictactoetaskmenu import TicTacToeTaskMenuFactory
import bec_widgets
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoetaskmenu import TicTacToeTaskMenuFactory
DOM_XML = """
<ui language='c++'>

View File

@@ -1,11 +1,12 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtCore import Slot
from qtpy.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
from qtpy.QtGui import QAction
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
from tictactoe import TicTacToe
from bec_widgets.examples.plugin_example_pyside.tictactoe import TicTacToe
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
class TicTacToeDialog(QDialog): # pragma: no cover

View File

@@ -6,7 +6,7 @@ from qtpy.QtCore import QObject, Qt, Signal, Slot
from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget
def SafeSlot(*slot_args, **slot_kwargs):
def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
"""Function with args, acting like a decorator, applying "error_managed" decorator + Qt Slot
to the passed function, to display errors instead of potentially raising an exception
@@ -34,10 +34,7 @@ class WarningPopupUtility(QObject):
Utility class to show warning popups in the application.
"""
def __init__(self, parent=None):
super().__init__(parent)
@Slot(str, str, str, QWidget)
@SafeSlot(str, str, str, QWidget)
def show_warning_message(self, title, message, detailed_text, widget):
msg = QMessageBox(widget)
msg.setIcon(QMessageBox.Warning)
@@ -60,7 +57,10 @@ class WarningPopupUtility(QObject):
self.show_warning_message(title, message, detailed_text, widget)
class ErrorPopupUtility(QObject):
_popup_utility_instance = None
class _ErrorPopupUtility(QObject):
"""
Utility class to manage error popups in the application to show error messages to the users.
This class is singleton and the error popup can be enabled or disabled globally or attach to widget methods with decorator @error_managed.
@@ -68,24 +68,14 @@ class ErrorPopupUtility(QObject):
error_occurred = Signal(str, str, QWidget)
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super(ErrorPopupUtility, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self, parent=None):
if not self._initialized:
super().__init__(parent=parent)
self.error_occurred.connect(self.show_error_message)
self.enable_error_popup = False
self._initialized = True
sys.excepthook = self.custom_exception_hook
super().__init__(parent=parent)
self.error_occurred.connect(self.show_error_message)
self.enable_error_popup = False
self._initialized = True
sys.excepthook = self.custom_exception_hook
@Slot(str, str, QWidget)
@SafeSlot(str, str, QWidget)
def show_error_message(self, title, message, widget):
detailed_text = self.format_traceback(message)
error_message = self.parse_error_message(detailed_text)
@@ -157,13 +147,12 @@ class ErrorPopupUtility(QObject):
"""
self.enable_error_popup = bool(state)
@classmethod
def reset_singleton(cls):
"""
Reset the singleton instance.
"""
cls._instance = None
cls._initialized = False
def ErrorPopupUtility():
global _popup_utility_instance
if not _popup_utility_instance:
_popup_utility_instance = _ErrorPopupUtility()
return _popup_utility_instance
class ExampleWidget(QWidget): # pragma: no cover

View File

@@ -1,6 +1,7 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
class SettingWidget(QWidget):
"""

View File

@@ -10,11 +10,11 @@ import yaml
from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QApplication
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))

View File

@@ -2,11 +2,10 @@ import itertools
import re
from typing import Literal
import bec_qthemes
import numpy as np
import pyqtgraph as pg
import qdarkstyle
from pydantic_core import PydanticCustomError
from qdarkstyle import DarkPalette, LightPalette
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QApplication
@@ -14,7 +13,7 @@ CURRENT_THEME = "dark"
def get_theme_palette():
return DarkPalette if CURRENT_THEME == "dark" else LightPalette
return bec_qthemes.load_palette(CURRENT_THEME)
def apply_theme(theme: Literal["dark", "light"]):
@@ -30,7 +29,7 @@ def apply_theme(theme: Literal["dark", "light"]):
pg_widget.setBackground("k" if theme == "dark" else "w")
# now define stylesheet according to theme and apply it
style = qdarkstyle.load_stylesheet(palette=get_theme_palette())
style = bec_qthemes.load_stylesheet(theme)
app.setStyleSheet(style)

View File

@@ -1,12 +1,12 @@
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import Qt, Slot
from qtpy.QtWidgets import QHeaderView, QTableWidget, QTableWidgetItem, QWidget
from qtpy.QtWidgets import QHBoxLayout, QHeaderView, QTableWidget, QTableWidgetItem, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
class BECQueue(BECWidget, QTableWidget):
class BECQueue(BECWidget, QWidget):
"""
Widget to display the BEC queue.
"""
@@ -19,10 +19,14 @@ class BECQueue(BECWidget, QTableWidget):
gui_id: str = None,
):
super().__init__(client, config, gui_id)
QTableWidget.__init__(self, parent=parent)
self.setColumnCount(3)
self.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
header = self.horizontalHeader()
QWidget.__init__(self, parent=parent)
self.table = QTableWidget(self)
self.layout = QHBoxLayout(self)
self.layout.addWidget(self.table)
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status"])
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
self.reset_content()
@@ -38,8 +42,8 @@ class BECQueue(BECWidget, QTableWidget):
"""
# only show the primary queue for now
queue_info = content.get("queue", {}).get("primary", {}).get("info", [])
self.setRowCount(len(queue_info))
self.clearContents()
self.table.setRowCount(len(queue_info))
self.table.clearContents()
if not queue_info:
self.reset_content()
@@ -73,6 +77,8 @@ class BECQueue(BECWidget, QTableWidget):
Returns:
QTableWidgetItem: The formatted item.
"""
if not content or not isinstance(content, str):
content = ""
item = QTableWidgetItem(content)
item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
return item
@@ -88,16 +94,16 @@ class BECQueue(BECWidget, QTableWidget):
status (str): The status.
"""
self.setItem(index, 0, self.format_item(scan_number))
self.setItem(index, 1, self.format_item(scan_type))
self.setItem(index, 2, self.format_item(status))
self.table.setItem(index, 0, self.format_item(scan_number))
self.table.setItem(index, 1, self.format_item(scan_type))
self.table.setItem(index, 2, self.format_item(status))
def reset_content(self):
"""
Reset the content of the table.
"""
self.setRowCount(1)
self.table.setRowCount(1)
self.set_row(0, "", "", "")

View File

@@ -56,6 +56,11 @@ class BECServiceStatusMixin(QObject):
self.client._update_existing_services()
self.services_update.emit(self.client._services_info, self.client._services_metric)
def cleanup(self):
"""Cleanup the BECServiceStatusMixin."""
self._service_update_timer.stop()
self._service_update_timer.deleteLater()
class BECStatusBox(BECWidget, QWidget):
"""An autonomous widget to display the status of BEC services.
@@ -290,6 +295,11 @@ class BECStatusBox(BECWidget, QWidget):
if objects["item"] == item:
objects["widget"].show_popup()
def cleanup(self):
"""Cleanup the BECStatusBox widget."""
self.bec_service_status.cleanup()
return super().cleanup()
def main():
"""Main method to run the BECStatusBox widget."""

View File

@@ -18,10 +18,11 @@ import pyte
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import QSize, QSocketNotifier, Qt
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QClipboard, QTextCursor
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
ansi_colors = {
"black": "#000000",
"red": "#CD0000",
@@ -289,7 +290,7 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
old["value"] = value
self.dataReady(self.backend.screen, reset_scroll=False)
@pyqtSlot(object)
@Slot(object)
def keyPressEvent(self, event):
"""
Redirect all keystrokes to the terminal process.

View File

@@ -34,7 +34,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
):
super().__init__(client=client, config=config, gui_id=gui_id)
QComboBox.__init__(self, parent=parent)
self.setMinimumSize(125, 26)
self.populate_combobox()
if arg_name is not None:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, Optional
from pydantic import Field
from pyqtgraph.dockarea import Dock
from pyqtgraph.dockarea import Dock, DockLabel
from bec_widgets.cli.rpc_wigdet_handler import widget_handler
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
@@ -25,6 +25,64 @@ class DockConfig(ConnectionConfig):
)
class CustomDockLabel(DockLabel):
def updateStyle(self):
r = "3px"
if self.dim:
fg = "#aaa"
bg = "#44a"
border = "#339"
else:
fg = "#fff"
bg = "#3f4042"
border = "#3f4042"
if self.orientation == "vertical":
self.vStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: 0px;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: %s;
border-width: 0px;
border-right: 2px solid %s;
padding-top: 3px;
padding-bottom: 3px;
font-size: %s;
}""" % (
bg,
fg,
r,
r,
border,
self.fontSize,
)
self.setStyleSheet(self.vStyle)
else:
self.hStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: %s;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
border-width: 0px;
border-bottom: 2px solid %s;
padding-left: 3px;
padding-right: 3px;
font-size: %s;
}""" % (
bg,
fg,
r,
r,
border,
self.fontSize,
)
self.setStyleSheet(self.hStyle)
class BECDock(BECWidget, Dock):
USER_ACCESS = [
"_config_dict",
@@ -51,6 +109,7 @@ class BECDock(BECWidget, Dock):
name: str | None = None,
client=None,
gui_id: str | None = None,
closable: bool = True,
**kwargs,
) -> None:
if config is None:
@@ -62,7 +121,9 @@ class BECDock(BECWidget, Dock):
config = DockConfig(**config)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id)
Dock.__init__(self, name=name, **kwargs)
label = CustomDockLabel(text=name, closable=closable)
Dock.__init__(self, name=name, label=label, **kwargs)
# Dock.__init__(self, name=name, **kwargs)
self.parent_dock_area = parent_dock_area

View File

@@ -164,7 +164,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
def __getitem__(self, key: tuple | str):
if isinstance(key, tuple) and len(key) == 2:
return self.axes(*key)
elif isinstance(key, str):
if isinstance(key, str):
widget = self._widgets.get(key)
if widget is None:
raise KeyError(f"No widget with ID {key}")
@@ -185,7 +185,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
self.change_theme(self.config.theme)
# widget_config has to be reset for not have each widget config twice when added to the figure
widget_configs = [config for config in self.config.widgets.values()]
widget_configs = list(self.config.widgets.values())
self.config.widgets = {}
for widget_config in widget_configs:
getattr(self, self.widget_method_map[widget_config.widget_class])(
@@ -233,8 +233,8 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
"""Export the plot widget."""
try:
plot_item = self.widget_list[0]
except:
raise ValueError("No plot widget available to export.")
except Exception as exc:
raise ValueError("No plot widget available to export.") from exc
scene = plot_item.scene()
scene.contextMenuItem = plot_item
@@ -529,12 +529,12 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
if row is not None and col is not None:
if self.getItem(row, col):
raise ValueError(f"Position at row {row} and column {col} is already occupied.")
else:
widget.config.row = row
widget.config.col = col
# Add widget to the figure
self.addItem(widget, row=row, col=col)
widget.config.row = row
widget.config.col = col
# Add widget to the figure
self.addItem(widget, row=row, col=col)
else:
row, col = self._find_next_empty_position()
widget.config.row = row
@@ -721,7 +721,7 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
# Populate the new grid with widgets' IDs
current_idx = 0
for widget_id, widget in self._widgets.items():
for widget_id in self._widgets:
row = current_idx // len(new_grid[0])
col = current_idx % len(new_grid[0])
new_grid[row][col] = widget_id

View File

@@ -1,8 +1,8 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QVBoxLayout
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.qt_utils.settings_dialog import SettingWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO

View File

@@ -7,9 +7,9 @@ import numpy as np
from bec_lib.endpoints import MessageEndpoints
from pydantic import BaseModel, Field, ValidationError
from qtpy.QtCore import QThread
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import EntryValidator
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
from bec_widgets.widgets.figure.plots.image.image_processor import (
@@ -247,7 +247,7 @@ class BECImageShow(BECPlotBase):
Returns:
BECImageItem: The image item.
"""
image_source = "device_monitor"
image_source = "device_monitor_2d"
image_exits = self._check_image_id(monitor, self._images)
if image_exits:
@@ -287,7 +287,7 @@ class BECImageShow(BECPlotBase):
**kwargs,
):
image_source = "custom"
# image_source = "device_monitor"
# image_source = "device_monitor_2d"
image_exits = self._check_image_id(name, self._images)
if image_exits:
@@ -500,21 +500,22 @@ class BECImageShow(BECPlotBase):
self.update_image(device, data)
self.update_vrange(device, self.processor.config.stats)
@pyqtSlot(dict)
def on_image_update(self, msg: dict):
@Slot(dict, dict)
def on_image_update(self, msg: dict, metadata: dict):
"""
Update the image of the device monitor from bec.
Args:
msg(dict): The message from bec.
metadata(dict): The metadata of the message.
"""
data = msg["data"]
device = msg["device"]
image = self._images["device_monitor"][device]
image = self._images["device_monitor_2d"][device]
image.raw_data = data
self.process_image(device, image, data)
@pyqtSlot(str, np.ndarray)
@Slot(str, np.ndarray)
def update_image(self, device: str, data: np.ndarray):
"""
Update the image of the device monitor.
@@ -523,10 +524,10 @@ class BECImageShow(BECPlotBase):
device(str): The name of the device.
data(np.ndarray): The data to be updated.
"""
image_to_update = self._images["device_monitor"][device]
image_to_update = self._images["device_monitor_2d"][device]
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
@pyqtSlot(str, ImageStats)
@Slot(str, ImageStats)
def update_vrange(self, device: str, stats: ImageStats):
"""
Update the scaling of the image.
@@ -534,7 +535,7 @@ class BECImageShow(BECPlotBase):
Args:
stats(ImageStats): The statistics of the image.
"""
image_to_update = self._images["device_monitor"][device]
image_to_update = self._images["device_monitor_2d"][device]
if image_to_update.config.autorange:
image_to_update.auto_update_vrange(stats)
@@ -547,7 +548,7 @@ class BECImageShow(BECPlotBase):
data = image.raw_data
self.process_image(image_id, image, data)
def _connect_device_monitor(self, monitor: str):
def _connect_device_monitor_2d(self, monitor: str):
"""
Connect to the device monitor.
@@ -561,13 +562,13 @@ class BECImageShow(BECPlotBase):
previous_monitor = None
if previous_monitor and image_item.connected is True:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(previous_monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(previous_monitor)
)
image_item.connected = False
if monitor and image_item.connected is False:
self.entry_validator.validate_monitor(monitor)
self.bec_dispatcher.connect_slot(
self.on_image_update, MessageEndpoints.device_monitor(monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
)
image_item.set_monitor(monitor)
image_item.connected = True
@@ -581,8 +582,8 @@ class BECImageShow(BECPlotBase):
if self.single_image is True and len(self.images) > 0:
self.remove_image(0)
self._images[source][name] = image
if source == "device_monitor":
self._connect_device_monitor(config.monitor)
if source == "device_monitor_2d":
self._connect_device_monitor_2d(config.monitor)
self.config.images[name] = config
if data is not None:
image.setImage(data)
@@ -668,15 +669,15 @@ class BECImageShow(BECPlotBase):
image = self.find_image_by_monitor(image_id)
if image:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(image.config.monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(image.config.monitor)
)
def cleanup(self):
"""
Clean up the widget.
"""
for monitor in self._images["device_monitor"]:
for monitor in self._images["device_monitor_2d"]:
self.bec_dispatcher.disconnect_slot(
self.on_image_update, MessageEndpoints.device_monitor(monitor)
self.on_image_update, MessageEndpoints.device_monitor_2d(monitor)
)
self.images.clear()

View File

@@ -10,9 +10,9 @@ from pydantic import Field, ValidationError, field_validator
from pydantic_core import PydanticCustomError
from qtpy import QtCore, QtGui
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import Colors, EntryValidator
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
from bec_widgets.widgets.figure.plots.waveform.waveform import Signal, SignalData
@@ -444,7 +444,7 @@ class BECMotorMap(BECPlotBase):
return None
@Slot()
def _update_plot(self):
def _update_plot(self, _=None):
"""Update the motor map plot."""
# If the number of points exceeds max_points, delete the oldest points
if len(self.database_buffer["x"]) > self.config.max_points:
@@ -493,13 +493,14 @@ class BECMotorMap(BECPlotBase):
f"Motor position: ({round(float(current_x),precision)}, {round(float(current_y),precision)})"
)
@Slot(dict)
def on_device_readback(self, msg: dict) -> None:
@Slot(dict, dict)
def on_device_readback(self, msg: dict, metadata: dict) -> None:
"""
Update the motor map plot with the new motor position.
Args:
msg(dict): Message from the device readback.
metadata(dict): Metadata of the message.
"""
if self.motor_x is None or self.motor_y is None:
return

View File

@@ -11,9 +11,9 @@ from bec_lib.endpoints import MessageEndpoints
from pydantic import Field, ValidationError, field_validator
from pyqtgraph.exporters import MatplotlibExporter
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import Colors, EntryValidator
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
@@ -391,7 +391,7 @@ class BECWaveform(BECPlotBase):
self.async_signal_update.emit()
self.scan_signal_update.emit()
@pyqtSlot()
@Slot()
def auto_range(self):
self.plot_item.autoRange()
@@ -408,7 +408,7 @@ class BECWaveform(BECPlotBase):
"""
self.plot_item.enableAutoRange(axis, enabled)
@pyqtSlot()
@Slot()
def auto_range(self):
self.plot_item.autoRange()
@@ -642,7 +642,7 @@ class BECWaveform(BECPlotBase):
self.refresh_dap()
return curve
@pyqtSlot()
@Slot()
def get_dap_params(self) -> dict:
"""
Get the DAP parameters of all DAP curves.
@@ -655,7 +655,7 @@ class BECWaveform(BECPlotBase):
params[curve_id] = curve.dap_params
return params
@pyqtSlot()
@Slot()
def get_dap_summary(self) -> dict:
"""
Get the DAP summary of all DAP curves.
@@ -921,7 +921,7 @@ class BECWaveform(BECPlotBase):
else:
raise IndexError(f"Curve order {N} out of range.")
@pyqtSlot(dict)
@Slot(dict)
def on_scan_status(self, msg):
"""
Handle the scan status message.
@@ -945,7 +945,7 @@ class BECWaveform(BECPlotBase):
for curve_id, curve in self._curves_data["async"].items():
self.setup_async(curve.config.signals.y.name)
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def on_scan_segment(self, msg: dict, metadata: dict):
"""
Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
@@ -1004,7 +1004,7 @@ class BECWaveform(BECPlotBase):
self.update_dap, MessageEndpoints.dap_response(f"{new_scan_id}-{self.gui_id}")
)
@pyqtSlot(str)
@Slot(str)
def setup_async(self, device: str):
self.bec_dispatcher.disconnect_slot(
self.on_async_readback, MessageEndpoints.device_async_readback(self.old_scan_id, device)
@@ -1020,8 +1020,8 @@ class BECWaveform(BECPlotBase):
from_start=True,
)
@pyqtSlot()
def refresh_dap(self):
@Slot()
def refresh_dap(self, _=None):
"""
Refresh the DAP curves with the latest data from the DAP model MessageEndpoints.dap_response().
"""
@@ -1069,7 +1069,7 @@ class BECWaveform(BECPlotBase):
)
self.client.connector.set_and_publish(MessageEndpoints.dap_request(), msg)
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def update_dap(self, msg, metadata):
self.msg = msg
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
@@ -1089,7 +1089,7 @@ class BECWaveform(BECPlotBase):
self.dap_summary_update.emit(curve.dap_summary)
break
@pyqtSlot(dict, dict)
@Slot(dict, dict)
def on_async_readback(self, msg, metadata):
"""
Get async data readback.
@@ -1127,7 +1127,7 @@ class BECWaveform(BECPlotBase):
else:
curve.setData(data_plot)
@pyqtSlot()
@Slot()
def replot_async_curve(self):
try:
data = self.scan_item.async_data
@@ -1152,8 +1152,8 @@ class BECWaveform(BECPlotBase):
else:
curve.setData(data_x, data_y)
@pyqtSlot()
def _update_scan_curves(self):
@Slot()
def _update_scan_curves(self, _=None):
"""
Update the scan curves with the data from the scan segment.
"""

View File

@@ -1,8 +1,8 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QVBoxLayout
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.qt_utils.settings_dialog import SettingWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO

View File

@@ -55,7 +55,7 @@ class SpinnerWidget(QWidget):
color_palette = get_theme_palette()
color = QColor(color_palette.COLOR_ACCENT_4)
color = QColor(color_palette.accent().color())
rect.adjust(line_width, line_width, -line_width, -line_width)
@@ -75,6 +75,10 @@ class SpinnerWidget(QWidget):
painter.drawArc(adjusted_rect, self.angle * 16, int(angle_span))
painter.end()
def closeEvent(self, event):
self.timer.stop()
super().closeEvent(event)
if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)

View File

@@ -1,6 +1,6 @@
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QPushButton
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils.bec_widget import BECWidget

View File

@@ -1,8 +1,8 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QTreeWidgetItem, QVBoxLayout
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import UILoader

View File

@@ -6,7 +6,6 @@ pydata-sphinx-theme
sphinx-copybutton
myst-parser
sphinx-design
PyQt6
PyQt6-WebEngine
PySide6
bec-widgets
tomli

View File

@@ -11,4 +11,5 @@ hidden: true
installation/
quick_start/
auto_updates/
video_tutorials/
```

View File

@@ -0,0 +1,17 @@
(user.video_tutorials)=
# Video Tutorials
This section includes video tutorials that demonstrate various use cases of `bec-widgets`, including video tutorials,
presentations, and conference talks.
## BSEG Meeting 24th July 2024
This video is a presentation of the BEC Widgets project at the BSEG meeting on the 24th July 2024. The presentation
covers the basic interactions, including visualization of live data acquisition and how to steer experiments using
user-friendly tools. Learn how BEC Widgets can enhance your experimental control projects with its intuitive interface
and powerful features.
Used version of BEC Widgets: 0.91.0
<iframe width="560" height="315" src="https://www.youtube.com/embed/qZ8fWXRAdHE" frameborder="0" allowfullscreen></iframe>

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "0.91.0"
version = "0.93.0"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [
@@ -19,7 +19,7 @@ dependencies = [
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
"pydantic~=2.0",
"pyqtgraph~=0.13",
"qdarkstyle>=3.2.2",
"bec_qthemes~=0.0",
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
"qtpy~=2.4",
"pyte", # needed for vt100 console

View File

@@ -98,7 +98,7 @@ def test_rpc_add_dock_with_figure_e2e(bec_client_lib, rpc_server_dock):
assert plt_data["bpm4i-bpm4i"]["y"] == plt_last_scan_data["bpm4i"]["bpm4i"].val
# image
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor_2d("eiger"))[
"data"
].data
time.sleep(0.5)

View File

@@ -120,7 +120,7 @@ def test_rpc_image(rpc_server_figure, bec_client_lib):
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
status.wait()
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor_2d("eiger"))[
"data"
].data
last_image_plot = im.images[0].get_data()

View File

@@ -12,6 +12,7 @@ def scan_control(qtbot, bec_client_lib): # , mock_dev):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_scan_control_populate_scans_e2e(scan_control):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,9 +1,16 @@
import pytest
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.qt_utils import error_popups
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
@pytest.fixture(autouse=True)
def qapplication(qapp): # pylint: disable=unused-argument
yield
qapp.processEvents() # make sure all events are processed before shutting down
@pytest.fixture(autouse=True)
def rpc_register():
yield RPCRegister()
@@ -11,7 +18,7 @@ def rpc_register():
@pytest.fixture(autouse=True)
def bec_dispatcher(threads_check):
def bec_dispatcher(threads_check): # pylint: disable=unused-argument
bec_dispatcher = bec_dispatcher_module.BECDispatcher()
yield bec_dispatcher
bec_dispatcher.disconnect_all()
@@ -19,3 +26,8 @@ def bec_dispatcher(threads_check):
bec_dispatcher.client.shutdown()
# reinitialize singleton for next test
bec_dispatcher_module.BECDispatcher.reset_singleton()
@pytest.fixture(autouse=True)
def clean_singleton():
error_popups._popup_utility_instance = None

View File

@@ -2,9 +2,9 @@
import time
import pytest
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QApplication
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import BECConnector, ConnectionConfig
from .client_mocks import mocked_client

View File

@@ -17,10 +17,8 @@ def bec_image_show(bec_figure):
def test_on_image_update(bec_image_show):
data = np.random.rand(100, 100)
msg = messages.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
bec_image_show.on_image_update(msg)
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
@@ -28,10 +26,8 @@ def test_on_image_update(bec_image_show):
def test_autorange_on_image_update(bec_image_show):
# Check if autorange mode "mean" works, should be default
data = np.random.rand(100, 100)
msg = messages.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
bec_image_show.on_image_update(msg)
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
vmin = max(np.mean(data) - 2 * np.std(data), 0)
@@ -39,7 +35,7 @@ def test_autorange_on_image_update(bec_image_show):
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
# Test general update with autorange True, mode "max"
bec_image_show.set_autorange_mode("max")
bec_image_show.on_image_update(msg)
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
vmin = np.min(data)
vmax = np.max(data)
@@ -47,18 +43,16 @@ def test_autorange_on_image_update(bec_image_show):
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-5, 1e-5)).all()
# Change the input data, and switch to autorange False, colormap levels should stay untouched
data *= 100
msg = messages.DeviceMonitorMessage(
device="eiger", data=data, metadata={"scan_id": "12345"}
).model_dump()
msg = messages.DeviceMonitor2DMessage(device="eiger", data=data, metadata={"scan_id": "12345"})
bec_image_show.set_autorange(False)
bec_image_show.on_image_update(msg)
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
assert np.array_equal(img.get_data(), data)
assert np.isclose(img.color_bar.getLevels(), (vmin, vmax), rtol=(1e-3, 1e-3)).all()
# Reactivate autorange, should now scale the new data
bec_image_show.set_autorange(True)
bec_image_show.set_autorange_mode("mean")
bec_image_show.on_image_update(msg)
bec_image_show.on_image_update(msg.content, msg.metadata)
img = bec_image_show.images[0]
vmin = max(np.mean(data) - 2 * np.std(data), 0)
vmax = np.mean(data) + 2 * np.std(data)

View File

@@ -1,5 +1,6 @@
import numpy as np
import pytest
from bec_lib.messages import DeviceMessage
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap, MotorMapConfig
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import SignalData
@@ -72,7 +73,8 @@ def test_motor_movement_updates_position_and_database(bec_figure):
# Simulate motor movement for 'samx' only
new_position_samx = 4.0
mm.on_device_readback({"signals": {"samx": {"value": new_position_samx}}})
msg = DeviceMessage(signals={"samx": {"value": new_position_samx}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
init_positions["samx"].append(new_position_samx)
init_positions["samy"].append(init_positions["samy"][-1])
@@ -96,7 +98,8 @@ def test_scatter_plot_rendering(bec_figure):
# Simulate motor movement for 'samx' only
new_position_samx = 4.0
mm.on_device_readback({"signals": {"samx": {"value": new_position_samx}}})
msg = DeviceMessage(signals={"samx": {"value": new_position_samx}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
mm._update_plot()
# Get the scatter plot item
@@ -116,8 +119,10 @@ def test_plot_visualization_consistency(bec_figure):
mm = bec_figure.motor_map("samx", "samy")
mm.change_motors("samx", "samy")
# Simulate updating the plot with new data
mm.on_device_readback({"signals": {"samx": {"value": 5}}})
mm.on_device_readback({"signals": {"samy": {"value": 9}}})
msg = DeviceMessage(signals={"samx": {"value": 5}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samy": {"value": 9}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
mm._update_plot()
scatter_plot_item = mm.plot_components["scatter"]
@@ -234,10 +239,14 @@ def test_motor_map_get_data_max_points(bec_figure, qtbot):
"samx": [motor_map_dev["samx"].read()["samx"]["value"]],
"samy": [motor_map_dev["samy"].read()["samy"]["value"]],
}
mm.on_device_readback({"signals": {"samx": {"value": 5.0}}})
mm.on_device_readback({"signals": {"samy": {"value": 9.0}}})
mm.on_device_readback({"signals": {"samx": {"value": 6.0}}})
mm.on_device_readback({"signals": {"samy": {"value": 7.0}}})
msg = DeviceMessage(signals={"samx": {"value": 5.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samy": {"value": 9.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samx": {"value": 6.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
msg = DeviceMessage(signals={"samy": {"value": 7.0}}, metadata={})
mm.on_device_readback(msg.content, msg.metadata)
expected_x = [init_positions["samx"][-1], 5.0, 5.0, 6.0, 6.0]
expected_y = [init_positions["samy"][-1], init_positions["samy"][-1], 9.0, 9.0, 7.0]

View File

@@ -93,19 +93,20 @@ def bec_queue(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_bec_queue(bec_queue, bec_queue_msg_full):
bec_queue.update_queue(bec_queue_msg_full.content, {})
assert bec_queue.rowCount() == 1
assert bec_queue.item(0, 0).text() == "1289"
assert bec_queue.item(0, 1).text() == "line_scan"
assert bec_queue.item(0, 2).text() == "COMPLETED"
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == "1289"
assert bec_queue.table.item(0, 1).text() == "line_scan"
assert bec_queue.table.item(0, 2).text() == "COMPLETED"
def test_bec_queue_empty(bec_queue):
bec_queue.update_queue({}, {})
assert bec_queue.rowCount() == 1
assert bec_queue.item(0, 0).text() == ""
assert bec_queue.item(0, 1).text() == ""
assert bec_queue.item(0, 2).text() == ""
assert bec_queue.table.rowCount() == 1
assert bec_queue.table.item(0, 0).text() == ""
assert bec_queue.table.item(0, 1).text() == ""
assert bec_queue.table.item(0, 2).text() == ""

View File

@@ -20,6 +20,7 @@ def status_box(qtbot, mocked_client, service_status_fixture):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_update_top_item(status_box):

View File

@@ -8,13 +8,18 @@ from .client_mocks import mocked_client
# DeviceInputBase is meant to be mixed in a QWidget
class DeviceInputWidget(DeviceInputBase, QWidget):
pass
def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(client=client, config=config, gui_id=gui_id)
QWidget.__init__(self, parent=parent)
@pytest.fixture
def device_input_base(mocked_client):
def device_input_base(qtbot, mocked_client):
widget = DeviceInputWidget(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_device_input_base_init(device_input_base):

View File

@@ -220,6 +220,7 @@ def scan_control(qtbot, mocked_client): # , mock_dev):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_populate_scans(scan_control, mocked_client):

View File

@@ -46,23 +46,23 @@ def test_setting_widget_display_current_settings(setting_widget):
# SettingsDialog tests
###################################
@pytest.fixture
def settings_dialog(qtbot):
def settings_dialog(qtbot, setting_widget):
parent_widget = QWidget()
settings_widget = SettingWidget()
settings_widget.set_target_widget = MagicMock()
settings_widget.display_current_settings = MagicMock()
settings_widget.accept_changes = MagicMock()
setting_widget.set_target_widget = MagicMock()
setting_widget.display_current_settings = MagicMock()
setting_widget.accept_changes = MagicMock()
dialog = SettingsDialog(
parent=parent_widget,
settings_widget=settings_widget,
settings_widget=setting_widget,
window_title="Test Settings",
config={"setting1": "value1", "setting2": "value2"},
)
qtbot.addWidget(dialog)
qtbot.waitExposed(dialog)
yield dialog, parent_widget, settings_widget
yield dialog, parent_widget, setting_widget
dialog.close()
parent_widget.close()
def test_settings_dialog_initialization(settings_dialog):

View File

@@ -12,6 +12,7 @@ def spinner_widget(qtbot):
qtbot.addWidget(spinner)
qtbot.waitExposed(spinner)
yield spinner
spinner.close()
def test_spinner_widget_paint_event(spinner_widget, qtbot):

View File

@@ -10,6 +10,7 @@ def toggle(qtbot):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
def test_toggle(toggle):

View File

@@ -14,7 +14,9 @@ from .client_mocks import mocked_client
def vscode_widget(qtbot, mocked_client):
with mock.patch("bec_widgets.widgets.vscode.vscode.subprocess.Popen") as mock_popen:
widget = VSCodeEditor(client=mocked_client)
# qtbot.addWidget(widget)
yield widget
# widget.close()
def test_vscode_widget(qtbot, vscode_widget):