mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-08 09:47:52 +02:00
Compare commits
3 Commits
v1.18.0
...
tomcat/pro
| Author | SHA1 | Date | |
|---|---|---|---|
| f343a692ff | |||
| e1576f41b0 | |||
| 98f86739e3 |
@@ -78,9 +78,9 @@ formatter:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
script:
|
||||
- pip install bec_lib[dev]
|
||||
- isort --check --diff --line-length=100 --profile=black --multi-line=3 --trailing-comma ./
|
||||
- black --check --diff --color --line-length=100 --skip-magic-trailing-comma ./
|
||||
- pip install black isort
|
||||
- isort --check --diff ./
|
||||
- black --check --diff --color ./
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
|
||||
|
||||
@@ -148,7 +148,7 @@ tests:
|
||||
- *clone-repos
|
||||
- *install-os-packages
|
||||
- *install-repos
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --maxfail=2 --random-order --full-trace ./tests/unit_tests
|
||||
- coverage report
|
||||
- coverage xml
|
||||
@@ -172,6 +172,7 @@ test-matrix:
|
||||
- "3.12"
|
||||
QT_PCKG:
|
||||
- "pyside6"
|
||||
- "pyqt6"
|
||||
|
||||
stage: AdditionalTests
|
||||
needs: []
|
||||
@@ -210,7 +211,7 @@ end-2-end-conda:
|
||||
- cd ../
|
||||
- pip install -e ./ophyd_devices
|
||||
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- cd ./tests/end-2-end
|
||||
- pytest -v --start-servers --flush-redis --random-order
|
||||
|
||||
|
||||
5890
CHANGELOG.md
5890
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
34
README.md
34
README.md
@@ -1,17 +1,12 @@
|
||||
# BEC Widgets
|
||||
|
||||
**⚠️ Important Notice:**
|
||||
|
||||
🚨 **PyQt6 is no longer supported** due to incompatibilities with Qt Designer. Please use **PySide6** instead. 🚨
|
||||
|
||||
BEC Widgets is a GUI framework designed for interaction with [BEC (Beamline Experiment Control)](https://gitlab.psi.ch/bec/bec).
|
||||
|
||||
## Installation
|
||||
|
||||
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install BEC Widgets:
|
||||
|
||||
```bash
|
||||
pip install bec_widgets[pyside6]
|
||||
pip install bec_widgets PyQt6
|
||||
```
|
||||
|
||||
For development purposes, you can clone the repository and install the package locally in editable mode:
|
||||
@@ -19,12 +14,22 @@ For development purposes, you can clone the repository and install the package l
|
||||
```bash
|
||||
git clone https://gitlab.psi.ch/bec/bec-widgets
|
||||
cd bec_widgets
|
||||
pip install -e .[dev,pyside6]
|
||||
pip install -e .[dev,pyqt6]
|
||||
```
|
||||
|
||||
BEC Widgets now **only supports PySide6**. Users must manually install PySide6 as no default Qt distribution is
|
||||
specified.
|
||||
BEC Widgets currently supports both Pyside6 and PyQt6, however, no default distribution is specified. As a result, users must install one of the supported
|
||||
Python Qt distributions manually.
|
||||
|
||||
To select a specific Python Qt distribution, install the package with an additional tag:
|
||||
|
||||
```bash
|
||||
pip install bec_widgets[pyqt6]
|
||||
```
|
||||
or
|
||||
|
||||
```bash
|
||||
pip install bec_widgets[pyside6]
|
||||
```
|
||||
## Documentation
|
||||
|
||||
Documentation of BEC Widgets can be found [here](https://bec-widgets.readthedocs.io/en/latest/). The documentation of the BEC can be found [here](https://bec.readthedocs.io/en/latest/).
|
||||
@@ -34,7 +39,7 @@ Documentation of BEC Widgets can be found [here](https://bec-widgets.readthedocs
|
||||
All commits should use the Angular commit scheme:
|
||||
|
||||
> #### <a name="commit-header"></a>Angular Commit Message Header
|
||||
>
|
||||
>
|
||||
> ```
|
||||
> <type>(<scope>): <short summary>
|
||||
> │ │ │
|
||||
@@ -48,13 +53,13 @@ All commits should use the Angular commit scheme:
|
||||
> │
|
||||
> └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
|
||||
> ```
|
||||
>
|
||||
>
|
||||
> The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
||||
|
||||
> ##### Type
|
||||
>
|
||||
>
|
||||
> Must be one of the following:
|
||||
>
|
||||
>
|
||||
> * **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
> * **ci**: Changes to our CI configuration files and scripts (examples: CircleCi, SauceLabs)
|
||||
> * **docs**: Documentation only changes
|
||||
@@ -66,5 +71,4 @@ All commits should use the Angular commit scheme:
|
||||
|
||||
## License
|
||||
|
||||
[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/)
|
||||
|
||||
[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/)
|
||||
100
bec_widgets/applications/tomcat_app/tomcat_app.py
Normal file
100
bec_widgets/applications/tomcat_app/tomcat_app.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import requests
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QActionGroup, QIcon
|
||||
from qtpy.QtWidgets import QApplication, QMainWindow, QStyle
|
||||
|
||||
import bec_widgets
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from bec_widgets.examples.general_app.web_links import BECWebLinksMixin
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
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 TomcatApp(QMainWindow, BECWidget):
|
||||
select_slice = Signal()
|
||||
|
||||
def __init__(self, parent=None, client=None, gui_id=None):
|
||||
super(TomcatApp, self).__init__(parent)
|
||||
BECWidget.__init__(self, client=client, gui_id=gui_id)
|
||||
ui_file_path = os.path.join(os.path.dirname(__file__), "tomcat_app.ui")
|
||||
self.load_ui(ui_file_path)
|
||||
|
||||
self.resize(1280, 720)
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
self.bec_dispatcher.connect_slot(self.test_connection, "GPU Fastapi message")
|
||||
self.bec_dispatcher.connect_slot(self.status_update, "GPU Fastapi message")
|
||||
|
||||
self.ui.slider_select.valueChanged.connect(self.select_slice_from_slider)
|
||||
self.proxy_slider = pg.SignalProxy(self.select_slice, rateLimit=2, slot=self.send_slice)
|
||||
|
||||
self.image_widget = self.ui.image_widget
|
||||
|
||||
def load_ui(self, ui_file):
|
||||
loader = UILoader(self)
|
||||
self.ui = loader.loader(ui_file)
|
||||
self.setCentralWidget(self.ui)
|
||||
|
||||
def status_update(self, msg):
|
||||
status = msg["data"]["GPU SVC Status"]
|
||||
|
||||
if status == "Running":
|
||||
self.ui.radio_io.setChecked(True)
|
||||
else:
|
||||
self.ui.radio_io.setChecked(False)
|
||||
|
||||
# @SafeSlot(dict, dict)
|
||||
def test_connection(self, msg):
|
||||
print("Test Connection")
|
||||
print(msg)
|
||||
# print(metadata)
|
||||
|
||||
def select_slice_from_slider(self, value):
|
||||
print(value)
|
||||
self.select_slice.emit()
|
||||
|
||||
@Slot()
|
||||
def send_slice(self):
|
||||
value = self.ui.slider_select.value()
|
||||
requests.post(
|
||||
"http://ra-gpu-006:8000/api/v1/reco/single_slice",
|
||||
json={"slice": value, "rot_center": 0},
|
||||
)
|
||||
print(f"Sending slice {value}")
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "BEC-General-App.png"), size=QSize(48, 48)
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
config = ServiceConfig(redis={"host": "ra-gpu-006", "port": 6379})
|
||||
|
||||
client = BECClient(config=config, connector_cls=QtRedisConnector)
|
||||
|
||||
main_window = TomcatApp(client=client)
|
||||
main_window.show()
|
||||
main_window.image_widget.image("RecoPreview")
|
||||
# custom_data = np.random.rand(100, 100)
|
||||
# main_window.image_widget._image.add_custom_image("custom", custom_data)
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
278
bec_widgets/applications/tomcat_app/tomcat_app.ui
Normal file
278
bec_widgets/applications/tomcat_app/tomcat_app.ui
Normal file
@@ -0,0 +1,278 @@
|
||||
<?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>800</width>
|
||||
<height>631</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="BECImageWidget" name="image_widget"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>29</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QDockWidget" name="status_dock">
|
||||
<property name="windowTitle">
|
||||
<string>Status</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>1</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Dataset loaded</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QRadioButton" name="radio_dataset">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>I/O Service</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QRadioButton" name="radio_io">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLineEdit" name="lineEdit_2"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>Reco Service</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QRadioButton" name="radioButton_4">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>reco-adress</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Streaming Service</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QRadioButton" name="radioButton_3">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="text">
|
||||
<string>stream-adress</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="dock_data">
|
||||
<property name="windowTitle">
|
||||
<string>Dataset</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>1</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Input Dataset (full HDF5 path)</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="line_edit_dataset"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_load">
|
||||
<property name="text">
|
||||
<string>Load Dataset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_close">
|
||||
<property name="text">
|
||||
<string>Close Dataset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Single Slice Live Preview</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="ToggleSwitch" name="toggle_preview"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Select Slice</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="slider_select">
|
||||
<property name="maximum">
|
||||
<number>500</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Current Index</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Rotation center (offset from center)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="slider_rotation">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Rotation Index</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_reconstruct">
|
||||
<property name="text">
|
||||
<string>Reconstruct</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_single">
|
||||
<property name="text">
|
||||
<string>Single Slice</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="button_async">
|
||||
<property name="text">
|
||||
<string>Sample Async task</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>BECImageWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_image_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>slider_select</sender>
|
||||
<signal>valueChanged(int)</signal>
|
||||
<receiver>label_4</receiver>
|
||||
<slot>setNum(int)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>61</x>
|
||||
<y>396</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>73</x>
|
||||
<y>425</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -31,10 +31,8 @@ class Widgets(str, enum.Enum):
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
LMFitDialog = "LMFitDialog"
|
||||
Minesweeper = "Minesweeper"
|
||||
PositionIndicator = "PositionIndicator"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerBox2D = "PositionerBox2D"
|
||||
PositionerControlLine = "PositionerControlLine"
|
||||
ResetButton = "ResetButton"
|
||||
ResumeButton = "ResumeButton"
|
||||
@@ -3183,9 +3181,6 @@ class LMFitDialog(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
@@ -3236,51 +3231,6 @@ class PositionerBox(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox2D(RPCBase):
|
||||
@rpc_call
|
||||
def set_positioner_hor(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner_ver(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBoxBase(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
|
||||
class PositionerControlLine(RPCBase):
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
|
||||
@@ -43,21 +43,14 @@ from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
||||
|
||||
def generate_client(self, class_container: BECClassContainer):
|
||||
"""
|
||||
Generate the client for the published classes, skipping any classes
|
||||
that have `RPC = False`.
|
||||
Generate the client for the published classes.
|
||||
|
||||
Args:
|
||||
class_container: The class container with the classes to generate the client for.
|
||||
"""
|
||||
# Filter out classes that explicitly have RPC=False
|
||||
rpc_top_level_classes = [
|
||||
cls for cls in class_container.rpc_top_level_classes if getattr(cls, "RPC", True)
|
||||
]
|
||||
rpc_top_level_classes = class_container.rpc_top_level_classes
|
||||
rpc_top_level_classes.sort(key=lambda x: x.__name__)
|
||||
|
||||
connector_classes = [
|
||||
cls for cls in class_container.connector_classes if getattr(cls, "RPC", True)
|
||||
]
|
||||
connector_classes = class_container.connector_classes
|
||||
connector_classes.sort(key=lambda x: x.__name__)
|
||||
|
||||
self.write_client_enum(rpc_top_level_classes)
|
||||
@@ -88,13 +81,13 @@ class Widgets(str, enum.Enum):
|
||||
|
||||
class_name = cls.__name__
|
||||
|
||||
if class_name == "BECDockArea":
|
||||
# Generate the content
|
||||
if cls.__name__ == "BECDockArea":
|
||||
self.content += f"""
|
||||
class {class_name}(RPCBase):"""
|
||||
else:
|
||||
self.content += f"""
|
||||
class {class_name}(RPCBase):"""
|
||||
|
||||
if not cls.USER_ACCESS:
|
||||
self.content += """...
|
||||
"""
|
||||
@@ -107,10 +100,8 @@ class {class_name}(RPCBase):"""
|
||||
method = method.split(".setter")[0]
|
||||
if obj is None:
|
||||
raise AttributeError(
|
||||
f"Method {method} not found in class {cls.__name__}. "
|
||||
f"Please check the USER_ACCESS list."
|
||||
f"Method {method} not found in class {cls.__name__}. Please check the USER_ACCESS list."
|
||||
)
|
||||
|
||||
if isinstance(obj, (property, QtProperty)):
|
||||
# for the cli, we can map qt properties to regular properties
|
||||
if is_property_setter:
|
||||
|
||||
@@ -218,11 +218,15 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
bec_logger.level = bec_logger.LOGLEVEL.INFO
|
||||
if args.hide:
|
||||
# if we start hidden, it means we are under control of the client
|
||||
# -> set the log level to critical to not see all the messages
|
||||
# pylint: disable=protected-access
|
||||
bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR
|
||||
bec_logger._update_sinks()
|
||||
# bec_logger._stderr_log_level = bec_logger.LOGLEVEL.CRITICAL
|
||||
bec_logger.level = bec_logger.LOGLEVEL.CRITICAL
|
||||
else:
|
||||
# verbose log
|
||||
bec_logger.level = bec_logger.LOGLEVEL.DEBUG
|
||||
|
||||
if args.gui_class == "BECDockArea":
|
||||
gui_class = BECDockArea
|
||||
|
||||
@@ -20,7 +20,6 @@ from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.widgets.containers.figure import BECFigure
|
||||
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
||||
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||
|
||||
|
||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
@@ -63,8 +62,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"btn4": self.btn4,
|
||||
"btn5": self.btn5,
|
||||
"btn6": self.btn6,
|
||||
"pb": self.pb,
|
||||
"pi": self.pi,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -95,15 +92,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
third_tab_layout.addWidget(self.lm)
|
||||
tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||
|
||||
fourth_tab = QWidget()
|
||||
fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||
self.pb = PlotBase()
|
||||
self.pi = self.pb.plot_item
|
||||
fourth_tab_layout.addWidget(self.pb)
|
||||
tab_widget.addTab(fourth_tab, "PltoBase")
|
||||
|
||||
tab_widget.setCurrentIndex(3)
|
||||
|
||||
group_box = QGroupBox("Jupyter Console", splitter)
|
||||
group_box_layout = QVBoxLayout(group_box)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
|
||||
@@ -2,90 +2,42 @@ import functools
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, QObject, Qt, Signal, Slot
|
||||
from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None, **prop_kwargs):
|
||||
def SafeProperty(prop_type, *prop_args, popup_error: bool = False, **prop_kwargs):
|
||||
"""
|
||||
Decorator to create a Qt Property with safe getter and setter so that
|
||||
Qt Designer won't crash if an exception occurs in either method.
|
||||
Decorator to create a Qt Property with a safe setter that won't crash Designer on errors.
|
||||
Behaves similarly to SafeSlot, but for properties.
|
||||
|
||||
Args:
|
||||
prop_type: The property type (e.g., str, bool, int, custom classes, etc.)
|
||||
popup_error (bool): If True, show a popup for any error; otherwise, ignore or log silently.
|
||||
default: Any default/fallback value to return if the getter raises an exception.
|
||||
*prop_args, **prop_kwargs: Passed along to the underlying Qt Property constructor.
|
||||
|
||||
Usage:
|
||||
@SafeProperty(int, default=-1)
|
||||
def some_value(self) -> int:
|
||||
# your getter logic
|
||||
return ... # if an exception is raised, returns -1
|
||||
|
||||
@some_value.setter
|
||||
def some_value(self, val: int):
|
||||
# your setter logic
|
||||
...
|
||||
prop_type: The property type (e.g., str, bool, "QStringList", etc.)
|
||||
popup_error (bool): If True, show popup on error, otherwise just handle it silently.
|
||||
*prop_args, **prop_kwargs: Additional arguments and keyword arguments accepted by Property.
|
||||
"""
|
||||
|
||||
def decorator(py_getter):
|
||||
"""Decorator for the user's property getter function."""
|
||||
|
||||
@functools.wraps(py_getter)
|
||||
def safe_getter(self_):
|
||||
try:
|
||||
return py_getter(self_)
|
||||
except Exception:
|
||||
# Identify which property function triggered error
|
||||
prop_name = f"{py_getter.__module__}.{py_getter.__qualname__}"
|
||||
error_msg = traceback.format_exc()
|
||||
|
||||
if popup_error:
|
||||
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=True)
|
||||
logger.error(f"SafeProperty error in GETTER of '{prop_name}':\n{error_msg}")
|
||||
return default
|
||||
|
||||
def decorator(getter):
|
||||
class PropertyWrapper:
|
||||
"""
|
||||
Intermediate wrapper used so that the user can optionally chain .setter(...).
|
||||
"""
|
||||
|
||||
def __init__(self, getter_func):
|
||||
# We store only our safe_getter in the wrapper
|
||||
self.getter_func = safe_getter
|
||||
self.getter_func = getter_func
|
||||
|
||||
def setter(self, setter_func):
|
||||
"""Wraps the user-defined setter to handle errors safely."""
|
||||
|
||||
@functools.wraps(setter_func)
|
||||
def safe_setter(self_, value):
|
||||
try:
|
||||
return setter_func(self_, value)
|
||||
except Exception:
|
||||
prop_name = f"{setter_func.__module__}.{setter_func.__qualname__}"
|
||||
error_msg = traceback.format_exc()
|
||||
|
||||
if popup_error:
|
||||
ErrorPopupUtility().custom_exception_hook(
|
||||
*sys.exc_info(), popup_error=True
|
||||
)
|
||||
logger.error(f"SafeProperty error in SETTER of '{prop_name}':\n{error_msg}")
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
# Return the full read/write Property
|
||||
return Property(prop_type, self.getter_func, safe_setter, *prop_args, **prop_kwargs)
|
||||
|
||||
def __call__(self):
|
||||
"""
|
||||
If user never calls `.setter(...)`, produce a read-only property.
|
||||
"""
|
||||
return Property(prop_type, self.getter_func, None, *prop_args, **prop_kwargs)
|
||||
|
||||
return PropertyWrapper(py_getter)
|
||||
return PropertyWrapper(getter)
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -106,13 +58,7 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
||||
try:
|
||||
return method(*args, **kwargs)
|
||||
except Exception:
|
||||
slot_name = f"{method.__module__}.{method.__qualname__}"
|
||||
error_msg = traceback.format_exc()
|
||||
if popup_error:
|
||||
ErrorPopupUtility().custom_exception_hook(
|
||||
*sys.exc_info(), popup_error=popup_error
|
||||
)
|
||||
logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
|
||||
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=popup_error)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ class RoundedFrame(BECWidget, QFrame):
|
||||
self._radius = radius
|
||||
|
||||
# Apply rounded frame styling
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("roundedFrame")
|
||||
self.update_style()
|
||||
|
||||
@@ -114,12 +113,10 @@ class RoundedFrame(BECWidget, QFrame):
|
||||
|
||||
# Apply axis label and tick colors
|
||||
plot_item = self.content_widget.getPlotItem()
|
||||
for axis in ["left", "right", "top", "bottom"]:
|
||||
plot_item.getAxis(axis).setPen(pg.mkPen(color=axis_color))
|
||||
plot_item.getAxis(axis).setTextPen(pg.mkPen(color=label_color))
|
||||
|
||||
# Change title color
|
||||
plot_item.titleLabel.setText(plot_item.titleLabel.text, color=label_color)
|
||||
plot_item.getAxis("left").setPen(pg.mkPen(color=axis_color))
|
||||
plot_item.getAxis("bottom").setPen(pg.mkPen(color=axis_color))
|
||||
plot_item.getAxis("left").setTextPen(pg.mkPen(color=label_color))
|
||||
plot_item.getAxis("bottom").setTextPen(pg.mkPen(color=label_color))
|
||||
|
||||
# Apply border style via stylesheet
|
||||
self.content_widget.setStyleSheet(
|
||||
|
||||
@@ -5,18 +5,18 @@ from qtpy.QtCore import Property, QEasingCurve, QPropertyAnimation
|
||||
from qtpy.QtGui import QAction
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QStackedWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar
|
||||
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
||||
|
||||
|
||||
class SidePanel(QWidget):
|
||||
@@ -34,13 +34,11 @@ class SidePanel(QWidget):
|
||||
):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("SidePanel")
|
||||
|
||||
self._orientation = orientation
|
||||
self._panel_max_width = panel_max_width
|
||||
self._animation_duration = animation_duration
|
||||
self._animations_enabled = animations_enabled
|
||||
self._orientation = orientation
|
||||
|
||||
self._panel_width = 0
|
||||
self._panel_height = 0
|
||||
@@ -70,7 +68,6 @@ class SidePanel(QWidget):
|
||||
self.stack_widget = QStackedWidget()
|
||||
self.stack_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||
self.stack_widget.setMinimumWidth(5)
|
||||
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
||||
|
||||
if self._orientation == "left":
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
@@ -80,10 +77,7 @@ class SidePanel(QWidget):
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
|
||||
self.container.layout.addWidget(self.stack_widget)
|
||||
|
||||
self.menu_anim = QPropertyAnimation(self, b"panel_width")
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||
self.panel_width = 0 # start hidden
|
||||
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
||||
|
||||
else:
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
@@ -100,7 +94,6 @@ class SidePanel(QWidget):
|
||||
self.stack_widget = QStackedWidget()
|
||||
self.stack_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.stack_widget.setMinimumHeight(5)
|
||||
self.stack_widget.setMaximumHeight(self._panel_max_width)
|
||||
|
||||
if self._orientation == "top":
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
@@ -110,46 +103,74 @@ class SidePanel(QWidget):
|
||||
self.main_layout.addWidget(self.toolbar)
|
||||
|
||||
self.container.layout.addWidget(self.stack_widget)
|
||||
self.stack_widget.setMaximumHeight(self._panel_max_width)
|
||||
|
||||
if self._orientation in ("left", "right"):
|
||||
self.menu_anim = QPropertyAnimation(self, b"panel_width")
|
||||
else:
|
||||
self.menu_anim = QPropertyAnimation(self, b"panel_height")
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.panel_height = 0 # start hidden
|
||||
|
||||
self.menu_anim.setDuration(self._animation_duration)
|
||||
self.menu_anim.setEasingCurve(QEasingCurve.InOutQuad)
|
||||
|
||||
if self._orientation in ("left", "right"):
|
||||
self.panel_width = 0
|
||||
else:
|
||||
self.panel_height = 0
|
||||
|
||||
@Property(int)
|
||||
def panel_width(self):
|
||||
"""Get the panel width."""
|
||||
"""
|
||||
Get the panel width.
|
||||
"""
|
||||
return self._panel_width
|
||||
|
||||
@panel_width.setter
|
||||
def panel_width(self, width: int):
|
||||
"""Set the panel width."""
|
||||
"""
|
||||
Set the panel width.
|
||||
|
||||
Args:
|
||||
width(int): The width of the panel.
|
||||
"""
|
||||
self._panel_width = width
|
||||
if self._orientation in ("left", "right"):
|
||||
self.stack_widget.setFixedWidth(width)
|
||||
|
||||
@Property(int)
|
||||
def panel_height(self):
|
||||
"""Get the panel height."""
|
||||
"""
|
||||
Get the panel height.
|
||||
"""
|
||||
return self._panel_height
|
||||
|
||||
@panel_height.setter
|
||||
def panel_height(self, height: int):
|
||||
"""Set the panel height."""
|
||||
"""
|
||||
Set the panel height.
|
||||
|
||||
Args:
|
||||
height(int): The height of the panel.
|
||||
"""
|
||||
self._panel_height = height
|
||||
if self._orientation in ("top", "bottom"):
|
||||
self.stack_widget.setFixedHeight(height)
|
||||
|
||||
@Property(int)
|
||||
def panel_max_width(self):
|
||||
"""Get the maximum width of the panel."""
|
||||
"""
|
||||
Get the maximum width of the panel.
|
||||
"""
|
||||
return self._panel_max_width
|
||||
|
||||
@panel_max_width.setter
|
||||
def panel_max_width(self, size: int):
|
||||
"""Set the maximum width of the panel."""
|
||||
"""
|
||||
Set the maximum width of the panel.
|
||||
|
||||
Args:
|
||||
size(int): The maximum width of the panel.
|
||||
"""
|
||||
self._panel_max_width = size
|
||||
if self._orientation in ("left", "right"):
|
||||
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
||||
@@ -158,28 +179,45 @@ class SidePanel(QWidget):
|
||||
|
||||
@Property(int)
|
||||
def animation_duration(self):
|
||||
"""Get the duration of the animation."""
|
||||
"""
|
||||
Get the duration of the animation.
|
||||
"""
|
||||
return self._animation_duration
|
||||
|
||||
@animation_duration.setter
|
||||
def animation_duration(self, duration: int):
|
||||
"""Set the duration of the animation."""
|
||||
"""
|
||||
Set the duration of the animation.
|
||||
|
||||
Args:
|
||||
duration(int): The duration of the animation.
|
||||
"""
|
||||
self._animation_duration = duration
|
||||
self.menu_anim.setDuration(duration)
|
||||
|
||||
@Property(bool)
|
||||
def animations_enabled(self):
|
||||
"""Get the status of the animations."""
|
||||
"""
|
||||
Get the status of the animations.
|
||||
"""
|
||||
return self._animations_enabled
|
||||
|
||||
@animations_enabled.setter
|
||||
def animations_enabled(self, enabled: bool):
|
||||
"""Set the status of the animations."""
|
||||
"""
|
||||
Set the status of the animations.
|
||||
|
||||
Args:
|
||||
enabled(bool): The status of the animations.
|
||||
"""
|
||||
self._animations_enabled = enabled
|
||||
|
||||
def show_panel(self, idx: int):
|
||||
"""
|
||||
Show the side panel with animation and switch to idx.
|
||||
|
||||
Args:
|
||||
idx(int): The index of the panel to show.
|
||||
"""
|
||||
self.stack_widget.setCurrentIndex(idx)
|
||||
self.panel_visible = True
|
||||
@@ -227,6 +265,9 @@ class SidePanel(QWidget):
|
||||
def switch_to(self, idx: int):
|
||||
"""
|
||||
Switch to the specified index without animation.
|
||||
|
||||
Args:
|
||||
idx(int): The index of the panel to switch to.
|
||||
"""
|
||||
if self.current_index != idx:
|
||||
self.stack_widget.setCurrentIndex(idx)
|
||||
@@ -243,35 +284,21 @@ class SidePanel(QWidget):
|
||||
widget(QWidget): The widget to add to the panel.
|
||||
title(str): The title of the panel.
|
||||
"""
|
||||
# container_widget: top-level container for the stacked page
|
||||
container_widget = QWidget()
|
||||
container_layout = QVBoxLayout(container_widget)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
container_layout.setSpacing(5)
|
||||
|
||||
container_widget.setStyleSheet("background-color: rgba(0,0,0,0);")
|
||||
title_label = QLabel(f"<b>{title}</b>")
|
||||
title_label.setStyleSheet("font-size: 16px;")
|
||||
spacer = QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||
container_layout.addWidget(title_label)
|
||||
container_layout.addWidget(widget)
|
||||
container_layout.addItem(spacer)
|
||||
container_layout.setContentsMargins(5, 5, 5, 5)
|
||||
container_layout.setSpacing(5)
|
||||
|
||||
# Create a QScrollArea for the actual widget to ensure scrolling if the widget inside is too large
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setFrameShape(QFrame.NoFrame)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
# Let the scroll area expand in both directions if there's room
|
||||
scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
scroll_area.setWidget(widget)
|
||||
|
||||
# Put the scroll area in the container layout
|
||||
container_layout.addWidget(scroll_area)
|
||||
|
||||
# Optionally stretch the scroll area to fill vertical space
|
||||
container_layout.setStretchFactor(scroll_area, 1)
|
||||
|
||||
# Add container_widget to the stacked widget
|
||||
index = self.stack_widget.count()
|
||||
self.stack_widget.addWidget(container_widget)
|
||||
|
||||
# Add an action to the toolbar
|
||||
action = MaterialIconAction(icon_name=icon_name, tooltip=tooltip, checkable=True)
|
||||
self.toolbar.add_action(action_id, action, target_widget=self)
|
||||
|
||||
@@ -299,11 +326,6 @@ class SidePanel(QWidget):
|
||||
action.action.toggled.connect(on_action_toggled)
|
||||
|
||||
|
||||
############################################
|
||||
# DEMO APPLICATION
|
||||
############################################
|
||||
|
||||
|
||||
class ExampleApp(QMainWindow): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -311,24 +333,20 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
||||
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
self.side_panel = SidePanel(self, orientation="left")
|
||||
|
||||
self.layout = QHBoxLayout(central_widget)
|
||||
|
||||
# Create side panel
|
||||
self.side_panel = SidePanel(self, orientation="left", panel_max_width=250)
|
||||
self.layout.addWidget(self.side_panel)
|
||||
|
||||
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
||||
|
||||
self.plot = BECWaveformWidget()
|
||||
self.layout.addWidget(self.plot)
|
||||
|
||||
self.add_side_menus()
|
||||
|
||||
def add_side_menus(self):
|
||||
widget1 = QWidget()
|
||||
layout1 = QVBoxLayout(widget1)
|
||||
for i in range(15):
|
||||
layout1.addWidget(QLabel(f"Widget 1 label row {i}"))
|
||||
widget1_layout = QVBoxLayout(widget1)
|
||||
widget1_layout.addWidget(QLabel("This is Widget 1"))
|
||||
self.side_panel.add_menu(
|
||||
action_id="widget1",
|
||||
icon_name="counter_1",
|
||||
@@ -338,8 +356,8 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
||||
)
|
||||
|
||||
widget2 = QWidget()
|
||||
layout2 = QVBoxLayout(widget2)
|
||||
layout2.addWidget(QLabel("Short widget 2 content"))
|
||||
widget2_layout = QVBoxLayout(widget2)
|
||||
widget2_layout.addWidget(QLabel("This is Widget 2"))
|
||||
self.side_panel.add_menu(
|
||||
action_id="widget2",
|
||||
icon_name="counter_2",
|
||||
@@ -349,9 +367,8 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
||||
)
|
||||
|
||||
widget3 = QWidget()
|
||||
layout3 = QVBoxLayout(widget3)
|
||||
for i in range(10):
|
||||
layout3.addWidget(QLabel(f"Line {i} for Widget 3"))
|
||||
widget3_layout = QVBoxLayout(widget3)
|
||||
widget3_layout.addWidget(QLabel("This is Widget 3"))
|
||||
self.side_panel.add_menu(
|
||||
action_id="widget3",
|
||||
icon_name="counter_3",
|
||||
@@ -364,6 +381,6 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
window = ExampleApp()
|
||||
window.resize(1000, 700)
|
||||
window.resize(800, 600)
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -2,20 +2,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Literal, Tuple
|
||||
from typing import Literal
|
||||
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QAction, QColor, QIcon
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QSizePolicy,
|
||||
QToolBar,
|
||||
@@ -34,7 +31,7 @@ class ToolBarAction(ABC):
|
||||
|
||||
Args:
|
||||
icon_path (str, optional): The name of the icon file from `assets/toolbar_icons`. Defaults to None.
|
||||
tooltip (str, optional): The tooltip for the action. Defaults to None.
|
||||
tooltip (bool, optional): The tooltip for the action. Defaults to None.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
"""
|
||||
|
||||
@@ -84,18 +81,15 @@ class IconAction(ToolBarAction):
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
|
||||
class MaterialIconAction(ToolBarAction):
|
||||
class MaterialIconAction:
|
||||
"""
|
||||
Action with a Material icon for the toolbar.
|
||||
|
||||
Args:
|
||||
icon_name (str, optional): The name of the Material icon. Defaults to None.
|
||||
tooltip (str, optional): The tooltip for the action. Defaults to None.
|
||||
icon_path (str, optional): The name of the Material icon. Defaults to None.
|
||||
tooltip (bool, optional): The tooltip for the action. Defaults to None.
|
||||
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||
filled (bool, optional): Whether the icon is filled. Defaults to False.
|
||||
color (str | tuple | QColor | dict[Literal["dark", "light"], str] | None, optional): The color of the icon.
|
||||
Defaults to None.
|
||||
parent (QWidget or None, optional): Parent widget for the underlying QAction.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -105,42 +99,30 @@ class MaterialIconAction(ToolBarAction):
|
||||
checkable: bool = False,
|
||||
filled: bool = False,
|
||||
color: str | tuple | QColor | dict[Literal["dark", "light"], str] | None = None,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||
self.icon_name = icon_name
|
||||
self.tooltip = tooltip
|
||||
self.checkable = checkable
|
||||
self.action = None
|
||||
self.filled = filled
|
||||
self.color = color
|
||||
# Generate the icon
|
||||
self.icon = material_icon(
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
icon = self.get_icon()
|
||||
self.action = QAction(icon, self.tooltip, target)
|
||||
self.action.setCheckable(self.checkable)
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
def get_icon(self):
|
||||
|
||||
icon = material_icon(
|
||||
self.icon_name,
|
||||
size=(20, 20),
|
||||
convert_to_pixmap=False,
|
||||
filled=self.filled,
|
||||
color=self.color,
|
||||
)
|
||||
# Immediately create an QAction with the given parent
|
||||
self.action = QAction(self.icon, self.tooltip, parent=parent)
|
||||
self.action.setCheckable(self.checkable)
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
"""
|
||||
Adds the action to the toolbar.
|
||||
|
||||
Args:
|
||||
toolbar(QToolBar): The toolbar to add the action to.
|
||||
target(QWidget): The target widget for the action.
|
||||
"""
|
||||
toolbar.addAction(self.action)
|
||||
|
||||
def get_icon(self):
|
||||
"""
|
||||
Returns the icon for the action.
|
||||
|
||||
Returns:
|
||||
QIcon: The icon for the action.
|
||||
"""
|
||||
return self.icon
|
||||
return icon
|
||||
|
||||
|
||||
class DeviceSelectionAction(ToolBarAction):
|
||||
@@ -150,6 +132,7 @@ class DeviceSelectionAction(ToolBarAction):
|
||||
Args:
|
||||
label (str): The label for the combobox.
|
||||
device_combobox (DeviceComboBox): The combobox for selecting the device.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label: str, device_combobox):
|
||||
@@ -177,6 +160,7 @@ class WidgetAction(ToolBarAction):
|
||||
Args:
|
||||
label (str|None): The label for the widget.
|
||||
widget (QWidget): The widget to be added to the toolbar.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label: str | None = None, widget: QWidget = None, parent=None):
|
||||
@@ -235,6 +219,7 @@ class ExpandableMenuAction(ToolBarAction):
|
||||
label (str): The label for the menu.
|
||||
actions (dict): A dictionary of actions to populate the menu.
|
||||
icon_path (str, optional): The path to the icon file. Defaults to None.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, label: str, actions: dict, icon_path: str = None):
|
||||
@@ -274,55 +259,6 @@ class ExpandableMenuAction(ToolBarAction):
|
||||
toolbar.addWidget(button)
|
||||
|
||||
|
||||
class ToolbarBundle:
|
||||
"""
|
||||
Represents a bundle of toolbar actions, keyed by action_id.
|
||||
Allows direct dictionary-like access: self.actions["some_id"] -> ToolBarAction object.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id: str = None, actions=None):
|
||||
"""
|
||||
Args:
|
||||
bundle_id (str): Unique identifier for the bundle.
|
||||
actions: Either None or a list of (action_id, ToolBarAction) tuples.
|
||||
"""
|
||||
self.bundle_id = bundle_id
|
||||
self._actions: dict[str, ToolBarAction] = {}
|
||||
|
||||
# If you passed in a list of tuples, load them into the dictionary
|
||||
if actions is not None:
|
||||
for action_id, action in actions:
|
||||
self._actions[action_id] = action
|
||||
|
||||
def add_action(self, action_id: str, action: ToolBarAction):
|
||||
"""
|
||||
Adds or replaces an action in the bundle.
|
||||
|
||||
Args:
|
||||
action_id (str): Unique identifier for the action.
|
||||
action (ToolBarAction): The action to add.
|
||||
"""
|
||||
self._actions[action_id] = action
|
||||
|
||||
def remove_action(self, action_id: str):
|
||||
"""
|
||||
Removes an action from the bundle by ID.
|
||||
Ignores if not present.
|
||||
|
||||
Args:
|
||||
action_id (str): Unique identifier for the action to remove.
|
||||
"""
|
||||
self._actions.pop(action_id, None)
|
||||
|
||||
@property
|
||||
def actions(self) -> dict[str, ToolBarAction]:
|
||||
"""
|
||||
Return the internal dictionary of actions so that you can do
|
||||
bundle.actions["drag_mode"] -> ToolBarAction instance.
|
||||
"""
|
||||
return self._actions
|
||||
|
||||
|
||||
class ModularToolBar(QToolBar):
|
||||
"""Modular toolbar with optional automatic initialization.
|
||||
|
||||
@@ -351,14 +287,10 @@ class ModularToolBar(QToolBar):
|
||||
# Set the initial orientation
|
||||
self.set_orientation(orientation)
|
||||
|
||||
# Initialize bundles
|
||||
self.bundles = {}
|
||||
self.toolbar_items = []
|
||||
|
||||
if actions is not None and target_widget is not None:
|
||||
self.populate_toolbar(actions, target_widget)
|
||||
|
||||
def populate_toolbar(self, actions: dict, target_widget: QWidget):
|
||||
def populate_toolbar(self, actions: dict, target_widget):
|
||||
"""Populates the toolbar with a set of actions.
|
||||
|
||||
Args:
|
||||
@@ -366,12 +298,9 @@ class ModularToolBar(QToolBar):
|
||||
target_widget (QWidget): The widget that the actions will target.
|
||||
"""
|
||||
self.clear()
|
||||
self.toolbar_items.clear() # Reset the order tracking
|
||||
for action_id, action in actions.items():
|
||||
action.add_to_toolbar(self, target_widget)
|
||||
self.widgets[action_id] = action
|
||||
self.toolbar_items.append(("action", action_id))
|
||||
self.update_separators() # Ensure separators are updated after populating
|
||||
|
||||
def set_background_color(self, color: str = "rgba(0, 0, 0, 0)"):
|
||||
"""
|
||||
@@ -416,7 +345,7 @@ class ModularToolBar(QToolBar):
|
||||
|
||||
def add_action(self, action_id: str, action: ToolBarAction, target_widget: QWidget):
|
||||
"""
|
||||
Adds a new standalone action to the toolbar dynamically.
|
||||
Adds a new action to the toolbar dynamically.
|
||||
|
||||
Args:
|
||||
action_id (str): Unique identifier for the action.
|
||||
@@ -427,8 +356,6 @@ class ModularToolBar(QToolBar):
|
||||
raise ValueError(f"Action with ID '{action_id}' already exists.")
|
||||
action.add_to_toolbar(self, target_widget)
|
||||
self.widgets[action_id] = action
|
||||
self.toolbar_items.append(("action", action_id))
|
||||
self.update_separators() # Update separators after adding the action
|
||||
|
||||
def hide_action(self, action_id: str):
|
||||
"""
|
||||
@@ -442,7 +369,6 @@ class ModularToolBar(QToolBar):
|
||||
action = self.widgets[action_id]
|
||||
if hasattr(action, "action") and isinstance(action.action, QAction):
|
||||
action.action.setVisible(False)
|
||||
self.update_separators() # Update separators after hiding the action
|
||||
|
||||
def show_action(self, action_id: str):
|
||||
"""
|
||||
@@ -456,217 +382,3 @@ class ModularToolBar(QToolBar):
|
||||
action = self.widgets[action_id]
|
||||
if hasattr(action, "action") and isinstance(action.action, QAction):
|
||||
action.action.setVisible(True)
|
||||
self.update_separators() # Update separators after showing the action
|
||||
|
||||
def add_bundle(self, bundle: ToolbarBundle, target_widget: QWidget):
|
||||
"""
|
||||
Adds a bundle of actions to the toolbar, separated by a separator.
|
||||
|
||||
Args:
|
||||
bundle (ToolbarBundle): The bundle to add.
|
||||
target_widget (QWidget): The target widget for the actions.
|
||||
"""
|
||||
if bundle.bundle_id in self.bundles:
|
||||
raise ValueError(f"ToolbarBundle with ID '{bundle.bundle_id}' already exists.")
|
||||
|
||||
# Add a separator before the bundle (but not to first one)
|
||||
if self.toolbar_items:
|
||||
sep = SeparatorAction()
|
||||
sep.add_to_toolbar(self, target_widget)
|
||||
self.toolbar_items.append(("separator", None))
|
||||
|
||||
# Add each action in the bundle
|
||||
for action_id, action_obj in bundle.actions.items():
|
||||
action_obj.add_to_toolbar(self, target_widget)
|
||||
self.widgets[action_id] = action_obj
|
||||
|
||||
# Register the bundle
|
||||
self.bundles[bundle.bundle_id] = list(bundle.actions.keys())
|
||||
self.toolbar_items.append(("bundle", bundle.bundle_id))
|
||||
|
||||
self.update_separators() # Update separators after adding the bundle
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""
|
||||
Overrides the context menu event to show a list of toolbar actions with checkboxes and icons, including separators.
|
||||
|
||||
Args:
|
||||
event(QContextMenuEvent): The context menu event.
|
||||
"""
|
||||
menu = QMenu(self)
|
||||
|
||||
# Iterate through the toolbar items in order
|
||||
for item_type, identifier in self.toolbar_items:
|
||||
if item_type == "separator":
|
||||
menu.addSeparator()
|
||||
elif item_type == "bundle":
|
||||
self.handle_bundle_context_menu(menu, identifier)
|
||||
elif item_type == "action":
|
||||
self.handle_action_context_menu(menu, identifier)
|
||||
|
||||
# Connect the triggered signal after all actions are added
|
||||
menu.triggered.connect(self.handle_menu_triggered)
|
||||
menu.exec_(event.globalPos())
|
||||
|
||||
def handle_bundle_context_menu(self, menu: QMenu, bundle_id: str):
|
||||
"""
|
||||
Adds a set of bundle actions to the context menu.
|
||||
|
||||
Args:
|
||||
menu (QMenu): The context menu to which the actions are added.
|
||||
bundle_id (str): The identifier for the bundle.
|
||||
"""
|
||||
action_ids = self.bundles.get(bundle_id, [])
|
||||
for act_id in action_ids:
|
||||
toolbar_action = self.widgets.get(act_id)
|
||||
if not isinstance(toolbar_action, ToolBarAction) or not hasattr(
|
||||
toolbar_action, "action"
|
||||
):
|
||||
continue
|
||||
qaction = toolbar_action.action
|
||||
if not isinstance(qaction, QAction):
|
||||
continue
|
||||
display_name = qaction.text() or toolbar_action.tooltip or act_id
|
||||
menu_action = QAction(display_name, self)
|
||||
menu_action.setCheckable(True)
|
||||
menu_action.setChecked(qaction.isVisible())
|
||||
menu_action.setData(act_id) # Store the action_id
|
||||
|
||||
# Set the icon if available
|
||||
if qaction.icon() and not qaction.icon().isNull():
|
||||
menu_action.setIcon(qaction.icon())
|
||||
|
||||
menu.addAction(menu_action)
|
||||
|
||||
def handle_action_context_menu(self, menu: QMenu, action_id: str):
|
||||
"""
|
||||
Adds a single toolbar action to the context menu.
|
||||
|
||||
Args:
|
||||
menu (QMenu): The context menu to which the action is added.
|
||||
action_id (str): Unique identifier for the action.
|
||||
"""
|
||||
toolbar_action = self.widgets.get(action_id)
|
||||
if not isinstance(toolbar_action, ToolBarAction) or not hasattr(toolbar_action, "action"):
|
||||
return
|
||||
qaction = toolbar_action.action
|
||||
if not isinstance(qaction, QAction):
|
||||
return
|
||||
display_name = qaction.text() or toolbar_action.tooltip or action_id
|
||||
menu_action = QAction(display_name, self)
|
||||
menu_action.setCheckable(True)
|
||||
menu_action.setChecked(qaction.isVisible())
|
||||
menu_action.setData(action_id) # Store the action_id
|
||||
|
||||
# Set the icon if available
|
||||
if qaction.icon() and not qaction.icon().isNull():
|
||||
menu_action.setIcon(qaction.icon())
|
||||
|
||||
menu.addAction(menu_action)
|
||||
|
||||
def handle_menu_triggered(self, action):
|
||||
"""Handles the toggling of toolbar actions from the context menu."""
|
||||
action_id = action.data()
|
||||
if action_id:
|
||||
self.toggle_action_visibility(action_id, action.isChecked())
|
||||
|
||||
def toggle_action_visibility(self, action_id: str, visible: bool):
|
||||
"""
|
||||
Toggles the visibility of a specific action on the toolbar.
|
||||
|
||||
Args:
|
||||
action_id(str): Unique identifier for the action to toggle.
|
||||
visible(bool): Whether the action should be visible.
|
||||
"""
|
||||
if action_id not in self.widgets:
|
||||
return
|
||||
|
||||
tool_action = self.widgets[action_id]
|
||||
if hasattr(tool_action, "action") and isinstance(tool_action.action, QAction):
|
||||
tool_action.action.setVisible(visible)
|
||||
self.update_separators()
|
||||
|
||||
def update_separators(self):
|
||||
"""
|
||||
Hide separators that are adjacent to another separator or have no actions next to them.
|
||||
"""
|
||||
toolbar_actions = self.actions()
|
||||
|
||||
for i, action in enumerate(toolbar_actions):
|
||||
if not action.isSeparator():
|
||||
continue
|
||||
# Find the previous visible action
|
||||
prev_visible = None
|
||||
for j in range(i - 1, -1, -1):
|
||||
if toolbar_actions[j].isVisible():
|
||||
prev_visible = toolbar_actions[j]
|
||||
break
|
||||
|
||||
# Find the next visible action
|
||||
next_visible = None
|
||||
for j in range(i + 1, len(toolbar_actions)):
|
||||
if toolbar_actions[j].isVisible():
|
||||
next_visible = toolbar_actions[j]
|
||||
break
|
||||
|
||||
# Determine if the separator should be hidden
|
||||
# Hide if both previous and next visible actions are separators or non-existent
|
||||
if (prev_visible is None or prev_visible.isSeparator()) and (
|
||||
next_visible is None or next_visible.isSeparator()
|
||||
):
|
||||
action.setVisible(False)
|
||||
else:
|
||||
action.setVisible(True)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Toolbar / ToolbarBundle Demo")
|
||||
|
||||
self.central_widget = QWidget()
|
||||
self.setCentralWidget(self.central_widget)
|
||||
|
||||
# Create a modular toolbar
|
||||
self.toolbar = ModularToolBar(parent=self, target_widget=self)
|
||||
self.addToolBar(self.toolbar)
|
||||
|
||||
# Example: Add a single bundle
|
||||
home_action = MaterialIconAction(
|
||||
icon_name="home", tooltip="Home", checkable=True, parent=self
|
||||
)
|
||||
settings_action = MaterialIconAction(
|
||||
icon_name="settings", tooltip="Settings", checkable=True, parent=self
|
||||
)
|
||||
profile_action = MaterialIconAction(
|
||||
icon_name="person", tooltip="Profile", checkable=True, parent=self
|
||||
)
|
||||
main_actions_bundle = ToolbarBundle(
|
||||
bundle_id="main_actions",
|
||||
actions=[
|
||||
("home_action", home_action),
|
||||
("settings_action", settings_action),
|
||||
("profile_action", profile_action),
|
||||
],
|
||||
)
|
||||
self.toolbar.add_bundle(main_actions_bundle, target_widget=self)
|
||||
|
||||
# Another bundle
|
||||
search_action = MaterialIconAction(
|
||||
icon_name="search", tooltip="Search", checkable=True, parent=self
|
||||
)
|
||||
help_action = MaterialIconAction(
|
||||
icon_name="help", tooltip="Help", checkable=True, parent=self
|
||||
)
|
||||
second_bundle = ToolbarBundle(
|
||||
bundle_id="secondary_actions",
|
||||
actions=[("search_action", search_action), ("help_action", help_action)],
|
||||
)
|
||||
self.toolbar.add_bundle(second_bundle, target_widget=self)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
main_window = MainWindow()
|
||||
main_window.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -224,11 +224,3 @@ DEVICES = [
|
||||
Positioner("test", limits=[-10, 10], read_value=2.0),
|
||||
Device("test_device"),
|
||||
]
|
||||
|
||||
|
||||
def check_remote_data_size(widget, plot_name, num_elements):
|
||||
"""
|
||||
Check if the remote data has the correct number of elements.
|
||||
Used in the qtbot.waitUntil function.
|
||||
"""
|
||||
return len(widget.get_all_data()[plot_name]["x"]) == num_elements
|
||||
|
||||
@@ -5,43 +5,28 @@ analyse data. Requesting a new fit may lead to request piling up and an overall
|
||||
will allow you to decide by yourself when to unblock and execute the callback again."""
|
||||
|
||||
from pyqtgraph import SignalProxy
|
||||
from qtpy.QtCore import QTimer, Signal
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from qtpy.QtCore import Signal, Slot
|
||||
|
||||
|
||||
class BECSignalProxy(SignalProxy):
|
||||
"""
|
||||
Thin wrapper around the SignalProxy class to allow signal calls to be blocked,
|
||||
but arguments still being stored.
|
||||
"""Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored
|
||||
|
||||
Args:
|
||||
*args: Arguments to pass to the SignalProxy class.
|
||||
rateLimit (int): The rateLimit of the proxy.
|
||||
timeout (float): The number of seconds after which the proxy automatically
|
||||
unblocks if still blocked. Default is 10.0 seconds.
|
||||
**kwargs: Keyword arguments to pass to the SignalProxy class.
|
||||
*args: Arguments to pass to the SignalProxy class
|
||||
rateLimit (int): The rateLimit of the proxy
|
||||
**kwargs: Keyword arguments to pass to the SignalProxy class
|
||||
|
||||
Example:
|
||||
>>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)
|
||||
"""
|
||||
>>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)"""
|
||||
|
||||
is_blocked = Signal(bool)
|
||||
|
||||
def __init__(self, *args, rateLimit=25, timeout=10.0, **kwargs):
|
||||
def __init__(self, *args, rateLimit=25, **kwargs):
|
||||
super().__init__(*args, rateLimit=rateLimit, **kwargs)
|
||||
self._blocking = False
|
||||
self.old_args = None
|
||||
self.new_args = None
|
||||
|
||||
# Store timeout value (in seconds)
|
||||
self._timeout = timeout
|
||||
|
||||
# Create a single-shot timer for auto-unblocking
|
||||
self._timer = QTimer()
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.timeout.connect(self._timeout_unblock)
|
||||
|
||||
@property
|
||||
def blocked(self):
|
||||
"""Returns if the proxy is blocked"""
|
||||
@@ -61,22 +46,9 @@ class BECSignalProxy(SignalProxy):
|
||||
self.old_args = args
|
||||
super().signalReceived(*args)
|
||||
|
||||
self._timer.start(int(self._timeout * 1000))
|
||||
|
||||
@SafeSlot()
|
||||
@Slot()
|
||||
def unblock_proxy(self):
|
||||
"""Unblock the proxy, and call the signalReceived method in case there was an update of the args."""
|
||||
if self.blocked:
|
||||
self._timer.stop()
|
||||
self.blocked = False
|
||||
if self.new_args != self.old_args:
|
||||
self.signalReceived(*self.new_args)
|
||||
|
||||
@SafeSlot()
|
||||
def _timeout_unblock(self):
|
||||
"""
|
||||
Internal method called by the QTimer upon timeout. Unblocks the proxy
|
||||
automatically if it is still blocked.
|
||||
"""
|
||||
if self.blocked:
|
||||
self.unblock_proxy()
|
||||
self.blocked = False
|
||||
if self.new_args != self.old_args:
|
||||
self.signalReceived(*self.new_args)
|
||||
|
||||
@@ -15,8 +15,6 @@ from qtpy.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||
|
||||
|
||||
class WidgetHandler(ABC):
|
||||
"""Abstract base class for all widget handlers."""
|
||||
@@ -127,19 +125,6 @@ class CheckBoxHandler(WidgetHandler):
|
||||
widget.toggled.connect(lambda val, w=widget: slot(w, val))
|
||||
|
||||
|
||||
class ToggleSwitchHandler(WidgetHandler):
|
||||
"""Handler for ToggleSwitch widgets."""
|
||||
|
||||
def get_value(self, widget, **kwargs):
|
||||
return widget.checked
|
||||
|
||||
def set_value(self, widget, value):
|
||||
widget.checked = value
|
||||
|
||||
def connect_change_signal(self, widget: ToggleSwitch, slot):
|
||||
widget.enabled.connect(lambda val, w=widget: slot(w, val))
|
||||
|
||||
|
||||
class LabelHandler(WidgetHandler):
|
||||
"""Handler for QLabel widgets."""
|
||||
|
||||
@@ -164,7 +149,6 @@ class WidgetIO:
|
||||
QDoubleSpinBox: SpinBoxHandler,
|
||||
QCheckBox: CheckBoxHandler,
|
||||
QLabel: LabelHandler,
|
||||
ToggleSwitch: ToggleSwitchHandler,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from qtpy.QtCore import QSettings
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QFileDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class WidgetStateManager:
|
||||
"""
|
||||
A class to manage the state of a widget by saving and loading the state to and from a INI file.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to manage the state for.
|
||||
"""
|
||||
|
||||
def __init__(self, widget):
|
||||
self.widget = widget
|
||||
|
||||
def save_state(self, filename: str = None):
|
||||
"""
|
||||
Save the state of the widget to an INI file.
|
||||
|
||||
Args:
|
||||
filename(str): The filename to save the state to.
|
||||
"""
|
||||
if not filename:
|
||||
filename, _ = QFileDialog.getSaveFileName(
|
||||
self.widget, "Save Settings", "", "INI Files (*.ini)"
|
||||
)
|
||||
if filename:
|
||||
settings = QSettings(filename, QSettings.IniFormat)
|
||||
self._save_widget_state_qsettings(self.widget, settings)
|
||||
|
||||
def load_state(self, filename: str = None):
|
||||
"""
|
||||
Load the state of the widget from an INI file.
|
||||
|
||||
Args:
|
||||
filename(str): The filename to load the state from.
|
||||
"""
|
||||
if not filename:
|
||||
filename, _ = QFileDialog.getOpenFileName(
|
||||
self.widget, "Load Settings", "", "INI Files (*.ini)"
|
||||
)
|
||||
if filename:
|
||||
settings = QSettings(filename, QSettings.IniFormat)
|
||||
self._load_widget_state_qsettings(self.widget, settings)
|
||||
|
||||
def _save_widget_state_qsettings(self, widget: QWidget, settings: QSettings):
|
||||
"""
|
||||
Save the state of the widget to QSettings.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to save the state for.
|
||||
settings(QSettings): The QSettings object to save the state to.
|
||||
"""
|
||||
if widget.property("skip_settings") is True:
|
||||
return
|
||||
|
||||
meta = widget.metaObject()
|
||||
widget_name = self._get_full_widget_name(widget)
|
||||
settings.beginGroup(widget_name)
|
||||
for i in range(meta.propertyCount()):
|
||||
prop = meta.property(i)
|
||||
name = prop.name()
|
||||
if (
|
||||
name == "objectName"
|
||||
or not prop.isReadable()
|
||||
or not prop.isWritable()
|
||||
or not prop.isStored() # can be extended to fine filter
|
||||
):
|
||||
continue
|
||||
value = widget.property(name)
|
||||
settings.setValue(name, value)
|
||||
settings.endGroup()
|
||||
|
||||
# Recursively process children (only if they aren't skipped)
|
||||
for child in widget.children():
|
||||
if (
|
||||
child.objectName()
|
||||
and child.property("skip_settings") is not True
|
||||
and not isinstance(child, QLabel)
|
||||
):
|
||||
self._save_widget_state_qsettings(child, settings)
|
||||
|
||||
def _load_widget_state_qsettings(self, widget: QWidget, settings: QSettings):
|
||||
"""
|
||||
Load the state of the widget from QSettings.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to load the state for.
|
||||
settings(QSettings): The QSettings object to load the state from.
|
||||
"""
|
||||
if widget.property("skip_settings") is True:
|
||||
return
|
||||
|
||||
meta = widget.metaObject()
|
||||
widget_name = self._get_full_widget_name(widget)
|
||||
settings.beginGroup(widget_name)
|
||||
for i in range(meta.propertyCount()):
|
||||
prop = meta.property(i)
|
||||
name = prop.name()
|
||||
if settings.contains(name):
|
||||
value = settings.value(name)
|
||||
widget.setProperty(name, value)
|
||||
settings.endGroup()
|
||||
|
||||
# Recursively process children (only if they aren't skipped)
|
||||
for child in widget.children():
|
||||
if (
|
||||
child.objectName()
|
||||
and child.property("skip_settings") is not True
|
||||
and not isinstance(child, QLabel)
|
||||
):
|
||||
self._load_widget_state_qsettings(child, settings)
|
||||
|
||||
def _get_full_widget_name(self, widget: QWidget):
|
||||
"""
|
||||
Get the full name of the widget including its parent names.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to get the full name for.
|
||||
|
||||
Returns:
|
||||
str: The full name of the widget.
|
||||
"""
|
||||
name = widget.objectName()
|
||||
parent = widget.parent()
|
||||
while parent:
|
||||
obj_name = parent.objectName() or parent.metaObject().className()
|
||||
name = obj_name + "." + name
|
||||
parent = parent.parent()
|
||||
return name
|
||||
|
||||
|
||||
class ExampleApp(QWidget): # pragma: no cover:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setObjectName("MainWindow")
|
||||
self.setWindowTitle("State Manager Example")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# A line edit to store some user text
|
||||
self.line_edit = QLineEdit(self)
|
||||
self.line_edit.setObjectName("MyLineEdit")
|
||||
self.line_edit.setPlaceholderText("Enter some text here...")
|
||||
layout.addWidget(self.line_edit)
|
||||
|
||||
# A spin box to hold a numeric value
|
||||
self.spin_box = QSpinBox(self)
|
||||
self.spin_box.setObjectName("MySpinBox")
|
||||
self.spin_box.setRange(0, 100)
|
||||
layout.addWidget(self.spin_box)
|
||||
|
||||
# A checkbox to hold a boolean value
|
||||
self.check_box = QCheckBox("Enable feature?", self)
|
||||
self.check_box.setObjectName("MyCheckBox")
|
||||
layout.addWidget(self.check_box)
|
||||
|
||||
# A checkbox that we want to skip
|
||||
self.check_box_skip = QCheckBox("Enable feature - skip save?", self)
|
||||
self.check_box_skip.setProperty("skip_state", True)
|
||||
self.check_box_skip.setObjectName("MyCheckBoxSkip")
|
||||
layout.addWidget(self.check_box_skip)
|
||||
|
||||
# CREATE A "SIDE PANEL" with nested structure and skip all what is inside
|
||||
self.side_panel = QWidget(self)
|
||||
self.side_panel.setObjectName("SidePanel")
|
||||
self.side_panel.setProperty("skip_settings", True) # skip the ENTIRE panel
|
||||
layout.addWidget(self.side_panel)
|
||||
|
||||
# Put some sub-widgets inside side_panel
|
||||
panel_layout = QVBoxLayout(self.side_panel)
|
||||
self.panel_label = QLabel("Label in side panel", self.side_panel)
|
||||
self.panel_label.setObjectName("PanelLabel")
|
||||
panel_layout.addWidget(self.panel_label)
|
||||
|
||||
self.panel_edit = QLineEdit(self.side_panel)
|
||||
self.panel_edit.setObjectName("PanelLineEdit")
|
||||
self.panel_edit.setPlaceholderText("I am inside side panel")
|
||||
panel_layout.addWidget(self.panel_edit)
|
||||
|
||||
self.panel_checkbox = QCheckBox("Enable feature in side panel?", self.side_panel)
|
||||
self.panel_checkbox.setObjectName("PanelCheckBox")
|
||||
panel_layout.addWidget(self.panel_checkbox)
|
||||
|
||||
# Save/Load buttons
|
||||
button_layout = QHBoxLayout()
|
||||
self.save_button = QPushButton("Save State", self)
|
||||
self.load_button = QPushButton("Load State", self)
|
||||
button_layout.addWidget(self.save_button)
|
||||
button_layout.addWidget(self.load_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Create the state manager
|
||||
self.state_manager = WidgetStateManager(self)
|
||||
|
||||
# Connect buttons
|
||||
self.save_button.clicked.connect(lambda: self.state_manager.save_state())
|
||||
self.load_button.clicked.connect(lambda: self.state_manager.load_state())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover:
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = ExampleApp()
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -20,7 +20,7 @@ from bec_widgets.qt_utils.toolbar import (
|
||||
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
||||
from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
|
||||
from bec_widgets.widgets.plots.image.image_widget import BECImageWidget
|
||||
|
||||
@@ -634,7 +634,7 @@ class BECImageShow(BECPlotBase):
|
||||
)
|
||||
image_item.connected = False
|
||||
if monitor and image_item.connected is False:
|
||||
self.entry_validator.validate_monitor(monitor)
|
||||
# self.entry_validator.validate_monitor(monitor)
|
||||
if self.image_type == "device_monitor_1d":
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor_1d(monitor)
|
||||
|
||||
@@ -34,7 +34,6 @@ class LayoutManagerWidget(QWidget):
|
||||
|
||||
def __init__(self, parent=None, auto_reindex=True):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("LayoutManagerWidget")
|
||||
self.layout = QGridLayout(self)
|
||||
self.auto_reindex = auto_reindex
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import (
|
||||
PositionerBox,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
|
||||
PositionerBox2D,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import (
|
||||
PositionerControlLine,
|
||||
)
|
||||
|
||||
__ALL__ = ["PositionerBox", "PositionerControlLine", "PositionerBox2D"]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .positioner_box_base import PositionerBoxBase
|
||||
|
||||
__ALL__ = ["PositionerBoxBase"]
|
||||
@@ -1,243 +0,0 @@
|
||||
import uuid
|
||||
from abc import abstractmethod
|
||||
from ast import Tuple
|
||||
from typing import Callable, TypedDict
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.messages import ScanQueueMessage
|
||||
from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDoubleSpinBox,
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
|
||||
PositionIndicator,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
)
|
||||
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DeviceUpdateUIComponents(TypedDict):
|
||||
spinner: SpinnerWidget
|
||||
setpoint: QLineEdit
|
||||
readback: QLabel
|
||||
position_indicator: PositionIndicator
|
||||
step_size: QDoubleSpinBox
|
||||
device_box: QGroupBox
|
||||
stop: QPushButton
|
||||
tweak_increase: QPushButton
|
||||
tweak_decrease: QPushButton
|
||||
|
||||
|
||||
class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
"""Contains some core logic for positioner box widgets"""
|
||||
|
||||
current_path = ""
|
||||
ICON_NAME = "switch_right"
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
"""Initialize the PositionerBox widget.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
device (Positioner): The device to control.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
|
||||
self._dialog = None
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
def _check_device_is_valid(self, device: str):
|
||||
"""Check if the device is a positioner
|
||||
|
||||
Args:
|
||||
device (str): The device name
|
||||
"""
|
||||
if device not in self.dev:
|
||||
logger.info(f"Device {device} not found in the device list")
|
||||
return False
|
||||
if not isinstance(self.dev[device], Positioner):
|
||||
logger.info(f"Device {device} is not a positioner")
|
||||
return False
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents: ...
|
||||
|
||||
def _init_device(
|
||||
self,
|
||||
device: str,
|
||||
position_emit: Callable[[float], None],
|
||||
limit_update: Callable[[tuple[float, float]], None],
|
||||
):
|
||||
"""Init the device view and readback"""
|
||||
if self._check_device_is_valid(device):
|
||||
data = self.dev[device].read()
|
||||
self._on_device_readback(
|
||||
device,
|
||||
self._device_ui_components(device),
|
||||
{"signals": data},
|
||||
{},
|
||||
position_emit,
|
||||
limit_update,
|
||||
)
|
||||
|
||||
def _stop_device(self, device: str):
|
||||
"""Stop call"""
|
||||
request_id = str(uuid.uuid4())
|
||||
params = {"device": device, "rpc_id": request_id, "func": "stop", "args": [], "kwargs": {}}
|
||||
msg = ScanQueueMessage(
|
||||
scan_type="device_rpc",
|
||||
parameter=params,
|
||||
queue="emergency",
|
||||
metadata={"RID": request_id, "response": False},
|
||||
)
|
||||
self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _on_device_readback(
|
||||
self,
|
||||
device: str,
|
||||
ui_components: DeviceUpdateUIComponents,
|
||||
msg_content: dict,
|
||||
metadata: dict,
|
||||
position_emit: Callable[[float], None],
|
||||
limit_update: Callable[[tuple[float, float]], None],
|
||||
):
|
||||
signals = msg_content.get("signals", {})
|
||||
# pylint: disable=protected-access
|
||||
hinted_signals = self.dev[device]._hints
|
||||
precision = self.dev[device].precision
|
||||
|
||||
spinner = ui_components["spinner"]
|
||||
position_indicator = ui_components["position_indicator"]
|
||||
readback = ui_components["readback"]
|
||||
setpoint = ui_components["setpoint"]
|
||||
|
||||
readback_val = None
|
||||
setpoint_val = None
|
||||
|
||||
if len(hinted_signals) == 1:
|
||||
signal = hinted_signals[0]
|
||||
readback_val = signals.get(signal, {}).get("value")
|
||||
|
||||
for setpoint_signal in ["setpoint", "user_setpoint"]:
|
||||
setpoint_val = signals.get(f"{device}_{setpoint_signal}", {}).get("value")
|
||||
if setpoint_val is not None:
|
||||
break
|
||||
|
||||
for moving_signal in ["motor_done_move", "motor_is_moving"]:
|
||||
is_moving = signals.get(f"{device}_{moving_signal}", {}).get("value")
|
||||
if is_moving is not None:
|
||||
break
|
||||
|
||||
if is_moving is not None:
|
||||
spinner.setVisible(True)
|
||||
if is_moving:
|
||||
spinner.start()
|
||||
spinner.setToolTip("Device is moving")
|
||||
self.set_global_state("warning")
|
||||
else:
|
||||
spinner.stop()
|
||||
spinner.setToolTip("Device is idle")
|
||||
self.set_global_state("success")
|
||||
else:
|
||||
spinner.setVisible(False)
|
||||
|
||||
if readback_val is not None:
|
||||
readback.setText(f"{readback_val:.{precision}f}")
|
||||
position_emit(readback_val)
|
||||
|
||||
if setpoint_val is not None:
|
||||
setpoint.setText(f"{setpoint_val:.{precision}f}")
|
||||
|
||||
limits = self.dev[device].limits
|
||||
limit_update(limits)
|
||||
if limits is not None and readback_val is not None and limits[0] != limits[1]:
|
||||
pos = (readback_val - limits[0]) / (limits[1] - limits[0])
|
||||
position_indicator.set_value(pos)
|
||||
|
||||
def _update_limits_ui(
|
||||
self, limits: tuple[float, float], position_indicator, setpoint_validator
|
||||
):
|
||||
if limits is not None and limits[0] != limits[1]:
|
||||
position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
|
||||
setpoint_validator.setRange(limits[0], limits[1])
|
||||
else:
|
||||
position_indicator.setToolTip("No limits set")
|
||||
setpoint_validator.setRange(float("-inf"), float("inf"))
|
||||
|
||||
def _update_device_ui(self, device: str, ui: DeviceUpdateUIComponents):
|
||||
ui["device_box"].setTitle(device)
|
||||
ui["readback"].setToolTip(f"{device} readback")
|
||||
ui["setpoint"].setToolTip(f"{device} setpoint")
|
||||
ui["step_size"].setToolTip(f"Step size for {device}")
|
||||
precision = self.dev[device].precision
|
||||
if precision is not None:
|
||||
ui["step_size"].setDecimals(precision)
|
||||
ui["step_size"].setValue(10**-precision * 10)
|
||||
|
||||
def _swap_readback_signal_connection(self, slot, old_device, new_device):
|
||||
self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))
|
||||
self.bec_dispatcher.connect_slot(slot, MessageEndpoints.device_readback(new_device))
|
||||
|
||||
def _toggle_enable_buttons(self, ui: DeviceUpdateUIComponents, enable: bool) -> None:
|
||||
"""Toogle enable/disable on available buttons
|
||||
|
||||
Args:
|
||||
enable (bool): Enable buttons
|
||||
"""
|
||||
ui["tweak_increase"].setEnabled(enable)
|
||||
ui["tweak_decrease"].setEnabled(enable)
|
||||
ui["stop"].setEnabled(enable)
|
||||
ui["setpoint"].setEnabled(enable)
|
||||
ui["step_size"].setEnabled(enable)
|
||||
|
||||
def _on_device_change(
|
||||
self,
|
||||
old_device: str,
|
||||
new_device: str,
|
||||
position_emit: Callable[[float], None],
|
||||
limit_update: Callable[[tuple[float, float]], None],
|
||||
on_device_readback: Callable,
|
||||
ui: DeviceUpdateUIComponents,
|
||||
):
|
||||
logger.info(f"Device changed from {old_device} to {new_device}")
|
||||
self._toggle_enable_buttons(ui, True)
|
||||
self._init_device(new_device, position_emit, limit_update)
|
||||
self._swap_readback_signal_connection(on_device_readback, old_device, new_device)
|
||||
self._update_device_ui(new_device, ui)
|
||||
|
||||
def _open_dialog_selection(self, set_positioner: Callable):
|
||||
def _ods():
|
||||
"""Open dialog window for positioner selection"""
|
||||
self._dialog = QDialog(self)
|
||||
self._dialog.setWindowTitle("Positioner Selection")
|
||||
layout = QVBoxLayout()
|
||||
line_edit = DeviceLineEdit(
|
||||
self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
|
||||
)
|
||||
line_edit.textChanged.connect(set_positioner)
|
||||
layout.addWidget(line_edit)
|
||||
close_button = QPushButton("Close")
|
||||
close_button.clicked.connect(self._dialog.accept)
|
||||
layout.addWidget(close_button)
|
||||
self._dialog.setLayout(layout)
|
||||
self._dialog.exec()
|
||||
self._dialog = None
|
||||
|
||||
return _ods
|
||||
@@ -0,0 +1,352 @@
|
||||
""" Module for a PositionerBox widget to control a positioner device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.messages import ScanQueueMessage
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtGui import QDoubleValidator
|
||||
from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout
|
||||
|
||||
from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors, set_theme
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
class PositionerBox(BECWidget, CompactPopupWidget):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
ui_file = "positioner_box.ui"
|
||||
dimensions = (234, 224)
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "switch_right"
|
||||
USER_ACCESS = ["set_positioner"]
|
||||
device_changed = Signal(str, str)
|
||||
# Signal emitted to inform listeners about a position update
|
||||
position_update = Signal(float)
|
||||
|
||||
def __init__(self, parent=None, device: Positioner = None, **kwargs):
|
||||
"""Initialize the PositionerBox widget.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
device (Positioner): The device to control.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
|
||||
self.get_bec_shortcuts()
|
||||
self._device = ""
|
||||
self._limits = None
|
||||
self._dialog = None
|
||||
|
||||
self.init_ui()
|
||||
|
||||
if device is not None:
|
||||
self.device = device
|
||||
self.init_device()
|
||||
|
||||
def init_ui(self):
|
||||
"""Init the ui"""
|
||||
self.device_changed.connect(self.on_device_change)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
|
||||
|
||||
self.addWidget(self.ui)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# fix the size of the device box
|
||||
db = self.ui.device_box
|
||||
db.setFixedHeight(self.dimensions[0])
|
||||
db.setFixedWidth(self.dimensions[1])
|
||||
|
||||
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
|
||||
self.ui.stop.clicked.connect(self.on_stop)
|
||||
self.ui.stop.setToolTip("Stop")
|
||||
self.ui.stop.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
self.ui.tweak_right.clicked.connect(self.on_tweak_right)
|
||||
self.ui.tweak_right.setToolTip("Tweak right")
|
||||
self.ui.tweak_left.clicked.connect(self.on_tweak_left)
|
||||
self.ui.tweak_left.setToolTip("Tweak left")
|
||||
self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
|
||||
|
||||
self.setpoint_validator = QDoubleValidator()
|
||||
self.ui.setpoint.setValidator(self.setpoint_validator)
|
||||
self.ui.spinner_widget.start()
|
||||
self.ui.tool_button.clicked.connect(self._open_dialog_selection)
|
||||
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
|
||||
self.ui.tool_button.setIcon(icon)
|
||||
|
||||
def _open_dialog_selection(self):
|
||||
"""Open dialog window for positioner selection"""
|
||||
self._dialog = QDialog(self)
|
||||
self._dialog.setWindowTitle("Positioner Selection")
|
||||
layout = QVBoxLayout()
|
||||
line_edit = DeviceLineEdit(
|
||||
self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
|
||||
)
|
||||
line_edit.textChanged.connect(self.set_positioner)
|
||||
layout.addWidget(line_edit)
|
||||
close_button = QPushButton("Close")
|
||||
close_button.clicked.connect(self._dialog.accept)
|
||||
layout.addWidget(close_button)
|
||||
self._dialog.setLayout(layout)
|
||||
self._dialog.exec()
|
||||
self._dialog = None
|
||||
|
||||
def init_device(self):
|
||||
"""Init the device view and readback"""
|
||||
if self._check_device_is_valid(self.device):
|
||||
data = self.dev[self.device].read()
|
||||
self.on_device_readback({"signals": data}, {})
|
||||
|
||||
def _toogle_enable_buttons(self, enable: bool) -> None:
|
||||
"""Toogle enable/disable on available buttons
|
||||
|
||||
Args:
|
||||
enable (bool): Enable buttons
|
||||
"""
|
||||
self.ui.tweak_left.setEnabled(enable)
|
||||
self.ui.tweak_right.setEnabled(enable)
|
||||
self.ui.stop.setEnabled(enable)
|
||||
self.ui.setpoint.setEnabled(enable)
|
||||
self.ui.step_size.setEnabled(enable)
|
||||
|
||||
@Property(str)
|
||||
def device(self):
|
||||
"""Property to set the device"""
|
||||
return self._device
|
||||
|
||||
@device.setter
|
||||
def device(self, value: str):
|
||||
"""Setter, checks if device is a string"""
|
||||
if not value or not isinstance(value, str):
|
||||
return
|
||||
if not self._check_device_is_valid(value):
|
||||
return
|
||||
old_device = self._device
|
||||
self._device = value
|
||||
if not self.label:
|
||||
self.label = value
|
||||
self.device_changed.emit(old_device, value)
|
||||
|
||||
@Property(bool)
|
||||
def hide_device_selection(self):
|
||||
"""Hide the device selection"""
|
||||
return not self.ui.tool_button.isVisible()
|
||||
|
||||
@hide_device_selection.setter
|
||||
def hide_device_selection(self, value: bool):
|
||||
"""Set the device selection visibility"""
|
||||
self.ui.tool_button.setVisible(not value)
|
||||
|
||||
@Slot(bool)
|
||||
def show_device_selection(self, value: bool):
|
||||
"""Show the device selection
|
||||
|
||||
Args:
|
||||
value (bool): Show the device selection
|
||||
"""
|
||||
self.hide_device_selection = not value
|
||||
|
||||
@Slot(str)
|
||||
def set_positioner(self, positioner: str | Positioner):
|
||||
"""Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
if isinstance(positioner, Positioner):
|
||||
positioner = positioner.name
|
||||
self.device = positioner
|
||||
|
||||
def _check_device_is_valid(self, device: str):
|
||||
"""Check if the device is a positioner
|
||||
|
||||
Args:
|
||||
device (str): The device name
|
||||
"""
|
||||
if device not in self.dev:
|
||||
logger.info(f"Device {device} not found in the device list")
|
||||
return False
|
||||
if not isinstance(self.dev[device], Positioner):
|
||||
logger.info(f"Device {device} is not a positioner")
|
||||
return False
|
||||
return True
|
||||
|
||||
@Slot(str, str)
|
||||
def on_device_change(self, old_device: str, new_device: str):
|
||||
"""Upon changing the device, a check will be performed if the device is a Positioner.
|
||||
|
||||
Args:
|
||||
old_device (str): The old device name.
|
||||
new_device (str): The new device name.
|
||||
"""
|
||||
if not self._check_device_is_valid(new_device):
|
||||
return
|
||||
logger.info(f"Device changed from {old_device} to {new_device}")
|
||||
self._toogle_enable_buttons(True)
|
||||
self.init_device()
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_device_readback, MessageEndpoints.device_readback(old_device)
|
||||
)
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_device_readback, MessageEndpoints.device_readback(new_device)
|
||||
)
|
||||
self.ui.device_box.setTitle(new_device)
|
||||
self.ui.readback.setToolTip(f"{self.device} readback")
|
||||
self.ui.setpoint.setToolTip(f"{self.device} setpoint")
|
||||
self.ui.step_size.setToolTip(f"Step size for {new_device}")
|
||||
|
||||
precision = self.dev[new_device].precision
|
||||
if precision is not None:
|
||||
self.ui.step_size.setDecimals(precision)
|
||||
self.ui.step_size.setValue(10**-precision * 10)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@Slot(dict, dict)
|
||||
def on_device_readback(self, msg_content: dict, metadata: dict):
|
||||
"""Callback for device readback.
|
||||
|
||||
Args:
|
||||
msg_content (dict): The message content.
|
||||
metadata (dict): The message metadata.
|
||||
"""
|
||||
signals = msg_content.get("signals", {})
|
||||
# pylint: disable=protected-access
|
||||
hinted_signals = self.dev[self.device]._hints
|
||||
precision = self.dev[self.device].precision
|
||||
|
||||
readback_val = None
|
||||
setpoint_val = None
|
||||
|
||||
if len(hinted_signals) == 1:
|
||||
signal = hinted_signals[0]
|
||||
readback_val = signals.get(signal, {}).get("value")
|
||||
|
||||
for setpoint_signal in ["setpoint", "user_setpoint"]:
|
||||
setpoint_val = signals.get(f"{self.device}_{setpoint_signal}", {}).get("value")
|
||||
if setpoint_val is not None:
|
||||
break
|
||||
|
||||
for moving_signal in ["motor_done_move", "motor_is_moving"]:
|
||||
is_moving = signals.get(f"{self.device}_{moving_signal}", {}).get("value")
|
||||
if is_moving is not None:
|
||||
break
|
||||
|
||||
if is_moving is not None:
|
||||
self.ui.spinner_widget.setVisible(True)
|
||||
if is_moving:
|
||||
self.ui.spinner_widget.start()
|
||||
self.ui.spinner_widget.setToolTip("Device is moving")
|
||||
self.set_global_state("warning")
|
||||
else:
|
||||
self.ui.spinner_widget.stop()
|
||||
self.ui.spinner_widget.setToolTip("Device is idle")
|
||||
self.set_global_state("success")
|
||||
else:
|
||||
self.ui.spinner_widget.setVisible(False)
|
||||
|
||||
if readback_val is not None:
|
||||
self.ui.readback.setText(f"{readback_val:.{precision}f}")
|
||||
self.position_update.emit(readback_val)
|
||||
|
||||
if setpoint_val is not None:
|
||||
self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")
|
||||
|
||||
limits = self.dev[self.device].limits
|
||||
self.update_limits(limits)
|
||||
if limits is not None and readback_val is not None and limits[0] != limits[1]:
|
||||
pos = (readback_val - limits[0]) / (limits[1] - limits[0])
|
||||
self.ui.position_indicator.set_value(pos)
|
||||
|
||||
def update_limits(self, limits: tuple):
|
||||
"""Update limits
|
||||
|
||||
Args:
|
||||
limits (tuple): Limits of the positioner
|
||||
"""
|
||||
if limits == self._limits:
|
||||
return
|
||||
self._limits = limits
|
||||
if limits is not None and limits[0] != limits[1]:
|
||||
self.ui.position_indicator.setToolTip(f"Min: {limits[0]}, Max: {limits[1]}")
|
||||
self.setpoint_validator.setRange(limits[0], limits[1])
|
||||
else:
|
||||
self.ui.position_indicator.setToolTip("No limits set")
|
||||
self.setpoint_validator.setRange(float("-inf"), float("inf"))
|
||||
|
||||
@Slot()
|
||||
def on_stop(self):
|
||||
"""Stop call"""
|
||||
request_id = str(uuid.uuid4())
|
||||
params = {
|
||||
"device": self.device,
|
||||
"rpc_id": request_id,
|
||||
"func": "stop",
|
||||
"args": [],
|
||||
"kwargs": {},
|
||||
}
|
||||
msg = ScanQueueMessage(
|
||||
scan_type="device_rpc",
|
||||
parameter=params,
|
||||
queue="emergency",
|
||||
metadata={"RID": request_id, "response": False},
|
||||
)
|
||||
self.client.connector.send(MessageEndpoints.scan_queue_request(), msg)
|
||||
|
||||
@property
|
||||
def step_size(self):
|
||||
"""Step size for tweak"""
|
||||
return self.ui.step_size.value()
|
||||
|
||||
@Slot()
|
||||
def on_tweak_right(self):
|
||||
"""Tweak motor right"""
|
||||
self.dev[self.device].move(self.step_size, relative=True)
|
||||
|
||||
@Slot()
|
||||
def on_tweak_left(self):
|
||||
"""Tweak motor left"""
|
||||
self.dev[self.device].move(-self.step_size, relative=True)
|
||||
|
||||
@Slot()
|
||||
def on_setpoint_change(self):
|
||||
"""Change the setpoint for the motor"""
|
||||
self.ui.setpoint.clearFocus()
|
||||
setpoint = self.ui.setpoint.text()
|
||||
self.dev[self.device].move(float(setpoint), relative=False)
|
||||
self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
|
||||
self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("dark")
|
||||
widget = PositionerBox(device="bpm4i")
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,242 +0,0 @@
|
||||
""" Module for a PositionerBox widget to control a positioner device."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtGui import QDoubleValidator
|
||||
from qtpy.QtWidgets import QDoubleSpinBox
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.colors import get_accent_colors, set_theme
|
||||
from bec_widgets.widgets.control.device_control.positioner_box._base import PositionerBoxBase
|
||||
from bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base import (
|
||||
DeviceUpdateUIComponents,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
|
||||
class PositionerBox(PositionerBoxBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
ui_file = "positioner_box.ui"
|
||||
dimensions = (234, 224)
|
||||
|
||||
PLUGIN = True
|
||||
|
||||
USER_ACCESS = ["set_positioner"]
|
||||
device_changed = Signal(str, str)
|
||||
# Signal emitted to inform listeners about a position update
|
||||
position_update = Signal(float)
|
||||
|
||||
def __init__(self, parent=None, device: Positioner | str | None = None, **kwargs):
|
||||
"""Initialize the PositionerBox widget.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
device (Positioner): The device to control.
|
||||
"""
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self._device = ""
|
||||
self._limits = None
|
||||
if self.current_path == "":
|
||||
self.current_path = os.path.dirname(__file__)
|
||||
|
||||
self.init_ui()
|
||||
self.device = device
|
||||
self._init_device(self.device, self.position_update.emit, self.update_limits)
|
||||
|
||||
def init_ui(self):
|
||||
"""Init the ui"""
|
||||
self.device_changed.connect(self.on_device_change)
|
||||
|
||||
self.ui = UILoader(self).loader(os.path.join(self.current_path, self.ui_file))
|
||||
|
||||
self.addWidget(self.ui)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# fix the size of the device box
|
||||
db = self.ui.device_box
|
||||
db.setFixedHeight(self.dimensions[0])
|
||||
db.setFixedWidth(self.dimensions[1])
|
||||
|
||||
self.ui.step_size.setStepType(QDoubleSpinBox.AdaptiveDecimalStepType)
|
||||
self.ui.stop.clicked.connect(self.on_stop)
|
||||
self.ui.stop.setToolTip("Stop")
|
||||
self.ui.stop.setStyleSheet(
|
||||
f"QPushButton {{background-color: {get_accent_colors().emergency.name()}; color: white;}}"
|
||||
)
|
||||
self.ui.tweak_right.clicked.connect(self.on_tweak_right)
|
||||
self.ui.tweak_right.setToolTip("Tweak right")
|
||||
self.ui.tweak_left.clicked.connect(self.on_tweak_left)
|
||||
self.ui.tweak_left.setToolTip("Tweak left")
|
||||
self.ui.setpoint.returnPressed.connect(self.on_setpoint_change)
|
||||
|
||||
self.setpoint_validator = QDoubleValidator()
|
||||
self.ui.setpoint.setValidator(self.setpoint_validator)
|
||||
self.ui.spinner_widget.start()
|
||||
self.ui.tool_button.clicked.connect(self._open_dialog_selection(self.set_positioner))
|
||||
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
|
||||
self.ui.tool_button.setIcon(icon)
|
||||
|
||||
def force_update_readback(self):
|
||||
self._init_device(self.device, self.position_update.emit, self.update_limits)
|
||||
|
||||
@SafeProperty(str)
|
||||
def device(self):
|
||||
"""Property to set the device"""
|
||||
return self._device
|
||||
|
||||
@device.setter
|
||||
def device(self, value: str):
|
||||
"""Setter, checks if device is a string"""
|
||||
if not value or not isinstance(value, str):
|
||||
return
|
||||
if not self._check_device_is_valid(value):
|
||||
return
|
||||
old_device = self._device
|
||||
self._device = value
|
||||
if not self.label:
|
||||
self.label = value
|
||||
self.device_changed.emit(old_device, value)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def hide_device_selection(self):
|
||||
"""Hide the device selection"""
|
||||
return not self.ui.tool_button.isVisible()
|
||||
|
||||
@hide_device_selection.setter
|
||||
def hide_device_selection(self, value: bool):
|
||||
"""Set the device selection visibility"""
|
||||
self.ui.tool_button.setVisible(not value)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def show_device_selection(self, value: bool):
|
||||
"""Show the device selection
|
||||
|
||||
Args:
|
||||
value (bool): Show the device selection
|
||||
"""
|
||||
self.hide_device_selection = not value
|
||||
|
||||
@SafeSlot(str)
|
||||
def set_positioner(self, positioner: str | Positioner):
|
||||
"""Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
if isinstance(positioner, Positioner):
|
||||
positioner = positioner.name
|
||||
self.device = positioner
|
||||
|
||||
@SafeSlot(str, str)
|
||||
def on_device_change(self, old_device: str, new_device: str):
|
||||
"""Upon changing the device, a check will be performed if the device is a Positioner.
|
||||
|
||||
Args:
|
||||
old_device (str): The old device name.
|
||||
new_device (str): The new device name.
|
||||
"""
|
||||
if not self._check_device_is_valid(new_device):
|
||||
return
|
||||
self._on_device_change(
|
||||
old_device,
|
||||
new_device,
|
||||
self.position_update.emit,
|
||||
self.update_limits,
|
||||
self.on_device_readback,
|
||||
self._device_ui_components(new_device),
|
||||
)
|
||||
|
||||
def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents:
|
||||
return {
|
||||
"spinner": self.ui.spinner_widget,
|
||||
"position_indicator": self.ui.position_indicator,
|
||||
"readback": self.ui.readback,
|
||||
"setpoint": self.ui.setpoint,
|
||||
"step_size": self.ui.step_size,
|
||||
"device_box": self.ui.device_box,
|
||||
"stop": self.ui.stop,
|
||||
"tweak_increase": self.ui.tweak_right,
|
||||
"tweak_decrease": self.ui.tweak_left,
|
||||
}
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_readback(self, msg_content: dict, metadata: dict):
|
||||
"""Callback for device readback.
|
||||
|
||||
Args:
|
||||
msg_content (dict): The message content.
|
||||
metadata (dict): The message metadata.
|
||||
"""
|
||||
self._on_device_readback(
|
||||
self.device,
|
||||
self._device_ui_components(self.device),
|
||||
msg_content,
|
||||
metadata,
|
||||
self.position_update.emit,
|
||||
self.update_limits,
|
||||
)
|
||||
|
||||
def update_limits(self, limits: tuple):
|
||||
"""Update limits
|
||||
|
||||
Args:
|
||||
limits (tuple): Limits of the positioner
|
||||
"""
|
||||
if limits == self._limits:
|
||||
return
|
||||
self._limits = limits
|
||||
self._update_limits_ui(limits, self.ui.position_indicator, self.setpoint_validator)
|
||||
|
||||
@SafeSlot()
|
||||
def on_stop(self):
|
||||
self._stop_device(self.device)
|
||||
|
||||
@property
|
||||
def step_size(self):
|
||||
"""Step size for tweak"""
|
||||
return self.ui.step_size.value()
|
||||
|
||||
@SafeSlot()
|
||||
def on_tweak_right(self):
|
||||
"""Tweak motor right"""
|
||||
self.dev[self.device].move(self.step_size, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_tweak_left(self):
|
||||
"""Tweak motor left"""
|
||||
self.dev[self.device].move(-self.step_size, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_setpoint_change(self):
|
||||
"""Change the setpoint for the motor"""
|
||||
self.ui.setpoint.clearFocus()
|
||||
setpoint = self.ui.setpoint.text()
|
||||
self.dev[self.device].move(float(setpoint), relative=False)
|
||||
self.ui.tweak_left.setToolTip(f"Tweak left by {self.step_size}")
|
||||
self.ui.tweak_right.setToolTip(f"Tweak right by {self.step_size}")
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("dark")
|
||||
widget = PositionerBox(device="bpm4i")
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['positioner_box_2d.py']}
|
||||
@@ -1,56 +0,0 @@
|
||||
# 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.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
|
||||
PositionerBox2D,
|
||||
)
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='PositionerBox2D' name='positioner_box2_d'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = PositionerBox2D(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Device Control"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(PositionerBox2D.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "positioner_box2_d"
|
||||
|
||||
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 "PositionerBox2D"
|
||||
|
||||
def toolTip(self):
|
||||
return "Simple Widget to control two positioners in box form"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -1,482 +0,0 @@
|
||||
""" Module for a PositionerBox2D widget to control two positioner devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtGui import QDoubleValidator
|
||||
from qtpy.QtWidgets import QDoubleSpinBox
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.widgets.control.device_control.positioner_box._base import PositionerBoxBase
|
||||
from bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base import (
|
||||
DeviceUpdateUIComponents,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
DeviceId = Literal["horizontal", "vertical"]
|
||||
|
||||
|
||||
class PositionerBox2D(PositionerBoxBase):
|
||||
"""Simple Widget to control two positioners in box form"""
|
||||
|
||||
ui_file = "positioner_box_2d.ui"
|
||||
|
||||
PLUGIN = True
|
||||
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
|
||||
|
||||
device_changed_hor = Signal(str, str)
|
||||
device_changed_ver = Signal(str, str)
|
||||
# Signals emitted to inform listeners about a position update
|
||||
position_update_hor = Signal(float)
|
||||
position_update_ver = Signal(float)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
device_hor: Positioner | str | None = None,
|
||||
device_ver: Positioner | str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the PositionerBox widget.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
device_hor (Positioner | str): The first device to control - assigned the horizontal axis.
|
||||
device_ver (Positioner | str): The second device to control - assigned the vertical axis.
|
||||
"""
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self._device_hor = ""
|
||||
self._device_ver = ""
|
||||
self._limits_hor = None
|
||||
self._limits_ver = None
|
||||
self._dialog = None
|
||||
if self.current_path == "":
|
||||
self.current_path = os.path.dirname(__file__)
|
||||
self.init_ui()
|
||||
self.device_hor = device_hor
|
||||
self.device_ver = device_ver
|
||||
|
||||
self.connect_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""Init the ui"""
|
||||
self.device_changed_hor.connect(self.on_device_change_hor)
|
||||
self.device_changed_ver.connect(self.on_device_change_ver)
|
||||
|
||||
self.ui = UILoader(self).loader(os.path.join(self.current_path, self.ui_file))
|
||||
self.setpoint_validator_hor = QDoubleValidator()
|
||||
self.setpoint_validator_ver = QDoubleValidator()
|
||||
|
||||
def connect_ui(self):
|
||||
"""Connect the UI components to signals, data, or routines"""
|
||||
self.addWidget(self.ui)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
def _init_ui(val: QDoubleValidator, device_id: DeviceId):
|
||||
ui = self._device_ui_components_hv(device_id)
|
||||
tweak_inc = (
|
||||
self.on_tweak_inc_hor if device_id == "horizontal" else self.on_tweak_inc_ver
|
||||
)
|
||||
tweak_dec = (
|
||||
self.on_tweak_dec_hor if device_id == "horizontal" else self.on_tweak_dec_ver
|
||||
)
|
||||
ui["setpoint"].setValidator(val)
|
||||
ui["setpoint"].returnPressed.connect(
|
||||
self.on_setpoint_change_hor
|
||||
if device_id == "horizontal"
|
||||
else self.on_setpoint_change_ver
|
||||
)
|
||||
ui["stop"].setToolTip("Stop")
|
||||
ui["step_size"].setStepType(QDoubleSpinBox.StepType.AdaptiveDecimalStepType)
|
||||
ui["tweak_increase"].clicked.connect(tweak_inc)
|
||||
ui["tweak_decrease"].clicked.connect(tweak_dec)
|
||||
|
||||
_init_ui(self.setpoint_validator_hor, "horizontal")
|
||||
_init_ui(self.setpoint_validator_ver, "vertical")
|
||||
|
||||
self.ui.stop_button.button.clicked.connect(self.on_stop)
|
||||
|
||||
self.ui.step_decrease_hor.clicked.connect(self.on_step_dec_hor)
|
||||
self.ui.step_decrease_ver.clicked.connect(self.on_step_dec_ver)
|
||||
self.ui.step_increase_hor.clicked.connect(self.on_step_inc_hor)
|
||||
self.ui.step_increase_ver.clicked.connect(self.on_step_inc_ver)
|
||||
|
||||
self.ui.tool_button_hor.clicked.connect(
|
||||
self._open_dialog_selection(self.set_positioner_hor)
|
||||
)
|
||||
self.ui.tool_button_ver.clicked.connect(
|
||||
self._open_dialog_selection(self.set_positioner_ver)
|
||||
)
|
||||
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
|
||||
self.ui.tool_button_hor.setIcon(icon)
|
||||
self.ui.tool_button_ver.setIcon(icon)
|
||||
|
||||
step_tooltip = "Step by the step size"
|
||||
tweak_tooltip = "Tweak by 1/10th the step size"
|
||||
|
||||
for b in [
|
||||
self.ui.step_increase_hor,
|
||||
self.ui.step_increase_ver,
|
||||
self.ui.step_decrease_hor,
|
||||
self.ui.step_decrease_ver,
|
||||
]:
|
||||
b.setToolTip(step_tooltip)
|
||||
|
||||
for b in [
|
||||
self.ui.tweak_increase_hor,
|
||||
self.ui.tweak_increase_ver,
|
||||
self.ui.tweak_decrease_hor,
|
||||
self.ui.tweak_decrease_ver,
|
||||
]:
|
||||
b.setToolTip(tweak_tooltip)
|
||||
|
||||
icon_options = {"size": (16, 16), "convert_to_pixmap": False}
|
||||
self.ui.tweak_increase_hor.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_right", **icon_options)
|
||||
)
|
||||
self.ui.step_increase_hor.setIcon(
|
||||
material_icon(icon_name="keyboard_double_arrow_right", **icon_options)
|
||||
)
|
||||
self.ui.tweak_decrease_hor.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_left", **icon_options)
|
||||
)
|
||||
self.ui.step_decrease_hor.setIcon(
|
||||
material_icon(icon_name="keyboard_double_arrow_left", **icon_options)
|
||||
)
|
||||
self.ui.tweak_increase_ver.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_up", **icon_options)
|
||||
)
|
||||
self.ui.step_increase_ver.setIcon(
|
||||
material_icon(icon_name="keyboard_double_arrow_up", **icon_options)
|
||||
)
|
||||
self.ui.tweak_decrease_ver.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_down", **icon_options)
|
||||
)
|
||||
self.ui.step_decrease_ver.setIcon(
|
||||
material_icon(icon_name="keyboard_double_arrow_down", **icon_options)
|
||||
)
|
||||
|
||||
@SafeProperty(str)
|
||||
def device_hor(self):
|
||||
"""SafeProperty to set the device"""
|
||||
return self._device_hor
|
||||
|
||||
@device_hor.setter
|
||||
def device_hor(self, value: str):
|
||||
"""Setter, checks if device is a string"""
|
||||
if not value or not isinstance(value, str):
|
||||
return
|
||||
if not self._check_device_is_valid(value):
|
||||
return
|
||||
if value == self.device_ver:
|
||||
return
|
||||
old_device = self._device_hor
|
||||
self._device_hor = value
|
||||
self.label = f"{self._device_hor}, {self._device_ver}"
|
||||
self.device_changed_hor.emit(old_device, value)
|
||||
self._init_device(self.device_hor, self.position_update_hor.emit, self.update_limits_hor)
|
||||
|
||||
@SafeProperty(str)
|
||||
def device_ver(self):
|
||||
"""SafeProperty to set the device"""
|
||||
return self._device_ver
|
||||
|
||||
@device_ver.setter
|
||||
def device_ver(self, value: str):
|
||||
"""Setter, checks if device is a string"""
|
||||
if not value or not isinstance(value, str):
|
||||
return
|
||||
if not self._check_device_is_valid(value):
|
||||
return
|
||||
if value == self.device_hor:
|
||||
return
|
||||
old_device = self._device_ver
|
||||
self._device_ver = value
|
||||
self.label = f"{self._device_hor}, {self._device_ver}"
|
||||
self.device_changed_ver.emit(old_device, value)
|
||||
self._init_device(self.device_ver, self.position_update_ver.emit, self.update_limits_ver)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def hide_device_selection(self):
|
||||
"""Hide the device selection"""
|
||||
return not self.ui.tool_button_hor.isVisible()
|
||||
|
||||
@hide_device_selection.setter
|
||||
def hide_device_selection(self, value: bool):
|
||||
"""Set the device selection visibility"""
|
||||
self.ui.tool_button_hor.setVisible(not value)
|
||||
self.ui.tool_button_ver.setVisible(not value)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def hide_device_boxes(self):
|
||||
"""Hide the device selection"""
|
||||
return not self.ui.device_box_hor.isVisible()
|
||||
|
||||
@hide_device_boxes.setter
|
||||
def hide_device_boxes(self, value: bool):
|
||||
"""Set the device selection visibility"""
|
||||
self.ui.device_box_hor.setVisible(not value)
|
||||
self.ui.device_box_ver.setVisible(not value)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def show_device_selection(self, value: bool):
|
||||
"""Show the device selection
|
||||
|
||||
Args:
|
||||
value (bool): Show the device selection
|
||||
"""
|
||||
self.hide_device_selection = not value
|
||||
|
||||
@SafeSlot(str)
|
||||
def set_positioner_hor(self, positioner: str | Positioner):
|
||||
"""Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
if isinstance(positioner, Positioner):
|
||||
positioner = positioner.name
|
||||
self.device_hor = positioner
|
||||
|
||||
@SafeSlot(str)
|
||||
def set_positioner_ver(self, positioner: str | Positioner):
|
||||
"""Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
if isinstance(positioner, Positioner):
|
||||
positioner = positioner.name
|
||||
self.device_ver = positioner
|
||||
|
||||
@SafeSlot(str, str)
|
||||
def on_device_change_hor(self, old_device: str, new_device: str):
|
||||
"""Upon changing the device, a check will be performed if the device is a Positioner.
|
||||
|
||||
Args:
|
||||
old_device (str): The old device name.
|
||||
new_device (str): The new device name.
|
||||
"""
|
||||
if not self._check_device_is_valid(new_device):
|
||||
return
|
||||
self._on_device_change(
|
||||
old_device,
|
||||
new_device,
|
||||
self.position_update_hor.emit,
|
||||
self.update_limits_hor,
|
||||
self.on_device_readback_hor,
|
||||
self._device_ui_components_hv("horizontal"),
|
||||
)
|
||||
|
||||
@SafeSlot(str, str)
|
||||
def on_device_change_ver(self, old_device: str, new_device: str):
|
||||
"""Upon changing the device, a check will be performed if the device is a Positioner.
|
||||
|
||||
Args:
|
||||
old_device (str): The old device name.
|
||||
new_device (str): The new device name.
|
||||
"""
|
||||
if not self._check_device_is_valid(new_device):
|
||||
return
|
||||
self._on_device_change(
|
||||
old_device,
|
||||
new_device,
|
||||
self.position_update_ver.emit,
|
||||
self.update_limits_ver,
|
||||
self.on_device_readback_ver,
|
||||
self._device_ui_components_hv("vertical"),
|
||||
)
|
||||
|
||||
def _device_ui_components_hv(self, device: DeviceId) -> DeviceUpdateUIComponents:
|
||||
if device == "horizontal":
|
||||
return {
|
||||
"spinner": self.ui.spinner_widget_hor,
|
||||
"position_indicator": self.ui.position_indicator_hor,
|
||||
"readback": self.ui.readback_hor,
|
||||
"setpoint": self.ui.setpoint_hor,
|
||||
"step_size": self.ui.step_size_hor,
|
||||
"device_box": self.ui.device_box_hor,
|
||||
"stop": self.ui.stop_button,
|
||||
"tweak_increase": self.ui.tweak_increase_hor,
|
||||
"tweak_decrease": self.ui.tweak_decrease_hor,
|
||||
}
|
||||
elif device == "vertical":
|
||||
return {
|
||||
"spinner": self.ui.spinner_widget_ver,
|
||||
"position_indicator": self.ui.position_indicator_ver,
|
||||
"readback": self.ui.readback_ver,
|
||||
"setpoint": self.ui.setpoint_ver,
|
||||
"step_size": self.ui.step_size_ver,
|
||||
"device_box": self.ui.device_box_ver,
|
||||
"stop": self.ui.stop_button,
|
||||
"tweak_increase": self.ui.tweak_increase_ver,
|
||||
"tweak_decrease": self.ui.tweak_decrease_ver,
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Device {device} is not represented by this UI")
|
||||
|
||||
def _device_ui_components(self, device: str):
|
||||
if device == self.device_hor:
|
||||
return self._device_ui_components_hv("horizontal")
|
||||
if device == self.device_ver:
|
||||
return self._device_ui_components_hv("vertical")
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_readback_hor(self, msg_content: dict, metadata: dict):
|
||||
"""Callback for device readback.
|
||||
|
||||
Args:
|
||||
msg_content (dict): The message content.
|
||||
metadata (dict): The message metadata.
|
||||
"""
|
||||
self._on_device_readback(
|
||||
self.device_hor,
|
||||
self._device_ui_components_hv("horizontal"),
|
||||
msg_content,
|
||||
metadata,
|
||||
self.position_update_hor.emit,
|
||||
self.update_limits_hor,
|
||||
)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_readback_ver(self, msg_content: dict, metadata: dict):
|
||||
"""Callback for device readback.
|
||||
|
||||
Args:
|
||||
msg_content (dict): The message content.
|
||||
metadata (dict): The message metadata.
|
||||
"""
|
||||
self._on_device_readback(
|
||||
self.device_ver,
|
||||
self._device_ui_components_hv("vertical"),
|
||||
msg_content,
|
||||
metadata,
|
||||
self.position_update_ver.emit,
|
||||
self.update_limits_ver,
|
||||
)
|
||||
|
||||
def update_limits_hor(self, limits: tuple):
|
||||
"""Update limits
|
||||
|
||||
Args:
|
||||
limits (tuple): Limits of the positioner
|
||||
"""
|
||||
if limits == self._limits_hor:
|
||||
return
|
||||
self._limits_hor = limits
|
||||
self._update_limits_ui(limits, self.ui.position_indicator_hor, self.setpoint_validator_hor)
|
||||
|
||||
def update_limits_ver(self, limits: tuple):
|
||||
"""Update limits
|
||||
|
||||
Args:
|
||||
limits (tuple): Limits of the positioner
|
||||
"""
|
||||
if limits == self._limits_ver:
|
||||
return
|
||||
self._limits_ver = limits
|
||||
self._update_limits_ui(limits, self.ui.position_indicator_ver, self.setpoint_validator_ver)
|
||||
|
||||
@SafeSlot()
|
||||
def on_stop(self):
|
||||
self._stop_device(f"{self.device_hor} or {self.device_ver}")
|
||||
|
||||
@SafeProperty(float)
|
||||
def step_size_hor(self):
|
||||
"""Step size for tweak"""
|
||||
return self.ui.step_size_hor.value()
|
||||
|
||||
@step_size_hor.setter
|
||||
def step_size_hor(self, val: float):
|
||||
"""Step size for tweak"""
|
||||
self.ui.step_size_hor.setValue(val)
|
||||
|
||||
@SafeProperty(float)
|
||||
def step_size_ver(self):
|
||||
"""Step size for tweak"""
|
||||
return self.ui.step_size_ver.value()
|
||||
|
||||
@step_size_ver.setter
|
||||
def step_size_ver(self, val: float):
|
||||
"""Step size for tweak"""
|
||||
self.ui.step_size_ver.setValue(val)
|
||||
|
||||
@SafeSlot()
|
||||
def on_tweak_inc_hor(self):
|
||||
"""Tweak device a up"""
|
||||
self.dev[self.device_hor].move(self.step_size_hor / 10, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_tweak_dec_hor(self):
|
||||
"""Tweak device a down"""
|
||||
self.dev[self.device_hor].move(-self.step_size_hor / 10, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_step_inc_hor(self):
|
||||
"""Tweak device a up"""
|
||||
self.dev[self.device_hor].move(self.step_size_hor, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_step_dec_hor(self):
|
||||
"""Tweak device a down"""
|
||||
self.dev[self.device_hor].move(-self.step_size_hor, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_tweak_inc_ver(self):
|
||||
"""Tweak device a up"""
|
||||
self.dev[self.device_ver].move(self.step_size_ver / 10, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_tweak_dec_ver(self):
|
||||
"""Tweak device b down"""
|
||||
self.dev[self.device_ver].move(-self.step_size_ver / 10, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_step_inc_ver(self):
|
||||
"""Tweak device b up"""
|
||||
self.dev[self.device_ver].move(self.step_size_ver, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_step_dec_ver(self):
|
||||
"""Tweak device a down"""
|
||||
self.dev[self.device_ver].move(-self.step_size_ver, relative=True)
|
||||
|
||||
@SafeSlot()
|
||||
def on_setpoint_change_hor(self):
|
||||
"""Change the setpoint for device a"""
|
||||
self.ui.setpoint_hor.clearFocus()
|
||||
setpoint = self.ui.setpoint_hor.text()
|
||||
self.dev[self.device_hor].move(float(setpoint), relative=False)
|
||||
|
||||
@SafeSlot()
|
||||
def on_setpoint_change_ver(self):
|
||||
"""Change the setpoint for device b"""
|
||||
self.ui.setpoint_ver.clearFocus()
|
||||
setpoint = self.ui.setpoint_ver.text()
|
||||
self.dev[self.device_ver].move(float(setpoint), relative=False)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("dark")
|
||||
widget = PositionerBox2D()
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,562 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>326</width>
|
||||
<height>323</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="0" column="1">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="2">
|
||||
<widget class="QGroupBox" name="device_box_ver">
|
||||
<property name="title">
|
||||
<string>No positioner selected</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_6" rowstretch="0,0,0,0,0,0">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QToolButton" name="tool_button_ver">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="readback_ver">
|
||||
<property name="text">
|
||||
<string>Position</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SpinnerWidget" name="spinner_widget_ver">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLineEdit" name="setpoint_ver">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="step_size_ver">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QGroupBox" name="device_box_hor">
|
||||
<property name="title">
|
||||
<string>No positioner selected</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5" rowstretch="0,0,0,0,0,0,0,0,0,0,0">
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLineEdit" name="setpoint_hor">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QToolButton" name="tool_button_hor">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="readback_hor">
|
||||
<property name="text">
|
||||
<string>Position</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SpinnerWidget" name="spinner_widget_hor">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||
<item>
|
||||
<widget class="QDoubleSpinBox" name="step_size_hor">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::StrongFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="PositionIndicator" name="position_indicator_ver">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximum" stdset="0">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="vertical" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="value" stdset="0">
|
||||
<double>0.500000000000000</double>
|
||||
</property>
|
||||
<property name="indicator_width" stdset="0">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rounded_corners" stdset="0">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="3">
|
||||
<widget class="QPushButton" name="step_increase_ver">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="5">
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="6" column="3">
|
||||
<widget class="QPushButton" name="step_decrease_ver">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="5">
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<spacer name="horizontalSpacer_16">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<spacer name="horizontalSpacer_14">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<spacer name="horizontalSpacer_17">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<spacer name="horizontalSpacer_7">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<spacer name="horizontalSpacer_6">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="5">
|
||||
<widget class="QPushButton" name="step_increase_hor">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="4">
|
||||
<spacer name="horizontalSpacer_10">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QPushButton" name="tweak_decrease_hor">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<spacer name="horizontalSpacer_15">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<spacer name="horizontalSpacer_35">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="QPushButton" name="tweak_decrease_ver">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="step_decrease_hor">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="4">
|
||||
<widget class="QPushButton" name="tweak_increase_hor">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QPushButton" name="tweak_increase_ver">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::FocusPolicy::NoFocus</enum>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<spacer name="horizontalSpacer_12">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<spacer name="horizontalSpacer_8">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="4">
|
||||
<spacer name="horizontalSpacer_11">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="5">
|
||||
<spacer name="horizontalSpacer_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="6" column="5">
|
||||
<widget class="StopButton" name="stop_button"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="PositionIndicator" name="position_indicator_hor">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximum" stdset="0">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<property name="value" stdset="0">
|
||||
<double>0.500000000000000</double>
|
||||
</property>
|
||||
<property name="indicator_width" stdset="0">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rounded_corners" stdset="0">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>StopButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>stop_button</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PositionIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>position_indicator</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SpinnerWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>spinner_widget</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>tool_button_hor</tabstop>
|
||||
<tabstop>tool_button_ver</tabstop>
|
||||
<tabstop>setpoint_hor</tabstop>
|
||||
<tabstop>setpoint_ver</tabstop>
|
||||
<tabstop>step_size_hor</tabstop>
|
||||
<tabstop>step_size_ver</tabstop>
|
||||
<tabstop>tweak_decrease_hor</tabstop>
|
||||
<tabstop>tweak_increase_ver</tabstop>
|
||||
<tabstop>tweak_increase_hor</tabstop>
|
||||
<tabstop>tweak_decrease_ver</tabstop>
|
||||
<tabstop>step_decrease_hor</tabstop>
|
||||
<tabstop>step_increase_ver</tabstop>
|
||||
<tabstop>step_increase_hor</tabstop>
|
||||
<tabstop>step_decrease_ver</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,17 +0,0 @@
|
||||
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.control.device_control.positioner_box.positioner_box_2d.positioner_box2_d_plugin import (
|
||||
PositionerBox2DPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerBox2DPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box import PositionerBox
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
@@ -1,8 +1,6 @@
|
||||
import os
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box import PositionerBox
|
||||
|
||||
|
||||
class PositionerControlLine(PositionerBox):
|
||||
@@ -14,14 +12,13 @@ class PositionerControlLine(PositionerBox):
|
||||
PLUGIN = True
|
||||
ICON_NAME = "switch_left"
|
||||
|
||||
def __init__(self, parent=None, device: Positioner | str | None = None, *args, **kwargs):
|
||||
def __init__(self, parent=None, device: Positioner = None, *args, **kwargs):
|
||||
"""Initialize the DeviceControlLine.
|
||||
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
device (Positioner): The device to control.
|
||||
"""
|
||||
self.current_path = os.path.dirname(__file__)
|
||||
super().__init__(parent=parent, device=device, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import os
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerControlLine
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line import (
|
||||
PositionerControlLine,
|
||||
)
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
@@ -6,7 +6,7 @@ def main(): # pragma: no cover
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box_plugin import (
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_plugin import (
|
||||
PositionerBoxPlugin,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ def main(): # pragma: no cover
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line_plugin import (
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line_plugin import (
|
||||
PositionerControlLinePlugin,
|
||||
)
|
||||
|
||||
@@ -4,12 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from bec_lib.device import Positioner
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QSize, Signal
|
||||
from qtpy.QtCore import Property, QSize, Signal, Slot
|
||||
from qtpy.QtWidgets import QGridLayout, QGroupBox, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box import PositionerBox
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -33,7 +32,7 @@ class PositionerGroupBox(QGroupBox):
|
||||
self.widget.position_update.connect(self._on_position_update)
|
||||
self.widget.expand.connect(self._on_expand)
|
||||
self.setTitle(self.device_name)
|
||||
self.widget.force_update_readback()
|
||||
self.widget.init_device() # force readback
|
||||
|
||||
def _on_expand(self, expand):
|
||||
if expand:
|
||||
@@ -83,7 +82,7 @@ class PositionerGroup(BECWidget, QWidget):
|
||||
def minimumSizeHint(self):
|
||||
return QSize(300, 30)
|
||||
|
||||
@SafeSlot(str)
|
||||
@Slot(str)
|
||||
def set_positioners(self, device_names: str):
|
||||
"""Redraw grid with positioners from device_names string
|
||||
|
||||
@@ -131,7 +130,7 @@ class PositionerGroup(BECWidget, QWidget):
|
||||
widget = self.sender()
|
||||
self.device_position_update.emit(widget.title(), pos)
|
||||
|
||||
@SafeProperty(str)
|
||||
@Property(str)
|
||||
def devices_list(self):
|
||||
"""Device names string separated by space"""
|
||||
return " ".join(self._device_widgets)
|
||||
@@ -145,7 +144,7 @@ class PositionerGroup(BECWidget, QWidget):
|
||||
return
|
||||
self.set_positioners(device_names)
|
||||
|
||||
@SafeProperty(int)
|
||||
@Property(int)
|
||||
def grid_max_cols(self):
|
||||
"""Max number of columns for widgets grid"""
|
||||
return self._grid_ncols
|
||||
|
||||
@@ -6,9 +6,11 @@ def main(): # pragma: no cover
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.games.minesweeper_plugin import MinesweeperPlugin
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox_plugin import (
|
||||
SignalComboBoxPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(MinesweeperPlugin())
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(SignalComboBoxPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['signal_combobox.py']}
|
||||
@@ -4,36 +4,36 @@
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.games.minesweeper import Minesweeper
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='Minesweeper' name='minesweeper'>
|
||||
<widget class='SignalComboBox' name='signal_combobox'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class MinesweeperPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = Minesweeper(parent)
|
||||
t = SignalComboBox(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Games"
|
||||
return "BEC Input Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(Minesweeper.ICON_NAME)
|
||||
return designer_material_icon(SignalComboBox.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "minesweeper"
|
||||
return "signal_combobox"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
@@ -45,10 +45,10 @@ class MinesweeperPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "Minesweeper"
|
||||
return "SignalComboBox"
|
||||
|
||||
def toolTip(self):
|
||||
return "Minesweeper"
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -1,10 +1,9 @@
|
||||
import os
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QPushButton, QTreeWidgetItem, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
@@ -44,8 +43,6 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
"""
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("LMFitDialog")
|
||||
self._ui_file = ui_file
|
||||
self.target_widget = target_widget
|
||||
|
||||
@@ -68,7 +65,7 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
|
||||
@property
|
||||
def enable_actions(self) -> bool:
|
||||
"""SafeProperty to enable the move to buttons."""
|
||||
"""Property to enable the move to buttons."""
|
||||
return self._enable_actions
|
||||
|
||||
@enable_actions.setter
|
||||
@@ -77,37 +74,37 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
for button in self.action_buttons.values():
|
||||
button.setEnabled(enable)
|
||||
|
||||
@SafeProperty(list)
|
||||
@Property(list)
|
||||
def active_action_list(self) -> list[str]:
|
||||
"""SafeProperty to list the names of the fit parameters for which actions should be enabled."""
|
||||
"""Property to list the names of the fit parameters for which actions should be enabled."""
|
||||
return self._active_actions
|
||||
|
||||
@active_action_list.setter
|
||||
def active_action_list(self, actions: list[str]):
|
||||
self._active_actions = actions
|
||||
|
||||
# This SafeSlot needed?
|
||||
@SafeSlot(bool)
|
||||
# This slot needed?
|
||||
@Slot(bool)
|
||||
def set_actions_enabled(self, enable: bool) -> bool:
|
||||
"""SafeSlot to enable the move to buttons.
|
||||
"""Slot to enable the move to buttons.
|
||||
|
||||
Args:
|
||||
enable (bool): Whether to enable the action buttons.
|
||||
"""
|
||||
self.enable_actions = enable
|
||||
|
||||
@SafeProperty(bool)
|
||||
@Property(bool)
|
||||
def always_show_latest(self):
|
||||
"""SafeProperty to indicate if always the latest DAP update is displayed."""
|
||||
"""Property to indicate if always the latest DAP update is displayed."""
|
||||
return self._always_show_latest
|
||||
|
||||
@always_show_latest.setter
|
||||
def always_show_latest(self, show: bool):
|
||||
self._always_show_latest = show
|
||||
|
||||
@SafeProperty(bool)
|
||||
@Property(bool)
|
||||
def hide_curve_selection(self):
|
||||
"""SafeProperty for showing the curve selection."""
|
||||
"""Property for showing the curve selection."""
|
||||
return not self.ui.group_curve_selection.isVisible()
|
||||
|
||||
@hide_curve_selection.setter
|
||||
@@ -119,9 +116,9 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
"""
|
||||
self.ui.group_curve_selection.setVisible(not show)
|
||||
|
||||
@SafeProperty(bool)
|
||||
@Property(bool)
|
||||
def hide_summary(self) -> bool:
|
||||
"""SafeProperty for showing the summary."""
|
||||
"""Property for showing the summary."""
|
||||
return not self.ui.group_summary.isVisible()
|
||||
|
||||
@hide_summary.setter
|
||||
@@ -133,9 +130,9 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
"""
|
||||
self.ui.group_summary.setVisible(not show)
|
||||
|
||||
@SafeProperty(bool)
|
||||
@Property(bool)
|
||||
def hide_parameters(self) -> bool:
|
||||
"""SafeProperty for showing the parameters."""
|
||||
"""Property for showing the parameters."""
|
||||
return not self.ui.group_parameters.isVisible()
|
||||
|
||||
@hide_parameters.setter
|
||||
@@ -149,7 +146,7 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
|
||||
@property
|
||||
def fit_curve_id(self) -> str:
|
||||
"""SafeProperty for the currently displayed fit curve_id."""
|
||||
"""Property for the currently displayed fit curve_id."""
|
||||
return self._fit_curve_id
|
||||
|
||||
@fit_curve_id.setter
|
||||
@@ -162,7 +159,7 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
self._fit_curve_id = curve_id
|
||||
self.selected_fit.emit(curve_id)
|
||||
|
||||
@SafeSlot(str)
|
||||
@Slot(str)
|
||||
def remove_dap_data(self, curve_id: str):
|
||||
"""Remove the DAP data for the given curve_id.
|
||||
|
||||
@@ -172,7 +169,7 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
self.summary_data.pop(curve_id, None)
|
||||
self.refresh_curve_list()
|
||||
|
||||
@SafeSlot(str)
|
||||
@Slot(str)
|
||||
def select_curve(self, curve_id: str):
|
||||
"""Select active curve_id in the curve list.
|
||||
|
||||
@@ -181,7 +178,7 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
"""
|
||||
self.fit_curve_id = curve_id
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
@Slot(dict, dict)
|
||||
def update_summary_tree(self, data: dict, metadata: dict):
|
||||
"""Update the summary tree with the given data.
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from bec_widgets.widgets.games.minesweeper import Minesweeper
|
||||
|
||||
__ALL__ = ["Minesweeper"]
|
||||
@@ -1,413 +0,0 @@
|
||||
import enum
|
||||
import random
|
||||
import time
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import QSize, Qt, QTimer, Signal, Slot
|
||||
from qtpy.QtGui import QBrush, QColor, QPainter, QPen
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
NUM_COLORS = {
|
||||
1: QColor("#f44336"),
|
||||
2: QColor("#9C27B0"),
|
||||
3: QColor("#3F51B5"),
|
||||
4: QColor("#03A9F4"),
|
||||
5: QColor("#00BCD4"),
|
||||
6: QColor("#4CAF50"),
|
||||
7: QColor("#E91E63"),
|
||||
8: QColor("#FF9800"),
|
||||
}
|
||||
|
||||
LEVELS: dict[str, tuple[int, int]] = {"1": (8, 10), "2": (16, 40), "3": (24, 99)}
|
||||
|
||||
|
||||
class GameStatus(enum.Enum):
|
||||
READY = 0
|
||||
PLAYING = 1
|
||||
FAILED = 2
|
||||
SUCCESS = 3
|
||||
|
||||
|
||||
class Pos(QWidget):
|
||||
expandable = Signal(int, int)
|
||||
clicked = Signal()
|
||||
ohno = Signal()
|
||||
|
||||
def __init__(self, x, y, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setFixedSize(QSize(20, 20))
|
||||
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_start = False
|
||||
self.is_mine = False
|
||||
self.adjacent_n = 0
|
||||
self.is_revealed = False
|
||||
self.is_flagged = False
|
||||
|
||||
def reset(self):
|
||||
"""Restore the tile to its original state before mine status is assigned"""
|
||||
self.is_start = False
|
||||
self.is_mine = False
|
||||
self.adjacent_n = 0
|
||||
|
||||
self.is_revealed = False
|
||||
self.is_flagged = False
|
||||
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
|
||||
r = event.rect()
|
||||
|
||||
if self.is_revealed:
|
||||
color = self.palette().base().color()
|
||||
outer, inner = color, color
|
||||
else:
|
||||
outer, inner = (self.palette().highlightedText().color(), self.palette().text().color())
|
||||
|
||||
p.fillRect(r, QBrush(inner))
|
||||
pen = QPen(outer)
|
||||
pen.setWidth(1)
|
||||
p.setPen(pen)
|
||||
p.drawRect(r)
|
||||
|
||||
if self.is_revealed:
|
||||
if self.is_mine:
|
||||
p.drawPixmap(r, material_icon("experiment", convert_to_pixmap=True, filled=True))
|
||||
|
||||
elif self.adjacent_n > 0:
|
||||
pen = QPen(NUM_COLORS[self.adjacent_n])
|
||||
p.setPen(pen)
|
||||
f = p.font()
|
||||
f.setBold(True)
|
||||
p.setFont(f)
|
||||
p.drawText(r, Qt.AlignHCenter | Qt.AlignVCenter, str(self.adjacent_n))
|
||||
|
||||
elif self.is_flagged:
|
||||
p.drawPixmap(
|
||||
r,
|
||||
material_icon(
|
||||
"flag",
|
||||
size=(50, 50),
|
||||
convert_to_pixmap=True,
|
||||
filled=True,
|
||||
color=self.palette().base().color(),
|
||||
),
|
||||
)
|
||||
p.end()
|
||||
|
||||
def flag(self):
|
||||
self.is_flagged = not self.is_flagged
|
||||
self.update()
|
||||
|
||||
self.clicked.emit()
|
||||
|
||||
def reveal(self):
|
||||
self.is_revealed = True
|
||||
self.update()
|
||||
|
||||
def click(self):
|
||||
if not self.is_revealed:
|
||||
self.reveal()
|
||||
if self.adjacent_n == 0:
|
||||
self.expandable.emit(self.x, self.y)
|
||||
|
||||
self.clicked.emit()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.RightButton and not self.is_revealed:
|
||||
self.flag()
|
||||
return
|
||||
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.click()
|
||||
if self.is_mine:
|
||||
self.ohno.emit()
|
||||
|
||||
|
||||
class Minesweeper(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "videogame_asset"
|
||||
USER_ACCESS = []
|
||||
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
self._ui_initialised = False
|
||||
self._timer_start_num_seconds = 0
|
||||
self._set_level_params(LEVELS["1"])
|
||||
|
||||
self._init_ui()
|
||||
self._init_map()
|
||||
|
||||
self.update_status(GameStatus.READY)
|
||||
self.reset_map()
|
||||
self.update_status(GameStatus.READY)
|
||||
|
||||
def _init_ui(self):
|
||||
if self._ui_initialised:
|
||||
return
|
||||
self._ui_initialised = True
|
||||
|
||||
status_hb = QHBoxLayout()
|
||||
self.mines = QLabel()
|
||||
self.mines.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
||||
f = self.mines.font()
|
||||
f.setPointSize(24)
|
||||
self.mines.setFont(f)
|
||||
|
||||
self.reset_button = QPushButton()
|
||||
self.reset_button.setFixedSize(QSize(32, 32))
|
||||
self.reset_button.setIconSize(QSize(32, 32))
|
||||
self.reset_button.setFlat(True)
|
||||
self.reset_button.pressed.connect(self.reset_button_pressed)
|
||||
|
||||
self.clock = QLabel()
|
||||
self.clock.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
||||
self.clock.setFont(f)
|
||||
self._timer = QTimer()
|
||||
self._timer.timeout.connect(self.update_timer)
|
||||
self._timer.start(1000) # 1 second timer
|
||||
self.mines.setText(f"{self.num_mines:03d}")
|
||||
self.clock.setText("000")
|
||||
|
||||
status_hb.addWidget(self.mines)
|
||||
status_hb.addWidget(self.reset_button)
|
||||
status_hb.addWidget(self.clock)
|
||||
|
||||
level_hb = QHBoxLayout()
|
||||
self.level_selector = QComboBox()
|
||||
self.level_selector.addItems(list(LEVELS.keys()))
|
||||
level_hb.addWidget(QLabel("Level: "))
|
||||
level_hb.addWidget(self.level_selector)
|
||||
self.level_selector.currentTextChanged.connect(self.change_level)
|
||||
|
||||
vb = QVBoxLayout()
|
||||
vb.addLayout(level_hb)
|
||||
vb.addLayout(status_hb)
|
||||
|
||||
self.grid = QGridLayout()
|
||||
self.grid.setSpacing(5)
|
||||
|
||||
vb.addLayout(self.grid)
|
||||
self.setLayout(vb)
|
||||
|
||||
def _init_map(self):
|
||||
"""Redraw the grid of mines"""
|
||||
|
||||
# Remove any previous grid items and reset the grid
|
||||
for i in reversed(range(self.grid.count())):
|
||||
w: Pos = self.grid.itemAt(i).widget()
|
||||
w.clicked.disconnect(self.on_click)
|
||||
w.expandable.disconnect(self.expand_reveal)
|
||||
w.ohno.disconnect(self.game_over)
|
||||
w.setParent(None)
|
||||
w.deleteLater()
|
||||
|
||||
# Add positions to the map
|
||||
for x in range(0, self.b_size):
|
||||
for y in range(0, self.b_size):
|
||||
w = Pos(x, y)
|
||||
self.grid.addWidget(w, y, x)
|
||||
# Connect signal to handle expansion.
|
||||
w.clicked.connect(self.on_click)
|
||||
w.expandable.connect(self.expand_reveal)
|
||||
w.ohno.connect(self.game_over)
|
||||
|
||||
def reset_map(self):
|
||||
"""
|
||||
Reset the map and add new mines.
|
||||
"""
|
||||
# Clear all mine positions
|
||||
for x in range(0, self.b_size):
|
||||
for y in range(0, self.b_size):
|
||||
w = self.grid.itemAtPosition(y, x).widget()
|
||||
w.reset()
|
||||
|
||||
# Add mines to the positions
|
||||
positions = []
|
||||
while len(positions) < self.num_mines:
|
||||
x, y = (random.randint(0, self.b_size - 1), random.randint(0, self.b_size - 1))
|
||||
if (x, y) not in positions:
|
||||
w = self.grid.itemAtPosition(y, x).widget()
|
||||
w.is_mine = True
|
||||
positions.append((x, y))
|
||||
|
||||
def get_adjacency_n(x, y):
|
||||
positions = self.get_surrounding(x, y)
|
||||
num_mines = sum(1 if w.is_mine else 0 for w in positions)
|
||||
|
||||
return num_mines
|
||||
|
||||
# Add adjacencies to the positions
|
||||
for x in range(0, self.b_size):
|
||||
for y in range(0, self.b_size):
|
||||
w = self.grid.itemAtPosition(y, x).widget()
|
||||
w.adjacent_n = get_adjacency_n(x, y)
|
||||
|
||||
# Place starting marker
|
||||
while True:
|
||||
x, y = (random.randint(0, self.b_size - 1), random.randint(0, self.b_size - 1))
|
||||
w = self.grid.itemAtPosition(y, x).widget()
|
||||
# We don't want to start on a mine.
|
||||
if (x, y) not in positions:
|
||||
w = self.grid.itemAtPosition(y, x).widget()
|
||||
w.is_start = True
|
||||
|
||||
# Reveal all positions around this, if they are not mines either.
|
||||
for w in self.get_surrounding(x, y):
|
||||
if not w.is_mine:
|
||||
w.click()
|
||||
break
|
||||
|
||||
def get_surrounding(self, x, y):
|
||||
positions = []
|
||||
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
|
||||
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
|
||||
positions.append(self.grid.itemAtPosition(yi, xi).widget())
|
||||
return positions
|
||||
|
||||
def get_num_hidden(self) -> int:
|
||||
"""
|
||||
Get the number of hidden positions.
|
||||
"""
|
||||
return sum(
|
||||
1
|
||||
for x in range(0, self.b_size)
|
||||
for y in range(0, self.b_size)
|
||||
if not self.grid.itemAtPosition(y, x).widget().is_revealed
|
||||
)
|
||||
|
||||
def get_num_remaining_flags(self) -> int:
|
||||
"""
|
||||
Get the number of remaining flags.
|
||||
"""
|
||||
return self.num_mines - sum(
|
||||
1
|
||||
for x in range(0, self.b_size)
|
||||
for y in range(0, self.b_size)
|
||||
if self.grid.itemAtPosition(y, x).widget().is_flagged
|
||||
)
|
||||
|
||||
def reset_button_pressed(self):
|
||||
match self.status:
|
||||
case GameStatus.PLAYING:
|
||||
self.game_over()
|
||||
case GameStatus.FAILED | GameStatus.SUCCESS:
|
||||
self.reset_map()
|
||||
|
||||
def reveal_map(self):
|
||||
for x in range(0, self.b_size):
|
||||
for y in range(0, self.b_size):
|
||||
w = self.grid.itemAtPosition(y, x).widget()
|
||||
w.reveal()
|
||||
|
||||
@Slot(str)
|
||||
def change_level(self, level: str):
|
||||
self._set_level_params(LEVELS[level])
|
||||
self._init_map()
|
||||
self.reset_map()
|
||||
|
||||
@Slot(int, int)
|
||||
def expand_reveal(self, x, y):
|
||||
"""
|
||||
Expand the reveal to the surrounding
|
||||
|
||||
Args:
|
||||
x (int): The x position.
|
||||
y (int): The y position.
|
||||
"""
|
||||
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
|
||||
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
|
||||
w = self.grid.itemAtPosition(yi, xi).widget()
|
||||
if not w.is_mine:
|
||||
w.click()
|
||||
|
||||
@Slot()
|
||||
def on_click(self):
|
||||
"""
|
||||
Handle the click event. If the game is not started, start the game.
|
||||
"""
|
||||
self.update_available_flags()
|
||||
if self.status != GameStatus.PLAYING:
|
||||
# First click.
|
||||
self.update_status(GameStatus.PLAYING)
|
||||
# Start timer.
|
||||
self._timer_start_num_seconds = int(time.time())
|
||||
return
|
||||
self.check_win()
|
||||
|
||||
def update_available_flags(self):
|
||||
"""
|
||||
Update the number of available flags.
|
||||
"""
|
||||
self.mines.setText(f"{self.get_num_remaining_flags():03d}")
|
||||
|
||||
def check_win(self):
|
||||
"""
|
||||
Check if the game is won.
|
||||
"""
|
||||
if self.get_num_hidden() == self.num_mines:
|
||||
self.update_status(GameStatus.SUCCESS)
|
||||
|
||||
def update_status(self, status: GameStatus):
|
||||
"""
|
||||
Update the status of the game.
|
||||
|
||||
Args:
|
||||
status (GameStatus): The status of the game.
|
||||
"""
|
||||
self.status = status
|
||||
match status:
|
||||
case GameStatus.READY:
|
||||
icon = material_icon(icon_name="add", convert_to_pixmap=False)
|
||||
case GameStatus.PLAYING:
|
||||
icon = material_icon(icon_name="smart_toy", convert_to_pixmap=False)
|
||||
case GameStatus.FAILED:
|
||||
icon = material_icon(icon_name="error", convert_to_pixmap=False)
|
||||
case GameStatus.SUCCESS:
|
||||
icon = material_icon(icon_name="celebration", convert_to_pixmap=False)
|
||||
self.reset_button.setIcon(icon)
|
||||
|
||||
def update_timer(self):
|
||||
"""
|
||||
Update the timer.
|
||||
"""
|
||||
if self.status == GameStatus.PLAYING:
|
||||
num_seconds = int(time.time()) - self._timer_start_num_seconds
|
||||
self.clock.setText(f"{num_seconds:03d}")
|
||||
|
||||
def game_over(self):
|
||||
"""Cause the game to end early"""
|
||||
self.reveal_map()
|
||||
self.update_status(GameStatus.FAILED)
|
||||
|
||||
def _set_level_params(self, level: tuple[int, int]):
|
||||
self.b_size, self.num_mines = level
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("light")
|
||||
widget = Minesweeper()
|
||||
widget.show()
|
||||
|
||||
app.exec_()
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['minesweeper.py']}
|
||||
@@ -190,7 +190,7 @@ class BECImageWidget(BECWidget, QWidget):
|
||||
###################################
|
||||
# User Access Methods from image
|
||||
###################################
|
||||
@SafeSlot(popup_error=True)
|
||||
@SafeSlot(popup_error=False)
|
||||
def image(
|
||||
self,
|
||||
monitor: str,
|
||||
|
||||
@@ -1,571 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from qtpy.QtCore import QPoint, QPointF, Qt, Signal
|
||||
from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.qt_utils.round_frame import RoundedFrame
|
||||
from bec_widgets.qt_utils.side_panel import SidePanel
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, SeparatorAction
|
||||
from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.utils.fps_counter import FPSCounter
|
||||
from bec_widgets.utils.widget_state_manager import WidgetStateManager
|
||||
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
||||
from bec_widgets.widgets.plots_next_gen.setting_menus.axis_settings import AxisSettings
|
||||
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions import (
|
||||
MouseInteractionToolbarBundle,
|
||||
)
|
||||
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.plot_export import PlotExportBundle
|
||||
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.save_state import SaveStateBundle
|
||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class BECViewBox(pg.ViewBox):
|
||||
sigPaint = Signal()
|
||||
|
||||
def paint(self, painter, opt, widget):
|
||||
super().paint(painter, opt, widget)
|
||||
self.sigPaint.emit()
|
||||
|
||||
def itemBoundsChanged(self, item):
|
||||
self._itemBoundsCache.pop(item, None)
|
||||
if (self.state["autoRange"][0] is not False) or (self.state["autoRange"][1] is not False):
|
||||
# check if the call is coming from a mouse-move event
|
||||
if hasattr(item, "skip_auto_range") and item.skip_auto_range:
|
||||
return
|
||||
self._autoRangeNeedsUpdate = True
|
||||
self.update()
|
||||
|
||||
|
||||
class PlotBase(BECWidget, QWidget):
|
||||
PLUGIN = False
|
||||
RPC = False
|
||||
|
||||
# Custom Signals
|
||||
property_changed = Signal(str, object)
|
||||
crosshair_position_changed = Signal(tuple)
|
||||
crosshair_position_clicked = Signal(tuple)
|
||||
crosshair_coordinates_changed = Signal(tuple)
|
||||
crosshair_coordinates_clicked = Signal(tuple)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
config: ConnectionConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("PlotBase")
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
# Layout Management
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout_manager = LayoutManagerWidget(parent=self)
|
||||
|
||||
# Property Manager
|
||||
self.state_manager = WidgetStateManager(self)
|
||||
|
||||
# Entry Validator
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
# Base widgets elements
|
||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
|
||||
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
||||
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||
self.init_toolbar()
|
||||
|
||||
# PlotItem Addons
|
||||
self.plot_item.addLegend()
|
||||
self.crosshair = None
|
||||
self.fps_monitor = None
|
||||
self.fps_label = QLabel(alignment=Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui(self):
|
||||
self.layout.addWidget(self.layout_manager)
|
||||
self.round_plot_widget = RoundedFrame(content_widget=self.plot_widget, theme_update=True)
|
||||
self.round_plot_widget.apply_theme("dark")
|
||||
|
||||
self.layout_manager.add_widget(self.round_plot_widget)
|
||||
self.layout_manager.add_widget_relative(self.fps_label, self.round_plot_widget, "top")
|
||||
self.fps_label.hide()
|
||||
self.layout_manager.add_widget_relative(self.side_panel, self.round_plot_widget, "left")
|
||||
self.layout_manager.add_widget_relative(self.toolbar, self.fps_label, "top")
|
||||
|
||||
self.add_side_menus()
|
||||
|
||||
# PlotItem ViewBox Signals
|
||||
self.plot_item.vb.sigStateChanged.connect(self.viewbox_state_changed)
|
||||
|
||||
def init_toolbar(self):
|
||||
|
||||
self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
|
||||
self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
|
||||
self.state_export_bundle = SaveStateBundle("state_export", target_widget=self)
|
||||
|
||||
# Add elements to toolbar
|
||||
self.toolbar.add_bundle(self.plot_export_bundle, target_widget=self)
|
||||
self.toolbar.add_bundle(self.state_export_bundle, target_widget=self)
|
||||
self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
|
||||
|
||||
self.toolbar.add_action("separator_0", SeparatorAction(), target_widget=self)
|
||||
self.toolbar.add_action(
|
||||
"crosshair",
|
||||
MaterialIconAction(icon_name="point_scan", tooltip="Show Crosshair", checkable=True),
|
||||
target_widget=self,
|
||||
)
|
||||
self.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self)
|
||||
self.toolbar.add_action(
|
||||
"fps_monitor",
|
||||
MaterialIconAction(icon_name="speed", tooltip="Show FPS Monitor", checkable=True),
|
||||
target_widget=self,
|
||||
)
|
||||
self.toolbar.addWidget(DarkModeButton(toolbar=True))
|
||||
|
||||
self.toolbar.widgets["fps_monitor"].action.toggled.connect(
|
||||
lambda checked: setattr(self, "enable_fps_monitor", checked)
|
||||
)
|
||||
self.toolbar.widgets["crosshair"].action.toggled.connect(self.toggle_crosshair)
|
||||
|
||||
def add_side_menus(self):
|
||||
"""Adds multiple menus to the side panel."""
|
||||
# Setting Axis Widget
|
||||
axis_setting = AxisSettings(target_widget=self)
|
||||
self.side_panel.add_menu(
|
||||
action_id="axis",
|
||||
icon_name="settings",
|
||||
tooltip="Show Axis Settings",
|
||||
widget=axis_setting,
|
||||
title="Axis Settings",
|
||||
)
|
||||
|
||||
################################################################################
|
||||
# Toggle UI Elements
|
||||
################################################################################
|
||||
|
||||
@SafeProperty(bool, doc="Show Toolbar")
|
||||
def enable_toolbar(self) -> bool:
|
||||
return self.toolbar.isVisible()
|
||||
|
||||
@enable_toolbar.setter
|
||||
def enable_toolbar(self, value: bool):
|
||||
self.toolbar.setVisible(value)
|
||||
|
||||
@SafeProperty(bool, doc="Show Side Panel")
|
||||
def enable_side_panel(self) -> bool:
|
||||
return self.side_panel.isVisible()
|
||||
|
||||
@enable_side_panel.setter
|
||||
def enable_side_panel(self, value: bool):
|
||||
self.side_panel.setVisible(value)
|
||||
|
||||
@SafeProperty(bool, doc="Enable the FPS monitor.")
|
||||
def enable_fps_monitor(self) -> bool:
|
||||
return self.fps_label.isVisible()
|
||||
|
||||
@enable_fps_monitor.setter
|
||||
def enable_fps_monitor(self, value: bool):
|
||||
if value and self.fps_monitor is None:
|
||||
self.hook_fps_monitor()
|
||||
elif not value and self.fps_monitor is not None:
|
||||
self.unhook_fps_monitor()
|
||||
|
||||
################################################################################
|
||||
# ViewBox State Signals
|
||||
################################################################################
|
||||
|
||||
def viewbox_state_changed(self):
|
||||
"""
|
||||
Emit a signal when the state of the viewbox has changed.
|
||||
Merges the default pyqtgraphs signal states and also CTRL menu toggles.
|
||||
"""
|
||||
|
||||
viewbox_state = self.plot_item.vb.getState()
|
||||
# Range Limits
|
||||
x_min, x_max = viewbox_state["targetRange"][0]
|
||||
y_min, y_max = viewbox_state["targetRange"][1]
|
||||
self.property_changed.emit("x_min", x_min)
|
||||
self.property_changed.emit("x_max", x_max)
|
||||
self.property_changed.emit("y_min", y_min)
|
||||
self.property_changed.emit("y_max", y_max)
|
||||
|
||||
# Grid Toggles
|
||||
|
||||
################################################################################
|
||||
# Plot Properties
|
||||
################################################################################
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
|
||||
"""
|
||||
property_map = {
|
||||
"title": self.title,
|
||||
"x_label": self.x_label,
|
||||
"y_label": self.y_label,
|
||||
"x_limits": self.x_limits,
|
||||
"y_limits": self.y_limits,
|
||||
"x_grid": self.x_grid,
|
||||
"y_grid": self.y_grid,
|
||||
"inner_axes": self.inner_axes,
|
||||
"outer_axes": self.outer_axes,
|
||||
"lock_aspect_ratio": self.lock_aspect_ratio,
|
||||
"auto_range_x": self.auto_range_x,
|
||||
"auto_range_y": self.auto_range_y,
|
||||
"x_log": self.x_log,
|
||||
"y_log": self.y_log,
|
||||
"legend_label_size": self.legend_label_size,
|
||||
}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key in property_map:
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
logger.warning(f"Property {key} not found.")
|
||||
|
||||
@SafeProperty(str, doc="The title of the axes.")
|
||||
def title(self) -> str:
|
||||
return self.plot_item.titleLabel.text
|
||||
|
||||
@title.setter
|
||||
def title(self, value: str):
|
||||
self.plot_item.setTitle(value)
|
||||
self.property_changed.emit("title", value)
|
||||
|
||||
@SafeProperty(str, doc="The text of the x label")
|
||||
def x_label(self) -> str:
|
||||
return self.plot_item.getAxis("bottom").labelText
|
||||
|
||||
@x_label.setter
|
||||
def x_label(self, value: str):
|
||||
self.plot_item.setLabel("bottom", text=value)
|
||||
self.property_changed.emit("x_label", value)
|
||||
|
||||
@SafeProperty(str, doc="The text of the y label")
|
||||
def y_label(self) -> str:
|
||||
return self.plot_item.getAxis("left").labelText
|
||||
|
||||
@y_label.setter
|
||||
def y_label(self, value: str):
|
||||
self.plot_item.setLabel("left", text=value)
|
||||
self.property_changed.emit("y_label", value)
|
||||
|
||||
def _tuple_to_qpointf(self, tuple: tuple | list):
|
||||
"""
|
||||
Helper function to convert a tuple to a QPointF.
|
||||
|
||||
Args:
|
||||
tuple(tuple|list): Tuple or list of two numbers.
|
||||
|
||||
Returns:
|
||||
QPointF: The tuple converted to a QPointF.
|
||||
"""
|
||||
if len(tuple) != 2:
|
||||
raise ValueError("Limits must be a tuple or list of two numbers.")
|
||||
min_val, max_val = tuple
|
||||
if not isinstance(min_val, (int, float)) or not isinstance(max_val, (int, float)):
|
||||
raise TypeError("Limits must be numbers.")
|
||||
if min_val > max_val:
|
||||
raise ValueError("Minimum limit cannot be greater than maximum limit.")
|
||||
return QPoint(*tuple)
|
||||
|
||||
################################################################################
|
||||
# X limits, has to be SaveProperty("QPointF") because of the tuple conversion for designer,
|
||||
# the python properties are used for CLI and API for context dialog settings.
|
||||
|
||||
@SafeProperty("QPointF")
|
||||
def x_limits(self) -> QPointF:
|
||||
current_lim = self.plot_item.vb.viewRange()[0]
|
||||
return QPointF(current_lim[0], current_lim[1])
|
||||
|
||||
@x_limits.setter
|
||||
def x_limits(self, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
value = self._tuple_to_qpointf(value)
|
||||
self.plot_item.vb.setXRange(value.x(), value.y(), padding=0)
|
||||
|
||||
@property
|
||||
def x_lim(self) -> tuple:
|
||||
return (self.x_limits.x(), self.x_limits.y())
|
||||
|
||||
@x_lim.setter
|
||||
def x_lim(self, value):
|
||||
self.x_limits = value
|
||||
|
||||
@property
|
||||
def x_min(self) -> float:
|
||||
return self.x_limits.x()
|
||||
|
||||
@x_min.setter
|
||||
def x_min(self, value: float):
|
||||
self.x_limits = (value, self.x_lim[1])
|
||||
|
||||
@property
|
||||
def x_max(self) -> float:
|
||||
return self.x_limits.y()
|
||||
|
||||
@x_max.setter
|
||||
def x_max(self, value: float):
|
||||
self.x_limits = (self.x_lim[0], value)
|
||||
|
||||
################################################################################
|
||||
# Y limits, has to be SaveProperty("QPointF") because of the tuple conversion for designer,
|
||||
# the python properties are used for CLI and API for context dialog settings.
|
||||
|
||||
@SafeProperty("QPointF")
|
||||
def y_limits(self) -> QPointF:
|
||||
current_lim = self.plot_item.vb.viewRange()[1]
|
||||
return QPointF(current_lim[0], current_lim[1])
|
||||
|
||||
@y_limits.setter
|
||||
def y_limits(self, value):
|
||||
if isinstance(value, (tuple, list)):
|
||||
value = self._tuple_to_qpointf(value)
|
||||
self.plot_item.vb.setYRange(value.x(), value.y(), padding=0)
|
||||
|
||||
@property
|
||||
def y_lim(self) -> tuple:
|
||||
return (self.y_limits.x(), self.y_limits.y())
|
||||
|
||||
@y_lim.setter
|
||||
def y_lim(self, value):
|
||||
self.y_limits = value
|
||||
|
||||
@property
|
||||
def y_min(self) -> float:
|
||||
return self.y_limits.x()
|
||||
|
||||
@y_min.setter
|
||||
def y_min(self, value: float):
|
||||
self.y_limits = (value, self.y_lim[1])
|
||||
|
||||
@property
|
||||
def y_max(self) -> float:
|
||||
return self.y_limits.y()
|
||||
|
||||
@y_max.setter
|
||||
def y_max(self, value: float):
|
||||
self.y_limits = (self.y_lim[0], value)
|
||||
|
||||
@SafeProperty(bool, doc="Show grid on the x-axis.")
|
||||
def x_grid(self) -> bool:
|
||||
return self.plot_item.ctrl.xGridCheck.isChecked()
|
||||
|
||||
@x_grid.setter
|
||||
def x_grid(self, value: bool):
|
||||
self.plot_item.showGrid(x=value)
|
||||
self.property_changed.emit("x_grid", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show grid on the y-axis.")
|
||||
def y_grid(self) -> bool:
|
||||
return self.plot_item.ctrl.yGridCheck.isChecked()
|
||||
|
||||
@y_grid.setter
|
||||
def y_grid(self, value: bool):
|
||||
self.plot_item.showGrid(y=value)
|
||||
self.property_changed.emit("y_grid", value)
|
||||
|
||||
@SafeProperty(bool, doc="Set X-axis to log scale if True, linear if False.")
|
||||
def x_log(self) -> bool:
|
||||
return bool(self.plot_item.vb.state.get("logMode", [False, False])[0])
|
||||
|
||||
@x_log.setter
|
||||
def x_log(self, value: bool):
|
||||
self.plot_item.setLogMode(x=value)
|
||||
self.property_changed.emit("x_log", value)
|
||||
|
||||
@SafeProperty(bool, doc="Set Y-axis to log scale if True, linear if False.")
|
||||
def y_log(self) -> bool:
|
||||
return bool(self.plot_item.vb.state.get("logMode", [False, False])[1])
|
||||
|
||||
@y_log.setter
|
||||
def y_log(self, value: bool):
|
||||
self.plot_item.setLogMode(y=value)
|
||||
self.property_changed.emit("y_log", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show the outer axes of the plot widget.")
|
||||
def outer_axes(self) -> bool:
|
||||
return self.plot_item.getAxis("top").isVisible()
|
||||
|
||||
@outer_axes.setter
|
||||
def outer_axes(self, value: bool):
|
||||
self.plot_item.showAxis("top", value)
|
||||
self.plot_item.showAxis("right", value)
|
||||
self.property_changed.emit("outer_axes", value)
|
||||
|
||||
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
|
||||
def inner_axes(self) -> bool:
|
||||
return self.plot_item.getAxis("bottom").isVisible()
|
||||
|
||||
@inner_axes.setter
|
||||
def inner_axes(self, value: bool):
|
||||
self.plot_item.showAxis("bottom", value)
|
||||
self.plot_item.showAxis("left", value)
|
||||
self.property_changed.emit("inner_axes", value)
|
||||
|
||||
@SafeProperty(bool, doc="Lock aspect ratio of the plot widget.")
|
||||
def lock_aspect_ratio(self) -> bool:
|
||||
return bool(self.plot_item.vb.getState()["aspectLocked"])
|
||||
|
||||
@lock_aspect_ratio.setter
|
||||
def lock_aspect_ratio(self, value: bool):
|
||||
self.plot_item.setAspectLocked(value)
|
||||
|
||||
@SafeProperty(bool, doc="Set auto range for the x-axis.")
|
||||
def auto_range_x(self) -> bool:
|
||||
return bool(self.plot_item.vb.getState()["autoRange"][0])
|
||||
|
||||
@auto_range_x.setter
|
||||
def auto_range_x(self, value: bool):
|
||||
self.plot_item.enableAutoRange(x=value)
|
||||
|
||||
@SafeProperty(bool, doc="Set auto range for the y-axis.")
|
||||
def auto_range_y(self) -> bool:
|
||||
return bool(self.plot_item.vb.getState()["autoRange"][1])
|
||||
|
||||
@auto_range_y.setter
|
||||
def auto_range_y(self, value: bool):
|
||||
self.plot_item.enableAutoRange(y=value)
|
||||
|
||||
@SafeProperty(int, doc="The font size of the legend font.")
|
||||
def legend_label_size(self) -> int:
|
||||
if not self.plot_item.legend:
|
||||
return
|
||||
scale = self.plot_item.legend.scale() * 9
|
||||
return scale
|
||||
|
||||
@legend_label_size.setter
|
||||
def legend_label_size(self, value: int):
|
||||
if not self.plot_item.legend:
|
||||
return
|
||||
scale = (
|
||||
value / 9
|
||||
) # 9 is the default font size of the legend, so we always scale it against 9
|
||||
self.plot_item.legend.setScale(scale)
|
||||
|
||||
################################################################################
|
||||
# FPS Counter
|
||||
################################################################################
|
||||
|
||||
def update_fps_label(self, fps: float) -> None:
|
||||
"""
|
||||
Update the FPS label.
|
||||
|
||||
Args:
|
||||
fps(float): The frames per second.
|
||||
"""
|
||||
if self.fps_label:
|
||||
self.fps_label.setText(f"FPS: {fps:.2f}")
|
||||
|
||||
def hook_fps_monitor(self):
|
||||
"""Hook the FPS monitor to the plot."""
|
||||
if self.fps_monitor is None:
|
||||
self.fps_monitor = FPSCounter(self.plot_item.vb)
|
||||
self.fps_label.show()
|
||||
|
||||
self.fps_monitor.sigFpsUpdate.connect(self.update_fps_label)
|
||||
self.update_fps_label(0)
|
||||
|
||||
def unhook_fps_monitor(self, delete_label=True):
|
||||
"""Unhook the FPS monitor from the plot."""
|
||||
if self.fps_monitor is not None and delete_label:
|
||||
# Remove Monitor
|
||||
self.fps_monitor.cleanup()
|
||||
self.fps_monitor.deleteLater()
|
||||
self.fps_monitor = None
|
||||
if self.fps_label is not None:
|
||||
# Hide Label
|
||||
self.fps_label.hide()
|
||||
|
||||
################################################################################
|
||||
# Crosshair
|
||||
################################################################################
|
||||
|
||||
def hook_crosshair(self) -> None:
|
||||
"""Hook the crosshair to all plots."""
|
||||
if self.crosshair is None:
|
||||
self.crosshair = Crosshair(self.plot_item, precision=3)
|
||||
self.crosshair.crosshairChanged.connect(self.crosshair_position_changed)
|
||||
self.crosshair.crosshairClicked.connect(self.crosshair_position_clicked)
|
||||
self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked1D.connect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.coordinatesChanged2D.connect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked2D.connect(self.crosshair_coordinates_clicked)
|
||||
|
||||
def unhook_crosshair(self) -> None:
|
||||
"""Unhook the crosshair from all plots."""
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.crosshairChanged.disconnect(self.crosshair_position_changed)
|
||||
self.crosshair.crosshairClicked.disconnect(self.crosshair_position_clicked)
|
||||
self.crosshair.coordinatesChanged1D.disconnect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked1D.disconnect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.coordinatesChanged2D.disconnect(self.crosshair_coordinates_changed)
|
||||
self.crosshair.coordinatesClicked2D.disconnect(self.crosshair_coordinates_clicked)
|
||||
self.crosshair.cleanup()
|
||||
self.crosshair.deleteLater()
|
||||
self.crosshair = None
|
||||
|
||||
def toggle_crosshair(self) -> None:
|
||||
"""Toggle the crosshair on all plots."""
|
||||
if self.crosshair is None:
|
||||
return self.hook_crosshair()
|
||||
|
||||
self.unhook_crosshair()
|
||||
|
||||
@SafeSlot()
|
||||
def reset(self) -> None:
|
||||
"""Reset the plot widget."""
|
||||
if self.crosshair is not None:
|
||||
self.crosshair.clear_markers()
|
||||
self.crosshair.update_markers()
|
||||
|
||||
def cleanup(self):
|
||||
self.unhook_crosshair()
|
||||
self.unhook_fps_monitor(delete_label=True)
|
||||
self.cleanup_pyqtgraph()
|
||||
|
||||
def cleanup_pyqtgraph(self):
|
||||
"""Cleanup pyqtgraph items."""
|
||||
item = self.plot_item
|
||||
item.vb.menu.close()
|
||||
item.vb.menu.deleteLater()
|
||||
item.ctrlMenu.close()
|
||||
item.ctrlMenu.deleteLater()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover:
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("dark")
|
||||
widget = PlotBase()
|
||||
widget.show()
|
||||
# Just some example data and parameters to test
|
||||
widget.y_grid = True
|
||||
widget.plot_item.plot([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,95 +0,0 @@
|
||||
import os
|
||||
|
||||
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingWidget
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class AxisSettings(SettingWidget):
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
# This is a settings widget that depends on the target widget
|
||||
# and should mirror what is in the target widget.
|
||||
# Saving settings for this widget could result in recursively setting the target widget.
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("AxisSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
form = UILoader().load_ui(os.path.join(current_path, "axis_settings_vertical.ui"), self)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
# # Scroll area
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShape(QFrame.NoFrame)
|
||||
self.scroll_area.setWidget(form)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.scroll_area)
|
||||
# self.layout.addWidget(self.ui)
|
||||
self.ui = form
|
||||
|
||||
self.connect_all_signals()
|
||||
if self.target_widget is not None:
|
||||
self.target_widget.property_changed.connect(self.update_property)
|
||||
|
||||
def connect_all_signals(self):
|
||||
for widget in [
|
||||
self.ui.title,
|
||||
self.ui.inner_axes,
|
||||
self.ui.outer_axes,
|
||||
self.ui.x_label,
|
||||
self.ui.x_min,
|
||||
self.ui.x_max,
|
||||
self.ui.x_log,
|
||||
self.ui.x_grid,
|
||||
self.ui.y_label,
|
||||
self.ui.y_min,
|
||||
self.ui.y_max,
|
||||
self.ui.y_log,
|
||||
self.ui.y_grid,
|
||||
]:
|
||||
WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||
|
||||
@SafeSlot()
|
||||
def set_property(self, widget: QWidget, value):
|
||||
"""
|
||||
Set property of the target widget based on the widget that emitted the signal.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget that emitted the signal.
|
||||
value(): The value to set the property to.
|
||||
"""
|
||||
|
||||
try: # to avoid crashing when the widget is not found in Designer
|
||||
property_name = widget.objectName()
|
||||
setattr(self.target_widget, property_name, value)
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
@SafeSlot()
|
||||
def update_property(self, property_name: str, value):
|
||||
"""
|
||||
Update the value of the widget based on the property name and value.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
property_name(str): The name of the property to update.
|
||||
value: The value to set the property to.
|
||||
"""
|
||||
try: # to avoid crashing when the widget is not found in Designer
|
||||
widget_to_set = self.ui.findChild(QWidget, property_name)
|
||||
except RuntimeError:
|
||||
return
|
||||
# Block signals to avoid triggering set_property again
|
||||
was_blocked = widget_to_set.blockSignals(True)
|
||||
WidgetIO.set_value(widget_to_set, value)
|
||||
widget_to_set.blockSignals(was_blocked)
|
||||
@@ -1,256 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>427</width>
|
||||
<height>270</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>250</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>278</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="plot_title"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_outer_axes">
|
||||
<property name="text">
|
||||
<string>Outer Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QGroupBox" name="y_axis_box">
|
||||
<property name="title">
|
||||
<string>Y Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="y_scale">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>log</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="y_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="y_label"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="y_scale_label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="y_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="y_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="y_grid">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="y_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="x_axis_box">
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="x_scale_label">
|
||||
<property name="text">
|
||||
<string>Scale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="x_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="x_scale">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>linear</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>log</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="x_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="x_label"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="x_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="x_grid">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="x_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="ToggleSwitch" name="switch_outer_axes">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,240 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>241</width>
|
||||
<height>526</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="x_axis_box">
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="x_scale_label">
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="x_label"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="x_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="ToggleSwitch" name="x_log">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="x_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="x_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="x_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="x_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="ToggleSwitch" name="x_grid">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="title"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_outer_axes">
|
||||
<property name="text">
|
||||
<string>Outer Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="y_axis_box">
|
||||
<property name="title">
|
||||
<string>Y Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QLabel" name="y_min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="y_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-9999.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>9999.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="y_label"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="y_scale_label">
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="y_label_label">
|
||||
<property name="text">
|
||||
<string>Label</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="y_max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="y_grid_label">
|
||||
<property name="text">
|
||||
<string>Grid</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="ToggleSwitch" name="y_log">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="ToggleSwitch" name="y_grid">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="ToggleSwitch" name="outer_axes">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Inner Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="ToggleSwitch" name="inner_axes"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,88 +0,0 @@
|
||||
import pyqtgraph as pg
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||
|
||||
|
||||
class MouseInteractionToolbarBundle(ToolbarBundle):
|
||||
"""
|
||||
A bundle of actions that are hooked in this constructor itself,
|
||||
so that you can immediately connect the signals and toggle states.
|
||||
|
||||
This bundle is for a toolbar that controls mouse interactions on a plot.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||
self.target_widget = target_widget
|
||||
|
||||
# Create each MaterialIconAction with a parent
|
||||
# so the signals can fire even if the toolbar isn't added yet.
|
||||
drag = MaterialIconAction(
|
||||
icon_name="drag_pan",
|
||||
tooltip="Drag Mouse Mode",
|
||||
checkable=True,
|
||||
parent=self.target_widget, # or any valid parent
|
||||
)
|
||||
rect = MaterialIconAction(
|
||||
icon_name="frame_inspect",
|
||||
tooltip="Rectangle Zoom Mode",
|
||||
checkable=True,
|
||||
parent=self.target_widget,
|
||||
)
|
||||
auto = MaterialIconAction(
|
||||
icon_name="open_in_full",
|
||||
tooltip="Autorange Plot",
|
||||
checkable=False,
|
||||
parent=self.target_widget,
|
||||
)
|
||||
aspect_ratio = MaterialIconAction(
|
||||
icon_name="aspect_ratio",
|
||||
tooltip="Lock image aspect ratio",
|
||||
checkable=True,
|
||||
parent=self.target_widget,
|
||||
)
|
||||
|
||||
# Add them to the bundle
|
||||
self.add_action("drag_mode", drag)
|
||||
self.add_action("rectangle_mode", rect)
|
||||
self.add_action("auto_range", auto)
|
||||
self.add_action("aspect_ratio", aspect_ratio)
|
||||
|
||||
# Immediately connect signals
|
||||
drag.action.toggled.connect(self.enable_mouse_pan_mode)
|
||||
rect.action.toggled.connect(self.enable_mouse_rectangle_mode)
|
||||
auto.action.triggered.connect(self.autorange_plot)
|
||||
aspect_ratio.action.toggled.connect(self.lock_aspect_ratio)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def enable_mouse_rectangle_mode(self, checked: bool):
|
||||
"""
|
||||
Enable the rectangle zoom mode on the plot widget.
|
||||
"""
|
||||
self.actions["drag_mode"].action.setChecked(not checked)
|
||||
if self.target_widget and checked:
|
||||
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.RectMode)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def enable_mouse_pan_mode(self, checked: bool):
|
||||
"""
|
||||
Enable the pan mode on the plot widget.
|
||||
"""
|
||||
self.actions["rectangle_mode"].action.setChecked(not checked)
|
||||
if self.target_widget and checked:
|
||||
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.PanMode)
|
||||
|
||||
@SafeSlot()
|
||||
def autorange_plot(self):
|
||||
"""
|
||||
Enable autorange on the plot widget.
|
||||
"""
|
||||
if self.target_widget:
|
||||
self.target_widget.auto_range_x = True
|
||||
self.target_widget.auto_range_y = True
|
||||
|
||||
@SafeSlot(bool)
|
||||
def lock_aspect_ratio(self, checked: bool):
|
||||
if self.target_widget:
|
||||
self.target_widget.lock_aspect_ratio = checked
|
||||
@@ -1,63 +0,0 @@
|
||||
from pyqtgraph.exporters import MatplotlibExporter
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||
|
||||
|
||||
class PlotExportBundle(ToolbarBundle):
|
||||
"""
|
||||
A bundle of actions that are hooked in this constructor itself,
|
||||
so that you can immediately connect the signals and toggle states.
|
||||
|
||||
This bundle is for a toolbar that controls exporting a plot.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||
self.target_widget = target_widget
|
||||
|
||||
# Create each MaterialIconAction with a parent
|
||||
# so the signals can fire even if the toolbar isn't added yet.
|
||||
save = MaterialIconAction(
|
||||
icon_name="save", tooltip="Open Export Dialog", parent=self.target_widget
|
||||
)
|
||||
matplotlib = MaterialIconAction(
|
||||
icon_name="photo_library", tooltip="Open Matplotlib Dialog", parent=self.target_widget
|
||||
)
|
||||
|
||||
# Add them to the bundle
|
||||
self.add_action("save", save)
|
||||
self.add_action("matplotlib", matplotlib)
|
||||
|
||||
# Immediately connect signals
|
||||
save.action.triggered.connect(self.export_dialog)
|
||||
matplotlib.action.triggered.connect(self.matplotlib_dialog)
|
||||
|
||||
@SafeSlot()
|
||||
def export_dialog(self):
|
||||
"""
|
||||
Open the export dialog for the plot widget.
|
||||
"""
|
||||
if self.target_widget:
|
||||
scene = self.target_widget.plot_item.scene()
|
||||
scene.contextMenuItem = self.target_widget.plot_item
|
||||
scene.showExportDialog()
|
||||
|
||||
@SafeSlot()
|
||||
def matplotlib_dialog(self):
|
||||
"""
|
||||
Export the plot widget to Matplotlib.
|
||||
"""
|
||||
if self.target_widget:
|
||||
try:
|
||||
import matplotlib as mpl
|
||||
|
||||
MatplotlibExporter(self.target_widget.plot_item).export()
|
||||
except:
|
||||
warning_util = WarningPopupUtility()
|
||||
warning_util.show_warning(
|
||||
title="Matplotlib not installed",
|
||||
message="Matplotlib is required for this feature.",
|
||||
detailed_text="Please install matplotlib in your Python environment by using 'pip install matplotlib'.",
|
||||
)
|
||||
return
|
||||
@@ -1,48 +0,0 @@
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||
|
||||
|
||||
class SaveStateBundle(ToolbarBundle):
|
||||
"""
|
||||
A bundle of actions that are hooked in this constructor itself,
|
||||
so that you can immediately connect the signals and toggle states.
|
||||
|
||||
This bundle is for a toolbar that controls saving the state of the widget.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||
self.target_widget = target_widget
|
||||
|
||||
# Create each MaterialIconAction with a parent
|
||||
# so the signals can fire even if the toolbar isn't added yet.
|
||||
save_state = MaterialIconAction(
|
||||
icon_name="download", tooltip="Save Widget State", parent=self.target_widget
|
||||
)
|
||||
load_state = MaterialIconAction(
|
||||
icon_name="upload", tooltip="Load Widget State", parent=self.target_widget
|
||||
)
|
||||
|
||||
# Add them to the bundle
|
||||
self.add_action("save", save_state)
|
||||
self.add_action("matplotlib", load_state)
|
||||
|
||||
# Immediately connect signals
|
||||
save_state.action.triggered.connect(self.save_state_dialog)
|
||||
load_state.action.triggered.connect(self.load_state_dialog)
|
||||
|
||||
@SafeSlot()
|
||||
def save_state_dialog(self):
|
||||
"""
|
||||
Open the export dialog to save a state of the widget.
|
||||
"""
|
||||
if self.target_widget:
|
||||
self.target_widget.state_manager.save_state()
|
||||
|
||||
@SafeSlot()
|
||||
def load_state_dialog(self):
|
||||
"""
|
||||
Load a saved state of the widget.
|
||||
"""
|
||||
if self.target_widget:
|
||||
self.target_widget.state_manager.load_state()
|
||||
@@ -75,7 +75,7 @@ class SpinnerWidget(QWidget):
|
||||
proportion = 1 / 4
|
||||
angle_span = int(proportion * 360 * 16)
|
||||
angle_span += angle_span * ease_in_out_sine(self.time / self.duration)
|
||||
painter.drawArc(adjusted_rect, int(self.angle * 16), int(angle_span))
|
||||
painter.drawArc(adjusted_rect, self.angle * 16, int(angle_span))
|
||||
painter.end()
|
||||
|
||||
def closeEvent(self, event):
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB |
@@ -9,24 +9,25 @@ Before installing BEC Widgets, please ensure the following requirements are met:
|
||||
|
||||
**Standard Installation**
|
||||
|
||||
To install BEC Widgets using the pip package manager, execute the following command in your terminal for getting the
|
||||
default PySide6 version into your python environment for BEC:
|
||||
To install BEC Widgets using the pip package manager, execute the following command in your terminal for getting the default PyQT6 version into your python environment for BEC:
|
||||
|
||||
|
||||
```bash
|
||||
pip install 'bec_widgets[pyqt6]'
|
||||
```
|
||||
|
||||
In case you want to use Pyside6, you can install it by using the following command:
|
||||
|
||||
```bash
|
||||
pip install 'bec_widgets[pyside6]'
|
||||
```
|
||||
|
||||
**Troubleshooting**
|
||||
|
||||
If you encounter issues during installation, particularly with Qt, try purging the pip cache:
|
||||
If you encounter issues during installation, particularly with PyQt, try purging the pip cache:
|
||||
|
||||
```bash
|
||||
pip cache purge
|
||||
```
|
||||
|
||||
This can resolve conflicts or issues with package installations.
|
||||
|
||||
```{warning}
|
||||
At the moment PyQt6 is no longer officially supported by BEC Widgets due to incompatibilities with Qt Designer. Please use PySide6 instead.
|
||||
```
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
(user.widgets.games)=
|
||||
|
||||
# Game widgets
|
||||
|
||||
To provide some entertainment during long nights at the beamline, there are game widgets available. Well, only one, so far.
|
||||
|
||||
## Minesweeper
|
||||
|
||||

|
||||
|
||||
The classic game Minesweeper. You may select from three different levels. The game can be ended or reset by clicking on the icon in the top-centre (the robot in the screenshot).
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,60 +0,0 @@
|
||||
(user.widgets.positioner_box_2d)=
|
||||
|
||||
# Positioner Box Widget
|
||||
|
||||
````{tab} Overview
|
||||
|
||||
The [`PositionerBox2D`](/api_reference/_autosummary/bec_widgets.cli.client.PositionerBox2D) widget is very similar to the ['PositionerBox'](/user/widgets/positioner_box/positioner_box) but allows controlling two positioners at the same time, in a horizontal and vertical orientation respectively. It is intended primarily for controlling axes which have a perpendicular relationship like that. In other cases, it may be better to use a `PositionerGroup` instead.
|
||||
|
||||
The `PositionerBox2D` has the same features as the standard `PositionerBox`, but additionally, step buttons which move the positioner by the selected step size, and tweak buttons which move by a tenth of the selected step size.
|
||||
|
||||
````
|
||||
|
||||
````{tab} Examples
|
||||
|
||||
The `PositionerBox2D` widget can be integrated within a GUI application either through direct code instantiation or by using `QtDesigner`. Below are examples demonstrating how to create and use the `PositionerBox2D` widget.
|
||||
|
||||
## Example 1 - Creating a PositionerBox in Code
|
||||
|
||||
In this example, we demonstrate how to create a `PositionerBox2D` widget in code and configure it for a specific device.
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from bec_widgets.widgets.positioner_box import PositionerBox2D
|
||||
|
||||
class MyGui(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setLayout(QVBoxLayout(self)) # Initialize the layout for the widget
|
||||
|
||||
# Create and add the PositionerBox to the layout
|
||||
self.positioner_box_2d = PositionerBox(device_hor="horizontal_motor", device_ver="vertical_motor")
|
||||
self.layout().addWidget(self.positioner_box_2d)
|
||||
|
||||
# Example of how this custom GUI might be used:
|
||||
app = QApplication([])
|
||||
my_gui = MyGui()
|
||||
my_gui.show()
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
## Example 2 - Selecting a Device via GUI
|
||||
|
||||
Users can select the positioner device by clicking the button under the device label, which opens a dialog for device selection.
|
||||
|
||||
## Example 3 - Customizing PositionerBox in QtDesigner
|
||||
|
||||
The `PositionerBox2D` widget can be added to a GUI through `QtDesigner`. Once integrated, you can configure the default device and customize the widget’s appearance and behavior directly within the designer.
|
||||
|
||||
```python
|
||||
# After adding the widget to a form in QtDesigner, you can configure the device:
|
||||
self.positioner_box.set_positioner_hor("samx")
|
||||
self.positioner_box.set_positioner_verr("samy")
|
||||
```
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.PositionerBox2D.rst
|
||||
```
|
||||
````
|
||||
@@ -102,14 +102,6 @@ Find and drag devices.
|
||||
Control individual device.
|
||||
```
|
||||
|
||||
```{grid-item-card} Positioner Box 2D
|
||||
:link: user.widgets.positioner_box_2d
|
||||
:link-type: ref
|
||||
:img-top: /assets/widget_screenshots/positioner_box_2d.png
|
||||
|
||||
Control two individual devices on perpendicular axes.
|
||||
```
|
||||
|
||||
```{grid-item-card} Ring Progress Bar
|
||||
:link: user.widgets.ring_progress_bar
|
||||
:link-type: ref
|
||||
@@ -268,7 +260,6 @@ buttons_appearance/buttons_appearance.md
|
||||
buttons_queue/button_queue.md
|
||||
device_browser/device_browser.md
|
||||
positioner_box/positioner_box.md
|
||||
positioner_box/positioner_box_2d.md
|
||||
text_box/text_box.md
|
||||
website/website.md
|
||||
toggle/toggle.md
|
||||
@@ -279,6 +270,5 @@ signal_input/signal_input.md
|
||||
position_indicator/position_indicator.md
|
||||
lmfit_dialog/lmfit_dialog.md
|
||||
dap_combo_box/dap_combo_box.md
|
||||
games/games.md
|
||||
|
||||
```
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "1.18.0"
|
||||
version = "1.12.0"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
@@ -37,6 +37,7 @@ dev = [
|
||||
"pytest-xvfb~=3.0",
|
||||
"pytest~=8.0",
|
||||
]
|
||||
pyqt6 = ["PyQt6>=6.7", "PyQt6-WebEngine>=6.7"]
|
||||
pyside6 = ["PySide6==6.7.2"]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -4,8 +4,7 @@ import numpy as np
|
||||
import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
|
||||
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
from bec_widgets.tests.utils import check_remote_data_size
|
||||
from bec_widgets.cli.client import BECDockArea, BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
from bec_widgets.utils import Colors
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -13,7 +12,7 @@ from bec_widgets.utils import Colors
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
|
||||
def test_rpc_add_dock_with_figure_e2e(qtbot, bec_client_lib, connected_client_dock):
|
||||
def test_rpc_add_dock_with_figure_e2e(bec_client_lib, connected_client_dock):
|
||||
# BEC client shortcuts
|
||||
dock = connected_client_dock
|
||||
client = bec_client_lib
|
||||
@@ -89,17 +88,14 @@ def test_rpc_add_dock_with_figure_e2e(qtbot, bec_client_lib, connected_client_do
|
||||
|
||||
# Try to make a scan
|
||||
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
|
||||
status.wait()
|
||||
|
||||
# wait for scan to finish
|
||||
while not status.status == "COMPLETED":
|
||||
time.sleep(0.2)
|
||||
|
||||
# plot
|
||||
item = queue.scan_storage.storage[-1]
|
||||
plt_last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
||||
num_elements = 10
|
||||
|
||||
plot_name = "bpm4i-bpm4i"
|
||||
|
||||
qtbot.waitUntil(lambda: check_remote_data_size(plt, plot_name, num_elements))
|
||||
|
||||
plt_data = plt.get_all_data()
|
||||
assert plt_data["bpm4i-bpm4i"]["x"] == plt_last_scan_data["samx"]["samx"].val
|
||||
assert plt_data["bpm4i-bpm4i"]["y"] == plt_last_scan_data["bpm4i"]["bpm4i"].val
|
||||
@@ -259,17 +255,11 @@ def test_auto_update(bec_client_lib, connected_client_dock_w_auto_updates, qtbot
|
||||
# get data from curves
|
||||
widgets = plt.widget_list
|
||||
qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
|
||||
plt_data = widgets[0].get_all_data()
|
||||
|
||||
item = queue.scan_storage.storage[-1]
|
||||
last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
||||
|
||||
num_elements = 10
|
||||
|
||||
plot_name = f"Scan {status.scan.scan_number} - {dock.selected_device}"
|
||||
|
||||
qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements))
|
||||
plt_data = widgets[0].get_all_data()
|
||||
|
||||
# check plotted data
|
||||
assert (
|
||||
plt_data[f"Scan {status.scan.scan_number} - bpm4i"]["x"]
|
||||
@@ -287,18 +277,12 @@ def test_auto_update(bec_client_lib, connected_client_dock_w_auto_updates, qtbot
|
||||
|
||||
plt = auto_updates.get_default_figure()
|
||||
widgets = plt.widget_list
|
||||
|
||||
qtbot.waitUntil(lambda: len(plt.widget_list) > 0, timeout=5000)
|
||||
plt_data = widgets[0].get_all_data()
|
||||
|
||||
item = queue.scan_storage.storage[-1]
|
||||
last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
||||
|
||||
plot_name = f"Scan {status.scan.scan_number} - bpm4i"
|
||||
|
||||
num_elements_bec = 25
|
||||
qtbot.waitUntil(lambda: check_remote_data_size(widgets[0], plot_name, num_elements_bec))
|
||||
plt_data = widgets[0].get_all_data()
|
||||
|
||||
# check plotted data
|
||||
assert (
|
||||
plt_data[f"Scan {status.scan.scan_number} - {dock.selected_device}"]["x"]
|
||||
@@ -371,7 +355,6 @@ def test_rpc_call_with_exception_in_safeslot_error_popup(connected_client_gui_ob
|
||||
gui = connected_client_gui_obj
|
||||
|
||||
gui.main.add_dock("test")
|
||||
qtbot.waitUntil(lambda: len(gui.main.panels) == 2) # default_figure + test
|
||||
with pytest.raises(ValueError):
|
||||
gui.main.add_dock("test")
|
||||
# time.sleep(0.1)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
|
||||
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
|
||||
from bec_widgets.tests.utils import check_remote_data_size
|
||||
|
||||
|
||||
def test_rpc_waveform1d_custom_curve(connected_client_figure):
|
||||
@@ -78,7 +78,7 @@ def test_rpc_plotting_shortcuts_init_configs(connected_client_figure, qtbot):
|
||||
}
|
||||
|
||||
|
||||
def test_rpc_waveform_scan(qtbot, connected_client_figure, bec_client_lib):
|
||||
def test_rpc_waveform_scan(connected_client_figure, bec_client_lib):
|
||||
fig = BECFigure(connected_client_figure)
|
||||
|
||||
# add 3 different curves to track
|
||||
@@ -97,11 +97,6 @@ def test_rpc_waveform_scan(qtbot, connected_client_figure, bec_client_lib):
|
||||
item = queue.scan_storage.storage[-1]
|
||||
last_scan_data = item.live_data if hasattr(item, "live_data") else item.data
|
||||
|
||||
num_elements = 10
|
||||
|
||||
for plot_name in ["bpm4i-bpm4i", "bpm3a-bpm3a", "bpm4d-bpm4d"]:
|
||||
qtbot.waitUntil(lambda: check_remote_data_size(plt, plot_name, num_elements))
|
||||
|
||||
# get data from curves
|
||||
plt_data = plt.get_all_data()
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import pytest
|
||||
from qtpy.QtWidgets import QDoubleSpinBox, QLineEdit
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||
from bec_widgets.widgets.plots_next_gen.setting_menus.axis_settings import AxisSettings
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def axis_settings_fixture(qtbot, mocked_client):
|
||||
"""
|
||||
Creates an AxisSettings widget, targeting the real PlotBase widget.
|
||||
"""
|
||||
|
||||
plot_base = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
axis_settings = create_widget(qtbot, AxisSettings, parent=None, target_widget=plot_base)
|
||||
return axis_settings, plot_base
|
||||
|
||||
|
||||
def test_axis_settings_init(axis_settings_fixture):
|
||||
"""
|
||||
Ensure AxisSettings constructs properly with a real PlotBase target.
|
||||
"""
|
||||
axis_settings, plot_base = axis_settings_fixture
|
||||
# Verify the UI was loaded and placed in a scroll area
|
||||
assert axis_settings.ui is not None
|
||||
assert axis_settings.scroll_area is not None
|
||||
assert axis_settings.layout.count() == 1 # scroll area
|
||||
# Check the target
|
||||
assert axis_settings.target_widget == plot_base
|
||||
|
||||
|
||||
def test_change_ui_updates_plot_base(axis_settings_fixture, qtbot):
|
||||
"""
|
||||
When user edits AxisSettings UI fields, verify that PlotBase's properties update.
|
||||
"""
|
||||
axis_settings, plot_base = axis_settings_fixture
|
||||
|
||||
# 1) Set the 'title'
|
||||
title_edit = axis_settings.ui.title
|
||||
assert isinstance(title_edit, QLineEdit)
|
||||
with qtbot.waitSignal(plot_base.property_changed, timeout=500) as signal:
|
||||
title_edit.setText("New Plot Title")
|
||||
|
||||
assert signal.args == ["title", "New Plot Title"]
|
||||
assert plot_base.title == "New Plot Title"
|
||||
|
||||
# 2) Set x_min spinbox
|
||||
x_max_spin = axis_settings.ui.x_max
|
||||
assert isinstance(x_max_spin, QDoubleSpinBox)
|
||||
with qtbot.waitSignal(plot_base.property_changed, timeout=500) as signal2:
|
||||
x_max_spin.setValue(123)
|
||||
assert plot_base.x_max == 123
|
||||
|
||||
# # 3) Toggle grid
|
||||
x_log_toggle = axis_settings.ui.x_log
|
||||
x_log_toggle.checked = True
|
||||
with qtbot.waitSignal(plot_base.property_changed, timeout=500) as signal3:
|
||||
x_log_toggle.checked = True
|
||||
|
||||
assert plot_base.x_log is True
|
||||
|
||||
|
||||
def test_plot_base_updates_ui(axis_settings_fixture, qtbot):
|
||||
"""
|
||||
When PlotBase properties change (on the Python side), AxisSettings UI should update.
|
||||
We do this by simulating that PlotBase sets properties and emits property_changed.
|
||||
(In real usage, PlotBase calls .property_changed.emit(...) in its setters.)
|
||||
"""
|
||||
axis_settings, plot_base = axis_settings_fixture
|
||||
|
||||
# 1) Set plot_base.title
|
||||
plot_base.title = "Plot Title from Code"
|
||||
assert axis_settings.ui.title.text() == "Plot Title from Code"
|
||||
|
||||
# 2) Set x_max
|
||||
plot_base.x_max = 100
|
||||
qtbot.wait(50)
|
||||
assert axis_settings.ui.x_max.value() == 100
|
||||
|
||||
# 3) Set x_log
|
||||
plot_base.x_log = True
|
||||
qtbot.wait(50)
|
||||
assert axis_settings.ui.x_log.checked is True
|
||||
|
||||
|
||||
def test_no_crash_no_target(qtbot):
|
||||
"""
|
||||
AxisSettings can be created with target_widget=None. It won't update anything,
|
||||
but it shouldn't crash on UI changes.
|
||||
"""
|
||||
axis_settings = create_widget(qtbot, AxisSettings, parent=None, target_widget=None)
|
||||
|
||||
axis_settings.ui.title.setText("No target")
|
||||
assert axis_settings.ui.title.text() == "No target"
|
||||
|
||||
|
||||
def test_scroll_area_behavior(axis_settings_fixture, qtbot):
|
||||
"""
|
||||
Optional: Check that the QScrollArea is set up in a resizable manner.
|
||||
"""
|
||||
axis_settings, plot_base = axis_settings_fixture
|
||||
scroll_area = axis_settings.scroll_area
|
||||
assert scroll_area.widgetResizable() is True
|
||||
@@ -1,33 +1,10 @@
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import pytestqt
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtWidgets import QMessageBox
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility, ExampleWidget, SafeProperty
|
||||
|
||||
|
||||
class TestSafePropertyClass(QObject):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._my_value = 10 # internal store
|
||||
|
||||
@SafeProperty(int, default=-1)
|
||||
def my_value(self) -> int:
|
||||
# artificially raise if it's 999 for testing
|
||||
if self._my_value == 999:
|
||||
raise ValueError("Invalid internal state in getter!")
|
||||
return self._my_value
|
||||
|
||||
@my_value.setter
|
||||
def my_value(self, val: int):
|
||||
# artificially raise if user sets -999 for testing
|
||||
if val == -999:
|
||||
raise ValueError("Invalid user input in setter!")
|
||||
self._my_value = val
|
||||
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility, ExampleWidget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -41,109 +18,46 @@ def widget(qtbot):
|
||||
|
||||
@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok)
|
||||
def test_show_error_message_global(mock_exec, widget, qtbot):
|
||||
"""
|
||||
Test that an error popup is shown if global error popups are enabled
|
||||
and the error_occurred signal is emitted manually.
|
||||
"""
|
||||
error_utility = ErrorPopupUtility()
|
||||
error_utility.enable_global_error_popups(True)
|
||||
|
||||
with qtbot.waitSignal(error_utility.error_occurred, timeout=1000) as blocker:
|
||||
error_utility.error_occurred.emit("Test Error", "This is a test error message.", widget)
|
||||
|
||||
assert mock_exec.called
|
||||
assert blocker.signal_triggered
|
||||
|
||||
|
||||
@pytest.mark.parametrize("global_pop", [False, True])
|
||||
@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok)
|
||||
def test_slot_with_popup_on_error(mock_exec, widget, qtbot, global_pop):
|
||||
error_utility = ErrorPopupUtility()
|
||||
error_utility.enable_global_error_popups(global_pop)
|
||||
|
||||
with qtbot.waitSignal(error_utility.error_occurred, timeout=200) as blocker:
|
||||
widget.method_with_error_handling()
|
||||
|
||||
assert blocker.signal_triggered
|
||||
assert mock_exec.called
|
||||
|
||||
|
||||
@pytest.mark.parametrize("global_pop", [False, True])
|
||||
@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok)
|
||||
def test_slot_with_popup_on_error(mock_exec, widget, qtbot, global_pop):
|
||||
"""
|
||||
If the slot is decorated with @SafeSlot(popup_error=True),
|
||||
we always expect a popup on error (and a signal) even if global popups are off.
|
||||
"""
|
||||
def test_slot_no_popup_by_default_on_error(mock_exec, widget, qtbot, capsys, global_pop):
|
||||
error_utility = ErrorPopupUtility()
|
||||
error_utility.enable_global_error_popups(global_pop)
|
||||
|
||||
with qtbot.waitSignal(error_utility.error_occurred, timeout=500) as blocker:
|
||||
widget.method_with_error_handling()
|
||||
try:
|
||||
with qtbot.waitSignal(error_utility.error_occurred, timeout=200) as blocker:
|
||||
widget.method_without_error_handling()
|
||||
except pytestqt.exceptions.TimeoutError:
|
||||
assert not global_pop
|
||||
|
||||
assert blocker.signal_triggered
|
||||
assert mock_exec.called # Because popup_error=True forces popup
|
||||
|
||||
|
||||
@pytest.mark.parametrize("global_pop", [False, True])
|
||||
@patch.object(bec_logger.logger, "error")
|
||||
@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok)
|
||||
def test_slot_no_popup_by_default_on_error(mock_exec, mock_log_error, widget, qtbot, global_pop):
|
||||
"""
|
||||
If the slot is decorated with @SafeSlot() (no popup_error=True),
|
||||
we never show a popup, even if global popups are on,
|
||||
because the code does not check 'enable_error_popup' for normal slots.
|
||||
"""
|
||||
error_utility = ErrorPopupUtility()
|
||||
error_utility.enable_global_error_popups(global_pop)
|
||||
|
||||
# We do NOT expect a popup or signal in either case, since code only logs
|
||||
with qtbot.assertNotEmitted(error_utility.error_occurred):
|
||||
widget.method_without_error_handling()
|
||||
|
||||
assert not mock_exec.called
|
||||
|
||||
# Confirm logger.error(...) was called
|
||||
mock_log_error.assert_called_once()
|
||||
logged_msg = mock_log_error.call_args[0][0]
|
||||
assert "ValueError" in logged_msg
|
||||
assert "SafeSlot error in slot" in logged_msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("global_pop", [False, True])
|
||||
@patch.object(bec_logger.logger, "error")
|
||||
@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok)
|
||||
def test_safe_property_getter_error(mock_exec, mock_log_error, qtbot, global_pop):
|
||||
"""
|
||||
If a property getter raises an error, we log it by default.
|
||||
(No popup is shown unless code specifically calls it.)
|
||||
"""
|
||||
error_utility = ErrorPopupUtility()
|
||||
error_utility.enable_global_error_popups(global_pop)
|
||||
|
||||
test_obj = TestSafePropertyClass()
|
||||
test_obj._my_value = 999 # triggers ValueError in getter => logs => returns default (-1)
|
||||
|
||||
val = test_obj.my_value
|
||||
assert val == -1
|
||||
|
||||
# No popup => mock_exec not called
|
||||
assert not mock_exec.called
|
||||
|
||||
# logger.error(...) is called once
|
||||
mock_log_error.assert_called_once()
|
||||
logged_msg = mock_log_error.call_args[0][0]
|
||||
assert "SafeProperty error in GETTER" in logged_msg
|
||||
assert "ValueError" in logged_msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("global_pop", [False, True])
|
||||
@patch.object(bec_logger.logger, "error")
|
||||
@patch.object(QMessageBox, "exec_", return_value=QMessageBox.Ok)
|
||||
def test_safe_property_setter_error(mock_exec, mock_log_error, qtbot, global_pop):
|
||||
"""
|
||||
If a property setter raises an error, we log it by default.
|
||||
(No popup is shown unless code specifically calls it.)
|
||||
"""
|
||||
error_utility = ErrorPopupUtility()
|
||||
error_utility.enable_global_error_popups(global_pop)
|
||||
|
||||
test_obj = TestSafePropertyClass()
|
||||
# Setting to -999 triggers setter error => logs => property returns None
|
||||
test_obj.my_value = -999
|
||||
|
||||
# No popup => mock_exec not called
|
||||
assert not mock_exec.called
|
||||
|
||||
# logger.error(...) is called once
|
||||
mock_log_error.assert_called_once()
|
||||
logged_msg = mock_log_error.call_args[0][0]
|
||||
assert "SafeProperty error in SETTER" in logged_msg
|
||||
assert "ValueError" in logged_msg
|
||||
if global_pop:
|
||||
assert blocker.signal_triggered
|
||||
assert mock_exec.called
|
||||
else:
|
||||
assert not blocker.signal_triggered
|
||||
assert not mock_exec.called
|
||||
stdout, stderr = capsys.readouterr()
|
||||
assert "ValueError" in stderr
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
|
||||
import pytest
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
from bec_widgets.widgets.games import Minesweeper
|
||||
from bec_widgets.widgets.games.minesweeper import LEVELS, GameStatus, Pos
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def minesweeper(qtbot):
|
||||
widget = Minesweeper()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_minesweeper_init(minesweeper: Minesweeper):
|
||||
assert minesweeper.status == GameStatus.READY
|
||||
|
||||
|
||||
def test_changing_level_updates_size_and_removes_old_grid_items(minesweeper: Minesweeper):
|
||||
assert minesweeper.b_size == LEVELS["1"][0]
|
||||
grid_items = [minesweeper.grid.itemAt(i).widget() for i in range(minesweeper.grid.count())]
|
||||
for w in grid_items:
|
||||
assert w.parent() is not None
|
||||
minesweeper.change_level("2")
|
||||
assert minesweeper.b_size == LEVELS["2"][0]
|
||||
for w in grid_items:
|
||||
assert w.parent() is None
|
||||
|
||||
|
||||
def test_game_state_changes_to_failed_on_loss(qtbot, minesweeper: Minesweeper):
|
||||
assert minesweeper.status == GameStatus.READY
|
||||
grid_items: list[Pos] = [
|
||||
minesweeper.grid.itemAt(i).widget() for i in range(minesweeper.grid.count())
|
||||
]
|
||||
mine = [p for p in grid_items if p.is_mine][0]
|
||||
|
||||
with qtbot.waitSignal(mine.ohno, timeout=1000):
|
||||
qtbot.mouseRelease(mine, Qt.MouseButton.LeftButton)
|
||||
assert minesweeper.status == GameStatus.FAILED
|
||||
|
||||
|
||||
def test_game_resets_on_reset_click(minesweeper: Minesweeper):
|
||||
assert minesweeper.status == GameStatus.READY
|
||||
minesweeper.grid.itemAt(1).widget().ohno.emit()
|
||||
assert minesweeper.status == GameStatus.FAILED
|
||||
minesweeper.reset_button_pressed()
|
||||
assert minesweeper.status == GameStatus.PLAYING
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Literal
|
||||
|
||||
import pytest
|
||||
from qtpy.QtCore import QPoint, Qt
|
||||
from qtpy.QtGui import QContextMenuEvent
|
||||
from qtpy.QtWidgets import QComboBox, QLabel, QMenu, QToolButton, QWidget
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QComboBox, QLabel, QToolButton, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.toolbar import (
|
||||
DeviceSelectionAction,
|
||||
@@ -12,10 +11,8 @@ from bec_widgets.qt_utils.toolbar import (
|
||||
MaterialIconAction,
|
||||
ModularToolBar,
|
||||
SeparatorAction,
|
||||
ToolbarBundle,
|
||||
WidgetAction,
|
||||
)
|
||||
from tests.unit_tests.conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -280,117 +277,3 @@ def test_show_action_nonexistent(toolbar_fixture):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
toolbar.show_action("nonexistent_action")
|
||||
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
||||
|
||||
|
||||
def test_add_bundle(toolbar_fixture, dummy_widget, icon_action, material_icon_action):
|
||||
"""Test adding a bundle of actions to the toolbar."""
|
||||
toolbar = toolbar_fixture
|
||||
bundle = ToolbarBundle(
|
||||
bundle_id="test_bundle",
|
||||
actions=[
|
||||
("icon_action_in_bundle", icon_action),
|
||||
("material_icon_in_bundle", material_icon_action),
|
||||
],
|
||||
)
|
||||
toolbar.add_bundle(bundle, dummy_widget)
|
||||
assert "test_bundle" in toolbar.bundles
|
||||
assert "icon_action_in_bundle" in toolbar.widgets
|
||||
assert "material_icon_in_bundle" in toolbar.widgets
|
||||
assert icon_action.action in toolbar.actions()
|
||||
assert material_icon_action.action in toolbar.actions()
|
||||
|
||||
|
||||
def test_invalid_orientation(dummy_widget):
|
||||
"""Test that an invalid orientation raises a ValueError."""
|
||||
toolbar = ModularToolBar(target_widget=dummy_widget, orientation="horizontal")
|
||||
with pytest.raises(ValueError):
|
||||
toolbar.set_orientation("diagonal")
|
||||
|
||||
|
||||
def test_widget_action_calculate_minimum_width(qtbot):
|
||||
"""Test calculate_minimum_width with various combo box items."""
|
||||
combo = QComboBox()
|
||||
combo.addItems(["Short", "Longer Item", "The Longest Item In Combo"])
|
||||
widget_action = WidgetAction(label="Test", widget=combo)
|
||||
width = widget_action.calculate_minimum_width(combo)
|
||||
assert width > 0
|
||||
# Width should be large enough to accommodate the longest item plus additional space
|
||||
assert width > 100
|
||||
|
||||
|
||||
# FIXME test is stucking CI, works locally
|
||||
# def test_context_menu_contains_added_actions(
|
||||
# qtbot, icon_action, material_icon_action, dummy_widget
|
||||
# ):
|
||||
# """
|
||||
# Test that the toolbar's context menu lists all added toolbar actions.
|
||||
# """
|
||||
# toolbar = create_widget(
|
||||
# qtbot, widget=ModularToolBar, target_widget=dummy_widget, orientation="horizontal"
|
||||
# )
|
||||
#
|
||||
# # Add two different actions
|
||||
# toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||||
# toolbar.add_action("material_icon_action", material_icon_action, dummy_widget)
|
||||
#
|
||||
# # Manually trigger the context menu event
|
||||
# event = QContextMenuEvent(QContextMenuEvent.Mouse, QPoint(10, 10))
|
||||
# toolbar.contextMenuEvent(event)
|
||||
#
|
||||
# # The QMenu is executed in contextMenuEvent, so we can fetch all possible actions
|
||||
# # from the displayed menu by searching for QMenu in the immediate children of the toolbar.
|
||||
# menus = toolbar.findChildren(QMenu)
|
||||
# assert len(menus) > 0
|
||||
# menu = menus[-1] # The most recently created menu
|
||||
#
|
||||
# menu_action_texts = [action.text() for action in menu.actions()]
|
||||
# # Check if the menu contains entries for both added actions
|
||||
# assert any(icon_action.tooltip in text or "icon_action" in text for text in menu_action_texts)
|
||||
# assert any(
|
||||
# material_icon_action.tooltip in text or "material_icon_action" in text
|
||||
# for text in menu_action_texts
|
||||
# )
|
||||
# menu.actions()[0].trigger() # Trigger the first action to close the menu
|
||||
# toolbar.close()
|
||||
|
||||
|
||||
# FIXME test is stucking CI, works locally
|
||||
# def test_context_menu_toggle_action_visibility(qtbot, icon_action, dummy_widget):
|
||||
# """
|
||||
# Test that toggling action visibility works correctly through the toolbar's context menu.
|
||||
# """
|
||||
# toolbar = create_widget(
|
||||
# qtbot, widget=ModularToolBar, target_widget=dummy_widget, orientation="horizontal"
|
||||
# )
|
||||
# # Add an action
|
||||
# toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||||
# assert icon_action.action.isVisible()
|
||||
#
|
||||
# # Manually trigger the context menu event
|
||||
# event = QContextMenuEvent(QContextMenuEvent.Mouse, QPoint(10, 10))
|
||||
# toolbar.contextMenuEvent(event)
|
||||
#
|
||||
# # Grab the menu that was created
|
||||
# menus = toolbar.findChildren(QMenu)
|
||||
# assert len(menus) > 0
|
||||
# menu = menus[-1]
|
||||
#
|
||||
# # Locate the QAction in the menu
|
||||
# matching_actions = [m for m in menu.actions() if m.text() == icon_action.tooltip]
|
||||
# assert len(matching_actions) == 1
|
||||
# action_in_menu = matching_actions[0]
|
||||
#
|
||||
# # Toggle it off (uncheck)
|
||||
# action_in_menu.setChecked(False)
|
||||
# menu.triggered.emit(action_in_menu)
|
||||
# # The action on the toolbar should now be hidden
|
||||
# assert not icon_action.action.isVisible()
|
||||
#
|
||||
# # Toggle it on (check)
|
||||
# action_in_menu.setChecked(True)
|
||||
# menu.triggered.emit(action_in_menu)
|
||||
# # The action on the toolbar should be visible again
|
||||
# assert icon_action.action.isVisible()
|
||||
#
|
||||
# menu.actions()[0].trigger() # Trigger the first action to close the menu
|
||||
# toolbar.close()
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_init_plot_base(qtbot, mocked_client):
|
||||
"""
|
||||
Test that PlotBase initializes without error and has expected default states.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
assert pb.objectName() == "PlotBase"
|
||||
# The default title/labels should be empty
|
||||
assert pb.title == ""
|
||||
assert pb.x_label == ""
|
||||
assert pb.y_label == ""
|
||||
# By default, no crosshair or FPS monitor
|
||||
assert pb.crosshair is None
|
||||
assert pb.fps_monitor is None
|
||||
# The side panel was created
|
||||
assert pb.side_panel is not None
|
||||
# The toolbar was created
|
||||
assert pb.toolbar is not None
|
||||
|
||||
|
||||
def test_set_title_emits_signal(qtbot, mocked_client):
|
||||
"""
|
||||
Test that setting the title updates the plot and emits a property_changed signal.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as signal:
|
||||
pb.title = "My Plot Title"
|
||||
# The signal should carry ("title", "My Plot Title")
|
||||
assert signal.args == ["title", "My Plot Title"]
|
||||
assert pb.plot_item.titleLabel.text == "My Plot Title"
|
||||
|
||||
# Get the property back from the object
|
||||
assert pb.title == "My Plot Title"
|
||||
|
||||
|
||||
def test_set_x_label_emits_signal(qtbot, mocked_client):
|
||||
"""
|
||||
Test setting x_label updates the plot and emits a property_changed signal.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as signal:
|
||||
pb.x_label = "Voltage (V)"
|
||||
assert signal.args == ["x_label", "Voltage (V)"]
|
||||
assert pb.x_label == "Voltage (V)"
|
||||
assert pb.plot_item.getAxis("bottom").labelText == "Voltage (V)"
|
||||
|
||||
|
||||
def test_set_y_label_emits_signal(qtbot, mocked_client):
|
||||
"""
|
||||
Test setting y_label updates the plot and emits a property_changed signal.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as signal:
|
||||
pb.y_label = "Current (A)"
|
||||
assert signal.args == ["y_label", "Current (A)"]
|
||||
assert pb.y_label == "Current (A)"
|
||||
assert pb.plot_item.getAxis("left").labelText == "Current (A)"
|
||||
|
||||
|
||||
def test_set_x_min_max(qtbot, mocked_client):
|
||||
"""
|
||||
Test setting x_min, x_max changes the actual X-range of the plot
|
||||
and emits signals.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
# Set x_max
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_max:
|
||||
pb.x_max = 50
|
||||
assert pb.x_max == 50.0
|
||||
|
||||
# Set x_min
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_min:
|
||||
pb.x_min = 5
|
||||
assert pb.x_min == 5.0
|
||||
|
||||
# Confirm the actual ViewBox range in pyqtgraph
|
||||
assert pb.plot_item.vb.viewRange()[0] == [5.0, 50.0]
|
||||
|
||||
|
||||
def test_set_y_min_max(qtbot, mocked_client):
|
||||
"""
|
||||
Test setting y_min, y_max changes the actual Y-range of the plot
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_max:
|
||||
pb.y_max = 100
|
||||
assert pb.y_max == 100.0
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_min:
|
||||
pb.y_min = 10
|
||||
assert pb.y_min == 10.0
|
||||
|
||||
# Confirm the actual ViewBox range
|
||||
assert pb.plot_item.vb.viewRange()[1] == [10.0, 100.0]
|
||||
|
||||
|
||||
def test_auto_range_x_y(qtbot, mocked_client):
|
||||
"""
|
||||
Test enabling and disabling autoRange for x and y axes.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
# auto_range_x = True
|
||||
pb.auto_range_x = True
|
||||
assert pb.plot_item.vb.state["autoRange"][0] is True
|
||||
pb.auto_range_y = True
|
||||
assert pb.plot_item.vb.state["autoRange"][1] is True
|
||||
# Turn off
|
||||
pb.auto_range_x = False
|
||||
assert pb.plot_item.vb.state["autoRange"][0] is False
|
||||
pb.auto_range_y = False
|
||||
assert pb.plot_item.vb.state["autoRange"][1] is False
|
||||
|
||||
|
||||
def test_x_log_y_log(qtbot, mocked_client):
|
||||
"""
|
||||
Test toggling log scale on x and y axes.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig1:
|
||||
pb.x_log = True
|
||||
assert pb.plot_item.vb.state["logMode"][0] is True
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig2:
|
||||
pb.x_log = False
|
||||
assert pb.plot_item.vb.state["logMode"][0] is False
|
||||
|
||||
# Y log
|
||||
pb.y_log = True
|
||||
assert pb.plot_item.vb.state["logMode"][1] is True
|
||||
pb.y_log = False
|
||||
assert pb.plot_item.vb.state["logMode"][1] is False
|
||||
|
||||
|
||||
def test_grid(qtbot, mocked_client):
|
||||
"""
|
||||
Test x_grid and y_grid toggles.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
# By default, might be off
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sigx:
|
||||
pb.x_grid = True
|
||||
assert sigx.args == ["x_grid", True]
|
||||
# Confirm in pyqtgraph
|
||||
assert pb.plot_item.ctrl.xGridCheck.isChecked() is True
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sigy:
|
||||
pb.y_grid = True
|
||||
assert sigy.args == ["y_grid", True]
|
||||
assert pb.plot_item.ctrl.yGridCheck.isChecked() is True
|
||||
|
||||
|
||||
def test_lock_aspect_ratio(qtbot, mocked_client):
|
||||
"""
|
||||
Test locking and unlocking the aspect ratio.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
# default is unlocked
|
||||
assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is False
|
||||
|
||||
pb.lock_aspect_ratio = True
|
||||
assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is True
|
||||
|
||||
pb.lock_aspect_ratio = False
|
||||
assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is False
|
||||
|
||||
|
||||
def test_inner_axes_toggle(qtbot, mocked_client):
|
||||
"""
|
||||
Test the 'inner_axes' property, which shows/hides bottom and left axes.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_off:
|
||||
pb.inner_axes = False
|
||||
assert sig_off.args == ["inner_axes", False]
|
||||
assert pb.plot_item.getAxis("bottom").isVisible() is False
|
||||
assert pb.plot_item.getAxis("left").isVisible() is False
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_on:
|
||||
pb.inner_axes = True
|
||||
assert sig_on.args == ["inner_axes", True]
|
||||
assert pb.plot_item.getAxis("bottom").isVisible() is True
|
||||
assert pb.plot_item.getAxis("left").isVisible() is True
|
||||
|
||||
|
||||
def test_outer_axes_toggle(qtbot, mocked_client):
|
||||
"""
|
||||
Test the 'outer_axes' property, which shows/hides top and right axes.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_on:
|
||||
pb.outer_axes = True
|
||||
assert sig_on.args == ["outer_axes", True]
|
||||
assert pb.plot_item.getAxis("top").isVisible() is True
|
||||
assert pb.plot_item.getAxis("right").isVisible() is True
|
||||
|
||||
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_off:
|
||||
pb.outer_axes = False
|
||||
assert sig_off.args == ["outer_axes", False]
|
||||
assert pb.plot_item.getAxis("top").isVisible() is False
|
||||
assert pb.plot_item.getAxis("right").isVisible() is False
|
||||
|
||||
|
||||
def test_crosshair_hook_unhook(qtbot, mocked_client):
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
assert pb.crosshair is None
|
||||
# Hook
|
||||
pb.hook_crosshair()
|
||||
assert pb.crosshair is not None
|
||||
# Unhook
|
||||
pb.unhook_crosshair()
|
||||
assert pb.crosshair is None
|
||||
|
||||
# toggle
|
||||
pb.toggle_crosshair()
|
||||
assert pb.crosshair is not None
|
||||
pb.toggle_crosshair()
|
||||
assert pb.crosshair is None
|
||||
|
||||
|
||||
def test_set_method(qtbot, mocked_client):
|
||||
"""
|
||||
Test using the set(...) convenience method to update multiple properties at once.
|
||||
"""
|
||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||
pb.set(
|
||||
title="Multi Set Title",
|
||||
x_label="Voltage",
|
||||
y_label="Current",
|
||||
x_grid=True,
|
||||
y_grid=True,
|
||||
x_log=True,
|
||||
outer_axes=True,
|
||||
)
|
||||
|
||||
assert pb.title == "Multi Set Title"
|
||||
assert pb.x_label == "Voltage"
|
||||
assert pb.y_label == "Current"
|
||||
assert pb.x_grid is True
|
||||
assert pb.y_grid is True
|
||||
assert pb.x_log is True
|
||||
assert pb.outer_axes is True
|
||||
@@ -7,8 +7,8 @@ from qtpy.QtCore import Qt, QTimer
|
||||
from qtpy.QtGui import QValidator
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import (
|
||||
PositionerBox,
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line import (
|
||||
PositionerControlLine,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
@@ -23,11 +23,11 @@ from .conftest import create_widget
|
||||
def positioner_box(qtbot, mocked_client):
|
||||
"""Fixture for PositionerBox widget"""
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base.uuid.uuid4"
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.uuid.uuid4"
|
||||
) as mock_uuid:
|
||||
mock_uuid.return_value = "fake_uuid"
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base.PositionerBoxBase._check_device_is_valid",
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = create_widget(qtbot, PositionerBox, device="samx", client=mocked_client)
|
||||
@@ -126,11 +126,11 @@ def test_positioner_control_line(qtbot, mocked_client):
|
||||
Inherits from PositionerBox, but the layout is changed. Check dimensions only
|
||||
"""
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base.uuid.uuid4"
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.uuid.uuid4"
|
||||
) as mock_uuid:
|
||||
mock_uuid.return_value = "fake_uuid"
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
|
||||
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.PositionerBox._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = PositionerControlLine(device="samx", client=mocked_client)
|
||||
@@ -158,10 +158,3 @@ def test_positioner_box_open_dialog_selection(qtbot, positioner_box):
|
||||
QTimer.singleShot(100, close_dialog)
|
||||
qtbot.mouseClick(positioner_box.ui.tool_button, Qt.LeftButton)
|
||||
assert positioner_box.device == "samy"
|
||||
|
||||
|
||||
def test_device_validity_check_rejects_non_positioner():
|
||||
# isinstance checks for PositionerBox are mocked out in the mock client
|
||||
positioner_box = mock.MagicMock(spec=PositionerBox)
|
||||
positioner_box.dev = {"test": 5.123}
|
||||
assert not PositionerBox._check_device_is_valid(positioner_box, "test")
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox2D
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def positioner_box_2d(qtbot, mocked_client):
|
||||
"""Fixture for PositionerBox widget"""
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base.uuid.uuid4"
|
||||
) as mock_uuid:
|
||||
mock_uuid.return_value = "fake_uuid"
|
||||
with mock.patch(
|
||||
"bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base.PositionerBoxBase._check_device_is_valid",
|
||||
return_value=True,
|
||||
):
|
||||
db = create_widget(
|
||||
qtbot, PositionerBox2D, device_hor="samx", device_ver="samy", client=mocked_client
|
||||
)
|
||||
yield db
|
||||
|
||||
|
||||
def test_positioner_box_2d(positioner_box_2d):
|
||||
"""Test init of 2D positioner box"""
|
||||
assert positioner_box_2d.device_hor == "samx"
|
||||
assert positioner_box_2d.device_ver == "samy"
|
||||
data_hor = positioner_box_2d.dev["samx"].read()
|
||||
data_ver = positioner_box_2d.dev["samy"].read()
|
||||
# Avoid check for Positioner class from BEC in _init_device
|
||||
|
||||
setpoint_hor_text = positioner_box_2d.ui.setpoint_hor.text()
|
||||
setpoint_ver_text = positioner_box_2d.ui.setpoint_ver.text()
|
||||
# check that the setpoint is taken correctly after init
|
||||
assert float(setpoint_hor_text) == data_hor["samx_setpoint"]["value"]
|
||||
assert float(setpoint_ver_text) == data_ver["samy_setpoint"]["value"]
|
||||
|
||||
# check that the precision is taken correctly after init
|
||||
precision_hor = positioner_box_2d.dev["samx"].precision
|
||||
precision_ver = positioner_box_2d.dev["samy"].precision
|
||||
assert setpoint_hor_text == f"{data_hor['samx_setpoint']['value']:.{precision_hor}f}"
|
||||
assert setpoint_ver_text == f"{data_ver['samy_setpoint']['value']:.{precision_ver}f}"
|
||||
|
||||
# check that the step size is set according to the device precision
|
||||
assert positioner_box_2d.ui.step_size_hor.value() == 10**-precision_hor * 10
|
||||
assert positioner_box_2d.ui.step_size_ver.value() == 10**-precision_ver * 10
|
||||
|
||||
|
||||
def test_positioner_box_move_hor_does_not_affect_ver(positioner_box_2d):
|
||||
"""Test that moving one positioner doesn't affect the other"""
|
||||
with (
|
||||
mock.patch.object(positioner_box_2d.dev["samx"], "move") as mock_move_hor,
|
||||
mock.patch.object(positioner_box_2d.dev["samy"], "move") as mock_move_ver,
|
||||
):
|
||||
positioner_box_2d.ui.step_size_hor.setValue(0.1)
|
||||
positioner_box_2d.on_tweak_inc_hor()
|
||||
mock_move_hor.assert_called_once_with(0.01, relative=True)
|
||||
mock_move_ver.assert_not_called()
|
||||
with (
|
||||
mock.patch.object(positioner_box_2d.dev["samx"], "move") as mock_move_hor,
|
||||
mock.patch.object(positioner_box_2d.dev["samy"], "move") as mock_move_ver,
|
||||
):
|
||||
positioner_box_2d.ui.step_size_ver.setValue(0.1)
|
||||
positioner_box_2d.on_step_dec_ver()
|
||||
mock_move_ver.assert_called_once_with(-0.1, relative=True)
|
||||
mock_move_hor.assert_not_called()
|
||||
|
||||
|
||||
def test_positioner_box_setpoint_changes(positioner_box_2d: PositionerBox2D):
|
||||
"""Test positioner box setpoint change"""
|
||||
with mock.patch.object(positioner_box_2d.dev["samx"], "move") as mock_move:
|
||||
positioner_box_2d.ui.setpoint_hor.setText("100")
|
||||
positioner_box_2d.on_setpoint_change_hor()
|
||||
mock_move.assert_called_once_with(100, relative=False)
|
||||
with mock.patch.object(positioner_box_2d.dev["samy"], "move") as mock_move:
|
||||
positioner_box_2d.ui.setpoint_ver.setText("100")
|
||||
positioner_box_2d.on_setpoint_change_ver()
|
||||
mock_move.assert_called_once_with(100, relative=False)
|
||||
@@ -19,12 +19,6 @@ def test_spinner_widget_paint_event(spinner_widget, qtbot):
|
||||
spinner_widget.paintEvent(None)
|
||||
|
||||
|
||||
def test_spinnner_with_float_angle(spinner_widget, qtbot):
|
||||
spinner_widget.start()
|
||||
spinner_widget.angle = 0.123453453453453
|
||||
spinner_widget.paintEvent(None)
|
||||
|
||||
|
||||
def test_spinner_widget_rendered(spinner_widget, qtbot, tmpdir):
|
||||
spinner_widget.update()
|
||||
qtbot.wait(200)
|
||||
|
||||
@@ -73,52 +73,3 @@ def test_bec_signal_proxy(qtbot, dap_combo_box):
|
||||
qtbot.wait(100)
|
||||
assert proxy.blocked is False
|
||||
assert proxy_container == [(("samx",),), (("samz",),)]
|
||||
|
||||
|
||||
def test_bec_signal_proxy_timeout(qtbot, dap_combo_box):
|
||||
"""
|
||||
Test that BECSignalProxy auto-unblocks after the specified timeout if no manual unblock
|
||||
occurs in the interim.
|
||||
"""
|
||||
proxy_container = []
|
||||
|
||||
def proxy_callback(*args):
|
||||
proxy_container.append(args)
|
||||
|
||||
# Create the proxy with a short 1-second timeout
|
||||
proxy = BECSignalProxy(
|
||||
dap_combo_box.x_axis_updated, rateLimit=25, slot=proxy_callback, timeout=1.0
|
||||
)
|
||||
|
||||
# Initially, ensure it's not blocked
|
||||
assert proxy.blocked is False
|
||||
|
||||
# Trigger the signal once (samx) -> the proxy should block
|
||||
dap_combo_box.x_axis = "samx"
|
||||
qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000)
|
||||
qtbot.wait(100)
|
||||
assert proxy.blocked is True
|
||||
# The first signal should be passed immediately to the callback
|
||||
assert proxy_container == [(("samx",),)]
|
||||
|
||||
# While still blocked, set another value (samz)
|
||||
dap_combo_box.x_axis = "samz"
|
||||
qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000)
|
||||
qtbot.wait(100)
|
||||
# Proxy is still blocked, so the callback shouldn't see "samz" yet
|
||||
assert len(proxy_container) == 1
|
||||
|
||||
# Wait just under 1 second -> should still be blocked
|
||||
qtbot.wait(700)
|
||||
assert proxy.blocked is True
|
||||
|
||||
# Wait a bit more than 1 s
|
||||
qtbot.wait(2000)
|
||||
|
||||
# Wait to catch the is_blocked signal that indicates it has unblocked
|
||||
qtbot.waitSignal(proxy.is_blocked, timeout=2000)
|
||||
# Now it should be unblocked
|
||||
assert proxy.blocked is False
|
||||
|
||||
# The second value "samz" should have been forwarded after auto-unblocking
|
||||
assert proxy_container == [(("samx",),), (("samz",),)]
|
||||
|
||||
@@ -12,7 +12,6 @@ from qtpy.QtWidgets import (
|
||||
)
|
||||
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy, WidgetIO
|
||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@@ -24,13 +23,10 @@ def example_widget(qtbot):
|
||||
combo_box = QComboBox(main_widget)
|
||||
table_widget = QTableWidget(2, 2, main_widget)
|
||||
spin_box = QSpinBox(main_widget)
|
||||
toggle = ToggleSwitch(main_widget)
|
||||
|
||||
layout.addWidget(line_edit)
|
||||
layout.addWidget(combo_box)
|
||||
layout.addWidget(table_widget)
|
||||
layout.addWidget(spin_box)
|
||||
layout.addWidget(toggle)
|
||||
|
||||
# Add text items to the combo box
|
||||
combo_box.addItems(["Option 1", "Option 2", "Option 3"])
|
||||
@@ -64,46 +60,44 @@ def test_export_import_config(example_widget):
|
||||
|
||||
expected_full = {
|
||||
"QWidget ()": {
|
||||
"QComboBox ()": {"QStandardItemModel ()": {}, "value": 1},
|
||||
"QLineEdit ()": {"QObject ()": {}, "value": "New Text"},
|
||||
"QSpinBox ()": {
|
||||
"QLineEdit (qt_spinbox_lineedit)": {"QObject ()": {}, "value": "10"},
|
||||
"QValidator (qt_spinboxvalidator)": {},
|
||||
"value": 10,
|
||||
},
|
||||
"QVBoxLayout ()": {},
|
||||
"QLineEdit ()": {"value": "New Text", "QObject ()": {}},
|
||||
"QComboBox ()": {"value": 1, "QStandardItemModel ()": {}},
|
||||
"QTableWidget ()": {
|
||||
"QAbstractButton ()": {},
|
||||
"QAbstractTableModel ()": {},
|
||||
"value": [["a", "b"], ["c", "d"]],
|
||||
"QWidget (qt_scrollarea_viewport)": {},
|
||||
"QStyledItemDelegate ()": {},
|
||||
"QHeaderView ()": {
|
||||
"QItemSelectionModel ()": {},
|
||||
"QWidget (qt_scrollarea_viewport)": {},
|
||||
"QWidget (qt_scrollarea_hcontainer)": {
|
||||
"QBoxLayout ()": {},
|
||||
"QScrollBar ()": {},
|
||||
"QBoxLayout ()": {},
|
||||
},
|
||||
"QWidget (qt_scrollarea_vcontainer)": {
|
||||
"QBoxLayout ()": {},
|
||||
"QScrollBar ()": {},
|
||||
"QBoxLayout ()": {},
|
||||
},
|
||||
"QWidget (qt_scrollarea_viewport)": {},
|
||||
"QItemSelectionModel ()": {},
|
||||
},
|
||||
"QAbstractButton ()": {},
|
||||
"QAbstractTableModel ()": {},
|
||||
"QItemSelectionModel ()": {},
|
||||
"QStyledItemDelegate ()": {},
|
||||
"QWidget (qt_scrollarea_hcontainer)": {"QBoxLayout ()": {}, "QScrollBar ()": {}},
|
||||
"QWidget (qt_scrollarea_vcontainer)": {"QBoxLayout ()": {}, "QScrollBar ()": {}},
|
||||
"QWidget (qt_scrollarea_viewport)": {},
|
||||
"value": [["a", "b"], ["c", "d"]],
|
||||
"QWidget (qt_scrollarea_hcontainer)": {"QScrollBar ()": {}, "QBoxLayout ()": {}},
|
||||
"QWidget (qt_scrollarea_vcontainer)": {"QScrollBar ()": {}, "QBoxLayout ()": {}},
|
||||
},
|
||||
"QSpinBox ()": {
|
||||
"value": 10,
|
||||
"QLineEdit (qt_spinbox_lineedit)": {"value": "10", "QObject ()": {}},
|
||||
"QValidator (qt_spinboxvalidator)": {},
|
||||
},
|
||||
"QVBoxLayout ()": {},
|
||||
"ToggleSwitch ()": {"value": True},
|
||||
}
|
||||
}
|
||||
expected_reduced = {
|
||||
"QWidget ()": {
|
||||
"QComboBox ()": {"value": 1},
|
||||
"QLineEdit ()": {"value": "New Text"},
|
||||
"QSpinBox ()": {"QLineEdit (qt_spinbox_lineedit)": {"value": "10"}, "value": 10},
|
||||
"QComboBox ()": {"value": 1},
|
||||
"QTableWidget ()": {"value": [["a", "b"], ["c", "d"]]},
|
||||
"ToggleSwitch ()": {"value": True},
|
||||
"QSpinBox ()": {"value": 10, "QLineEdit (qt_spinbox_lineedit)": {"value": "10"}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +111,6 @@ def test_widget_io_get_set_value(example_widget):
|
||||
combo_box = example_widget.findChild(QComboBox)
|
||||
table_widget = example_widget.findChild(QTableWidget)
|
||||
spin_box = example_widget.findChild(QSpinBox)
|
||||
toggle = example_widget.findChild(ToggleSwitch)
|
||||
|
||||
# Check initial values
|
||||
assert WidgetIO.get_value(line_edit) == ""
|
||||
@@ -127,21 +120,18 @@ def test_widget_io_get_set_value(example_widget):
|
||||
["Initial C", "Initial D"],
|
||||
]
|
||||
assert WidgetIO.get_value(spin_box) == 0
|
||||
assert WidgetIO.get_value(toggle) == True
|
||||
|
||||
# Set new values
|
||||
WidgetIO.set_value(line_edit, "Hello")
|
||||
WidgetIO.set_value(combo_box, "Option 2")
|
||||
WidgetIO.set_value(table_widget, [["X", "Y"], ["Z", "W"]])
|
||||
WidgetIO.set_value(spin_box, 5)
|
||||
WidgetIO.set_value(toggle, False)
|
||||
|
||||
# Check updated values
|
||||
assert WidgetIO.get_value(line_edit) == "Hello"
|
||||
assert WidgetIO.get_value(combo_box, as_string=True) == "Option 2"
|
||||
assert WidgetIO.get_value(table_widget) == [["X", "Y"], ["Z", "W"]]
|
||||
assert WidgetIO.get_value(spin_box) == 5
|
||||
assert WidgetIO.get_value(toggle) == False
|
||||
|
||||
|
||||
def test_widget_io_signal(qtbot, example_widget):
|
||||
@@ -150,7 +140,6 @@ def test_widget_io_signal(qtbot, example_widget):
|
||||
combo_box = example_widget.findChild(QComboBox)
|
||||
spin_box = example_widget.findChild(QSpinBox)
|
||||
table_widget = example_widget.findChild(QTableWidget)
|
||||
toggle = example_widget.findChild(ToggleSwitch)
|
||||
|
||||
# We'll store changes in a list to verify the slot is called
|
||||
changes = []
|
||||
@@ -163,7 +152,6 @@ def test_widget_io_signal(qtbot, example_widget):
|
||||
WidgetIO.connect_widget_change_signal(combo_box, universal_slot)
|
||||
WidgetIO.connect_widget_change_signal(spin_box, universal_slot)
|
||||
WidgetIO.connect_widget_change_signal(table_widget, universal_slot)
|
||||
WidgetIO.connect_widget_change_signal(toggle, universal_slot)
|
||||
|
||||
# Trigger changes
|
||||
line_edit.setText("NewText")
|
||||
@@ -185,8 +173,3 @@ def test_widget_io_signal(qtbot, example_widget):
|
||||
qtbot.waitUntil(lambda: len(changes) > 3)
|
||||
# The entire table value should be retrieved
|
||||
assert changes[-1][1][0][0] == "ChangedCell"
|
||||
|
||||
# Test the toggle switch
|
||||
toggle.checked = False
|
||||
qtbot.waitUntil(lambda: len(changes) > 4)
|
||||
assert changes[-1][1] == False
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from qtpy.QtCore import Property
|
||||
from qtpy.QtWidgets import QCheckBox, QGroupBox, QLineEdit, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.widget_state_manager import WidgetStateManager
|
||||
|
||||
|
||||
class MyLineEdit(QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
# Internal attribute to hold the color string
|
||||
self._customColor = ""
|
||||
|
||||
@Property(str)
|
||||
def customColor(self):
|
||||
return self._customColor
|
||||
|
||||
@customColor.setter
|
||||
def customColor(self, color):
|
||||
self._customColor = color
|
||||
|
||||
|
||||
# A specialized widget that has a property declared with stored=False
|
||||
class MyLineEditStoredFalse(QLineEdit):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._noStoreProperty = ""
|
||||
|
||||
@Property(str, stored=False)
|
||||
def noStoreProperty(self):
|
||||
return self._noStoreProperty
|
||||
|
||||
@noStoreProperty.setter
|
||||
def noStoreProperty(self, value):
|
||||
self._noStoreProperty = value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_widget(qtbot):
|
||||
w = QWidget()
|
||||
w.setObjectName("MainWidget")
|
||||
layout = QVBoxLayout(w)
|
||||
|
||||
child1 = MyLineEdit(w)
|
||||
child1.setObjectName("ChildLineEdit1")
|
||||
child1.setText("Hello")
|
||||
child1.customColor = "red"
|
||||
|
||||
child2 = MyLineEdit(w)
|
||||
child2.setObjectName("ChildLineEdit2")
|
||||
child2.setText("World")
|
||||
child2.customColor = "blue"
|
||||
|
||||
# A widget that we want to skip settings
|
||||
skip_widget = QCheckBox("Skip Widget", w)
|
||||
skip_widget.setObjectName("SkipCheckBox")
|
||||
skip_widget.setChecked(True)
|
||||
skip_widget.setProperty("skip_settings", True)
|
||||
|
||||
layout.addWidget(child1)
|
||||
layout.addWidget(child2)
|
||||
layout.addWidget(skip_widget)
|
||||
|
||||
qtbot.addWidget(w)
|
||||
qtbot.waitExposed(w)
|
||||
return w
|
||||
|
||||
|
||||
def test_save_load_widget_state(test_widget):
|
||||
"""
|
||||
Test saving and loading the state
|
||||
"""
|
||||
manager = WidgetStateManager(test_widget)
|
||||
|
||||
# Before saving, confirm initial properties
|
||||
child1 = test_widget.findChild(MyLineEdit, "ChildLineEdit1")
|
||||
child2 = test_widget.findChild(MyLineEdit, "ChildLineEdit2")
|
||||
assert child1.text() == "Hello"
|
||||
assert child1.customColor == "red"
|
||||
assert child2.text() == "World"
|
||||
assert child2.customColor == "blue"
|
||||
|
||||
# Create a temporary file to save settings
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".ini") as tmp_file:
|
||||
tmp_filename = tmp_file.name
|
||||
|
||||
# Save the current state
|
||||
manager.save_state(tmp_filename)
|
||||
|
||||
# Modify the widget properties
|
||||
child1.setText("Changed1")
|
||||
child1.customColor = "green"
|
||||
child2.setText("Changed2")
|
||||
child2.customColor = "yellow"
|
||||
|
||||
assert child1.text() == "Changed1"
|
||||
assert child1.customColor == "green"
|
||||
assert child2.text() == "Changed2"
|
||||
assert child2.customColor == "yellow"
|
||||
|
||||
# Load the previous state
|
||||
manager.load_state(tmp_filename)
|
||||
|
||||
# Confirm that the state has been restored
|
||||
assert child1.text() == "Hello"
|
||||
assert child1.customColor == "red"
|
||||
assert child2.text() == "World"
|
||||
assert child2.customColor == "blue"
|
||||
|
||||
# Clean up temporary file
|
||||
os.remove(tmp_filename)
|
||||
|
||||
|
||||
def test_save_load_without_filename(test_widget, monkeypatch, qtbot):
|
||||
"""
|
||||
Test that the dialog would open if filename is not provided.
|
||||
"""
|
||||
manager = WidgetStateManager(test_widget)
|
||||
|
||||
# Mock QFileDialog.getSaveFileName to return a temporary filename
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".ini") as tmp_file:
|
||||
tmp_filename = tmp_file.name
|
||||
|
||||
def mock_getSaveFileName(*args, **kwargs):
|
||||
return tmp_filename, "INI Files (*.ini)"
|
||||
|
||||
def mock_getOpenFileName(*args, **kwargs):
|
||||
return tmp_filename, "INI Files (*.ini)"
|
||||
|
||||
from qtpy.QtWidgets import QFileDialog
|
||||
|
||||
monkeypatch.setattr(QFileDialog, "getSaveFileName", mock_getSaveFileName)
|
||||
monkeypatch.setattr(QFileDialog, "getOpenFileName", mock_getOpenFileName)
|
||||
|
||||
# Initial values
|
||||
child1 = test_widget.findChild(MyLineEdit, "ChildLineEdit1")
|
||||
assert child1.text() == "Hello"
|
||||
|
||||
# Save state without providing filename -> uses dialog mock
|
||||
manager.save_state()
|
||||
|
||||
# Change property
|
||||
child1.setText("Modified")
|
||||
|
||||
# Load state using dialog mock
|
||||
manager.load_state()
|
||||
|
||||
# State should be restored
|
||||
assert child1.text() == "Hello"
|
||||
|
||||
# Clean up
|
||||
os.remove(tmp_filename)
|
||||
|
||||
|
||||
def test_skip_settings(test_widget):
|
||||
"""
|
||||
Verify that a widget with skip_settings=True is not saved/loaded.
|
||||
"""
|
||||
manager = WidgetStateManager(test_widget)
|
||||
|
||||
skip_checkbox = test_widget.findChild(QCheckBox, "SkipCheckBox")
|
||||
# Double check initial state
|
||||
assert skip_checkbox.isChecked() is True
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".ini") as tmp_file:
|
||||
tmp_filename = tmp_file.name
|
||||
|
||||
# Save state
|
||||
manager.save_state(tmp_filename)
|
||||
|
||||
# Change skip checkbox state
|
||||
skip_checkbox.setChecked(False)
|
||||
assert skip_checkbox.isChecked() is False
|
||||
|
||||
# Load state
|
||||
manager.load_state(tmp_filename)
|
||||
|
||||
# The skip checkbox should not revert because it was never saved.
|
||||
assert skip_checkbox.isChecked() is False
|
||||
|
||||
os.remove(tmp_filename)
|
||||
|
||||
|
||||
def test_property_stored_false(qtbot):
|
||||
"""
|
||||
Verify that a property with stored=False is not saved.
|
||||
"""
|
||||
w = QWidget()
|
||||
w.setObjectName("TestStoredFalse")
|
||||
layout = QVBoxLayout(w)
|
||||
|
||||
stored_false_widget = MyLineEditStoredFalse(w)
|
||||
stored_false_widget.setObjectName("NoStoreLineEdit")
|
||||
stored_false_widget.setText("VisibleText") # normal text property is stored
|
||||
stored_false_widget.noStoreProperty = "ShouldNotBeStored"
|
||||
layout.addWidget(stored_false_widget)
|
||||
|
||||
qtbot.addWidget(w)
|
||||
qtbot.waitExposed(w)
|
||||
|
||||
manager = WidgetStateManager(w)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".ini") as tmp_file:
|
||||
tmp_filename = tmp_file.name
|
||||
|
||||
# Save the current state
|
||||
manager.save_state(tmp_filename)
|
||||
|
||||
# Modify the properties
|
||||
stored_false_widget.setText("ChangedText")
|
||||
stored_false_widget.noStoreProperty = "ChangedNoStore"
|
||||
|
||||
# Load the previous state
|
||||
manager.load_state(tmp_filename)
|
||||
|
||||
# The text should have reverted
|
||||
assert stored_false_widget.text() == "VisibleText"
|
||||
# The noStoreProperty should remain changed, as it was never saved.
|
||||
assert stored_false_widget.noStoreProperty == "ChangedNoStore"
|
||||
|
||||
os.remove(tmp_filename)
|
||||
|
||||
|
||||
def test_skip_parent_settings(qtbot):
|
||||
"""
|
||||
Demonstrates that if a PARENT widget has skip_settings=True, all its
|
||||
children (even if they do NOT have skip_settings=True) also get skipped.
|
||||
"""
|
||||
main_widget = QWidget()
|
||||
main_widget.setObjectName("TopWidget")
|
||||
layout = QVBoxLayout(main_widget)
|
||||
|
||||
# Create a parent widget with skip_settings=True
|
||||
parent_group = QGroupBox("ParentGroup", main_widget)
|
||||
parent_group.setObjectName("ParentGroupBox")
|
||||
parent_group.setProperty("skip_settings", True) # The crucial setting
|
||||
|
||||
child_layout = QVBoxLayout(parent_group)
|
||||
|
||||
child_line_edit_1 = MyLineEdit(parent_group)
|
||||
child_line_edit_1.setObjectName("ChildLineEditA")
|
||||
child_line_edit_1.setText("OriginalA")
|
||||
|
||||
child_line_edit_2 = MyLineEdit(parent_group)
|
||||
child_line_edit_2.setObjectName("ChildLineEditB")
|
||||
child_line_edit_2.setText("OriginalB")
|
||||
|
||||
child_layout.addWidget(child_line_edit_1)
|
||||
child_layout.addWidget(child_line_edit_2)
|
||||
parent_group.setLayout(child_layout)
|
||||
|
||||
layout.addWidget(parent_group)
|
||||
main_widget.setLayout(layout)
|
||||
|
||||
qtbot.addWidget(main_widget)
|
||||
qtbot.waitExposed(main_widget)
|
||||
|
||||
manager = WidgetStateManager(main_widget)
|
||||
|
||||
# Create a temp file to hold settings
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".ini") as tmp_file:
|
||||
tmp_filename = tmp_file.name
|
||||
|
||||
# Save the state
|
||||
manager.save_state(tmp_filename)
|
||||
|
||||
# Change child widget values
|
||||
child_line_edit_1.setText("ChangedA")
|
||||
child_line_edit_2.setText("ChangedB")
|
||||
|
||||
# Load state
|
||||
manager.load_state(tmp_filename)
|
||||
|
||||
# Because the PARENT has skip_settings=True, none of its children get saved or loaded
|
||||
# Hence, the changes remain and do NOT revert
|
||||
assert child_line_edit_1.text() == "ChangedA"
|
||||
assert child_line_edit_2.text() == "ChangedB"
|
||||
|
||||
os.remove(tmp_filename)
|
||||
Reference in New Issue
Block a user