mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-09 02:07:55 +01:00
fix: adjust ring progress bar to ads
This commit is contained in:
@@ -4713,29 +4713,6 @@ class ResumeButton(RPCBase):
|
||||
|
||||
|
||||
class Ring(RPCBase):
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_value(self, value: "int | float"):
|
||||
"""
|
||||
@@ -4755,14 +4732,24 @@ class Ring(RPCBase):
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_background(self, color: "str | tuple"):
|
||||
def set_background(self, color: "str | tuple | QColor"):
|
||||
"""
|
||||
Set the background color for the ring widget
|
||||
Set the background color for the ring widget. The background color is only used when colors are not linked.
|
||||
|
||||
Args:
|
||||
color(str | tuple): Background color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_colors_linked(self, linked: "bool"):
|
||||
"""
|
||||
Set whether the colors are linked for the ring widget.
|
||||
If colors are linked, changing the main color will also change the background color.
|
||||
|
||||
Args:
|
||||
linked(bool): Whether to link the colors for the ring widget
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_line_width(self, width: "int"):
|
||||
"""
|
||||
@@ -4785,14 +4772,16 @@ class Ring(RPCBase):
|
||||
@rpc_call
|
||||
def set_start_angle(self, start_angle: "int"):
|
||||
"""
|
||||
Set the start angle for the ring widget
|
||||
Set the start angle for the ring widget.
|
||||
|
||||
Args:
|
||||
start_angle(int): Start angle for the ring widget in degrees
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_update(self, mode: "Literal['manual', 'scan', 'device']", device: "str" = None):
|
||||
def set_update(
|
||||
self, mode: "Literal['manual', 'scan', 'device']", device: "str" = "", signal: "str" = ""
|
||||
):
|
||||
"""
|
||||
Set the update mode for the ring widget.
|
||||
Modes:
|
||||
@@ -4803,193 +4792,24 @@ class Ring(RPCBase):
|
||||
Args:
|
||||
mode(str): Update mode for the ring widget. Can be "manual", "scan" or "device"
|
||||
device(str): Device name for the device readback mode, only used when mode is "device"
|
||||
signal(str): Signal name for the device readback mode, only used when mode is "device"
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def reset_connection(self):
|
||||
def set_precision(self, precision: "int"):
|
||||
"""
|
||||
Reset the connections for the ring widget. Disconnect the current slot and endpoint.
|
||||
Set the precision for the ring widget.
|
||||
|
||||
Args:
|
||||
precision(int): Precision for the ring widget
|
||||
"""
|
||||
|
||||
|
||||
class RingProgressBar(RPCBase):
|
||||
"""Show the progress of devices, scans or custom values in the form of ring progress bars."""
|
||||
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
def remove(self):
|
||||
"""
|
||||
Get all registered RPC objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _rpc_id(self) -> "str":
|
||||
"""
|
||||
Get the RPC ID of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def _config_dict(self) -> "dict":
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rings(self) -> "list[Ring]":
|
||||
"""
|
||||
Returns a list of all rings in the progress bar.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def update_config(self, config: "RingProgressBarConfig | dict"):
|
||||
"""
|
||||
Update the configuration of the widget.
|
||||
|
||||
Args:
|
||||
config(SpiralProgressBarConfig|dict): Configuration to update.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_ring(self, **kwargs) -> "Ring":
|
||||
"""
|
||||
Add a new progress bar.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the new progress bar.
|
||||
|
||||
Returns:
|
||||
Ring: Ring object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove_ring(self, index: "int"):
|
||||
"""
|
||||
Remove a progress bar by index.
|
||||
|
||||
Args:
|
||||
index(int): Index of the progress bar to remove.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_precision(self, precision: "int", bar_index: "int | None" = None):
|
||||
"""
|
||||
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
|
||||
|
||||
Args:
|
||||
precision(int): Precision for the progress bars.
|
||||
bar_index(int): Index of the progress bar to set the precision for. If provided, only a single precision can be set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_min_max_values(
|
||||
self,
|
||||
min_values: "int | float | list[int | float]",
|
||||
max_values: "int | float | list[int | float]",
|
||||
):
|
||||
"""
|
||||
Set the minimum and maximum values for the progress bars.
|
||||
|
||||
Args:
|
||||
min_values(int|float | list[float]): Minimum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of minimum values for each progress bar.
|
||||
max_values(int|float | list[float]): Maximum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of maximum values for each progress bar.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_number_of_bars(self, num_bars: "int"):
|
||||
"""
|
||||
Set the number of progress bars to display.
|
||||
|
||||
Args:
|
||||
num_bars(int): Number of progress bars to display.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_value(self, values: "int | list", ring_index: "int" = None):
|
||||
"""
|
||||
Set the values for the progress bars.
|
||||
|
||||
Args:
|
||||
values(int | tuple): Value(s) for the progress bars. If multiple progress bars are displayed, provide a tuple of values for each progress bar.
|
||||
ring_index(int): Index of the progress bar to set the value for. If provided, only a single value can be set.
|
||||
|
||||
Examples:
|
||||
>>> SpiralProgressBar.set_value(50)
|
||||
>>> SpiralProgressBar.set_value([30, 40, 50]) # (outer, middle, inner)
|
||||
>>> SpiralProgressBar.set_value(60, bar_index=1) # Set the value for the middle progress bar.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_colors_from_map(self, colormap, color_format: "Literal['RGB', 'HEX']" = "RGB"):
|
||||
"""
|
||||
Set the colors for the progress bars from a colormap.
|
||||
|
||||
Args:
|
||||
colormap(str): Name of the colormap.
|
||||
color_format(Literal["RGB","HEX"]): Format of the returned colors ('RGB', 'HEX').
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_colors_directly(
|
||||
self, colors: "list[str | tuple] | str | tuple", bar_index: "int" = None
|
||||
):
|
||||
"""
|
||||
Set the colors for the progress bars directly.
|
||||
|
||||
Args:
|
||||
colors(list[str | tuple] | str | tuple): Color(s) for the progress bars. If multiple progress bars are displayed, provide a list of colors for each progress bar.
|
||||
bar_index(int): Index of the progress bar to set the color for. If provided, only a single color can be set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_line_widths(self, widths: "int | list[int]", bar_index: "int" = None):
|
||||
"""
|
||||
Set the line widths for the progress bars.
|
||||
|
||||
Args:
|
||||
widths(int | list[int]): Line width(s) for the progress bars. If multiple progress bars are displayed, provide a list of line widths for each progress bar.
|
||||
bar_index(int): Index of the progress bar to set the line width for. If provided, only a single line width can be set.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_gap(self, gap: "int"):
|
||||
"""
|
||||
Set the gap between the progress bars.
|
||||
|
||||
Args:
|
||||
gap(int): Gap between the progress bars.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_diameter(self, diameter: "int"):
|
||||
"""
|
||||
Set the diameter of the widget.
|
||||
|
||||
Args:
|
||||
diameter(int): Diameter of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def reset_diameter(self):
|
||||
"""
|
||||
Reset the fixed size of the widget.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def enable_auto_updates(self, enable: "bool" = True):
|
||||
"""
|
||||
Enable or disable updates based on scan status. Overrides manual updates.
|
||||
The behaviour of the whole progress bar widget will be driven by the scan queue status.
|
||||
|
||||
Args:
|
||||
enable(bool): True or False.
|
||||
|
||||
Returns:
|
||||
bool: True if scan segment updates are enabled.
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@@ -5011,6 +4831,56 @@ class RingProgressBar(RPCBase):
|
||||
Take a screenshot of the dock area and save it to a file.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def rings(self) -> list[bec_widgets.widgets.progress.ring_progress_bar.ring.Ring]:
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def add_ring(
|
||||
self, config: dict | None = None
|
||||
) -> bec_widgets.widgets.progress.ring_progress_bar.ring.Ring:
|
||||
"""
|
||||
Add a new ring to the ring progress bar.
|
||||
Optionally, a configuration dictionary can be provided but the ring
|
||||
can also be configured later. The config dictionary must provide
|
||||
the qproperties of the Qt Ring object.
|
||||
|
||||
Args:
|
||||
config(dict | None): Optional configuration dictionary for the ring.
|
||||
|
||||
Returns:
|
||||
Ring: The newly added ring object.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def remove_ring(self, index: int | None = None):
|
||||
"""
|
||||
Remove a ring from the ring progress bar.
|
||||
Args:
|
||||
index(int | None): Index of the ring to remove. If None, removes the last ring.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_gap(self, value: int):
|
||||
"""
|
||||
Set the gap between rings.
|
||||
|
||||
Args:
|
||||
value(int): Gap value in pixels.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_center_label(self, text: str):
|
||||
"""
|
||||
Set the center label text.
|
||||
|
||||
Args:
|
||||
text(str): Text for the center label.
|
||||
"""
|
||||
|
||||
|
||||
class SBBMonitor(RPCBase):
|
||||
"""A widget to display the SBB monitor website."""
|
||||
|
||||
@@ -77,7 +77,7 @@ from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
|
||||
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
|
||||
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
from bec_widgets.widgets.progress.ring_progress_bar import RingProgressBar
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
|
||||
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
|
||||
from bec_widgets.widgets.utility.logpanel import LogPanel
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .ring_progress_bar import RingProgressBar
|
||||
|
||||
@@ -1,130 +1,88 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
from typing import TYPE_CHECKING, Callable, Literal
|
||||
|
||||
from bec_lib.endpoints import EndpointInfo, MessageEndpoints
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtGui
|
||||
from qtpy.QtCore import QObject
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||
from bec_widgets.utils.colors import Colors
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
|
||||
|
||||
class ProgressbarConnections(BaseModel):
|
||||
slot: Literal["on_scan_progress", "on_device_readback", None] = None
|
||||
endpoint: EndpointInfo | str | None = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@field_validator("endpoint")
|
||||
@classmethod
|
||||
def validate_endpoint(cls, v, values):
|
||||
slot = values.data["slot"]
|
||||
v = v.endpoint if isinstance(v, EndpointInfo) else v
|
||||
if slot == "on_scan_progress":
|
||||
if v != MessageEndpoints.scan_progress().endpoint:
|
||||
raise PydanticCustomError(
|
||||
"unsupported endpoint",
|
||||
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'.",
|
||||
{"wrong_value": v},
|
||||
)
|
||||
elif slot == "on_device_readback":
|
||||
if not v.startswith(MessageEndpoints.device_readback("").endpoint):
|
||||
raise PydanticCustomError(
|
||||
"unsupported endpoint",
|
||||
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'.",
|
||||
{"wrong_value": v},
|
||||
)
|
||||
return v
|
||||
logger = bec_logger.logger
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
|
||||
RingProgressContainerWidget,
|
||||
)
|
||||
|
||||
|
||||
class ProgressbarConfig(ConnectionConfig):
|
||||
value: int | float | None = Field(0, description="Value for the progress bars.")
|
||||
direction: int | None = Field(
|
||||
value: int | float = Field(0, description="Value for the progress bars.")
|
||||
direction: int = Field(
|
||||
-1, description="Direction of the progress bars. -1 for clockwise, 1 for counter-clockwise."
|
||||
)
|
||||
color: str | tuple | None = Field(
|
||||
color: str | tuple = Field(
|
||||
(0, 159, 227, 255),
|
||||
description="Color for the progress bars. Can be tuple (R, G, B, A) or string HEX Code.",
|
||||
)
|
||||
background_color: str | tuple | None = Field(
|
||||
background_color: str | tuple = Field(
|
||||
(200, 200, 200, 50),
|
||||
description="Background color for the progress bars. Can be tuple (R, G, B, A) or string HEX Code.",
|
||||
)
|
||||
index: int | None = Field(0, description="Index of the progress bar. 0 is outer ring.")
|
||||
line_width: int | None = Field(10, description="Line widths for the progress bars.")
|
||||
start_position: int | None = Field(
|
||||
link_colors: bool = Field(
|
||||
True,
|
||||
description="Whether to link the background color to the main color. If True, changing the main color will also change the background color.",
|
||||
)
|
||||
line_width: int = Field(20, description="Line widths for the progress bars.")
|
||||
start_position: int = Field(
|
||||
90,
|
||||
description="Start position for the progress bars in degrees. Default is 90 degrees - corespons to "
|
||||
"the top of the ring.",
|
||||
)
|
||||
min_value: int | float | None = Field(0, description="Minimum value for the progress bars.")
|
||||
max_value: int | float | None = Field(100, description="Maximum value for the progress bars.")
|
||||
precision: int | None = Field(3, description="Precision for the progress bars.")
|
||||
update_behaviour: Literal["manual", "auto"] | None = Field(
|
||||
"auto", description="Update behaviour for the progress bars."
|
||||
min_value: int | float = Field(0, description="Minimum value for the progress bars.")
|
||||
max_value: int | float = Field(100, description="Maximum value for the progress bars.")
|
||||
precision: int = Field(3, description="Precision for the progress bars.")
|
||||
mode: Literal["manual", "scan", "device"] = Field(
|
||||
"manual", description="Update mode for the progress bars."
|
||||
)
|
||||
connections: ProgressbarConnections | None = Field(
|
||||
default_factory=ProgressbarConnections, description="Connections for the progress bars."
|
||||
device: str | None = Field(
|
||||
None,
|
||||
description="Device name for the device readback mode, only used when mode is 'device'.",
|
||||
)
|
||||
signal: str | None = Field(
|
||||
None,
|
||||
description="Signal name for the device readback mode, only used when mode is 'device'.",
|
||||
)
|
||||
|
||||
|
||||
class RingConfig(ProgressbarConfig):
|
||||
index: int | None = Field(0, description="Index of the progress bar. 0 is outer ring.")
|
||||
start_position: int | None = Field(
|
||||
90,
|
||||
description="Start position for the progress bars in degrees. Default is 90 degrees - corespons to "
|
||||
"the top of the ring.",
|
||||
)
|
||||
|
||||
|
||||
class Ring(BECConnector, QObject):
|
||||
class Ring(BECConnector, QWidget):
|
||||
USER_ACCESS = [
|
||||
"_get_all_rpc",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"set_value",
|
||||
"set_color",
|
||||
"set_background",
|
||||
"set_colors_linked",
|
||||
"set_line_width",
|
||||
"set_min_max_values",
|
||||
"set_start_angle",
|
||||
"set_update",
|
||||
"reset_connection",
|
||||
"set_precision",
|
||||
]
|
||||
RPC = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
config: RingConfig | dict | None = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = RingConfig(widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = RingConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.parent_progress_widget = parent
|
||||
|
||||
self.color = None
|
||||
self.background_color = None
|
||||
self.start_position = None
|
||||
self.config = config
|
||||
def __init__(self, parent: RingProgressContainerWidget | None = None, client=None, **kwargs):
|
||||
self.progress_container = parent
|
||||
self.config: ProgressbarConfig = ProgressbarConfig(widget_class=self.__class__.__name__) # type: ignore
|
||||
super().__init__(parent=parent, client=client, config=self.config, **kwargs)
|
||||
self._color: QColor = self.convert_color(self.config.color)
|
||||
self._background_color: QColor = self.convert_color(self.config.background_color)
|
||||
self.registered_slot: tuple[Callable, str | EndpointInfo] | None = None
|
||||
self.RID = None
|
||||
self._init_config_params()
|
||||
|
||||
def _init_config_params(self):
|
||||
self.color = self.convert_color(self.config.color)
|
||||
self.background_color = self.convert_color(self.config.background_color)
|
||||
self._gap = 5
|
||||
self.set_start_angle(self.config.start_position)
|
||||
if self.config.connections:
|
||||
self.set_connections(self.config.connections.slot, self.config.connections.endpoint)
|
||||
|
||||
def set_value(self, value: int | float):
|
||||
"""
|
||||
@@ -133,11 +91,7 @@ class Ring(BECConnector, QObject):
|
||||
Args:
|
||||
value(int | float): Value for the ring widget
|
||||
"""
|
||||
self.config.value = round(
|
||||
float(max(self.config.min_value, min(self.config.max_value, value))),
|
||||
self.config.precision,
|
||||
)
|
||||
self.parent_progress_widget.update()
|
||||
self.value = value
|
||||
|
||||
def set_color(self, color: str | tuple):
|
||||
"""
|
||||
@@ -146,20 +100,53 @@ class Ring(BECConnector, QObject):
|
||||
Args:
|
||||
color(str | tuple): Color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
self.config.color = color
|
||||
self.color = self.convert_color(color)
|
||||
self.parent_progress_widget.update()
|
||||
self._color = self.convert_color(color)
|
||||
self.config.color = self._color.name()
|
||||
|
||||
def set_background(self, color: str | tuple):
|
||||
# Automatically set background color
|
||||
if self.config.link_colors:
|
||||
self._auto_set_background_color()
|
||||
|
||||
self.update()
|
||||
|
||||
def set_background(self, color: str | tuple | QColor):
|
||||
"""
|
||||
Set the background color for the ring widget
|
||||
Set the background color for the ring widget. The background color is only used when colors are not linked.
|
||||
|
||||
Args:
|
||||
color(str | tuple): Background color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
self.config.background_color = color
|
||||
self.color = self.convert_color(color)
|
||||
self.parent_progress_widget.update()
|
||||
# Only allow manual background color changes when colors are not linked
|
||||
if self.config.link_colors:
|
||||
return
|
||||
|
||||
self._background_color = self.convert_color(color)
|
||||
self.config.background_color = self._background_color.name()
|
||||
self.update()
|
||||
|
||||
def _auto_set_background_color(self):
|
||||
"""
|
||||
Automatically set the background color based on the main color and the current theme.
|
||||
"""
|
||||
palette = self.palette()
|
||||
bg = palette.color(QtGui.QPalette.ColorRole.Window)
|
||||
bg_color = Colors.subtle_background_color(self._color, bg)
|
||||
self.config.background_color = bg_color.name()
|
||||
self._background_color = bg_color
|
||||
self.update()
|
||||
|
||||
def set_colors_linked(self, linked: bool):
|
||||
"""
|
||||
Set whether the colors are linked for the ring widget.
|
||||
If colors are linked, changing the main color will also change the background color.
|
||||
|
||||
Args:
|
||||
linked(bool): Whether to link the colors for the ring widget
|
||||
"""
|
||||
self.config.link_colors = linked
|
||||
if linked:
|
||||
self._auto_set_background_color()
|
||||
self.update()
|
||||
|
||||
def set_line_width(self, width: int):
|
||||
"""
|
||||
@@ -169,7 +156,7 @@ class Ring(BECConnector, QObject):
|
||||
width(int): Line width for the ring widget
|
||||
"""
|
||||
self.config.line_width = width
|
||||
self.parent_progress_widget.update()
|
||||
self.update()
|
||||
|
||||
def set_min_max_values(self, min_value: int | float, max_value: int | float):
|
||||
"""
|
||||
@@ -181,35 +168,21 @@ class Ring(BECConnector, QObject):
|
||||
"""
|
||||
self.config.min_value = min_value
|
||||
self.config.max_value = max_value
|
||||
self.parent_progress_widget.update()
|
||||
self.update()
|
||||
|
||||
def set_start_angle(self, start_angle: int):
|
||||
"""
|
||||
Set the start angle for the ring widget
|
||||
Set the start angle for the ring widget.
|
||||
|
||||
Args:
|
||||
start_angle(int): Start angle for the ring widget in degrees
|
||||
"""
|
||||
self.config.start_position = start_angle
|
||||
self.start_position = start_angle * 16
|
||||
self.parent_progress_widget.update()
|
||||
self.update()
|
||||
|
||||
@staticmethod
|
||||
def convert_color(color):
|
||||
"""
|
||||
Convert the color to QColor
|
||||
|
||||
Args:
|
||||
color(str | tuple): Color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
converted_color = None
|
||||
if isinstance(color, str):
|
||||
converted_color = QtGui.QColor(color)
|
||||
elif isinstance(color, tuple):
|
||||
converted_color = QtGui.QColor(*color)
|
||||
return converted_color
|
||||
|
||||
def set_update(self, mode: Literal["manual", "scan", "device"], device: str = None):
|
||||
def set_update(
|
||||
self, mode: Literal["manual", "scan", "device"], device: str = "", signal: str = ""
|
||||
):
|
||||
"""
|
||||
Set the update mode for the ring widget.
|
||||
Modes:
|
||||
@@ -220,47 +193,167 @@ class Ring(BECConnector, QObject):
|
||||
Args:
|
||||
mode(str): Update mode for the ring widget. Can be "manual", "scan" or "device"
|
||||
device(str): Device name for the device readback mode, only used when mode is "device"
|
||||
signal(str): Signal name for the device readback mode, only used when mode is "device"
|
||||
"""
|
||||
if mode == "manual":
|
||||
if self.config.connections.slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
getattr(self, self.config.connections.slot), self.config.connections.endpoint
|
||||
|
||||
match mode:
|
||||
case "manual":
|
||||
if self.config.mode == "manual":
|
||||
return
|
||||
if self.registered_slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(*self.registered_slot)
|
||||
self.config.mode = "manual"
|
||||
self.registered_slot = None
|
||||
case "scan":
|
||||
if self.config.mode == "scan":
|
||||
return
|
||||
if self.registered_slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(*self.registered_slot)
|
||||
self.config.mode = "scan"
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_scan_progress, MessageEndpoints.scan_progress()
|
||||
)
|
||||
self.config.connections.slot = None
|
||||
self.config.connections.endpoint = None
|
||||
elif mode == "scan":
|
||||
self.set_connections("on_scan_progress", MessageEndpoints.scan_progress())
|
||||
elif mode == "device":
|
||||
self.set_connections("on_device_readback", MessageEndpoints.device_readback(device))
|
||||
self.registered_slot = (self.on_scan_progress, MessageEndpoints.scan_progress())
|
||||
case "device":
|
||||
if self.registered_slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(*self.registered_slot)
|
||||
self.config.mode = "device"
|
||||
if device == "":
|
||||
self.registered_slot = None
|
||||
return
|
||||
self.config.device = device
|
||||
# self.config.signal = self._get_signal_from_device(device, signal)
|
||||
signal = self._update_device_connection(device, signal)
|
||||
self.config.signal = signal
|
||||
|
||||
self.parent_progress_widget.enable_auto_updates(False)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported mode: {mode}")
|
||||
|
||||
def set_connections(self, slot: str, endpoint: str | EndpointInfo):
|
||||
def set_precision(self, precision: int):
|
||||
"""
|
||||
Set the connections for the ring widget
|
||||
Set the precision for the ring widget.
|
||||
|
||||
Args:
|
||||
slot(str): Slot for the ring widget update. Can be "on_scan_progress" or "on_device_readback".
|
||||
endpoint(str | EndpointInfo): Endpoint for the ring widget update. Endpoint has to match the slot type.
|
||||
precision(int): Precision for the ring widget
|
||||
"""
|
||||
if self.config.connections.endpoint == endpoint and self.config.connections.slot == slot:
|
||||
return
|
||||
if self.config.connections.slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
getattr(self, self.config.connections.slot), self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections = ProgressbarConnections(slot=slot, endpoint=endpoint)
|
||||
self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint)
|
||||
self.config.precision = precision
|
||||
self.update()
|
||||
|
||||
def reset_connection(self):
|
||||
def set_direction(self, direction: int):
|
||||
"""
|
||||
Reset the connections for the ring widget. Disconnect the current slot and endpoint.
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.config.connections.slot, self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections = ProgressbarConnections()
|
||||
Set the direction for the ring widget.
|
||||
|
||||
Args:
|
||||
direction(int): Direction for the ring widget. -1 for clockwise, 1 for counter-clockwise.
|
||||
"""
|
||||
self.config.direction = direction
|
||||
self.update()
|
||||
|
||||
def _get_signals_for_device(self, device: str) -> dict[str, list[str]]:
|
||||
"""
|
||||
Get the signals for the device.
|
||||
|
||||
Args:
|
||||
device(str): Device name for the device
|
||||
|
||||
Returns:
|
||||
dict[str, list[str]]: Dictionary with the signals for the device
|
||||
"""
|
||||
dm = self.bec_dispatcher.client.device_manager
|
||||
if not dm:
|
||||
raise ValueError("Device manager is not available in the BEC client.")
|
||||
dev_obj = dm.devices.get(device)
|
||||
if dev_obj is None:
|
||||
raise ValueError(f"Device '{device}' not found in device manager.")
|
||||
|
||||
progress_signals = [
|
||||
obj["component_name"]
|
||||
for obj in dev_obj._info["signals"].values()
|
||||
if obj["signal_class"] == "ProgressSignal"
|
||||
]
|
||||
hinted_signals = [
|
||||
obj["obj_name"]
|
||||
for obj in dev_obj._info["signals"].values()
|
||||
if obj["kind_str"] == "hinted"
|
||||
and obj["signal_class"]
|
||||
not in ["ProgressSignal", "AyncSignal", "AsyncMultiSignal", "DynamicSignal"]
|
||||
]
|
||||
|
||||
normal_signals = [
|
||||
obj["component_name"]
|
||||
for obj in dev_obj._info["signals"].values()
|
||||
if obj["kind_str"] == "normal"
|
||||
]
|
||||
return {
|
||||
"progress_signals": progress_signals,
|
||||
"hinted_signals": hinted_signals,
|
||||
"normal_signals": normal_signals,
|
||||
}
|
||||
|
||||
def _update_device_connection(self, device: str, signal: str | None) -> str:
|
||||
"""
|
||||
Update the device connection for the ring widget.
|
||||
|
||||
In general, we support two modes here:
|
||||
- If signal is provided, we use that directly.
|
||||
- If signal is not provided, we try to get the signal from the device manager.
|
||||
We first check for progress signals, then for hinted signals, and finally for normal signals.
|
||||
|
||||
Depending on what type of signal we get (progress or hinted/normal), we subscribe to different endpoints.
|
||||
|
||||
Args:
|
||||
device(str): Device name for the device mode
|
||||
signal(str): Signal name for the device mode
|
||||
|
||||
Returns:
|
||||
str: The selected signal name for the device mode
|
||||
"""
|
||||
logger.info(f"Updating device connection for device '{device}' and signal '{signal}'")
|
||||
dm = self.bec_dispatcher.client.device_manager
|
||||
if not dm:
|
||||
raise ValueError("Device manager is not available in the BEC client.")
|
||||
dev_obj = dm.devices.get(device)
|
||||
if dev_obj is None:
|
||||
return ""
|
||||
|
||||
signals = self._get_signals_for_device(device)
|
||||
progress_signals = signals["progress_signals"]
|
||||
hinted_signals = signals["hinted_signals"]
|
||||
normal_signals = signals["normal_signals"]
|
||||
|
||||
if not signal:
|
||||
# If signal is not provided, we try to get it from the device manager
|
||||
if len(progress_signals) > 0:
|
||||
signal = progress_signals[0]
|
||||
logger.info(
|
||||
f"Using progress signal '{signal}' for device '{device}' in ring progress bar."
|
||||
)
|
||||
elif len(hinted_signals) > 0:
|
||||
signal = hinted_signals[0]
|
||||
logger.info(
|
||||
f"Using hinted signal '{signal}' for device '{device}' in ring progress bar."
|
||||
)
|
||||
elif len(normal_signals) > 0:
|
||||
signal = normal_signals[0]
|
||||
logger.info(
|
||||
f"Using normal signal '{signal}' for device '{device}' in ring progress bar."
|
||||
)
|
||||
else:
|
||||
logger.warning(f"No signals found for device '{device}' in ring progress bar.")
|
||||
return ""
|
||||
|
||||
if signal in progress_signals:
|
||||
endpoint = MessageEndpoints.device_progress(device)
|
||||
self.bec_dispatcher.connect_slot(self.on_device_progress, endpoint)
|
||||
self.registered_slot = (self.on_device_progress, endpoint)
|
||||
return signal
|
||||
if signal in hinted_signals or signal in normal_signals:
|
||||
endpoint = MessageEndpoints.device_readback(device)
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoint)
|
||||
self.registered_slot = (self.on_device_readback, endpoint)
|
||||
return signal
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_scan_progress(self, msg, meta):
|
||||
"""
|
||||
Update the ring widget with the scan progress.
|
||||
@@ -273,8 +366,9 @@ class Ring(BECConnector, QObject):
|
||||
if current_RID != self.RID:
|
||||
self.set_min_max_values(0, msg.get("max_value", 100))
|
||||
self.set_value(msg.get("value", 0))
|
||||
self.parent_progress_widget.update()
|
||||
self.update()
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_readback(self, msg, meta):
|
||||
"""
|
||||
Update the ring widget with the device readback.
|
||||
@@ -283,11 +377,242 @@ class Ring(BECConnector, QObject):
|
||||
msg(dict): Message with the device readback
|
||||
meta(dict): Metadata for the message
|
||||
"""
|
||||
if isinstance(self.config.connections.endpoint, EndpointInfo):
|
||||
endpoint = self.config.connections.endpoint.endpoint
|
||||
else:
|
||||
endpoint = self.config.connections.endpoint
|
||||
device = endpoint.split("/")[-1]
|
||||
value = msg.get("signals").get(device).get("value")
|
||||
device = self.config.device
|
||||
if device is None:
|
||||
return
|
||||
signal = self.config.signal or device
|
||||
value = msg.get("signals", {}).get(signal, {}).get("value", None)
|
||||
if value is None:
|
||||
return
|
||||
self.set_value(value)
|
||||
self.parent_progress_widget.update()
|
||||
self.update()
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_progress(self, msg, meta):
|
||||
"""
|
||||
Update the ring widget with the device progress.
|
||||
|
||||
Args:
|
||||
msg(dict): Message with the device progress
|
||||
meta(dict): Metadata for the message
|
||||
"""
|
||||
device = self.config.device
|
||||
if device is None:
|
||||
return
|
||||
max_val = msg.get("max_value", 100)
|
||||
self.set_min_max_values(0, max_val)
|
||||
value = msg.get("value", 0)
|
||||
if msg.get("done"):
|
||||
value = max_val
|
||||
self.set_value(value)
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
if not self.progress_container:
|
||||
return
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
||||
size = min(self.width(), self.height())
|
||||
|
||||
# Center the ring
|
||||
x_offset = (self.width() - size) // 2
|
||||
y_offset = (self.height() - size) // 2
|
||||
|
||||
max_ring_size = self.progress_container.get_max_ring_size()
|
||||
|
||||
rect = QtCore.QRect(x_offset, y_offset, size, size)
|
||||
rect.adjust(max_ring_size, max_ring_size, -max_ring_size, -max_ring_size)
|
||||
|
||||
# Background arc
|
||||
painter.setPen(
|
||||
QtGui.QPen(self._background_color, self.config.line_width, QtCore.Qt.PenStyle.SolidLine)
|
||||
)
|
||||
|
||||
gap: int = self.gap # type: ignore
|
||||
|
||||
# Important: Qt uses a 16th of a degree for angles. start_position is therefore multiplied by 16.
|
||||
start_position: float = self.config.start_position * 16 # type: ignore
|
||||
|
||||
adjusted_rect = QtCore.QRect(
|
||||
rect.left() + gap, rect.top() + gap, rect.width() - 2 * gap, rect.height() - 2 * gap
|
||||
)
|
||||
painter.drawArc(adjusted_rect, start_position, 360 * 16)
|
||||
|
||||
# Foreground arc
|
||||
pen = QtGui.QPen(self.color, self.config.line_width, QtCore.Qt.PenStyle.SolidLine)
|
||||
pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap)
|
||||
painter.setPen(pen)
|
||||
proportion = (self.config.value - self.config.min_value) / (
|
||||
(self.config.max_value - self.config.min_value) + 1e-3
|
||||
)
|
||||
angle = int(proportion * 360 * 16 * self.config.direction)
|
||||
painter.drawArc(adjusted_rect, start_position, angle)
|
||||
painter.end()
|
||||
|
||||
def convert_color(self, color: str | tuple | QColor) -> QColor:
|
||||
"""
|
||||
Convert the color to QColor
|
||||
|
||||
Args:
|
||||
color(str | tuple | QColor): Color for the ring widget. Can be HEX code or tuple (R, G, B, A) or QColor.
|
||||
"""
|
||||
|
||||
if isinstance(color, QColor):
|
||||
return color
|
||||
if isinstance(color, str):
|
||||
return QtGui.QColor(color)
|
||||
if isinstance(color, (tuple, list)):
|
||||
return QtGui.QColor(*color)
|
||||
raise ValueError(f"Unsupported color format: {color}")
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the ring widget.
|
||||
Disconnect any registered slots.
|
||||
"""
|
||||
if self.registered_slot is not None:
|
||||
self.bec_dispatcher.disconnect_slot(*self.registered_slot)
|
||||
self.registered_slot = None
|
||||
|
||||
###############################################
|
||||
####### QProperties ###########################
|
||||
###############################################
|
||||
|
||||
@SafeProperty(int)
|
||||
def gap(self) -> int:
|
||||
return self._gap
|
||||
|
||||
@gap.setter
|
||||
def gap(self, value: int):
|
||||
self._gap = value
|
||||
self.update()
|
||||
|
||||
@SafeProperty(bool)
|
||||
def link_colors(self) -> bool:
|
||||
return self.config.link_colors
|
||||
|
||||
@link_colors.setter
|
||||
def link_colors(self, value: bool):
|
||||
logger.info(f"Setting link_colors to {value}")
|
||||
self.set_colors_linked(value)
|
||||
|
||||
@SafeProperty(QColor)
|
||||
def color(self) -> QColor:
|
||||
return self._color
|
||||
|
||||
@color.setter
|
||||
def color(self, value: QColor):
|
||||
self.set_color(value)
|
||||
|
||||
@SafeProperty(QColor)
|
||||
def background_color(self) -> QColor:
|
||||
return self._background_color
|
||||
|
||||
@background_color.setter
|
||||
def background_color(self, value: QColor):
|
||||
self.set_background(value)
|
||||
|
||||
@SafeProperty(float)
|
||||
def value(self) -> float:
|
||||
return self.config.value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: float):
|
||||
self.config.value = round(
|
||||
float(max(self.config.min_value, min(self.config.max_value, value))),
|
||||
self.config.precision,
|
||||
)
|
||||
self.update()
|
||||
|
||||
@SafeProperty(float)
|
||||
def min_value(self) -> float:
|
||||
return self.config.min_value
|
||||
|
||||
@min_value.setter
|
||||
def min_value(self, value: float):
|
||||
self.config.min_value = value
|
||||
self.update()
|
||||
|
||||
@SafeProperty(float)
|
||||
def max_value(self) -> float:
|
||||
return self.config.max_value
|
||||
|
||||
@max_value.setter
|
||||
def max_value(self, value: float):
|
||||
self.config.max_value = value
|
||||
self.update()
|
||||
|
||||
@SafeProperty(str)
|
||||
def mode(self) -> str:
|
||||
return self.config.mode
|
||||
|
||||
@mode.setter
|
||||
def mode(self, value: str):
|
||||
self.set_update(value)
|
||||
|
||||
@SafeProperty(str)
|
||||
def device(self) -> str:
|
||||
return self.config.device or ""
|
||||
|
||||
@device.setter
|
||||
def device(self, value: str):
|
||||
self.config.device = value
|
||||
|
||||
@SafeProperty(str)
|
||||
def signal(self) -> str:
|
||||
return self.config.signal or ""
|
||||
|
||||
@signal.setter
|
||||
def signal(self, value: str):
|
||||
self.config.signal = value
|
||||
|
||||
@SafeProperty(int)
|
||||
def line_width(self) -> int:
|
||||
return self.config.line_width
|
||||
|
||||
@line_width.setter
|
||||
def line_width(self, value: int):
|
||||
self.config.line_width = value
|
||||
self.update()
|
||||
|
||||
@SafeProperty(int)
|
||||
def start_position(self) -> int:
|
||||
return self.config.start_position
|
||||
|
||||
@start_position.setter
|
||||
def start_position(self, value: int):
|
||||
self.config.start_position = value
|
||||
self.update()
|
||||
|
||||
@SafeProperty(int)
|
||||
def precision(self) -> int:
|
||||
return self.config.precision
|
||||
|
||||
@precision.setter
|
||||
def precision(self, value: int):
|
||||
self.config.precision = value
|
||||
self.update()
|
||||
|
||||
@SafeProperty(int)
|
||||
def direction(self) -> int:
|
||||
return self.config.direction
|
||||
|
||||
@direction.setter
|
||||
def direction(self, value: int):
|
||||
self.config.direction = value
|
||||
self.update()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("dark")
|
||||
ring = Ring()
|
||||
ring.export_settings()
|
||||
ring.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -1,348 +1,154 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
import json
|
||||
from typing import Literal
|
||||
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtCore import QSize, Slot
|
||||
from qtpy.QtWidgets import QSizePolicy, QWidget
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import Colors, ConnectionConfig, EntryValidator
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring import Ring, RingConfig
|
||||
from bec_widgets.utils.error_popups import SafeProperty
|
||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring import Ring
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_settings_cards import RingSettings
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class RingProgressBarConfig(ConnectionConfig):
|
||||
color_map: Optional[str] = Field(
|
||||
"plasma", description="Color scheme for the progress bars.", validate_default=True
|
||||
)
|
||||
min_number_of_bars: int = Field(1, description="Minimum number of progress bars to display.")
|
||||
max_number_of_bars: int = Field(10, description="Maximum number of progress bars to display.")
|
||||
num_bars: int = Field(1, description="Number of progress bars to display.")
|
||||
gap: int | None = Field(20, description="Gap between progress bars.")
|
||||
auto_updates: bool | None = Field(
|
||||
True, description="Enable or disable updates based on scan queue status."
|
||||
)
|
||||
rings: list[RingConfig] | None = Field([], description="List of ring configurations.")
|
||||
|
||||
@field_validator("num_bars")
|
||||
@classmethod
|
||||
def validate_num_bars(cls, v, values):
|
||||
min_number_of_bars = values.data.get("min_number_of_bars", None)
|
||||
max_number_of_bars = values.data.get("max_number_of_bars", None)
|
||||
if min_number_of_bars is not None and max_number_of_bars is not None:
|
||||
logger.info(
|
||||
f"Number of bars adjusted to be between defined min:{min_number_of_bars} and max:{max_number_of_bars} number of bars."
|
||||
)
|
||||
v = max(min_number_of_bars, min(v, max_number_of_bars))
|
||||
return v
|
||||
|
||||
@field_validator("rings")
|
||||
@classmethod
|
||||
def validate_rings(cls, v, values):
|
||||
if v is not None and v is not []:
|
||||
num_bars = values.data.get("num_bars", None)
|
||||
if len(v) != num_bars:
|
||||
raise PydanticCustomError(
|
||||
"different number of configs",
|
||||
f"Length of rings configuration ({len(v)}) does not match the number of bars ({num_bars}).",
|
||||
{"wrong_value": len(v)},
|
||||
)
|
||||
indices = [ring.index for ring in v]
|
||||
if sorted(indices) != list(range(len(indices))):
|
||||
raise PydanticCustomError(
|
||||
"wrong indices",
|
||||
f"Indices of ring configurations must be unique and in order from 0 to num_bars {num_bars}.",
|
||||
{"wrong_value": indices},
|
||||
)
|
||||
return v
|
||||
|
||||
_validate_colormap = field_validator("color_map")(Colors.validate_color_map)
|
||||
|
||||
|
||||
class RingProgressBar(BECWidget, QWidget):
|
||||
class RingProgressContainerWidget(QWidget):
|
||||
"""
|
||||
Show the progress of devices, scans or custom values in the form of ring progress bars.
|
||||
A container widget for the Ring Progress Bar widget.
|
||||
It holds the rings and manages their layout and painting.
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "track_changes"
|
||||
USER_ACCESS = [
|
||||
"_get_all_rpc",
|
||||
"_rpc_id",
|
||||
"_config_dict",
|
||||
"rings",
|
||||
"update_config",
|
||||
"add_ring",
|
||||
"remove_ring",
|
||||
"set_precision",
|
||||
"set_min_max_values",
|
||||
"set_number_of_bars",
|
||||
"set_value",
|
||||
"set_colors_from_map",
|
||||
"set_colors_directly",
|
||||
"set_line_widths",
|
||||
"set_gap",
|
||||
"set_diameter",
|
||||
"reset_diameter",
|
||||
"enable_auto_updates",
|
||||
"attach",
|
||||
"detach",
|
||||
"screenshot",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
config: RingProgressBarConfig | dict | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
num_bars: int | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = RingProgressBarConfig(widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = RingProgressBarConfig(**config, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
self.RID = None
|
||||
|
||||
# For updating bar behaviour
|
||||
self._auto_updates = True
|
||||
self._rings = []
|
||||
|
||||
if num_bars is not None:
|
||||
self.config.num_bars = max(
|
||||
self.config.min_number_of_bars, min(num_bars, self.config.max_number_of_bars)
|
||||
)
|
||||
def __init__(self, parent: QWidget | None = None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self.rings: list[Ring] = []
|
||||
self.gap = 20 # Gap between rings
|
||||
self.color_map: str = "turbo"
|
||||
self.setLayout(QHBoxLayout())
|
||||
self.initialize_bars()
|
||||
|
||||
self.enable_auto_updates(self.config.auto_updates)
|
||||
self.initialize_center_label()
|
||||
|
||||
@property
|
||||
def rings(self) -> list[Ring]:
|
||||
"""Returns a list of all rings in the progress bar."""
|
||||
return self._rings
|
||||
|
||||
@rings.setter
|
||||
def rings(self, value: list[Ring]):
|
||||
self._rings = value
|
||||
|
||||
def update_config(self, config: RingProgressBarConfig | dict):
|
||||
"""
|
||||
Update the configuration of the widget.
|
||||
|
||||
Args:
|
||||
config(SpiralProgressBarConfig|dict): Configuration to update.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
config = RingProgressBarConfig(**config, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
self.clear_all()
|
||||
def num_bars(self) -> int:
|
||||
return len(self.rings)
|
||||
|
||||
def initialize_bars(self):
|
||||
"""
|
||||
Initialize the progress bars.
|
||||
"""
|
||||
start_positions = [90 * 16] * self.config.num_bars
|
||||
directions = [-1] * self.config.num_bars
|
||||
for _ in range(self.num_bars):
|
||||
self.add_ring()
|
||||
|
||||
self.config.rings = [
|
||||
RingConfig(
|
||||
widget_class="Ring",
|
||||
index=i,
|
||||
start_positions=start_positions[i],
|
||||
directions=directions[i],
|
||||
)
|
||||
for i in range(self.config.num_bars)
|
||||
]
|
||||
self._rings = [Ring(parent=self, config=config) for config in self.config.rings]
|
||||
if self.color_map:
|
||||
self.set_colors_from_map(self.color_map)
|
||||
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
|
||||
min_size = self._calculate_minimum_size()
|
||||
self.setMinimumSize(min_size)
|
||||
# Set outer ring to listen to scan progress
|
||||
self.rings[0].set_update(mode="scan")
|
||||
self.update()
|
||||
|
||||
def add_ring(self, **kwargs) -> Ring:
|
||||
def add_ring(self, config: dict | None = None) -> Ring:
|
||||
"""
|
||||
Add a new progress bar.
|
||||
Add a new ring to the container.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the new progress bar.
|
||||
config(dict | None): Optional configuration dictionary for the ring.
|
||||
|
||||
Returns:
|
||||
Ring: Ring object.
|
||||
Ring: The newly added ring object.
|
||||
"""
|
||||
if self.config.num_bars < self.config.max_number_of_bars:
|
||||
ring_index = self.config.num_bars
|
||||
ring_config = RingConfig(
|
||||
widget_class="Ring",
|
||||
index=ring_index,
|
||||
start_positions=90 * 16,
|
||||
directions=-1,
|
||||
**kwargs,
|
||||
)
|
||||
ring = Ring(parent=self, config=ring_config)
|
||||
self.config.num_bars += 1
|
||||
self._rings.append(ring)
|
||||
self.config.rings.append(ring.config)
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
base_line_width = self._rings[ring.config.index].config.line_width
|
||||
self.set_line_widths(base_line_width, ring.config.index)
|
||||
self.update()
|
||||
return ring
|
||||
ring = Ring(parent=self)
|
||||
ring.setGeometry(self.rect())
|
||||
ring.gap = self.gap * len(self.rings)
|
||||
ring.set_value(0)
|
||||
self.rings.append(ring)
|
||||
if config:
|
||||
# We have to first get the link_colors property before loading the settings
|
||||
# While this is an ugly hack, we do not have control over the order of properties
|
||||
# being set when loading.
|
||||
ring.link_colors = config.pop("link_colors", True)
|
||||
ring.load_settings(config)
|
||||
if self.color_map:
|
||||
self.set_colors_from_map(self.color_map)
|
||||
ring.show()
|
||||
ring.raise_()
|
||||
self.update()
|
||||
return ring
|
||||
|
||||
def remove_ring(self, index: int):
|
||||
def remove_ring(self, index: int | None = None):
|
||||
"""
|
||||
Remove a progress bar by index.
|
||||
Remove a ring from the container.
|
||||
|
||||
Args:
|
||||
index(int): Index of the progress bar to remove.
|
||||
index(int | None): Index of the ring to remove. If None, removes the last ring.
|
||||
"""
|
||||
ring = self._find_ring_by_index(index)
|
||||
self._cleanup_ring(ring)
|
||||
self.update()
|
||||
|
||||
def _cleanup_ring(self, ring: Ring) -> None:
|
||||
ring.reset_connection()
|
||||
self._rings.remove(ring)
|
||||
self.config.rings.remove(ring.config)
|
||||
self.config.num_bars -= 1
|
||||
self._reindex_rings()
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
# Remove ring from rpc, afterwards call close event.
|
||||
ring.rpc_register.remove_rpc(ring)
|
||||
if self.num_bars == 0:
|
||||
return
|
||||
if index is None:
|
||||
index = self.num_bars - 1
|
||||
index = self._validate_index(index)
|
||||
ring = self.rings[index]
|
||||
ring.cleanup()
|
||||
ring.close()
|
||||
ring.deleteLater()
|
||||
# del ring
|
||||
|
||||
def _reindex_rings(self):
|
||||
"""
|
||||
Reindex the progress bars.
|
||||
"""
|
||||
for i, ring in enumerate(self._rings):
|
||||
ring.config.index = i
|
||||
|
||||
def set_precision(self, precision: int, bar_index: int | None = None):
|
||||
"""
|
||||
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
|
||||
|
||||
Args:
|
||||
precision(int): Precision for the progress bars.
|
||||
bar_index(int): Index of the progress bar to set the precision for. If provided, only a single precision can be set.
|
||||
"""
|
||||
if bar_index is not None:
|
||||
bar_index = self._bar_index_check(bar_index)
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
ring.config.precision = precision
|
||||
else:
|
||||
for ring in self._rings:
|
||||
ring.config.precision = precision
|
||||
self.rings.pop(index)
|
||||
# Update gaps for remaining rings
|
||||
for i, r in enumerate(self.rings):
|
||||
r.gap = self.gap * i
|
||||
self.update()
|
||||
|
||||
def set_min_max_values(
|
||||
self,
|
||||
min_values: int | float | list[int | float],
|
||||
max_values: int | float | list[int | float],
|
||||
):
|
||||
def initialize_center_label(self):
|
||||
"""
|
||||
Set the minimum and maximum values for the progress bars.
|
||||
|
||||
Args:
|
||||
min_values(int|float | list[float]): Minimum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of minimum values for each progress bar.
|
||||
max_values(int|float | list[float]): Maximum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of maximum values for each progress bar.
|
||||
Initialize the center label.
|
||||
"""
|
||||
if isinstance(min_values, (int, float)):
|
||||
min_values = [min_values]
|
||||
if isinstance(max_values, (int, float)):
|
||||
max_values = [max_values]
|
||||
min_values = self._adjust_list_to_bars(min_values)
|
||||
max_values = self._adjust_list_to_bars(max_values)
|
||||
for ring, min_value, max_value in zip(self._rings, min_values, max_values):
|
||||
ring.set_min_max_values(min_value, max_value)
|
||||
self.update()
|
||||
layout = self.layout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
def set_number_of_bars(self, num_bars: int):
|
||||
self.center_label = QLabel("", parent=self)
|
||||
self.center_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.center_label)
|
||||
|
||||
def _calculate_minimum_size(self):
|
||||
"""
|
||||
Set the number of progress bars to display.
|
||||
|
||||
Args:
|
||||
num_bars(int): Number of progress bars to display.
|
||||
Calculate the minimum size of the widget.
|
||||
"""
|
||||
num_bars = max(
|
||||
self.config.min_number_of_bars, min(num_bars, self.config.max_number_of_bars)
|
||||
)
|
||||
current_num_bars = self.config.num_bars
|
||||
if not self.rings:
|
||||
return QSize(10, 10)
|
||||
ring_widths = self.get_ring_line_widths()
|
||||
total_width = sum(ring_widths) + self.gap * (self.num_bars - 1)
|
||||
diameter = max(total_width * 2, 50)
|
||||
|
||||
if num_bars > current_num_bars:
|
||||
for i in range(current_num_bars, num_bars):
|
||||
new_ring_config = RingConfig(
|
||||
widget_class="Ring", index=i, start_positions=90 * 16, directions=-1
|
||||
)
|
||||
self.config.rings.append(new_ring_config)
|
||||
new_ring = Ring(parent=self, config=new_ring_config)
|
||||
self._rings.append(new_ring)
|
||||
return QSize(diameter, diameter)
|
||||
|
||||
elif num_bars < current_num_bars:
|
||||
for i in range(current_num_bars - 1, num_bars - 1, -1):
|
||||
self.remove_ring(i)
|
||||
|
||||
self.config.num_bars = num_bars
|
||||
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
|
||||
base_line_width = self._rings[0].config.line_width
|
||||
self.set_line_widths(base_line_width)
|
||||
|
||||
self.update()
|
||||
|
||||
def set_value(self, values: int | list, ring_index: int = None):
|
||||
def get_ring_line_widths(self):
|
||||
"""
|
||||
Set the values for the progress bars.
|
||||
|
||||
Args:
|
||||
values(int | tuple): Value(s) for the progress bars. If multiple progress bars are displayed, provide a tuple of values for each progress bar.
|
||||
ring_index(int): Index of the progress bar to set the value for. If provided, only a single value can be set.
|
||||
|
||||
Examples:
|
||||
>>> SpiralProgressBar.set_value(50)
|
||||
>>> SpiralProgressBar.set_value([30, 40, 50]) # (outer, middle, inner)
|
||||
>>> SpiralProgressBar.set_value(60, bar_index=1) # Set the value for the middle progress bar.
|
||||
Get the line widths of the rings.
|
||||
"""
|
||||
if ring_index is not None:
|
||||
ring = self._find_ring_by_index(ring_index)
|
||||
if isinstance(values, list):
|
||||
values = values[0]
|
||||
logger.warning(
|
||||
f"Warning: Only a single value can be set for a single progress bar. Using the first value in the list {values}"
|
||||
)
|
||||
ring.set_value(values)
|
||||
else:
|
||||
if isinstance(values, int):
|
||||
values = [values]
|
||||
values = self._adjust_list_to_bars(values)
|
||||
for ring, value in zip(self._rings, values):
|
||||
ring.set_value(value)
|
||||
self.update()
|
||||
if not self.rings:
|
||||
return [10]
|
||||
ring_widths = [ring.config.line_width for ring in self.rings]
|
||||
return ring_widths
|
||||
|
||||
def get_max_ring_size(self) -> int:
|
||||
"""
|
||||
Get the size of the rings.
|
||||
"""
|
||||
if not self.rings:
|
||||
return 10
|
||||
ring_widths = self.get_ring_line_widths()
|
||||
return max(ring_widths)
|
||||
|
||||
def sizeHint(self):
|
||||
min_size = self._calculate_minimum_size()
|
||||
return min_size
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""
|
||||
Handle resize events to update ring geometries.
|
||||
"""
|
||||
super().resizeEvent(event)
|
||||
for ring in self.rings:
|
||||
ring.setGeometry(self.rect())
|
||||
|
||||
def set_colors_from_map(self, colormap, color_format: Literal["RGB", "HEX"] = "RGB"):
|
||||
"""
|
||||
@@ -356,12 +162,14 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
raise ValueError(
|
||||
f"Colormap '{colormap}' not found in the current installation of pyqtgraph"
|
||||
)
|
||||
colors = Colors.golden_angle_color(colormap, self.config.num_bars, color_format)
|
||||
colors = Colors.golden_angle_color(colormap, self.num_bars, color_format)
|
||||
self.set_colors_directly(colors)
|
||||
self.config.color_map = colormap
|
||||
self.color_map = colormap
|
||||
self.update()
|
||||
|
||||
def set_colors_directly(self, colors: list[str | tuple] | str | tuple, bar_index: int = None):
|
||||
def set_colors_directly(
|
||||
self, colors: list[str | tuple] | str | tuple, bar_index: int | None = None
|
||||
):
|
||||
"""
|
||||
Set the colors for the progress bars directly.
|
||||
|
||||
@@ -370,170 +178,16 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
bar_index(int): Index of the progress bar to set the color for. If provided, only a single color can be set.
|
||||
"""
|
||||
if bar_index is not None and isinstance(colors, (str, tuple)):
|
||||
bar_index = self._bar_index_check(bar_index)
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
ring.set_color(colors)
|
||||
bar_index = self._validate_index(bar_index)
|
||||
self.rings[bar_index].set_color(colors)
|
||||
else:
|
||||
if isinstance(colors, (str, tuple)):
|
||||
colors = [colors]
|
||||
colors = self._adjust_list_to_bars(colors)
|
||||
for ring, color in zip(self._rings, colors):
|
||||
for ring, color in zip(self.rings, colors):
|
||||
ring.set_color(color)
|
||||
self.update()
|
||||
|
||||
def set_line_widths(self, widths: int | list[int], bar_index: int = None):
|
||||
"""
|
||||
Set the line widths for the progress bars.
|
||||
|
||||
Args:
|
||||
widths(int | list[int]): Line width(s) for the progress bars. If multiple progress bars are displayed, provide a list of line widths for each progress bar.
|
||||
bar_index(int): Index of the progress bar to set the line width for. If provided, only a single line width can be set.
|
||||
"""
|
||||
if bar_index is not None:
|
||||
bar_index = self._bar_index_check(bar_index)
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
if isinstance(widths, list):
|
||||
widths = widths[0]
|
||||
logger.warning(
|
||||
f"Warning: Only a single line width can be set for a single progress bar. Using the first value in the list {widths}"
|
||||
)
|
||||
ring.set_line_width(widths)
|
||||
else:
|
||||
if isinstance(widths, int):
|
||||
widths = [widths]
|
||||
widths = self._adjust_list_to_bars(widths)
|
||||
self.config.gap = max(widths) * 2
|
||||
for ring, width in zip(self._rings, widths):
|
||||
ring.set_line_width(width)
|
||||
min_size = self._calculate_minimum_size()
|
||||
self.setMinimumSize(min_size)
|
||||
self.update()
|
||||
|
||||
def set_gap(self, gap: int):
|
||||
"""
|
||||
Set the gap between the progress bars.
|
||||
|
||||
Args:
|
||||
gap(int): Gap between the progress bars.
|
||||
"""
|
||||
self.config.gap = gap
|
||||
self.update()
|
||||
|
||||
def set_diameter(self, diameter: int):
|
||||
"""
|
||||
Set the diameter of the widget.
|
||||
|
||||
Args:
|
||||
diameter(int): Diameter of the widget.
|
||||
"""
|
||||
size = QSize(diameter, diameter)
|
||||
self.resize(size)
|
||||
self.setFixedSize(size)
|
||||
|
||||
def _find_ring_by_index(self, index: int) -> Ring:
|
||||
"""
|
||||
Find the ring by index.
|
||||
|
||||
Args:
|
||||
index(int): Index of the ring.
|
||||
|
||||
Returns:
|
||||
Ring: Ring object.
|
||||
"""
|
||||
for ring in self._rings:
|
||||
if ring.config.index == index:
|
||||
return ring
|
||||
raise ValueError(f"Ring with index {index} not found.")
|
||||
|
||||
def enable_auto_updates(self, enable: bool = True):
|
||||
"""
|
||||
Enable or disable updates based on scan status. Overrides manual updates.
|
||||
The behaviour of the whole progress bar widget will be driven by the scan queue status.
|
||||
|
||||
Args:
|
||||
enable(bool): True or False.
|
||||
|
||||
Returns:
|
||||
bool: True if scan segment updates are enabled.
|
||||
"""
|
||||
|
||||
self._auto_updates = enable
|
||||
if enable is True:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
else:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
return self._auto_updates
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_scan_queue_status(self, msg, meta):
|
||||
"""
|
||||
Slot to handle scan queue status messages. Decides what update to perform based on the scan queue status.
|
||||
|
||||
Args:
|
||||
msg(dict): Message from the BEC.
|
||||
meta(dict): Metadata from the BEC.
|
||||
"""
|
||||
primary_queue = msg.get("queue").get("primary")
|
||||
info = primary_queue.get("info", None)
|
||||
|
||||
if not info:
|
||||
return
|
||||
active_request_block = info[0].get("active_request_block", None)
|
||||
if not active_request_block:
|
||||
return
|
||||
report_instructions = active_request_block.get("report_instructions", None)
|
||||
if not report_instructions:
|
||||
return
|
||||
|
||||
instruction_type = list(report_instructions[0].keys())[0]
|
||||
if instruction_type == "scan_progress":
|
||||
self._hook_scan_progress(ring_index=0)
|
||||
elif instruction_type == "readback":
|
||||
devices = report_instructions[0].get("readback").get("devices")
|
||||
start = report_instructions[0].get("readback").get("start")
|
||||
end = report_instructions[0].get("readback").get("end")
|
||||
if self.config.num_bars != len(devices):
|
||||
self.set_number_of_bars(len(devices))
|
||||
for index, device in enumerate(devices):
|
||||
self._hook_readback(index, device, start[index], end[index])
|
||||
else:
|
||||
logger.error(f"{instruction_type} not supported yet.")
|
||||
|
||||
def _hook_scan_progress(self, ring_index: int | None = None):
|
||||
"""
|
||||
Hook the scan progress to the progress bars.
|
||||
|
||||
Args:
|
||||
ring_index(int): Index of the progress bar to hook the scan progress to.
|
||||
"""
|
||||
if ring_index is not None:
|
||||
ring = self._find_ring_by_index(ring_index)
|
||||
else:
|
||||
ring = self._rings[0]
|
||||
|
||||
if ring.config.connections.slot == "on_scan_progress":
|
||||
return
|
||||
ring.set_connections("on_scan_progress", MessageEndpoints.scan_progress())
|
||||
|
||||
def _hook_readback(self, bar_index: int, device: str, min: float | int, max: float | int):
|
||||
"""
|
||||
Hook the readback values to the progress bars.
|
||||
|
||||
Args:
|
||||
bar_index(int): Index of the progress bar to hook the readback values to.
|
||||
device(str): Device to readback values from.
|
||||
min(float|int): Minimum value for the progress bar.
|
||||
max(float|int): Maximum value for the progress bar.
|
||||
"""
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
ring.set_min_max_values(min, max)
|
||||
endpoint = MessageEndpoints.device_readback(device)
|
||||
ring.set_connections("on_device_readback", endpoint)
|
||||
|
||||
def _adjust_list_to_bars(self, items: list) -> list:
|
||||
"""
|
||||
Utility method to adjust the list of parameters to match the number of progress bars.
|
||||
@@ -550,101 +204,249 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
)
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
if len(items) < self.config.num_bars:
|
||||
if len(items) < self.num_bars:
|
||||
last_item = items[-1]
|
||||
items.extend([last_item] * (self.config.num_bars - len(items)))
|
||||
elif len(items) > self.config.num_bars:
|
||||
items = items[: self.config.num_bars]
|
||||
items.extend([last_item] * (self.num_bars - len(items)))
|
||||
elif len(items) > self.num_bars:
|
||||
items = items[: self.num_bars]
|
||||
return items
|
||||
|
||||
def _bar_index_check(self, bar_index: int):
|
||||
def _validate_index(self, index: int) -> int:
|
||||
"""
|
||||
Utility method to check if the bar index is within the range of the number of progress bars.
|
||||
Check if the provided index is valid for the number of bars.
|
||||
|
||||
Args:
|
||||
bar_index(int): Index of the progress bar to set the value for.
|
||||
index(int): Index to check.
|
||||
Returns:
|
||||
int: Validated index.
|
||||
"""
|
||||
if not (0 <= bar_index < self.config.num_bars):
|
||||
raise ValueError(
|
||||
f"bar_index {bar_index} out of range of number of bars {self.config.num_bars}."
|
||||
)
|
||||
return bar_index
|
||||
|
||||
def paintEvent(self, event):
|
||||
if not self._rings:
|
||||
return
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
size = min(self.width(), self.height())
|
||||
rect = QtCore.QRect(0, 0, size, size)
|
||||
rect.adjust(
|
||||
max(ring.config.line_width for ring in self._rings),
|
||||
max(ring.config.line_width for ring in self._rings),
|
||||
-max(ring.config.line_width for ring in self._rings),
|
||||
-max(ring.config.line_width for ring in self._rings),
|
||||
)
|
||||
|
||||
for i, ring in enumerate(self._rings):
|
||||
# Background arc
|
||||
painter.setPen(
|
||||
QtGui.QPen(ring.background_color, ring.config.line_width, QtCore.Qt.SolidLine)
|
||||
)
|
||||
offset = self.config.gap * i
|
||||
adjusted_rect = QtCore.QRect(
|
||||
rect.left() + offset,
|
||||
rect.top() + offset,
|
||||
rect.width() - 2 * offset,
|
||||
rect.height() - 2 * offset,
|
||||
)
|
||||
painter.drawArc(adjusted_rect, ring.config.start_position, 360 * 16)
|
||||
|
||||
# Foreground arc
|
||||
pen = QtGui.QPen(ring.color, ring.config.line_width, QtCore.Qt.SolidLine)
|
||||
pen.setCapStyle(QtCore.Qt.RoundCap)
|
||||
painter.setPen(pen)
|
||||
proportion = (ring.config.value - ring.config.min_value) / (
|
||||
(ring.config.max_value - ring.config.min_value) + 1e-3
|
||||
)
|
||||
angle = int(proportion * 360 * 16 * ring.config.direction)
|
||||
painter.drawArc(adjusted_rect, ring.start_position, angle)
|
||||
|
||||
def reset_diameter(self):
|
||||
"""
|
||||
Reset the fixed size of the widget.
|
||||
"""
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||||
self.setMinimumSize(self._calculate_minimum_size())
|
||||
self.setMaximumSize(16777215, 16777215)
|
||||
|
||||
def _calculate_minimum_size(self):
|
||||
"""
|
||||
Calculate the minimum size of the widget.
|
||||
"""
|
||||
if not self.config.rings:
|
||||
logger.warning("no rings to get size from setting size to 10x10")
|
||||
return QSize(10, 10)
|
||||
ring_widths = [self.config.rings[i].line_width for i in range(self.config.num_bars)]
|
||||
total_width = sum(ring_widths) + self.config.gap * (self.config.num_bars - 1)
|
||||
diameter = max(total_width * 2, 50)
|
||||
|
||||
return QSize(diameter, diameter)
|
||||
|
||||
def sizeHint(self):
|
||||
min_size = self._calculate_minimum_size()
|
||||
return min_size
|
||||
try:
|
||||
self.rings[index]
|
||||
except IndexError:
|
||||
raise IndexError(f"Index {index} is out of range for {self.num_bars} rings.")
|
||||
return index
|
||||
|
||||
def clear_all(self):
|
||||
for ring in self._rings:
|
||||
ring.reset_connection()
|
||||
self._rings.clear()
|
||||
"""
|
||||
Clear all rings from the widget.
|
||||
"""
|
||||
for ring in self.rings:
|
||||
ring.close()
|
||||
ring.deleteLater()
|
||||
self.rings = []
|
||||
self.update()
|
||||
self.initialize_bars()
|
||||
|
||||
|
||||
class RingProgressBar(BECWidget, QWidget):
|
||||
ICON_NAME = "track_changes"
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
|
||||
USER_ACCESS = [
|
||||
*BECWidget.USER_ACCESS,
|
||||
"screenshot",
|
||||
"rings",
|
||||
"add_ring",
|
||||
"remove_ring",
|
||||
"set_gap",
|
||||
"set_center_label",
|
||||
]
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, client=None, **kwargs):
|
||||
super().__init__(parent=parent, client=client, theme_update=True, **kwargs)
|
||||
|
||||
self.setWindowTitle("Ring Progress Bar")
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.toolbar = ModularToolBar(self)
|
||||
self._init_toolbar()
|
||||
self.layout.addWidget(self.toolbar)
|
||||
|
||||
# Placeholder for the actual ring progress bar widget
|
||||
self.ring_progress_bar = RingProgressContainerWidget(self)
|
||||
self.layout.addWidget(self.ring_progress_bar)
|
||||
|
||||
self.settings_dialog = None
|
||||
|
||||
self.toolbar.show_bundles(["rpb_settings"])
|
||||
|
||||
def apply_theme(self, theme: str):
|
||||
super().apply_theme(theme)
|
||||
if self.ring_progress_bar.color_map:
|
||||
self.ring_progress_bar.set_colors_from_map(self.ring_progress_bar.color_map)
|
||||
|
||||
def _init_toolbar(self):
|
||||
settings_action = MaterialIconAction(
|
||||
icon_name="settings",
|
||||
tooltip="Show Ring Progress Bar Settings",
|
||||
checkable=True,
|
||||
parent=self,
|
||||
)
|
||||
self.toolbar.add_action("rpb_settings", settings_action)
|
||||
settings_action.action.triggered.connect(self._open_settings_dialog)
|
||||
|
||||
def _open_settings_dialog(self):
|
||||
""" "
|
||||
Open the settings dialog for the ring progress bar.
|
||||
"""
|
||||
settings_action = self.toolbar.components.get_action("rpb_settings").action
|
||||
if self.settings_dialog is None or not self.settings_dialog.isVisible():
|
||||
settings = RingSettings(parent=self, target_widget=self, popup=True)
|
||||
self.settings_dialog = SettingsDialog(
|
||||
self,
|
||||
settings_widget=settings,
|
||||
window_title="Ring Progress Bar Settings",
|
||||
modal=False,
|
||||
)
|
||||
self.settings_dialog.resize(900, 500)
|
||||
self.settings_dialog.finished.connect(self._settings_dialog_closed)
|
||||
self.settings_dialog.show()
|
||||
|
||||
settings_action.setChecked(True)
|
||||
else:
|
||||
# Dialog is already open, raise it
|
||||
self.settings_dialog.raise_()
|
||||
self.settings_dialog.activateWindow()
|
||||
settings_action.setChecked(True)
|
||||
|
||||
def _settings_dialog_closed(self):
|
||||
"""
|
||||
Handle the settings dialog being closed.
|
||||
"""
|
||||
settings_action = self.toolbar.components.get_action("rpb_settings").action
|
||||
settings_action.setChecked(False)
|
||||
self.settings_dialog = None
|
||||
|
||||
#################################################
|
||||
###### RPC User Access Methods ##################
|
||||
#################################################
|
||||
|
||||
def add_ring(self, config: dict | None = None) -> Ring:
|
||||
"""
|
||||
Add a new ring to the ring progress bar.
|
||||
Optionally, a configuration dictionary can be provided but the ring
|
||||
can also be configured later. The config dictionary must provide
|
||||
the qproperties of the Qt Ring object.
|
||||
|
||||
Args:
|
||||
config(dict | None): Optional configuration dictionary for the ring.
|
||||
|
||||
Returns:
|
||||
Ring: The newly added ring object.
|
||||
"""
|
||||
return self.ring_progress_bar.add_ring(config=config)
|
||||
|
||||
def remove_ring(self, index: int | None = None):
|
||||
"""
|
||||
Remove a ring from the ring progress bar.
|
||||
Args:
|
||||
index(int | None): Index of the ring to remove. If None, removes the last ring.
|
||||
"""
|
||||
if self.ring_progress_bar.num_bars == 0:
|
||||
return
|
||||
self.ring_progress_bar.remove_ring(index=index)
|
||||
|
||||
def set_gap(self, value: int):
|
||||
"""
|
||||
Set the gap between rings.
|
||||
|
||||
Args:
|
||||
value(int): Gap value in pixels.
|
||||
"""
|
||||
self.gap = value
|
||||
|
||||
def set_center_label(self, text: str):
|
||||
"""
|
||||
Set the center label text.
|
||||
|
||||
Args:
|
||||
text(str): Text for the center label.
|
||||
"""
|
||||
self.center_label = text
|
||||
|
||||
@property
|
||||
def rings(self) -> list[Ring]:
|
||||
return self.ring_progress_bar.rings
|
||||
|
||||
###############################################
|
||||
####### QProperties ###########################
|
||||
###############################################
|
||||
|
||||
@SafeProperty(int)
|
||||
def gap(self) -> int:
|
||||
return self.ring_progress_bar.gap
|
||||
|
||||
@gap.setter
|
||||
def gap(self, value: int):
|
||||
self.ring_progress_bar.gap = value
|
||||
self.ring_progress_bar.update()
|
||||
|
||||
@SafeProperty(str)
|
||||
def color_map(self) -> str:
|
||||
return self.ring_progress_bar.color_map or ""
|
||||
|
||||
@color_map.setter
|
||||
def color_map(self, colormap: str):
|
||||
if colormap == "":
|
||||
self.ring_progress_bar.color_map = ""
|
||||
return
|
||||
if colormap not in pg.colormap.listMaps():
|
||||
return
|
||||
self.ring_progress_bar.set_colors_from_map(colormap)
|
||||
self.ring_progress_bar.color_map = colormap
|
||||
|
||||
@SafeProperty(str)
|
||||
def center_label(self) -> str:
|
||||
return self.ring_progress_bar.center_label.text()
|
||||
|
||||
@center_label.setter
|
||||
def center_label(self, text: str):
|
||||
self.ring_progress_bar.center_label.setText(text)
|
||||
|
||||
@SafeProperty(str, designable=False, popup_error=True)
|
||||
def ring_json(self) -> str:
|
||||
"""
|
||||
A JSON string property that serializes all ring pydantic configs.
|
||||
"""
|
||||
raw_list = []
|
||||
for ring in self.rings:
|
||||
cfg_dict = ring.config.model_dump()
|
||||
raw_list.append(cfg_dict)
|
||||
return json.dumps(raw_list, indent=2)
|
||||
|
||||
@ring_json.setter
|
||||
def ring_json(self, json_data: str):
|
||||
"""
|
||||
Load rings from a JSON string and add them to the ring progress bar.
|
||||
"""
|
||||
try:
|
||||
ring_configs = json.loads(json_data)
|
||||
self.ring_progress_bar.clear_all()
|
||||
for cfg_dict in ring_configs:
|
||||
self.add_ring(config=cfg_dict)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to decode JSON: {e}")
|
||||
|
||||
def cleanup(self):
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
for ring in self._rings:
|
||||
self._cleanup_ring(ring)
|
||||
self._rings.clear()
|
||||
self.ring_progress_bar.clear_all()
|
||||
self.ring_progress_bar.close()
|
||||
self.ring_progress_bar.deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("dark")
|
||||
widget = RingProgressBar()
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -51,7 +51,7 @@ class RingProgressBarPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
return "RingProgressBar"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
return "RingProgressBar"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.settings_dialog import SettingWidget
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring import Ring
|
||||
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
|
||||
RingProgressBar,
|
||||
RingProgressContainerWidget,
|
||||
)
|
||||
|
||||
|
||||
class RingCardWidget(QFrame):
|
||||
def __init__(self, ring: Ring, container: RingProgressContainerWidget, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.ring = ring
|
||||
self.container = container
|
||||
self.details_visible = False
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setObjectName("RingCardWidget")
|
||||
|
||||
bg = self._get_theme_color("BORDER")
|
||||
self.setStyleSheet(
|
||||
f"""
|
||||
#RingCardWidget {{
|
||||
border: 1px solid {bg.name() if bg else '#CCCCCC'};
|
||||
border-radius: 4px;
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
layout.setSpacing(6)
|
||||
|
||||
self._init_header(layout)
|
||||
self._init_details(layout)
|
||||
|
||||
self._init_values()
|
||||
self._connect_signals()
|
||||
self.mode_combo.setCurrentText(self._get_display_mode_string(self.ring.config.mode))
|
||||
self._set_widget_mode_enabled(self.ring.config.mode)
|
||||
|
||||
def _get_theme_color(self, color_name: str) -> QColor | None:
|
||||
app = QApplication.instance()
|
||||
if not app:
|
||||
return
|
||||
if not app.theme:
|
||||
return
|
||||
return app.theme.color(color_name)
|
||||
|
||||
def _init_header(self, parent_layout: QVBoxLayout):
|
||||
"""Create the collapsible header with basic controls"""
|
||||
header = QWidget()
|
||||
layout = QHBoxLayout(header)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(8)
|
||||
|
||||
self.expand_btn = QPushButton("▶")
|
||||
self.expand_btn.setFixedWidth(24)
|
||||
self.expand_btn.clicked.connect(self.toggle_details)
|
||||
|
||||
self.mode_combo = QComboBox()
|
||||
self.mode_combo.addItems(["Manual", "Scan Progress", "Device Readback"])
|
||||
self.mode_combo.currentTextChanged.connect(self._update_mode)
|
||||
|
||||
delete_btn = QPushButton(material_icon("delete"), "")
|
||||
|
||||
color = self._get_theme_color("ACCENT_HIGHLIGHT")
|
||||
delete_btn.setStyleSheet(f"background-color: {color.name() if color else '#CC181E'}")
|
||||
delete_btn.clicked.connect(self._delete_self)
|
||||
|
||||
layout.addWidget(self.expand_btn)
|
||||
layout.addWidget(QLabel("Mode"))
|
||||
layout.addWidget(self.mode_combo)
|
||||
layout.addStretch()
|
||||
layout.addWidget(delete_btn)
|
||||
|
||||
parent_layout.addWidget(header)
|
||||
|
||||
def _init_details(self, parent_layout: QVBoxLayout):
|
||||
"""Create the collapsible details area with the UI file"""
|
||||
self.details = QWidget()
|
||||
self.details.setVisible(False)
|
||||
|
||||
details_layout = QVBoxLayout(self.details)
|
||||
details_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Load UI file into details area
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "ring_settings.ui"), self.details)
|
||||
details_layout.addWidget(self.ui)
|
||||
|
||||
parent_layout.addWidget(self.details)
|
||||
|
||||
def toggle_details(self):
|
||||
"""Toggle visibility of the details area"""
|
||||
self.details_visible = not self.details_visible
|
||||
self.details.setVisible(self.details_visible)
|
||||
self.expand_btn.setText("▼" if self.details_visible else "▶")
|
||||
|
||||
# --------------------------------------------------------
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connect UI signals to ring methods"""
|
||||
# Data connections
|
||||
self.ui.value_spin_box.valueChanged.connect(self.ring.set_value)
|
||||
self.ui.min_spin_box.valueChanged.connect(self._update_min_max)
|
||||
self.ui.max_spin_box.valueChanged.connect(self._update_min_max)
|
||||
|
||||
# Config connections
|
||||
self.ui.start_angle_spin_box.valueChanged.connect(self.ring.set_start_angle)
|
||||
self.ui.direction_combo_box.currentIndexChanged.connect(self._update_direction)
|
||||
self.ui.line_width_spin_box.valueChanged.connect(self.ring.set_line_width)
|
||||
self.ui.background_color_button.color_changed.connect(self.ring.set_background)
|
||||
self.ui.ring_color_button.color_changed.connect(self._on_ring_color_changed)
|
||||
self.ui.device_combo_box.device_selected.connect(self._on_device_changed)
|
||||
self.ui.signal_combo_box.device_signal_changed.connect(self._on_signal_changed)
|
||||
|
||||
def _init_values(self):
|
||||
"""Initialize UI values from ring config"""
|
||||
# Data values
|
||||
self.ui.value_spin_box.setRange(-1e6, 1e6)
|
||||
self.ui.value_spin_box.setValue(self.ring.config.value)
|
||||
|
||||
self.ui.min_spin_box.setRange(-1e6, 1e6)
|
||||
self.ui.min_spin_box.setValue(self.ring.config.min_value)
|
||||
|
||||
self.ui.max_spin_box.setRange(-1e6, 1e6)
|
||||
self.ui.max_spin_box.setValue(self.ring.config.max_value)
|
||||
self._update_min_max()
|
||||
|
||||
self.ui.device_combo_box.setEditable(True)
|
||||
self.ui.signal_combo_box.setEditable(True)
|
||||
|
||||
device, signal = self.ring.config.device, self.ring.config.signal
|
||||
if device:
|
||||
self.ui.device_combo_box.set_device(device)
|
||||
if signal:
|
||||
for i in range(self.ui.signal_combo_box.count()):
|
||||
data_item = self.ui.signal_combo_box.itemData(i)
|
||||
if data_item and data_item.get("obj_name") == signal:
|
||||
self.ui.signal_combo_box.setCurrentIndex(i)
|
||||
break
|
||||
|
||||
# Config values
|
||||
self.ui.start_angle_spin_box.setValue(self.ring.config.start_position)
|
||||
self.ui.direction_combo_box.setCurrentIndex(0 if self.ring.config.direction == -1 else 1)
|
||||
self.ui.line_width_spin_box.setRange(1, 100)
|
||||
self.ui.line_width_spin_box.setValue(self.ring.config.line_width)
|
||||
|
||||
# Colors
|
||||
self.ui.ring_color_button.set_color(self.ring.color)
|
||||
self.ui.color_sync_button.setCheckable(True)
|
||||
self.ui.color_sync_button.setChecked(self.ring.config.link_colors)
|
||||
|
||||
# Set initial button state based on link_colors
|
||||
if self.ring.config.link_colors:
|
||||
self.ui.color_sync_button.setIcon(material_icon("link"))
|
||||
self.ui.color_sync_button.setToolTip(
|
||||
"Colors are linked - background derives from main color"
|
||||
)
|
||||
self.ui.background_color_button.setEnabled(False)
|
||||
self.ui.background_color_label.setEnabled(False)
|
||||
# Trigger sync to ensure background color is derived from main color
|
||||
self.ring.set_color(self.ring.config.color)
|
||||
self.ui.background_color_button.set_color(self.ring.background_color)
|
||||
else:
|
||||
self.ui.color_sync_button.setIcon(material_icon("link_off"))
|
||||
self.ui.color_sync_button.setToolTip(
|
||||
"Colors are unlinked - set background independently"
|
||||
)
|
||||
self.ui.background_color_button.setEnabled(True)
|
||||
self.ui.background_color_label.setEnabled(True)
|
||||
self.ui.background_color_button.set_color(self.ring.background_color)
|
||||
|
||||
self.ui.color_sync_button.toggled.connect(self._toggle_color_link)
|
||||
|
||||
# --------------------------------------------------------
|
||||
|
||||
def _toggle_color_link(self, checked: bool):
|
||||
"""Toggle the color linking between main and background color"""
|
||||
self.ring.config.link_colors = checked
|
||||
|
||||
# Update button icon and tooltip based on state
|
||||
if checked:
|
||||
self.ui.color_sync_button.setIcon(material_icon("link"))
|
||||
self.ui.color_sync_button.setToolTip(
|
||||
"Colors are linked - background derives from main color"
|
||||
)
|
||||
# Trigger background color update by calling set_color
|
||||
self.ring.set_color(self.ring.config.color)
|
||||
# Update UI to show the new background color
|
||||
self.ui.background_color_button.set_color(self.ring.background_color)
|
||||
else:
|
||||
self.ui.color_sync_button.setIcon(material_icon("link_off"))
|
||||
self.ui.color_sync_button.setToolTip(
|
||||
"Colors are unlinked - set background independently"
|
||||
)
|
||||
|
||||
# Enable/disable background color controls based on link state
|
||||
self.ui.background_color_button.setEnabled(not checked)
|
||||
self.ui.background_color_label.setEnabled(not checked)
|
||||
|
||||
def _on_ring_color_changed(self, color: QColor):
|
||||
"""Handle ring color changes and update background if colors are linked"""
|
||||
self.ring.set_color(color)
|
||||
# If colors are linked, update the background color button to show the new derived color
|
||||
if self.ring.config.link_colors:
|
||||
self.ui.background_color_button.set_color(self.ring.background_color)
|
||||
|
||||
def _update_min_max(self):
|
||||
self.ui.value_spin_box.setRange(self.ui.min_spin_box.value(), self.ui.max_spin_box.value())
|
||||
self.ring.set_min_max_values(self.ui.min_spin_box.value(), self.ui.max_spin_box.value())
|
||||
|
||||
def _update_direction(self, index: int):
|
||||
self.ring.config.direction = -1 if index == 0 else 1
|
||||
self.ring.update()
|
||||
|
||||
@SafeSlot(str)
|
||||
def _on_device_changed(self, device: str):
|
||||
signal = self.ui.signal_combo_box.get_signal_name()
|
||||
self.ring.set_update("device", device=device, signal=signal)
|
||||
self.ring.config.device = device
|
||||
|
||||
@SafeSlot(str)
|
||||
def _on_signal_changed(self, signal: str):
|
||||
device = self.ui.device_combo_box.currentText()
|
||||
signal = self.ui.signal_combo_box.get_signal_name()
|
||||
if not device or device not in self.container.bec_dispatcher.client.device_manager.devices:
|
||||
return
|
||||
self.ring.set_update("device", device=device, signal=signal)
|
||||
self.ring.config.signal = signal
|
||||
|
||||
def _unify_mode_string(self, mode: str) -> str:
|
||||
"""Convert mode string to a unified format"""
|
||||
mode = mode.lower()
|
||||
if mode == "scan progress":
|
||||
return "scan"
|
||||
if mode == "device readback":
|
||||
return "device"
|
||||
return mode
|
||||
|
||||
def _get_display_mode_string(self, mode: str) -> str:
|
||||
"""Convert mode string to display format"""
|
||||
match mode:
|
||||
case "manual":
|
||||
return "Manual"
|
||||
case "scan":
|
||||
return "Scan Progress"
|
||||
case "device":
|
||||
return "Device Readback"
|
||||
return mode.capitalize()
|
||||
|
||||
def _update_mode(self, mode: str):
|
||||
"""Update the ring's mode based on combo box selection"""
|
||||
mode = self._unify_mode_string(mode)
|
||||
match mode:
|
||||
case "manual":
|
||||
self.ring.set_update("manual")
|
||||
case "scan":
|
||||
self.ring.set_update("scan")
|
||||
case "device":
|
||||
self.ring.set_update("device", device=self.ui.device_combo_box.currentText())
|
||||
self._set_widget_mode_enabled(mode)
|
||||
|
||||
def _set_widget_mode_enabled(self, mode: str):
|
||||
"""Show/hide controls based on the current mode"""
|
||||
mode = self._unify_mode_string(mode)
|
||||
self.ui.device_combo_box.setEnabled(mode == "device")
|
||||
self.ui.signal_combo_box.setEnabled(mode == "device")
|
||||
self.ui.device_label.setEnabled(mode == "device")
|
||||
self.ui.signal_label.setEnabled(mode == "device")
|
||||
self.ui.min_label.setEnabled(mode in ["manual", "device"])
|
||||
self.ui.max_label.setEnabled(mode in ["manual", "device"])
|
||||
self.ui.value_label.setEnabled(mode == "manual")
|
||||
self.ui.value_spin_box.setEnabled(mode == "manual")
|
||||
self.ui.min_spin_box.setEnabled(mode in ["manual", "device"])
|
||||
self.ui.max_spin_box.setEnabled(mode in ["manual", "device"])
|
||||
|
||||
def _delete_self(self):
|
||||
"""Delete this ring from the container"""
|
||||
if self.ring in self.container.rings:
|
||||
self.container.rings.remove(self.ring)
|
||||
self.ring.deleteLater()
|
||||
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the card widget"""
|
||||
self.ui.device_combo_box.close()
|
||||
self.ui.device_combo_box.deleteLater()
|
||||
self.ui.signal_combo_box.close()
|
||||
self.ui.signal_combo_box.deleteLater()
|
||||
self.close()
|
||||
self.deleteLater()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Ring settings widget
|
||||
# ============================================================
|
||||
|
||||
|
||||
class RingSettings(SettingWidget):
|
||||
def __init__(
|
||||
self, parent=None, target_widget: RingProgressBar | None = None, popup=False, **kwargs
|
||||
):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self.setProperty("skip_settings", True)
|
||||
self.target_widget = target_widget
|
||||
self.popup = popup
|
||||
if not target_widget:
|
||||
return
|
||||
self.container: RingProgressContainerWidget = target_widget.ring_progress_bar
|
||||
self.original_num_bars = len(self.container.rings)
|
||||
self.original_configs = [ring.config.model_dump() for ring in self.container.rings]
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
add_button = QPushButton(material_icon("add"), "Add Ring")
|
||||
add_button.clicked.connect(self.add_ring)
|
||||
|
||||
self.center_label_edit = QLineEdit(self.container.center_label.text())
|
||||
self.center_label_edit.setPlaceholderText("Center Label")
|
||||
self.center_label_edit.textChanged.connect(self._update_center_label)
|
||||
|
||||
self.colormap_toggle = QPushButton()
|
||||
self.colormap_toggle.setCheckable(True)
|
||||
self.colormap_toggle.setIcon(material_icon("palette"))
|
||||
self.colormap_toggle.setToolTip(
|
||||
f"Colormap mode is {'enabled' if self.container.color_map else 'disabled'}"
|
||||
)
|
||||
self.colormap_toggle.toggled.connect(self._toggle_colormap_mode)
|
||||
|
||||
self.colormap_button = BECColorMapWidget(parent=self)
|
||||
self.colormap_button.setToolTip("Set a global colormap for all rings")
|
||||
self.colormap_button.colormap_changed_signal.connect(self._set_global_colormap)
|
||||
|
||||
toolbar = QHBoxLayout()
|
||||
|
||||
toolbar.addWidget(add_button)
|
||||
toolbar.addWidget(self.center_label_edit)
|
||||
|
||||
toolbar.addStretch()
|
||||
toolbar.addWidget(self.colormap_toggle)
|
||||
toolbar.addWidget(self.colormap_button)
|
||||
|
||||
layout.addLayout(toolbar)
|
||||
|
||||
self.scroll = QScrollArea()
|
||||
self.scroll.setWidgetResizable(True)
|
||||
|
||||
self.cards_container = QWidget()
|
||||
self.cards_layout = QVBoxLayout(self.cards_container)
|
||||
self.cards_layout.setSpacing(10)
|
||||
self.cards_layout.addStretch()
|
||||
|
||||
self.scroll.setWidget(self.cards_container)
|
||||
layout.addWidget(self.scroll)
|
||||
|
||||
self.refresh_from_container()
|
||||
self.original_label = self.container.center_label.text()
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return QSize(720, 520)
|
||||
|
||||
def refresh_from_container(self):
|
||||
if not self.container:
|
||||
return
|
||||
|
||||
for ring in self.container.rings:
|
||||
card = RingCardWidget(ring, self.container)
|
||||
self.cards_layout.insertWidget(self.cards_layout.count() - 1, card)
|
||||
|
||||
if self.container.color_map:
|
||||
self.colormap_button.colormap = self.container.color_map
|
||||
self.colormap_toggle.setChecked(bool(self.container.color_map))
|
||||
|
||||
@SafeSlot()
|
||||
def add_ring(self):
|
||||
if not self.container:
|
||||
return
|
||||
self.container.add_ring()
|
||||
ring = self.container.rings[len(self.container.rings) - 1]
|
||||
if ring:
|
||||
card = RingCardWidget(ring, self.container)
|
||||
self.cards_layout.insertWidget(self.cards_layout.count() - 1, card)
|
||||
|
||||
# If a global colormap is set, apply it
|
||||
if self.container.color_map:
|
||||
self._toggle_colormap_mode(bool(self.container.color_map))
|
||||
|
||||
@SafeSlot(str)
|
||||
def _update_center_label(self, text: str):
|
||||
if not self.container:
|
||||
return
|
||||
self.container.center_label.setText(text)
|
||||
|
||||
@SafeSlot(bool)
|
||||
def _toggle_colormap_mode(self, enabled: bool):
|
||||
self.colormap_toggle.setToolTip(f"Colormap mode is {'enabled' if enabled else 'disabled'}")
|
||||
if enabled:
|
||||
colormap = self.colormap_button.colormap
|
||||
self._set_global_colormap(colormap)
|
||||
else:
|
||||
self.container.color_map = ""
|
||||
for i in range(self.cards_layout.count() - 1): # -1 to exclude the stretch
|
||||
widget = self.cards_layout.itemAt(i).widget()
|
||||
if not isinstance(widget, RingCardWidget):
|
||||
continue
|
||||
widget.ui.ring_color_button.setEnabled(not enabled)
|
||||
widget.ui.ring_color_button.setToolTip(
|
||||
"Disabled in colormap mode" if enabled else "Set the ring color"
|
||||
)
|
||||
widget.ui.ring_color_label.setEnabled(not enabled)
|
||||
widget.ui.background_color_button.setEnabled(
|
||||
not enabled and not widget.ring.config.link_colors
|
||||
)
|
||||
widget.ui.color_sync_button.setEnabled(not enabled)
|
||||
|
||||
@SafeSlot(str)
|
||||
def _set_global_colormap(self, colormap: str):
|
||||
if not self.container:
|
||||
return
|
||||
self.container.set_colors_from_map(colormap)
|
||||
|
||||
# Update all ring card color buttons to reflect the new colors
|
||||
for i in range(self.cards_layout.count() - 1): # -1 to exclude the stretch
|
||||
widget = self.cards_layout.itemAt(i).widget()
|
||||
if not isinstance(widget, RingCardWidget):
|
||||
continue
|
||||
widget.ui.ring_color_button.set_color(widget.ring.color)
|
||||
if widget.ring.config.link_colors:
|
||||
widget.ui.background_color_button.set_color(widget.ring.background_color)
|
||||
|
||||
@SafeSlot()
|
||||
def accept_changes(self):
|
||||
if not self.container:
|
||||
return
|
||||
|
||||
self.original_configs = [ring.config.model_dump() for ring in self.container.rings]
|
||||
|
||||
for i, ring in enumerate(self.container.rings):
|
||||
ring.setGeometry(self.container.rect())
|
||||
ring.gap = self.container.gap * i
|
||||
ring.show() # Ensure ring is visible
|
||||
ring.raise_() # Bring ring to front
|
||||
|
||||
self.container.center_label.setText(self.center_label_edit.text())
|
||||
self.original_label = self.container.center_label.text()
|
||||
self.original_num_bars = len(self.container.rings)
|
||||
|
||||
self.container.update()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the settings widget.
|
||||
"""
|
||||
# Remove any rings that were added but not applied
|
||||
if not self.container:
|
||||
return
|
||||
if len(self.container.rings) > self.original_num_bars:
|
||||
remove_rings = self.container.rings[self.original_num_bars :]
|
||||
for ring in remove_rings:
|
||||
self.container.rings.remove(ring)
|
||||
ring.deleteLater()
|
||||
rings_to_add = max(0, self.original_num_bars - len(self.container.rings))
|
||||
for _ in range(rings_to_add):
|
||||
self.container.add_ring()
|
||||
|
||||
# apply original configs to all rings
|
||||
for i, ring in enumerate(self.container.rings):
|
||||
ring.config = ring.config.model_validate(self.original_configs[i])
|
||||
|
||||
for i in range(self.cards_layout.count()):
|
||||
item = self.cards_layout.itemAt(i)
|
||||
if not item or not item.widget():
|
||||
continue
|
||||
widget: RingCardWidget = item.widget()
|
||||
widget.cleanup()
|
||||
self.container.update()
|
||||
self.container.center_label.setText(self.original_label)
|
||||
235
bec_widgets/widgets/progress/ring_progress_bar/ring_settings.ui
Normal file
235
bec_widgets/widgets/progress/ring_progress_bar/ring_settings.ui
Normal file
@@ -0,0 +1,235 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>731</width>
|
||||
<height>199</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QGroupBox" name="data_group_box">
|
||||
<property name="title">
|
||||
<string>Data</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="1" column="0">
|
||||
<widget class="QDoubleSpinBox" name="value_spin_box"/>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="2">
|
||||
<widget class="DeviceComboBox" name="device_combo_box"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="value_label">
|
||||
<property name="text">
|
||||
<string>Value</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="min_label">
|
||||
<property name="text">
|
||||
<string>Min</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="max_label">
|
||||
<property name="text">
|
||||
<string>Max</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QDoubleSpinBox" name="max_spin_box"/>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="signal_label">
|
||||
<property name="text">
|
||||
<string>Signal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="min_spin_box"/>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="2">
|
||||
<widget class="SignalComboBox" name="signal_combo_box"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="device_label">
|
||||
<property name="text">
|
||||
<string>Device</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="3">
|
||||
<widget class="Line" name="data_hor_line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QGroupBox" name="config_group_box">
|
||||
<property name="title">
|
||||
<string>Config</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="2" column="0" colspan="3">
|
||||
<widget class="Line" name="config_hor_line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QSpinBox" name="start_angle_spin_box">
|
||||
<property name="suffix">
|
||||
<string>°</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>360</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>90</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="line_width_label">
|
||||
<property name="text">
|
||||
<string>Line Width</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="direction_combo_box">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Clockwise</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Counter-clockwise</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="direction_label">
|
||||
<property name="text">
|
||||
<string>Direction</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QSpinBox" name="line_width_spin_box">
|
||||
<property name="value">
|
||||
<number>12</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="start_angle_label">
|
||||
<property name="text">
|
||||
<string>Start Angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="ColorButtonNative" name="background_color_button"/>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="ColorButtonNative" name="ring_color_button"/>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="background_color_label">
|
||||
<property name="text">
|
||||
<string>Background Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="ring_color_label">
|
||||
<property name="text">
|
||||
<string>Ring Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="QToolButton" name="color_sync_button">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ColorButtonNative</class>
|
||||
<extends></extends>
|
||||
<header>color_button_native</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>DeviceComboBox</class>
|
||||
<extends></extends>
|
||||
<header>device_combo_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>SignalComboBox</class>
|
||||
<extends></extends>
|
||||
<header>signal_combo_box</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>device_combo_box</sender>
|
||||
<signal>currentTextChanged(QString)</signal>
|
||||
<receiver>signal_combo_box</receiver>
|
||||
<slot>set_device(QString)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>209</x>
|
||||
<y>133</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>213</x>
|
||||
<y>153</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>device_combo_box</sender>
|
||||
<signal>device_reset()</signal>
|
||||
<receiver>signal_combo_box</receiver>
|
||||
<slot>reset_selection()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>135</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>250</x>
|
||||
<y>147</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -24,11 +24,13 @@ In this example, we demonstrate how to add a `RingProgressBar` widget to a `BECD
|
||||
|
||||
```python
|
||||
# Add a new dock with a RingProgressBar widget
|
||||
dock_area = gui.new('my_new_dock_area') # Create a new dock area
|
||||
progress = dock_area.new().new(gui.available_widgets.RingProgressBar)
|
||||
dock_area = gui.new() # Create a new dock area
|
||||
progress = dock_area.new(gui.available_widgets.RingProgressBar)
|
||||
|
||||
# Customize the size of the progress ring
|
||||
progress.set_line_widths(20)
|
||||
# Add a ring to the RingProgressBar
|
||||
progress.add_ring()
|
||||
ring = progress.rings[0]
|
||||
ring.set_value(50) # Set the progress value to 50
|
||||
```
|
||||
|
||||
## Example 2 - Adding Multiple Rings to Track Parallel Tasks
|
||||
@@ -40,8 +42,7 @@ By default, the `RingProgressBar` widget displays a single ring. You can add add
|
||||
progress.add_ring()
|
||||
|
||||
# Customize the rings
|
||||
progress.rings[0].set_line_widths(20) # Set the width of the first ring
|
||||
progress.rings[1].set_line_widths(10) # Set the width of the second ring
|
||||
progress.rings[1].set_value(30) # Set the second ring to 30
|
||||
```
|
||||
|
||||
## Example 3 - Integrating with Device Readback and Scans
|
||||
@@ -56,44 +57,6 @@ progress.rings[0].set_update("scan")
|
||||
progress.rings[1].set_update("device", "samx")
|
||||
```
|
||||
|
||||
## Example 4 - Customizing Visual Elements of the Rings
|
||||
|
||||
The `RingProgressBar` widget offers various customization options, such as changing colors, line widths, and the gap between rings.
|
||||
|
||||
```python
|
||||
# Set the color of the first ring to blue
|
||||
progress.rings[0].set_color("blue")
|
||||
|
||||
# Set the background color of the second ring
|
||||
progress.rings[1].set_background("gray")
|
||||
|
||||
# Adjust the gap between the rings
|
||||
progress.set_gap(5)
|
||||
|
||||
# Set the diameter of the progress bar
|
||||
progress.set_diameter(150)
|
||||
```
|
||||
|
||||
## Example 5 - Manual Updates and Precision Control
|
||||
|
||||
While the `RingProgressBar` supports automatic updates, you can also manually control the progress and set the precision for each ring.
|
||||
|
||||
```python
|
||||
# Disable automatic updates and manually set the progress value
|
||||
progress.enable_auto_updates(False)
|
||||
progress.rings[0].set_value(75) # Set the first ring to 75%
|
||||
|
||||
# Set precision for the progress display
|
||||
progress.set_precision(2) # Display progress with two decimal places
|
||||
|
||||
|
||||
# Setting multiple rigns with different values
|
||||
progress.set_number_of_bars(3)
|
||||
|
||||
# Set the values of the rings to 50, 75, and 25 from outer to inner ring
|
||||
progress.set_value([50, 75, 25])
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
````{tab} API
|
||||
|
||||
@@ -537,30 +537,32 @@ def test_widgets_e2e_positioner_control_line(
|
||||
|
||||
|
||||
# TODO passes locally, fails on CI for some reason... -> issue #1003
|
||||
# @pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||
# def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||
# """Test the RingProgressBar widget"""
|
||||
# gui = connected_client_gui_obj
|
||||
# bec = gui._client
|
||||
# # Create dock_area and widget
|
||||
# widget = create_widget(qtbot, gui, gui.available_widgets.RingProgressBar)
|
||||
# widget: client.RingProgressBar
|
||||
#
|
||||
# widget.set_number_of_bars(3)
|
||||
# widget.rings[0].set_update("manual")
|
||||
# widget.rings[0].set_value(30)
|
||||
# widget.rings[0].set_min_max_values(0, 100)
|
||||
# widget.rings[1].set_update("scan")
|
||||
# widget.rings[2].set_update("device", device="samx")
|
||||
#
|
||||
# # Test rpc calls
|
||||
# dev = bec.device_manager.devices
|
||||
# scans = bec.scans
|
||||
# # Do a scan
|
||||
# scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False).wait()
|
||||
#
|
||||
# # Test removing the widget, or leaving it open for the next test
|
||||
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||
def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||
"""Test the RingProgressBar widget"""
|
||||
gui = connected_client_gui_obj
|
||||
bec = gui._client
|
||||
# Create dock_area and widget
|
||||
widget = create_widget(qtbot, gui, gui.available_widgets.RingProgressBar)
|
||||
widget: client.RingProgressBar
|
||||
|
||||
widget.add_ring()
|
||||
widget.add_ring()
|
||||
widget.add_ring()
|
||||
widget.rings[0].set_update("manual")
|
||||
widget.rings[0].set_value(30)
|
||||
widget.rings[0].set_min_max_values(0, 100)
|
||||
widget.rings[1].set_update("scan")
|
||||
widget.rings[2].set_update("device", device="samx")
|
||||
|
||||
# Test rpc calls
|
||||
dev = bec.device_manager.devices
|
||||
scans = bec.scans
|
||||
# Do a scan
|
||||
scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False).wait()
|
||||
|
||||
# Test removing the widget, or leaving it open for the next test
|
||||
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||
|
||||
|
||||
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import ValidationError
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
from bec_widgets.utils import Colors
|
||||
from bec_widgets.widgets.progress.ring_progress_bar import RingProgressBar
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring import ProgressbarConnections, RingConfig
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBarConfig
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
@@ -29,176 +30,117 @@ def test_bar_init(ring_progress_bar):
|
||||
assert ring_progress_bar.gui_id == ring_progress_bar.config.gui_id
|
||||
|
||||
|
||||
def test_config_validation_num_of_bars():
|
||||
config = RingProgressBarConfig(num_bars=100, min_num_bars=1, max_num_bars=10)
|
||||
|
||||
assert config.num_bars == 10
|
||||
def test_rpb_center_label(ring_progress_bar):
|
||||
test_text = "Center Label"
|
||||
ring_progress_bar.set_center_label(test_text)
|
||||
assert ring_progress_bar.center_label == test_text
|
||||
assert ring_progress_bar.ring_progress_bar.center_label.text() == test_text
|
||||
|
||||
|
||||
def test_config_validation_num_of_ring_error():
|
||||
ring_config_0 = RingConfig(index=0)
|
||||
ring_config_1 = RingConfig(index=1)
|
||||
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
RingProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=1)
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == "different number of configs"
|
||||
assert "Length of rings configuration (2) does not match the number of bars (1)." in str(
|
||||
excinfo.value
|
||||
)
|
||||
|
||||
|
||||
def test_config_validation_ring_indices_wrong_order():
|
||||
ring_config_0 = RingConfig(index=2)
|
||||
ring_config_1 = RingConfig(index=5)
|
||||
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
RingProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2)
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == "wrong indices"
|
||||
assert (
|
||||
"Indices of ring configurations must be unique and in order from 0 to num_bars 2."
|
||||
in str(excinfo.value)
|
||||
)
|
||||
|
||||
|
||||
def test_config_validation_ring_same_indices():
|
||||
ring_config_0 = RingConfig(index=0)
|
||||
ring_config_1 = RingConfig(index=0)
|
||||
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
RingProgressBarConfig(rings=[ring_config_0, ring_config_1], num_bars=2)
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == "wrong indices"
|
||||
assert (
|
||||
"Indices of ring configurations must be unique and in order from 0 to num_bars 2."
|
||||
in str(excinfo.value)
|
||||
)
|
||||
|
||||
|
||||
def test_config_validation_invalid_colormap():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
RingProgressBarConfig(color_map="crazy_colors")
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == "unsupported colormap"
|
||||
assert "Colormap 'crazy_colors' not found in the current installation of pyqtgraph" in str(
|
||||
excinfo.value
|
||||
)
|
||||
|
||||
|
||||
def test_ring_connection_endpoint_validation():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
ProgressbarConnections(slot="on_scan_progress", endpoint="non_existing")
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == "unsupported endpoint"
|
||||
assert (
|
||||
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'."
|
||||
in str(excinfo.value)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
ProgressbarConnections(slot="on_device_readback", endpoint="non_existing")
|
||||
errors = excinfo.value.errors()
|
||||
assert len(errors) == 1
|
||||
assert errors[0]["type"] == "unsupported endpoint"
|
||||
assert (
|
||||
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'."
|
||||
in str(excinfo.value)
|
||||
)
|
||||
|
||||
|
||||
def test_bar_add_number_of_bars(ring_progress_bar):
|
||||
assert ring_progress_bar.config.num_bars == 1
|
||||
|
||||
ring_progress_bar.set_number_of_bars(5)
|
||||
assert ring_progress_bar.config.num_bars == 5
|
||||
|
||||
ring_progress_bar.set_number_of_bars(2)
|
||||
assert ring_progress_bar.config.num_bars == 2
|
||||
|
||||
|
||||
def test_add_remove_bars_individually(ring_progress_bar):
|
||||
def test_add_ring(qtbot, ring_progress_bar):
|
||||
ring_progress_bar.show()
|
||||
initial_num_bars = ring_progress_bar.ring_progress_bar.num_bars
|
||||
assert initial_num_bars == len(ring_progress_bar.rings)
|
||||
ring_progress_bar.add_ring()
|
||||
assert ring_progress_bar.ring_progress_bar.num_bars == initial_num_bars + 1
|
||||
assert len(ring_progress_bar.rings) == initial_num_bars + 1
|
||||
qtbot.wait(200)
|
||||
|
||||
|
||||
def test_remove_ring(ring_progress_bar):
|
||||
ring_progress_bar.add_ring()
|
||||
initial_num_bars = ring_progress_bar.ring_progress_bar.num_bars
|
||||
assert initial_num_bars == len(ring_progress_bar.rings)
|
||||
ring_progress_bar.remove_ring()
|
||||
assert ring_progress_bar.ring_progress_bar.num_bars == initial_num_bars - 1
|
||||
assert len(ring_progress_bar.rings) == initial_num_bars - 1
|
||||
|
||||
assert ring_progress_bar.config.num_bars == 3
|
||||
assert len(ring_progress_bar.config.rings) == 3
|
||||
|
||||
ring_progress_bar.remove_ring(1)
|
||||
assert ring_progress_bar.config.num_bars == 2
|
||||
assert len(ring_progress_bar.config.rings) == 2
|
||||
assert ring_progress_bar.rings[0].config.index == 0
|
||||
assert ring_progress_bar.rings[1].config.index == 1
|
||||
def test_remove_ring_no_bars(ring_progress_bar):
|
||||
# Remove all rings first
|
||||
while ring_progress_bar.ring_progress_bar.num_bars > 0:
|
||||
ring_progress_bar.remove_ring()
|
||||
initial_num_bars = ring_progress_bar.ring_progress_bar.num_bars
|
||||
assert initial_num_bars == 0
|
||||
# Attempt to remove a ring when there are none
|
||||
ring_progress_bar.remove_ring()
|
||||
assert ring_progress_bar.ring_progress_bar.num_bars == initial_num_bars
|
||||
assert len(ring_progress_bar.rings) == initial_num_bars
|
||||
|
||||
|
||||
def test_bar_set_value(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(5)
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
assert ring_progress_bar.config.num_bars == 5
|
||||
assert len(ring_progress_bar.config.rings) == 5
|
||||
assert len(ring_progress_bar.rings) == 5
|
||||
|
||||
ring_progress_bar.set_value([10, 20, 30, 40, 50])
|
||||
ring_progress_bar.rings[0].set_value(10)
|
||||
ring_progress_bar.rings[1].set_value(20)
|
||||
ring_values = [ring.config.value for ring in ring_progress_bar.rings]
|
||||
assert ring_values == [10, 20, 30, 40, 50]
|
||||
|
||||
# update just one bar
|
||||
ring_progress_bar.set_value(90, 1)
|
||||
ring_values = [ring.config.value for ring in ring_progress_bar.rings]
|
||||
assert ring_values == [10, 90, 30, 40, 50]
|
||||
assert ring_values == [10, 20]
|
||||
|
||||
|
||||
def test_bar_set_precision(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(3)
|
||||
# Add 3 rings
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
assert ring_progress_bar.config.num_bars == 3
|
||||
assert len(ring_progress_bar.config.rings) == 3
|
||||
assert ring_progress_bar.ring_progress_bar.num_bars == 3
|
||||
assert len(ring_progress_bar.rings) == 3
|
||||
|
||||
ring_progress_bar.set_precision(2)
|
||||
# Set precision for all rings
|
||||
for ring in ring_progress_bar.rings:
|
||||
ring.set_precision(2)
|
||||
ring_precision = [ring.config.precision for ring in ring_progress_bar.rings]
|
||||
assert ring_precision == [2, 2, 2]
|
||||
|
||||
ring_progress_bar.set_value([10.1234, 20.1234, 30.1234])
|
||||
# Set values
|
||||
for i, ring in enumerate(ring_progress_bar.rings):
|
||||
ring.set_value([10.1234, 20.1234, 30.1234][i])
|
||||
ring_values = [ring.config.value for ring in ring_progress_bar.rings]
|
||||
assert ring_values == [10.12, 20.12, 30.12]
|
||||
|
||||
ring_progress_bar.set_precision(4, 1)
|
||||
# Set precision for ring at index 1
|
||||
ring_progress_bar.rings[1].set_precision(4)
|
||||
ring_precision = [ring.config.precision for ring in ring_progress_bar.rings]
|
||||
assert ring_precision == [2, 4, 2]
|
||||
|
||||
ring_progress_bar.set_value([10.1234, 20.1234, 30.1234])
|
||||
# Set values again
|
||||
for i, ring in enumerate(ring_progress_bar.rings):
|
||||
ring.set_value([10.1234, 20.1234, 30.1234][i])
|
||||
ring_values = [ring.config.value for ring in ring_progress_bar.rings]
|
||||
assert ring_values == [10.12, 20.1234, 30.12]
|
||||
|
||||
|
||||
def test_set_min_max_value(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(2)
|
||||
# Add 2 rings
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
ring_progress_bar.set_min_max_values(0, 10)
|
||||
# Set min/max values for all rings
|
||||
for ring in ring_progress_bar.rings:
|
||||
ring.set_min_max_values(0, 10)
|
||||
ring_min_values = [ring.config.min_value for ring in ring_progress_bar.rings]
|
||||
ring_max_values = [ring.config.max_value for ring in ring_progress_bar.rings]
|
||||
|
||||
assert ring_min_values == [0, 0]
|
||||
assert ring_max_values == [10, 10]
|
||||
|
||||
ring_progress_bar.set_value([5, 15])
|
||||
# Set values
|
||||
ring_progress_bar.rings[0].set_value(5)
|
||||
ring_progress_bar.rings[1].set_value(15)
|
||||
ring_values = [ring.config.value for ring in ring_progress_bar.rings]
|
||||
assert ring_values == [5, 10]
|
||||
|
||||
|
||||
def test_setup_colors_from_colormap(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(5)
|
||||
ring_progress_bar.set_colors_from_map("viridis", "RGB")
|
||||
# Add 5 rings
|
||||
for _ in range(5):
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.ring_progress_bar.set_colors_from_map("viridis", "RGB")
|
||||
|
||||
expected_colors = Colors.golden_angle_color("viridis", 5, "RGB")
|
||||
converted_colors = [ring.color.getRgb() for ring in ring_progress_bar.rings]
|
||||
ring_config_colors = [ring.config.color for ring in ring_progress_bar.rings]
|
||||
ring_config_colors = [QColor(ring.config.color).getRgb() for ring in ring_progress_bar.rings]
|
||||
|
||||
assert expected_colors == converted_colors
|
||||
assert ring_config_colors == expected_colors
|
||||
@@ -206,13 +148,15 @@ def test_setup_colors_from_colormap(ring_progress_bar):
|
||||
|
||||
def get_colors_from_rings(rings):
|
||||
converted_colors = [ring.color.getRgb() for ring in rings]
|
||||
ring_config_colors = [ring.config.color for ring in rings]
|
||||
ring_config_colors = [QColor(ring.config.color).getRgb() for ring in rings]
|
||||
return converted_colors, ring_config_colors
|
||||
|
||||
|
||||
def test_set_colors_from_colormap_and_change_num_of_bars(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(2)
|
||||
ring_progress_bar.set_colors_from_map("viridis", "RGB")
|
||||
# Add 2 rings
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.ring_progress_bar.set_colors_from_map("viridis", "RGB")
|
||||
|
||||
expected_colors = Colors.golden_angle_color("viridis", 2, "RGB")
|
||||
converted_colors, ring_config_colors = get_colors_from_rings(ring_progress_bar.rings)
|
||||
@@ -221,7 +165,9 @@ def test_set_colors_from_colormap_and_change_num_of_bars(ring_progress_bar):
|
||||
assert ring_config_colors == expected_colors
|
||||
|
||||
# increase the number of bars to 6
|
||||
ring_progress_bar.set_number_of_bars(6)
|
||||
for _ in range(4):
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.ring_progress_bar.set_colors_from_map("viridis", "RGB")
|
||||
expected_colors = Colors.golden_angle_color("viridis", 6, "RGB")
|
||||
converted_colors, ring_config_colors = get_colors_from_rings(ring_progress_bar.rings)
|
||||
|
||||
@@ -229,7 +175,9 @@ def test_set_colors_from_colormap_and_change_num_of_bars(ring_progress_bar):
|
||||
assert ring_config_colors == expected_colors
|
||||
|
||||
# decrease the number of bars to 3
|
||||
ring_progress_bar.set_number_of_bars(3)
|
||||
for _ in range(3):
|
||||
ring_progress_bar.remove_ring()
|
||||
ring_progress_bar.ring_progress_bar.set_colors_from_map("viridis", "RGB")
|
||||
expected_colors = Colors.golden_angle_color("viridis", 3, "RGB")
|
||||
converted_colors, ring_config_colors = get_colors_from_rings(ring_progress_bar.rings)
|
||||
|
||||
@@ -238,100 +186,284 @@ def test_set_colors_from_colormap_and_change_num_of_bars(ring_progress_bar):
|
||||
|
||||
|
||||
def test_set_colors_directly(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(3)
|
||||
# Add 3 rings
|
||||
for _ in range(3):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
# setting as a list of rgb tuples
|
||||
colors = [(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255)]
|
||||
ring_progress_bar.set_colors_directly(colors)
|
||||
ring_progress_bar.ring_progress_bar.set_colors_directly(colors)
|
||||
converted_colors = get_colors_from_rings(ring_progress_bar.rings)[0]
|
||||
|
||||
assert colors == converted_colors
|
||||
|
||||
ring_progress_bar.set_colors_directly((255, 0, 0, 255), 1)
|
||||
ring_progress_bar.ring_progress_bar.set_colors_directly((255, 0, 0, 255), 1)
|
||||
converted_colors = get_colors_from_rings(ring_progress_bar.rings)[0]
|
||||
|
||||
assert converted_colors == [(255, 0, 0, 255), (255, 0, 0, 255), (0, 0, 255, 255)]
|
||||
|
||||
|
||||
def test_set_line_width(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(3)
|
||||
# Add 3 rings
|
||||
for _ in range(3):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
ring_progress_bar.set_line_widths(5)
|
||||
# Set line width for all rings
|
||||
for ring in ring_progress_bar.rings:
|
||||
ring.set_line_width(5)
|
||||
line_widths = [ring.config.line_width for ring in ring_progress_bar.rings]
|
||||
|
||||
assert line_widths == [5, 5, 5]
|
||||
|
||||
ring_progress_bar.set_line_widths([10, 20, 30])
|
||||
# Set individual line widths
|
||||
for i, ring in enumerate(ring_progress_bar.rings):
|
||||
ring.set_line_width([10, 20, 30][i])
|
||||
line_widths = [ring.config.line_width for ring in ring_progress_bar.rings]
|
||||
|
||||
assert line_widths == [10, 20, 30]
|
||||
|
||||
ring_progress_bar.set_line_widths(15, 1)
|
||||
# Set line width for ring at index 1
|
||||
ring_progress_bar.rings[1].set_line_width(15)
|
||||
line_widths = [ring.config.line_width for ring in ring_progress_bar.rings]
|
||||
|
||||
assert line_widths == [10, 15, 30]
|
||||
|
||||
|
||||
def test_set_gap(ring_progress_bar):
|
||||
ring_progress_bar.set_number_of_bars(3)
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.set_gap(20)
|
||||
|
||||
assert ring_progress_bar.config.gap == 20
|
||||
assert ring_progress_bar.ring_progress_bar.gap == 20 == ring_progress_bar.gap
|
||||
|
||||
|
||||
def test_auto_update(ring_progress_bar):
|
||||
ring_progress_bar.enable_auto_updates(True)
|
||||
def test_remove_ring_by_index(ring_progress_bar):
|
||||
# Add 5 rings
|
||||
for _ in range(5):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
scan_queue_status_scan_progress = {
|
||||
"queue": {
|
||||
"primary": {
|
||||
"info": [{"active_request_block": {"report_instructions": [{"scan_progress": 10}]}}]
|
||||
}
|
||||
}
|
||||
assert ring_progress_bar.ring_progress_bar.num_bars == 5
|
||||
|
||||
# Store the ring at index 2 before removal
|
||||
ring_at_3 = ring_progress_bar.rings[3]
|
||||
|
||||
# Remove ring at index 2 (middle ring)
|
||||
ring_progress_bar.remove_ring(index=2)
|
||||
|
||||
assert ring_progress_bar.ring_progress_bar.num_bars == 4
|
||||
# Ring that was at index 3 is now at index 2
|
||||
assert ring_progress_bar.rings[2] == ring_at_3
|
||||
|
||||
|
||||
def test_remove_ring_updates_gaps(ring_progress_bar):
|
||||
# Add 3 rings with default gap
|
||||
for _ in range(3):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
initial_gap = ring_progress_bar.gap
|
||||
# Gaps should be: 0, gap, 2*gap
|
||||
expected_gaps = [0, initial_gap, 2 * initial_gap]
|
||||
actual_gaps = [ring.gap for ring in ring_progress_bar.rings]
|
||||
assert actual_gaps == expected_gaps
|
||||
|
||||
# Remove middle ring
|
||||
ring_progress_bar.remove_ring(index=1)
|
||||
|
||||
# Gaps should now be: 0, gap (for the remaining 2 rings)
|
||||
expected_gaps = [0, initial_gap]
|
||||
actual_gaps = [ring.gap for ring in ring_progress_bar.rings]
|
||||
assert actual_gaps == expected_gaps
|
||||
|
||||
|
||||
def test_center_label_property(ring_progress_bar):
|
||||
test_text = "Test Label"
|
||||
ring_progress_bar.center_label = test_text
|
||||
|
||||
assert ring_progress_bar.center_label == test_text
|
||||
assert ring_progress_bar.ring_progress_bar.center_label.text() == test_text
|
||||
|
||||
|
||||
def test_color_map_property(ring_progress_bar):
|
||||
# Add some rings
|
||||
for _ in range(3):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
# Set colormap via property
|
||||
ring_progress_bar.color_map = "plasma"
|
||||
|
||||
assert ring_progress_bar.color_map == "plasma"
|
||||
assert ring_progress_bar.ring_progress_bar.color_map == "plasma"
|
||||
|
||||
# Verify colors were applied
|
||||
expected_colors = Colors.golden_angle_color("plasma", 3, "RGB")
|
||||
actual_colors = [ring.color.getRgb() for ring in ring_progress_bar.rings]
|
||||
assert actual_colors == expected_colors
|
||||
|
||||
|
||||
def test_color_map_property_invalid_colormap(ring_progress_bar):
|
||||
# Make sure that invalid colormaps do not crash the application
|
||||
ring_progress_bar.color_map = "plasma"
|
||||
ring_progress_bar.color_map = "invalid_colormap_name"
|
||||
|
||||
assert ring_progress_bar.color_map == "plasma" # Should remain unchanged
|
||||
|
||||
|
||||
def test_ring_json_serialization(ring_progress_bar):
|
||||
# Add rings with specific configurations
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.add_ring()
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
# Configure rings
|
||||
ring_progress_bar.rings[0].set_value(25)
|
||||
ring_progress_bar.rings[0].set_color((255, 0, 0, 255))
|
||||
ring_progress_bar.rings[1].set_value(50)
|
||||
ring_progress_bar.rings[1].set_line_width(15)
|
||||
ring_progress_bar.rings[2].set_value(75)
|
||||
ring_progress_bar.rings[2].set_precision(4)
|
||||
|
||||
# Get JSON
|
||||
json_str = ring_progress_bar.ring_json
|
||||
|
||||
# Verify it's valid JSON
|
||||
ring_configs = json.loads(json_str)
|
||||
assert isinstance(ring_configs, list)
|
||||
assert len(ring_configs) == 3
|
||||
|
||||
# Check some values
|
||||
assert ring_configs[0]["value"] == 25
|
||||
assert ring_configs[1]["value"] == 50
|
||||
assert ring_configs[1]["line_width"] == 15
|
||||
assert ring_configs[2]["precision"] == 4
|
||||
|
||||
|
||||
def test_ring_json_deserialization(ring_progress_bar):
|
||||
# Create JSON config
|
||||
ring_configs = [
|
||||
{"value": 10, "color": (100, 150, 200, 255), "line_width": 8},
|
||||
{"value": 20, "precision": 2, "min_value": 0, "max_value": 50},
|
||||
{"value": 30, "direction": 1},
|
||||
]
|
||||
json_str = json.dumps(ring_configs)
|
||||
|
||||
# Load via property
|
||||
ring_progress_bar.ring_json = json_str
|
||||
|
||||
# Verify rings were created
|
||||
assert len(ring_progress_bar.rings) == 3
|
||||
|
||||
# Verify configurations
|
||||
assert ring_progress_bar.rings[0].config.value == 10
|
||||
assert ring_progress_bar.rings[0].config.line_width == 8
|
||||
assert ring_progress_bar.rings[1].config.precision == 2
|
||||
assert ring_progress_bar.rings[1].config.max_value == 50
|
||||
assert ring_progress_bar.rings[2].config.direction == 1
|
||||
|
||||
|
||||
def test_ring_json_replaces_existing_rings(ring_progress_bar):
|
||||
# Add some initial rings
|
||||
for _ in range(5):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
assert len(ring_progress_bar.rings) == 5
|
||||
|
||||
# Load new config with only 2 rings
|
||||
ring_configs = [{"value": 10}, {"value": 20}]
|
||||
ring_progress_bar.ring_json = json.dumps(ring_configs)
|
||||
|
||||
# Should have replaced all rings
|
||||
assert len(ring_progress_bar.rings) == 2
|
||||
assert ring_progress_bar.rings[0].config.value == 10
|
||||
assert ring_progress_bar.rings[1].config.value == 20
|
||||
|
||||
|
||||
def test_add_ring_with_config(ring_progress_bar):
|
||||
config = {
|
||||
"value": 42,
|
||||
"color": (128, 64, 192, 255),
|
||||
"line_width": 12,
|
||||
"precision": 1,
|
||||
"min_value": 0,
|
||||
"max_value": 100,
|
||||
}
|
||||
meta = {}
|
||||
ring_progress_bar.color_map = ""
|
||||
ring_progress_bar.add_ring(config=config)
|
||||
|
||||
ring_progress_bar.on_scan_queue_status(scan_queue_status_scan_progress, meta)
|
||||
assert len(ring_progress_bar.rings) == 1
|
||||
ring = ring_progress_bar.rings[0]
|
||||
|
||||
assert ring_progress_bar._auto_updates is True
|
||||
assert len(ring_progress_bar._rings) == 1
|
||||
assert ring_progress_bar._rings[0].config.connections == ProgressbarConnections(
|
||||
slot="on_scan_progress", endpoint=MessageEndpoints.scan_progress()
|
||||
)
|
||||
assert ring.config.value == 42
|
||||
assert ring.config.line_width == 12
|
||||
assert ring.config.precision == 1
|
||||
assert ring.config.max_value == 100
|
||||
assert ring.config.color == "#8040c0" # Hex representation of (128, 64, 192)
|
||||
|
||||
scan_queue_status_device_readback = {
|
||||
"queue": {
|
||||
"primary": {
|
||||
"info": [
|
||||
{
|
||||
"active_request_block": {
|
||||
"report_instructions": [
|
||||
{
|
||||
"readback": {
|
||||
"devices": ["samx", "samy"],
|
||||
"start": [1, 2],
|
||||
"end": [10, 20],
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
ring_progress_bar.on_scan_queue_status(scan_queue_status_device_readback, meta)
|
||||
|
||||
assert ring_progress_bar._auto_updates is True
|
||||
assert len(ring_progress_bar._rings) == 2
|
||||
assert ring_progress_bar._rings[0].config.connections == ProgressbarConnections(
|
||||
slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samx")
|
||||
)
|
||||
assert ring_progress_bar._rings[1].config.connections == ProgressbarConnections(
|
||||
slot="on_device_readback", endpoint=MessageEndpoints.device_readback("samy")
|
||||
)
|
||||
def test_set_colors_directly_single_color_extends_to_all(ring_progress_bar):
|
||||
# Add 4 rings
|
||||
for _ in range(4):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
assert ring_progress_bar._rings[0].config.min_value == 1
|
||||
assert ring_progress_bar._rings[0].config.max_value == 10
|
||||
assert ring_progress_bar._rings[1].config.min_value == 2
|
||||
assert ring_progress_bar._rings[1].config.max_value == 20
|
||||
# Set a single color, should extend to all rings
|
||||
single_color = (200, 100, 50, 255)
|
||||
ring_progress_bar.ring_progress_bar.set_colors_directly(single_color)
|
||||
|
||||
colors = [ring.color.getRgb() for ring in ring_progress_bar.rings]
|
||||
assert all(color == single_color for color in colors)
|
||||
|
||||
|
||||
def test_set_colors_directly_list_too_short(ring_progress_bar):
|
||||
# Add 5 rings
|
||||
for _ in range(5):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
# Provide only 2 colors
|
||||
colors = [(255, 0, 0, 255), (0, 255, 0, 255)]
|
||||
ring_progress_bar.ring_progress_bar.set_colors_directly(colors)
|
||||
|
||||
# Last color should be extended to remaining rings
|
||||
actual_colors = [ring.color.getRgb() for ring in ring_progress_bar.rings]
|
||||
assert actual_colors[0] == (255, 0, 0, 255)
|
||||
assert actual_colors[1] == (0, 255, 0, 255)
|
||||
assert all(color == (0, 255, 0, 255) for color in actual_colors[2:])
|
||||
|
||||
|
||||
def test_gap_affects_ring_positioning(ring_progress_bar):
|
||||
# Add 3 rings
|
||||
for _ in range(3):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
initial_gap = ring_progress_bar.gap
|
||||
|
||||
# Change gap
|
||||
new_gap = 30
|
||||
ring_progress_bar.set_gap(new_gap)
|
||||
|
||||
# Verify gaps are updated but update method is needed for visual changes
|
||||
assert ring_progress_bar.gap == new_gap
|
||||
|
||||
|
||||
def test_clear_all_rings(ring_progress_bar):
|
||||
# Add multiple rings
|
||||
for _ in range(5):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
assert len(ring_progress_bar.rings) == 5
|
||||
|
||||
# Clear all
|
||||
ring_progress_bar.ring_progress_bar.clear_all()
|
||||
|
||||
assert len(ring_progress_bar.rings) == 0
|
||||
assert ring_progress_bar.ring_progress_bar.num_bars == 0
|
||||
|
||||
|
||||
def test_rings_property_returns_correct_list(ring_progress_bar):
|
||||
# Add some rings
|
||||
for _ in range(3):
|
||||
ring_progress_bar.add_ring()
|
||||
|
||||
rings_via_property = ring_progress_bar.rings
|
||||
rings_direct = ring_progress_bar.ring_progress_bar.rings
|
||||
|
||||
# Should return the same list
|
||||
assert rings_via_property is rings_direct
|
||||
assert len(rings_via_property) == 3
|
||||
|
||||
602
tests/unit_tests/test_ring_progress_bar_ring.py
Normal file
602
tests/unit_tests/test_ring_progress_bar_ring.py
Normal file
@@ -0,0 +1,602 @@
|
||||
# pylint: disable=missing-function-docstring, missing-module-docstring
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
from bec_widgets.tests.utils import FakeDevice
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring import ProgressbarConfig, Ring
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
|
||||
RingProgressContainerWidget,
|
||||
)
|
||||
|
||||
from .client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ring_container(qtbot, mocked_client):
|
||||
container = RingProgressContainerWidget()
|
||||
qtbot.addWidget(container)
|
||||
yield container
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ring_widget(qtbot, ring_container, mocked_client):
|
||||
ring = Ring(parent=ring_container, client=mocked_client)
|
||||
qtbot.addWidget(ring)
|
||||
qtbot.waitExposed(ring)
|
||||
yield ring
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ring_widget_with_device(ring_widget):
|
||||
mock_device = FakeDevice(name="samx")
|
||||
ring_widget.bec_dispatcher.client.device_manager.devices["samx"] = mock_device
|
||||
yield ring_widget
|
||||
|
||||
|
||||
def test_ring_initialization(ring_widget):
|
||||
assert ring_widget is not None
|
||||
assert isinstance(ring_widget.config, ProgressbarConfig)
|
||||
assert ring_widget.config.mode == "manual"
|
||||
assert ring_widget.config.value == 0
|
||||
assert ring_widget.registered_slot is None
|
||||
|
||||
|
||||
def test_ring_has_default_config_values(ring_widget):
|
||||
assert ring_widget.config.direction == -1
|
||||
assert ring_widget.config.line_width == 20
|
||||
assert ring_widget.config.start_position == 90
|
||||
assert ring_widget.config.min_value == 0
|
||||
assert ring_widget.config.max_value == 100
|
||||
assert ring_widget.config.precision == 3
|
||||
|
||||
|
||||
###################################
|
||||
# set_update method tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_set_update_to_manual(ring_widget):
|
||||
# Start in manual mode
|
||||
assert ring_widget.config.mode == "manual"
|
||||
|
||||
# Set to manual again (should return early)
|
||||
ring_widget.set_update("manual")
|
||||
assert ring_widget.config.mode == "manual"
|
||||
assert ring_widget.registered_slot is None
|
||||
|
||||
|
||||
def test_set_update_to_scan(ring_widget):
|
||||
# Mock the dispatcher to avoid actual connections
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
|
||||
# Set to scan mode
|
||||
ring_widget.set_update("scan")
|
||||
|
||||
assert ring_widget.config.mode == "scan"
|
||||
# Verify that connect_slot was called
|
||||
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
||||
call_args = ring_widget.bec_dispatcher.connect_slot.call_args
|
||||
assert call_args[0][0] == ring_widget.on_scan_progress
|
||||
assert "scan_progress" in str(call_args[0][1])
|
||||
|
||||
|
||||
def test_set_update_from_scan_to_manual(ring_widget):
|
||||
# Mock the dispatcher
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
||||
|
||||
# Set to scan mode first
|
||||
ring_widget.set_update("scan")
|
||||
assert ring_widget.config.mode == "scan"
|
||||
|
||||
# Now switch back to manual
|
||||
ring_widget.set_update("manual")
|
||||
|
||||
assert ring_widget.config.mode == "manual"
|
||||
assert ring_widget.registered_slot is None
|
||||
|
||||
|
||||
def test_set_update_to_device(ring_widget_with_device):
|
||||
ring_widget = ring_widget_with_device
|
||||
# Mock the dispatcher
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
|
||||
# Set to device mode
|
||||
test_device = "samx"
|
||||
ring_widget.set_update("device", device=test_device)
|
||||
|
||||
assert ring_widget.config.mode == "device"
|
||||
assert ring_widget.config.device == test_device
|
||||
assert ring_widget.config.signal == "samx"
|
||||
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
||||
|
||||
|
||||
def test_set_update_from_device_to_manual(ring_widget_with_device):
|
||||
ring_widget = ring_widget_with_device
|
||||
# Mock the dispatcher
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
||||
|
||||
# Set to device mode first
|
||||
ring_widget.set_update("device", device="samx")
|
||||
assert ring_widget.config.mode == "device"
|
||||
|
||||
# Switch to manual
|
||||
ring_widget.set_update("manual")
|
||||
|
||||
assert ring_widget.config.mode == "manual"
|
||||
assert ring_widget.registered_slot is None
|
||||
|
||||
|
||||
def test_set_update_scan_to_device(ring_widget_with_device):
|
||||
ring_widget = ring_widget_with_device
|
||||
# Mock the dispatcher
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
||||
|
||||
# Set to scan mode first
|
||||
ring_widget.set_update("scan")
|
||||
assert ring_widget.config.mode == "scan"
|
||||
|
||||
# Switch to device mode
|
||||
ring_widget.set_update("device", device="samx")
|
||||
|
||||
assert ring_widget.config.mode == "device"
|
||||
assert ring_widget.config.device == "samx"
|
||||
|
||||
|
||||
def test_set_update_device_to_scan(ring_widget_with_device):
|
||||
ring_widget = ring_widget_with_device
|
||||
# Mock the dispatcher
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
||||
|
||||
# Set to device mode first
|
||||
ring_widget.set_update("device", device="samx")
|
||||
assert ring_widget.config.mode == "device"
|
||||
|
||||
# Switch to scan mode
|
||||
ring_widget.set_update("scan")
|
||||
|
||||
assert ring_widget.config.mode == "scan"
|
||||
|
||||
|
||||
def test_set_update_same_device_resubscribes(ring_widget_with_device):
|
||||
ring_widget = ring_widget_with_device
|
||||
# Mock the dispatcher
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
ring_widget.bec_dispatcher.disconnect_slot = MagicMock()
|
||||
|
||||
# Set to device mode
|
||||
test_device = "samx"
|
||||
ring_widget.set_update("device", device=test_device)
|
||||
|
||||
# Reset mocks
|
||||
ring_widget.bec_dispatcher.connect_slot.reset_mock()
|
||||
ring_widget.bec_dispatcher.disconnect_slot.reset_mock()
|
||||
|
||||
# Set to same device mode again (should resubscribe)
|
||||
ring_widget.set_update("device", device=test_device)
|
||||
|
||||
# Should disconnect and reconnect
|
||||
ring_widget.bec_dispatcher.disconnect_slot.assert_called_once()
|
||||
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
||||
|
||||
|
||||
def test_set_update_invalid_mode(ring_widget):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
ring_widget.set_update("invalid_mode")
|
||||
|
||||
assert "Unsupported mode: invalid_mode" in str(excinfo.value)
|
||||
|
||||
|
||||
###################################
|
||||
# Value and property tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_set_value(ring_widget):
|
||||
ring_widget.set_value(42.5)
|
||||
assert ring_widget.config.value == 42.5
|
||||
|
||||
|
||||
def test_set_value_with_min_max_clamping(ring_widget):
|
||||
ring_widget.set_min_max_values(0, 100)
|
||||
|
||||
# Set value above max
|
||||
ring_widget.set_value(150)
|
||||
assert ring_widget.config.value == 100
|
||||
|
||||
# Set value below min
|
||||
ring_widget.set_value(-10)
|
||||
assert ring_widget.config.value == 0
|
||||
|
||||
|
||||
def test_set_precision(ring_widget):
|
||||
ring_widget.set_precision(2)
|
||||
assert ring_widget.config.precision == 2
|
||||
|
||||
ring_widget.set_value(10.12345)
|
||||
assert ring_widget.config.value == 10.12
|
||||
|
||||
|
||||
def test_set_min_max_values(ring_widget):
|
||||
ring_widget.set_min_max_values(10, 90)
|
||||
|
||||
assert ring_widget.config.min_value == 10
|
||||
assert ring_widget.config.max_value == 90
|
||||
|
||||
|
||||
def test_set_line_width(ring_widget):
|
||||
ring_widget.set_line_width(25)
|
||||
assert ring_widget.config.line_width == 25
|
||||
|
||||
|
||||
def test_set_start_angle(ring_widget):
|
||||
ring_widget.set_start_angle(180)
|
||||
assert ring_widget.config.start_position == 180
|
||||
|
||||
|
||||
###################################
|
||||
# Color management tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_set_color(ring_widget):
|
||||
test_color = (255, 128, 64, 255)
|
||||
ring_widget.set_color(test_color)
|
||||
|
||||
# Color is stored as hex string internally
|
||||
assert ring_widget.color.getRgb() == test_color
|
||||
|
||||
|
||||
def test_set_color_with_link_colors_updates_background(ring_widget):
|
||||
# Enable color linking
|
||||
ring_widget.config.link_colors = True
|
||||
|
||||
# Store original background
|
||||
original_bg = ring_widget.background_color.getRgb()
|
||||
|
||||
test_color = (255, 100, 50, 255)
|
||||
ring_widget.set_color(test_color)
|
||||
|
||||
# Background should be derived using subtle_background_color
|
||||
bg_color = ring_widget.background_color
|
||||
# Background should have changed
|
||||
assert bg_color.getRgb() != original_bg
|
||||
# Background should be different from the main color
|
||||
assert bg_color.getRgb() != test_color
|
||||
|
||||
|
||||
def test_set_background_when_colors_unlinked(ring_widget):
|
||||
# Disable color linking
|
||||
ring_widget.config.link_colors = False
|
||||
|
||||
test_bg = (100, 100, 100, 128)
|
||||
ring_widget.set_background(test_bg)
|
||||
|
||||
assert ring_widget.background_color.getRgb() == test_bg
|
||||
|
||||
|
||||
def test_set_background_when_colors_linked_does_nothing(ring_widget):
|
||||
# Enable color linking
|
||||
ring_widget.config.link_colors = True
|
||||
|
||||
original_bg = ring_widget.background_color.getRgb()
|
||||
test_bg = (100, 100, 100, 128)
|
||||
|
||||
ring_widget.set_background(test_bg)
|
||||
|
||||
# Background should not change when colors are linked
|
||||
assert ring_widget.background_color.getRgb() == original_bg
|
||||
|
||||
|
||||
def test_color_link_derives_background(ring_widget):
|
||||
ring_widget.config.link_colors = True
|
||||
|
||||
bright_color = QColor(255, 255, 0, 255) # Bright yellow
|
||||
original_bg = ring_widget.background_color.getRgb()
|
||||
|
||||
ring_widget.set_color(bright_color.getRgb())
|
||||
|
||||
# Get the derived background color
|
||||
bg_color = ring_widget.background_color
|
||||
|
||||
# Background should have changed
|
||||
assert bg_color.getRgb() != original_bg
|
||||
# Background should be a subtle blend, not the same as the main color
|
||||
assert bg_color.getRgb() != bright_color.getRgb()
|
||||
|
||||
|
||||
def test_convert_color_from_tuple(ring_widget):
|
||||
color_tuple = (200, 150, 100, 255)
|
||||
qcolor = ring_widget.convert_color(color_tuple)
|
||||
|
||||
assert isinstance(qcolor, QColor)
|
||||
assert qcolor.getRgb() == color_tuple
|
||||
|
||||
|
||||
def test_convert_color_from_hex_string(ring_widget):
|
||||
hex_color = "#FF8040FF"
|
||||
qcolor = ring_widget.convert_color(hex_color)
|
||||
|
||||
assert isinstance(qcolor, QColor)
|
||||
assert qcolor.isValid()
|
||||
|
||||
|
||||
###################################
|
||||
# Gap property tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_gap_property(ring_widget):
|
||||
ring_widget.gap = 15
|
||||
assert ring_widget.gap == 15
|
||||
|
||||
|
||||
###################################
|
||||
# Config validation tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_config_default_values():
|
||||
config = ProgressbarConfig()
|
||||
|
||||
assert config.value == 0
|
||||
assert config.direction == -1
|
||||
assert config.line_width == 20
|
||||
assert config.start_position == 90
|
||||
assert config.min_value == 0
|
||||
assert config.max_value == 100
|
||||
assert config.precision == 3
|
||||
assert config.mode == "manual"
|
||||
assert config.link_colors is True
|
||||
|
||||
|
||||
def test_config_with_custom_values():
|
||||
config = ProgressbarConfig(
|
||||
value=50, direction=1, line_width=20, min_value=10, max_value=90, precision=2, mode="scan"
|
||||
)
|
||||
|
||||
assert config.value == 50
|
||||
assert config.direction == 1
|
||||
assert config.line_width == 20
|
||||
assert config.min_value == 10
|
||||
assert config.max_value == 90
|
||||
assert config.precision == 2
|
||||
assert config.mode == "scan"
|
||||
|
||||
|
||||
###################################
|
||||
# set_direction tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_set_direction_clockwise(ring_widget):
|
||||
ring_widget.set_direction(-1)
|
||||
assert ring_widget.config.direction == -1
|
||||
|
||||
|
||||
def test_set_direction_counter_clockwise(ring_widget):
|
||||
ring_widget.set_direction(1)
|
||||
assert ring_widget.config.direction == 1
|
||||
|
||||
|
||||
###################################
|
||||
# _update_device_connection tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_update_device_connection_with_progress_signal(ring_widget_with_device):
|
||||
ring_widget = ring_widget_with_device
|
||||
samx = ring_widget.bec_dispatcher.client.device_manager.devices.samx
|
||||
samx._info["signals"]["progress"] = {
|
||||
"obj_name": "samx_progress",
|
||||
"component_name": "progress",
|
||||
"signal_class": "ProgressSignal",
|
||||
"kind_str": "hinted",
|
||||
}
|
||||
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
|
||||
ring_widget._update_device_connection("samx", "progress")
|
||||
|
||||
# Should connect to device_progress endpoint
|
||||
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
||||
call_args = ring_widget.bec_dispatcher.connect_slot.call_args
|
||||
assert call_args[0][0] == ring_widget.on_device_progress
|
||||
|
||||
|
||||
def test_update_device_connection_with_hinted_signal(ring_widget):
|
||||
mock_device = FakeDevice(name="samx")
|
||||
mock_device._info = {
|
||||
"signals": {
|
||||
"samx": {"obj_name": "samx", "signal_class": "SomeOtherSignal", "kind_str": "hinted"}
|
||||
}
|
||||
}
|
||||
|
||||
ring_widget.bec_dispatcher.client.device_manager.devices["samx"] = mock_device
|
||||
|
||||
ring_widget.bec_dispatcher.connect_slot = MagicMock()
|
||||
|
||||
ring_widget._update_device_connection("samx", "samx")
|
||||
|
||||
# Should connect to device_readback endpoint
|
||||
ring_widget.bec_dispatcher.connect_slot.assert_called_once()
|
||||
call_args = ring_widget.bec_dispatcher.connect_slot.call_args
|
||||
assert call_args[0][0] == ring_widget.on_device_readback
|
||||
|
||||
|
||||
def test_update_device_connection_no_device_manager(ring_widget):
|
||||
ring_widget.bec_dispatcher.client.device_manager = None
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
ring_widget._update_device_connection("samx", "signal")
|
||||
assert "Device manager is not available" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_update_device_connection_device_not_found(ring_widget):
|
||||
mock_device = FakeDevice(name="samx")
|
||||
ring_widget.bec_dispatcher.client.device_manager.devices["samx"] = mock_device
|
||||
|
||||
# Should return without raising an error
|
||||
ring_widget._update_device_connection("nonexistent", "signal")
|
||||
|
||||
|
||||
###################################
|
||||
# on_scan_progress tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_on_scan_progress_updates_value(ring_widget):
|
||||
msg = {"value": 42, "max_value": 100}
|
||||
meta = {"RID": "test_rid_123"}
|
||||
|
||||
ring_widget.on_scan_progress(msg, meta)
|
||||
|
||||
assert ring_widget.config.value == 42
|
||||
|
||||
|
||||
def test_on_scan_progress_updates_min_max_on_new_rid(ring_widget):
|
||||
msg = {"value": 50, "max_value": 200}
|
||||
meta = {"RID": "new_rid"}
|
||||
|
||||
ring_widget.RID = "old_rid"
|
||||
ring_widget.on_scan_progress(msg, meta)
|
||||
|
||||
assert ring_widget.config.min_value == 0
|
||||
assert ring_widget.config.max_value == 200
|
||||
assert ring_widget.config.value == 50
|
||||
|
||||
|
||||
def test_on_scan_progress_same_rid_no_min_max_update(ring_widget):
|
||||
msg = {"value": 75, "max_value": 300}
|
||||
meta = {"RID": "same_rid"}
|
||||
|
||||
ring_widget.RID = "same_rid"
|
||||
ring_widget.set_min_max_values(0, 100)
|
||||
|
||||
ring_widget.on_scan_progress(msg, meta)
|
||||
|
||||
# Max value should not be updated when RID is the same
|
||||
assert ring_widget.config.max_value == 100
|
||||
assert ring_widget.config.value == 75
|
||||
|
||||
|
||||
###################################
|
||||
# on_device_readback tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_on_device_readback_updates_value(ring_widget):
|
||||
ring_widget.config.device = "samx"
|
||||
ring_widget.config.signal = "readback"
|
||||
|
||||
msg = {"signals": {"readback": {"value": 12.34}}}
|
||||
meta = {}
|
||||
|
||||
ring_widget.on_device_readback(msg, meta)
|
||||
|
||||
assert ring_widget.config.value == 12.34
|
||||
|
||||
|
||||
def test_on_device_readback_uses_device_name_when_no_signal(ring_widget):
|
||||
ring_widget.config.device = "samy"
|
||||
ring_widget.config.signal = None
|
||||
|
||||
msg = {"signals": {"samy": {"value": 56.78}}}
|
||||
meta = {}
|
||||
|
||||
ring_widget.on_device_readback(msg, meta)
|
||||
|
||||
assert ring_widget.config.value == 56.78
|
||||
|
||||
|
||||
def test_on_device_readback_no_device_returns_early(ring_widget):
|
||||
ring_widget.config.device = None
|
||||
|
||||
msg = {"signals": {"samx": {"value": 99.99}}}
|
||||
meta = {}
|
||||
|
||||
initial_value = ring_widget.config.value
|
||||
ring_widget.on_device_readback(msg, meta)
|
||||
|
||||
# Value should not change
|
||||
assert ring_widget.config.value == initial_value
|
||||
|
||||
|
||||
def test_on_device_readback_missing_signal_data(ring_widget):
|
||||
ring_widget.config.device = "samx"
|
||||
ring_widget.config.signal = "missing_signal"
|
||||
|
||||
msg = {"signals": {"other_signal": {"value": 11.11}}}
|
||||
meta = {}
|
||||
|
||||
initial_value = ring_widget.config.value
|
||||
ring_widget.on_device_readback(msg, meta)
|
||||
|
||||
# Value should not change when signal is missing
|
||||
assert ring_widget.config.value == initial_value
|
||||
|
||||
|
||||
###################################
|
||||
# on_device_progress tests
|
||||
###################################
|
||||
|
||||
|
||||
def test_on_device_progress_updates_value_and_max(ring_widget):
|
||||
ring_widget.config.device = "samx"
|
||||
|
||||
msg = {"value": 30, "max_value": 150, "done": False}
|
||||
meta = {}
|
||||
|
||||
ring_widget.on_device_progress(msg, meta)
|
||||
|
||||
assert ring_widget.config.value == 30
|
||||
assert ring_widget.config.max_value == 150
|
||||
|
||||
|
||||
def test_on_device_progress_done_sets_to_max(ring_widget):
|
||||
ring_widget.config.device = "samx"
|
||||
|
||||
msg = {"value": 80, "max_value": 100, "done": True}
|
||||
meta = {}
|
||||
|
||||
ring_widget.on_device_progress(msg, meta)
|
||||
|
||||
# When done is True, value should be set to max_value
|
||||
assert ring_widget.config.value == 100
|
||||
assert ring_widget.config.max_value == 100
|
||||
|
||||
|
||||
def test_on_device_progress_no_device_returns_early(ring_widget):
|
||||
ring_widget.config.device = None
|
||||
|
||||
msg = {"value": 50, "max_value": 100, "done": False}
|
||||
meta = {}
|
||||
|
||||
initial_value = ring_widget.config.value
|
||||
initial_max = ring_widget.config.max_value
|
||||
|
||||
ring_widget.on_device_progress(msg, meta)
|
||||
|
||||
# Nothing should change
|
||||
assert ring_widget.config.value == initial_value
|
||||
assert ring_widget.config.max_value == initial_max
|
||||
|
||||
|
||||
def test_on_device_progress_default_values(ring_widget):
|
||||
ring_widget.config.device = "samx"
|
||||
|
||||
# Message without value and max_value
|
||||
msg = {}
|
||||
meta = {}
|
||||
|
||||
ring_widget.on_device_progress(msg, meta)
|
||||
|
||||
# Should use defaults: value=0, max_value=100
|
||||
assert ring_widget.config.value == 0
|
||||
assert ring_widget.config.max_value == 100
|
||||
66
tests/unit_tests/test_ring_progress_settings.py
Normal file
66
tests/unit_tests/test_ring_progress_settings.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import pytest
|
||||
|
||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
|
||||
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_settings_cards import RingSettings
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ring_progress_bar_widget(qtbot, mocked_client):
|
||||
widget = RingProgressBar(client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpb_settings_dialog(qtbot, ring_progress_bar_widget):
|
||||
settings = RingSettings(
|
||||
parent=ring_progress_bar_widget, target_widget=ring_progress_bar_widget, popup=True
|
||||
)
|
||||
dialog = SettingsDialog(
|
||||
ring_progress_bar_widget,
|
||||
settings_widget=settings,
|
||||
window_title="Ring Progress Bar Settings",
|
||||
modal=False,
|
||||
)
|
||||
qtbot.addWidget(dialog)
|
||||
qtbot.waitExposed(dialog)
|
||||
yield dialog
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpb_settings_dialog_with_rings(qtbot, ring_progress_bar_widget):
|
||||
ring_progress_bar_widget.add_ring()
|
||||
ring_progress_bar_widget.add_ring()
|
||||
settings = RingSettings(
|
||||
parent=ring_progress_bar_widget, target_widget=ring_progress_bar_widget, popup=True
|
||||
)
|
||||
dialog = SettingsDialog(
|
||||
ring_progress_bar_widget,
|
||||
settings_widget=settings,
|
||||
window_title="Ring Progress Bar Settings",
|
||||
modal=False,
|
||||
)
|
||||
qtbot.addWidget(dialog)
|
||||
qtbot.waitExposed(dialog)
|
||||
yield dialog
|
||||
|
||||
|
||||
def test_ring_progress_settings_dialog_opens(rpb_settings_dialog):
|
||||
"""Test that the Ring Progress Bar settings dialog opens correctly."""
|
||||
dialog = rpb_settings_dialog
|
||||
dialog.show()
|
||||
assert dialog.isVisible()
|
||||
assert dialog.windowTitle() == "Ring Progress Bar Settings"
|
||||
dialog.accept()
|
||||
|
||||
|
||||
def test_ring_progress_settings_dialog_with_rings(rpb_settings_dialog_with_rings):
|
||||
"""Test that the Ring Progress Bar settings dialog opens correctly with rings."""
|
||||
dialog = rpb_settings_dialog_with_rings
|
||||
dialog.show()
|
||||
assert dialog.isVisible()
|
||||
assert dialog.windowTitle() == "Ring Progress Bar Settings"
|
||||
dialog.accept() # Close the dialog
|
||||
Reference in New Issue
Block a user