mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-18 22:35:38 +02:00
Compare commits
3 Commits
feat/next-
...
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
|
||||
|
||||
|
||||
5903
CHANGELOG.md
5903
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,11 +31,8 @@ class Widgets(str, enum.Enum):
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
LMFitDialog = "LMFitDialog"
|
||||
LogPanel = "LogPanel"
|
||||
Minesweeper = "Minesweeper"
|
||||
PositionIndicator = "PositionIndicator"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerBox2D = "PositionerBox2D"
|
||||
PositionerControlLine = "PositionerControlLine"
|
||||
ResetButton = "ResetButton"
|
||||
ResumeButton = "ResumeButton"
|
||||
@@ -2996,140 +2993,6 @@ class BECWaveformWidget(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class Curve(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the curve from the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def dap_params(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the curve.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- color: str
|
||||
- symbol: str
|
||||
- symbol_color: str
|
||||
- symbol_size: int
|
||||
- pen_width: int
|
||||
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_data(self, x, y):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_color(self, color: "str", symbol_color: "Optional[str]" = None):
|
||||
"""
|
||||
Change the color of the curve.
|
||||
|
||||
Args:
|
||||
color(str): Color of the curve.
|
||||
symbol_color(str, optional): Color of the symbol. Defaults to None.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_color_map_z(self, colormap: "str"):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol(self, symbol: "str"):
|
||||
"""
|
||||
Change the symbol of the curve.
|
||||
|
||||
Args:
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol_color(self, symbol_color: "str"):
|
||||
"""
|
||||
Change the symbol color of the curve.
|
||||
|
||||
Args:
|
||||
symbol_color(str): Color of the symbol.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_symbol_size(self, symbol_size: "int"):
|
||||
"""
|
||||
Change the symbol size of the curve.
|
||||
|
||||
Args:
|
||||
symbol_size(int): Size of the symbol.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_pen_width(self, pen_width: "int"):
|
||||
"""
|
||||
Change the pen width of the curve.
|
||||
|
||||
Args:
|
||||
pen_width(int): Width of the pen.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_pen_style(self, pen_style: "Literal['solid', 'dash', 'dot', 'dashdot']"):
|
||||
"""
|
||||
Change the pen style of the curve.
|
||||
|
||||
Args:
|
||||
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_data(self) -> "tuple[np.ndarray, np.ndarray]":
|
||||
"""
|
||||
Get the data of the curve.
|
||||
Returns:
|
||||
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def dap_params(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
class DapComboBox(RPCBase):
|
||||
@rpc_call
|
||||
def select_y_axis(self, y_axis: str):
|
||||
@@ -3318,29 +3181,6 @@ class LMFitDialog(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class LogPanel(RPCBase):
|
||||
@rpc_call
|
||||
def set_plain_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the plain text of the widget.
|
||||
|
||||
Args:
|
||||
text (str): The text to set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_html_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the HTML text of the widget.
|
||||
|
||||
Args:
|
||||
text (str): The text to set.
|
||||
"""
|
||||
|
||||
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
@@ -3391,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
|
||||
|
||||
@@ -17,12 +17,9 @@ from qtpy.QtWidgets import (
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.widgets.containers.expantion_panel.expansion_panel import ExpansionPanel
|
||||
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
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
|
||||
|
||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
@@ -65,10 +62,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"btn4": self.btn4,
|
||||
"btn5": self.btn5,
|
||||
"btn6": self.btn6,
|
||||
"pb": self.pb,
|
||||
"pi": self.pi,
|
||||
"wfng": self.wfng,
|
||||
"ep": self.ep,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -99,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, "PlotBase")
|
||||
|
||||
tab_widget.setCurrentIndex(3)
|
||||
|
||||
group_box = QGroupBox("Jupyter Console", splitter)
|
||||
group_box_layout = QVBoxLayout(group_box)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
@@ -121,26 +105,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.btn5 = QPushButton("Button 5")
|
||||
self.btn6 = QPushButton("Button 6")
|
||||
|
||||
fifth_tab = QWidget()
|
||||
fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
self.wfng = Waveform()
|
||||
fifth_tab_layout.addWidget(self.wfng)
|
||||
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
tab_widget.setCurrentIndex(4)
|
||||
# add stuff to the new Waveform widget
|
||||
self._init_waveform()
|
||||
|
||||
six_tab = QWidget()
|
||||
six_tab_layout = QVBoxLayout(six_tab)
|
||||
self.ep = ExpansionPanel()
|
||||
self.ep.content_layout.addWidget(self.btn1)
|
||||
self.ep.content_layout.addWidget(self.btn2)
|
||||
self.ep.content_layout.addWidget(self.btn3)
|
||||
self.ep.content_layout.addWidget(self.btn4)
|
||||
six_tab_layout.addWidget(self.ep)
|
||||
tab_widget.addTab(six_tab, "Exp Panel")
|
||||
tab_widget.setCurrentIndex(5)
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
|
||||
@@ -149,13 +113,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
|
||||
self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
def _init_waveform(self):
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve1")
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve2")
|
||||
# self.wfng._add_curve_custom(x=np.arange(10), y=np.random.rand(10), label="curve3")
|
||||
self.wfng.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
|
||||
self.wfng.plot(y_name="bpm3a", y_entry="bpm3a", dap="GaussianModel")
|
||||
|
||||
def _init_figure(self):
|
||||
self.w1 = self.figure.plot(x_name="samx", y_name="bpm4i", row=0, col=0)
|
||||
self.w1.set(
|
||||
@@ -222,11 +179,9 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.im.image("waveform", "1d")
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.wf = self.d2.add_widget("BECWaveformWidget", row=0, col=0)
|
||||
self.wf.plot("bpm4i")
|
||||
self.wf.plot("bpm3a")
|
||||
self.wf = self.d2.add_widget("BECFigure", row=0, col=0)
|
||||
|
||||
self.mw = None # self.wf.multi_waveform(monitor="waveform") # , config=config)
|
||||
self.mw = self.wf.multi_waveform(monitor="waveform") # , config=config)
|
||||
|
||||
self.dock.save_state()
|
||||
|
||||
@@ -252,7 +207,7 @@ if __name__ == "__main__": # pragma: no cover
|
||||
app.setApplicationName("Jupyter Console")
|
||||
app.setApplicationDisplayName("Jupyter Console")
|
||||
apply_theme("dark")
|
||||
icon = material_icon("terminal", color=(255, 255, 255, 255), filled=True)
|
||||
icon = material_icon("terminal", color="#434343", filled=True)
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import Optional
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
@@ -17,9 +17,6 @@ from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot
|
||||
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
logger = bec_logger.logger
|
||||
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
||||
|
||||
|
||||
@@ -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_())
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import Field
|
||||
from pyqtgraph.dockarea.DockArea import DockArea
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QPainter, QPaintEvent
|
||||
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QApplication, QSizePolicy, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.toolbar import (
|
||||
@@ -20,18 +20,16 @@ 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
|
||||
from bec_widgets.widgets.plots.motor_map.motor_map_widget import BECMotorMapWidget
|
||||
from bec_widgets.widgets.plots.multi_waveform.multi_waveform_widget import BECMultiWaveformWidget
|
||||
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
||||
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
|
||||
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
|
||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
|
||||
@@ -107,13 +105,6 @@ class BECDockArea(BECWidget, QWidget):
|
||||
tooltip="Add Motor Map",
|
||||
filled=True,
|
||||
),
|
||||
"separator_next_gen": SeparatorAction(),
|
||||
"waveform_ng": MaterialIconAction(
|
||||
icon_name=Waveform.ICON_NAME,
|
||||
color="#FFD700",
|
||||
tooltip="Add Waveform Next Gen",
|
||||
filled=True,
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_0": SeparatorAction(),
|
||||
@@ -148,9 +139,6 @@ class BECDockArea(BECWidget, QWidget):
|
||||
tooltip="Add Circular ProgressBar",
|
||||
filled=True,
|
||||
),
|
||||
"log_panel": MaterialIconAction(
|
||||
icon_name=LogPanel.ICON_NAME, tooltip="Add LogPanel", filled=True
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator_2": SeparatorAction(),
|
||||
@@ -190,9 +178,6 @@ class BECDockArea(BECWidget, QWidget):
|
||||
self.toolbar.widgets["menu_plots"].widgets["motor_map"].triggered.connect(
|
||||
lambda: self.add_dock(widget="BECMotorMapWidget", prefix="motor_map")
|
||||
)
|
||||
self.toolbar.widgets["menu_plots"].widgets["waveform_ng"].triggered.connect(
|
||||
lambda: self.add_dock(widget="Waveform", prefix="waveform_ng")
|
||||
)
|
||||
|
||||
# Menu Devices
|
||||
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect(
|
||||
@@ -215,9 +200,6 @@ class BECDockArea(BECWidget, QWidget):
|
||||
self.toolbar.widgets["menu_utils"].widgets["progress_bar"].triggered.connect(
|
||||
lambda: self.add_dock(widget="RingProgressBar", prefix="progress_bar")
|
||||
)
|
||||
self.toolbar.widgets["menu_utils"].widgets["log_panel"].triggered.connect(
|
||||
lambda: self.add_dock(widget="LogPanel", prefix="log_panel")
|
||||
)
|
||||
|
||||
# Icons
|
||||
self.toolbar.widgets["attach_all"].action.triggered.connect(self.attach_all)
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QEvent, Qt
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
|
||||
class ExpansionPanel(BECWidget, QWidget):
|
||||
"""
|
||||
A collapsible container widget for Qt Designer.
|
||||
|
||||
Key improvements in this version:
|
||||
- A "title_color" property (type "QColor") that accepts either
|
||||
a QColor or a string (hex/rgb/named color).
|
||||
- The label text color is updated accordingly.
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
config: ConnectionConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
title="Panel",
|
||||
expanded=False,
|
||||
) -> 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)
|
||||
|
||||
self.setObjectName("ExpansionPanel")
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
|
||||
# Properties
|
||||
self._expanded = expanded
|
||||
self._title_color = QColor("black") # Default text color
|
||||
self.header_frame = None
|
||||
self.content_frame = None
|
||||
|
||||
# Main layout
|
||||
self._main_layout = QVBoxLayout(self)
|
||||
self._main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._main_layout.setSpacing(0)
|
||||
|
||||
# Create header / content
|
||||
self._init_header(title)
|
||||
self._init_content()
|
||||
|
||||
# Visible or hidden
|
||||
self.content_frame.setVisible(expanded)
|
||||
|
||||
# Defer event filter after constructing frames
|
||||
self.installEventFilter(self)
|
||||
|
||||
def _init_header(self, title):
|
||||
"""
|
||||
Create the header frame with arrow button and label.
|
||||
"""
|
||||
self.header_frame = QFrame(self)
|
||||
self.header_frame.setObjectName("headerFrame")
|
||||
self.header_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.header_frame.setStyleSheet(
|
||||
"""
|
||||
#headerFrame {
|
||||
background-color: "#141414";
|
||||
border-radius: 4px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
self._header_layout = QHBoxLayout(self.header_frame)
|
||||
self._header_layout.setContentsMargins(3, 2, 3, 2)
|
||||
self._header_layout.setSpacing(5)
|
||||
|
||||
self.btn_toggle = QPushButton("▼" if self._expanded else "►", self.header_frame)
|
||||
self.btn_toggle.setFixedSize(25, 25)
|
||||
self.btn_toggle.setStyleSheet("border: none; font-weight: bold;")
|
||||
self.btn_toggle.clicked.connect(self.toggle)
|
||||
self._header_layout.addWidget(self.btn_toggle, alignment=Qt.AlignVCenter)
|
||||
|
||||
self.label_title = QLabel(title, self.header_frame)
|
||||
self.label_title.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self._header_layout.addWidget(self.label_title, alignment=Qt.AlignVCenter | Qt.AlignLeft)
|
||||
self._header_layout.addStretch()
|
||||
|
||||
self._main_layout.addWidget(self.header_frame)
|
||||
|
||||
def _init_content(self):
|
||||
"""Create the collapsible content frame (with its own layout)."""
|
||||
self.content_frame = QFrame(self)
|
||||
self.content_frame.setObjectName("ContentFrame")
|
||||
self.content_frame.setStyleSheet(
|
||||
"""
|
||||
#ContentFrame {
|
||||
border-radius: 4px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.content_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self._content_layout = QVBoxLayout(self.content_frame)
|
||||
self._content_layout.setContentsMargins(5, 5, 5, 5)
|
||||
self._content_layout.setSpacing(5)
|
||||
|
||||
self._main_layout.addWidget(self.content_frame)
|
||||
|
||||
@SafeSlot()
|
||||
def toggle(self):
|
||||
"""Collapse or expand the content area."""
|
||||
self._expanded = not self._expanded
|
||||
if self.content_frame:
|
||||
self.content_frame.setVisible(self._expanded)
|
||||
self.btn_toggle.setText("▼" if self._expanded else "►")
|
||||
|
||||
@SafeProperty(bool)
|
||||
def expanded(self) -> bool:
|
||||
return self._expanded
|
||||
|
||||
@expanded.setter
|
||||
def expanded(self, value: bool):
|
||||
if value != self._expanded:
|
||||
self.toggle()
|
||||
|
||||
@SafeProperty(str)
|
||||
def title(self):
|
||||
"""Property for the header text."""
|
||||
if self.label_title:
|
||||
return self.label_title.text()
|
||||
return ""
|
||||
|
||||
@title.setter
|
||||
def title(self, value: str):
|
||||
if self.label_title:
|
||||
self.label_title.setText(value)
|
||||
|
||||
@SafeProperty("QColor")
|
||||
def title_color(self):
|
||||
"""
|
||||
A 'QColor' property recognized by Designer. We also accept
|
||||
strings for convenience (e.g. "#FF0000", "rgb(0,255,0)", "blue").
|
||||
"""
|
||||
return self._title_color
|
||||
|
||||
@title_color.setter
|
||||
def title_color(self, color):
|
||||
# TODO set bec widget color validator here
|
||||
|
||||
# If user passes a string instead of a QColor, parse it
|
||||
if isinstance(color, str):
|
||||
new_col = QColor(color)
|
||||
if not new_col.isValid():
|
||||
return # ignore invalid color string
|
||||
self._title_color = new_col
|
||||
elif isinstance(color, QColor):
|
||||
self._title_color = color
|
||||
else:
|
||||
# unknown type, ignore
|
||||
return
|
||||
|
||||
# Update label's style
|
||||
color_hex = self._title_color.name() # e.g. "#RRGGBB"
|
||||
self.label_title.setStyleSheet(f"color: {color_hex};")
|
||||
|
||||
@property
|
||||
def content_layout(self) -> QVBoxLayout:
|
||||
"""Layout of the content frame for programmatic additions."""
|
||||
return self._content_layout
|
||||
|
||||
@property
|
||||
def header_layout(self) -> QHBoxLayout:
|
||||
"""Layout of the content frame for programmatic additions."""
|
||||
return self._header_layout
|
||||
|
||||
def event(self, e):
|
||||
"""
|
||||
If Designer adds child widgets, re-parent them into _content_layout
|
||||
unless they're our known frames.
|
||||
"""
|
||||
if e.type() == QEvent.ChildAdded:
|
||||
child_obj = e.child()
|
||||
if child_obj is not None and isinstance(child_obj, QWidget):
|
||||
if self.header_frame is not None and self.content_frame is not None:
|
||||
if child_obj not in (self.header_frame, self.content_frame):
|
||||
self._content_layout.addWidget(child_obj)
|
||||
|
||||
return super().event(e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test if not using Designer
|
||||
from qtpy.QtWidgets import QApplication, QPushButton, QVBoxLayout
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
panel = ExpansionPanel(title="Test Panel", expanded=False)
|
||||
panel2 = ExpansionPanel(title="Test Panel 2", expanded=False)
|
||||
|
||||
# You can set the color via hex string
|
||||
panel.title_color = "#FF00FF"
|
||||
|
||||
panel.content_layout.addWidget(QPushButton("Test Button 1"))
|
||||
panel.content_layout.addWidget(QPushButton("Test Button 2"))
|
||||
|
||||
panel2.content_layout.addWidget(QPushButton("Test Button 1"))
|
||||
panel2.content_layout.addWidget(QPushButton("Test Button 2"))
|
||||
|
||||
container = QWidget()
|
||||
lay = QVBoxLayout(container)
|
||||
lay.addWidget(panel)
|
||||
lay.addWidget(panel2)
|
||||
container.resize(400, 300)
|
||||
container.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['expansion_panel.py']}
|
||||
@@ -1,53 +0,0 @@
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
|
||||
# Make sure the path below is correct
|
||||
from bec_widgets.widgets.containers.expantion_panel.expansion_panel import ExpansionPanel
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='ExpansionPanel' name='expansion_panel'/>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class ExpansionPanelPlugin(QDesignerCustomWidgetInterface):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.initialized = False
|
||||
|
||||
def createWidget(self, parent):
|
||||
return ExpansionPanel(parent=parent)
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Widgets"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(ExpansionPanel.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_widgets.widgets.containers.expantion_panel.expansion_panel"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
if self.initialized:
|
||||
return
|
||||
self.initialized = True
|
||||
|
||||
def isContainer(self):
|
||||
return True # crucial for Designer to allow dropping child widgets
|
||||
|
||||
def isInitialized(self):
|
||||
return self.initialized
|
||||
|
||||
def name(self):
|
||||
return "ExpansionPanel"
|
||||
|
||||
def toolTip(self):
|
||||
return "A collapsible panel container widget"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -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.containers.expantion_panel.expansion_panel_plugin import (
|
||||
ExpansionPanelPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(ExpansionPanelPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -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,7 +6,7 @@ def main(): # pragma: no cover
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combo_box_plugin import (
|
||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox_plugin import (
|
||||
SignalComboBoxPlugin,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox im
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='SignalComboBox' name='signal_combo_box'>
|
||||
<widget class='SignalComboBox' name='signal_combobox'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
@@ -33,7 +33,7 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return designer_material_icon(SignalComboBox.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "signal_combo_box"
|
||||
return "signal_combobox"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
@@ -48,7 +48,7 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "SignalComboBox"
|
||||
|
||||
def toolTip(self):
|
||||
return "Signal ComboBox Example for BEC Widgets with autocomplete."
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -50,7 +50,7 @@ class SignalLineEditPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "SignalLineEdit"
|
||||
|
||||
def toolTip(self):
|
||||
return "Signal LineEdit Example for BEC Widgets with autocomplete."
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
""" Module for DapComboBox widget class to select a DAP model from a combobox. """
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from PySide6.QtWidgets import QSizePolicy
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DapComboBox(BECWidget, QComboBox):
|
||||
class DapComboBox(BECWidget, QWidget):
|
||||
"""
|
||||
The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC.
|
||||
|
||||
@@ -40,13 +39,17 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
def __init__(
|
||||
self, parent=None, client=None, gui_id: str | None = None, default_fit: str | None = None
|
||||
):
|
||||
BECWidget.__init__(self, client=client, gui_id=gui_id)
|
||||
QComboBox.__init__(self, parent=parent)
|
||||
super().__init__(client=client, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.fit_model_combobox = QComboBox(self)
|
||||
self.layout.addWidget(self.fit_model_combobox)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._available_models = None
|
||||
self._x_axis = None
|
||||
self._y_axis = None
|
||||
self.populate_fit_model_combobox()
|
||||
self.currentTextChanged.connect(self._update_current_fit)
|
||||
self.fit_model_combobox.currentTextChanged.connect(self._update_current_fit)
|
||||
# Set default fit model
|
||||
self.select_default_fit(default_fit)
|
||||
|
||||
@@ -121,7 +124,7 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
x_axis(str): X axis.
|
||||
"""
|
||||
self.x_axis = x_axis
|
||||
self._update_current_fit(self.currentText())
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_y_axis(self, y_axis: str):
|
||||
@@ -131,7 +134,7 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
y_axis(str): Y axis.
|
||||
"""
|
||||
self.y_axis = y_axis
|
||||
self._update_current_fit(self.currentText())
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_fit_model(self, fit_name: str | None):
|
||||
@@ -142,14 +145,14 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
"""
|
||||
if not self._validate_dap_model(fit_name):
|
||||
raise ValueError(f"Fit {fit_name} is not valid.")
|
||||
self.setCurrentText(fit_name)
|
||||
self.fit_model_combobox.setCurrentText(fit_name)
|
||||
|
||||
def populate_fit_model_combobox(self):
|
||||
"""Populate the fit_model_combobox with the devices."""
|
||||
# pylint: disable=protected-access
|
||||
self.available_models = [model for model in self.client.dap._available_dap_plugins.keys()]
|
||||
self.clear()
|
||||
self.addItems(self.available_models)
|
||||
self.fit_model_combobox.clear()
|
||||
self.fit_model_combobox.addItems(self.available_models)
|
||||
|
||||
def _validate_dap_model(self, model: str | None) -> bool:
|
||||
"""Validate the DAP model.
|
||||
@@ -166,14 +169,14 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
# widget.setFixedSize(200, 200)
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(DapComboBox())
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>337</width>
|
||||
<height>552</height>
|
||||
<width>303</width>
|
||||
<height>457</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -35,17 +35,11 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_curve_selection">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>100</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Select Curve</string>
|
||||
</property>
|
||||
@@ -66,7 +60,7 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_summary">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -74,7 +68,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
@@ -119,7 +113,7 @@
|
||||
<item>
|
||||
<widget class="QGroupBox" name="group_parameters">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@@ -127,7 +121,7 @@
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>200</height>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
|
||||
@@ -5,9 +5,9 @@ from html.parser import HTMLParser
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field
|
||||
from qtpy.QtCore import Property, Slot
|
||||
from qtpy.QtWidgets import QTextEdit, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
@@ -66,7 +66,7 @@ class TextBox(BECWidget, QWidget):
|
||||
else:
|
||||
self.set_html_text(DEFAULT_TEXT)
|
||||
|
||||
@SafeSlot(str)
|
||||
@Slot(str)
|
||||
def set_plain_text(self, text: str) -> None:
|
||||
"""Set the plain text of the widget.
|
||||
|
||||
@@ -77,7 +77,7 @@ class TextBox(BECWidget, QWidget):
|
||||
self.config.text = text
|
||||
self.config.is_html = False
|
||||
|
||||
@SafeSlot(str)
|
||||
@Slot(str)
|
||||
def set_html_text(self, text: str) -> None:
|
||||
"""Set the HTML text of the widget.
|
||||
|
||||
@@ -88,7 +88,7 @@ class TextBox(BECWidget, QWidget):
|
||||
self.config.text = text
|
||||
self.config.is_html = True
|
||||
|
||||
@SafeProperty(str)
|
||||
@Property(str)
|
||||
def plain_text(self) -> str:
|
||||
"""Get the text of the widget.
|
||||
|
||||
@@ -106,7 +106,7 @@ class TextBox(BECWidget, QWidget):
|
||||
"""
|
||||
self.set_plain_text(text)
|
||||
|
||||
@SafeProperty(str)
|
||||
@Property(str)
|
||||
def html_text(self) -> str:
|
||||
"""Get the HTML text of the widget.
|
||||
|
||||
|
||||
@@ -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']}
|
||||
@@ -1,54 +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.games.minesweeper import Minesweeper
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='Minesweeper' name='minesweeper'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class MinesweeperPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = Minesweeper(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Games"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(Minesweeper.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "minesweeper"
|
||||
|
||||
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 "Minesweeper"
|
||||
|
||||
def toolTip(self):
|
||||
return "Minesweeper"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -1,15 +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.games.minesweeper_plugin import MinesweeperPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(MinesweeperPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -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,602 +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.roi_bundle import ROIBundle
|
||||
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._user_x_label = ""
|
||||
self._x_label_suffix = ""
|
||||
|
||||
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)
|
||||
self.roi_bundle = ROIBundle("roi", 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_bundle(self.roi_bundle, 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)
|
||||
)
|
||||
|
||||
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._user_x_label
|
||||
|
||||
@x_label.setter
|
||||
def x_label(self, value: str):
|
||||
self._user_x_label = value
|
||||
self._apply_x_label()
|
||||
self.property_changed.emit("x_label", self._user_x_label)
|
||||
|
||||
@property
|
||||
def x_label_suffix(self) -> str:
|
||||
"""
|
||||
A read-only (or internal) suffix automatically appended to the user label.
|
||||
Not settable by the user directly from the UI.
|
||||
"""
|
||||
return self._x_label_suffix
|
||||
|
||||
def set_x_label_suffix(self, suffix: str):
|
||||
"""
|
||||
Public or protected method to update the suffix.
|
||||
The user code or subclass (Waveform) can call this
|
||||
when x_mode changes, but the AxisSettings won't show it.
|
||||
"""
|
||||
self._x_label_suffix = suffix
|
||||
self._apply_x_label()
|
||||
|
||||
@property
|
||||
def x_label_combined(self) -> str:
|
||||
"""
|
||||
The final label shown on the axis = user portion + suffix.
|
||||
"""
|
||||
return self._user_x_label + self._x_label_suffix
|
||||
|
||||
def _apply_x_label(self):
|
||||
"""
|
||||
Actually updates the pyqtgraph axis label text to
|
||||
the combined label. Called whenever user label or suffix changes.
|
||||
"""
|
||||
final_label = self.x_label_combined
|
||||
self.plot_item.setLabel("bottom", text=final_label)
|
||||
|
||||
@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,245 +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>218</width>
|
||||
<height>561</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>General</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QLineEdit" name="title"/>
|
||||
</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="2">
|
||||
<widget class="ToggleSwitch" name="inner_axes"/>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_outer_axes">
|
||||
<property name="text">
|
||||
<string>Outer Axes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="ToggleSwitch" name="outer_axes">
|
||||
<property name="checked" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<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>
|
||||
<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>
|
||||
</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,32 +0,0 @@
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||
|
||||
|
||||
class ROIBundle(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 crosshair and ROI interaction.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id="roi", 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.
|
||||
crosshair = MaterialIconAction(
|
||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
||||
)
|
||||
roi = MaterialIconAction(
|
||||
icon_name="align_justify_space_between",
|
||||
tooltip="Add ROI region for DAP",
|
||||
checkable=True,
|
||||
)
|
||||
|
||||
# Add them to the bundle
|
||||
self.add_action("crosshair", crosshair)
|
||||
self.add_action("roi_linear", roi)
|
||||
|
||||
# Immediately connect signals
|
||||
crosshair.action.toggled.connect(self.target_widget.toggle_crosshair)
|
||||
@@ -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()
|
||||
@@ -1,268 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy import QtCore
|
||||
|
||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# noinspection PyDataclass
|
||||
class DeviceSignal(BaseModel):
|
||||
"""The configuration of a signal in the 1D waveform widget."""
|
||||
|
||||
name: str
|
||||
entry: str
|
||||
dap: Optional[str] = None # TODO utilize differently than in past
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
# noinspection PyDataclass
|
||||
class CurveConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the curve.")
|
||||
label: Optional[str] = Field(None, description="The label of the curve.")
|
||||
color: Optional[str | tuple] = Field(None, description="The color of the curve.")
|
||||
symbol: Optional[str | None] = Field("o", description="The symbol of the curve.")
|
||||
symbol_color: Optional[str | tuple] = Field(
|
||||
None, description="The color of the symbol of the curve."
|
||||
)
|
||||
symbol_size: Optional[int] = Field(7, description="The size of the symbol of the curve.")
|
||||
pen_width: Optional[int] = Field(4, description="The width of the pen of the curve.")
|
||||
pen_style: Optional[Literal["solid", "dash", "dot", "dashdot"]] = Field(
|
||||
"solid", description="The style of the pen of the curve."
|
||||
)
|
||||
source: Literal["device", "dap", "custom"] = Field(
|
||||
"custom", description="The source of the curve."
|
||||
)
|
||||
signal: Optional[DeviceSignal] = Field(None, description="The signal of the curve.")
|
||||
# TODO do validator for parent_label
|
||||
parent_label: Optional[str] = Field(
|
||||
None, description="The label of the parent plot, only relevant for dap curves."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
_validate_symbol_color = field_validator("symbol_color")(Colors.validate_color)
|
||||
|
||||
|
||||
class Curve(BECConnector, pg.PlotDataItem):
|
||||
USER_ACCESS = [
|
||||
"remove",
|
||||
"dap_params",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
"set_color_map_z",
|
||||
"set_symbol",
|
||||
"set_symbol_color",
|
||||
"set_symbol_size",
|
||||
"set_pen_width",
|
||||
"set_pen_style",
|
||||
"get_data",
|
||||
"dap_params",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
config: Optional[CurveConfig] = None,
|
||||
gui_id: Optional[str] = None,
|
||||
parent_item: Optional[Waveform] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = CurveConfig(label=name, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
# config.widget_class = self.__class__.__name__
|
||||
super().__init__(config=config, gui_id=gui_id)
|
||||
pg.PlotDataItem.__init__(self, name=name)
|
||||
|
||||
self.parent_item = parent_item
|
||||
self.apply_config()
|
||||
self.dap_params = None
|
||||
self.dap_summary = None
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
|
||||
def apply_config(self):
|
||||
pen_style_map = {
|
||||
"solid": QtCore.Qt.SolidLine,
|
||||
"dash": QtCore.Qt.DashLine,
|
||||
"dot": QtCore.Qt.DotLine,
|
||||
"dashdot": QtCore.Qt.DashDotLine,
|
||||
}
|
||||
pen_style = pen_style_map.get(self.config.pen_style, QtCore.Qt.SolidLine)
|
||||
|
||||
pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=pen_style)
|
||||
self.setPen(pen)
|
||||
|
||||
if self.config.symbol:
|
||||
symbol_color = self.config.symbol_color or self.config.color
|
||||
brush = pg.mkBrush(color=symbol_color)
|
||||
|
||||
self.setSymbolBrush(brush)
|
||||
self.setSymbolSize(self.config.symbol_size)
|
||||
self.setSymbol(self.config.symbol)
|
||||
|
||||
@property
|
||||
def dap_params(self):
|
||||
return self._dap_params
|
||||
|
||||
@dap_params.setter
|
||||
def dap_params(self, value):
|
||||
self._dap_params = value
|
||||
|
||||
@property
|
||||
def dap_summary(self):
|
||||
return self._dap_report
|
||||
|
||||
@dap_summary.setter
|
||||
def dap_summary(self, value):
|
||||
self._dap_report = value
|
||||
|
||||
def set_data(self, x, y):
|
||||
if self.config.source == "custom":
|
||||
self.setData(x, y)
|
||||
else:
|
||||
raise ValueError(f"Source {self.config.source} do not allow custom data setting.")
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the curve.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- color: str
|
||||
- symbol: str
|
||||
- symbol_color: str
|
||||
- symbol_size: int
|
||||
- pen_width: int
|
||||
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
|
||||
"""
|
||||
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"color": self.set_color,
|
||||
"color_map_z": self.set_color_map_z,
|
||||
"symbol": self.set_symbol,
|
||||
"symbol_color": self.set_symbol_color,
|
||||
"symbol_size": self.set_symbol_size,
|
||||
"pen_width": self.set_pen_width,
|
||||
"pen_style": self.set_pen_style,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
logger.warning(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def set_color(self, color: str, symbol_color: Optional[str] = None):
|
||||
"""
|
||||
Change the color of the curve.
|
||||
|
||||
Args:
|
||||
color(str): Color of the curve.
|
||||
symbol_color(str, optional): Color of the symbol. Defaults to None.
|
||||
"""
|
||||
self.config.color = color
|
||||
self.config.symbol_color = symbol_color or color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol(self, symbol: str):
|
||||
"""
|
||||
Change the symbol of the curve.
|
||||
|
||||
Args:
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
self.config.symbol = symbol
|
||||
self.setSymbol(symbol)
|
||||
self.updateItems()
|
||||
|
||||
def set_symbol_color(self, symbol_color: str):
|
||||
"""
|
||||
Change the symbol color of the curve.
|
||||
|
||||
Args:
|
||||
symbol_color(str): Color of the symbol.
|
||||
"""
|
||||
self.config.symbol_color = symbol_color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol_size(self, symbol_size: int):
|
||||
"""
|
||||
Change the symbol size of the curve.
|
||||
|
||||
Args:
|
||||
symbol_size(int): Size of the symbol.
|
||||
"""
|
||||
self.config.symbol_size = symbol_size
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_width(self, pen_width: int):
|
||||
"""
|
||||
Change the pen width of the curve.
|
||||
|
||||
Args:
|
||||
pen_width(int): Width of the pen.
|
||||
"""
|
||||
self.config.pen_width = pen_width
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_style(self, pen_style: Literal["solid", "dash", "dot", "dashdot"]):
|
||||
"""
|
||||
Change the pen style of the curve.
|
||||
|
||||
Args:
|
||||
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
|
||||
"""
|
||||
self.config.pen_style = pen_style
|
||||
self.apply_config()
|
||||
|
||||
def set_color_map_z(self, colormap: str):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
self.config.color_map_z = colormap
|
||||
self.apply_config()
|
||||
self.parent_item.scan_history(-1)
|
||||
|
||||
def get_data(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Get the data of the curve.
|
||||
Returns:
|
||||
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
|
||||
"""
|
||||
try:
|
||||
x_data, y_data = self.getData()
|
||||
except TypeError:
|
||||
x_data, y_data = np.array([]), np.array([])
|
||||
return x_data, y_data
|
||||
|
||||
def clear_data(self):
|
||||
self.setData([], [])
|
||||
|
||||
def remove(self):
|
||||
"""Remove the curve from the plot."""
|
||||
# self.parent_item.removeItem(self)
|
||||
self.parent_item.remove_curve(self.name())
|
||||
self.rpc_register.remove_rpc(self)
|
||||
@@ -1,30 +0,0 @@
|
||||
import os
|
||||
|
||||
from PySide6.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
|
||||
|
||||
class CurveItem(QWidget):
|
||||
"""
|
||||
#TODO change this nonsense docstring
|
||||
Widget that lets a user set up curves for the Waveform widget.
|
||||
It allows:
|
||||
- Selecting color palette for the entire widget
|
||||
- Choosing x-axis mode
|
||||
- Selecting device and signal
|
||||
- Adding a new curve
|
||||
- Viewing existing curves in a QTreeWidget
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
|
||||
self.setObjectName("CurveSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "curve_settings.ui"), self)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.ui)
|
||||
@@ -1,75 +0,0 @@
|
||||
import os
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from pyqtgraph import ColorMapWidget
|
||||
from PySide6.QtWidgets import QComboBox, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.containers.expantion_panel.expansion_panel import ExpansionPanel
|
||||
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
|
||||
DeviceLineEdit,
|
||||
)
|
||||
from bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit import (
|
||||
SignalLineEdit,
|
||||
)
|
||||
|
||||
|
||||
class CurveSettingWidget(QWidget):
|
||||
"""
|
||||
Widget that lets a user set up curves for the Waveform widget.
|
||||
It allows:
|
||||
- Selecting color palette for the entire widget
|
||||
- Choosing x-axis mode
|
||||
- Selecting device and signal
|
||||
- Adding a new curve
|
||||
- Viewing existing curves in a QTreeWidget
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
|
||||
self.setObjectName("CurveSettings")
|
||||
self.current_path = os.path.dirname(__file__)
|
||||
self.main_setting_ui = UILoader().load_ui(
|
||||
os.path.join(self.current_path, "main_settings.ui"), self
|
||||
)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
# self.layout.addWidget(self.ui)
|
||||
|
||||
self.main_setting = ExpansionPanel(title="Main Settings", expanded=True)
|
||||
self.curve_setting_1 = ExpansionPanel(title="Curve 1", expanded=False)
|
||||
self.curve_setting_2 = ExpansionPanel(title="Curve 2", expanded=False)
|
||||
self.curve_setting_3 = ExpansionPanel(title="Curve 3", expanded=False)
|
||||
|
||||
self.layout.addWidget(self.main_setting)
|
||||
for cs in [self.curve_setting_1, self.curve_setting_2, self.curve_setting_3]:
|
||||
self.layout.addWidget(cs)
|
||||
self._init_curve(cs)
|
||||
self._init_main_settings()
|
||||
# add spacer
|
||||
self.layout.addStretch()
|
||||
|
||||
def _init_main_settings(self):
|
||||
self.main_setting.content_layout.addWidget(self.main_setting_ui)
|
||||
|
||||
def _init_curve(self, curve_setting):
|
||||
icon = material_icon("delete", color=(255, 0, 0, 255))
|
||||
delete_button = QPushButton()
|
||||
delete_button.setIcon(icon)
|
||||
curve_ui = UILoader().load_ui(os.path.join(self.current_path, "curve_3.ui"), self)
|
||||
curve_setting.header_layout.addWidget(delete_button)
|
||||
curve_setting.content_layout.addWidget(curve_ui)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = CurveSettingWidget()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -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.plots_next_gen.waveform.demo.waveform_plot_plugin import (
|
||||
WaveformPlotPlugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlotPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,127 +0,0 @@
|
||||
"""
|
||||
waveform_plot.py
|
||||
A minimal demonstration widget with multiple properties:
|
||||
- deviceName (str)
|
||||
- curvesJson (str)
|
||||
- someFlag (bool)
|
||||
|
||||
It uses pyqtgraph to show dummy curves from 'curvesJson'.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import Property, QPointF
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class WaveformPlot(QWidget):
|
||||
"""
|
||||
Minimal demonstration of a multi-property widget:
|
||||
- deviceName (string)
|
||||
- curvesJson (string containing JSON)
|
||||
- someFlag (boolean)
|
||||
"""
|
||||
|
||||
ICON_NAME = "multiline_chart" # For a designer icon, if desired
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._device_name = "MyDevice"
|
||||
self._curves_json = "[]"
|
||||
self._some_flag = False
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.plot_item = pg.PlotItem()
|
||||
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
|
||||
layout.addWidget(self.plot_widget)
|
||||
|
||||
self._plot_curves = []
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Property #1: deviceName
|
||||
# ------------------------------------------------------------------------
|
||||
def getDeviceName(self) -> str:
|
||||
return self._device_name
|
||||
|
||||
def setDeviceName(self, val: str):
|
||||
if self._device_name != val:
|
||||
self._device_name = val
|
||||
# You might do something in your real code
|
||||
# e.g. re-subscribe to a device, etc.
|
||||
|
||||
deviceName = Property(str, fget=getDeviceName, fset=setDeviceName)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Property #2: curvesJson
|
||||
# ------------------------------------------------------------------------
|
||||
def getCurvesJson(self) -> str:
|
||||
return self._curves_json
|
||||
|
||||
def setCurvesJson(self, new_json: str):
|
||||
if self._curves_json != new_json:
|
||||
self._curves_json = new_json
|
||||
self._rebuild_curves()
|
||||
|
||||
curvesJson = Property(str, fget=getCurvesJson, fset=setCurvesJson, designable=False)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Property #3: someFlag
|
||||
# ------------------------------------------------------------------------
|
||||
def getSomeFlag(self) -> bool:
|
||||
return self._some_flag
|
||||
|
||||
def setSomeFlag(self, val: bool):
|
||||
if self._some_flag != val:
|
||||
self._some_flag = val
|
||||
# React to the flag in your real code if needed
|
||||
|
||||
someFlag = Property(bool, fget=getSomeFlag, fset=setSomeFlag)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Re-build the curves from the JSON
|
||||
# ------------------------------------------------------------------------
|
||||
def _rebuild_curves(self):
|
||||
# Remove existing PlotDataItems
|
||||
for c in self._plot_curves:
|
||||
self.plot_item.removeItem(c)
|
||||
self._plot_curves.clear()
|
||||
|
||||
# Try parse JSON
|
||||
try:
|
||||
arr = json.loads(self._curves_json)
|
||||
if not isinstance(arr, list):
|
||||
raise ValueError("curvesJson must be a JSON list.")
|
||||
except Exception:
|
||||
# If parsing fails, do nothing
|
||||
return
|
||||
|
||||
# Create new PlotDataItems from the JSON
|
||||
for idx, cdef in enumerate(arr):
|
||||
label = cdef.get("label", f"Curve{idx + 1}")
|
||||
color = cdef.get("color", "blue")
|
||||
|
||||
x = [0, 1, 2, 3, 4]
|
||||
y = [val + idx for val in x]
|
||||
|
||||
item = pg.PlotDataItem(x, y, pen=color, name=label)
|
||||
self.plot_item.addItem(item)
|
||||
self._plot_curves.append(item)
|
||||
|
||||
|
||||
# Optional standalone test
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
w = WaveformPlot()
|
||||
w.deviceName = "TestDevice"
|
||||
w.curvesJson = json.dumps([{"label": "A", "color": "red"}, {"label": "B", "color": "green"}])
|
||||
w.someFlag = True
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['waveform_demo.py']}
|
||||
@@ -1,178 +0,0 @@
|
||||
"""
|
||||
waveform_plot_config_dialog.py
|
||||
A single dialog that configures a WaveformPlot's properties:
|
||||
- deviceName
|
||||
- someFlag
|
||||
- curvesJson (with add/remove curve, color picking, etc.)
|
||||
|
||||
You can call this dialog in normal code:
|
||||
dlg = WaveformPlotConfigDialog(myWaveformPlot)
|
||||
if dlg.exec_() == QDialog.Accepted:
|
||||
# properties updated
|
||||
|
||||
Or from a QDesignerTaskMenu (see waveform_plot_taskmenu.py).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QColorDialog,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class WaveformPlotConfigDialog(QDialog):
|
||||
"""
|
||||
Edits three properties of a WaveformPlot:
|
||||
- deviceName (string)
|
||||
- someFlag (bool)
|
||||
- curvesJson (JSON array of {label, color})
|
||||
|
||||
In real usage, you might add more fields (pen widths, device signals, etc.).
|
||||
"""
|
||||
|
||||
def __init__(self, waveform_plot, parent=None):
|
||||
super().__init__(parent, Qt.WindowTitleHint | Qt.WindowSystemMenuHint)
|
||||
self.setWindowTitle("WaveformPlot Configuration")
|
||||
|
||||
self._wp = waveform_plot # We'll read and write properties on this widget
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
# ---------------------------
|
||||
# Row 1: deviceName
|
||||
# ---------------------------
|
||||
row1 = QHBoxLayout()
|
||||
row1.addWidget(QLabel("Device Name:"))
|
||||
self._device_name_edit = QLineEdit(self)
|
||||
self._device_name_edit.setText(self._wp.deviceName)
|
||||
row1.addWidget(self._device_name_edit)
|
||||
main_layout.addLayout(row1)
|
||||
|
||||
# ---------------------------
|
||||
# Row 2: someFlag (bool)
|
||||
# ---------------------------
|
||||
row2 = QHBoxLayout()
|
||||
self._flag_checkbox = QCheckBox("someFlag", self)
|
||||
self._flag_checkbox.setChecked(self._wp.someFlag)
|
||||
row2.addWidget(self._flag_checkbox)
|
||||
row2.addStretch()
|
||||
main_layout.addLayout(row2)
|
||||
|
||||
# ---------------------------
|
||||
# The curves config area
|
||||
# We'll store an internal list of curves
|
||||
# so we can load them from curvesJson
|
||||
# and then re-serialize after changes.
|
||||
# ---------------------------
|
||||
self._curves_data = []
|
||||
try:
|
||||
arr = json.loads(self._wp.curvesJson)
|
||||
if isinstance(arr, list):
|
||||
self._curves_data = arr
|
||||
except:
|
||||
pass
|
||||
|
||||
self._curves_layout = QVBoxLayout()
|
||||
main_layout.addLayout(self._curves_layout)
|
||||
|
||||
add_curve_btn = QPushButton("Add Curve")
|
||||
add_curve_btn.clicked.connect(self._on_add_curve)
|
||||
main_layout.addWidget(add_curve_btn)
|
||||
|
||||
# OK / Cancel
|
||||
box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)
|
||||
box.accepted.connect(self.accept)
|
||||
box.rejected.connect(self.reject)
|
||||
main_layout.addWidget(box)
|
||||
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def _refresh_curves_rows(self):
|
||||
# Clear old row widgets
|
||||
while True:
|
||||
item = self._curves_layout.takeAt(0)
|
||||
if not item:
|
||||
break
|
||||
w = item.widget()
|
||||
if w:
|
||||
w.deleteLater()
|
||||
|
||||
# Create row per curve
|
||||
for idx, cinfo in enumerate(self._curves_data):
|
||||
row_widget = self._create_curve_row(idx, cinfo)
|
||||
self._curves_layout.addWidget(row_widget)
|
||||
|
||||
def _create_curve_row(self, idx, cinfo):
|
||||
container = QWidget(self)
|
||||
hl = QHBoxLayout(container)
|
||||
|
||||
label_edit = QLineEdit(cinfo.get("label", ""), container)
|
||||
label_edit.setPlaceholderText("Label")
|
||||
label_edit.textChanged.connect(lambda txt, i=idx: self._on_label_changed(i, txt))
|
||||
hl.addWidget(label_edit)
|
||||
|
||||
color_btn = QPushButton(cinfo.get("color", "Pick Color"), container)
|
||||
color_btn.clicked.connect(lambda _=None, i=idx: self._pick_color(i))
|
||||
hl.addWidget(color_btn)
|
||||
|
||||
rm_btn = QPushButton("X", container)
|
||||
rm_btn.clicked.connect(lambda _=None, i=idx: self._on_remove_curve(i))
|
||||
hl.addWidget(rm_btn)
|
||||
|
||||
return container
|
||||
|
||||
def _on_add_curve(self):
|
||||
self._curves_data.append({"label": "NewCurve", "color": "blue"})
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def _on_remove_curve(self, idx: int):
|
||||
if 0 <= idx < len(self._curves_data):
|
||||
self._curves_data.pop(idx)
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def _on_label_changed(self, idx: int, new_label: str):
|
||||
if 0 <= idx < len(self._curves_data):
|
||||
self._curves_data[idx]["label"] = new_label
|
||||
|
||||
def _pick_color(self, idx: int):
|
||||
if 0 <= idx < len(self._curves_data):
|
||||
dlg = QColorDialog(self)
|
||||
if dlg.exec_() == QDialog.Accepted:
|
||||
c = dlg.selectedColor().name()
|
||||
self._curves_data[idx]["color"] = c
|
||||
self._refresh_curves_rows()
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
If user presses OK, update the widget's properties:
|
||||
deviceName
|
||||
someFlag
|
||||
curvesJson
|
||||
"""
|
||||
# 1) deviceName
|
||||
self._wp.deviceName = self._device_name_edit.text().strip()
|
||||
|
||||
# 2) someFlag
|
||||
self._wp.someFlag = self._flag_checkbox.isChecked()
|
||||
|
||||
# 3) curvesJson
|
||||
new_json = json.dumps(self._curves_data, indent=2)
|
||||
self._wp.curvesJson = new_json
|
||||
|
||||
super().accept()
|
||||
|
||||
# For standalone usage, you can do:
|
||||
# dlg = WaveformPlotConfigDialog(wp)
|
||||
# if dlg.exec_() == QDialog.Accepted:
|
||||
# # properties were updated
|
||||
@@ -1,90 +0,0 @@
|
||||
"""
|
||||
waveform_plot_plugin.py
|
||||
Registers WaveformPlot with Qt Designer,
|
||||
including the WaveformPlotTaskMenu extension.
|
||||
"""
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from .waveform_demo import WaveformPlot
|
||||
from .waveform_plot_taskmenu import WaveformPlotTaskMenuFactory
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='WaveformPlot' name='waveformPlot'>
|
||||
<property name='geometry'>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>300</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name='deviceName'>
|
||||
<string>MyDevice</string>
|
||||
</property>
|
||||
<property name='someFlag'>
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name='curvesJson'>
|
||||
<string>[{"label": "DefaultCurve", "color": "red"}]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class WaveformPlotPlugin(QDesignerCustomWidgetInterface):
|
||||
"""
|
||||
Exposes WaveformPlot to Designer, plus sets up the Task Menu extension
|
||||
for "Edit Configuration..." popup.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._initialized = False
|
||||
|
||||
def initialize(self, form_editor):
|
||||
if self._initialized:
|
||||
return
|
||||
self._initialized = True
|
||||
|
||||
# Register the TaskMenu extension
|
||||
manager = form_editor.extensionManager()
|
||||
if manager:
|
||||
factory = WaveformPlotTaskMenuFactory(manager)
|
||||
manager.registerExtensions(factory, "org.qt-project.Qt.Designer.TaskMenu")
|
||||
|
||||
def isInitialized(self):
|
||||
return self._initialized
|
||||
|
||||
def createWidget(self, parent):
|
||||
return WaveformPlot(parent)
|
||||
|
||||
def name(self):
|
||||
return "WaveformPlot"
|
||||
|
||||
def group(self):
|
||||
return "Waveform Widgets"
|
||||
|
||||
def icon(self):
|
||||
# If you have a real icon, load it here
|
||||
return QIcon()
|
||||
|
||||
def toolTip(self):
|
||||
return "A multi-property WaveformPlot example"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def includeFile(self):
|
||||
# The Python import path for your waveforms
|
||||
# E.g. "my_widgets.waveform.waveform_plot"
|
||||
return __name__
|
||||
@@ -1,48 +0,0 @@
|
||||
"""
|
||||
waveform_plot_taskmenu.py
|
||||
Task Menu extension for Qt Designer.
|
||||
It attaches "Edit Configuration..." to the WaveformPlot,
|
||||
launching WaveformPlotConfigDialog.
|
||||
"""
|
||||
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtDesigner import QExtensionFactory, QPyDesignerTaskMenuExtension
|
||||
from qtpy.QtGui import QAction
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo.waveform_demo import WaveformPlot
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo.waveform_plot_config_dialog import (
|
||||
WaveformPlotConfigDialog,
|
||||
)
|
||||
|
||||
|
||||
class WaveformPlotTaskMenu(QPyDesignerTaskMenuExtension):
|
||||
def __init__(self, widget: WaveformPlot, parent=None):
|
||||
super().__init__(parent)
|
||||
self._widget = widget
|
||||
self._edit_action = QAction("Edit Configuration...", self)
|
||||
self._edit_action.triggered.connect(self._on_edit)
|
||||
|
||||
def taskActions(self):
|
||||
return [self._edit_action]
|
||||
|
||||
def preferredEditAction(self):
|
||||
# Double-click in Designer might open this
|
||||
return self._edit_action
|
||||
|
||||
@Slot()
|
||||
def _on_edit(self):
|
||||
# Show the same config dialog we can use in normal code
|
||||
dlg = WaveformPlotConfigDialog(self._widget)
|
||||
dlg.exec_() # If user presses OK, the widget's properties are updated
|
||||
|
||||
|
||||
class WaveformPlotTaskMenuFactory(QExtensionFactory):
|
||||
"""
|
||||
Creates a WaveformPlotTaskMenu if the widget is an instance of WaveformPlot
|
||||
and the requested extension is 'TaskMenu'.
|
||||
"""
|
||||
|
||||
def createExtension(self, obj, iid, parent):
|
||||
if iid == "org.qt-project.Qt.Designer.TaskMenu" and isinstance(obj, WaveformPlot):
|
||||
return WaveformPlotTaskMenu(obj, parent)
|
||||
return None
|
||||
@@ -1,55 +0,0 @@
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
|
||||
class CurveConfig(BaseModel):
|
||||
label: str = Field(..., description="Label/ID of this curve")
|
||||
color: str = Field("blue", description="Curve color")
|
||||
symbol: Optional[str] = Field(None, description="Symbol e.g. 'o', 'x'")
|
||||
pen_width: int = Field(2, description="Pen width in px")
|
||||
pen_style: Literal["solid", "dash", "dot", "dashdot"] = "solid"
|
||||
|
||||
# You can add device/signal if desired:
|
||||
# signals: Optional[Signal] = None
|
||||
# etc.
|
||||
|
||||
class Config:
|
||||
model_config = {"validate_assignment": True}
|
||||
|
||||
|
||||
pen_style_map = {
|
||||
"solid": Qt.SolidLine,
|
||||
"dash": Qt.DashLine,
|
||||
"dot": Qt.DotLine,
|
||||
"dashdot": Qt.DashDotLine,
|
||||
}
|
||||
|
||||
|
||||
class BECCurve(pg.PlotDataItem):
|
||||
"""
|
||||
A custom PlotDataItem that holds a reference to a Pydantic-based CurveConfig.
|
||||
"""
|
||||
|
||||
def __init__(self, config: CurveConfig, parent=None):
|
||||
super().__init__(name=config.label) # set the PlotDataItem name
|
||||
self.config = config
|
||||
self._parent = parent # optional reference to the WaveformPlot
|
||||
# now apply config to actual PlotDataItem
|
||||
self.apply_config()
|
||||
|
||||
def apply_config(self):
|
||||
style = pen_style_map.get(self.config.pen_style, Qt.SolidLine)
|
||||
pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=style)
|
||||
self.setPen(pen)
|
||||
|
||||
if self.config.symbol is not None:
|
||||
self.setSymbol(self.config.symbol)
|
||||
else:
|
||||
self.setSymbol(None)
|
||||
|
||||
def set_data_custom(self, x, y):
|
||||
# If you only want to allow custom data if config.source == "custom", etc.
|
||||
self.setData(x, y)
|
||||
@@ -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.plots_next_gen.waveform.demo_2.waveform_demo2_plugin import (
|
||||
WaveformPlotDemo2Plugin,
|
||||
)
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlotDemo2Plugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,144 +0,0 @@
|
||||
import json
|
||||
from typing import List
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import Property
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo_2.demo_curve import BECCurve, CurveConfig
|
||||
|
||||
|
||||
class WaveformPlotDemo2(QWidget):
|
||||
"""
|
||||
A Plot widget that stores multiple curves in a single JSON property (`curvesJson`).
|
||||
Internally, we keep a list of (CurveConfig, BECCurve).
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._curves_json = "[]"
|
||||
self._curves: List[BECCurve] = [] # the actual PlotDataItems
|
||||
self._curve_configs: List[CurveConfig] = []
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.plot_item = pg.PlotItem()
|
||||
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
|
||||
layout.addWidget(self.plot_widget)
|
||||
self.plot_item.addLegend()
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# QProperty: curvesJson
|
||||
# ------------------------------------------------------------------------
|
||||
def getCurvesJson(self) -> str:
|
||||
return self._curves_json
|
||||
|
||||
def setCurvesJson(self, val: str):
|
||||
if self._curves_json != val:
|
||||
self._curves_json = val
|
||||
self._rebuild_curves_from_json()
|
||||
|
||||
curvesJson = Property(str, fget=getCurvesJson, fset=setCurvesJson)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Internal method: parse JSON -> create/update BECCurve objects
|
||||
# ------------------------------------------------------------------------
|
||||
def _rebuild_curves_from_json(self):
|
||||
# 1) Remove existing items from the plot
|
||||
for c in self._curves:
|
||||
self.plot_item.removeItem(c)
|
||||
self._curves.clear()
|
||||
self._curve_configs.clear()
|
||||
|
||||
# 2) Parse JSON
|
||||
try:
|
||||
raw_list = json.loads(self._curves_json)
|
||||
if not isinstance(raw_list, list):
|
||||
raise ValueError("curvesJson must be a JSON list.")
|
||||
except Exception:
|
||||
raw_list = []
|
||||
|
||||
# 3) Convert each raw dict -> CurveConfig -> BECCurve
|
||||
for entry in raw_list:
|
||||
try:
|
||||
cfg = CurveConfig(**entry)
|
||||
except Exception:
|
||||
# fallback or skip
|
||||
continue
|
||||
curve_obj = BECCurve(config=cfg, parent=self)
|
||||
# For demonstration, set some dummy data
|
||||
xdata = [0, 1, 2, 3, 4]
|
||||
ydata = [val + hash(cfg.label) % 3 for val in xdata]
|
||||
curve_obj.setData(xdata, ydata)
|
||||
|
||||
self.plot_item.addItem(curve_obj)
|
||||
self._curves.append(curve_obj)
|
||||
self._curve_configs.append(cfg)
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# CLI / dynamic methods to add, remove, or modify curves at runtime
|
||||
# ------------------------------------------------------------------------
|
||||
def list_curve_labels(self) -> list[str]:
|
||||
return [cfg.label for cfg in self._curve_configs]
|
||||
|
||||
def get_curve(self, label: str) -> BECCurve:
|
||||
# Return the actual BECCurve object (or a config, or both)
|
||||
for c in self._curves:
|
||||
if c.config.label == label:
|
||||
return c
|
||||
raise ValueError(f"No curve with label='{label}'")
|
||||
|
||||
def add_curve(self, cfg: CurveConfig):
|
||||
"""
|
||||
Add a new curve from code. We just insert the new config
|
||||
into the list, then re-serialize to JSON => triggers rebuild
|
||||
"""
|
||||
# insert new config to the internal list
|
||||
self._curve_configs.append(cfg)
|
||||
self._sync_json_from_configs()
|
||||
|
||||
def remove_curve(self, label: str):
|
||||
for i, c in enumerate(self._curve_configs):
|
||||
if c.label == label:
|
||||
self._curve_configs.pop(i)
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"No curve with label='{label}' found to remove.")
|
||||
|
||||
self._sync_json_from_configs()
|
||||
|
||||
def set_curve_property(self, label: str, **kwargs):
|
||||
"""
|
||||
For example, set_curve_property("Curve1", color="red", pen_width=4)
|
||||
We'll update the pydantic model, then re-sync to JSON, rebuild.
|
||||
"""
|
||||
c = self._find_config(label)
|
||||
for k, v in kwargs.items():
|
||||
setattr(c, k, v) # pydantic assignment
|
||||
self._sync_json_from_configs()
|
||||
|
||||
def _find_config(self, label: str) -> CurveConfig:
|
||||
for cfg in self._curve_configs:
|
||||
if cfg.label == label:
|
||||
return cfg
|
||||
raise ValueError(f"No config with label='{label}' found.")
|
||||
|
||||
def _sync_json_from_configs(self):
|
||||
"""
|
||||
Re-serialize our internal curve configs -> JSON string,
|
||||
call setCurvesJson(...) => triggers the rebuild in the same widget
|
||||
so the user and Designer stay in sync
|
||||
"""
|
||||
raw_list = [cfg.dict() for cfg in self._curve_configs]
|
||||
new_json = json.dumps(raw_list, indent=2)
|
||||
self.setCurvesJson(new_json)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
w = WaveformPlotDemo2()
|
||||
w.show()
|
||||
w.add_curve(CurveConfig(label="Curve1", color="red"))
|
||||
w.add_curve(CurveConfig(label="Curve2", color="blue"))
|
||||
app.exec_()
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['waveform_demo2.py']}
|
||||
@@ -1,81 +0,0 @@
|
||||
"""
|
||||
waveform_plot_plugin.py
|
||||
Registers WaveformPlotDemo2 with Qt Designer,
|
||||
including the WaveformPlotDemo2TaskMenu extension.
|
||||
"""
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtGui import QIcon
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.waveform.demo_2.waveform_demo2 import WaveformPlotDemo2
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='WaveformPlotDemo2' name='WaveformPlotDemo2'>
|
||||
<property name='geometry'>
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>300</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name='deviceName'>
|
||||
<string>MyDevice</string>
|
||||
</property>
|
||||
<property name='someFlag'>
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name='curvesJson'>
|
||||
<string>[{"label": "DefaultCurve", "color": "red"}]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class WaveformPlotDemo2Plugin(QDesignerCustomWidgetInterface):
|
||||
"""
|
||||
Exposes WaveformPlotDemo2 to Designer, plus sets up the Task Menu extension
|
||||
for "Edit Configuration..." popup.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._initialized = False
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isInitialized(self):
|
||||
return self._initialized
|
||||
|
||||
def createWidget(self, parent):
|
||||
return WaveformPlotDemo2(parent)
|
||||
|
||||
def name(self):
|
||||
return "WaveformPlotDemo2"
|
||||
|
||||
def group(self):
|
||||
return "Waveform Widgets"
|
||||
|
||||
def icon(self):
|
||||
# If you have a real icon, load it here
|
||||
return QIcon()
|
||||
|
||||
def toolTip(self):
|
||||
return "A multi-property WaveformPlotDemo2 example"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def includeFile(self):
|
||||
# The Python import path for your waveforms
|
||||
# E.g. "my_widgets.waveform.waveform_plot"
|
||||
return __name__
|
||||
@@ -1,15 +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.plots_next_gen.waveform.waveform_plugin import WaveformPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(WaveformPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,183 +0,0 @@
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QTreeWidgetItem
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class CurveSettingWidgetOld(QWidget):
|
||||
"""
|
||||
Widget that lets a user set up curves for the Waveform widget.
|
||||
It allows:
|
||||
- Selecting color palette for the entire widget
|
||||
- Choosing x-axis mode
|
||||
- Selecting device and signal
|
||||
- Adding a new curve
|
||||
- Viewing existing curves in a QTreeWidget
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||
super().__init__(parent, *args, **kwargs)
|
||||
|
||||
self.setObjectName("CurveSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "curve_settings.ui"), self)
|
||||
|
||||
self.target_widget = target_widget
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.addWidget(self.ui)
|
||||
|
||||
self.connect_all_signals()
|
||||
|
||||
self.refresh_tree_from_waveform() # TODO implement
|
||||
|
||||
def connect_all_signals(self):
|
||||
self.ui.pushButton.clicked.connect(self.on_apply_color_palette)
|
||||
self.ui.x_mode.currentTextChanged.connect(self.enable_ui_elements_x_mode)
|
||||
self.ui.x_mode.currentTextChanged.connect(self.on_x_mode_changed)
|
||||
self.enable_ui_elements_x_mode() # Enable or disable the x-axis mode elements based on the x-axis mode
|
||||
|
||||
self.ui.add_curve.clicked.connect(self.add_curve_from_ui)
|
||||
|
||||
# TODO: Implement this method
|
||||
|
||||
# General property forwarding for the target widget
|
||||
# for widget in [self.ui.x_mode]:
|
||||
# WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||
|
||||
def enable_ui_elements_x_mode(self):
|
||||
"""
|
||||
Enable or disable the x-axis mode elements based on the x-axis mode.
|
||||
"""
|
||||
combo_box_mode = self.ui.x_mode.currentText()
|
||||
if combo_box_mode == "device":
|
||||
self.ui.device_line_edit.setEnabled(True)
|
||||
self.ui.signal_line_edit.setEnabled(True)
|
||||
else:
|
||||
self.ui.device_line_edit.setEnabled(False)
|
||||
self.ui.signal_line_edit.setEnabled(False)
|
||||
|
||||
@SafeSlot("QString")
|
||||
def on_x_mode_changed(self, text):
|
||||
"""
|
||||
Update the x-axis mode of the target widget.
|
||||
"""
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
self.target_widget.x_mode = text
|
||||
if text == "device":
|
||||
self.target_widget.device = self.ui.device_line_edit.text()
|
||||
self.target_widget.signal = self.ui.signal_line_edit.text()
|
||||
|
||||
self.refresh_tree_from_waveform() # TODO implement
|
||||
|
||||
@SafeSlot()
|
||||
def on_apply_color_palette(self):
|
||||
"""
|
||||
Apply the selected color palette to the target widget.
|
||||
"""
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
color_map = getattr(self.ui.bec_color_map_widget, "colormap", None)
|
||||
|
||||
self.target_widget.color_palette = color_map
|
||||
|
||||
self.refresh_tree_from_waveform() # TODO implement
|
||||
|
||||
def add_curve_from_ui(self):
|
||||
"""
|
||||
Add a curve to the target widget based on the UI elements.
|
||||
"""
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
def refresh_tree_from_waveform(self):
|
||||
"""
|
||||
Clears the treeWidget and repopulates it with the current curves
|
||||
from the target_widget’s curve_json.
|
||||
"""
|
||||
self.ui.treeWidget.clear()
|
||||
if not self.target_widget:
|
||||
return
|
||||
|
||||
# The Waveform has a SafeProperty 'curve_json' that returns JSON for all device curves
|
||||
# or you can iterate over target_widget.curves and build your own representation.
|
||||
# For a simpler approach, we’ll just iterate curves directly:
|
||||
|
||||
for curve in self.target_widget.curves:
|
||||
# Make a top-level item in the tree for each curve
|
||||
top_item = QTreeWidgetItem(self.ui.treeWidget)
|
||||
top_item.setText(0, "CURVE")
|
||||
top_item.setText(1, curve.name()) # e.g. "myDevice-myEntry"
|
||||
|
||||
# Child: device name
|
||||
dev_item = QTreeWidgetItem(top_item)
|
||||
dev_item.setText(0, "device")
|
||||
if curve.config.signal:
|
||||
dev_item.setText(1, curve.config.signal.name)
|
||||
|
||||
# Child: entry
|
||||
entry_item = QTreeWidgetItem(top_item)
|
||||
entry_item.setText(0, "signal")
|
||||
if curve.config.signal:
|
||||
entry_item.setText(1, curve.config.signal.entry)
|
||||
|
||||
# Child: color
|
||||
color_item = QTreeWidgetItem(top_item)
|
||||
color_item.setText(0, "color")
|
||||
if curve.config.color:
|
||||
color_item.setText(1, str(curve.config.color))
|
||||
|
||||
# Child: source (custom/device/dap)
|
||||
source_item = QTreeWidgetItem(top_item)
|
||||
source_item.setText(0, "source")
|
||||
source_item.setText(1, curve.config.source)
|
||||
|
||||
# Expand the top-level item
|
||||
self.ui.treeWidget.addTopLevelItem(top_item)
|
||||
top_item.setExpanded(True)
|
||||
|
||||
# Optionally, resize columns
|
||||
# self.ui.treeWidget.header().resizeSections(Qt.ResizeToContents)
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
||||
property_name = widget.objectName()
|
||||
setattr(self.target_widget, property_name, value)
|
||||
|
||||
@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,189 +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>256</width>
|
||||
<height>563</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Color Palette</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton">
|
||||
<property name="text">
|
||||
<string>Apply</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECColorMapWidget" name="bec_color_map_widget"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>151</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Device</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="DeviceLineEdit" name="device_line_edit"/>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="x_mode">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>auto</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>index</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>timestamp</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>device</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="SignalLineEdit" name="signal_line_edit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_curve">
|
||||
<property name="text">
|
||||
<string>Add Curve</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeWidget" name="treeWidget">
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Property</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Value</string>
|
||||
</property>
|
||||
</column>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>DEVICE</string>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>device</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>name</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>color</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>style</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</item>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DeviceLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>device_line_edit</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SignalLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>signal_line_edit</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECColorMapWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_color_map_widget</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>device_line_edit</sender>
|
||||
<signal>device_selected(QString)</signal>
|
||||
<receiver>signal_line_edit</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>158</x>
|
||||
<y>174</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>165</x>
|
||||
<y>222</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -1,84 +0,0 @@
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
|
||||
|
||||
|
||||
class WaveformROIManager(QObject):
|
||||
"""
|
||||
A reusable helper class that manages a single linear ROI region on a given plot item.
|
||||
It provides signals to notify about region changes and active state.
|
||||
"""
|
||||
|
||||
roi_changed = Signal(tuple) # Emitted when the ROI (left, right) changes
|
||||
roi_active = Signal(bool) # Emitted when ROI is enabled or disabled
|
||||
|
||||
def __init__(self, plot_item: pg.PlotItem, parent=None):
|
||||
super().__init__(parent)
|
||||
self._plot_item = plot_item
|
||||
self._roi_wrapper: LinearRegionWrapper | None = None
|
||||
self._roi_region: tuple[float, float] | None = None
|
||||
self._accent_colors = get_accent_colors()
|
||||
|
||||
@property
|
||||
def roi_region(self) -> tuple[float, float] | None:
|
||||
return self._roi_region
|
||||
|
||||
@roi_region.setter
|
||||
def roi_region(self, value: tuple[float, float] | None):
|
||||
self._roi_region = value
|
||||
if self._roi_wrapper is not None and value is not None:
|
||||
self._roi_wrapper.linear_region_selector.setRegion(value)
|
||||
|
||||
@Slot(bool)
|
||||
def toggle_roi(self, enabled: bool) -> None:
|
||||
if enabled:
|
||||
self._enable_roi()
|
||||
else:
|
||||
self._disable_roi()
|
||||
|
||||
@Slot(tuple)
|
||||
def select_roi(self, region: tuple[float, float]):
|
||||
# If ROI not present, enabling it
|
||||
if self._roi_wrapper is None:
|
||||
self.toggle_roi(True)
|
||||
self.roi_region = region
|
||||
|
||||
def _enable_roi(self):
|
||||
if self._roi_wrapper is not None:
|
||||
# Already enabled
|
||||
return
|
||||
color = self._accent_colors.default
|
||||
color.setAlpha(int(0.2 * 255))
|
||||
hover_color = self._accent_colors.default
|
||||
hover_color.setAlpha(int(0.35 * 255))
|
||||
|
||||
self._roi_wrapper = LinearRegionWrapper(
|
||||
self._plot_item, color=color, hover_color=hover_color, parent=self
|
||||
)
|
||||
self._roi_wrapper.add_region_selector()
|
||||
self._roi_wrapper.region_changed.connect(self._on_region_changed)
|
||||
|
||||
# If we already had a region, apply it
|
||||
if self._roi_region is not None:
|
||||
self._roi_wrapper.linear_region_selector.setRegion(self._roi_region)
|
||||
else:
|
||||
self._roi_region = self._roi_wrapper.linear_region_selector.getRegion()
|
||||
|
||||
self.roi_active.emit(True)
|
||||
|
||||
def _disable_roi(self):
|
||||
if self._roi_wrapper is not None:
|
||||
self._roi_wrapper.region_changed.disconnect(self._on_region_changed)
|
||||
self._roi_wrapper.cleanup()
|
||||
self._roi_wrapper.deleteLater()
|
||||
self._roi_wrapper = None
|
||||
|
||||
self._roi_region = None
|
||||
self.roi_active.emit(False)
|
||||
|
||||
@Slot(tuple)
|
||||
def _on_region_changed(self, region: tuple[float, float]):
|
||||
self._roi_region = region
|
||||
self.roi_changed.emit(region)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
{'files': ['waveform.py']}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user