mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
fix(plot_framework): all widgets, popups and side menus cleanups adjusted
This commit is contained in:
@ -14,10 +14,8 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
from bec_lib import messages
|
from bec_lib import messages
|
||||||
from bec_lib.connector import MessageObject
|
from bec_lib.connector import MessageObject
|
||||||
|
|
||||||
from bec_widgets.cli.client_utils import BECGuiClient
|
|
||||||
|
|
||||||
|
|
||||||
import bec_widgets.cli.client as client
|
import bec_widgets.cli.client as client
|
||||||
|
from bec_widgets.cli.client_utils import BECGuiClient
|
||||||
else:
|
else:
|
||||||
client = lazy_import("bec_widgets.cli.client") # avoid circular import
|
client = lazy_import("bec_widgets.cli.client") # avoid circular import
|
||||||
messages = lazy_import("bec_lib.messages")
|
messages = lazy_import("bec_lib.messages")
|
||||||
|
@ -77,13 +77,6 @@ class BECWidget(BECConnector):
|
|||||||
logger.debug(f"Subscribing to theme updates for {self.__class__.__name__}")
|
logger.debug(f"Subscribing to theme updates for {self.__class__.__name__}")
|
||||||
self._connect_to_theme_change()
|
self._connect_to_theme_change()
|
||||||
|
|
||||||
def _ensure_bec_app(self):
|
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from bec_widgets.utils.bec_qapp import BECApplication
|
|
||||||
|
|
||||||
app = BECApplication.from_qapplication()
|
|
||||||
return app
|
|
||||||
|
|
||||||
def _connect_to_theme_change(self):
|
def _connect_to_theme_change(self):
|
||||||
"""Connect to the theme change signal."""
|
"""Connect to the theme change signal."""
|
||||||
qapp = QApplication.instance()
|
qapp = QApplication.instance()
|
||||||
@ -113,6 +106,7 @@ class BECWidget(BECConnector):
|
|||||||
"""Cleanup the widget."""
|
"""Cleanup the widget."""
|
||||||
with RPCRegister.delayed_broadcast():
|
with RPCRegister.delayed_broadcast():
|
||||||
# All widgets need to call super().cleanup() in their cleanup method
|
# All widgets need to call super().cleanup() in their cleanup method
|
||||||
|
logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
|
||||||
self.rpc_register.remove_rpc(self)
|
self.rpc_register.remove_rpc(self)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from PySide6.QtGui import QCloseEvent
|
||||||
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
class SettingWidget(QWidget):
|
class SettingWidget(QWidget):
|
||||||
"""
|
"""
|
||||||
@ -37,6 +41,15 @@ class SettingWidget(QWidget):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleanup the settings widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def closeEvent(self, event: QCloseEvent) -> None:
|
||||||
|
self.cleanup()
|
||||||
|
return super().closeEvent(event)
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
class SettingsDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
@ -99,8 +112,17 @@ class SettingsDialog(QDialog):
|
|||||||
Accept the changes made in the settings widget and close the dialog.
|
Accept the changes made in the settings widget and close the dialog.
|
||||||
"""
|
"""
|
||||||
self.widget.accept_changes()
|
self.widget.accept_changes()
|
||||||
|
self.cleanup()
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def reject(self):
|
||||||
|
"""
|
||||||
|
Reject the changes made in the settings widget and close the dialog.
|
||||||
|
"""
|
||||||
|
self.cleanup()
|
||||||
|
super().reject()
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def apply_changes(self):
|
def apply_changes(self):
|
||||||
"""
|
"""
|
||||||
@ -114,7 +136,10 @@ class SettingsDialog(QDialog):
|
|||||||
"""
|
"""
|
||||||
self.button_box.close()
|
self.button_box.close()
|
||||||
self.button_box.deleteLater()
|
self.button_box.deleteLater()
|
||||||
|
self.widget.close()
|
||||||
|
self.widget.deleteLater()
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
|
logger.info("Closing settings dialog")
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
@ -133,7 +133,7 @@ class Image(PlotBase):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||||
)
|
)
|
||||||
self._main_image.parent_image = self
|
self._main_image = ImageItem(parent_image=self, parent_id=self.gui_id)
|
||||||
|
|
||||||
self.plot_item.addItem(self._main_image)
|
self.plot_item.addItem(self._main_image)
|
||||||
self.scan_id = None
|
self.scan_id = None
|
||||||
@ -913,10 +913,14 @@ class Image(PlotBase):
|
|||||||
"""
|
"""
|
||||||
Disconnect the image update signals and clean up the image.
|
Disconnect the image update signals and clean up the image.
|
||||||
"""
|
"""
|
||||||
|
# Main Image cleanup
|
||||||
if self._main_image.config.monitor is not None:
|
if self._main_image.config.monitor is not None:
|
||||||
self.disconnect_monitor(self._main_image.config.monitor)
|
self.disconnect_monitor(self._main_image.config.monitor)
|
||||||
self._main_image.config.monitor = None
|
self._main_image.config.monitor = None
|
||||||
|
self.plot_item.removeItem(self._main_image)
|
||||||
|
self._main_image = None
|
||||||
|
|
||||||
|
# Colorbar Cleanup
|
||||||
if self._color_bar:
|
if self._color_bar:
|
||||||
if self.config.color_bar == "full":
|
if self.config.color_bar == "full":
|
||||||
self.cleanup_histogram_lut_item(self._color_bar)
|
self.cleanup_histogram_lut_item(self._color_bar)
|
||||||
@ -925,6 +929,10 @@ class Image(PlotBase):
|
|||||||
self._color_bar.deleteLater()
|
self._color_bar.deleteLater()
|
||||||
self._color_bar = None
|
self._color_bar = None
|
||||||
|
|
||||||
|
# Toolbar cleanup
|
||||||
|
self.toolbar.widgets["monitor"].widget.close()
|
||||||
|
self.toolbar.widgets["monitor"].widget.deleteLater()
|
||||||
|
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,7 +28,9 @@ class MonitorSelectionToolbarBundle(ToolbarBundle):
|
|||||||
|
|
||||||
# 1) Device combo box
|
# 1) Device combo box
|
||||||
self.device_combo_box = DeviceComboBox(
|
self.device_combo_box = DeviceComboBox(
|
||||||
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=[ReadoutPriority.ASYNC]
|
parent=self.target_widget,
|
||||||
|
device_filter=BECDeviceFilter.DEVICE,
|
||||||
|
readout_priority_filter=[ReadoutPriority.ASYNC],
|
||||||
)
|
)
|
||||||
self.device_combo_box.addItem("", None)
|
self.device_combo_box.addItem("", None)
|
||||||
self.device_combo_box.setCurrentText("")
|
self.device_combo_box.setCurrentText("")
|
||||||
|
@ -791,6 +791,10 @@ class MotorMap(PlotBase):
|
|||||||
data = {"x": self._buffer["x"], "y": self._buffer["y"]}
|
data = {"x": self._buffer["x"], "y": self._buffer["y"]}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.motor_selection_bundle.cleanup()
|
||||||
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
class DemoApp(QMainWindow): # pragma: no cover
|
class DemoApp(QMainWindow): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -27,14 +27,18 @@ class MotorSelectionToolbarBundle(ToolbarBundle):
|
|||||||
self.target_widget = target_widget
|
self.target_widget = target_widget
|
||||||
|
|
||||||
# Motor X
|
# Motor X
|
||||||
self.motor_x = DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
|
self.motor_x = DeviceComboBox(
|
||||||
|
parent=self.target_widget, device_filter=[BECDeviceFilter.POSITIONER]
|
||||||
|
)
|
||||||
self.motor_x.addItem("", None)
|
self.motor_x.addItem("", None)
|
||||||
self.motor_x.setCurrentText("")
|
self.motor_x.setCurrentText("")
|
||||||
self.motor_x.setToolTip("Select Motor X")
|
self.motor_x.setToolTip("Select Motor X")
|
||||||
self.motor_x.setItemDelegate(NoCheckDelegate(self.motor_x))
|
self.motor_x.setItemDelegate(NoCheckDelegate(self.motor_x))
|
||||||
|
|
||||||
# Motor X
|
# Motor X
|
||||||
self.motor_y = DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
|
self.motor_y = DeviceComboBox(
|
||||||
|
parent=self.target_widget, device_filter=[BECDeviceFilter.POSITIONER]
|
||||||
|
)
|
||||||
self.motor_y.addItem("", None)
|
self.motor_y.addItem("", None)
|
||||||
self.motor_y.setCurrentText("")
|
self.motor_y.setCurrentText("")
|
||||||
self.motor_y.setToolTip("Select Motor Y")
|
self.motor_y.setToolTip("Select Motor Y")
|
||||||
@ -58,3 +62,9 @@ class MotorSelectionToolbarBundle(ToolbarBundle):
|
|||||||
or motor_y != self.target_widget.config.y_motor.name
|
or motor_y != self.target_widget.config.y_motor.name
|
||||||
):
|
):
|
||||||
self.target_widget.map(motor_x, motor_y)
|
self.target_widget.map(motor_x, motor_y)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.motor_x.close()
|
||||||
|
self.motor_x.deleteLater()
|
||||||
|
self.motor_y.close()
|
||||||
|
self.motor_y.deleteLater()
|
||||||
|
@ -496,3 +496,9 @@ class MultiWaveform(PlotBase):
|
|||||||
self.monitor_selection_bundle.colormap_widget.blockSignals(True)
|
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.colormap = self.config.color_palette
|
||||||
self.monitor_selection_bundle.colormap_widget.blockSignals(False)
|
self.monitor_selection_bundle.colormap_widget.blockSignals(False)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self._disconnect_monitor()
|
||||||
|
self.clear_curves()
|
||||||
|
self.monitor_selection_bundle.cleanup()
|
||||||
|
super().cleanup()
|
||||||
|
@ -58,3 +58,10 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
|||||||
@SafeSlot(str)
|
@SafeSlot(str)
|
||||||
def change_colormap(self, colormap: str):
|
def change_colormap(self, colormap: str):
|
||||||
self.target_widget.color_palette = colormap
|
self.target_widget.color_palette = colormap
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleanup the toolbar bundle.
|
||||||
|
"""
|
||||||
|
self.monitor.close()
|
||||||
|
self.monitor.deleteLater()
|
||||||
|
@ -69,7 +69,7 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
config: ConnectionConfig | None = None,
|
config: ConnectionConfig | None = None,
|
||||||
client=None,
|
client=None,
|
||||||
gui_id: str | None = None,
|
gui_id: str | None = None,
|
||||||
popups: bool = False,
|
popups: bool = True,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
if config is None:
|
if config is None:
|
||||||
@ -170,6 +170,9 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
# hide some options by default
|
# hide some options by default
|
||||||
self.toolbar.toggle_action_visibility("fps_monitor", False)
|
self.toolbar.toggle_action_visibility("fps_monitor", False)
|
||||||
|
|
||||||
|
# Get default viewbox state
|
||||||
|
self.mouse_bundle.get_viewbox_mode()
|
||||||
|
|
||||||
def add_side_menus(self):
|
def add_side_menus(self):
|
||||||
"""Adds multiple menus to the side panel."""
|
"""Adds multiple menus to the side panel."""
|
||||||
# Setting Axis Widget
|
# Setting Axis Widget
|
||||||
|
@ -107,14 +107,15 @@ class ScatterWaveform(PlotBase):
|
|||||||
):
|
):
|
||||||
if config is None:
|
if config is None:
|
||||||
config = ScatterWaveformConfig(widget_class=self.__class__.__name__)
|
config = ScatterWaveformConfig(widget_class=self.__class__.__name__)
|
||||||
|
# Specific GUI elements
|
||||||
|
self.scatter_dialog = None
|
||||||
|
self.scatter_curve_settings = None
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||||
)
|
)
|
||||||
self._main_curve = ScatterCurve(parent_item=self)
|
self._main_curve = ScatterCurve(parent_item=self)
|
||||||
|
|
||||||
# Specific GUI elements
|
|
||||||
self.scatter_dialog = None
|
|
||||||
|
|
||||||
# Scan Data
|
# Scan Data
|
||||||
self.old_scan_id = None
|
self.old_scan_id = None
|
||||||
self.scan_id = None
|
self.scan_id = None
|
||||||
@ -128,24 +129,26 @@ class ScatterWaveform(PlotBase):
|
|||||||
self.proxy_update_sync = pg.SignalProxy(
|
self.proxy_update_sync = pg.SignalProxy(
|
||||||
self.sync_signal_update, rateLimit=25, slot=self.update_sync_curves
|
self.sync_signal_update, rateLimit=25, slot=self.update_sync_curves
|
||||||
)
|
)
|
||||||
|
|
||||||
self._init_scatter_curve_settings()
|
self._init_scatter_curve_settings()
|
||||||
self.update_with_scan_history(-1)
|
self.update_with_scan_history(-1)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific GUI interactions
|
# Widget Specific GUI interactions
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
def _init_scatter_curve_settings(self):
|
def _init_scatter_curve_settings(self):
|
||||||
"""
|
"""
|
||||||
Initialize the scatter curve settings menu.
|
Initialize the scatter curve settings menu.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scatter_curve_settings = ScatterCurveSettings(parent=self, target_widget=self, popup=False)
|
self.scatter_curve_settings = ScatterCurveSettings(
|
||||||
|
parent=self, target_widget=self, popup=False
|
||||||
|
)
|
||||||
self.side_panel.add_menu(
|
self.side_panel.add_menu(
|
||||||
action_id="scatter_curve",
|
action_id="scatter_curve",
|
||||||
icon_name="scatter_plot",
|
icon_name="scatter_plot",
|
||||||
tooltip="Show Scatter Curve Settings",
|
tooltip="Show Scatter Curve Settings",
|
||||||
widget=scatter_curve_settings,
|
widget=self.scatter_curve_settings,
|
||||||
title="Scatter Curve Settings",
|
title="Scatter Curve Settings",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -461,17 +464,30 @@ class ScatterWaveform(PlotBase):
|
|||||||
logger.warning(f"Neither scan_id or scan_number was provided, fetching the latest scan")
|
logger.warning(f"Neither scan_id or scan_number was provided, fetching the latest scan")
|
||||||
scan_index = -1
|
scan_index = -1
|
||||||
|
|
||||||
if scan_index is not None:
|
if scan_index is None:
|
||||||
if len(self.client.history) == 0:
|
|
||||||
logger.info("No scans executed so far. Skipping scan history update.")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.scan_item = self.client.history[scan_index]
|
|
||||||
metadata = self.scan_item.metadata
|
|
||||||
self.scan_id = metadata["bec"]["scan_id"]
|
|
||||||
else:
|
|
||||||
self.scan_id = scan_id
|
self.scan_id = scan_id
|
||||||
self.scan_item = self.client.history.get_by_scan_id(scan_id)
|
self.scan_item = self.client.history.get_by_scan_id(scan_id)
|
||||||
|
self.sync_signal_update.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
if scan_index == -1:
|
||||||
|
scan_item = self.client.queue.scan_storage.current_scan
|
||||||
|
if scan_item is not None:
|
||||||
|
if scan_item.status_message is None:
|
||||||
|
logger.warning(f"Scan item with {scan_item.scan_id} has no status message.")
|
||||||
|
return
|
||||||
|
self.scan_item = scan_item
|
||||||
|
self.scan_id = scan_item.scan_id
|
||||||
|
self.sync_signal_update.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(self.client.history) == 0:
|
||||||
|
logger.info("No scans executed so far. Skipping scan history update.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.scan_item = self.client.history[scan_index]
|
||||||
|
metadata = self.scan_item.metadata
|
||||||
|
self.scan_id = metadata["bec"]["scan_id"]
|
||||||
|
|
||||||
self.sync_signal_update.emit()
|
self.sync_signal_update.emit()
|
||||||
|
|
||||||
@ -487,6 +503,22 @@ class ScatterWaveform(PlotBase):
|
|||||||
self.crosshair.clear_markers()
|
self.crosshair.clear_markers()
|
||||||
self._main_curve.clear()
|
self._main_curve.clear()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleanup the widget and disconnect all signals.
|
||||||
|
"""
|
||||||
|
if self.scatter_dialog is not None:
|
||||||
|
self.scatter_dialog.close()
|
||||||
|
self.scatter_dialog.deleteLater()
|
||||||
|
if self.scatter_curve_settings is not None:
|
||||||
|
self.scatter_curve_settings.cleanup()
|
||||||
|
print("scatter_curve_settings celanup called")
|
||||||
|
self.bec_dispatcher.disconnect_slot(self.on_scan_status, MessageEndpoints.scan_status())
|
||||||
|
self.bec_dispatcher.disconnect_slot(self.on_scan_progress, MessageEndpoints.scan_progress())
|
||||||
|
self.plot_item.removeItem(self._main_curve)
|
||||||
|
self._main_curve = None
|
||||||
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
class DemoApp(QMainWindow): # pragma: no cover
|
class DemoApp(QMainWindow): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -122,3 +122,17 @@ class ScatterCurveSettings(SettingWidget):
|
|||||||
color_map=color_map,
|
color_map=color_map,
|
||||||
validate_bec=validate_bec,
|
validate_bec=validate_bec,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.ui.x_name.close()
|
||||||
|
self.ui.x_name.deleteLater()
|
||||||
|
self.ui.x_entry.close()
|
||||||
|
self.ui.x_entry.deleteLater()
|
||||||
|
self.ui.y_name.close()
|
||||||
|
self.ui.y_name.deleteLater()
|
||||||
|
self.ui.y_entry.close()
|
||||||
|
self.ui.y_entry.deleteLater()
|
||||||
|
self.ui.z_name.close()
|
||||||
|
self.ui.z_name.deleteLater()
|
||||||
|
self.ui.z_entry.close()
|
||||||
|
self.ui.z_entry.deleteLater()
|
||||||
|
@ -56,9 +56,6 @@ class MouseInteractionToolbarBundle(ToolbarBundle):
|
|||||||
rect.action.toggled.connect(self.enable_mouse_rectangle_mode)
|
rect.action.toggled.connect(self.enable_mouse_rectangle_mode)
|
||||||
auto.action.triggered.connect(self.autorange_plot)
|
auto.action.triggered.connect(self.autorange_plot)
|
||||||
|
|
||||||
# Give some time to check the state
|
|
||||||
QTimer.singleShot(10, self.get_viewbox_mode)
|
|
||||||
|
|
||||||
def get_viewbox_mode(self):
|
def get_viewbox_mode(self):
|
||||||
"""
|
"""
|
||||||
Returns the current interaction mode of a PyQtGraph ViewBox and sets the corresponding action.
|
Returns the current interaction mode of a PyQtGraph ViewBox and sets the corresponding action.
|
||||||
|
@ -93,7 +93,8 @@ class Curve(BECConnector, pg.PlotDataItem):
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.parent_item = parent_item
|
self.parent_item = parent_item
|
||||||
self.parent_id = self.parent_item.gui_id
|
self.parent_id = self.parent_item.gui_id
|
||||||
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
|
object_name = name.replace("-", "_") if name else None
|
||||||
|
super().__init__(name=name, object_name=object_name, config=config, gui_id=gui_id, **kwargs)
|
||||||
|
|
||||||
self.apply_config()
|
self.apply_config()
|
||||||
self.dap_params = None
|
self.dap_params = None
|
||||||
|
@ -28,7 +28,6 @@ class CurveSetting(SettingWidget):
|
|||||||
def __init__(self, parent=None, target_widget: Waveform = None, *args, **kwargs):
|
def __init__(self, parent=None, target_widget: Waveform = None, *args, **kwargs):
|
||||||
super().__init__(parent=parent, *args, **kwargs)
|
super().__init__(parent=parent, *args, **kwargs)
|
||||||
self.setProperty("skip_settings", True)
|
self.setProperty("skip_settings", True)
|
||||||
self.setObjectName("CurveSetting")
|
|
||||||
self.target_widget = target_widget
|
self.target_widget = target_widget
|
||||||
|
|
||||||
self.layout = QVBoxLayout(self)
|
self.layout = QVBoxLayout(self)
|
||||||
@ -52,7 +51,7 @@ class CurveSetting(SettingWidget):
|
|||||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
|
||||||
self.device_x_label = QLabel("Device")
|
self.device_x_label = QLabel("Device")
|
||||||
self.device_x = DeviceLineEdit()
|
self.device_x = DeviceLineEdit(parent=self)
|
||||||
|
|
||||||
self.signal_x_label = QLabel("Signal")
|
self.signal_x_label = QLabel("Signal")
|
||||||
self.signal_x = QLineEdit()
|
self.signal_x = QLineEdit()
|
||||||
@ -117,3 +116,10 @@ class CurveSetting(SettingWidget):
|
|||||||
"""Refresh the curve tree and the x axis combo box in the case Waveform is modified from rpc."""
|
"""Refresh the curve tree and the x axis combo box in the case Waveform is modified from rpc."""
|
||||||
self.curve_manager.refresh_from_waveform()
|
self.curve_manager.refresh_from_waveform()
|
||||||
self._get_x_mode_from_waveform()
|
self._get_x_mode_from_waveform()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Cleanup the widget."""
|
||||||
|
self.device_x.close()
|
||||||
|
self.device_x.deleteLater()
|
||||||
|
self.curve_manager.close()
|
||||||
|
self.curve_manager.deleteLater()
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
from bec_qthemes._icon.material_icons import material_icon
|
from bec_qthemes._icon.material_icons import material_icon
|
||||||
from qtpy.QtGui import QColor
|
from qtpy.QtGui import QColor
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
@ -36,6 +37,9 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||||
|
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
class ColorButton(QPushButton):
|
class ColorButton(QPushButton):
|
||||||
"""A QPushButton subclass that displays a color.
|
"""A QPushButton subclass that displays a color.
|
||||||
|
|
||||||
@ -110,11 +114,16 @@ class CurveRow(QTreeWidgetItem):
|
|||||||
self.curve_tree = tree.parent() # The CurveTree widget
|
self.curve_tree = tree.parent() # The CurveTree widget
|
||||||
self.curve_tree.all_items.append(self) # Track stable ordering
|
self.curve_tree.all_items.append(self) # Track stable ordering
|
||||||
|
|
||||||
|
# BEC user input
|
||||||
|
self.device_edit = None
|
||||||
|
self.dap_combo = None
|
||||||
|
|
||||||
self.dev = device_manager
|
self.dev = device_manager
|
||||||
self.entry_validator = EntryValidator(self.dev)
|
self.entry_validator = EntryValidator(self.dev)
|
||||||
|
|
||||||
self.config = config or CurveConfig()
|
self.config = config or CurveConfig()
|
||||||
self.source = self.config.source
|
self.source = self.config.source
|
||||||
|
self.dap_rows = []
|
||||||
|
|
||||||
# Create column 0 (Actions)
|
# Create column 0 (Actions)
|
||||||
self._init_actions()
|
self._init_actions()
|
||||||
@ -155,8 +164,8 @@ class CurveRow(QTreeWidgetItem):
|
|||||||
"""Create columns 1 and 2. For device rows, we have device/entry edits; for dap rows, label/model combo."""
|
"""Create columns 1 and 2. For device rows, we have device/entry edits; for dap rows, label/model combo."""
|
||||||
if self.source == "device":
|
if self.source == "device":
|
||||||
# Device row: columns 1..2 are device line edits
|
# Device row: columns 1..2 are device line edits
|
||||||
self.device_edit = DeviceLineEdit()
|
self.device_edit = DeviceLineEdit(parent=self.tree)
|
||||||
self.entry_edit = QLineEdit() # TODO in future will be signal line edit
|
self.entry_edit = QLineEdit(parent=self.tree) # TODO in future will be signal line edit
|
||||||
if self.config.signal:
|
if self.config.signal:
|
||||||
self.device_edit.setText(self.config.signal.name or "")
|
self.device_edit.setText(self.config.signal.name or "")
|
||||||
self.entry_edit.setText(self.config.signal.entry or "")
|
self.entry_edit.setText(self.config.signal.entry or "")
|
||||||
@ -168,7 +177,7 @@ class CurveRow(QTreeWidgetItem):
|
|||||||
# DAP row: column1= "Model" label, column2= DapComboBox
|
# DAP row: column1= "Model" label, column2= DapComboBox
|
||||||
self.label_widget = QLabel("Model")
|
self.label_widget = QLabel("Model")
|
||||||
self.tree.setItemWidget(self, 1, self.label_widget)
|
self.tree.setItemWidget(self, 1, self.label_widget)
|
||||||
self.dap_combo = DapComboBox()
|
self.dap_combo = DapComboBox(parent=self.tree)
|
||||||
self.dap_combo.populate_fit_model_combobox()
|
self.dap_combo.populate_fit_model_combobox()
|
||||||
# If config.signal has a dap
|
# If config.signal has a dap
|
||||||
if self.config.signal and self.config.signal.dap:
|
if self.config.signal and self.config.signal.dap:
|
||||||
@ -258,15 +267,31 @@ class CurveRow(QTreeWidgetItem):
|
|||||||
|
|
||||||
def remove_self(self):
|
def remove_self(self):
|
||||||
"""Remove this row from the tree and from the parent's item list."""
|
"""Remove this row from the tree and from the parent's item list."""
|
||||||
# If top-level:
|
# Recursively remove all child rows first
|
||||||
|
for i in reversed(range(self.childCount())):
|
||||||
|
child = self.child(i)
|
||||||
|
if isinstance(child, CurveRow):
|
||||||
|
child.remove_self()
|
||||||
|
|
||||||
|
# Clean up the widget references if they still exist
|
||||||
|
if getattr(self, "device_edit", None) is not None:
|
||||||
|
self.device_edit.close()
|
||||||
|
self.device_edit.deleteLater()
|
||||||
|
self.device_edit = None
|
||||||
|
|
||||||
|
if getattr(self, "dap_combo", None) is not None:
|
||||||
|
self.dap_combo.close()
|
||||||
|
self.dap_combo.deleteLater()
|
||||||
|
self.dap_combo = None
|
||||||
|
|
||||||
|
# Remove the item from the tree widget
|
||||||
index = self.tree.indexOfTopLevelItem(self)
|
index = self.tree.indexOfTopLevelItem(self)
|
||||||
if index != -1:
|
if index != -1:
|
||||||
self.tree.takeTopLevelItem(index)
|
self.tree.takeTopLevelItem(index)
|
||||||
else:
|
elif self.parent_item:
|
||||||
# If child item
|
self.parent_item.removeChild(self)
|
||||||
if self.parent_item:
|
|
||||||
self.parent_item.removeChild(self)
|
# Finally, remove self from the registration list in the curve tree
|
||||||
# Also remove from all_items
|
|
||||||
curve_tree = self.tree.parent()
|
curve_tree = self.tree.parent()
|
||||||
if self in curve_tree.all_items:
|
if self in curve_tree.all_items:
|
||||||
curve_tree.all_items.remove(self)
|
curve_tree.all_items.remove(self)
|
||||||
@ -320,6 +345,10 @@ class CurveRow(QTreeWidgetItem):
|
|||||||
|
|
||||||
return self.config.model_dump()
|
return self.config.model_dump()
|
||||||
|
|
||||||
|
def closeEvent(self, event) -> None:
|
||||||
|
logger.info(f"CurveRow closeEvent: {self.config.label}")
|
||||||
|
return super().closeEvent(event)
|
||||||
|
|
||||||
|
|
||||||
class CurveTree(BECWidget, QWidget):
|
class CurveTree(BECWidget, QWidget):
|
||||||
"""A tree widget that manages device and DAP curves."""
|
"""A tree widget that manages device and DAP curves."""
|
||||||
@ -535,3 +564,13 @@ class CurveTree(BECWidget, QWidget):
|
|||||||
for dap in dap_curves:
|
for dap in dap_curves:
|
||||||
if dap.config.parent_label == dev.config.label:
|
if dap.config.parent_label == dev.config.label:
|
||||||
CurveRow(self.tree, parent_item=dr, config=dap.config, device_manager=self.dev)
|
CurveRow(self.tree, parent_item=dr, config=dap.config, device_manager=self.dev)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Cleanup the widget."""
|
||||||
|
all_items = list(self.all_items)
|
||||||
|
for item in all_items:
|
||||||
|
item.remove_self()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self.cleanup()
|
||||||
|
return super().closeEvent(event)
|
||||||
|
@ -10,14 +10,7 @@ from bec_lib import bec_logger, messages
|
|||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from pydantic import Field, ValidationError, field_validator
|
from pydantic import Field, ValidationError, field_validator
|
||||||
from qtpy.QtCore import QTimer, Signal
|
from qtpy.QtCore import QTimer, Signal
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import QApplication, QDialog, QHBoxLayout, QMainWindow, QVBoxLayout, QWidget
|
||||||
QApplication,
|
|
||||||
QDialog,
|
|
||||||
QHBoxLayout,
|
|
||||||
QMainWindow,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
||||||
@ -320,6 +313,8 @@ class Waveform(PlotBase):
|
|||||||
"""
|
"""
|
||||||
Slot for when the axis settings dialog is closed.
|
Slot for when the axis settings dialog is closed.
|
||||||
"""
|
"""
|
||||||
|
self.curve_settings_dialog.close()
|
||||||
|
self.curve_settings_dialog.deleteLater()
|
||||||
self.curve_settings_dialog = None
|
self.curve_settings_dialog = None
|
||||||
self.toolbar.widgets["curve"].action.setChecked(False)
|
self.toolbar.widgets["curve"].action.setChecked(False)
|
||||||
|
|
||||||
@ -863,6 +858,9 @@ class Waveform(PlotBase):
|
|||||||
Clear all curves from the plot widget.
|
Clear all curves from the plot widget.
|
||||||
"""
|
"""
|
||||||
curve_list = self.curves
|
curve_list = self.curves
|
||||||
|
self._dap_curves = []
|
||||||
|
self._sync_curves = []
|
||||||
|
self._async_curves = []
|
||||||
for curve in curve_list:
|
for curve in curve_list:
|
||||||
self.remove_curve(curve.name())
|
self.remove_curve(curve.name())
|
||||||
if self.crosshair is not None:
|
if self.crosshair is not None:
|
||||||
@ -943,6 +941,7 @@ class Waveform(PlotBase):
|
|||||||
self.on_async_readback,
|
self.on_async_readback,
|
||||||
MessageEndpoints.device_async_readback(self.scan_id, curve.name()),
|
MessageEndpoints.device_async_readback(self.scan_id, curve.name()),
|
||||||
)
|
)
|
||||||
|
curve.rpc_register.remove_rpc(curve)
|
||||||
|
|
||||||
# Remove itself from the DAP summary only for side panels
|
# Remove itself from the DAP summary only for side panels
|
||||||
if (
|
if (
|
||||||
@ -1330,7 +1329,9 @@ class Waveform(PlotBase):
|
|||||||
# find the device curve
|
# find the device curve
|
||||||
parent_curve = self._find_curve_by_label(parent_label)
|
parent_curve = self._find_curve_by_label(parent_label)
|
||||||
if parent_curve is None:
|
if parent_curve is None:
|
||||||
logger.warning(f"No device curve found for DAP curve '{dap_curve.name()}'!")
|
logger.warning(
|
||||||
|
f"No device curve found for DAP curve '{dap_curve.name()}'!"
|
||||||
|
) # TODO triggerd when DAP curve is removed from the curve dialog, why?
|
||||||
continue
|
continue
|
||||||
|
|
||||||
x_data, y_data = parent_curve.get_data()
|
x_data, y_data = parent_curve.get_data()
|
||||||
@ -1565,7 +1566,6 @@ class Waveform(PlotBase):
|
|||||||
found_sync = True
|
found_sync = True
|
||||||
else:
|
else:
|
||||||
logger.warning("Device {dev_name} not found in readout priority list.")
|
logger.warning("Device {dev_name} not found in readout priority list.")
|
||||||
|
|
||||||
# Determine the mode of the scan
|
# Determine the mode of the scan
|
||||||
if found_async and found_sync:
|
if found_async and found_sync:
|
||||||
mode = "mixed"
|
mode = "mixed"
|
||||||
@ -1599,18 +1599,31 @@ class Waveform(PlotBase):
|
|||||||
logger.warning(f"Neither scan_id or scan_number was provided, fetching the latest scan")
|
logger.warning(f"Neither scan_id or scan_number was provided, fetching the latest scan")
|
||||||
scan_index = -1
|
scan_index = -1
|
||||||
|
|
||||||
if scan_index is not None:
|
if scan_index is None:
|
||||||
if len(self.client.history) == 0:
|
|
||||||
logger.info("No scans executed so far. Skipping scan history update.")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.scan_item = self.client.history[scan_index]
|
|
||||||
metadata = self.scan_item.metadata
|
|
||||||
self.scan_id = metadata["bec"]["scan_id"]
|
|
||||||
else:
|
|
||||||
self.scan_id = scan_id
|
self.scan_id = scan_id
|
||||||
self.scan_item = self.client.history.get_by_scan_id(scan_id)
|
self.scan_item = self.client.history.get_by_scan_id(scan_id)
|
||||||
|
self._emit_signal_update()
|
||||||
|
return
|
||||||
|
|
||||||
|
if scan_index == -1:
|
||||||
|
scan_item = self.client.queue.scan_storage.current_scan
|
||||||
|
if scan_item is not None:
|
||||||
|
self.scan_item = scan_item
|
||||||
|
self.scan_id = scan_item.scan_id
|
||||||
|
self._emit_signal_update()
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(self.client.history) == 0:
|
||||||
|
logger.info("No scans executed so far. Skipping scan history update.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.scan_item = self.client.history[scan_index]
|
||||||
|
metadata = self.scan_item.metadata
|
||||||
|
self.scan_id = metadata["bec"]["scan_id"]
|
||||||
|
|
||||||
|
self._emit_signal_update()
|
||||||
|
|
||||||
|
def _emit_signal_update(self):
|
||||||
self._categorise_device_curves()
|
self._categorise_device_curves()
|
||||||
|
|
||||||
self.setup_dap_for_scan()
|
self.setup_dap_for_scan()
|
||||||
@ -1733,9 +1746,11 @@ class Waveform(PlotBase):
|
|||||||
self.clear_all()
|
self.clear_all()
|
||||||
if self.curve_settings_dialog is not None:
|
if self.curve_settings_dialog is not None:
|
||||||
self.curve_settings_dialog.close()
|
self.curve_settings_dialog.close()
|
||||||
|
self.curve_settings_dialog.deleteLater()
|
||||||
self.curve_settings_dialog = None
|
self.curve_settings_dialog = None
|
||||||
if self.dap_summary_dialog is not None:
|
if self.dap_summary_dialog is not None:
|
||||||
self.dap_summary_dialog.close()
|
self.dap_summary_dialog.close()
|
||||||
|
self.dap_summary_dialog.deleteLater()
|
||||||
self.dap_summary_dialog = None
|
self.dap_summary_dialog = None
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
|
@ -29,8 +29,6 @@ def test_axis_settings_init(axis_settings_fixture):
|
|||||||
assert axis_settings.layout.count() == 1 # scroll area
|
assert axis_settings.layout.count() == 1 # scroll area
|
||||||
# Check the target
|
# Check the target
|
||||||
assert axis_settings.target_widget == plot_base
|
assert axis_settings.target_widget == plot_base
|
||||||
# Check the object name
|
|
||||||
assert axis_settings.objectName() == "AxisSettings"
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_ui_updates_plot_base(axis_settings_fixture, qtbot):
|
def test_change_ui_updates_plot_base(axis_settings_fixture, qtbot):
|
||||||
|
@ -33,8 +33,6 @@ def test_curve_setting_init(curve_setting_fixture):
|
|||||||
"""
|
"""
|
||||||
curve_setting, wf = curve_setting_fixture
|
curve_setting, wf = curve_setting_fixture
|
||||||
|
|
||||||
# Basic checks
|
|
||||||
assert curve_setting.objectName() == "CurveSetting"
|
|
||||||
# The layout should be QVBoxLayout
|
# The layout should be QVBoxLayout
|
||||||
assert isinstance(curve_setting.layout, QVBoxLayout)
|
assert isinstance(curve_setting.layout, QVBoxLayout)
|
||||||
|
|
||||||
|
@ -134,12 +134,6 @@ def test_curve_access_pattern(qtbot, mocked_client):
|
|||||||
assert wf.get_curve(0) == c1
|
assert wf.get_curve(0) == c1
|
||||||
assert wf.get_curve(1) == c2
|
assert wf.get_curve(1) == c2
|
||||||
|
|
||||||
# Check that the curve is accessible by label
|
|
||||||
assert wf["bpm4i-bpm4i"] == c1
|
|
||||||
assert wf["bpm3a-bpm3a"] == c2
|
|
||||||
assert wf[0] == c1
|
|
||||||
assert wf[1] == c2
|
|
||||||
|
|
||||||
assert wf.curves[0] == c1
|
assert wf.curves[0] == c1
|
||||||
assert wf.curves[1] == c2
|
assert wf.curves[1] == c2
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user