1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-08 17:57:54 +02:00

Compare commits

...

15 Commits

Author SHA1 Message Date
b1ef2b20d2 fix(motor_map): fixed bug with residual trace after changing motors 2024-07-02 20:36:20 +02:00
055ad0f22f refactor(motor_widgets_settings): moved to separate folder 2024-07-02 20:27:48 +02:00
8827ab7ff7 feat(motor_map_widget): plugin added 2024-07-02 20:20:17 +02:00
9f33a5c81f feat(motor_map_widget): complete toolbar 2024-07-02 20:08:08 +02:00
a40662071b feat(toolbar):colors adjusted 2024-07-02 19:53:59 +02:00
f4f92202c1 feat(color_button): patched ColorButton from pyqtgraph to be able to be opened in another QDialog 2024-07-02 19:52:46 +02:00
aa33d04444 feat(motor_map_widget): combobox in toolbar changes color if changed 2024-07-02 15:49:32 +02:00
9e4d407893 refactor(toolbar): cleanup 2024-07-02 15:48:31 +02:00
55be644bf3 refactor(motor_map_widget): moved to top level 2024-07-02 15:48:31 +02:00
5bba788153 feat(motor_map): method to reset history trace 2024-07-02 14:14:10 +02:00
6fe568f83b refactor(motor_map_widget): slots changed 2024-07-02 14:14:09 +02:00
e7b3109493 feat(axis_setting): general axis setting dialog for bec figure subplots 2024-07-02 14:12:06 +02:00
fb17bf66cc feat(widget_io): added method to check and adjust limits of spinboxes 2024-07-02 14:12:06 +02:00
1f2f4824bf fix(widget_io): added option to set value of combobox by str 2024-07-02 14:12:06 +02:00
51f54ca22e feat(motor_widget): Motor Widget with toolbar WIP 2024-07-02 14:12:06 +02:00
20 changed files with 1009 additions and 110 deletions

View File

@@ -18,6 +18,7 @@ class Widgets(str, enum.Enum):
BECDock = "BECDock"
BECDockArea = "BECDockArea"
BECFigure = "BECFigure"
BECMotorMapWidget = "BECMotorMapWidget"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
TextBox = "TextBox"
@@ -1192,6 +1193,7 @@ class BECMotorMap(RPCBase):
def get_data(self) -> "dict":
"""
Get the data of the motor map.
Returns:
dict: Data of the motor map.
"""
@@ -1202,6 +1204,94 @@ class BECMotorMap(RPCBase):
Remove the plot widget from the figure.
"""
@rpc_call
def reset_history(self):
"""
Reset the history of the motor map.
"""
class BECMotorMapWidget(RPCBase):
@rpc_call
def change_motors(
self,
motor_x: "str",
motor_y: "str",
motor_x_entry: "str" = None,
motor_y_entry: "str" = None,
validate_bec: "bool" = True,
) -> "None":
"""
Change the active motors for the plot.
Args:
motor_x(str): Motor name for the X axis.
motor_y(str): Motor name for the Y axis.
motor_x_entry(str): Motor entry for the X axis.
motor_y_entry(str): Motor entry for the Y axis.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
"""
@rpc_call
def set_max_points(self, max_points: "int") -> "None":
"""
Set the maximum number of points to display on the motor map.
Args:
max_points(int): Maximum number of points to display.
"""
@rpc_call
def set_precision(self, precision: "int") -> "None":
"""
Set the precision of the motor map.
Args:
precision(int): Precision to set.
"""
@rpc_call
def set_num_dim_points(self, num_dim_points: "int") -> "None":
"""
Set the number of points to display on the motor map.
Args:
num_dim_points(int): Number of points to display.
"""
@rpc_call
def set_background_value(self, background_value: "int") -> "None":
"""
Set the background value of the motor map.
Args:
background_value(int): Background value of the motor map.
"""
@rpc_call
def set_scatter_size(self, scatter_size: "int") -> "None":
"""
Set the scatter size of the motor map.
Args:
scatter_size(int): Scatter size of the motor map.
"""
@rpc_call
def get_data(self) -> "dict":
"""
Get the data of the motor map.
Returns:
dict: Data of the motor map.
"""
@rpc_call
def reset_history(self) -> "None":
"""
Reset the history of the motor map.
"""
class BECPlotBase(RPCBase):
@property

View File

@@ -44,8 +44,11 @@ class ComboBoxHandler(WidgetHandler):
def get_value(self, widget: QComboBox) -> int:
return widget.currentIndex()
def set_value(self, widget: QComboBox, value: int) -> None:
widget.setCurrentIndex(value)
def set_value(self, widget: QComboBox, value: int | str) -> None:
if isinstance(value, str):
value = widget.findText(value)
if isinstance(value, int):
widget.setCurrentIndex(value)
class TableWidgetHandler(WidgetHandler):
@@ -142,6 +145,26 @@ class WidgetIO:
elif not ignore_errors:
raise ValueError(f"No handler for widget type: {type(widget)}")
@staticmethod
def check_and_adjust_limits(spin_box: QDoubleSpinBox, number: float):
"""
Check if the new limits are within the current limits, if not adjust the limits.
Args:
number(float): The new value to check against the limits.
"""
min_value = spin_box.minimum()
max_value = spin_box.maximum()
# Calculate the new limits
new_limit = number + 5 * number
if number < min_value:
spin_box.setMinimum(new_limit)
elif number > max_value:
spin_box.setMaximum(new_limit)
@staticmethod
def _find_handler(widget):
"""

