Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fa7ca8f09 | |||
| b2f7d3c5f3 | |||
| e3b5e338bf | |||
| c8e614b575 | |||
| 8e44ca1ad0 | |||
| 286ad7196b | |||
| adef25f4e2 | |||
| 60f7d54e2b | |||
| dd932dd8f3 | |||
| d3c1a1b2ed | |||
| 7ea4a482e7 | |||
| 9045323049 | |||
| d15b22250f | |||
| 5557bfe717 | |||
| a8576c164c | |||
| f5807ec5cd | |||
| b0d786b991 | |||
| 774044d2a7 | |||
| 84a59f70ee | |||
| de303f0227 | |||
| cb2131b1de | |||
| 7d07cea946 | |||
| f6d1d0bbe3 | |||
| a52182dca9 | |||
| 6731b655e7 | |||
| bd126dddbb | |||
| e6976dc151 | |||
| b1aff6d791 | |||
| 7bdca84314 | |||
| 6b3ea0101e | |||
| 06d7741622 | |||
| 6b15abcc73 | |||
| 998a745133 |
@@ -1,5 +1,109 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.112.1 (2024-09-19)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(dap_combo_box): updated screenshot ([`e3b5e33`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e3b5e338bfaec276979183fb6d79ab41a7ca21e1))
|
||||
|
||||
* docs(device_box): updated screenshot ([`c8e614b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c8e614b575b48be788a6389a7aa0cfa033d86ab8))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: test e2e dap wait_for_fit ([`b2f7d3c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b2f7d3c5f3f4bf00cc628f788e2c278ebb5688ae))
|
||||
|
||||
## v0.112.0 (2024-09-17)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: console: various improvements, auto-adapt rows to widget size, Qt Designer plugin ([`286ad71`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/286ad7196b0b8562d648fb304eab7d759b6a959b))
|
||||
|
||||
## v0.111.0 (2024-09-17)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(position_indicator): updated position indicator documentation and added designer properties ([`60f7d54`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/60f7d54e2b4c3129de6c95729b8b4aea1757174f))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(position_indicator): improved design and added more customization options ([`d15b222`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d15b22250fbceb708d89872c0380693e04acb107))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(position_indicator): fixed user access ([`dd932dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dd932dd8f3910ab67ec8403124f4e176d048e542))
|
||||
|
||||
* fix(generate_cli): fixed type annotations ([`d3c1a1b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d3c1a1b2edcba7afea9d369820fa7974ac29c333))
|
||||
|
||||
* fix(positioner_box): visual improvements to the positioner_box and positioner_control_line ([`7ea4a48`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7ea4a482e7cd9499a7268ac887b345cab01632aa))
|
||||
|
||||
* fix(palette viewer): fixed background for tool tip ([`9045323`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9045323049d2a39c36fc8845f3b2883d6933436b))
|
||||
|
||||
## v0.110.0 (2024-09-12)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(palette_viewer): added widget to display the current palette and accent colors ([`a8576c1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a8576c164cad17746ec4fcd5c775fb78f70c055c))
|
||||
|
||||
## v0.109.1 (2024-09-09)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: refactor textbox widget, remove inheritance, adhere to bec style; closes #324 ([`b0d786b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b0d786b991677c0846a0c6ba3f2252d48d94ccaa))
|
||||
|
||||
## v0.109.0 (2024-09-06)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(accent colors): added helper function to get all accent colors ([`84a59f7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/84a59f70eed6d8a3c3aeeabc77a5f9ea4e864f61))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(theme): fixed theme access for themecontainer ([`de303f0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/de303f0227fc9d3a74a0410f1e7999ac5132273c))
|
||||
|
||||
## v0.108.0 (2024-09-06)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(progressbar): added docs ([`7d07cea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7d07cea946f9c884477b01bebfb60b332ff09e0a))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(progressbar): added bec progressbar ([`f6d1d0b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f6d1d0bbe3ba30a3b7291cd36a1f7f8e6bd5b895))
|
||||
|
||||
* feat(generate_cli): added support for property and qproperty setter ([`a52182d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a52182dca978833bfc3fad755c596d3a2ef45c42))
|
||||
|
||||
## v0.107.0 (2024-09-06)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: extend waveform docs ([`e6976dc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e6976dc15105209852090a00a97b7cda723142e9))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: add roi select for dap, allow automatic clear curves on plot request ([`7bdca84`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7bdca8431496fe6562d2c28f5a6af869d1a2e654))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: change style to bec_accent_colors ([`bd126dd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bd126dddbbec3e6c448cce263433d328d577c5c0))
|
||||
|
||||
### Test
|
||||
|
||||
* test: add tests, including extension to end-2-end test ([`b1aff6d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b1aff6d791ff847eb2f628e66ccaa4672fdeea08))
|
||||
|
||||
## v0.106.0 (2024-09-05)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(plot_base): toggle to switch outer axes for plotting widgets ([`06d7741`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/06d7741622aea8556208cd17cae521c37333f8b6))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: use DAPComboBox in curve_dialog selection ([`998a745`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/998a7451335b1b35c3e18691d3bab8d882e2d30b))
|
||||
|
||||
### Test
|
||||
|
||||
* test: fix tests ([`6b15abc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6b15abcc73170cb49292741a619a08ee615e6250))
|
||||
|
||||
## v0.105.0 (2024-09-04)
|
||||
|
||||
### Feature
|
||||
@@ -44,114 +148,12 @@
|
||||
|
||||
## v0.103.0 (2024-09-04)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: prefill variables for manual pipeline start ([`158c19e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/158c19eda771562a325fd59405f9fd4cb9a17ed6))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(vscode): open vscode on a free port ([`52da835`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/52da835803f2453096a8b7df23bee5fdf93ae2bb))
|
||||
|
||||
* feat(website): added method to wait until the webpage is loaded ([`9be19d4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9be19d4abebad08c5fc6bea936dd97475fe8f628))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(theme): fixed segfault for webengineview for auto updates ([`9866075`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9866075100577948755b563dc7b7dc4cdc60d040))
|
||||
|
||||
### Test
|
||||
|
||||
* test(webview): fixed tests after refactoring ([`d5eb30c`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d5eb30cd7df4cb0dc3275dd362768afc211eaf2d))
|
||||
|
||||
* test(vscode): popen call does not have to be the only one ([`39f98ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/39f98ec223ba8b59e478ac788c08c59ffe886b4e))
|
||||
|
||||
## v0.102.0 (2024-09-04)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(buttons): buttons section of docs split to appearance and queue buttons ([`047aa26`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/047aa26a60220c826cc1375cf81daf11d1f3ab5c))
|
||||
|
||||
* docs(tests): added tests tutorial for widget ([`18d8561`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/18d8561c965d149a7662085f7dbe2a39a8c4a475))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(queue): BECQueue controls extended with Resume, Stop, Abort, Reset buttons ([`0d7c10e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0d7c10e670e4937787e1afaa19ca8259ac752486))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(queue_reset_button): queue reset has to be confirmed with msgBox ([`9dd43aa`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9dd43aa1fd3991368002605df4389a7a7271011b))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(tests): positioner box test changed to use create_widget fixture ([`df5eff3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/df5eff3147c79ff0278e6a5a09c8f73d5236aed3))
|
||||
|
||||
## v0.101.0 (2024-09-02)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat: add Dap dialog widget ([`9781b77`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9781b77de27b2810fbb1047a61b1832dd186db01))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: add docs, cleanup ([`61ecf49`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/61ecf491e52bfbfa0d5a84764a9095310659043d))
|
||||
|
||||
## v0.100.0 (2024-09-01)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(becwidget): improvements to the bec widget base class docs; fixed type hint import for sphinx ([`99d5e8e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99d5e8e71c7f89a53d7967126f4056dde005534c))
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(theme): added theme handler to bec widget base class; added tests ([`7fb938a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7fb938a8506685278ee5eeb6fe9a03f74b713cf8))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(pyqt slot): removed slot decorator to avoid problems with pyqt6 ([`6c1f89a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6c1f89ad39b7240ab1d1c1123422b99ae195bf01))
|
||||
|
||||
## v0.99.15 (2024-08-31)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(theme): update pg axes on theme update ([`af23e74`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/af23e74f71152f4abc319ab7b45e65deefde3519))
|
||||
|
||||
* fix(positioner_box): fixed positioner box dialog; added test; closes #332 ([`0bf1cf9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/0bf1cf9b8ab2f9171d5ff63d4e3672eb93e9a5fa))
|
||||
|
||||
## v0.99.14 (2024-08-30)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(color_button): signal and slot added for selecting color and for emitting color after change ([`99a98de`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/99a98de8a3b7a83d71e4b567e865ac6f5c62a754))
|
||||
|
||||
* fix(color_button): inheritance changed to QWidget ([`3c0e501`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/3c0e501c56227d4d98ff0ac2186ff5065bff8d7a))
|
||||
|
||||
## v0.99.13 (2024-08-30)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: minor updates to the widget tutorial ([`ec9c8f2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ec9c8f29633364c45ebd998a5411d428c1ce488d))
|
||||
|
||||
* docs(widget tutorial): step by step guide added ([`b32ced8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b32ced85fff628a9e1303a781630cdae3865238e))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(dark mode button): fixed dark mode button state for external updates, including auto ([`a3110d9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a3110d98147295dcb1f9353f9aaf5461cba9232a))
|
||||
|
||||
## v0.99.12 (2024-08-29)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(toolbar): widget action added ([`2efd487`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2efd48736cbe04e84533f7933c552ea8274e2162))
|
||||
|
||||
* fix(reset_button): reset button added ([`6ed1efc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6ed1efc6af193908f70aa37fb73157d2ca6a62f4))
|
||||
|
||||
* fix(abort_button): abort button added; some minor fixes ([`a568633`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a568633c3206a8c26069d140f2d9a548bf4124b0))
|
||||
|
||||
## v0.99.11 (2024-08-29)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(resume_button): resume button added ([`8be8295`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8be8295b2b38f36da210ab36c5da6d0a00e330cc))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(icons): general app icon changed; jupyter app icon changed to material icon ([`5d73fe4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5d73fe455a568ad40a9fadc5ce6e249d782ad20d))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# This file was automatically generated by generate_cli.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
@@ -19,6 +21,7 @@ class Widgets(str, enum.Enum):
|
||||
BECFigure = "BECFigure"
|
||||
BECImageWidget = "BECImageWidget"
|
||||
BECMotorMapWidget = "BECMotorMapWidget"
|
||||
BECProgressBar = "BECProgressBar"
|
||||
BECQueue = "BECQueue"
|
||||
BECStatusBox = "BECStatusBox"
|
||||
BECWaveformWidget = "BECWaveformWidget"
|
||||
@@ -28,6 +31,7 @@ class Widgets(str, enum.Enum):
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
LMFitDialog = "LMFitDialog"
|
||||
PositionIndicator = "PositionIndicator"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerControlLine = "PositionerControlLine"
|
||||
ResetButton = "ResetButton"
|
||||
@@ -1653,6 +1657,15 @@ class BECPlotBase(RPCBase):
|
||||
y(bool): Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_outer_axes(self, show: "bool" = True):
|
||||
"""
|
||||
Set the outer axes of the plot widget.
|
||||
|
||||
Args:
|
||||
show(bool): Show the outer axes.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self, lock):
|
||||
"""
|
||||
@@ -1684,6 +1697,57 @@ class BECPlotBase(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECProgressBar(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, value):
|
||||
"""
|
||||
Set the value of the progress bar.
|
||||
|
||||
Args:
|
||||
value (float): The value to set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_maximum(self, maximum: float):
|
||||
"""
|
||||
Set the maximum value of the progress bar.
|
||||
|
||||
Args:
|
||||
maximum (float): The maximum value.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_minimum(self, minimum: float):
|
||||
"""
|
||||
Set the minimum value of the progress bar.
|
||||
|
||||
Args:
|
||||
minimum (float): The minimum value.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def label_template(self):
|
||||
"""
|
||||
The template for the center label. Use $value, $maximum, and $percentage to insert the values.
|
||||
|
||||
Examples:
|
||||
>>> progressbar.label_template = "$value / $maximum - $percentage %"
|
||||
>>> progressbar.label_template = "$value / $percentage %"
|
||||
"""
|
||||
|
||||
@label_template.setter
|
||||
@rpc_call
|
||||
def label_template(self):
|
||||
"""
|
||||
The template for the center label. Use $value, $maximum, and $percentage to insert the values.
|
||||
|
||||
Examples:
|
||||
>>> progressbar.label_template = "$value / $maximum - $percentage %"
|
||||
>>> progressbar.label_template = "$value / $percentage %"
|
||||
"""
|
||||
|
||||
|
||||
class BECQueue(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -1878,7 +1942,7 @@ class BECWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict | pd.DataFrame":
|
||||
def get_all_data(self, output: "Literal['dict', 'pandas']" = "dict") -> "dict":
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
|
||||
@@ -2041,6 +2105,25 @@ class BECWaveform(RPCBase):
|
||||
size(int): Font size of the legend.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def toggle_roi(self, toggled: "bool") -> "None":
|
||||
"""
|
||||
Toggle the linear region selector on the plot.
|
||||
|
||||
Args:
|
||||
toggled(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def select_roi(self, region: "tuple[float, float]"):
|
||||
"""
|
||||
Set the fit region of the plot widget. At the moment only a single region is supported.
|
||||
To remove the roi region again, use toggle_roi_region
|
||||
|
||||
Args:
|
||||
region(tuple[float, float]): The fit region.
|
||||
"""
|
||||
|
||||
|
||||
class BECWaveformWidget(RPCBase):
|
||||
@property
|
||||
@@ -2312,12 +2395,30 @@ class BECWaveformWidget(RPCBase):
|
||||
Export the plot widget to Matplotlib.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def toggle_roi(self, checked: "bool"):
|
||||
"""
|
||||
Toggle the linear region selector.
|
||||
|
||||
Args:
|
||||
checked(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def select_roi(self, region: "tuple"):
|
||||
"""
|
||||
Set the region of interest of the plot widget.
|
||||
|
||||
Args:
|
||||
region(tuple): Region of interest.
|
||||
"""
|
||||
|
||||
|
||||
class DapComboBox(RPCBase):
|
||||
@rpc_call
|
||||
def select_y_axis(self, y_axis: str):
|
||||
"""
|
||||
Receive update signal for the y axis.
|
||||
Slot to update the y axis.
|
||||
|
||||
Args:
|
||||
y_axis(str): Y axis.
|
||||
@@ -2326,19 +2427,19 @@ class DapComboBox(RPCBase):
|
||||
@rpc_call
|
||||
def select_x_axis(self, x_axis: str):
|
||||
"""
|
||||
Receive update signal for the x axis.
|
||||
Slot to update the x axis.
|
||||
|
||||
Args:
|
||||
x_axis(str): X axis.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def select_fit(self, fit_name: str | None):
|
||||
def select_fit_model(self, fit_name: str | None):
|
||||
"""
|
||||
Select current fit.
|
||||
Slot to update the fit model.
|
||||
|
||||
Args:
|
||||
default_device(str): Default device name.
|
||||
Args:
|
||||
default_device(str): Default device name.
|
||||
"""
|
||||
|
||||
|
||||
@@ -2441,6 +2542,45 @@ class LMFitDialog(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_range(self, min_value: float, max_value: float):
|
||||
"""
|
||||
Set the range of the position indicator
|
||||
|
||||
Args:
|
||||
min_value(float): Minimum value of the range
|
||||
max_value(float): Maximum value of the range
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def vertical(self):
|
||||
"""
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def indicator_width(self):
|
||||
"""
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rounded_corners(self):
|
||||
"""
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox(RPCBase):
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
@@ -2816,31 +2956,21 @@ class StopButton(RPCBase):
|
||||
|
||||
class TextBox(RPCBase):
|
||||
@rpc_call
|
||||
def set_color(self, background_color: str, font_color: str) -> None:
|
||||
def set_plain_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the background color of the widget.
|
||||
|
||||
Args:
|
||||
background_color (str): The color to set the background in HEX.
|
||||
font_color (str): The color to set the font in HEX.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the text of the widget.
|
||||
Set the plain text of the widget.
|
||||
|
||||
Args:
|
||||
text (str): The text to set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_font_size(self, size: int) -> None:
|
||||
def set_html_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the font size of the text in the widget.
|
||||
Set the HTML text of the widget.
|
||||
|
||||
Args:
|
||||
size (int): The font size to set.
|
||||
text (str): The text to set.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import sys
|
||||
|
||||
import black
|
||||
import isort
|
||||
from qtpy.QtCore import Property as QtProperty
|
||||
|
||||
from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator
|
||||
from bec_widgets.utils.plugin_utils import BECClassContainer, get_rpc_classes
|
||||
@@ -30,6 +31,7 @@ else:
|
||||
class ClientGenerator:
|
||||
def __init__(self):
|
||||
self.header = """# This file was automatically generated by generate_cli.py\n
|
||||
from __future__ import annotations
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
@@ -90,11 +92,27 @@ class {class_name}(RPCBase):"""
|
||||
self.content += """...
|
||||
"""
|
||||
for method in cls.USER_ACCESS:
|
||||
obj = getattr(cls, method)
|
||||
if isinstance(obj, property):
|
||||
self.content += """
|
||||
is_property_setter = False
|
||||
obj = getattr(cls, method, None)
|
||||
if obj is None:
|
||||
obj = getattr(cls, method.split(".setter")[0], None)
|
||||
is_property_setter = True
|
||||
method = method.split(".setter")[0]
|
||||
if obj is None:
|
||||
raise AttributeError(
|
||||
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:
|
||||
self.content += f"""
|
||||
@{method}.setter
|
||||
@rpc_call"""
|
||||
else:
|
||||
self.content += """
|
||||
@property
|
||||
@rpc_call"""
|
||||
|
||||
sig = str(inspect.signature(obj.fget))
|
||||
doc = inspect.getdoc(obj.fget)
|
||||
else:
|
||||
|
||||
@@ -86,10 +86,15 @@ class BECWidgetsCLIServer:
|
||||
return obj
|
||||
|
||||
def run_rpc(self, obj, method, args, kwargs):
|
||||
logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
|
||||
method_obj = getattr(obj, method)
|
||||
# check if the method accepts args and kwargs
|
||||
if not callable(method_obj):
|
||||
res = method_obj
|
||||
if not args:
|
||||
res = method_obj
|
||||
else:
|
||||
setattr(obj, method, args[0])
|
||||
res = None
|
||||
else:
|
||||
sig = inspect.signature(method_obj)
|
||||
if sig.parameters:
|
||||
@@ -245,5 +250,5 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.argv = ["bec_widgets.cli.server", "--id", "test", "--gui_class", "BECDockArea"]
|
||||
sys.argv = ["bec_widgets.cli.server", "--id", "e2860", "--gui_class", "BECDockArea"]
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors, get_theme_palette
|
||||
from bec_widgets.widgets.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
|
||||
class PaletteViewer(BECWidget, QWidget):
|
||||
"""
|
||||
This class is a widget that displays current palette colors.
|
||||
"""
|
||||
|
||||
ICON_NAME = "palette"
|
||||
|
||||
def __init__(self, *args, parent=None, **kwargs):
|
||||
super().__init__(*args, theme_update=True, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.setFixedSize(400, 600)
|
||||
layout = QVBoxLayout(self)
|
||||
dark_mode_button = DarkModeButton(self)
|
||||
layout.addWidget(dark_mode_button)
|
||||
|
||||
# Create a scroll area to hold the color boxes
|
||||
scroll_area = QScrollArea(self)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
# Create a frame to hold the color boxes
|
||||
self.frame = QFrame(self)
|
||||
self.frame_layout = QGridLayout(self.frame)
|
||||
self.frame_layout.setSpacing(0)
|
||||
self.frame_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
scroll_area.setWidget(self.frame)
|
||||
layout.addWidget(scroll_area)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.update_palette()
|
||||
|
||||
def apply_theme(self, theme) -> None:
|
||||
"""
|
||||
Apply the theme to the widget.
|
||||
|
||||
Args:
|
||||
theme (str): The theme to apply.
|
||||
"""
|
||||
self.update_palette()
|
||||
|
||||
def clear_palette(self) -> None:
|
||||
"""
|
||||
Clear the palette colors from the frame.
|
||||
Recursively removes all widgets and layouts in the frame layout.
|
||||
"""
|
||||
# Iterate over all items in the layout in reverse to safely remove them
|
||||
for i in reversed(range(self.frame_layout.count())):
|
||||
item = self.frame_layout.itemAt(i)
|
||||
|
||||
# If the item is a layout, clear its contents
|
||||
if isinstance(item, QHBoxLayout):
|
||||
# Recursively remove all widgets from the layout
|
||||
for j in reversed(range(item.count())):
|
||||
widget = item.itemAt(j).widget()
|
||||
if widget:
|
||||
item.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
self.frame_layout.removeItem(item)
|
||||
|
||||
# If the item is a widget, remove and delete it
|
||||
elif item.widget():
|
||||
widget = item.widget()
|
||||
self.frame_layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
||||
def update_palette(self) -> None:
|
||||
"""
|
||||
Update the palette colors in the frame.
|
||||
"""
|
||||
self.clear_palette()
|
||||
palette_label = QLabel("Palette Colors (e.g. palette.windowText().color())")
|
||||
palette_label.setStyleSheet("font-weight: bold;")
|
||||
self.frame_layout.addWidget(palette_label, 0, 0)
|
||||
|
||||
palette = get_theme_palette()
|
||||
# Add the palette colors (roles) to the frame
|
||||
palette_roles = [
|
||||
palette.windowText,
|
||||
palette.toolTipText,
|
||||
palette.placeholderText,
|
||||
palette.text,
|
||||
palette.buttonText,
|
||||
palette.highlight,
|
||||
palette.link,
|
||||
palette.light,
|
||||
palette.midlight,
|
||||
palette.mid,
|
||||
palette.shadow,
|
||||
palette.button,
|
||||
palette.brightText,
|
||||
palette.toolTipBase,
|
||||
palette.alternateBase,
|
||||
palette.dark,
|
||||
palette.base,
|
||||
palette.window,
|
||||
palette.highlightedText,
|
||||
palette.linkVisited,
|
||||
]
|
||||
|
||||
offset = 1
|
||||
for i, pal in enumerate(palette_roles):
|
||||
i += offset
|
||||
color = pal().color()
|
||||
label_layout = QHBoxLayout()
|
||||
color_label = QLabel(f"{pal().color().name()} ({pal.__name__})")
|
||||
background_label = self.background_label_with_clipboard(color)
|
||||
label_layout.addWidget(color_label)
|
||||
label_layout.addWidget(background_label)
|
||||
self.frame_layout.addLayout(label_layout, i, 0)
|
||||
|
||||
# add a horizontal spacer
|
||||
spacer = QLabel()
|
||||
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self.frame_layout.addWidget(spacer, i + 1, 0)
|
||||
|
||||
accent_colors_label = QLabel("Accent Colors (e.g. accent_colors.default)")
|
||||
accent_colors_label.setStyleSheet("font-weight: bold;")
|
||||
self.frame_layout.addWidget(accent_colors_label, i + 2, 0)
|
||||
|
||||
accent_colors = get_accent_colors()
|
||||
items = [
|
||||
(accent_colors.default, "default"),
|
||||
(accent_colors.success, "success"),
|
||||
(accent_colors.warning, "warning"),
|
||||
(accent_colors.emergency, "emergency"),
|
||||
(accent_colors.highlight, "highlight"),
|
||||
]
|
||||
|
||||
offset = len(palette_roles) + 2
|
||||
for i, (color, name) in enumerate(items):
|
||||
i += offset
|
||||
label_layout = QHBoxLayout()
|
||||
color_label = QLabel(f"{color.name()} ({name})")
|
||||
background_label = self.background_label_with_clipboard(color)
|
||||
label_layout.addWidget(color_label)
|
||||
label_layout.addWidget(background_label)
|
||||
self.frame_layout.addLayout(label_layout, i + 2, 0)
|
||||
|
||||
def background_label_with_clipboard(self, color) -> QLabel:
|
||||
"""
|
||||
Create a label with a background color that copies the color to the clipboard when clicked.
|
||||
|
||||
Args:
|
||||
color (QColor): The color to display in the background.
|
||||
|
||||
Returns:
|
||||
QLabel: The label with the background color.
|
||||
"""
|
||||
button = QLabel()
|
||||
button.setStyleSheet(f"QLabel {{ background-color: {color.name()}; }}")
|
||||
button.setToolTip("Click to copy color to clipboard")
|
||||
button.setCursor(Qt.PointingHandCursor)
|
||||
button.mousePressEvent = lambda event: QApplication.clipboard().setText(color.name())
|
||||
return button
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
viewer = PaletteViewer()
|
||||
viewer.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -71,7 +71,7 @@ class BECWidget(BECConnector):
|
||||
if theme is None:
|
||||
qapp = QApplication.instance()
|
||||
if hasattr(qapp, "theme"):
|
||||
theme = qapp.theme["theme"]
|
||||
theme = qapp.theme.theme
|
||||
else:
|
||||
theme = "dark"
|
||||
self.apply_theme(theme)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import re
|
||||
from typing import Literal
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import bec_qthemes
|
||||
import numpy as np
|
||||
@@ -8,25 +10,38 @@ import pyqtgraph as pg
|
||||
from bec_qthemes._os_appearance.listener import OSThemeSwitchListener
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QApplication, QPushButton, QToolButton
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_qthemes._main import AccentColors
|
||||
|
||||
|
||||
def get_theme_palette():
|
||||
if QApplication.instance() is None or not hasattr(QApplication.instance(), "theme"):
|
||||
theme = "dark"
|
||||
else:
|
||||
theme = QApplication.instance().theme["theme"]
|
||||
theme = QApplication.instance().theme.theme
|
||||
return bec_qthemes.load_palette(theme)
|
||||
|
||||
|
||||
def get_accent_colors() -> AccentColors | None:
|
||||
"""
|
||||
Get the accent colors for the current theme. These colors are extensions of the color palette
|
||||
and are used to highlight specific elements in the UI.
|
||||
"""
|
||||
if QApplication.instance() is None or not hasattr(QApplication.instance(), "theme"):
|
||||
return None
|
||||
return QApplication.instance().theme.accent_colors
|
||||
|
||||
|
||||
def _theme_update_callback():
|
||||
"""
|
||||
Internal callback function to update the theme based on the system theme.
|
||||
"""
|
||||
app = QApplication.instance()
|
||||
# pylint: disable=protected-access
|
||||
app.theme["theme"] = app.os_listener._theme.lower()
|
||||
app.theme_signal.theme_updated.emit(app.theme["theme"])
|
||||
app.theme.theme = app.os_listener._theme.lower()
|
||||
app.theme_signal.theme_updated.emit(app.theme.theme)
|
||||
apply_theme(app.os_listener._theme.lower())
|
||||
|
||||
|
||||
@@ -124,7 +139,7 @@ class Colors:
|
||||
color_index = int(color_selection[ii])
|
||||
color = cmap_colors[color_index]
|
||||
app = QApplication.instance()
|
||||
if hasattr(app, "theme") and app.theme["theme"] == "light":
|
||||
if hasattr(app, "theme") and app.theme.theme == "light":
|
||||
background = 255
|
||||
else:
|
||||
background = 0
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
""" Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
||||
The class is mainly designed for usage with the BECWaveform and 1D plots. """
|
||||
|
||||
import pyqtgraph as pg
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
|
||||
class LinearRegionWrapper(QObject):
|
||||
"""Wrapper class for the LinearRegionItem in pyqtgraph for 1D plots (BECWaveform)
|
||||
|
||||
Args:
|
||||
plot_item (pg.PlotItem): The plot item to add the region selector to.
|
||||
parent (QObject): The parent object.
|
||||
color (QColor): The color of the region selector.
|
||||
hover_color (QColor): The color of the region selector when the mouse is over it.
|
||||
"""
|
||||
|
||||
# Signal with the region tuble (start, end)
|
||||
region_changed = Signal(tuple)
|
||||
|
||||
def __init__(
|
||||
self, plot_item: pg.PlotItem, color: QColor = None, hover_color: QColor = None, parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._edge_width = 2
|
||||
self.plot_item = plot_item
|
||||
self.linear_region_selector = pg.LinearRegionItem()
|
||||
self.proxy = None
|
||||
self.change_roi_color((color, hover_color))
|
||||
|
||||
# Slot for changing the color of the region selector (edge and fill)
|
||||
@Slot(tuple)
|
||||
def change_roi_color(self, colors: tuple[QColor | str | tuple, QColor | str | tuple]):
|
||||
"""Change the color and hover color of the region selector.
|
||||
Hover color means the color when the mouse is over the region.
|
||||
|
||||
Args:
|
||||
colors (tuple): Tuple with the color and hover color
|
||||
"""
|
||||
color, hover_color = colors
|
||||
if color is not None:
|
||||
self.linear_region_selector.setBrush(pg.mkBrush(color))
|
||||
if hover_color is not None:
|
||||
self.linear_region_selector.setHoverBrush(pg.mkBrush(hover_color))
|
||||
|
||||
@Slot()
|
||||
def add_region_selector(self):
|
||||
"""Add the region selector to the plot item"""
|
||||
self.plot_item.addItem(self.linear_region_selector)
|
||||
# Use proxy to limit the update rate of the region change signal to 10Hz
|
||||
self.proxy = pg.SignalProxy(
|
||||
self.linear_region_selector.sigRegionChanged,
|
||||
rateLimit=10,
|
||||
slot=self._region_change_proxy,
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def remove_region_selector(self):
|
||||
"""Remove the region selector from the plot item"""
|
||||
self.proxy.disconnect()
|
||||
self.proxy = None
|
||||
self.plot_item.removeItem(self.linear_region_selector)
|
||||
|
||||
def _region_change_proxy(self):
|
||||
"""Emit the region change signal"""
|
||||
region = self.linear_region_selector.getRegion()
|
||||
self.region_changed.emit(region)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
self.remove_region_selector()
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['bec_progressbar.py']}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.bec_progressbar.bec_progressbar import BECProgressBar
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECProgressBar' name='bec_progress_bar'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class BECProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = BECProgressBar(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Utils"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(BECProgressBar.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_progress_bar"
|
||||
|
||||
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 "BECProgressBar"
|
||||
|
||||
def toolTip(self):
|
||||
return "A custom progress bar with smooth transitions and a modern design."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,258 @@
|
||||
import sys
|
||||
from string import Template
|
||||
|
||||
from qtpy.QtCore import Property, QEasingCurve, QPropertyAnimation, QRectF, Qt, QTimer, Slot
|
||||
from qtpy.QtGui import QColor, QPainter, QPainterPath
|
||||
from qtpy.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
|
||||
|
||||
class BECProgressBar(BECWidget, QWidget):
|
||||
"""
|
||||
A custom progress bar with smooth transitions. The displayed text can be customized using a template.
|
||||
"""
|
||||
|
||||
USER_ACCESS = [
|
||||
"set_value",
|
||||
"set_maximum",
|
||||
"set_minimum",
|
||||
"label_template",
|
||||
"label_template.setter",
|
||||
]
|
||||
ICON_NAME = "page_control"
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
accent_colors = get_accent_colors()
|
||||
|
||||
# internal values
|
||||
self._oversampling_factor = 50
|
||||
self._value = 0
|
||||
self._target_value = 0
|
||||
self._maximum = 100 * self._oversampling_factor
|
||||
|
||||
# User values
|
||||
self._user_value = 0
|
||||
self._user_minimum = 0
|
||||
self._user_maximum = 100
|
||||
self._label_template = "$value / $maximum - $percentage %"
|
||||
|
||||
# Color settings
|
||||
self._background_color = QColor(30, 30, 30)
|
||||
self._progress_color = accent_colors.highlight # QColor(210, 55, 130)
|
||||
|
||||
self._completed_color = accent_colors.success
|
||||
self._border_color = QColor(50, 50, 50)
|
||||
|
||||
# layout settings
|
||||
self._value_animation = QPropertyAnimation(self, b"_progressbar_value")
|
||||
self._value_animation.setDuration(200)
|
||||
self._value_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
||||
|
||||
# label on top of the progress bar
|
||||
self.center_label = QLabel(self)
|
||||
self.center_label.setAlignment(Qt.AlignCenter)
|
||||
self.center_label.setStyleSheet("color: white;")
|
||||
self.center_label.setMinimumSize(0, 0)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(self.center_label)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.update()
|
||||
|
||||
@Property(str, doc="The template for the center label. Use $value, $maximum, and $percentage.")
|
||||
def label_template(self):
|
||||
"""
|
||||
The template for the center label. Use $value, $maximum, and $percentage to insert the values.
|
||||
|
||||
Examples:
|
||||
>>> progressbar.label_template = "$value / $maximum - $percentage %"
|
||||
>>> progressbar.label_template = "$value / $percentage %"
|
||||
|
||||
"""
|
||||
return self._label_template
|
||||
|
||||
@label_template.setter
|
||||
def label_template(self, template):
|
||||
self._label_template = template
|
||||
self.set_value(self._user_value)
|
||||
self.update()
|
||||
|
||||
@Property(float, designable=False)
|
||||
def _progressbar_value(self):
|
||||
"""
|
||||
The current value of the progress bar.
|
||||
"""
|
||||
return self._value
|
||||
|
||||
@_progressbar_value.setter
|
||||
def _progressbar_value(self, val):
|
||||
self._value = val
|
||||
self.update()
|
||||
|
||||
def _update_template(self):
|
||||
template = Template(self._label_template)
|
||||
return template.safe_substitute(
|
||||
value=self._user_value,
|
||||
maximum=self._user_maximum,
|
||||
percentage=int((self.map_value(self._user_value) / self._maximum) * 100),
|
||||
)
|
||||
|
||||
@Slot(float)
|
||||
@Slot(int)
|
||||
def set_value(self, value):
|
||||
"""
|
||||
Set the value of the progress bar.
|
||||
|
||||
Args:
|
||||
value (float): The value to set.
|
||||
"""
|
||||
if value > self._user_maximum:
|
||||
value = self._user_maximum
|
||||
elif value < self._user_minimum:
|
||||
value = self._user_minimum
|
||||
self._target_value = self.map_value(value)
|
||||
self._user_value = value
|
||||
self.center_label.setText(self._update_template())
|
||||
self.animate_progress()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
rect = self.rect().adjusted(10, 0, -10, -1)
|
||||
|
||||
# Draw background
|
||||
painter.setBrush(self._background_color)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawRoundedRect(rect, 10, 10) # Rounded corners
|
||||
|
||||
# Draw border
|
||||
painter.setBrush(Qt.NoBrush)
|
||||
painter.setPen(self._border_color)
|
||||
painter.drawRoundedRect(rect, 10, 10)
|
||||
|
||||
# Determine progress color based on completion
|
||||
if self._value >= self._maximum:
|
||||
current_color = self._completed_color
|
||||
else:
|
||||
current_color = self._progress_color
|
||||
|
||||
# Set clipping region to preserve the background's rounded corners
|
||||
progress_rect = rect.adjusted(
|
||||
0, 0, int(-rect.width() + (self._value / self._maximum) * rect.width()), 0
|
||||
)
|
||||
clip_path = QPainterPath()
|
||||
clip_path.addRoundedRect(QRectF(rect), 10, 10) # Clip to the background's rounded corners
|
||||
painter.setClipPath(clip_path)
|
||||
|
||||
# Draw progress bar
|
||||
painter.setBrush(current_color)
|
||||
painter.drawRect(progress_rect) # Less rounded, no additional rounding
|
||||
|
||||
painter.end()
|
||||
|
||||
def animate_progress(self):
|
||||
"""
|
||||
Animate the progress bar from the current value to the target value.
|
||||
"""
|
||||
self._value_animation.stop()
|
||||
self._value_animation.setStartValue(self._value)
|
||||
self._value_animation.setEndValue(self._target_value)
|
||||
self._value_animation.start()
|
||||
|
||||
@Property(float)
|
||||
def maximum(self):
|
||||
"""
|
||||
The maximum value of the progress bar.
|
||||
"""
|
||||
return self._user_maximum
|
||||
|
||||
@maximum.setter
|
||||
def maximum(self, maximum: float):
|
||||
"""
|
||||
Set the maximum value of the progress bar.
|
||||
"""
|
||||
self.set_maximum(maximum)
|
||||
|
||||
@Property(float)
|
||||
def minimum(self):
|
||||
"""
|
||||
The minimum value of the progress bar.
|
||||
"""
|
||||
return self._user_minimum
|
||||
|
||||
@minimum.setter
|
||||
def minimum(self, minimum: float):
|
||||
self.set_minimum(minimum)
|
||||
|
||||
@Property(float)
|
||||
def initial_value(self):
|
||||
"""
|
||||
The initial value of the progress bar.
|
||||
"""
|
||||
return self._user_value
|
||||
|
||||
@initial_value.setter
|
||||
def initial_value(self, value: float):
|
||||
self.set_value(value)
|
||||
|
||||
@Slot(float)
|
||||
def set_maximum(self, maximum: float):
|
||||
"""
|
||||
Set the maximum value of the progress bar.
|
||||
|
||||
Args:
|
||||
maximum (float): The maximum value.
|
||||
"""
|
||||
self._user_maximum = maximum
|
||||
self.set_value(self._user_value) # Update the value to fit the new range
|
||||
self.update()
|
||||
|
||||
@Slot(float)
|
||||
def set_minimum(self, minimum: float):
|
||||
"""
|
||||
Set the minimum value of the progress bar.
|
||||
|
||||
Args:
|
||||
minimum (float): The minimum value.
|
||||
"""
|
||||
self._user_minimum = minimum
|
||||
self.set_value(self._user_value) # Update the value to fit the new range
|
||||
self.update()
|
||||
|
||||
def map_value(self, value: float):
|
||||
"""
|
||||
Map the user value to the range [0, 100*self._oversampling_factor] for the progress
|
||||
"""
|
||||
return (
|
||||
(value - self._user_minimum) / (self._user_maximum - self._user_minimum) * self._maximum
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
progressBar = BECProgressBar()
|
||||
progressBar.show()
|
||||
progressBar.set_minimum(-100)
|
||||
progressBar.set_maximum(0)
|
||||
|
||||
# Example of setting values
|
||||
def update_progress():
|
||||
value = progressBar._user_value + 2.5
|
||||
if value > progressBar._user_maximum:
|
||||
value = -100 # progressBar._maximum / progressBar._upsampling_factor
|
||||
progressBar.set_value(value)
|
||||
|
||||
timer = QTimer()
|
||||
timer.timeout.connect(update_progress)
|
||||
timer.start(200) # Update every half second
|
||||
|
||||
sys.exit(app.exec())
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.bec_progressbar.bec_progress_bar_plugin import BECProgressBarPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECProgressBarPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,24 +1,24 @@
|
||||
"""
|
||||
BECConsole is a Qt widget that runs a Bash shell. The widget can be used and
|
||||
embedded like any other Qt widget.
|
||||
BECConsole is a Qt widget that runs a Bash shell.
|
||||
|
||||
BECConsole is powered by Pyte, a Python based terminal emulator
|
||||
BECConsole VT100 emulation is powered by Pyte,
|
||||
(https://github.com/selectel/pyte).
|
||||
"""
|
||||
|
||||
import collections
|
||||
import fcntl
|
||||
import html
|
||||
import os
|
||||
import pty
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import pyte
|
||||
from pyte.screens import History
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
from qtpy.QtCore import QSize, QSocketNotifier, Qt
|
||||
from qtpy.QtCore import Property as pyqtProperty
|
||||
from qtpy.QtCore import QSize, QSocketNotifier, Qt, QTimer
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtGui import QClipboard, QTextCursor
|
||||
from qtpy.QtGui import QClipboard, QColor, QPalette, QTextCursor
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QScrollBar, QSizePolicy
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
@@ -137,13 +137,52 @@ def QtKeyToAscii(event):
|
||||
|
||||
|
||||
class Screen(pyte.HistoryScreen):
|
||||
def __init__(self, stdin_fd, numColumns, numLines, historyLength):
|
||||
super().__init__(numColumns, numLines, historyLength, ratio=1 / numLines)
|
||||
def __init__(self, stdin_fd, cols, rows, historyLength):
|
||||
super().__init__(cols, rows, historyLength, ratio=1 / rows)
|
||||
self._fd = stdin_fd
|
||||
|
||||
def write_process_input(self, data):
|
||||
"""Response to CPR request for example"""
|
||||
os.write(self._fd, data.encode("utf-8"))
|
||||
"""Response to CPR request (for example),
|
||||
this can be for other requests
|
||||
"""
|
||||
try:
|
||||
os.write(self._fd, data.encode("utf-8"))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def resize(self, lines, columns):
|
||||
lines = lines or self.lines
|
||||
columns = columns or self.columns
|
||||
|
||||
if lines == self.lines and columns == self.columns:
|
||||
return # No changes.
|
||||
|
||||
self.dirty.clear()
|
||||
self.dirty.update(range(lines))
|
||||
|
||||
self.save_cursor()
|
||||
if lines < self.lines:
|
||||
if lines <= self.cursor.y:
|
||||
nlines_to_move_up = self.lines - lines
|
||||
for i in range(nlines_to_move_up):
|
||||
line = self.buffer[i] # .pop(0)
|
||||
self.history.top.append(line)
|
||||
self.cursor_position(0, 0)
|
||||
self.delete_lines(nlines_to_move_up)
|
||||
self.restore_cursor()
|
||||
self.cursor.y -= nlines_to_move_up
|
||||
else:
|
||||
self.restore_cursor()
|
||||
|
||||
self.lines, self.columns = lines, columns
|
||||
self.history = History(
|
||||
self.history.top,
|
||||
self.history.bottom,
|
||||
1 / self.lines,
|
||||
self.history.size,
|
||||
self.history.position,
|
||||
)
|
||||
self.set_margins()
|
||||
|
||||
|
||||
class Backend(QtCore.QObject):
|
||||
@@ -155,17 +194,17 @@ class Backend(QtCore.QObject):
|
||||
"""
|
||||
|
||||
# Signals to communicate with ``_TerminalWidget``.
|
||||
startWork = pyqtSignal()
|
||||
dataReady = pyqtSignal(object)
|
||||
processExited = pyqtSignal()
|
||||
|
||||
def __init__(self, fd, numColumns, numLines):
|
||||
def __init__(self, fd, cols, rows):
|
||||
super().__init__()
|
||||
|
||||
# File descriptor that connects to Bash process.
|
||||
self.fd = fd
|
||||
|
||||
# Setup Pyte (hard coded display size for now).
|
||||
self.screen = Screen(self.fd, numColumns, numLines, 10000)
|
||||
self.screen = Screen(self.fd, cols, rows, 10000)
|
||||
self.stream = pyte.ByteStream()
|
||||
self.stream.attach(self.screen)
|
||||
|
||||
@@ -174,12 +213,14 @@ class Backend(QtCore.QObject):
|
||||
|
||||
def _fd_readable(self):
|
||||
"""
|
||||
Poll the Bash output, run it through Pyte, and notify the main applet.
|
||||
Poll the Bash output, run it through Pyte, and notify
|
||||
"""
|
||||
# Read the shell output until the file descriptor is closed.
|
||||
try:
|
||||
out = os.read(self.fd, 2**16)
|
||||
except OSError:
|
||||
self.processExited.emit()
|
||||
self.notifier.setEnabled(False)
|
||||
return
|
||||
|
||||
# Feed output into Pyte's state machine and send the new screen
|
||||
@@ -188,93 +229,256 @@ class Backend(QtCore.QObject):
|
||||
self.dataReady.emit(self.screen)
|
||||
|
||||
|
||||
class BECConsole(QtWidgets.QScrollArea):
|
||||
class BECConsole(QtWidgets.QWidget):
|
||||
"""Container widget for the terminal text area"""
|
||||
|
||||
def __init__(self, parent=None, numLines=50, numColumns=125):
|
||||
ICON_NAME = "terminal"
|
||||
|
||||
def __init__(self, parent=None, cols=132):
|
||||
super().__init__(parent)
|
||||
|
||||
self.innerWidget = QtWidgets.QWidget(self)
|
||||
QHBoxLayout(self.innerWidget)
|
||||
self.innerWidget.layout().setContentsMargins(0, 0, 0, 0)
|
||||
self.term = _TerminalWidget(self, cols, rows=43)
|
||||
self.scroll_bar = QScrollBar(Qt.Vertical, self)
|
||||
# self.scroll_bar.hide()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.addWidget(self.term)
|
||||
layout.addWidget(self.scroll_bar)
|
||||
layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||
|
||||
self.term = _TerminalWidget(self.innerWidget, numLines, numColumns)
|
||||
self.term.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.innerWidget.layout().addWidget(self.term)
|
||||
pal = QPalette()
|
||||
self.set_bgcolor(pal.window().color())
|
||||
self.set_fgcolor(pal.windowText().color())
|
||||
self.term.set_scroll_bar(self.scroll_bar)
|
||||
self.set_cmd("bec --nogui")
|
||||
|
||||
self.scroll_bar = QScrollBar(Qt.Vertical, self.term)
|
||||
self.innerWidget.layout().addWidget(self.scroll_bar)
|
||||
self._check_designer_timer = QTimer()
|
||||
self._check_designer_timer.timeout.connect(self.check_designer)
|
||||
self._check_designer_timer.start(1000)
|
||||
|
||||
self.term.set_scroll(self.scroll_bar)
|
||||
def minimumSizeHint(self):
|
||||
size = self.term.sizeHint()
|
||||
size.setWidth(size.width() + self.scroll_bar.width())
|
||||
return size
|
||||
|
||||
self.setWidget(self.innerWidget)
|
||||
def sizeHint(self):
|
||||
return self.minimumSizeHint()
|
||||
|
||||
def start(self, cmd=["bec", "--nogui"], deactivate_ctrl_d=True):
|
||||
def check_designer(self, calls={"n": 0}):
|
||||
calls["n"] += 1
|
||||
if self.term.fd is not None:
|
||||
# already started
|
||||
self._check_designer_timer.stop()
|
||||
elif self.window().windowTitle().endswith("[Preview]"):
|
||||
# assuming Designer preview -> start
|
||||
self._check_designer_timer.stop()
|
||||
self.term.start()
|
||||
elif calls["n"] >= 3:
|
||||
# assuming not in Designer -> stop checking
|
||||
self._check_designer_timer.stop()
|
||||
|
||||
def get_rows(self):
|
||||
return self.term.rows
|
||||
|
||||
def set_rows(self, rows):
|
||||
self.term.rows = rows
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
|
||||
def get_cols(self):
|
||||
return self.term.cols
|
||||
|
||||
def set_cols(self, cols):
|
||||
self.term.cols = cols
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
|
||||
def get_bgcolor(self):
|
||||
return QColor.fromString(self.term.bg_color)
|
||||
|
||||
def set_bgcolor(self, color):
|
||||
self.term.bg_color = color.name(QColor.HexRgb)
|
||||
|
||||
def get_fgcolor(self):
|
||||
return QColor.fromString(self.term.fg_color)
|
||||
|
||||
def set_fgcolor(self, color):
|
||||
self.term.fg_color = color.name(QColor.HexRgb)
|
||||
|
||||
def get_cmd(self):
|
||||
return self.term._cmd
|
||||
|
||||
def set_cmd(self, cmd):
|
||||
self.term._cmd = cmd
|
||||
if self.term.fd is None:
|
||||
# not started yet
|
||||
self.term.clear()
|
||||
self.term.appendHtml(f"<h2>BEC Console - {repr(cmd)}</h2>")
|
||||
|
||||
def start(self, deactivate_ctrl_d=True):
|
||||
self.term.start(deactivate_ctrl_d=deactivate_ctrl_d)
|
||||
|
||||
def push(self, text):
|
||||
"""Push some text to the terminal"""
|
||||
return self.term.push(text)
|
||||
|
||||
cols = pyqtProperty(int, get_cols, set_cols)
|
||||
rows = pyqtProperty(int, get_rows, set_rows)
|
||||
bgcolor = pyqtProperty(QColor, get_bgcolor, set_bgcolor)
|
||||
fgcolor = pyqtProperty(QColor, get_fgcolor, set_fgcolor)
|
||||
cmd = pyqtProperty(str, get_cmd, set_cmd)
|
||||
|
||||
|
||||
class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
"""
|
||||
Start ``Backend`` process and render Pyte output as text.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, numColumns=125, numLines=50, **kwargs):
|
||||
super().__init__(parent)
|
||||
|
||||
def __init__(self, parent, cols=125, rows=50, **kwargs):
|
||||
# file descriptor to communicate with the subprocess
|
||||
self.fd = None
|
||||
self.backend = None
|
||||
self.lock = threading.Lock()
|
||||
# command to execute
|
||||
self._cmd = None
|
||||
self._cmd = ""
|
||||
# should ctrl-d be deactivated ? (prevent Python exit)
|
||||
self._deactivate_ctrl_d = False
|
||||
|
||||
# Default colors
|
||||
pal = QPalette()
|
||||
self._fg_color = pal.text().color().name()
|
||||
self._bg_color = pal.base().color().name()
|
||||
|
||||
# Specify the terminal size in terms of lines and columns.
|
||||
self.numLines = numLines
|
||||
self.numColumns = numColumns
|
||||
self.output = [""] * numLines
|
||||
self._rows = rows
|
||||
self._cols = cols
|
||||
self.output = collections.deque()
|
||||
|
||||
super().__init__(parent)
|
||||
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding)
|
||||
|
||||
# Disable default scrollbars (we use our own, to be set via .set_scroll_bar())
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.scroll_bar = None
|
||||
|
||||
# Use Monospace fonts and disable line wrapping.
|
||||
self.setFont(QtGui.QFont("Courier", 9))
|
||||
self.setFont(QtGui.QFont("Monospace"))
|
||||
self.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
|
||||
|
||||
# Disable vertical scrollbar (we use our own, to be set via .set_scroll())
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
|
||||
fmt = QtGui.QFontMetrics(self.font())
|
||||
self._char_width = fmt.width("w")
|
||||
self._char_height = fmt.height()
|
||||
self.setCursorWidth(self._char_width)
|
||||
# self.setStyleSheet("QPlainTextEdit { color: #ffff00; background-color: #303030; } ");
|
||||
char_width = fmt.width("w")
|
||||
self.setCursorWidth(char_width)
|
||||
|
||||
def start(self, deactivate_ctrl_d=False):
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
self.update_stylesheet()
|
||||
|
||||
@property
|
||||
def bg_color(self):
|
||||
return self._bg_color
|
||||
|
||||
@bg_color.setter
|
||||
def bg_color(self, hexcolor):
|
||||
self._bg_color = hexcolor
|
||||
self.update_stylesheet()
|
||||
|
||||
@property
|
||||
def fg_color(self):
|
||||
return self._fg_color
|
||||
|
||||
@fg_color.setter
|
||||
def fg_color(self, hexcolor):
|
||||
self._fg_color = hexcolor
|
||||
self.update_stylesheet()
|
||||
|
||||
def update_stylesheet(self):
|
||||
self.setStyleSheet(
|
||||
f"QPlainTextEdit {{ border: 0; color: {self._fg_color}; background-color: {self._bg_color}; }} "
|
||||
)
|
||||
|
||||
@property
|
||||
def rows(self):
|
||||
return self._rows
|
||||
|
||||
@rows.setter
|
||||
def rows(self, rows: int):
|
||||
if self.backend is None:
|
||||
# not initialized yet, ok to change
|
||||
self._rows = rows
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
else:
|
||||
raise RuntimeError("Cannot change rows after console is started.")
|
||||
|
||||
@property
|
||||
def cols(self):
|
||||
return self._cols
|
||||
|
||||
@cols.setter
|
||||
def cols(self, cols: int):
|
||||
if self.fd is None:
|
||||
# not initialized yet, ok to change
|
||||
self._cols = cols
|
||||
self.adjustSize()
|
||||
self.updateGeometry()
|
||||
else:
|
||||
raise RuntimeError("Cannot change cols after console is started.")
|
||||
|
||||
def start(self, deactivate_ctrl_d: bool = False):
|
||||
self._deactivate_ctrl_d = deactivate_ctrl_d
|
||||
|
||||
# Start the Bash process
|
||||
self.fd = self.forkShell()
|
||||
self.update_term_size()
|
||||
|
||||
# Create the ``Backend`` object
|
||||
self.backend = Backend(self.fd, self.numColumns, self.numLines)
|
||||
self.backend.dataReady.connect(self.dataReady)
|
||||
# Start the Bash process
|
||||
self.fd = self.fork_shell()
|
||||
|
||||
if self.fd:
|
||||
# Create the ``Backend`` object
|
||||
self.backend = Backend(self.fd, self.cols, self.rows)
|
||||
self.backend.dataReady.connect(self.data_ready)
|
||||
self.backend.processExited.connect(self.process_exited)
|
||||
else:
|
||||
self.process_exited()
|
||||
|
||||
def process_exited(self):
|
||||
self.fd = None
|
||||
self.clear()
|
||||
self.appendHtml(f"<br><h2>{repr(self._cmd)} - Process exited.</h2>")
|
||||
self.setReadOnly(True)
|
||||
|
||||
def data_ready(self, screen):
|
||||
"""Handle new screen: redraw, set scroll bar max and slider, move cursor to its position
|
||||
|
||||
This method is triggered via a signal from ``Backend``.
|
||||
"""
|
||||
self.redraw_screen()
|
||||
self.adjust_scroll_bar()
|
||||
self.move_cursor()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
width = self._char_width * self.numColumns
|
||||
height = self._char_height * self.numLines
|
||||
return QSize(width, height + 20)
|
||||
"""Return minimum size for current cols and rows"""
|
||||
fmt = QtGui.QFontMetrics(self.font())
|
||||
char_width = fmt.width("w")
|
||||
char_height = fmt.height()
|
||||
width = char_width * self.cols
|
||||
height = char_height * self.rows
|
||||
return QSize(width, height)
|
||||
|
||||
def set_scroll(self, scroll):
|
||||
self.scroll = scroll
|
||||
self.scroll.setMinimum(0)
|
||||
self.scroll.valueChanged.connect(self.scroll_value_change)
|
||||
def sizeHint(self):
|
||||
return self.minimumSizeHint()
|
||||
|
||||
def scroll_value_change(self, value, old={"value": 0}):
|
||||
def set_scroll_bar(self, scroll_bar):
|
||||
self.scroll_bar = scroll_bar
|
||||
self.scroll_bar.setMinimum(0)
|
||||
self.scroll_bar.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
def scroll_value_change(self, value, old={"value": -1}):
|
||||
if self.backend is None:
|
||||
return
|
||||
if old["value"] == -1:
|
||||
old["value"] = self.scroll_bar.maximum()
|
||||
if value <= old["value"]:
|
||||
# scroll up
|
||||
# value is number of lines from the start
|
||||
@@ -288,13 +492,35 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
for i in range(nlines):
|
||||
self.backend.screen.next_page()
|
||||
old["value"] = value
|
||||
self.dataReady(self.backend.screen, reset_scroll=False)
|
||||
self.redraw_screen()
|
||||
|
||||
def adjust_scroll_bar(self):
|
||||
sb = self.scroll_bar
|
||||
sb.valueChanged.disconnect(self.scroll_value_change)
|
||||
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
||||
sb.setMaximum(tmp if tmp > 0 else 0)
|
||||
sb.setSliderPosition(tmp if tmp > 0 else 0)
|
||||
# if tmp > 0:
|
||||
# # show scrollbar, but delayed - prevent recursion with widget size change
|
||||
# QTimer.singleShot(0, scrollbar.show)
|
||||
# else:
|
||||
# QTimer.singleShot(0, scrollbar.hide)
|
||||
sb.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
def write(self, data):
|
||||
try:
|
||||
os.write(self.fd, data)
|
||||
except (IOError, OSError):
|
||||
self.process_exited()
|
||||
|
||||
@Slot(object)
|
||||
def keyPressEvent(self, event):
|
||||
"""
|
||||
Redirect all keystrokes to the terminal process.
|
||||
"""
|
||||
if self.fd is None:
|
||||
# not started
|
||||
return
|
||||
# Convert the Qt key to the correct ASCII code.
|
||||
if (
|
||||
self._deactivate_ctrl_d
|
||||
@@ -311,15 +537,17 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
# MacOS only: CMD-V handling
|
||||
self._push_clipboard()
|
||||
elif code is not None:
|
||||
os.write(self.fd, code)
|
||||
self.write(code)
|
||||
|
||||
def push(self, text):
|
||||
"""
|
||||
Write 'text' to terminal
|
||||
"""
|
||||
os.write(self.fd, text.encode("utf-8"))
|
||||
self.write(text.encode("utf-8"))
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
if self.fd is None:
|
||||
return
|
||||
menu = self.createStandardContextMenu()
|
||||
for action in menu.actions():
|
||||
# remove all actions except copy and paste
|
||||
@@ -341,7 +569,20 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
clipboard = QApplication.instance().clipboard()
|
||||
self.push(clipboard.text())
|
||||
|
||||
def move_cursor(self):
|
||||
textCursor = self.textCursor()
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
||||
)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
||||
)
|
||||
self.setTextCursor(textCursor)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self.fd is None:
|
||||
return
|
||||
if event.button() == Qt.MiddleButton:
|
||||
# push primary selection buffer ("mouse clipboard") to terminal
|
||||
clipboard = QApplication.instance().clipboard()
|
||||
@@ -355,34 +596,34 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
# mouse was used to select text -> nothing to do
|
||||
pass
|
||||
else:
|
||||
# a simple 'click', make cursor going to end
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Down, QTextCursor.MoveAnchor, self.backend.screen.cursor.y
|
||||
)
|
||||
textCursor.movePosition(
|
||||
QTextCursor.Right, QTextCursor.MoveAnchor, self.backend.screen.cursor.x
|
||||
)
|
||||
self.setTextCursor(textCursor)
|
||||
self.ensureCursorVisible()
|
||||
# a simple 'click', move scrollbar to end
|
||||
self.scroll_bar.setSliderPosition(self.scroll_bar.maximum())
|
||||
self.move_cursor()
|
||||
return None
|
||||
return super().mouseReleaseEvent(event)
|
||||
|
||||
def dataReady(self, screenData, reset_scroll=True):
|
||||
def redraw_screen(self):
|
||||
"""
|
||||
Render the new screen as text into the widget.
|
||||
Render the screen as formatted text into the widget.
|
||||
"""
|
||||
screen = self.backend.screen
|
||||
|
||||
This method is triggered via a signal from ``Backend``.
|
||||
"""
|
||||
with self.lock:
|
||||
# Clear the widget
|
||||
# Clear the widget
|
||||
if screen.dirty:
|
||||
self.clear()
|
||||
while len(self.output) < (max(screen.dirty) + 1):
|
||||
self.output.append("")
|
||||
while len(self.output) > (max(screen.dirty) + 1):
|
||||
self.output.pop()
|
||||
|
||||
# Prepare the HTML output
|
||||
for line_no in screenData.dirty:
|
||||
for line_no in screen.dirty:
|
||||
line = text = ""
|
||||
style = old_style = ""
|
||||
for ch in screenData.buffer[line_no].values():
|
||||
old_idx = 0
|
||||
for idx, ch in screen.buffer[line_no].items():
|
||||
text += " " * (idx - old_idx - 1)
|
||||
old_idx = idx
|
||||
style = f"{'background-color:%s;' % ansi_colors.get(ch.bg, ansi_colors['black']) if ch.bg!='default' else ''}{'color:%s;' % ansi_colors.get(ch.fg, ansi_colors['white']) if ch.fg!='default' else ''}{'font-weight:bold;' if ch.bold else ''}{'font-style:italic;' if ch.italics else ''}"
|
||||
if style != old_style:
|
||||
if old_style:
|
||||
@@ -396,45 +637,47 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
line += f"<span style={repr(style)}>{html.escape(text, quote=True)}</span>"
|
||||
else:
|
||||
line += html.escape(text, quote=True)
|
||||
# do a check at the cursor position:
|
||||
# it is possible x pos > output line length,
|
||||
# for example if last escape codes are "cursor forward" past end of text,
|
||||
# like IPython does for "..." prompt (in a block, like "for" loop or "while" for example)
|
||||
# In this case, cursor is at 12 but last text output is at 8 -> insert spaces
|
||||
if line_no == screen.cursor.y:
|
||||
llen = len(screen.buffer[line_no])
|
||||
if llen < screen.cursor.x:
|
||||
line += " " * (screen.cursor.x - llen)
|
||||
self.output[line_no] = line
|
||||
# fill the text area with HTML contents in one go
|
||||
self.appendHtml(f"<pre>{chr(10).join(self.output)}</pre>")
|
||||
# done updates, all clean
|
||||
screenData.dirty.clear()
|
||||
# did updates, all clean
|
||||
screen.dirty.clear()
|
||||
|
||||
# Activate cursor
|
||||
textCursor = self.textCursor()
|
||||
textCursor.setPosition(0)
|
||||
textCursor.movePosition(QTextCursor.Down, QTextCursor.MoveAnchor, screenData.cursor.y)
|
||||
textCursor.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, screenData.cursor.x)
|
||||
self.setTextCursor(textCursor)
|
||||
self.ensureCursorVisible()
|
||||
def update_term_size(self):
|
||||
fmt = QtGui.QFontMetrics(self.font())
|
||||
char_width = fmt.width("w")
|
||||
char_height = fmt.height()
|
||||
self._cols = int(self.width() / char_width)
|
||||
self._rows = int(self.height() / char_height)
|
||||
|
||||
# manage scroll
|
||||
if reset_scroll:
|
||||
self.scroll.valueChanged.disconnect(self.scroll_value_change)
|
||||
tmp = len(self.backend.screen.history.top) + len(self.backend.screen.history.bottom)
|
||||
self.scroll.setMaximum(tmp if tmp > 0 else 0)
|
||||
self.scroll.setSliderPosition(len(self.backend.screen.history.top))
|
||||
self.scroll.valueChanged.connect(self.scroll_value_change)
|
||||
|
||||
# def resizeEvent(self, event):
|
||||
# with self.lock:
|
||||
# self.numColumns = int(self.width() / self._char_width)
|
||||
# self.numLines = int(self.height() / self._char_height)
|
||||
# self.output = [""] * self.numLines
|
||||
# print("RESIZING TO", self.numColumns, "x", self.numLines)
|
||||
# self.backend.screen.resize(self.numLines, self.numColumns)
|
||||
def resizeEvent(self, event):
|
||||
self.update_term_size()
|
||||
if self.fd:
|
||||
self.backend.screen.resize(self._rows, self._cols)
|
||||
self.redraw_screen()
|
||||
self.adjust_scroll_bar()
|
||||
self.move_cursor()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if not self.fd:
|
||||
return
|
||||
y = event.angleDelta().y()
|
||||
if y > 0:
|
||||
self.backend.screen.prev_page()
|
||||
else:
|
||||
self.backend.screen.next_page()
|
||||
self.dataReady(self.backend.screen, reset_scroll=False)
|
||||
self.redraw_screen()
|
||||
|
||||
def forkShell(self):
|
||||
def fork_shell(self):
|
||||
"""
|
||||
Fork the current process and execute bec in shell.
|
||||
"""
|
||||
@@ -443,32 +686,30 @@ class _TerminalWidget(QtWidgets.QPlainTextEdit):
|
||||
except (IOError, OSError):
|
||||
return False
|
||||
if pid == 0:
|
||||
# Safe way to make it work under BSD and Linux
|
||||
try:
|
||||
ls = os.environ["LANG"].split(".")
|
||||
except KeyError:
|
||||
ls = []
|
||||
if len(ls) < 2:
|
||||
ls = ["en_US", "UTF-8"]
|
||||
os.putenv("COLUMNS", str(self.cols))
|
||||
os.putenv("LINES", str(self.rows))
|
||||
os.putenv("TERM", "linux")
|
||||
os.putenv("LANG", ls[0] + ".UTF-8")
|
||||
if not self._cmd:
|
||||
self._cmd = os.environ["SHELL"]
|
||||
cmd = self._cmd
|
||||
if isinstance(cmd, str):
|
||||
cmd = cmd.split()
|
||||
try:
|
||||
os.putenv("COLUMNS", str(self.numColumns))
|
||||
os.putenv("LINES", str(self.numLines))
|
||||
os.putenv("TERM", "linux")
|
||||
os.putenv("LANG", ls[0] + ".UTF-8")
|
||||
if isinstance(self._cmd, str):
|
||||
os.execvp(self._cmd, self._cmd)
|
||||
else:
|
||||
os.execvp(self._cmd[0], self._cmd)
|
||||
# print "child_pid", child_pid, sts
|
||||
os.execvp(cmd[0], cmd)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
# self.proc_finish(sid)
|
||||
os._exit(0)
|
||||
else:
|
||||
# We are in the parent process.
|
||||
# Set file control
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
print("Spawned Bash shell (PID {})".format(pid))
|
||||
return fd
|
||||
|
||||
|
||||
@@ -478,17 +719,13 @@ if __name__ == "__main__":
|
||||
|
||||
from qtpy import QtGui, QtWidgets
|
||||
|
||||
# Terminal size in characters.
|
||||
numLines = 25
|
||||
numColumns = 100
|
||||
|
||||
# Create the Qt application and QBash instance.
|
||||
# Create the Qt application and console.
|
||||
app = QtWidgets.QApplication([])
|
||||
mainwin = QtWidgets.QMainWindow()
|
||||
title = "BECConsole ({}x{})".format(numColumns, numLines)
|
||||
title = "BECConsole"
|
||||
mainwin.setWindowTitle(title)
|
||||
|
||||
console = BECConsole(mainwin, numColumns, numLines)
|
||||
console = BECConsole(mainwin)
|
||||
mainwin.setCentralWidget(console)
|
||||
console.start()
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['console.py']}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
import os
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.console.console import BECConsole
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='BECConsole' name='bec_console'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class BECConsolePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = BECConsole(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "BEC Console"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(BECConsole.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "bec_console"
|
||||
|
||||
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 "BECConsole"
|
||||
|
||||
def toolTip(self):
|
||||
return "A terminal-like vt100 widget."
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.console.console_plugin import BECConsolePlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(BECConsolePlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -165,21 +165,18 @@ class DapComboBox(BECWidget, QWidget):
|
||||
return True
|
||||
|
||||
|
||||
# pragma: no cover
|
||||
def main():
|
||||
"""Main function to run the DapComboBox widget."""
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("auto")
|
||||
widget = DapComboBox()
|
||||
app = QApplication([])
|
||||
set_theme("dark")
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(DapComboBox())
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
app.exec_()
|
||||
|
||||
@@ -60,7 +60,7 @@ class DarkModeButton(BECWidget, QWidget):
|
||||
bool: True if dark mode is enabled, False otherwise.
|
||||
"""
|
||||
qapp = QApplication.instance()
|
||||
if hasattr(qapp, "theme") and qapp.theme["theme"] == "dark":
|
||||
if hasattr(qapp, "theme") and qapp.theme.theme == "dark":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -31,6 +31,7 @@ class AxisSettings(SettingWidget):
|
||||
|
||||
# Top Box
|
||||
WidgetIO.set_value(self.ui.plot_title, axis_config["title"])
|
||||
self.ui.switch_outer_axes.checked = axis_config["outer_axes"]
|
||||
|
||||
# X Axis Box
|
||||
WidgetIO.set_value(self.ui.x_label, axis_config["x_label"])
|
||||
@@ -63,6 +64,7 @@ class AxisSettings(SettingWidget):
|
||||
@Slot()
|
||||
def accept_changes(self):
|
||||
title = WidgetIO.get_value(self.ui.plot_title)
|
||||
outer_axes = self.ui.switch_outer_axes.checked
|
||||
|
||||
# X Axis
|
||||
x_label = WidgetIO.get_value(self.ui.x_label)
|
||||
@@ -86,3 +88,4 @@ class AxisSettings(SettingWidget):
|
||||
y_lim=y_lim,
|
||||
)
|
||||
self.target_widget.set_grid(x_grid, y_grid)
|
||||
self.target_widget.set_outer_axes(outer_axes)
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>417</width>
|
||||
<height>250</height>
|
||||
<width>427</width>
|
||||
<height>270</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
@@ -26,7 +26,28 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="1">
|
||||
<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>
|
||||
@@ -120,7 +141,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QGroupBox" name="x_axis_box">
|
||||
<property name="title">
|
||||
<string>X Axis</string>
|
||||
@@ -214,22 +235,22 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="plot_title_label">
|
||||
<property name="text">
|
||||
<string>Plot Title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="plot_title"/>
|
||||
</item>
|
||||
</layout>
|
||||
<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,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Literal, Optional
|
||||
|
||||
import bec_qthemes
|
||||
@@ -31,6 +32,7 @@ class AxisConfig(BaseModel):
|
||||
y_lim: Optional[tuple] = Field(None, description="The limits of the y-axis.")
|
||||
x_grid: bool = Field(False, description="Show grid on the x-axis.")
|
||||
y_grid: bool = Field(False, description="Show grid on the y-axis.")
|
||||
outer_axes: bool = Field(False, description="Show the outer axes of the plot widget.")
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
@@ -75,6 +77,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"set_outer_axes",
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"remove",
|
||||
@@ -116,7 +119,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
if theme is None:
|
||||
qapp = QApplication.instance()
|
||||
if hasattr(qapp, "theme"):
|
||||
theme = qapp.theme["theme"]
|
||||
theme = qapp.theme.theme
|
||||
else:
|
||||
theme = "dark"
|
||||
self.apply_theme(theme)
|
||||
@@ -336,6 +339,17 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
self.config.axis.x_grid = x
|
||||
self.config.axis.y_grid = y
|
||||
|
||||
def set_outer_axes(self, show: bool = True):
|
||||
"""
|
||||
Set the outer axes of the plot widget.
|
||||
|
||||
Args:
|
||||
show(bool): Show the outer axes.
|
||||
"""
|
||||
self.plot_item.showAxis("top", show)
|
||||
self.plot_item.showAxis("right", show)
|
||||
self.config.axis.outer_axes = show
|
||||
|
||||
def add_legend(self):
|
||||
"""Add legend to the plot"""
|
||||
self.plot_item.addLegend()
|
||||
|
||||
@@ -12,10 +12,12 @@ from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from pyqtgraph.exporters import MatplotlibExporter
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
|
||||
from bec_widgets.utils import Colors, EntryValidator
|
||||
from bec_widgets.utils.colors import get_accent_colors
|
||||
from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
|
||||
BECCurve,
|
||||
@@ -74,6 +76,8 @@ class BECWaveform(BECPlotBase):
|
||||
"remove",
|
||||
"clear_all",
|
||||
"set_legend_label_size",
|
||||
"toggle_roi",
|
||||
"select_roi",
|
||||
]
|
||||
scan_signal_update = pyqtSignal()
|
||||
async_signal_update = pyqtSignal()
|
||||
@@ -81,6 +85,9 @@ class BECWaveform(BECPlotBase):
|
||||
dap_summary_update = pyqtSignal(dict, dict)
|
||||
autorange_signal = pyqtSignal()
|
||||
new_scan = pyqtSignal()
|
||||
roi_changed = pyqtSignal(tuple)
|
||||
roi_active = pyqtSignal(bool)
|
||||
request_dap_refresh = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -100,6 +107,9 @@ class BECWaveform(BECPlotBase):
|
||||
self.old_scan_id = None
|
||||
self.scan_id = None
|
||||
self.scan_item = None
|
||||
self._roi_region = None
|
||||
self.roi_select = None
|
||||
self._accent_colors = get_accent_colors()
|
||||
self._x_axis_mode = {
|
||||
"name": None,
|
||||
"entry": None,
|
||||
@@ -130,6 +140,63 @@ class BECWaveform(BECPlotBase):
|
||||
self.add_legend()
|
||||
self.apply_config(self.config)
|
||||
|
||||
@Slot(bool)
|
||||
def toggle_roi(self, toggled: bool) -> None:
|
||||
"""Toggle the linear region selector on the plot.
|
||||
|
||||
Args:
|
||||
toggled(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
if toggled:
|
||||
return self._hook_roi()
|
||||
return self._unhook_roi()
|
||||
|
||||
@Slot(tuple)
|
||||
def select_roi(self, region: tuple[float, float]):
|
||||
"""Set the fit region of the plot widget. At the moment only a single region is supported.
|
||||
To remove the roi region again, use toggle_roi_region
|
||||
|
||||
Args:
|
||||
region(tuple[float, float]): The fit region.
|
||||
"""
|
||||
if self.roi_region == (None, None):
|
||||
self.toggle_roi(True)
|
||||
try:
|
||||
self.roi_select.linear_region_selector.setRegion(region)
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting region {tuple}; Exception raised: {e}")
|
||||
raise ValueError(f"Error setting region {tuple}; Exception raised: {e}")
|
||||
|
||||
def _hook_roi(self):
|
||||
"""Hook the linear region selector to the plot."""
|
||||
color = self._accent_colors.default
|
||||
color.setAlpha(int(0.2 * 255))
|
||||
hover_color = self._accent_colors.default
|
||||
hover_color.setAlpha(int(0.35 * 255))
|
||||
if self.roi_select is None:
|
||||
self.roi_select = LinearRegionWrapper(
|
||||
self.plot_item, color=color, hover_color=hover_color, parent=self
|
||||
)
|
||||
self.roi_select.add_region_selector()
|
||||
self.roi_select.region_changed.connect(self.roi_changed)
|
||||
self.roi_select.region_changed.connect(self.set_roi_region)
|
||||
self.request_dap_refresh.connect(self.refresh_dap)
|
||||
self._emit_roi_region()
|
||||
self.roi_active.emit(True)
|
||||
|
||||
def _unhook_roi(self):
|
||||
"""Unhook the linear region selector from the plot."""
|
||||
if self.roi_select is not None:
|
||||
self.roi_select.region_changed.disconnect(self.roi_changed)
|
||||
self.roi_select.region_changed.disconnect(self.set_roi_region)
|
||||
self.request_dap_refresh.disconnect(self.refresh_dap)
|
||||
self.roi_active.emit(False)
|
||||
self.roi_region = None
|
||||
self.refresh_dap()
|
||||
self.roi_select.cleanup()
|
||||
self.roi_select.deleteLater()
|
||||
self.roi_select = None
|
||||
|
||||
def apply_config(self, config: dict | SubplotConfig, replot_last_scan: bool = False):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
@@ -171,6 +238,48 @@ class BECWaveform(BECPlotBase):
|
||||
for curve in self.curves:
|
||||
curve.config.parent_id = new_gui_id
|
||||
|
||||
###################################
|
||||
# Fit Range Properties
|
||||
###################################
|
||||
|
||||
@property
|
||||
def roi_region(self) -> tuple[float, float] | None:
|
||||
"""
|
||||
Get the fit region of the plot widget.
|
||||
|
||||
Returns:
|
||||
tuple: The fit region.
|
||||
"""
|
||||
if self._roi_region is not None:
|
||||
return self._roi_region
|
||||
return None, None
|
||||
|
||||
@roi_region.setter
|
||||
def roi_region(self, value: tuple[float, float] | None):
|
||||
"""Set the fit region of the plot widget.
|
||||
|
||||
Args:
|
||||
value(tuple[float, float]|None): The fit region.
|
||||
"""
|
||||
self._roi_region = value
|
||||
if value is not None:
|
||||
self.request_dap_refresh.emit()
|
||||
|
||||
@Slot(tuple)
|
||||
def set_roi_region(self, region: tuple[float, float]):
|
||||
"""
|
||||
Set the fit region of the plot widget.
|
||||
|
||||
Args:
|
||||
region(tuple[float, float]): The fit region.
|
||||
"""
|
||||
self.roi_region = region
|
||||
|
||||
def _emit_roi_region(self):
|
||||
"""Emit the current ROI from selector the plot widget."""
|
||||
if self.roi_select is not None:
|
||||
self.set_roi_region(self.roi_select.linear_region_selector.getRegion())
|
||||
|
||||
###################################
|
||||
# Waveform Properties
|
||||
###################################
|
||||
@@ -1058,13 +1167,14 @@ class BECWaveform(BECPlotBase):
|
||||
y_entry = curve.config.signals.y.entry
|
||||
model_name = curve.config.signals.dap
|
||||
model = getattr(self.dap, model_name)
|
||||
x_min, x_max = self.roi_region
|
||||
|
||||
msg = messages.DAPRequestMessage(
|
||||
dap_cls="LmfitService1D",
|
||||
dap_type="on_demand",
|
||||
config={
|
||||
"args": [self.scan_id, x_name, x_entry, y_name, y_entry],
|
||||
"kwargs": {},
|
||||
"kwargs": {"x_min": x_min, "x_max": x_max},
|
||||
"class_args": model._plugin_info["class_args"],
|
||||
"class_kwargs": model._plugin_info["class_kwargs"],
|
||||
},
|
||||
@@ -1304,7 +1414,8 @@ class BECWaveform(BECPlotBase):
|
||||
self.scan_signal_update.emit()
|
||||
self.async_signal_update.emit()
|
||||
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict | pd.DataFrame:
|
||||
# pylint: ignore: undefined-variable
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict: # | pd.DataFrame:
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
|
||||
@@ -1351,13 +1462,21 @@ class BECWaveform(BECPlotBase):
|
||||
"""
|
||||
MatplotlibExporter(self.plot_item).export()
|
||||
|
||||
def clear_all(self):
|
||||
def clear_source(self, source: Literal["DAP", "async", "scan_segment", "custom"]):
|
||||
"""Clear speicific source from self._curves_data.
|
||||
|
||||
Args:
|
||||
source (Literal["DAP", "async", "scan_segment", "custom"]): Source to be cleared.
|
||||
"""
|
||||
curves_data = self._curves_data
|
||||
sources = list(curves_data.keys())
|
||||
curve_ids_to_remove = list(curves_data[source].keys())
|
||||
for curve_id in curve_ids_to_remove:
|
||||
self.remove_curve(curve_id)
|
||||
|
||||
def clear_all(self):
|
||||
sources = list(self._curves_data.keys())
|
||||
for source in sources:
|
||||
curve_ids_to_remove = list(curves_data[source].keys())
|
||||
for curve_id in curve_ids_to_remove:
|
||||
self.remove_curve(curve_id)
|
||||
self.clear_source(source)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget connection from BECDispatcher."""
|
||||
|
||||
@@ -1,67 +1,285 @@
|
||||
from qtpy.QtCore import Qt, Slot
|
||||
from qtpy.QtGui import QPainter, QPen
|
||||
import numpy as np
|
||||
from qtpy.QtCore import Property, QSize, Qt, Slot
|
||||
from qtpy.QtGui import QBrush, QColor, QPainter, QPainterPath, QPen
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_accent_colors, get_theme_palette
|
||||
|
||||
class PositionIndicator(QWidget):
|
||||
|
||||
class PositionIndicator(BECWidget, QWidget):
|
||||
USER_ACCESS = ["set_value", "set_range", "vertical", "indicator_width", "rounded_corners"]
|
||||
|
||||
ICON_NAME = "horizontal_distribute"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.position = 0.5
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self.position = 50
|
||||
self.min_value = 0
|
||||
self.max_value = 100
|
||||
self.scaling_factor = 0.5
|
||||
self.setMinimumHeight(10)
|
||||
self.is_vertical = False
|
||||
self._current_indicator_position = 0
|
||||
self._draw_position = 0
|
||||
self._rounded_corners = 10
|
||||
self._indicator_width = 2
|
||||
self._indicator_color = get_accent_colors().success
|
||||
self._background_color = get_theme_palette().mid().color()
|
||||
self._use_color_palette = True
|
||||
|
||||
def set_range(self, min_value, max_value):
|
||||
def set_range(self, min_value: float, max_value: float):
|
||||
"""
|
||||
Set the range of the position indicator
|
||||
|
||||
Args:
|
||||
min_value(float): Minimum value of the range
|
||||
max_value(float): Maximum value of the range
|
||||
"""
|
||||
self.minimum = min_value
|
||||
self.maximum = max_value
|
||||
|
||||
@Property(float)
|
||||
def minimum(self):
|
||||
"""
|
||||
Property to get the minimum value of the position indicator
|
||||
"""
|
||||
return self.min_value
|
||||
|
||||
@minimum.setter
|
||||
def minimum(self, min_value: float):
|
||||
"""
|
||||
Setter for the minimum property
|
||||
|
||||
Args:
|
||||
min_value: The minimum value of the position indicator
|
||||
"""
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
self.update()
|
||||
|
||||
@Property(float)
|
||||
def maximum(self):
|
||||
"""
|
||||
Property to get the maximum value of the position indicator
|
||||
"""
|
||||
return self.max_value
|
||||
|
||||
@maximum.setter
|
||||
def maximum(self, max_value: float):
|
||||
"""
|
||||
Setter for the maximum property
|
||||
|
||||
Args:
|
||||
max_value: The maximum value of the position indicator
|
||||
"""
|
||||
self.max_value = max_value
|
||||
self.update()
|
||||
|
||||
@Property(bool)
|
||||
def vertical(self):
|
||||
"""
|
||||
Property to determine the orientation of the position indicator
|
||||
"""
|
||||
return self.is_vertical
|
||||
|
||||
@vertical.setter
|
||||
def vertical(self, is_vertical: bool):
|
||||
"""
|
||||
Setter for the vertical property
|
||||
|
||||
Args:
|
||||
is_vertical: True if the indicator should be vertical, False if horizontal
|
||||
"""
|
||||
|
||||
self.is_vertical = is_vertical
|
||||
self.update()
|
||||
|
||||
@Property(float)
|
||||
def value(self):
|
||||
"""
|
||||
Property to get the current value of the position indicator
|
||||
"""
|
||||
return self.position
|
||||
|
||||
@value.setter
|
||||
def value(self, position: float):
|
||||
"""
|
||||
Setter for the value property
|
||||
|
||||
Args:
|
||||
position: The new position of the indicator
|
||||
"""
|
||||
self.set_value(position)
|
||||
|
||||
@Property(int)
|
||||
def indicator_width(self):
|
||||
"""
|
||||
Property to get the width of the indicator
|
||||
"""
|
||||
return self._indicator_width
|
||||
|
||||
@indicator_width.setter
|
||||
def indicator_width(self, width: int):
|
||||
"""
|
||||
Setter for the indicator width property
|
||||
|
||||
Args:
|
||||
width: The new width of the indicator
|
||||
"""
|
||||
self._indicator_width = width
|
||||
self.update()
|
||||
|
||||
@Property(int)
|
||||
def rounded_corners(self):
|
||||
"""
|
||||
Property to get the rounded corners of the position indicator
|
||||
"""
|
||||
return self._rounded_corners
|
||||
|
||||
@rounded_corners.setter
|
||||
def rounded_corners(self, value: int):
|
||||
"""
|
||||
Setter for the rounded corners property
|
||||
|
||||
Args:
|
||||
value: The new value for the rounded corners
|
||||
"""
|
||||
self._rounded_corners = value
|
||||
self.update()
|
||||
|
||||
@Property(QColor)
|
||||
def indicator_color(self):
|
||||
"""
|
||||
Property to get the color of the indicator
|
||||
"""
|
||||
return self._indicator_color
|
||||
|
||||
@indicator_color.setter
|
||||
def indicator_color(self, color: QColor):
|
||||
"""
|
||||
Setter for the indicator color property
|
||||
|
||||
Args:
|
||||
color: The new color for the indicator
|
||||
"""
|
||||
self._indicator_color = color
|
||||
self.update()
|
||||
|
||||
@Property(QColor)
|
||||
def background_color(self):
|
||||
"""
|
||||
Property to get the background color of the position indicator
|
||||
"""
|
||||
return self._background_color
|
||||
|
||||
@background_color.setter
|
||||
def background_color(self, color: QColor):
|
||||
"""
|
||||
Setter for the background color property
|
||||
|
||||
Args:
|
||||
color: The new background color
|
||||
"""
|
||||
self._background_color = color
|
||||
self.update()
|
||||
|
||||
@Property(bool)
|
||||
def use_color_palette(self):
|
||||
"""
|
||||
Property to determine if the indicator should use the color palette or the custom color.
|
||||
"""
|
||||
return self._use_color_palette
|
||||
|
||||
@use_color_palette.setter
|
||||
def use_color_palette(self, use_palette: bool):
|
||||
"""
|
||||
Setter for the use color palette property
|
||||
|
||||
Args:
|
||||
use_palette: True if the indicator should use the color palette, False if custom color
|
||||
"""
|
||||
self._use_color_palette = use_palette
|
||||
self.update()
|
||||
|
||||
# @Property(float)
|
||||
@Slot(int)
|
||||
@Slot(float)
|
||||
def on_position_update(self, position: float):
|
||||
def set_value(self, position: float):
|
||||
self.position = position
|
||||
self.update()
|
||||
|
||||
def _get_indicator_color(self):
|
||||
if self._use_color_palette:
|
||||
return get_accent_colors().success
|
||||
return self._indicator_color
|
||||
|
||||
def _get_background_brush(self):
|
||||
if self._use_color_palette:
|
||||
return get_theme_palette().mid()
|
||||
return QBrush(self._background_color)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
width = self.width()
|
||||
height = self.height()
|
||||
|
||||
# Draw horizontal line
|
||||
painter.setPen(Qt.black)
|
||||
painter.drawLine(0, height // 2, width, height // 2)
|
||||
# Set up the brush for the background
|
||||
painter.setBrush(self._get_background_brush())
|
||||
|
||||
# Draw shorter vertical line at the current position
|
||||
x_pos = int(self.position * width)
|
||||
painter.setPen(QPen(Qt.red, 2))
|
||||
short_line_height = int(height * self.scaling_factor)
|
||||
painter.drawLine(
|
||||
x_pos,
|
||||
(height // 2) - (short_line_height // 2),
|
||||
x_pos,
|
||||
(height // 2) + (short_line_height // 2),
|
||||
# Create a QPainterPath with a rounded rectangle for clipping
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(0, 0, width, height, self._rounded_corners, self._rounded_corners)
|
||||
|
||||
# Set clipping to the rounded rectangle
|
||||
painter.setClipPath(path)
|
||||
|
||||
# Draw the rounded rectangle background first
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawRoundedRect(0, 0, width, height, self._rounded_corners, self._rounded_corners)
|
||||
|
||||
# get the position scaled to the defined min and max values
|
||||
self._current_indicator_position = position = np.interp(
|
||||
self.position, [self.min_value, self.max_value], [0, 100]
|
||||
)
|
||||
|
||||
# Draw thicker vertical lines at the ends
|
||||
end_line_pen = QPen(Qt.blue, 5)
|
||||
painter.setPen(end_line_pen)
|
||||
painter.drawLine(0, 0, 0, height)
|
||||
painter.drawLine(width - 1, 0, width - 1, height)
|
||||
if self.is_vertical:
|
||||
# If vertical, rotate the coordinate system by -90 degrees
|
||||
painter.translate(width // 2, height // 2) # Move origin to center
|
||||
painter.rotate(-90) # Rotate by -90 degrees for vertical drawing
|
||||
painter.translate(-height // 2, -width // 2) # Restore the origin for drawing
|
||||
|
||||
# Switch width and height for the vertical orientation
|
||||
width, height = height, width
|
||||
|
||||
# Draw the moving vertical indicator, respecting the clip path
|
||||
self._draw_position = x_pos = round(
|
||||
position * width / 100
|
||||
) # Position for the vertical line
|
||||
|
||||
indicator_pen = QPen(self._get_indicator_color(), self._indicator_width)
|
||||
painter.setPen(indicator_pen)
|
||||
painter.drawLine(x_pos, 0, x_pos, height)
|
||||
|
||||
painter.end()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
# Set the smallest possible size
|
||||
return QSize(10, 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
from bec_qthemes import setup_theme
|
||||
from qtpy.QtWidgets import QApplication, QSlider, QVBoxLayout
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
setup_theme("dark")
|
||||
# Create position indicator and slider
|
||||
position_indicator = PositionIndicator()
|
||||
# position_indicator.set_range(0, 1)
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.valueChanged.connect(lambda value: position_indicator.on_position_update(value / 100))
|
||||
|
||||
slider.valueChanged.connect(lambda value: position_indicator.set_value(value))
|
||||
position_indicator.is_vertical = False
|
||||
# position_indicator.set_value(100)
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(position_indicator)
|
||||
layout.addWidget(slider)
|
||||
|
||||
@@ -16,7 +16,7 @@ from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout, QW
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.utils.colors import get_accent_colors, set_theme
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -73,6 +73,10 @@ class PositionerBox(BECWidget, QWidget):
|
||||
|
||||
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)
|
||||
@@ -249,7 +253,7 @@ class PositionerBox(BECWidget, QWidget):
|
||||
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.on_position_update(pos)
|
||||
self.ui.position_indicator.set_value(pos)
|
||||
|
||||
def update_limits(self, limits: tuple):
|
||||
"""Update limits
|
||||
|
||||
@@ -170,7 +170,20 @@
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PositionIndicator" name="position_indicator"/>
|
||||
<widget class="PositionIndicator" name="position_indicator">
|
||||
<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>
|
||||
<item>
|
||||
<widget class="QLabel" name="readback">
|
||||
|
||||
@@ -24,11 +24,9 @@ class PositionerControlLine(PositionerBox):
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
import qdarktheme
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
qdarktheme.setup_theme("dark")
|
||||
widget = PositionerControlLine(device="samy")
|
||||
|
||||
widget.show()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>785</width>
|
||||
<width>612</width>
|
||||
<height>91</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -59,16 +59,6 @@
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="PositionIndicator" name="position_indicator">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="SpinnerWidget" name="spinner_widget">
|
||||
<property name="minimumSize">
|
||||
@@ -139,14 +129,14 @@
|
||||
<widget class="QToolButton" name="tweak_left">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -154,8 +144,8 @@
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
@@ -170,14 +160,14 @@
|
||||
<widget class="QToolButton" name="tweak_right">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
@@ -185,8 +175,8 @@
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>30</width>
|
||||
<height>30</height>
|
||||
<width>25</width>
|
||||
<height>25</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
@@ -194,6 +184,34 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PositionIndicator" name="position_indicator">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>15</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>15</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</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="rounded_corners" stdset="0">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -1,39 +1,47 @@
|
||||
import re
|
||||
"""Module for a text box widget that displays text in plain and HTML format and adheres to the BECWidget interface & style."""
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from qtpy.QtWidgets import QTextEdit
|
||||
import re
|
||||
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.utils.bec_connector import ConnectionConfig
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import Colors
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
DEFAULT_TEXT = "<h1>Welcome to the BEC Widget TextBox</h1><p>A widget that allows user to display text in plain and HTML format.</p><p>This is an example of displaying HTML text.</p>"
|
||||
|
||||
|
||||
class TextBoxConfig(ConnectionConfig):
|
||||
"""Configuration for the TextBox widget.
|
||||
|
||||
theme: str = Field("dark", description="The theme of the figure widget.")
|
||||
font_color: str = Field("#FFF", description="The font color of the text")
|
||||
background_color: str = Field("#000", description="The background color of the widget.")
|
||||
font_size: int = Field(16, description="The font size of the text in the widget.")
|
||||
text: str = Field("", description="The text to display in the widget.")
|
||||
Args:
|
||||
text (str, optional): The text to display in the widget. Defaults to None.
|
||||
is_html (bool, optional): Whether the text is in HTML format or not. Defaults to False.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@field_validator("theme")
|
||||
def validate_theme(cls, v):
|
||||
"""Validate the theme of the figure widget."""
|
||||
if v not in ["dark", "light"]:
|
||||
raise ValueError("Theme must be either 'dark' or 'light'")
|
||||
return v
|
||||
|
||||
_validate_font_color = field_validator("font_color")(Colors.validate_color)
|
||||
_validate_background_color = field_validator("background_color")(Colors.validate_color)
|
||||
text: str | None = Field(None, description="The text to display in the widget.")
|
||||
is_html: bool = Field(False, description="Whether the text is in HTML format or not.")
|
||||
|
||||
|
||||
class TextBox(BECWidget, QTextEdit):
|
||||
class TextBox(BECWidget, QWidget):
|
||||
"""A widget that displays text in plain and HTML format
|
||||
|
||||
USER_ACCESS = ["set_color", "set_text", "set_font_size"]
|
||||
Args:
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
client ([type], optional): The client to use. Defaults to None.
|
||||
config ([type], optional): The config to use. Defaults to None.
|
||||
gui_id ([type], optional): The gui_id to use. Defaults to None.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["set_plain_text", "set_html_text"]
|
||||
ICON_NAME = "chat"
|
||||
|
||||
def __init__(self, parent=None, text: str = "", client=None, config=None, gui_id=None):
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||
if config is None:
|
||||
config = TextBoxConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
@@ -41,80 +49,79 @@ class TextBox(BECWidget, QTextEdit):
|
||||
config = TextBoxConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QTextEdit.__init__(self, parent=parent)
|
||||
|
||||
QWidget.__init__(self, parent)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.text_box_text_edit = QTextEdit(parent=self)
|
||||
self.layout.addWidget(self.text_box_text_edit)
|
||||
self.setLayout(self.layout)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.config = config
|
||||
self.setReadOnly(True)
|
||||
self.setGeometry(self.rect())
|
||||
self.set_color(self.config.background_color, self.config.font_color)
|
||||
if not text:
|
||||
text = "<h1>Welcome to the BEC Widget TextBox</h1><p>A widget that allows user to display text in plain and HTML format.</p><p>This is an example of displaying HTML text.</p>"
|
||||
self.set_text(text)
|
||||
|
||||
def change_theme(self) -> None:
|
||||
"""
|
||||
Change the theme of the figure widget.
|
||||
"""
|
||||
if self.config.theme == "dark":
|
||||
theme = "light"
|
||||
font_color = "#000"
|
||||
background_color = "#FFF"
|
||||
self.text_box_text_edit.setReadOnly(True)
|
||||
if self.config.text is not None:
|
||||
if self.config.is_html:
|
||||
self.set_html_text(self.config.text)
|
||||
else:
|
||||
self.set_plain_text(self.config.text)
|
||||
else:
|
||||
theme = "dark"
|
||||
font_color = "#FFF"
|
||||
background_color = "#000"
|
||||
self.config.theme = theme
|
||||
self.set_color(background_color, font_color)
|
||||
self.set_html_text(DEFAULT_TEXT)
|
||||
|
||||
def set_color(self, background_color: str, font_color: str) -> None:
|
||||
"""Set the background color of the widget.
|
||||
@Slot(str)
|
||||
def set_plain_text(self, text: str) -> None:
|
||||
"""Set the plain text of the widget.
|
||||
|
||||
Args:
|
||||
background_color (str): The color to set the background in HEX.
|
||||
font_color (str): The color to set the font in HEX.
|
||||
|
||||
text (str): The text to set.
|
||||
"""
|
||||
self.config.background_color = background_color
|
||||
self.config.font_color = font_color
|
||||
self._update_stylesheet()
|
||||
self.text_box_text_edit.setPlainText(text)
|
||||
self.config.text = text
|
||||
self.config.is_html = False
|
||||
|
||||
def set_font_size(self, size: int) -> None:
|
||||
"""Set the font size of the text in the widget.
|
||||
@Slot(str)
|
||||
def set_html_text(self, text: str) -> None:
|
||||
"""Set the HTML text of the widget.
|
||||
|
||||
Args:
|
||||
size (int): The font size to set.
|
||||
text (str): The text to set.
|
||||
"""
|
||||
self.config.font_size = size
|
||||
self._update_stylesheet()
|
||||
self.text_box_text_edit.setHtml(text)
|
||||
self.config.text = text
|
||||
self.config.is_html = True
|
||||
|
||||
def _update_stylesheet(self):
|
||||
"""Update the stylesheet of the widget."""
|
||||
self.setStyleSheet(
|
||||
f"background-color: {self.config.background_color}; color: {self.config.font_color}; font-size: {self.config.font_size}px"
|
||||
)
|
||||
@Property(str)
|
||||
def plain_text(self) -> str:
|
||||
"""Get the text of the widget.
|
||||
|
||||
def set_text(self, text: str) -> None:
|
||||
Returns:
|
||||
str: The text of the widget.
|
||||
"""
|
||||
return self.text_box_text_edit.toPlainText()
|
||||
|
||||
@plain_text.setter
|
||||
def plain_text(self, text: str) -> None:
|
||||
"""Set the text of the widget.
|
||||
|
||||
Args:
|
||||
text (str): The text to set.
|
||||
"""
|
||||
if self.is_html(text):
|
||||
self.setHtml(text)
|
||||
else:
|
||||
self.setPlainText(text)
|
||||
self.config.text = text
|
||||
self.set_plain_text(text)
|
||||
|
||||
def is_html(self, text: str) -> bool:
|
||||
"""Check if the text contains HTML tags.
|
||||
|
||||
Args:
|
||||
text (str): The text to check.
|
||||
@Property(str)
|
||||
def html_text(self) -> str:
|
||||
"""Get the HTML text of the widget.
|
||||
|
||||
Returns:
|
||||
bool: True if the text contains HTML tags, False otherwise.
|
||||
str: The HTML text of the widget.
|
||||
"""
|
||||
return bool(re.search(r"<[a-zA-Z/][^>]*>", text))
|
||||
return self.text_box_text_edit.toHtml()
|
||||
|
||||
@html_text.setter
|
||||
def html_text(self, text: str) -> None:
|
||||
"""Set the HTML text of the widget.
|
||||
|
||||
Args:
|
||||
text (str): The HTML text to set.
|
||||
"""
|
||||
self.set_html_text(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -123,7 +130,6 @@ if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
widget = TextBox()
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -13,6 +13,7 @@ from bec_widgets.qt_utils.error_popups import WarningPopupUtility
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingWidget
|
||||
from bec_widgets.utils import Colors, UILoader
|
||||
from bec_widgets.widgets.color_button.color_button import ColorButton
|
||||
from bec_widgets.widgets.dap_combo_box.dap_combo_box import DapComboBox
|
||||
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
@@ -241,8 +242,9 @@ class DialogRow(QObject):
|
||||
self.device_line_edit = DeviceLineEdit()
|
||||
self.entry_line_edit = QLineEdit()
|
||||
|
||||
self.dap_combo = QComboBox()
|
||||
self.populate_dap_combobox()
|
||||
self.dap_combo = DapComboBox()
|
||||
self.dap_combo.populate_fit_model_combobox()
|
||||
self.dap_combo.select_fit_model("GaussianModel")
|
||||
|
||||
# Styling
|
||||
self.color_button = ColorButton()
|
||||
@@ -261,16 +263,6 @@ class DialogRow(QObject):
|
||||
lambda: self.remove_row()
|
||||
) # From some reason do not work without lambda
|
||||
|
||||
def populate_dap_combobox(self):
|
||||
available_models = [
|
||||
attr
|
||||
for attr in dir(self.client.dap)
|
||||
if not attr.startswith("__")
|
||||
and not callable(getattr(self.client.dap, attr))
|
||||
and not attr.startswith("_")
|
||||
]
|
||||
self.dap_combo.addItems(available_models)
|
||||
|
||||
def add_scan_row(self):
|
||||
if self.config is not None:
|
||||
self.device_line_edit.setText(self.config.signals.y.name)
|
||||
@@ -298,7 +290,7 @@ class DialogRow(QObject):
|
||||
if self.config is not None:
|
||||
self.device_line_edit.setText(self.config.signals.y.name)
|
||||
self.entry_line_edit.setText(self.config.signals.y.entry)
|
||||
self.dap_combo.setCurrentText(self.config.signals.dap)
|
||||
self.dap_combo.fit_model_combobox.setCurrentText(self.config.signals.dap)
|
||||
self.color_button.set_color(self.config.color)
|
||||
self.style_combo.setCurrentText(self.config.pen_style)
|
||||
self.width.setValue(self.config.pen_width)
|
||||
@@ -312,7 +304,7 @@ class DialogRow(QObject):
|
||||
|
||||
self.table_widget.setCellWidget(self.row, 0, self.device_line_edit)
|
||||
self.table_widget.setCellWidget(self.row, 1, self.entry_line_edit)
|
||||
self.table_widget.setCellWidget(self.row, 2, self.dap_combo)
|
||||
self.table_widget.setCellWidget(self.row, 2, self.dap_combo.fit_model_combobox)
|
||||
self.table_widget.setCellWidget(self.row, 3, self.color_button)
|
||||
self.table_widget.setCellWidget(self.row, 4, self.style_combo)
|
||||
self.table_widget.setCellWidget(self.row, 5, self.width)
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Literal
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||
@@ -55,6 +55,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"lock_aspect_ratio",
|
||||
"export",
|
||||
"export_to_matplotlib",
|
||||
"toggle_roi",
|
||||
"select_roi",
|
||||
]
|
||||
scan_signal_update = Signal()
|
||||
async_signal_update = Signal()
|
||||
@@ -70,6 +72,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
crosshair_coordinates_changed_string = Signal(str)
|
||||
crosshair_coordinates_clicked = Signal(tuple)
|
||||
crosshair_coordinates_clicked_string = Signal(str)
|
||||
roi_changed = Signal(tuple)
|
||||
roi_active = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -120,6 +124,11 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"crosshair": MaterialIconAction(
|
||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
||||
),
|
||||
"roi_select": MaterialIconAction(
|
||||
icon_name="align_justify_space_between",
|
||||
tooltip="Add ROI region for DAP",
|
||||
checkable=True,
|
||||
),
|
||||
},
|
||||
target_widget=self,
|
||||
)
|
||||
@@ -133,6 +142,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.waveform.apply_config(config)
|
||||
|
||||
self.config = config
|
||||
self._clear_curves_on_plot_update = False
|
||||
|
||||
self.hook_waveform_signals()
|
||||
self._hook_actions()
|
||||
@@ -160,6 +170,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.waveform.crosshair_position_clicked.connect(
|
||||
self._emit_crosshair_position_clicked_string
|
||||
)
|
||||
self.waveform.roi_changed.connect(self.roi_changed)
|
||||
self.waveform.roi_active.connect(self.roi_active)
|
||||
|
||||
def _hook_actions(self):
|
||||
self.toolbar.widgets["save"].action.triggered.connect(self.export)
|
||||
@@ -173,6 +185,7 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
self.toolbar.widgets["fit_params"].action.triggered.connect(self.show_fit_summary_dialog)
|
||||
self.toolbar.widgets["axis_settings"].action.triggered.connect(self.show_axis_settings)
|
||||
self.toolbar.widgets["crosshair"].action.triggered.connect(self.waveform.toggle_crosshair)
|
||||
self.toolbar.widgets["roi_select"].action.toggled.connect(self.waveform.toggle_roi)
|
||||
# self.toolbar.widgets["import"].action.triggered.connect(
|
||||
# lambda: self.load_config(path=None, gui=True)
|
||||
# )
|
||||
@@ -180,6 +193,29 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
# lambda: self.save_config(path=None, gui=True)
|
||||
# )
|
||||
|
||||
@Slot(bool)
|
||||
def toogle_roi_select(self, checked: bool):
|
||||
"""Toggle the linear region selector.
|
||||
|
||||
Args:
|
||||
checked(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
self.toolbar.widgets["roi_select"].action.setChecked(checked)
|
||||
|
||||
@Property(bool)
|
||||
def clear_curves_on_plot_update(self) -> bool:
|
||||
"""If True, clear curves on plot update."""
|
||||
return self._clear_curves_on_plot_update
|
||||
|
||||
@clear_curves_on_plot_update.setter
|
||||
def clear_curves_on_plot_update(self, value: bool):
|
||||
"""Set the clear curves on plot update property.
|
||||
|
||||
Args:
|
||||
value(bool): If True, clear curves on plot update.
|
||||
"""
|
||||
self._clear_curves_on_plot_update = value
|
||||
|
||||
@SafeSlot(tuple)
|
||||
def _emit_crosshair_coordinates_changed_string(self, coordinates):
|
||||
self.crosshair_coordinates_changed_string.emit(str(coordinates))
|
||||
@@ -260,7 +296,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_colormap(colormap)
|
||||
|
||||
@SafeSlot(str, popup_error=True)
|
||||
@Slot(str, str) # Slot for x_name, x_entry
|
||||
@SafeSlot(str, popup_error=True) # Slot for x_name and
|
||||
def set_x(self, x_name: str, x_entry: str | None = None):
|
||||
"""
|
||||
Change the x axis of the plot widget.
|
||||
@@ -275,7 +312,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_x(x_name, x_entry)
|
||||
|
||||
@SafeSlot(str, popup_error=True)
|
||||
@Slot(str) # Slot for y_name
|
||||
@SafeSlot(popup_error=True)
|
||||
def plot(
|
||||
self,
|
||||
arg1: list | np.ndarray | str | None = None,
|
||||
@@ -315,7 +353,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
# self._check_if_scans_have_same_x(enabled=True, x_name_to_check=x_name)
|
||||
if self.clear_curves_on_plot_update is True:
|
||||
self.waveform.clear_source(source="scan_segment")
|
||||
return self.waveform.plot(
|
||||
arg1=arg1,
|
||||
x=x,
|
||||
@@ -334,6 +373,9 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@Slot(
|
||||
str, str, str, str, str, str, bool
|
||||
) # Slot for x_name, y_name, x_entry, y_entry, color, validate_bec
|
||||
@SafeSlot(str, str, str, popup_error=True)
|
||||
def add_dap(
|
||||
self,
|
||||
@@ -362,6 +404,8 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
if self.clear_curves_on_plot_update is True:
|
||||
self.waveform.clear_source(source="DAP")
|
||||
return self.waveform.add_dap(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
@@ -543,6 +587,23 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_auto_range(enabled, axis)
|
||||
|
||||
def toggle_roi(self, checked: bool):
|
||||
"""Toggle the linear region selector.
|
||||
|
||||
Args:
|
||||
checked(bool): If True, enable the linear region selector.
|
||||
"""
|
||||
self.waveform.toggle_roi(checked)
|
||||
|
||||
def select_roi(self, region: tuple):
|
||||
"""
|
||||
Set the region of interest of the plot widget.
|
||||
|
||||
Args:
|
||||
region(tuple): Region of interest.
|
||||
"""
|
||||
self.waveform.select_roi(region)
|
||||
|
||||
@SafeSlot()
|
||||
def _auto_range_from_toolbar(self):
|
||||
"""
|
||||
@@ -560,6 +621,15 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
"""
|
||||
self.waveform.set_grid(x_grid, y_grid)
|
||||
|
||||
def set_outer_axes(self, show: bool):
|
||||
"""
|
||||
Set the outer axes visibility of the plot widget.
|
||||
|
||||
Args:
|
||||
show(bool): Visibility of the outer axes.
|
||||
"""
|
||||
self.waveform.set_outer_axes(show)
|
||||
|
||||
def lock_aspect_ratio(self, lock: bool):
|
||||
"""
|
||||
Lock the aspect ratio of the plot widget.
|
||||
@@ -633,11 +703,12 @@ class BECWaveformWidget(BECWidget, QWidget):
|
||||
def main(): # pragma: no cover
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("auto")
|
||||
widget = BECWaveformWidget()
|
||||
widget.plot(x_name="samx", y_name="bpm4i")
|
||||
widget.plot(y_name="bpm3i")
|
||||
widget.plot(y_name="bpm4a")
|
||||
widget.plot(y_name="bpm5i")
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -0,0 +1,39 @@
|
||||
(user.widgets.bec_progressbar)=
|
||||
# BEC Progressbar
|
||||
|
||||
```{tab} Overview
|
||||
|
||||
The BEC Progressbar widget is a general purpose progress bar that follows the BEC theme and style. It can be embedded in any application to display the progress of a task or operation.
|
||||
|
||||
## Key Features:
|
||||
- **Modern Design**: The BEC Progressbar widget is designed with a modern and sleek appearance, following the BEC theme.
|
||||
- **Customizable**: Users can customize the appearance and behavior of the progress bar to suit their application.
|
||||
- **Responsive**: The progress bar updates in real-time to reflect the progress of the task or operation.
|
||||
|
||||
## Screenshot
|
||||

|
||||

|
||||
|
||||
```
|
||||
|
||||
````{tab} Examples
|
||||
|
||||
The `BECProgressBar` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `BECProgressBar` widget.
|
||||
|
||||
## Example 1 - Adding BEC Status Box to BECDockArea
|
||||
|
||||
In this example, we demonstrate how to add a `BECProgressBar` widget to a `BECDockArea`, allowing users to manually set and update the progress states.
|
||||
|
||||
```python
|
||||
# Add a new dock with a BECStatusBox widget
|
||||
pb = gui.add_dock().add_widget("BECProgressBar")
|
||||
pb.set_value(50)
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.BECProgressbar.rst
|
||||
```
|
||||
````
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -15,7 +15,7 @@ The [`BEC Status Box`](/api_reference/_autosummary/bec_widgets.cli.client.BECSta
|
||||
|
||||
````{tab} Examples
|
||||
|
||||
The `BECStatusBox` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `QtDesigner`. Below are examples demonstrating how to create and use the `BECStatusBox` widget.
|
||||
The `BECStatusBox` widget can be integrated within a [`BECDockArea`](user.widgets.bec_dock_area) or used as an individual component in your application through `BECDesigner`. Below are examples demonstrating how to create and use the `BECStatusBox` widget.
|
||||
|
||||
## Example 1 - Adding BEC Status Box to BECDockArea
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ waveform.dap_summary_update.connect(lmfit_dialog.update_summary_tree)
|
||||
````
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.LMFitDialog.rst
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.rst
|
||||
```
|
||||
````
|
||||
|
||||
|
||||
@@ -10,8 +10,28 @@ The [`PositionIndicator`](/api_reference/_autosummary/bec_widgets.cli.client.Pos
|
||||
- **Position Visualization**: Displays the current position of a motor on a linear scale, showing its location relative to the defined limits.
|
||||
- **Customizable Range**: The widget allows you to set the minimum and maximum range, adapting to different motor configurations.
|
||||
- **Real-Time Updates**: Responds to real-time updates, allowing the position indicator to move dynamically as the motor's position changes.
|
||||
- **Compact Design**: The widget is designed to be compact and visually appealing, making it suitable for various GUI applications.
|
||||
- **Customizable Appearance**: The appearance of the position indicator can be customized to match the overall design of your application, including colors, orientation, and size.
|
||||
- **QtDesigner Integration**: Can be added directly in code or through `QtDesigner`, making it adaptable to various use cases.
|
||||
|
||||
|
||||
## BEC Designer Customization
|
||||
Within the BECDesigner's [property editor](https://doc.qt.io/qt-6/designer-widget-mode.html#the-property-editor/), the `PositionIndicator` widget can be customized to suit your application's requirements. The widget provides the following customization options:
|
||||
- **minimum**: The minimum value of the position indicator.
|
||||
- **maximum**: The maximum value of the position indicator.
|
||||
- **value**: The current value of the position indicator.
|
||||
- **vertical**: A boolean value indicating whether the position indicator is oriented vertically or horizontally.
|
||||
- **indicator_width**: The width of the position indicator.
|
||||
- **rounded_corners**: The radius of the rounded corners of the position indicator.
|
||||
- **indicator_color**: The color of the position indicator.
|
||||
- **background_color**: The color of the background of the position indicator.
|
||||
- **use_color_palette**: A boolean value indicating whether to use the color palette for the position indicator or the custom colors.
|
||||
|
||||
**BEC Designer properties:**
|
||||
```{figure} ./position_indicator_designer_props.png
|
||||
```
|
||||
|
||||
|
||||
````
|
||||
|
||||
````{tab} Examples
|
||||
@@ -33,7 +53,7 @@ position_indicator = PositionIndicator()
|
||||
|
||||
# Create a slider to simulate position changes
|
||||
slider = QSlider(Qt.Horizontal)
|
||||
slider.valueChanged.connect(lambda value: position_indicator.on_position_update(value / 100))
|
||||
slider.valueChanged.connect(lambda value: position_indicator.set_value(value))
|
||||
|
||||
# Create a layout and add the widgets
|
||||
layout = QVBoxLayout()
|
||||
@@ -63,7 +83,13 @@ The `PositionIndicator` can be added to your GUI layout using `QtDesigner`. Once
|
||||
|
||||
```python
|
||||
# Example: Updating the position in a QtDesigner-based application
|
||||
self.position_indicator.on_position_update(new_position_value)
|
||||
self.position_indicator.set_value(new_position_value)
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
```{eval-rst}
|
||||
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.PositionIndicator.rst
|
||||
```
|
||||
````
|
||||
|
After Width: | Height: | Size: 26 KiB |
@@ -30,6 +30,7 @@ Within the BECDesigner's [property editor](https://doc.qt.io/qt-6/designer-widge
|
||||
- **Hide Scan Control**: Allows you to hide the scan control buttons from the widget interface. This is useful when you want to place the control buttons in a different location.
|
||||
- **Hide Scan Selection**: Allows you to hide the scan selection combobox from the widget interface. This is useful when you want to restrict the user to a specific scan type or implement a custom scan selection mechanism.
|
||||
- **Hide Scan Remember Toggle**: Allows you to hide the toggle button that reloads scan parameters from the last executed scan. This is useful if you want to disable or restrict this functionality in specific scenarios.
|
||||
- **Hide Bundle Buttons**: Allows you to hide the buttons that add or remove argument bundles from the widget interface. This is useful when you want to restrict the user from adding additional motor bundles to the scan by accident.
|
||||
|
||||
**BEC Designer properties:**
|
||||
```{figure} ./hide_scan_control.png
|
||||
|
||||
@@ -8,8 +8,11 @@ The [`Text Box Widget`](/api_reference/_autosummary/bec_widgets.cli.client.TextB
|
||||
|
||||
## Key Features:
|
||||
- **Text Display**: Display either plain text or HTML content, with automatic detection of the format.
|
||||
- **Customizable Appearance**: Set the background and font colors to match the design of your application.
|
||||
- **Font Size Adjustment**: Customize the font size of the displayed text for better readability.
|
||||
- **Automatic styling**: The widget automatically adheres to BEC's style guides. No need to worry about background colors, font sizes, or other appearance settings.
|
||||
|
||||
## BEC Designer Properties
|
||||
```{figure} ../../assets/widget_screenshots/text_box_properties.png
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
@@ -38,24 +41,6 @@ The `TextBox` widget can automatically detect and render HTML content. This exam
|
||||
text_box.set_text("<h1>Welcome to BEC Widgets</h1><p>This is an example of displaying <strong>HTML</strong> text.</p>")
|
||||
```
|
||||
|
||||
## Example 3 - Customizing Appearance
|
||||
|
||||
The `TextBox` widget allows you to customize the background and font colors to fit your application's design. Below is an example of how to set these properties.
|
||||
|
||||
```python
|
||||
# Set the background color to white and the font color to black
|
||||
text_box.set_color(background_color="#FFF", font_color="#000")
|
||||
```
|
||||
|
||||
## Example 4 - Adjusting Font Size
|
||||
|
||||
To improve readability or fit more text within the widget, you can adjust the font size.
|
||||
|
||||
```python
|
||||
# Set the font size to 14 pixels
|
||||
text_box.set_font_size(14)
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
|
||||
@@ -75,7 +75,7 @@ dev.bpm3i.sim.select_sim_model("StepModel")
|
||||
```
|
||||
## Example 4 - Adding Data Processing Pipeline Curve with LMFit Models
|
||||
|
||||
In addition to the scan curve, you can also add a second curve that fits the signal using a specified model from [LMFit](https://lmfit.github.io/lmfit-py/builtin_models.html). The following code snippet demonstrates how to create a 1D waveform curve with an attached DAP process, or how to add a DAP process to an existing curve using the BEC CLI. Please note that for this example, both devices were set as Gaussian signals.
|
||||
In addition to the scan curve, you can also add a second curve that fits the signal using a specified model from [LMFit](https://lmfit.github.io/lmfit-py/builtin_models.html). The following code snippet demonstrates how to create a 1D waveform curve with an attached DAP process, or how to add a DAP process to an existing curve using the BEC CLI. Please note that for this example, both devices were set as Gaussian signals. You can also add a region of interest (roi) to the plot which will respected by all running DAP processes.
|
||||
|
||||
```python
|
||||
# Add a new dock, a new BECFigure, and a BECWaveForm to the dock with a GaussianModel DAP
|
||||
@@ -87,6 +87,13 @@ plt.plot(x_name='samx', y_name='bpm3a')
|
||||
# Add DAP to the second curve
|
||||
plt.add_dap(x_name='samx', y_name='bpm3a', dap="GaussianModel")
|
||||
|
||||
# Add roi to the plot, this limits the DAP fit to the selected region x_min=-1, x_max=1
|
||||
# The fit will automatically update
|
||||
plt.select_roi(region=(-1, 1))
|
||||
|
||||
# To remove the DAP from the curve, you can use the toggle button in the toolbar or the following command
|
||||
plt.toggle_roi(False)
|
||||
|
||||
```
|
||||
|
||||
To get the parameters of the fit, you need to retrieve the curve objects and call the `dap_params` property.
|
||||
|
||||
@@ -191,6 +191,14 @@ Angular like toggle switch.
|
||||
Display spinner widget for loading or device movement.
|
||||
```
|
||||
|
||||
```{grid-item-card} BEC Progressbar
|
||||
:link: user.widgets.bec_progressbar
|
||||
:link-type: ref
|
||||
:img-top: /assets/widget_screenshots/bec_progressbar.png
|
||||
|
||||
Modern progress bar for BEC.
|
||||
```
|
||||
|
||||
```{grid-item-card} Position Indicator
|
||||
:link: user.widgets.position_indicator
|
||||
:link-type: ref
|
||||
@@ -239,6 +247,7 @@ text_box/text_box.md
|
||||
website/website.md
|
||||
toggle/toggle.md
|
||||
spinner/spinner.md
|
||||
bec_progressbar/bec_progressbar.md
|
||||
device_input/device_input.md
|
||||
position_indicator/position_indicator.md
|
||||
lmfit_dialog/lmfit_dialog.md
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "0.105.0"
|
||||
version = "0.112.1"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
|
||||
@@ -183,11 +183,20 @@ def test_dap_rpc(rpc_server_figure, bec_client_lib, qtbot):
|
||||
def wait_for_fit():
|
||||
dap_curve = plt.get_curve("bpm4i-bpm4i-GaussianModel")
|
||||
fit_params = dap_curve.dap_params
|
||||
if fit_params is None:
|
||||
return False
|
||||
print(fit_params)
|
||||
return np.isclose(fit_params["center"], 5, atol=0.5)
|
||||
|
||||
qtbot.waitUntil(wait_for_fit, timeout=10000)
|
||||
|
||||
# Repeat fit after adding a region of interest
|
||||
plt.select_roi(region=(3, 7))
|
||||
res = scans.line_scan(dev.samx, 0, 8, steps=50, relative=False)
|
||||
res.wait()
|
||||
|
||||
qtbot.waitUntil(wait_for_fit, timeout=10000)
|
||||
|
||||
|
||||
def test_removing_subplots(rpc_server_figure, bec_client_lib):
|
||||
fig = BECFigure(rpc_server_figure)
|
||||
|
||||
@@ -177,6 +177,7 @@ def test_motor_map_init_from_config(qtbot, mocked_client):
|
||||
"y_lim": None,
|
||||
"x_grid": True,
|
||||
"y_grid": True,
|
||||
"outer_axes": False,
|
||||
},
|
||||
"signals": {
|
||||
"source": "device_readback",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.bec_progressbar.bec_progressbar import BECProgressBar
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def progressbar(qtbot):
|
||||
widget = BECProgressBar()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_progressbar(progressbar):
|
||||
progressbar.update()
|
||||
|
||||
|
||||
def test_progressbar_set_value(qtbot, progressbar):
|
||||
progressbar.set_minimum(0)
|
||||
progressbar.set_maximum(100)
|
||||
progressbar.set_value(50)
|
||||
progressbar.paintEvent(None)
|
||||
|
||||
qtbot.waitUntil(
|
||||
lambda: np.isclose(
|
||||
progressbar._value, progressbar._user_value * progressbar._oversampling_factor
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_progressbar_label(progressbar):
|
||||
progressbar.label_template = "Test: $value"
|
||||
progressbar.set_value(50)
|
||||
assert progressbar.center_label.text() == "Test: 50"
|
||||
@@ -65,6 +65,8 @@ def test_client_generator_with_black_formatting():
|
||||
'''\
|
||||
# This file was automatically generated by generate_cli.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
from bec_widgets.qt_utils.palette_viewer import PaletteViewer
|
||||
from bec_widgets.widgets.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def palette_viewer(qtbot):
|
||||
widget = PaletteViewer()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_palette_viewer_renders_palette_and_accents(qtbot, palette_viewer):
|
||||
assert palette_viewer.frame_layout.count() == 28
|
||||
palette_viewer.clear_palette()
|
||||
assert palette_viewer.frame_layout.count() == 0
|
||||
palette_viewer.update_palette()
|
||||
assert palette_viewer.frame_layout.count() == 28
|
||||
|
||||
|
||||
def test_palette_viewer_updates_on_theme_change(qtbot, palette_viewer):
|
||||
light_window_text_color = palette_viewer.frame_layout.itemAt(1).itemAt(0).widget().text()
|
||||
assert "(windowText)" in light_window_text_color
|
||||
light_hex_color = light_window_text_color.split(" ")[0]
|
||||
|
||||
button = palette_viewer.findChild(DarkModeButton)
|
||||
qtbot.mouseClick(button.mode_button, Qt.MouseButton.LeftButton)
|
||||
qtbot.wait(100)
|
||||
|
||||
dark_window_text_color = palette_viewer.frame_layout.itemAt(1).itemAt(0).widget().text()
|
||||
assert "(windowText)" in dark_window_text_color
|
||||
dark_hex_color = dark_window_text_color.split(" ")[0]
|
||||
|
||||
assert light_hex_color != dark_hex_color
|
||||
@@ -0,0 +1,42 @@
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.position_indicator.position_indicator import PositionIndicator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def position_indicator(qtbot):
|
||||
"""Fixture for PositionIndicator widget"""
|
||||
pi = PositionIndicator()
|
||||
qtbot.addWidget(pi)
|
||||
qtbot.waitExposed(pi)
|
||||
return pi
|
||||
|
||||
|
||||
def test_position_indicator_set_range(position_indicator):
|
||||
"""
|
||||
Test set_range method of PositionIndicator
|
||||
"""
|
||||
position_indicator.set_range(0, 20)
|
||||
assert position_indicator.minimum == 0
|
||||
assert position_indicator.maximum == 20
|
||||
|
||||
|
||||
def test_position_indicator_set_value(position_indicator):
|
||||
"""
|
||||
Test set_value method of PositionIndicator and the correct mapping of the value
|
||||
within the paintEvent method
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
position_indicator.set_value(50)
|
||||
assert position_indicator.position == 50
|
||||
|
||||
position_indicator.paintEvent(None)
|
||||
assert position_indicator._current_indicator_position == 50
|
||||
|
||||
position_indicator.set_value(100)
|
||||
position_indicator.paintEvent(None)
|
||||
assert position_indicator._draw_position == position_indicator.width()
|
||||
|
||||
position_indicator.vertical = True
|
||||
position_indicator.paintEvent(None)
|
||||
assert position_indicator._draw_position == position_indicator.height()
|
||||
@@ -1,9 +1,6 @@
|
||||
import re
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.text_box.text_box import TextBox
|
||||
from bec_widgets.widgets.text_box.text_box import DEFAULT_TEXT, TextBox
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
@@ -18,37 +15,16 @@ def text_box_widget(qtbot, mocked_client):
|
||||
|
||||
def test_textbox_widget(text_box_widget):
|
||||
"""Test the TextBox widget."""
|
||||
# Test default
|
||||
assert text_box_widget.config.text == DEFAULT_TEXT
|
||||
# Test set text
|
||||
text = "Hello World!"
|
||||
text_box_widget.set_text(text)
|
||||
assert text_box_widget.toPlainText() == text
|
||||
|
||||
text_box_widget.set_color("#FFDDC1", "#123456")
|
||||
text_box_widget.set_font_size(20)
|
||||
assert (
|
||||
text_box_widget.styleSheet() == "background-color: #FFDDC1; color: #123456; font-size: 20px"
|
||||
)
|
||||
text_box_widget.set_color("white", "blue")
|
||||
text_box_widget.set_font_size(14)
|
||||
assert text_box_widget.styleSheet() == "background-color: white; color: blue; font-size: 14px"
|
||||
text_box_widget.set_plain_text(text)
|
||||
assert text_box_widget.plain_text == text
|
||||
# Test set HTML text
|
||||
text = "<h1>Welcome to PyQt6</h1><p>This is an example of displaying <strong>HTML</strong> text.</p>"
|
||||
with mock.patch.object(text_box_widget, "setHtml") as mocked_set_html:
|
||||
text_box_widget.set_text(text)
|
||||
assert mocked_set_html.call_count == 1
|
||||
assert mocked_set_html.call_args == mock.call(text)
|
||||
|
||||
|
||||
def test_textbox_change_theme(text_box_widget):
|
||||
"""Test change theme functionaility"""
|
||||
# Default is dark theme
|
||||
text_box_widget.change_theme()
|
||||
assert text_box_widget.config.theme == "light"
|
||||
text_box_widget.set_html_text(text)
|
||||
assert (
|
||||
text_box_widget.styleSheet()
|
||||
== f"background-color: #FFF; color: #000; font-size: {text_box_widget.config.font_size}px"
|
||||
)
|
||||
text_box_widget.change_theme()
|
||||
assert text_box_widget.config.theme == "dark"
|
||||
assert (
|
||||
text_box_widget.styleSheet()
|
||||
== f"background-color: #000; color: #FFF; font-size: {text_box_widget.config.font_size}px"
|
||||
text_box_widget.plain_text
|
||||
== "Welcome to PyQt6\nThis is an example of displaying HTML text."
|
||||
)
|
||||
|
||||
@@ -72,6 +72,7 @@ def test_create_waveform1D_by_config(qtbot, mocked_client):
|
||||
"y_lim": None,
|
||||
"x_grid": False,
|
||||
"y_grid": False,
|
||||
"outer_axes": False,
|
||||
},
|
||||
"color_palette": "magma",
|
||||
"curves": {
|
||||
|
||||
@@ -7,6 +7,7 @@ from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.utils.colors import apply_theme, get_theme_palette, set_theme
|
||||
from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
|
||||
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
|
||||
from bec_widgets.widgets.waveform.waveform_popups.curve_dialog.curve_dialog import CurveSettings
|
||||
from bec_widgets.widgets.waveform.waveform_popups.dap_summary_dialog.dap_summary_dialog import (
|
||||
@@ -20,6 +21,8 @@ from .conftest import create_widget
|
||||
|
||||
@pytest.fixture
|
||||
def waveform_widget(qtbot, mocked_client):
|
||||
models = ["GaussianModel", "LorentzModel", "SineModel"]
|
||||
mocked_client.dap._available_dap_plugins.keys.return_value = models
|
||||
widget = BECWaveformWidget(client=mocked_client())
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
@@ -357,7 +360,8 @@ def test_curve_dialog_async(qtbot, waveform_widget):
|
||||
|
||||
|
||||
def test_curve_dialog_dap(qtbot, waveform_widget):
|
||||
waveform_widget.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
|
||||
# Don't use default dap for curve_dialog dialog
|
||||
waveform_widget.plot(x_name="samx", y_name="bpm4i", dap="LorentzModel")
|
||||
|
||||
curve_dialog = show_curve_dialog(qtbot, waveform_widget)
|
||||
|
||||
@@ -369,6 +373,7 @@ def test_curve_dialog_dap(qtbot, waveform_widget):
|
||||
assert curve_dialog.widget.ui.dap_table.rowCount() == 1
|
||||
assert curve_dialog.widget.ui.dap_table.cellWidget(0, 0).text() == "bpm4i"
|
||||
assert curve_dialog.widget.ui.dap_table.cellWidget(0, 1).text() == "bpm4i"
|
||||
assert curve_dialog.widget.ui.dap_table.cellWidget(0, 2).currentText() == "LorentzModel"
|
||||
assert curve_dialog.widget.ui.x_mode.currentText() == "device"
|
||||
assert curve_dialog.widget.ui.x_name.isEnabled() == True
|
||||
assert curve_dialog.widget.ui.x_entry.isEnabled() == True
|
||||
@@ -514,3 +519,63 @@ def test_waveform_widget_theme_update(qtbot, waveform_widget):
|
||||
bg_color = waveform_widget.fig.backgroundBrush().color()
|
||||
assert bg_color == QColor("black")
|
||||
assert waveform_color == waveform_color_dark
|
||||
|
||||
|
||||
def test_waveform_roi_selection_creation(waveform_widget, qtbot):
|
||||
"""Test ROI selection for waveform widget.
|
||||
|
||||
This checks that the ROI select is properly created and removed when the button is toggled.
|
||||
"""
|
||||
# Check if curve is create upon ROI select slot
|
||||
# This also checks that the button in the toolbar works
|
||||
container = []
|
||||
|
||||
def callback(msg):
|
||||
container.append(msg)
|
||||
|
||||
waveform_widget.waveform.roi_active.connect(callback)
|
||||
assert waveform_widget.waveform.roi_select is None
|
||||
assert waveform_widget.waveform.roi_region == (None, None)
|
||||
# Toggle the ROI select
|
||||
waveform_widget.toogle_roi_select(True)
|
||||
assert isinstance(waveform_widget.waveform.roi_select, LinearRegionWrapper)
|
||||
# This is the default region for the pg.LinearRegionItem
|
||||
assert waveform_widget.waveform.roi_region == (0, 1)
|
||||
# Untoggle the ROI select
|
||||
waveform_widget.toogle_roi_select(False)
|
||||
assert waveform_widget.waveform.roi_select is None
|
||||
assert container[0] is True
|
||||
assert container[1] is False
|
||||
|
||||
|
||||
def test_waveform_roi_selection_updates_fit(waveform_widget, qtbot):
|
||||
"""This test checks that upon selection of a new region, the fit is updated and all signals are emitted as expected."""
|
||||
container = []
|
||||
|
||||
def callback(msg):
|
||||
container.append(msg)
|
||||
|
||||
waveform_widget.waveform.roi_changed.connect(callback)
|
||||
# Mock refresh_dap method
|
||||
with patch.object(waveform_widget.waveform, "refresh_dap") as mock_refresh_dap:
|
||||
waveform_widget.toogle_roi_select(True)
|
||||
waveform_widget.waveform.roi_select.linear_region_selector.setRegion([0.5, 1.5])
|
||||
qtbot.wait(200)
|
||||
assert waveform_widget.waveform.roi_region == (0.5, 1.5)
|
||||
waveform_widget.toogle_roi_select(False)
|
||||
assert waveform_widget.waveform.roi_region == (None, None)
|
||||
assert len(container) == 1
|
||||
assert container[0] == (0.5, 1.5)
|
||||
# 3 refresh DAP calls: 1x upon hook, 1x unhook and 1x from roi_changed
|
||||
assert mock_refresh_dap.call_count == 3
|
||||
|
||||
|
||||
def test_waveform_roi_selection_change_color(waveform_widget, qtbot):
|
||||
"""This test checks that the color of the ROI region can be changed."""
|
||||
waveform_widget.toogle_roi_select(True)
|
||||
waveform_widget.waveform.roi_select.change_roi_color((QColor("red"), QColor("blue")))
|
||||
# I can only get the brush from the RegionSelectItem
|
||||
assert (
|
||||
waveform_widget.waveform.roi_select.linear_region_selector.currentBrush.color()
|
||||
== QColor("red")
|
||||
)
|
||||
|
||||