mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat(multi_waveform): multi-waveform widget based on new PlotBase
This commit is contained in:
@ -32,6 +32,7 @@ class Widgets(str, enum.Enum):
|
|||||||
LogPanel = "LogPanel"
|
LogPanel = "LogPanel"
|
||||||
Minesweeper = "Minesweeper"
|
Minesweeper = "Minesweeper"
|
||||||
MotorMap = "MotorMap"
|
MotorMap = "MotorMap"
|
||||||
|
MultiWaveform = "MultiWaveform"
|
||||||
PositionIndicator = "PositionIndicator"
|
PositionIndicator = "PositionIndicator"
|
||||||
PositionerBox = "PositionerBox"
|
PositionerBox = "PositionerBox"
|
||||||
PositionerBox2D = "PositionerBox2D"
|
PositionerBox2D = "PositionerBox2D"
|
||||||
@ -3662,6 +3663,415 @@ class MotorMap(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MultiWaveform(RPCBase):
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def enable_toolbar(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show Toolbar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@enable_toolbar.setter
|
||||||
|
@rpc_call
|
||||||
|
def enable_toolbar(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show Toolbar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def enable_side_panel(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show Side Panel
|
||||||
|
"""
|
||||||
|
|
||||||
|
@enable_side_panel.setter
|
||||||
|
@rpc_call
|
||||||
|
def enable_side_panel(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show Side Panel
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def enable_fps_monitor(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Enable the FPS monitor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@enable_fps_monitor.setter
|
||||||
|
@rpc_call
|
||||||
|
def enable_fps_monitor(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Enable the FPS monitor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Set the properties of the plot widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Keyword arguments for the properties to be set.
|
||||||
|
|
||||||
|
Possible properties:
|
||||||
|
- title: str
|
||||||
|
- x_label: str
|
||||||
|
- y_label: str
|
||||||
|
- x_scale: Literal["linear", "log"]
|
||||||
|
- y_scale: Literal["linear", "log"]
|
||||||
|
- x_lim: tuple
|
||||||
|
- y_lim: tuple
|
||||||
|
- legend_label_size: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def title(self) -> "str":
|
||||||
|
"""
|
||||||
|
Set title of the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@title.setter
|
||||||
|
@rpc_call
|
||||||
|
def title(self) -> "str":
|
||||||
|
"""
|
||||||
|
Set title of the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def x_label(self) -> "str":
|
||||||
|
"""
|
||||||
|
The set label for the x-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@x_label.setter
|
||||||
|
@rpc_call
|
||||||
|
def x_label(self) -> "str":
|
||||||
|
"""
|
||||||
|
The set label for the x-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def y_label(self) -> "str":
|
||||||
|
"""
|
||||||
|
The set label for the y-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@y_label.setter
|
||||||
|
@rpc_call
|
||||||
|
def y_label(self) -> "str":
|
||||||
|
"""
|
||||||
|
The set label for the y-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def x_limits(self) -> "QPointF":
|
||||||
|
"""
|
||||||
|
Get the x limits of the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@x_limits.setter
|
||||||
|
@rpc_call
|
||||||
|
def x_limits(self) -> "QPointF":
|
||||||
|
"""
|
||||||
|
Get the x limits of the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def y_limits(self) -> "QPointF":
|
||||||
|
"""
|
||||||
|
Get the y limits of the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@y_limits.setter
|
||||||
|
@rpc_call
|
||||||
|
def y_limits(self) -> "QPointF":
|
||||||
|
"""
|
||||||
|
Get the y limits of the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def x_grid(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show grid on the x-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@x_grid.setter
|
||||||
|
@rpc_call
|
||||||
|
def x_grid(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show grid on the x-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def y_grid(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show grid on the y-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@y_grid.setter
|
||||||
|
@rpc_call
|
||||||
|
def y_grid(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show grid on the y-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def inner_axes(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show inner axes of the plot widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@inner_axes.setter
|
||||||
|
@rpc_call
|
||||||
|
def inner_axes(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show inner axes of the plot widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def outer_axes(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show the outer axes of the plot widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@outer_axes.setter
|
||||||
|
@rpc_call
|
||||||
|
def outer_axes(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Show the outer axes of the plot widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def lock_aspect_ratio(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Lock aspect ratio of the plot widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@lock_aspect_ratio.setter
|
||||||
|
@rpc_call
|
||||||
|
def lock_aspect_ratio(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Lock aspect ratio of the plot widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def auto_range_x(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set auto range for the x-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@auto_range_x.setter
|
||||||
|
@rpc_call
|
||||||
|
def auto_range_x(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set auto range for the x-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def auto_range_y(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set auto range for the y-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@auto_range_y.setter
|
||||||
|
@rpc_call
|
||||||
|
def auto_range_y(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set auto range for the y-axis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def x_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set X-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@x_log.setter
|
||||||
|
@rpc_call
|
||||||
|
def x_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set X-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def y_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set Y-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@y_log.setter
|
||||||
|
@rpc_call
|
||||||
|
def y_log(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Set Y-axis to log scale if True, linear if False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def legend_label_size(self) -> "int":
|
||||||
|
"""
|
||||||
|
The font size of the legend font.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@legend_label_size.setter
|
||||||
|
@rpc_call
|
||||||
|
def legend_label_size(self) -> "int":
|
||||||
|
"""
|
||||||
|
The font size of the legend font.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def highlighted_index(self):
|
||||||
|
"""
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@highlighted_index.setter
|
||||||
|
@rpc_call
|
||||||
|
def highlighted_index(self):
|
||||||
|
"""
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def highlight_last_curve(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Get the highlight_last_curve property.
|
||||||
|
Returns:
|
||||||
|
bool: The highlight_last_curve property.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@highlight_last_curve.setter
|
||||||
|
@rpc_call
|
||||||
|
def highlight_last_curve(self) -> "bool":
|
||||||
|
"""
|
||||||
|
Get the highlight_last_curve property.
|
||||||
|
Returns:
|
||||||
|
bool: The highlight_last_curve property.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def color_palette(self) -> "str":
|
||||||
|
"""
|
||||||
|
The color palette of the figure widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@color_palette.setter
|
||||||
|
@rpc_call
|
||||||
|
def color_palette(self) -> "str":
|
||||||
|
"""
|
||||||
|
The color palette of the figure widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def opacity(self) -> "int":
|
||||||
|
"""
|
||||||
|
The opacity of the figure widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@opacity.setter
|
||||||
|
@rpc_call
|
||||||
|
def opacity(self) -> "int":
|
||||||
|
"""
|
||||||
|
The opacity of the figure widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def flush_buffer(self) -> "bool":
|
||||||
|
"""
|
||||||
|
The flush_buffer property.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@flush_buffer.setter
|
||||||
|
@rpc_call
|
||||||
|
def flush_buffer(self) -> "bool":
|
||||||
|
"""
|
||||||
|
The flush_buffer property.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def max_trace(self) -> "int":
|
||||||
|
"""
|
||||||
|
The maximum number of traces to display on the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@max_trace.setter
|
||||||
|
@rpc_call
|
||||||
|
def max_trace(self) -> "int":
|
||||||
|
"""
|
||||||
|
The maximum number of traces to display on the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@rpc_call
|
||||||
|
def monitor(self) -> "str":
|
||||||
|
"""
|
||||||
|
The monitor of the figure widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@monitor.setter
|
||||||
|
@rpc_call
|
||||||
|
def monitor(self) -> "str":
|
||||||
|
"""
|
||||||
|
The monitor of the figure widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_curve_limit(self, max_trace: "int", flush_buffer: "bool"):
|
||||||
|
"""
|
||||||
|
Set the maximum number of traces to display on the plot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_trace (int): The maximum number of traces to display.
|
||||||
|
flush_buffer (bool): Flush the buffer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def plot(self, monitor: "str", color_palette: "str | None" = "magma"):
|
||||||
|
"""
|
||||||
|
Create a plot for the given monitor.
|
||||||
|
Args:
|
||||||
|
monitor (str): The monitor to set.
|
||||||
|
color_palette (str|None): The color palette to use for the plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def set_curve_highlight(self, index: "int"):
|
||||||
|
"""
|
||||||
|
Set the curve highlight based on visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index (int): The index of the curve to highlight among visible curves.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def clear_curves(self):
|
||||||
|
"""
|
||||||
|
Remove all curves from the plot, excluding crosshair items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PositionIndicator(RPCBase):
|
class PositionIndicator(RPCBase):
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def set_value(self, position: float):
|
def set_value(self, position: float):
|
||||||
|
@ -22,6 +22,7 @@ from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutM
|
|||||||
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
||||||
from bec_widgets.widgets.plots_next_gen.image.image import Image
|
from bec_widgets.widgets.plots_next_gen.image.image import Image
|
||||||
from bec_widgets.widgets.plots_next_gen.motor_map.motor_map import MotorMap
|
from bec_widgets.widgets.plots_next_gen.motor_map.motor_map import MotorMap
|
||||||
|
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform import MultiWaveform
|
||||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||||
from bec_widgets.widgets.plots_next_gen.scatter_waveform.scatter_waveform import ScatterWaveform
|
from bec_widgets.widgets.plots_next_gen.scatter_waveform.scatter_waveform import ScatterWaveform
|
||||||
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
from bec_widgets.widgets.plots_next_gen.waveform.waveform import Waveform
|
||||||
@ -69,6 +70,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|||||||
"wf": self.wf,
|
"wf": self.wf,
|
||||||
"scatter": self.scatter,
|
"scatter": self.scatter,
|
||||||
"scatter_mi": self.scatter,
|
"scatter_mi": self.scatter,
|
||||||
|
"mwf": self.mwf,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -152,6 +154,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|||||||
tab_widget.addTab(eighth_tab, "Motor Map")
|
tab_widget.addTab(eighth_tab, "Motor Map")
|
||||||
tab_widget.setCurrentIndex(7)
|
tab_widget.setCurrentIndex(7)
|
||||||
|
|
||||||
|
ninth_tab = QWidget()
|
||||||
|
ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||||
|
self.mwf = MultiWaveform()
|
||||||
|
ninth_tab_layout.addWidget(self.mwf)
|
||||||
|
tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||||
|
tab_widget.setCurrentIndex(8)
|
||||||
|
|
||||||
# add stuff to the new Waveform widget
|
# add stuff to the new Waveform widget
|
||||||
self._init_waveform()
|
self._init_waveform()
|
||||||
|
|
||||||
|
@ -0,0 +1,501 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from pydantic import Field, ValidationError, field_validator
|
||||||
|
from qtpy.QtCore import Signal
|
||||||
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
|
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
from bec_widgets.qt_utils.side_panel import SidePanel
|
||||||
|
from bec_widgets.utils import Colors, ConnectionConfig
|
||||||
|
from bec_widgets.widgets.plots_next_gen.multi_waveform.settings.control_panel import (
|
||||||
|
MultiWaveformControlPanel,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.plots_next_gen.multi_waveform.toolbar_bundles.monitor_selection import (
|
||||||
|
MultiWaveformSelectionToolbarBundle,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class MultiWaveformConfig(ConnectionConfig):
|
||||||
|
color_palette: str | None = Field(
|
||||||
|
"magma", description="The color palette of the figure widget.", validate_default=True
|
||||||
|
)
|
||||||
|
curve_limit: int | None = Field(
|
||||||
|
200, description="The maximum number of curves to display on the plot."
|
||||||
|
)
|
||||||
|
flush_buffer: bool | None = Field(
|
||||||
|
False, description="Flush the buffer of the plot widget when the curve limit is reached."
|
||||||
|
)
|
||||||
|
monitor: str | None = Field(None, description="The monitor to set for the plot widget.")
|
||||||
|
curve_width: int | None = Field(1, description="The width of the curve on the plot.")
|
||||||
|
opacity: int | None = Field(50, description="The opacity of the curve on the plot.")
|
||||||
|
highlight_last_curve: bool | None = Field(
|
||||||
|
True, description="Highlight the last curve on the plot."
|
||||||
|
)
|
||||||
|
|
||||||
|
model_config: dict = {"validate_assignment": True}
|
||||||
|
_validate_color_map_z = field_validator("color_palette")(Colors.validate_color_map)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiWaveform(PlotBase):
|
||||||
|
PLUGIN = True
|
||||||
|
RPC = True
|
||||||
|
ICON_NAME = "ssid_chart"
|
||||||
|
USER_ACCESS = [
|
||||||
|
# General PlotBase Settings
|
||||||
|
"enable_toolbar",
|
||||||
|
"enable_toolbar.setter",
|
||||||
|
"enable_side_panel",
|
||||||
|
"enable_side_panel.setter",
|
||||||
|
"enable_fps_monitor",
|
||||||
|
"enable_fps_monitor.setter",
|
||||||
|
"set",
|
||||||
|
"title",
|
||||||
|
"title.setter",
|
||||||
|
"x_label",
|
||||||
|
"x_label.setter",
|
||||||
|
"y_label",
|
||||||
|
"y_label.setter",
|
||||||
|
"x_limits",
|
||||||
|
"x_limits.setter",
|
||||||
|
"y_limits",
|
||||||
|
"y_limits.setter",
|
||||||
|
"x_grid",
|
||||||
|
"x_grid.setter",
|
||||||
|
"y_grid",
|
||||||
|
"y_grid.setter",
|
||||||
|
"inner_axes",
|
||||||
|
"inner_axes.setter",
|
||||||
|
"outer_axes",
|
||||||
|
"outer_axes.setter",
|
||||||
|
"lock_aspect_ratio",
|
||||||
|
"lock_aspect_ratio.setter",
|
||||||
|
"auto_range_x",
|
||||||
|
"auto_range_x.setter",
|
||||||
|
"auto_range_y",
|
||||||
|
"auto_range_y.setter",
|
||||||
|
"x_log",
|
||||||
|
"x_log.setter",
|
||||||
|
"y_log",
|
||||||
|
"y_log.setter",
|
||||||
|
"legend_label_size",
|
||||||
|
"legend_label_size.setter",
|
||||||
|
# MultiWaveform Specific RPC Access
|
||||||
|
"highlighted_index",
|
||||||
|
"highlighted_index.setter",
|
||||||
|
"highlight_last_curve",
|
||||||
|
"highlight_last_curve.setter",
|
||||||
|
"color_palette",
|
||||||
|
"color_palette.setter",
|
||||||
|
"opacity",
|
||||||
|
"opacity.setter",
|
||||||
|
"flush_buffer",
|
||||||
|
"flush_buffer.setter",
|
||||||
|
"max_trace",
|
||||||
|
"max_trace.setter",
|
||||||
|
"monitor",
|
||||||
|
"monitor.setter",
|
||||||
|
"set_curve_limit",
|
||||||
|
"plot",
|
||||||
|
"set_curve_highlight",
|
||||||
|
"clear_curves",
|
||||||
|
]
|
||||||
|
|
||||||
|
monitor_signal_updated = Signal()
|
||||||
|
highlighted_curve_index_changed = Signal(int)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
config: MultiWaveformConfig | None = None,
|
||||||
|
client=None,
|
||||||
|
gui_id: str | None = None,
|
||||||
|
popups: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
if config is None:
|
||||||
|
config = MultiWaveformConfig(widget_class=self.__class__.__name__)
|
||||||
|
super().__init__(
|
||||||
|
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# For PropertyManager identification
|
||||||
|
self.setObjectName("MultiWaveform")
|
||||||
|
|
||||||
|
# Scan Data
|
||||||
|
self.old_scan_id = None
|
||||||
|
self.scan_id = None
|
||||||
|
self.connected = False
|
||||||
|
self._current_highlight_index = 0
|
||||||
|
self._curves = deque()
|
||||||
|
self.visible_curves = []
|
||||||
|
self.number_of_visible_curves = 0
|
||||||
|
|
||||||
|
self._init_control_panel()
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Widget Specific GUI interactions
|
||||||
|
################################################################################
|
||||||
|
def _init_toolbar(self):
|
||||||
|
self.monitor_selection_bundle = MultiWaveformSelectionToolbarBundle(
|
||||||
|
bundle_id="motor_selection", target_widget=self
|
||||||
|
)
|
||||||
|
self.toolbar.add_bundle(self.monitor_selection_bundle, target_widget=self)
|
||||||
|
super()._init_toolbar()
|
||||||
|
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
||||||
|
|
||||||
|
def _init_control_panel(self):
|
||||||
|
self.control_panel = SidePanel(self, orientation="top", panel_max_width=90)
|
||||||
|
self.layout_manager.add_widget_relative(
|
||||||
|
self.control_panel, self.round_plot_widget, "bottom"
|
||||||
|
)
|
||||||
|
self.controls = MultiWaveformControlPanel(target_widget=self)
|
||||||
|
self.control_panel.add_menu(
|
||||||
|
action_id="control",
|
||||||
|
icon_name="tune",
|
||||||
|
tooltip="Show Control panel",
|
||||||
|
widget=self.controls,
|
||||||
|
title=None,
|
||||||
|
)
|
||||||
|
self.control_panel.toolbar.widgets["control"].action.trigger()
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Widget Specific Properties
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
@property
|
||||||
|
def curves(self) -> deque:
|
||||||
|
"""
|
||||||
|
Get the curves of the plot widget as a deque.
|
||||||
|
Returns:
|
||||||
|
deque: Deque of curves.
|
||||||
|
"""
|
||||||
|
return self._curves
|
||||||
|
|
||||||
|
@curves.setter
|
||||||
|
def curves(self, value: deque):
|
||||||
|
self._curves = value
|
||||||
|
|
||||||
|
@SafeProperty(int, designable=False)
|
||||||
|
def highlighted_index(self):
|
||||||
|
return self._current_highlight_index
|
||||||
|
|
||||||
|
@highlighted_index.setter
|
||||||
|
def highlighted_index(self, value: int):
|
||||||
|
self._current_highlight_index = value
|
||||||
|
self.property_changed.emit("highlighted_index", value)
|
||||||
|
self.set_curve_highlight(value)
|
||||||
|
|
||||||
|
@SafeProperty(bool)
|
||||||
|
def highlight_last_curve(self) -> bool:
|
||||||
|
"""
|
||||||
|
Get the highlight_last_curve property.
|
||||||
|
Returns:
|
||||||
|
bool: The highlight_last_curve property.
|
||||||
|
"""
|
||||||
|
return self.config.highlight_last_curve
|
||||||
|
|
||||||
|
@highlight_last_curve.setter
|
||||||
|
def highlight_last_curve(self, value: bool):
|
||||||
|
self.config.highlight_last_curve = value
|
||||||
|
self.property_changed.emit("highlight_last_curve", value)
|
||||||
|
self.set_curve_highlight(-1)
|
||||||
|
|
||||||
|
@SafeProperty(str)
|
||||||
|
def color_palette(self) -> str:
|
||||||
|
"""
|
||||||
|
The color palette of the figure widget.
|
||||||
|
"""
|
||||||
|
return self.config.color_palette
|
||||||
|
|
||||||
|
@color_palette.setter
|
||||||
|
def color_palette(self, value: str):
|
||||||
|
"""
|
||||||
|
Set the color palette of the figure widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(str): The color palette to set.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.config.color_palette = value
|
||||||
|
except ValidationError:
|
||||||
|
return
|
||||||
|
self.set_curve_highlight(self._current_highlight_index)
|
||||||
|
self._sync_monitor_selection_toolbar()
|
||||||
|
|
||||||
|
@SafeProperty(int)
|
||||||
|
def opacity(self) -> int:
|
||||||
|
"""
|
||||||
|
The opacity of the figure widget.
|
||||||
|
"""
|
||||||
|
return self.config.opacity
|
||||||
|
|
||||||
|
@opacity.setter
|
||||||
|
def opacity(self, value: int):
|
||||||
|
"""
|
||||||
|
Set the opacity of the figure widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(int): The opacity to set.
|
||||||
|
"""
|
||||||
|
self.config.opacity = max(0, min(100, value))
|
||||||
|
self.property_changed.emit("opacity", value)
|
||||||
|
self.set_curve_highlight(self._current_highlight_index)
|
||||||
|
|
||||||
|
@SafeProperty(bool)
|
||||||
|
def flush_buffer(self) -> bool:
|
||||||
|
"""
|
||||||
|
The flush_buffer property.
|
||||||
|
"""
|
||||||
|
return self.config.flush_buffer
|
||||||
|
|
||||||
|
@flush_buffer.setter
|
||||||
|
def flush_buffer(self, value: bool):
|
||||||
|
self.config.flush_buffer = value
|
||||||
|
self.property_changed.emit("flush_buffer", value)
|
||||||
|
self.set_curve_limit(
|
||||||
|
max_trace=self.config.curve_limit, flush_buffer=self.config.flush_buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
@SafeProperty(int)
|
||||||
|
def max_trace(self) -> int:
|
||||||
|
"""
|
||||||
|
The maximum number of traces to display on the plot.
|
||||||
|
"""
|
||||||
|
return self.config.curve_limit
|
||||||
|
|
||||||
|
@max_trace.setter
|
||||||
|
def max_trace(self, value: int):
|
||||||
|
"""
|
||||||
|
Set the maximum number of traces to display on the plot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(int): The maximum number of traces to display.
|
||||||
|
"""
|
||||||
|
self.config.curve_limit = value
|
||||||
|
self.property_changed.emit("max_trace", value)
|
||||||
|
self.set_curve_limit(
|
||||||
|
max_trace=self.config.curve_limit, flush_buffer=self.config.flush_buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
@SafeProperty(str)
|
||||||
|
def monitor(self) -> str:
|
||||||
|
"""
|
||||||
|
The monitor of the figure widget.
|
||||||
|
"""
|
||||||
|
return self.config.monitor
|
||||||
|
|
||||||
|
@monitor.setter
|
||||||
|
def monitor(self, value: str):
|
||||||
|
"""
|
||||||
|
Set the monitor of the figure widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value(str): The monitor to set.
|
||||||
|
"""
|
||||||
|
self.plot(value)
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# High Level methods for API
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
@SafeSlot(popup_error=True)
|
||||||
|
def plot(self, monitor: str, color_palette: str | None = "magma"):
|
||||||
|
"""
|
||||||
|
Create a plot for the given monitor.
|
||||||
|
Args:
|
||||||
|
monitor (str): The monitor to set.
|
||||||
|
color_palette (str|None): The color palette to use for the plot.
|
||||||
|
"""
|
||||||
|
self.entry_validator.validate_monitor(monitor)
|
||||||
|
self._disconnect_monitor()
|
||||||
|
self.config.monitor = monitor
|
||||||
|
self._connect_monitor()
|
||||||
|
if color_palette is not None:
|
||||||
|
self.color_palette = color_palette
|
||||||
|
self._sync_monitor_selection_toolbar()
|
||||||
|
|
||||||
|
@SafeSlot(int, bool)
|
||||||
|
def set_curve_limit(self, max_trace: int, flush_buffer: bool):
|
||||||
|
"""
|
||||||
|
Set the maximum number of traces to display on the plot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_trace (int): The maximum number of traces to display.
|
||||||
|
flush_buffer (bool): Flush the buffer.
|
||||||
|
"""
|
||||||
|
if max_trace != self.config.curve_limit:
|
||||||
|
self.config.curve_limit = max_trace
|
||||||
|
if flush_buffer != self.config.flush_buffer:
|
||||||
|
self.config.flush_buffer = flush_buffer
|
||||||
|
|
||||||
|
if self.config.curve_limit is None:
|
||||||
|
self.scale_colors()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.config.flush_buffer:
|
||||||
|
# Remove excess curves from the plot and the deque
|
||||||
|
while len(self.curves) > self.config.curve_limit:
|
||||||
|
curve = self.curves.popleft()
|
||||||
|
self.plot_item.removeItem(curve)
|
||||||
|
else:
|
||||||
|
# Hide or show curves based on the new max_trace
|
||||||
|
num_curves_to_show = min(self.config.curve_limit, len(self.curves))
|
||||||
|
for i, curve in enumerate(self.curves):
|
||||||
|
if i < len(self.curves) - num_curves_to_show:
|
||||||
|
curve.hide()
|
||||||
|
else:
|
||||||
|
curve.show()
|
||||||
|
self.scale_colors()
|
||||||
|
self.monitor_signal_updated.emit()
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# BEC Update Methods
|
||||||
|
################################################################################
|
||||||
|
@SafeSlot(dict, dict)
|
||||||
|
def on_monitor_1d_update(self, msg: dict, metadata: dict):
|
||||||
|
"""
|
||||||
|
Update the plot widget with the monitor data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg(dict): The message data.
|
||||||
|
metadata(dict): The metadata of the message.
|
||||||
|
"""
|
||||||
|
data = msg.get("data", None)
|
||||||
|
current_scan_id = metadata.get("scan_id", None)
|
||||||
|
|
||||||
|
if current_scan_id != self.scan_id:
|
||||||
|
self.scan_id = current_scan_id
|
||||||
|
self.clear_curves()
|
||||||
|
self.curves.clear()
|
||||||
|
if self.crosshair:
|
||||||
|
self.crosshair.clear_markers()
|
||||||
|
|
||||||
|
# Always create a new curve and add it
|
||||||
|
curve = pg.PlotDataItem()
|
||||||
|
curve.setData(data)
|
||||||
|
self.plot_item.addItem(curve)
|
||||||
|
self.curves.append(curve)
|
||||||
|
|
||||||
|
# Max Trace and scale colors
|
||||||
|
self.set_curve_limit(self.config.curve_limit, self.config.flush_buffer)
|
||||||
|
|
||||||
|
@SafeSlot(int)
|
||||||
|
def set_curve_highlight(self, index: int):
|
||||||
|
"""
|
||||||
|
Set the curve highlight based on visible curves.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index (int): The index of the curve to highlight among visible curves.
|
||||||
|
"""
|
||||||
|
self.plot_item.visible_curves = [curve for curve in self.curves if curve.isVisible()]
|
||||||
|
num_visible_curves = len(self.plot_item.visible_curves)
|
||||||
|
self.number_of_visible_curves = num_visible_curves
|
||||||
|
|
||||||
|
if num_visible_curves == 0:
|
||||||
|
return # No curves to highlight
|
||||||
|
|
||||||
|
if index >= num_visible_curves:
|
||||||
|
index = num_visible_curves - 1
|
||||||
|
elif index < 0:
|
||||||
|
index = num_visible_curves + index
|
||||||
|
self._current_highlight_index = index
|
||||||
|
num_colors = num_visible_curves
|
||||||
|
colors = Colors.evenly_spaced_colors(
|
||||||
|
colormap=self.config.color_palette, num=num_colors, format="HEX"
|
||||||
|
)
|
||||||
|
for i, curve in enumerate(self.plot_item.visible_curves):
|
||||||
|
curve.setPen()
|
||||||
|
if i == self._current_highlight_index:
|
||||||
|
curve.setPen(pg.mkPen(color=colors[i], width=5))
|
||||||
|
curve.setAlpha(alpha=1, auto=False)
|
||||||
|
curve.setZValue(1)
|
||||||
|
else:
|
||||||
|
curve.setPen(pg.mkPen(color=colors[i], width=1))
|
||||||
|
curve.setAlpha(alpha=self.config.opacity / 100, auto=False)
|
||||||
|
curve.setZValue(0)
|
||||||
|
|
||||||
|
self.highlighted_curve_index_changed.emit(self._current_highlight_index)
|
||||||
|
|
||||||
|
def _disconnect_monitor(self):
|
||||||
|
try:
|
||||||
|
previous_monitor = self.config.monitor
|
||||||
|
except AttributeError:
|
||||||
|
previous_monitor = None
|
||||||
|
|
||||||
|
if previous_monitor and self.connected is True:
|
||||||
|
self.bec_dispatcher.disconnect_slot(
|
||||||
|
self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(previous_monitor)
|
||||||
|
)
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def _connect_monitor(self):
|
||||||
|
"""
|
||||||
|
Connect the monitor to the plot widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.config.monitor and self.connected is False:
|
||||||
|
self.bec_dispatcher.connect_slot(
|
||||||
|
self.on_monitor_1d_update, MessageEndpoints.device_monitor_1d(self.config.monitor)
|
||||||
|
)
|
||||||
|
self.connected = True
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Utility Methods
|
||||||
|
################################################################################
|
||||||
|
def scale_colors(self):
|
||||||
|
"""
|
||||||
|
Scale the colors of the curves based on the current colormap.
|
||||||
|
"""
|
||||||
|
# TODO probably has to be changed to property
|
||||||
|
if self.config.highlight_last_curve:
|
||||||
|
self.set_curve_highlight(-1) # Use -1 to highlight the last visible curve
|
||||||
|
else:
|
||||||
|
self.set_curve_highlight(self._current_highlight_index)
|
||||||
|
|
||||||
|
def hook_crosshair(self) -> None:
|
||||||
|
"""
|
||||||
|
Specific hookfor crosshair, since it is for multiple curves.
|
||||||
|
"""
|
||||||
|
super().hook_crosshair()
|
||||||
|
if self.crosshair:
|
||||||
|
self.highlighted_curve_index_changed.connect(self.crosshair.update_highlighted_curve)
|
||||||
|
if self.curves:
|
||||||
|
self.crosshair.update_highlighted_curve(self._current_highlight_index)
|
||||||
|
|
||||||
|
def clear_curves(self):
|
||||||
|
"""
|
||||||
|
Remove all curves from the plot, excluding crosshair items.
|
||||||
|
"""
|
||||||
|
items_to_remove = []
|
||||||
|
for item in self.plot_item.items:
|
||||||
|
if not getattr(item, "is_crosshair", False) and isinstance(item, pg.PlotDataItem):
|
||||||
|
items_to_remove.append(item)
|
||||||
|
for item in items_to_remove:
|
||||||
|
self.plot_item.removeItem(item)
|
||||||
|
|
||||||
|
def _sync_monitor_selection_toolbar(self):
|
||||||
|
"""
|
||||||
|
Sync the motor map selection toolbar with the current motor map.
|
||||||
|
"""
|
||||||
|
if self.monitor_selection_bundle is not None:
|
||||||
|
monitor = self.monitor_selection_bundle.monitor.currentText()
|
||||||
|
color_palette = self.monitor_selection_bundle.colormap_widget.colormap
|
||||||
|
|
||||||
|
if monitor != self.config.monitor:
|
||||||
|
self.monitor_selection_bundle.monitor.blockSignals(True)
|
||||||
|
self.monitor_selection_bundle.monitor.set_device(self.config.monitor)
|
||||||
|
self.monitor_selection_bundle.monitor.check_validity(self.config.monitor)
|
||||||
|
self.monitor_selection_bundle.monitor.blockSignals(False)
|
||||||
|
|
||||||
|
if color_palette != self.config.color_palette:
|
||||||
|
self.monitor_selection_bundle.colormap_widget.blockSignals(True)
|
||||||
|
self.monitor_selection_bundle.colormap_widget.colormap = self.config.color_palette
|
||||||
|
self.monitor_selection_bundle.colormap_widget.blockSignals(False)
|
@ -0,0 +1 @@
|
|||||||
|
{'files': ['multi_waveform.py']}
|
@ -0,0 +1,54 @@
|
|||||||
|
# Copyright (C) 2022 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||||
|
|
||||||
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||||
|
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform import MultiWaveform
|
||||||
|
|
||||||
|
DOM_XML = """
|
||||||
|
<ui language='c++'>
|
||||||
|
<widget class='MultiWaveform' name='multi_waveform'>
|
||||||
|
</widget>
|
||||||
|
</ui>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MultiWaveformPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._form_editor = None
|
||||||
|
|
||||||
|
def createWidget(self, parent):
|
||||||
|
t = MultiWaveform(parent)
|
||||||
|
return t
|
||||||
|
|
||||||
|
def domXml(self):
|
||||||
|
return DOM_XML
|
||||||
|
|
||||||
|
def group(self):
|
||||||
|
return "Plot Widgets Next Gen"
|
||||||
|
|
||||||
|
def icon(self):
|
||||||
|
return designer_material_icon(MultiWaveform.ICON_NAME)
|
||||||
|
|
||||||
|
def includeFile(self):
|
||||||
|
return "multi_waveform"
|
||||||
|
|
||||||
|
def initialize(self, form_editor):
|
||||||
|
self._form_editor = form_editor
|
||||||
|
|
||||||
|
def isContainer(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isInitialized(self):
|
||||||
|
return self._form_editor is not None
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
return "MultiWaveform"
|
||||||
|
|
||||||
|
def toolTip(self):
|
||||||
|
return "MultiWaveform"
|
||||||
|
|
||||||
|
def whatsThis(self):
|
||||||
|
return self.toolTip()
|
@ -0,0 +1,17 @@
|
|||||||
|
def main(): # pragma: no cover
|
||||||
|
from qtpy import PYSIDE6
|
||||||
|
|
||||||
|
if not PYSIDE6:
|
||||||
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
|
return
|
||||||
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
|
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform_plugin import (
|
||||||
|
MultiWaveformPlugin,
|
||||||
|
)
|
||||||
|
|
||||||
|
QPyDesignerCustomWidgetCollection.addCustomWidget(MultiWaveformPlugin())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
main()
|
@ -0,0 +1,145 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.qt_utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils import UILoader
|
||||||
|
from bec_widgets.utils.widget_io import WidgetIO
|
||||||
|
|
||||||
|
|
||||||
|
class MultiWaveformControlPanel(SettingWidget):
|
||||||
|
"""
|
||||||
|
A settings widget MultiWaveformControlPanel that allows the user to modify the properties.
|
||||||
|
|
||||||
|
The widget has skip_settings property set to True, which means it should not be saved
|
||||||
|
in the settings file. It is used to mirror the properties of the target widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||||
|
super().__init__(parent=parent, *args, **kwargs)
|
||||||
|
|
||||||
|
self.setProperty("skip_settings", True)
|
||||||
|
self.setObjectName("MultiWaveformControlPanel")
|
||||||
|
current_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
form = UILoader().load_ui(os.path.join(current_path, "multi_waveform_controls.ui"), self)
|
||||||
|
|
||||||
|
self.target_widget = target_widget
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.ui = form
|
||||||
|
|
||||||
|
self.ui_widget_list = [
|
||||||
|
self.ui.opacity,
|
||||||
|
self.ui.highlighted_index,
|
||||||
|
self.ui.highlight_last_curve,
|
||||||
|
self.ui.flush_buffer,
|
||||||
|
self.ui.max_trace,
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.target_widget is not None:
|
||||||
|
self.connect_all_signals()
|
||||||
|
self.target_widget.property_changed.connect(self.update_property)
|
||||||
|
self.target_widget.monitor_signal_updated.connect(self.update_controls_limits)
|
||||||
|
self.ui.highlight_last_curve.toggled.connect(self.set_highlight_last_curve)
|
||||||
|
|
||||||
|
self.fetch_all_properties()
|
||||||
|
|
||||||
|
def connect_all_signals(self):
|
||||||
|
for widget in self.ui_widget_list:
|
||||||
|
WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def set_property(self, widget: QWidget, value):
|
||||||
|
"""
|
||||||
|
Set property of the target widget based on the widget that emitted the signal.
|
||||||
|
The name of the property has to be the same as the objectName of the widget
|
||||||
|
and compatible with WidgetIO.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget(QWidget): The widget that emitted the signal.
|
||||||
|
value(): The value to set the property to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try: # to avoid crashing when the widget is not found in Designer
|
||||||
|
property_name = widget.objectName()
|
||||||
|
setattr(self.target_widget, property_name, value)
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def update_property(self, property_name: str, value):
|
||||||
|
"""
|
||||||
|
Update the value of the widget based on the property name and value.
|
||||||
|
The name of the property has to be the same as the objectName of the widget
|
||||||
|
and compatible with WidgetIO.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
property_name(str): The name of the property to update.
|
||||||
|
value: The value to set the property to.
|
||||||
|
"""
|
||||||
|
try: # to avoid crashing when the widget is not found in Designer
|
||||||
|
widget_to_set = self.ui.findChild(QWidget, property_name)
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
|
if widget_to_set is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
WidgetIO.set_value(widget_to_set, value)
|
||||||
|
|
||||||
|
def fetch_all_properties(self):
|
||||||
|
"""
|
||||||
|
Fetch all properties from the target widget and update the settings widget.
|
||||||
|
"""
|
||||||
|
for widget in self.ui_widget_list:
|
||||||
|
property_name = widget.objectName()
|
||||||
|
value = getattr(self.target_widget, property_name)
|
||||||
|
WidgetIO.set_value(widget, value)
|
||||||
|
|
||||||
|
def accept_changes(self):
|
||||||
|
"""
|
||||||
|
Apply all properties from the settings widget to the target widget.
|
||||||
|
"""
|
||||||
|
for widget in self.ui_widget_list:
|
||||||
|
property_name = widget.objectName()
|
||||||
|
value = WidgetIO.get_value(widget)
|
||||||
|
setattr(self.target_widget, property_name, value)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def update_controls_limits(self):
|
||||||
|
"""
|
||||||
|
Update the limits of the controls.
|
||||||
|
"""
|
||||||
|
num_curves = len(self.target_widget.curves)
|
||||||
|
if num_curves == 0:
|
||||||
|
num_curves = 1 # Avoid setting max to 0
|
||||||
|
current_index = num_curves - 1
|
||||||
|
self.ui.highlighted_index.setMinimum(0)
|
||||||
|
self.ui.highlighted_index.setMaximum(self.target_widget.number_of_visible_curves - 1)
|
||||||
|
self.ui.spinbox_index.setMaximum(self.target_widget.number_of_visible_curves - 1)
|
||||||
|
if self.ui.highlight_last_curve.isChecked():
|
||||||
|
self.ui.highlighted_index.setValue(current_index)
|
||||||
|
self.ui.spinbox_index.setValue(current_index)
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def set_highlight_last_curve(self, enable: bool) -> None:
|
||||||
|
"""
|
||||||
|
Enable or disable highlighting of the last curve.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enable(bool): True to enable highlighting of the last curve, False to disable.
|
||||||
|
"""
|
||||||
|
self.target_widget.config.highlight_last_curve = enable
|
||||||
|
if enable:
|
||||||
|
self.ui.highlighted_index.setEnabled(False)
|
||||||
|
self.ui.spinbox_index.setEnabled(False)
|
||||||
|
self.ui.highlight_last_curve.setChecked(True)
|
||||||
|
self.target_widget.set_curve_highlight(-1)
|
||||||
|
else:
|
||||||
|
self.ui.highlighted_index.setEnabled(True)
|
||||||
|
self.ui.spinbox_index.setEnabled(True)
|
||||||
|
self.ui.highlight_last_curve.setChecked(False)
|
||||||
|
index = self.ui.spinbox_index.value()
|
||||||
|
self.target_widget.set_curve_highlight(index)
|
@ -0,0 +1,164 @@
|
|||||||
|
<?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>561</width>
|
||||||
|
<height>86</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_curve_index">
|
||||||
|
<property name="text">
|
||||||
|
<string>Curve Index</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QSlider" name="highlighted_index">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QSpinBox" name="spinbox_index"/>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="3" colspan="3">
|
||||||
|
<widget class="QCheckBox" name="highlight_last_curve">
|
||||||
|
<property name="text">
|
||||||
|
<string>Highlight always last curve</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_opacity">
|
||||||
|
<property name="text">
|
||||||
|
<string>Opacity</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QSlider" name="opacity">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Orientation::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="3">
|
||||||
|
<widget class="QLabel" name="label_max_trace">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max Trace</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="4">
|
||||||
|
<widget class="QSpinBox" name="max_trace">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>How many curves should be displayed</string>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>500</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>200</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="5">
|
||||||
|
<widget class="QCheckBox" name="flush_buffer">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>If hiddne curves should be deleted.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Flush Buffer</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QSpinBox" name="spinbox_opacity">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>opacity</sender>
|
||||||
|
<signal>valueChanged(int)</signal>
|
||||||
|
<receiver>spinbox_opacity</receiver>
|
||||||
|
<slot>setValue(int)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>211</x>
|
||||||
|
<y>66</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>260</x>
|
||||||
|
<y>59</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>spinbox_opacity</sender>
|
||||||
|
<signal>valueChanged(int)</signal>
|
||||||
|
<receiver>opacity</receiver>
|
||||||
|
<slot>setValue(int)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>269</x>
|
||||||
|
<y>62</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>182</x>
|
||||||
|
<y>62</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>highlighted_index</sender>
|
||||||
|
<signal>valueChanged(int)</signal>
|
||||||
|
<receiver>spinbox_index</receiver>
|
||||||
|
<slot>setValue(int)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>191</x>
|
||||||
|
<y>27</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>256</x>
|
||||||
|
<y>27</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>spinbox_index</sender>
|
||||||
|
<signal>valueChanged(int)</signal>
|
||||||
|
<receiver>highlighted_index</receiver>
|
||||||
|
<slot>setValue(int)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>264</x>
|
||||||
|
<y>20</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>195</x>
|
||||||
|
<y>24</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -0,0 +1,58 @@
|
|||||||
|
from bec_lib.device import ReadoutPriority
|
||||||
|
from qtpy.QtCore import Qt
|
||||||
|
from qtpy.QtWidgets import QStyledItemDelegate
|
||||||
|
|
||||||
|
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.qt_utils.toolbar import ToolbarBundle, WidgetAction
|
||||||
|
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||||
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
|
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
||||||
|
|
||||||
|
|
||||||
|
class NoCheckDelegate(QStyledItemDelegate):
|
||||||
|
"""To reduce space in combo boxes by removing the checkmark."""
|
||||||
|
|
||||||
|
def initStyleOption(self, option, index):
|
||||||
|
super().initStyleOption(option, index)
|
||||||
|
# Remove any check indicator
|
||||||
|
option.checkState = Qt.Unchecked
|
||||||
|
|
||||||
|
|
||||||
|
class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||||
|
"""
|
||||||
|
A bundle of actions for a toolbar that selects motors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bundle_id="monitor_selection", target_widget=None, **kwargs):
|
||||||
|
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||||
|
self.target_widget = target_widget
|
||||||
|
|
||||||
|
# Monitor Selection
|
||||||
|
self.monitor = DeviceComboBox(
|
||||||
|
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=ReadoutPriority.ASYNC
|
||||||
|
)
|
||||||
|
self.monitor.addItem("", None)
|
||||||
|
self.monitor.setCurrentText("")
|
||||||
|
self.monitor.setToolTip("Select Monitor")
|
||||||
|
self.monitor.setItemDelegate(NoCheckDelegate(self.monitor))
|
||||||
|
self.add_action("monitor", WidgetAction(widget=self.monitor, adjust_size=False))
|
||||||
|
|
||||||
|
# Colormap Selection
|
||||||
|
self.colormap_widget = BECColorMapWidget(cmap="magma")
|
||||||
|
self.add_action("color_map", WidgetAction(widget=self.colormap_widget, adjust_size=False))
|
||||||
|
|
||||||
|
# Connect slots, a device will be connected upon change of any combobox
|
||||||
|
self.monitor.currentTextChanged.connect(lambda: self.connect())
|
||||||
|
self.colormap_widget.colormap_changed_signal.connect(self.change_colormap)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def connect(self):
|
||||||
|
monitor = self.monitor.currentText()
|
||||||
|
|
||||||
|
if monitor != "":
|
||||||
|
if monitor != self.target_widget.config.monitor:
|
||||||
|
self.target_widget.monitor = monitor
|
||||||
|
|
||||||
|
@SafeSlot(str)
|
||||||
|
def change_colormap(self, colormap: str):
|
||||||
|
self.target_widget.color_palette = colormap
|
342
tests/unit_tests/test_multi_waveform_next_gen.py
Normal file
342
tests/unit_tests/test_multi_waveform_next_gen.py
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from bec_widgets.widgets.plots_next_gen.multi_waveform.multi_waveform import MultiWaveform
|
||||||
|
from tests.unit_tests.client_mocks import mocked_client
|
||||||
|
|
||||||
|
from .conftest import create_widget
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# MultiWaveform widget base functionality tests
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_initialization(qtbot, mocked_client):
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
|
||||||
|
assert mw.objectName() == "MultiWaveform"
|
||||||
|
# Inherited from PlotBase
|
||||||
|
assert mw.title == ""
|
||||||
|
assert mw.x_label == ""
|
||||||
|
assert mw.y_label == ""
|
||||||
|
# No crosshair or FPS monitor by default
|
||||||
|
assert mw.crosshair is None
|
||||||
|
assert mw.fps_monitor is None
|
||||||
|
# No curves initially
|
||||||
|
assert len(mw.plot_item.curves) == 0
|
||||||
|
# Multiwaveform specific
|
||||||
|
assert mw.monitor is None
|
||||||
|
assert mw.color_palette == "magma"
|
||||||
|
assert mw.max_trace == 200
|
||||||
|
assert mw.flush_buffer is False
|
||||||
|
assert mw.highlight_last_curve is True
|
||||||
|
assert mw.opacity == 50
|
||||||
|
assert mw.scan_id is None
|
||||||
|
assert mw.highlighted_index == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_set_monitor(qtbot, mocked_client):
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
assert mw.monitor is None
|
||||||
|
|
||||||
|
# Set a monitor
|
||||||
|
mw.plot("waveform1d")
|
||||||
|
assert mw.monitor == "waveform1d"
|
||||||
|
assert mw.config.monitor == "waveform1d"
|
||||||
|
assert mw.connected is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_set_properties(qtbot, mocked_client):
|
||||||
|
"""Check that MultiWaveform properties can be set and retrieved correctly."""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
|
||||||
|
# Default checks
|
||||||
|
assert mw.color_palette == "magma"
|
||||||
|
assert mw.max_trace == 200
|
||||||
|
assert mw.flush_buffer is False
|
||||||
|
assert mw.highlight_last_curve is True
|
||||||
|
assert mw.opacity == 50
|
||||||
|
|
||||||
|
# Change properties
|
||||||
|
mw.color_palette = "viridis"
|
||||||
|
mw.max_trace = 10
|
||||||
|
mw.flush_buffer = True
|
||||||
|
mw.highlight_last_curve = False
|
||||||
|
mw.opacity = 75
|
||||||
|
|
||||||
|
# Verify that changes took effect
|
||||||
|
assert mw.color_palette == "viridis"
|
||||||
|
assert mw.max_trace == 10
|
||||||
|
assert mw.flush_buffer is True
|
||||||
|
assert mw.highlight_last_curve is False
|
||||||
|
assert mw.opacity == 75
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_curve_limit_no_flush(qtbot, mocked_client):
|
||||||
|
"""Check that limiting the number of curves without flush simply hides older ones."""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
mw.max_trace = 3
|
||||||
|
mw.flush_buffer = False
|
||||||
|
|
||||||
|
# Simulate updates that create multiple curves
|
||||||
|
for i in range(5):
|
||||||
|
msg_data = {"data": np.array([i, i + 0.5, i + 1])}
|
||||||
|
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||||
|
|
||||||
|
# There should be 5 curves in total, but only the last 3 are visible
|
||||||
|
assert len(mw.curves) == 5
|
||||||
|
visible_curves = [c for c in mw.curves if c.isVisible()]
|
||||||
|
assert len(visible_curves) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_curve_limit_flush(qtbot, mocked_client):
|
||||||
|
"""Check that limiting the number of curves with flush removes older ones."""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
mw.max_trace = 3
|
||||||
|
mw.flush_buffer = True
|
||||||
|
|
||||||
|
# Simulate adding multiple curves
|
||||||
|
for i in range(5):
|
||||||
|
msg_data = {"data": np.array([i, i + 0.5, i + 1])}
|
||||||
|
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||||
|
|
||||||
|
# Only 3 curves remain after flush
|
||||||
|
assert len(mw.curves) == 3
|
||||||
|
# They should match the last 3 that were inserted
|
||||||
|
x_data, y_data = mw.curves[0].getData()
|
||||||
|
assert np.array_equal(y_data, [2, 2.5, 3])
|
||||||
|
x_data, y_data = mw.curves[1].getData()
|
||||||
|
assert np.array_equal(y_data, [3, 3.5, 4])
|
||||||
|
x_data, y_data = mw.curves[2].getData()
|
||||||
|
assert np.array_equal(y_data, [4, 4.5, 5])
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_highlight_last_curve(qtbot, mocked_client):
|
||||||
|
"""Check highlight_last_curve behavior."""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
mw.max_trace = 5
|
||||||
|
mw.flush_buffer = False
|
||||||
|
|
||||||
|
# Simulate adding multiple curves
|
||||||
|
for i in range(3):
|
||||||
|
msg_data = {"data": np.array([i, i + 1, i + 2])}
|
||||||
|
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||||
|
|
||||||
|
# Initially highlight_last_curve is True, so the last visible curve is highlighted
|
||||||
|
# The highlight index should be -1 in the code's logic
|
||||||
|
assert mw.highlight_last_curve is True
|
||||||
|
|
||||||
|
# Disable highlight_last_curve
|
||||||
|
mw.highlight_last_curve = False
|
||||||
|
|
||||||
|
# Force highlight of the 1st visible curve (index 0 among visible)
|
||||||
|
mw.set_curve_highlight(0)
|
||||||
|
assert mw.highlighted_index == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_opacity_changes(qtbot, mocked_client):
|
||||||
|
"""Check changing opacity affects existing curves."""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
mw.plot("waveform1d")
|
||||||
|
|
||||||
|
# Add one curve
|
||||||
|
msg_data = {"data": np.array([10, 20, 30])}
|
||||||
|
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||||
|
assert len(mw.curves) == 1
|
||||||
|
|
||||||
|
# Default opacity is 50
|
||||||
|
assert mw.opacity == 50
|
||||||
|
|
||||||
|
# Change opacity
|
||||||
|
mw.opacity = 80
|
||||||
|
assert mw.opacity == 80
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_set_colormap(qtbot, mocked_client):
|
||||||
|
"""Check that setting a new colormap updates curve colors."""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
mw.plot("waveform1d")
|
||||||
|
|
||||||
|
# Simulate multiple curve updates
|
||||||
|
for i in range(3):
|
||||||
|
msg_data = {"data": np.array([i, i + 1, i + 2])}
|
||||||
|
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_1"})
|
||||||
|
|
||||||
|
# Default color_palette is "magma"
|
||||||
|
assert mw.color_palette == "magma"
|
||||||
|
# Now change to a new colormap
|
||||||
|
mw.color_palette = "viridis"
|
||||||
|
assert mw.color_palette == "viridis"
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiwaveform_simulate_updates(qtbot, mocked_client):
|
||||||
|
"""Simulate a series of 1D updates to ensure the data is appended and the correct number of curves appear."""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
mw.plot("waveform1d")
|
||||||
|
|
||||||
|
data_series = [np.random.rand(5), np.random.rand(5), np.random.rand(5)]
|
||||||
|
for idx, arr in enumerate(data_series):
|
||||||
|
msg_data = {"data": arr}
|
||||||
|
mw.on_monitor_1d_update(msg_data, metadata={"scan_id": "scan_99"})
|
||||||
|
# Each update should add a new curve
|
||||||
|
assert len(mw.curves) == idx + 1
|
||||||
|
x_data, y_data = mw.curves[-1].getData()
|
||||||
|
assert np.array_equal(y_data, arr)
|
||||||
|
|
||||||
|
# Check that the scan_id was updated
|
||||||
|
assert mw.scan_id == "scan_99"
|
||||||
|
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# MultiWaveform control panel and toolbar
|
||||||
|
##################################################
|
||||||
|
|
||||||
|
|
||||||
|
def test_control_panel_updates_widget(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Interact with the control panel’s UI elements and confirm the widget’s properties are updated.
|
||||||
|
"""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
|
||||||
|
assert mw.opacity == 50
|
||||||
|
assert mw.flush_buffer is False
|
||||||
|
assert mw.max_trace == 200
|
||||||
|
assert mw.highlight_last_curve is True
|
||||||
|
|
||||||
|
mw.controls.ui.opacity.setValue(80)
|
||||||
|
assert mw.opacity == 80
|
||||||
|
|
||||||
|
mw.controls.ui.flush_buffer.setChecked(True)
|
||||||
|
assert mw.flush_buffer is True
|
||||||
|
|
||||||
|
mw.controls.ui.max_trace.setValue(12)
|
||||||
|
assert mw.max_trace == 12
|
||||||
|
|
||||||
|
mw.controls.ui.highlight_last_curve.setChecked(False)
|
||||||
|
assert mw.highlight_last_curve is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_widget_updates_control_panel(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Change properties directly on the MultiWaveform and verify the control panel UI reflects those changes.
|
||||||
|
"""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
|
||||||
|
mw.opacity = 25
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert mw.controls.ui.opacity.value() == 25
|
||||||
|
|
||||||
|
mw.flush_buffer = True
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert mw.controls.ui.flush_buffer.isChecked() is True
|
||||||
|
|
||||||
|
mw.max_trace = 9
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert mw.controls.ui.max_trace.value() == 9
|
||||||
|
|
||||||
|
mw.highlight_last_curve = False
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert mw.controls.ui.highlight_last_curve.isChecked() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_selection_toolbar_updates_widget(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Confirm that selecting a monitor and a colormap from the selection toolbar
|
||||||
|
updates the widget properties.
|
||||||
|
"""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
toolbar = mw.monitor_selection_bundle
|
||||||
|
monitor_combo = toolbar.monitor
|
||||||
|
colormap_widget = toolbar.colormap_widget
|
||||||
|
|
||||||
|
monitor_combo.addItem("waveform1d")
|
||||||
|
monitor_combo.setCurrentText("waveform1d")
|
||||||
|
assert mw.monitor == "waveform1d"
|
||||||
|
|
||||||
|
colormap_widget.colormap = "viridis"
|
||||||
|
assert mw.color_palette == "viridis"
|
||||||
|
|
||||||
|
|
||||||
|
def test_control_panel_opacity_slider_spinbox(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Verify that when the user moves the opacity slider or spinbox, the widget's
|
||||||
|
opacity property updates, and vice versa. Also confirm they stay in sync.
|
||||||
|
"""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
|
||||||
|
slider_opacity = mw.controls.ui.opacity
|
||||||
|
spinbox_opacity = mw.controls.ui.spinbox_opacity
|
||||||
|
|
||||||
|
# Default
|
||||||
|
assert mw.opacity == 50
|
||||||
|
assert slider_opacity.value() == 50
|
||||||
|
assert spinbox_opacity.value() == 50
|
||||||
|
|
||||||
|
# Move the slider
|
||||||
|
slider_opacity.setValue(75)
|
||||||
|
assert mw.opacity == 75
|
||||||
|
assert spinbox_opacity.value() == 75
|
||||||
|
|
||||||
|
# Move the spinbox
|
||||||
|
spinbox_opacity.setValue(20)
|
||||||
|
assert mw.opacity == 20
|
||||||
|
assert slider_opacity.value() == 20
|
||||||
|
|
||||||
|
mw.opacity = 95
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert slider_opacity.value() == 95
|
||||||
|
assert spinbox_opacity.value() == 95
|
||||||
|
|
||||||
|
|
||||||
|
def test_control_panel_highlight_slider_spinbox(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test that the slider and spinbox for curve highlighting update
|
||||||
|
the widget’s highlighted_index property, and are disabled if
|
||||||
|
highlight_last_curve is True.
|
||||||
|
"""
|
||||||
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
|
||||||
|
slider_index = mw.controls.ui.highlighted_index
|
||||||
|
spinbox_index = mw.controls.ui.spinbox_index
|
||||||
|
checkbox_highlight_last = mw.controls.ui.highlight_last_curve
|
||||||
|
|
||||||
|
# By default highlight_last_curve is True, so slider/spinbox are disabled:
|
||||||
|
assert checkbox_highlight_last.isChecked() is True
|
||||||
|
assert not slider_index.isEnabled()
|
||||||
|
assert not spinbox_index.isEnabled()
|
||||||
|
|
||||||
|
# Uncheck highlight_last_curve -> slider/spinbox become enabled
|
||||||
|
checkbox_highlight_last.setChecked(False)
|
||||||
|
assert checkbox_highlight_last.isChecked() is False
|
||||||
|
assert slider_index.isEnabled()
|
||||||
|
assert spinbox_index.isEnabled()
|
||||||
|
|
||||||
|
# Simulate a few curves so there's something to highlight
|
||||||
|
data_arrays = [np.array([0, 1, 2]), np.array([3, 4, 5]), np.array([6, 7, 8])]
|
||||||
|
for arr in data_arrays:
|
||||||
|
mw.on_monitor_1d_update({"data": arr}, {"scan_id": "scan_123"})
|
||||||
|
|
||||||
|
# The number_of_visible_curves == 3 now
|
||||||
|
max_index = mw.number_of_visible_curves - 1
|
||||||
|
assert max_index == 2
|
||||||
|
|
||||||
|
# Move the slider to index 1
|
||||||
|
slider_index.setValue(1)
|
||||||
|
assert mw.highlighted_index == 1
|
||||||
|
assert spinbox_index.value() == 1
|
||||||
|
|
||||||
|
# Move the spinbox to index 2
|
||||||
|
spinbox_index.setValue(2)
|
||||||
|
assert mw.highlighted_index == 2
|
||||||
|
assert slider_index.value() == 2
|
||||||
|
|
||||||
|
# Directly set mw.highlighted_index
|
||||||
|
mw.highlighted_index = 0
|
||||||
|
qtbot.wait(100)
|
||||||
|
assert slider_index.value() == 0
|
||||||
|
assert spinbox_index.value() == 0
|
||||||
|
|
||||||
|
# Re-check highlight_last_curve -> slider/spinbox disabled again
|
||||||
|
checkbox_highlight_last.setChecked(True)
|
||||||
|
assert not slider_index.isEnabled()
|
||||||
|
assert not spinbox_index.isEnabled()
|
||||||
|
assert mw.highlighted_index == 2
|
Reference in New Issue
Block a user