View File

@@ -0,0 +1,17 @@
import pyqtgraph as pg
class ColorButton(pg.ColorButton):
"""
A ColorButton that opens a dialog to select a color. Inherits from pyqtgraph.ColorButton.
Patches event loop of the ColorDialog, if opened in another QDialog.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def selectColor(self):
self.origColor = self.color()
self.colorDialog.setCurrentColor(self.color())
self.colorDialog.open()
self.colorDialog.exec()

View File

@@ -0,0 +1,61 @@
import os
import qdarktheme
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO
class AxisSettings(QWidget):
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent=parent, *args, **kwargs)
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "axis_settings.ui"), self)
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
# Hardcoded values for best appearance
self.setMinimumHeight(280)
self.setMaximumHeight(280)
self.resize(380, 280)
@Slot(dict)
def display_current_settings(self, axis_config: dict):
# Top Box
WidgetIO.set_value(self.ui.plot_title, axis_config["title"])
# X Axis Box
WidgetIO.set_value(self.ui.x_label, axis_config["x_label"])
WidgetIO.set_value(self.ui.x_scale, axis_config["x_scale"])
WidgetIO.set_value(self.ui.x_grid, axis_config["x_grid"])
if axis_config["x_lim"] is not None:
WidgetIO.check_and_adjust_limits(self.ui.x_min, axis_config["x_lim"][0])
WidgetIO.check_and_adjust_limits(self.ui.x_max, axis_config["x_lim"][1])
WidgetIO.set_value(self.ui.x_min, axis_config["x_lim"][0])
WidgetIO.set_value(self.ui.x_max, axis_config["x_lim"][1])
# Y Axis Box
WidgetIO.set_value(self.ui.y_label, axis_config["y_label"])
WidgetIO.set_value(self.ui.y_scale, axis_config["y_scale"])
WidgetIO.set_value(self.ui.y_grid, axis_config["y_grid"])
if axis_config["y_lim"] is not None:
WidgetIO.check_and_adjust_limits(self.ui.y_min, axis_config["y_lim"][0])
WidgetIO.check_and_adjust_limits(self.ui.y_max, axis_config["y_lim"][1])
WidgetIO.set_value(self.ui.y_min, axis_config["y_lim"][0])
WidgetIO.set_value(self.ui.y_max, axis_config["y_lim"][1])
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
qdarktheme.setup_theme("dark")
window = AxisSettings()
window.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1,249 @@
<?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>417</width>
<height>250</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>250</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>278</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="plot_title_label">
<property name="text">
<string>Plot Title</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="plot_title"/>
</item>
</layout>
</item>
<item row="1" column="0" colspan="3">
<widget class="Line" name="line_H">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="x_axis_box">
<property name="title">
<string>X Axis</string>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="3" column="0">
<widget class="QLabel" name="x_scale_label">
<property name="text">
<string>Scale</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QDoubleSpinBox" name="x_min">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="minimum">
<double>-9999.000000000000000</double>
</property>
<property name="maximum">
<double>9999.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="x_min_label">
<property name="text">
<string>Min</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QDoubleSpinBox" name="x_max">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="minimum">
<double>-9999.000000000000000</double>
</property>
<property name="maximum">
<double>9999.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="x_scale">
<item>
<property name="text">
<string>linear</string>
</property>
</item>
<item>
<property name="text">
<string>log</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="x_max_label">
<property name="text">
<string>Max</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="x_label"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="x_label_label">
<property name="text">
<string>Label</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="x_grid">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="x_grid_label">
<property name="text">
<string>Grid</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="1">
<widget class="Line" name="line_V">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QGroupBox" name="y_axis_box">
<property name="title">
<string>Y Axis</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="3" column="2">
<widget class="QComboBox" name="y_scale">
<item>
<property name="text">
<string>linear</string>
</property>
</item>
<item>
<property name="text">
<string>log</string>
</property>
</item>
</widget>
</item>
<item row="2" column="2">
<widget class="QDoubleSpinBox" name="y_max">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="minimum">
<double>-9999.000000000000000</double>
</property>
<property name="maximum">
<double>9999.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QLabel" name="y_min_label">
<property name="text">
<string>Min</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QDoubleSpinBox" name="y_min">
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="minimum">
<double>-9999.000000000000000</double>
</property>
<property name="maximum">
<double>9999.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="y_label"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="y_scale_label">
<property name="text">
<string>Scale</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="y_label_label">
<property name="text">
<string>Label</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="y_max_label">
<property name="text">
<string>Max</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="y_grid">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="y_grid_label">
<property name="text">
<string>Grid</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -10,7 +10,7 @@ from pydantic import Field, ValidationError, field_validator
from pydantic_core import PydanticCustomError
from qtpy import QtCore, QtGui
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import Colors, EntryValidator
@@ -24,7 +24,7 @@ class MotorMapConfig(SubplotConfig):
(255, 255, 255, 255), description="The color of the last point of current position."
)
scatter_size: Optional[int] = Field(5, description="Size of the scatter points.")
max_points: Optional[int] = Field(1000, description="Maximum number of points to display.")
max_points: Optional[int] = Field(5000, description="Maximum number of points to display.")
num_dim_points: Optional[int] = Field(
100,
description="Number of points to dim before the color remains same for older recorded position.",
@@ -59,6 +59,7 @@ class BECMotorMap(BECPlotBase):
"set_scatter_size",
"get_data",
"remove",
"reset_history",
]
# QT Signals
@@ -120,7 +121,7 @@ class BECMotorMap(BECPlotBase):
motor_y_entry=self.config.signals.y.entry,
)
@pyqtSlot(str, str, str, str, bool)
@Slot(str, str, str, str, bool)
def change_motors(
self,
motor_x: str,
@@ -139,6 +140,8 @@ class BECMotorMap(BECPlotBase):
motor_y_entry(str): Motor entry for the Y axis.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
"""
self.plot_item.clear()
motor_x_entry, motor_y_entry = self._validate_signal_entries(
motor_x, motor_y, motor_x_entry, motor_y_entry, validate_bec
)
@@ -164,13 +167,22 @@ class BECMotorMap(BECPlotBase):
def get_data(self) -> dict:
"""
Get the data of the motor map.
Returns:
dict: Data of the motor map.
"""
data = {"x": self.database_buffer["x"], "y": self.database_buffer["y"]}
return data
def set_color(self, color: [str | tuple]):
def reset_history(self):
"""
Reset the history of the motor map.
"""
self.database_buffer["x"] = [self.database_buffer["x"][-1]]
self.database_buffer["y"] = [self.database_buffer["y"][-1]]
self.update_signal.emit()
def set_color(self, color: str | tuple):
"""
Set color of the motor trace.
@@ -428,6 +440,7 @@ class BECMotorMap(BECPlotBase):
print(f"The device '{motor}' does not have defined limits.")
return None
@Slot()
def _update_plot(self):
"""Update the motor map plot."""
# If the number of points exceeds max_points, delete the oldest points
@@ -477,7 +490,7 @@ class BECMotorMap(BECPlotBase):
f"Motor position: ({round(float(current_x),precision)}, {round(float(current_y),precision)})"
)
@pyqtSlot(dict)
@Slot(dict)
def on_device_readback(self, msg: dict) -> None:
"""
Update the motor map plot with the new motor position.

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M17 7h-4v2h4c1.65 0 3 1.35 3 3s-1.35 3-3 3h-4v2h4c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-6 8H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-2zm-3-4h8v2H8z"/>
</svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.09-.16-.26-.25-.44-.25-.06 0-.12.01-.17.03l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.06-.02-.12-.03-.18-.03-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.09.16.26.25.44.25.06 0 .12-.01.17-.03l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
{'files': ['motor_map_widget.py','motor_map_widget_plugin.py']}

View File

@@ -0,0 +1,55 @@
import os
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtGui import QIcon
from bec_widgets.widgets.motor_map.motor_map_widget import BECMotorMapWidget
DOM_XML = """
<ui language='c++'>
<widget class='BECMotorMapWidget' name='bec_motor_map_widget'>
</widget>
</ui>
"""
class BECMotorMapWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = BECMotorMapWidget(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Visualization Widgets"
def icon(self):
current_path = os.path.dirname(__file__)
icon_path = os.path.join(current_path, "assets", "motor_map.png")
return QIcon(icon_path)
def includeFile(self):
return "bec_motor_map_widget"
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 "BECMotorMapWidget"
def toolTip(self):
return "BECMotorMapWidget"
def whatsThis(self):
return self.toolTip()

View File

@@ -0,0 +1,82 @@
import os
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QLabel, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.widget_io import WidgetIO
from bec_widgets.widgets.buttons.color_button import ColorButton
class MotorMapSettings(QWidget):
def __init__(self, parent=None, target_widget: QWidget = None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
current_path = os.path.dirname(__file__)
self.ui = UILoader().load_ui(os.path.join(current_path, "motor_map_settings.ui"), self)
self.target_widget = target_widget
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
self._add_color_button()
def _add_color_button(self):
label = QLabel("Color")
self.ui.color = ColorButton()
self.ui.gridLayout.addWidget(label, 5, 0)
self.ui.gridLayout.addWidget(self.ui.color, 5, 1)
@Slot(dict)
def display_current_settings(self, config: dict):
WidgetIO.set_value(self.ui.max_points, config["max_points"])
WidgetIO.set_value(self.ui.trace_dim, config["num_dim_points"])
WidgetIO.set_value(self.ui.precision, config["precision"])
WidgetIO.set_value(self.ui.scatter_size, config["scatter_size"])
background_intensity = int((config["background_value"] / 255) * 100)
WidgetIO.set_value(self.ui.background_value, background_intensity)
color = config["color"]
self.ui.color.setColor(color)
@Slot()
def accept_changes(self):
max_points = WidgetIO.get_value(self.ui.max_points)
num_dim_points = WidgetIO.get_value(self.ui.trace_dim)
precision = WidgetIO.get_value(self.ui.precision)
scatter_size = WidgetIO.get_value(self.ui.scatter_size)
background_intensity = int(WidgetIO.get_value(self.ui.background_value) * 0.01 * 255)
color = self.ui.color.color().toTuple()
if self.target_widget is not None:
self.target_widget.set_max_points(max_points)
self.target_widget.set_num_dim_points(num_dim_points)
self.target_widget.set_precision(precision)
self.target_widget.set_scatter_size(scatter_size)
self.target_widget.set_background_value(background_intensity)
self.target_widget.set_color(color)
class MotorMapDialog(QDialog):
def __init__(self, parent=None, target_widget: QWidget = None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.setModal(False)
self.setWindowTitle("Motor Map Settings")
self.target_widget = target_widget
self.widget = MotorMapSettings(target_widget=self.target_widget)
self.widget.display_current_settings(self.target_widget._config_dict)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.widget)
self.layout.addWidget(self.button_box)
@Slot()
def accept(self):
self.widget.accept_changes()
super().accept()

View File

@@ -0,0 +1,91 @@
<?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>237</width>
<height>184</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="max_point_label">
<property name="text">
<string>Max Points</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="max_points">
<property name="maximum">
<number>10000</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="trace_label">
<property name="text">
<string>Trace Dim</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="trace_dim">
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="precision_label">
<property name="text">
<string>Precision</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="precision">
<property name="maximum">
<number>15</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="scatter_size_label">
<property name="text">
<string>Scatter Size</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QSpinBox" name="scatter_size">
<property name="maximum">
<number>20</number>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="background_label">
<property name="text">
<string>Background Intensity</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="background_value">
<property name="maximum">
<number>100</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,59 @@
import os
from qtpy.QtCore import QSize
from qtpy.QtGui import QAction, QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget
from bec_widgets.widgets.device_inputs import DeviceComboBox
from bec_widgets.widgets.toolbar.toolbar import ToolBarAction
class DeviceSelectionAction(ToolBarAction):
def __init__(self, label: str):
self.label = label
self.device_combobox = DeviceComboBox(device_filter="Positioner")
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
def add_to_toolbar(self, toolbar, target):
widget = QWidget()
layout = QHBoxLayout(widget)
label = QLabel(f"{self.label}")
layout.addWidget(label)
layout.addWidget(self.device_combobox)
toolbar.addWidget(widget)
def set_combobox_style(self, color: str):
self.device_combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
class ConnectAction(ToolBarAction):
def add_to_toolbar(self, toolbar, target):
current_path = os.path.dirname(__file__)
parent_path = os.path.dirname(current_path)
icon = QIcon()
icon.addFile(os.path.join(parent_path, "assets", "connection.svg"), size=QSize(20, 20))
self.action = QAction(icon, "Connect Motors", target)
toolbar.addAction(self.action)
class ResetHistoryAction(ToolBarAction):
def add_to_toolbar(self, toolbar, target):
current_path = os.path.dirname(__file__)
parent_path = os.path.dirname(current_path)
icon = QIcon()
icon.addFile(os.path.join(parent_path, "assets", "history.svg"), size=QSize(20, 20))
self.action = QAction(icon, "Reset Trace History", target)
toolbar.addAction(self.action)
class SettingsAction(ToolBarAction):
def add_to_toolbar(self, toolbar, target):
current_path = os.path.dirname(__file__)
parent_path = os.path.dirname(current_path)
icon = QIcon()
icon.addFile(os.path.join(parent_path, "assets", "settings.svg"), size=QSize(20, 20))
self.action = QAction(icon, "Open Configuration Dialog", target)
toolbar.addAction(self.action)

View File

@@ -0,0 +1,191 @@
from __future__ import annotations
from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.figure.plots.motor_map.motor_map import MotorMapConfig
from bec_widgets.widgets.motor_map.motor_map_dialog.motor_map_settings import MotorMapDialog
from bec_widgets.widgets.motor_map.motor_map_dialog.motor_map_toolbar import (
ConnectAction,
DeviceSelectionAction,
ResetHistoryAction,
SettingsAction,
)
from bec_widgets.widgets.toolbar import ModularToolBar
class BECMotorMapWidget(BECConnector, QWidget):
USER_ACCESS = [
"change_motors",
"set_max_points",
"set_precision",
"set_num_dim_points",
"set_background_value",
"set_scatter_size",
"get_data",
"reset_history",
]
def __init__(
self,
parent: QWidget | None = None,
config: MotorMapConfig | None = None,
client=None,
gui_id: str | None = None,
) -> None:
if config is None:
config = MotorMapConfig(widget_class=self.__class__.__name__)
else:
if isinstance(config, dict):
config = MotorMapConfig(**config)
super().__init__(client=client, gui_id=gui_id)
QWidget.__init__(self, parent)
self.layout = QVBoxLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
self.fig = BECFigure()
self.toolbar = ModularToolBar(
actions={
"motor_x": DeviceSelectionAction("Motor X:"),
"motor_y": DeviceSelectionAction("Motor Y:"),
"connect": ConnectAction(),
"history": ResetHistoryAction(),
"config": SettingsAction(),
},
target_widget=self,
)
self.layout.addWidget(self.toolbar)
self.layout.addWidget(self.fig)
self.map = self.fig.motor_map()
self.map.apply_config(config)
self._hook_actions()
self.config = config
def _hook_actions(self):
self.toolbar.widgets["connect"].action.triggered.connect(self._action_motors)
self.toolbar.widgets["config"].action.triggered.connect(self.show_settings)
self.toolbar.widgets["history"].action.triggered.connect(self.reset_history)
def _action_motors(self):
toolbar_x = self.toolbar.widgets["motor_x"].device_combobox
toolbar_y = self.toolbar.widgets["motor_y"].device_combobox
motor_x = toolbar_x.currentText()
motor_y = toolbar_y.currentText()
self.change_motors(motor_x, motor_y, None, None, True)
toolbar_x.setStyleSheet("QComboBox {{ background-color: " "; }}")
toolbar_y.setStyleSheet("QComboBox {{ background-color: " "; }}")
def show_settings(self) -> None:
dialog = MotorMapDialog(self, target_widget=self)
dialog.exec()
###################################
# User Access Methods from MotorMap
###################################
def change_motors(
self,
motor_x: str,
motor_y: str,
motor_x_entry: str = None,
motor_y_entry: str = None,
validate_bec: bool = True,
) -> None:
"""
Change the active motors for the plot.
Args:
motor_x(str): Motor name for the X axis.
motor_y(str): Motor name for the Y axis.
motor_x_entry(str): Motor entry for the X axis.
motor_y_entry(str): Motor entry for the Y axis.
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
"""
self.map.change_motors(motor_x, motor_y, motor_x_entry, motor_y_entry, validate_bec)
def get_data(self) -> dict:
"""
Get the data of the motor map.
Returns:
dict: Data of the motor map.
"""
return self.map.get_data()
def reset_history(self) -> None:
"""
Reset the history of the motor map.
"""
self.map.reset_history()
def set_color(self, color: str | tuple):
"""
Set the color of the motor map.
Args:
color(str, tuple): Color to set.
"""
self.map.set_color(color)
def set_max_points(self, max_points: int) -> None:
"""
Set the maximum number of points to display on the motor map.
Args:
max_points(int): Maximum number of points to display.
"""
self.map.set_max_points(max_points)
def set_precision(self, precision: int) -> None:
"""
Set the precision of the motor map.
Args:
precision(int): Precision to set.
"""
self.map.set_precision(precision)
def set_num_dim_points(self, num_dim_points: int) -> None:
"""
Set the number of points to display on the motor map.
Args:
num_dim_points(int): Number of points to display.
"""
self.map.set_num_dim_points(num_dim_points)
def set_background_value(self, background_value: int) -> None:
"""
Set the background value of the motor map.
Args:
background_value(int): Background value of the motor map.
"""
self.map.set_background_value(background_value)
def set_scatter_size(self, scatter_size: int) -> None:
"""
Set the scatter size of the motor map.
Args:
scatter_size(int): Scatter size of the motor map.
"""
self.map.set_scatter_size(scatter_size)
if __name__ == "__main__":
import sys
from PySide6.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = BECMotorMapWidget()
widget.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.motor_map.bec_motor_map_widget_plugin import BECMotorMapWidgetPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(BECMotorMapWidgetPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -1,123 +1,70 @@
from abc import ABC, abstractmethod
from collections import defaultdict
# pylint: disable=no-name-in-module
from qtpy.QtCore import QSize, QTimer
from qtpy.QtCore import QSize
from qtpy.QtGui import QAction
from qtpy.QtWidgets import QApplication, QStyle, QToolBar, QWidget
from qtpy.QtWidgets import QHBoxLayout, QLabel, QSpinBox, QToolBar, QWidget
class ToolBarAction(ABC):
"""Abstract base class for action creators for the toolbar."""
@abstractmethod
def create(self, target: QWidget):
"""Creates and returns an action to be added to a toolbar.
This method must be implemented by subclasses.
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
"""Adds an action or widget to a toolbar.
Args:
target (QWidget): The widget that the action will target.
Returns:
QAction: The action created for the toolbar.
toolbar (QToolBar): The toolbar to add the action or widget to.
target (QWidget): The target widget for the action.
"""
class OpenFileAction: # (ToolBarAction):
"""Action creator for the 'Open File' action in the toolbar."""
class ColumnAdjustAction(ToolBarAction):
"""Toolbar spinbox to adjust number of columns in the plot layout"""
def create(self, target: QWidget):
"""Creates an 'Open File' action for the toolbar.
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
"""Creates a access history button for the toolbar.
Args:
target (QWidget): The widget that the 'Open File' action will be targeted.
toolbar (QToolBar): The toolbar to add the action to.
target (QWidget): The widget that the 'Access Scan History' action will be targeted.
Returns:
QAction: The 'Open File' action created for the toolbar.
QAction: The 'Access Scan History' action created for the toolbar.
"""
icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogOpenButton)
action = QAction(icon, "Open File", target)
# action = QAction("Open File", target)
action.triggered.connect(target.open_file)
return action
widget = QWidget()
layout = QHBoxLayout(widget)
label = QLabel("Columns:")
spin_box = QSpinBox()
spin_box.setMinimum(1) # Set minimum value
spin_box.setMaximum(10) # Set maximum value
spin_box.setValue(target.get_column_count()) # Initial value
spin_box.valueChanged.connect(lambda value: target.set_column_count(value))
class SaveFileAction:
"""Action creator for the 'Save File' action in the toolbar."""
def create(self, target):
"""Creates a 'Save File' action for the toolbar.
Args:
target (QWidget): The widget that the 'Save File' action will be targeted.
Returns:
QAction: The 'Save File' action created for the toolbar.
"""
icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton)
action = QAction(icon, "Save File", target)
# action = QAction("Save File", target)
action.triggered.connect(target.save_file)
return action
class RunScriptAction:
"""Action creator for the 'Run Script' action in the toolbar."""
def create(self, target):
"""Creates a 'Run Script' action for the toolbar.
Args:
target (QWidget): The widget that the 'Run Script' action will be targeted.
Returns:
QAction: The 'Run Script' action created for the toolbar.
"""
icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)
action = QAction(icon, "Run Script", target)
# action = QAction("Run Script", target)
action.triggered.connect(target.run_script)
return action
layout.addWidget(label)
layout.addWidget(spin_box)
toolbar.addWidget(widget)
class ModularToolBar(QToolBar):
"""Modular toolbar with optional automatic initialization.
Args:
parent (QWidget, optional): The parent widget of the toolbar. Defaults to None.
auto_init (bool, optional): If True, automatically populates the toolbar based on the parent widget.
actions (list[ToolBarAction], optional): A list of action creators to populate the toolbar. Defaults to None.
target_widget (QWidget, optional): The widget that the actions will target. Defaults to None.
color (str, optional): The background color of the toolbar. Defaults to "black".
"""
def __init__(self, parent=None, auto_init=True):
def __init__(self, parent=None, actions=None, target_widget=None, color: str = "black"):
super().__init__(parent)
self.auto_init = auto_init
self.handler = {
"BECEditor": [OpenFileAction(), SaveFileAction(), RunScriptAction()],
# BECMonitor: [SomeOtherAction(), AnotherAction()], # Example for another widget
}
self.setStyleSheet("QToolBar { background: transparent; }")
# Set the icon size for the toolbar
self.setIconSize(QSize(20, 20))
if self.auto_init:
QTimer.singleShot(0, self.auto_detect_and_populate)
self.widgets = defaultdict(dict)
self.set_background_color(color)
def auto_detect_and_populate(self):
"""Automatically detects the parent widget and populates the toolbar with relevant actions."""
if not self.auto_init:
return
if actions is not None and target_widget is not None:
self.populate_toolbar(actions, target_widget)
parent_widget = self.parent()
if parent_widget is None:
return
parent_widget_class_name = type(parent_widget).__name__
for widget_type_name, actions in self.handler.items():
if parent_widget_class_name == widget_type_name:
self.populate_toolbar(actions, parent_widget)
return
def populate_toolbar(self, actions, target_widget):
def populate_toolbar(self, actions: dict, target_widget):
"""Populates the toolbar with a set of actions.
Args:
@@ -125,20 +72,13 @@ class ModularToolBar(QToolBar):
target_widget (QWidget): The widget that the actions will target.
"""
self.clear()
for action_creator in actions:
action = action_creator.create(target_widget)
self.addAction(action)
for action_id, action in actions.items():
action.add_to_toolbar(self, target_widget)
self.widgets[action_id] = action
def set_manual_actions(self, actions, target_widget):
"""Manually sets the actions for the toolbar.
Args:
actions (list[QAction or ToolBarAction]): A list of actions or action creators to populate the toolbar.
target_widget (QWidget): The widget that the actions will target.
"""
self.clear()
for action in actions:
if isinstance(action, QAction):
self.addAction(action)
elif isinstance(action, ToolBarAction):
self.addAction(action.create(target_widget))
def set_background_color(self, color: str):
self.setStyleSheet(f"QToolBar {{ background: {color}; }}")
self.setIconSize(QSize(20, 20))
self.setMovable(False)
self.setFloatable(False)
self.setContentsMargins(0, 0, 0, 0)