diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py index ca5928b0..52d612e1 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py @@ -89,7 +89,7 @@ class LamniGuiTools: if self._lamnigui_is_missing("xeyegui"): self.lamnigui_remove_all_docks() self.xeyegui = self.gui.lamni.new( - "XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT + "OMNY_XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT ) # An existing xeyegui widget is reused across calls, so a Ctrl-C # that interrupted a prior run mid-RPC (e.g. mid set_live_view_signal) @@ -110,7 +110,7 @@ class LamniGuiTools: if self._lamnigui_is_missing("xeyegui"): self.lamnigui_remove_all_docks() self.xeyegui = self.gui.lamni.new( - "XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT + "OMNY_XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT ) self.xeyegui.switch_tab("fit") diff --git a/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/omny_general_tools.py b/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/omny_general_tools.py index 8f8de0dd..a93600da 100644 --- a/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/omny_general_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/omny_general_tools.py @@ -108,7 +108,7 @@ class OMNYTools: this method does not create it): gui.new("test", timeout=20) - console = gui.test.new("ConsoleButtonsWidget", object_name="console", timeout=20) + console = gui.test.new("z_ConsoleButtonsWidget", object_name="console", timeout=20) Then call, e.g. from an IPython session: diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py b/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py index d2a3910e..83685508 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py @@ -66,7 +66,7 @@ class flomniGuiTools: self.flomnigui_show_gui() if self._flomnigui_is_missing("xeyegui"): self.flomnigui_remove_all_docks() - self.xeyegui = self.gui.flomni.new("XRayEye", object_name="xrayeye") + self.xeyegui = self.gui.flomni.new("OMNY_XRayEye", object_name="xrayeye") # start live if not dev.cam_xeye.live_mode_enabled.get(): dev.cam_xeye.live_mode_enabled.put(True) @@ -76,7 +76,7 @@ class flomniGuiTools: self.flomnigui_show_gui() if self._flomnigui_is_missing("xeyegui"): self.flomnigui_remove_all_docks() - self.xeyegui = self.gui.flomni.new("XRayEye", object_name="xrayeye") + self.xeyegui = self.gui.flomni.new("OMNY_XRayEye", object_name="xrayeye") self.xeyegui.switch_tab("fit") def _flomnigui_check_attribute_not_exists(self, attribute_name): @@ -141,7 +141,7 @@ class flomniGuiTools: # board) so it needs its own stop_all_axes() call -- also # explained in ConsoleButtonsWidget's docstring. self.console = self.gui.flomni.new( - "ConsoleButtonsWidget", + "z_ConsoleButtonsWidget", object_name="console", where="bottom", hard_stop_device_name="ftransy", diff --git a/csaxs_bec/bec_widgets/widgets/client.py b/csaxs_bec/bec_widgets/widgets/client.py index 00522c64..abe03e3b 100644 --- a/csaxs_bec/bec_widgets/widgets/client.py +++ b/csaxs_bec/bec_widgets/widgets/client.py @@ -13,139 +13,41 @@ logger = bec_logger.logger _Widgets = { - "ConsoleButtonsWidget": "ConsoleButtonsWidget", + "DataViewer": "DataViewer", + "OMNY_SampleStorage": "OMNY_SampleStorage", + "OMNY_TomoParams": "OMNY_TomoParams", + "OMNY_XRayEye": "OMNY_XRayEye", "SAXSWidget": "SAXSWidget", - "SampleStorageWidget": "SampleStorageWidget", - "SlitControlWidget": "SlitControlWidget", - "TomoParamsWidget": "TomoParamsWidget", - "XRayEye": "XRayEye", + "SlitControl": "SlitControl", + "z_ConsoleButtonsWidget": "z_ConsoleButtonsWidget", } -class ConsoleButtonsWidget(RPCBase): - """Small Yes / No / Abort control widget, intended as a GUI replacement for""" +class DataViewer(RPCBase): + """Main widget of Data Viewer""" - _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.console_buttons.console_buttons" + _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.data_viewer.data_viewer" - @property @rpc_call - def message(self): + def remove(self): """ - None + Cleanup the BECConnector """ - @message.setter @rpc_call - def message(self): + def attach(self): """ None """ @rpc_call - def response(self) -> "str": + def detach(self): """ - Current button response: "yes", "no", or "" if no button has been - pressed (or since the last clear_response()). - - Note: intentionally a plain method, not @SafeProperty. SafeProperty - only becomes a real Qt property descriptor once a setter is chained - in the class body (see `message` above); a getter-only SafeProperty - is left as an unconverted internal wrapper object, which the RPC - generator then exposes as a callable stub anyway. Since `response` - has no RPC-facing setter, a plain method avoids that trap entirely - - same pattern XRayEye uses for its own read-only `active_roi`. - """ - - @rpc_call - def clear_response(self): - """ - None + Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. """ -class SAXSWidget(RPCBase): - """Widget for preparing SAXS measurement tables from an image ROI.""" - - _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.saxs_widget.saxs_widget" - - @rpc_call - def add_scanning_row( - self, - sample_name: "str | None" = None, - exposure_time: "float | None" = None, - fast_axis: "str | None" = None, - stepper_axis: "str | None" = None, - fast_step: "float | None" = None, - stepper_step: "float | None" = None, - comment: "str" = "", - ) -> "SAXSRow | None": - """ - None - """ - - @rpc_call - def add_tensor_rows( - self, - sample_name: "str | None" = None, - exposure_time: "float | None" = None, - fast_axis: "str | None" = None, - stepper_axis: "str | None" = None, - fast_step: "float | None" = None, - stepper_step: "float | None" = None, - comment: "str" = "", - roty: "float | None" = None, - rotx_values: "list[float] | None" = None, - roty_values: "list[float] | None" = None, - angle_pairs: "list[tuple[float, float]] | None" = None, - ) -> "list[SAXSRow]": - """ - None - """ - - @rpc_call - def clear_rows(self) -> "None": - """ - None - """ - - @rpc_call - def delete_selected_rows(self) -> "None": - """ - None - """ - - @rpc_call - def export_csv(self, file_name: "str | None" = None, selected: "bool" = False) -> "str | None": - """ - None - """ - - @rpc_call - def rows_as_dicts(self) -> "list[dict]": - """ - None - """ - - @rpc_call - def set_positioners(self, x_positioner: "str", y_positioner: "str") -> "None": - """ - None - """ - - @rpc_call - def mark_scan_result(self, sample_id_or_row, scan_number, success: "bool") -> "None": - """ - None - """ - - @rpc_call - def move_to_selected_row(self, index: "int") -> "None": - """ - None - """ - - -class SampleStorageWidget(RPCBase): +class OMNY_SampleStorage(RPCBase): """View and correct the FlOMNI sample-storage records.""" _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage" @@ -164,50 +66,7 @@ class SampleStorageWidget(RPCBase): """ -class SlitControlWidget(RPCBase): - """Interactive GUI for cSAXS slit center and size control.""" - - _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.slit_control.slit_control" - - @rpc_timeout(20) - @rpc_call - def set_slit(self, slit: "int"): - """ - Switch the widget to control a different slit (1–6). - """ - - @rpc_call - def move_center(self, direction: "str"): - """ - Move the slit center. direction: up / down / left / right. - """ - - @rpc_call - def move_size(self, direction: "str"): - """ - Adjust slit size. up/right = grow, down/left = shrink. - """ - - @rpc_call - def set_step(self, value: "float"): - """ - Set step size in mm (clamped to 0.005–2.0 mm). - """ - - @rpc_call - def double_step(self): - """ - None - """ - - @rpc_call - def halve_step(self): - """ - None - """ - - -class TomoParamsWidget(RPCBase): +class OMNY_TomoParams(RPCBase): """Interactive GUI for tomo scan parameter editing and queue management, for""" _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params" @@ -266,7 +125,7 @@ class TomoParamsWidget(RPCBase): """ -class XRayEye(RPCBase): +class OMNY_XRayEye(RPCBase): _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye" @rpc_call @@ -447,6 +306,131 @@ class XRayEye(RPCBase): """ +class SAXSWidget(RPCBase): + """Widget for preparing SAXS measurement tables from an image ROI.""" + + _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.saxs_widget.saxs_widget" + + @rpc_call + def add_scanning_row( + self, + sample_name: "str | None" = None, + exposure_time: "float | None" = None, + fast_axis: "str | None" = None, + stepper_axis: "str | None" = None, + fast_step: "float | None" = None, + stepper_step: "float | None" = None, + comment: "str" = "", + ) -> "SAXSRow | None": + """ + None + """ + + @rpc_call + def add_tensor_rows( + self, + sample_name: "str | None" = None, + exposure_time: "float | None" = None, + fast_axis: "str | None" = None, + stepper_axis: "str | None" = None, + fast_step: "float | None" = None, + stepper_step: "float | None" = None, + comment: "str" = "", + roty: "float | None" = None, + rotx_values: "list[float] | None" = None, + roty_values: "list[float] | None" = None, + angle_pairs: "list[tuple[float, float]] | None" = None, + ) -> "list[SAXSRow]": + """ + None + """ + + @rpc_call + def clear_rows(self) -> "None": + """ + None + """ + + @rpc_call + def delete_selected_rows(self) -> "None": + """ + None + """ + + @rpc_call + def export_csv(self, file_name: "str | None" = None, selected: "bool" = False) -> "str | None": + """ + None + """ + + @rpc_call + def rows_as_dicts(self) -> "list[dict]": + """ + None + """ + + @rpc_call + def set_positioners(self, x_positioner: "str", y_positioner: "str") -> "None": + """ + None + """ + + @rpc_call + def mark_scan_result(self, sample_id_or_row, scan_number, success: "bool") -> "None": + """ + None + """ + + @rpc_call + def move_to_selected_row(self, index: "int") -> "None": + """ + None + """ + + +class SlitControl(RPCBase): + """Interactive GUI for cSAXS slit center and size control.""" + + _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.slit_control.slit_control" + + @rpc_timeout(20) + @rpc_call + def set_slit(self, slit: "int"): + """ + Switch the widget to control a different slit (1–6). + """ + + @rpc_call + def move_center(self, direction: "str"): + """ + Move the slit center. direction: up / down / left / right. + """ + + @rpc_call + def move_size(self, direction: "str"): + """ + Adjust slit size. up/right = grow, down/left = shrink. + """ + + @rpc_call + def set_step(self, value: "float"): + """ + Set step size in mm (clamped to 0.005–2.0 mm). + """ + + @rpc_call + def double_step(self): + """ + None + """ + + @rpc_call + def halve_step(self): + """ + None + """ + + class XRayEye2DControl(RPCBase): _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye" @@ -467,3 +451,44 @@ class XRayEye2DControl(RPCBase): """ Detach the widget from its parent dock widget (if widget is in the dock), making it a floating widget. """ + + +class z_ConsoleButtonsWidget(RPCBase): + """Small Yes / No / Abort control widget, intended as a GUI replacement for""" + + _IMPORT_MODULE = "csaxs_bec.bec_widgets.widgets.console_buttons.console_buttons" + + @property + @rpc_call + def message(self): + """ + None + """ + + @message.setter + @rpc_call + def message(self): + """ + None + """ + + @rpc_call + def response(self) -> "str": + """ + Current button response: "yes", "no", or "" if no button has been + pressed (or since the last clear_response()). + + Note: intentionally a plain method, not @SafeProperty. SafeProperty + only becomes a real Qt property descriptor once a setter is chained + in the class body (see `message` above); a getter-only SafeProperty + is left as an unconverted internal wrapper object, which the RPC + generator then exposes as a callable stub anyway. Since `response` + has no RPC-facing setter, a plain method avoids that trap entirely - + same pattern OMNY_XRayEye uses for its own read-only `active_roi`. + """ + + @rpc_call + def clear_response(self): + """ + None + """ diff --git a/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py b/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py index 648f314b..c109557f 100644 --- a/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py +++ b/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons.py @@ -11,7 +11,7 @@ from qtpy.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidge logger = bec_logger.logger -class ConsoleButtonsWidget(BECWidget, QWidget): +class z_ConsoleButtonsWidget(BECWidget, QWidget): """ Small Yes / No / Abort control widget, intended as a GUI replacement for console prompts (e.g. ``OMNYTools.yesno()``) and as a hard motion-stop @@ -177,7 +177,7 @@ class ConsoleButtonsWidget(BECWidget, QWidget): try: self.queue.request_scan_abortion() except Exception: - logger.exception("ConsoleButtonsWidget: request_scan_abortion() failed") + logger.exception("z_ConsoleButtonsWidget: request_scan_abortion() failed") # 2) SIGINT before the hard stop -- gives a client-side polling loop # (if this is a self-abort) a head start to notice the interrupt @@ -187,7 +187,7 @@ class ConsoleButtonsWidget(BECWidget, QWidget): # looks like a BEC client -- it does not when the widget is # opened standalone from the launcher menu. if self._client_is_bec_process(): - logger.warning(f"ConsoleButtonsWidget: sending SIGINT to client pid {self._client_pid}") + logger.warning(f"z_ConsoleButtonsWidget: sending SIGINT to client pid {self._client_pid}") os.kill(self._client_pid, signal.SIGINT) # 3) Hard motion stop(s) -- not delayed by anything above: sending @@ -213,11 +213,11 @@ class ConsoleButtonsWidget(BECWidget, QWidget): device = None if device is None: return - logger.warning(f"ConsoleButtonsWidget: hard-stopping {device_name} ({method_name})") + logger.warning(f"z_ConsoleButtonsWidget: hard-stopping {device_name} ({method_name})") try: getattr(device.controller, method_name)() except Exception: - logger.exception(f"ConsoleButtonsWidget: hard motion stop of {device_name} failed") + logger.exception(f"z_ConsoleButtonsWidget: hard motion stop of {device_name} failed") @SafeProperty(str) def message(self): @@ -242,7 +242,7 @@ class ConsoleButtonsWidget(BECWidget, QWidget): is left as an unconverted internal wrapper object, which the RPC generator then exposes as a callable stub anyway. Since `response` has no RPC-facing setter, a plain method avoids that trap entirely - - same pattern XRayEye uses for its own read-only `active_roi`. + same pattern OMNY_XRayEye uses for its own read-only `active_roi`. """ return self._response diff --git a/csaxs_bec/bec_widgets/widgets/console_buttons/register_console_buttons_widget.py b/csaxs_bec/bec_widgets/widgets/console_buttons/register_z_console_buttons_widget.py similarity index 59% rename from csaxs_bec/bec_widgets/widgets/console_buttons/register_console_buttons_widget.py rename to csaxs_bec/bec_widgets/widgets/console_buttons/register_z_console_buttons_widget.py index a6115fdc..d1f05abc 100644 --- a/csaxs_bec/bec_widgets/widgets/console_buttons/register_console_buttons_widget.py +++ b/csaxs_bec/bec_widgets/widgets/console_buttons/register_z_console_buttons_widget.py @@ -6,9 +6,9 @@ def main(): # pragma: no cover return from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection - from csaxs_bec.bec_widgets.widgets.console_buttons.console_buttons_widget_plugin import ConsoleButtonsWidgetPlugin + from csaxs_bec.bec_widgets.widgets.console_buttons.z_console_buttons_widget_plugin import z_ConsoleButtonsWidgetPlugin - QPyDesignerCustomWidgetCollection.addCustomWidget(ConsoleButtonsWidgetPlugin()) + QPyDesignerCustomWidgetCollection.addCustomWidget(z_ConsoleButtonsWidgetPlugin()) if __name__ == "__main__": # pragma: no cover diff --git a/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons_widget.pyproject b/csaxs_bec/bec_widgets/widgets/console_buttons/z_console_buttons_widget.pyproject similarity index 100% rename from csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons_widget.pyproject rename to csaxs_bec/bec_widgets/widgets/console_buttons/z_console_buttons_widget.pyproject diff --git a/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons_widget_plugin.py b/csaxs_bec/bec_widgets/widgets/console_buttons/z_console_buttons_widget_plugin.py similarity index 72% rename from csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons_widget_plugin.py rename to csaxs_bec/bec_widgets/widgets/console_buttons/z_console_buttons_widget_plugin.py index ca1e7c9d..dc16e221 100644 --- a/csaxs_bec/bec_widgets/widgets/console_buttons/console_buttons_widget_plugin.py +++ b/csaxs_bec/bec_widgets/widgets/console_buttons/z_console_buttons_widget_plugin.py @@ -5,17 +5,17 @@ from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtWidgets import QWidget from bec_widgets.utils.bec_designer import designer_material_icon -from csaxs_bec.bec_widgets.widgets.console_buttons.console_buttons import ConsoleButtonsWidget +from csaxs_bec.bec_widgets.widgets.console_buttons.console_buttons import z_ConsoleButtonsWidget DOM_XML = """ - + """ -class ConsoleButtonsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover +class z_ConsoleButtonsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover def __init__(self): super().__init__() self._form_editor = None @@ -23,7 +23,7 @@ class ConsoleButtonsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no def createWidget(self, parent): if parent is None: return QWidget() - t = ConsoleButtonsWidget(parent) + t = z_ConsoleButtonsWidget(parent) return t def domXml(self): @@ -33,10 +33,10 @@ class ConsoleButtonsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no return "" def icon(self): - return designer_material_icon(ConsoleButtonsWidget.ICON_NAME) + return designer_material_icon(z_ConsoleButtonsWidget.ICON_NAME) def includeFile(self): - return "console_buttons_widget" + return "z_console_buttons_widget" def initialize(self, form_editor): self._form_editor = form_editor @@ -48,7 +48,7 @@ class ConsoleButtonsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no return self._form_editor is not None def name(self): - return "ConsoleButtonsWidget" + return "z_ConsoleButtonsWidget" def toolTip(self): return "" diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/__init__.py b/csaxs_bec/bec_widgets/widgets/data_viewer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer.py b/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer.py new file mode 100644 index 00000000..1fdd5157 --- /dev/null +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer.py @@ -0,0 +1,266 @@ +""" +Data Viewer: Custom BEC widget to view data from scans. +""" + +import os +import subprocess +import sys +from datetime import datetime +from typing import Literal + +from bec_lib import bec_logger +from bec_lib.endpoints import MessageEndpoints +from bec_widgets.utils.bec_dispatcher import BECDispatcher +from bec_widgets.utils.bec_widget import BECWidget +from bec_widgets.utils.colors import apply_theme, get_accent_colors +from bec_widgets.utils.error_popups import SafeSlot + +# pylint: disable=E0611 +from qtpy.QtWidgets import QApplication, QFileDialog, QMessageBox, QVBoxLayout, QWidget + +from .panels.input_panel import InputPanel +from .panels.scan_view import ScanViewer + +logger = bec_logger.logger + +MAX_HIST_LEN = 100 + + +class DataViewer(BECWidget, QWidget): + """ + Main widget of Data Viewer + """ + + PLUGIN = True + ICON_NAME = "find_in_page" + + def __init__(self, parent=None, **kwargs): + super().__init__(parent=parent, **kwargs) + self.get_bec_shortcuts() + + central = QWidget() + self.root_layout = QVBoxLayout(central) + self.input = InputPanel() + self.viewer = ScanViewer() + self.root_layout.addWidget(self.input, 0) + self.root_layout.addWidget(self.viewer, 1) + + self.setLayout(self.root_layout) + self.setWindowTitle("Data Viewer") + + self.history = [] + self.bec_dispatcher.connect_slot(self.on_history_update, MessageEndpoints.scan_history()) + self.on_history_update() + + self.current_row = 0 + + self.input.scan_sel.currentItemChanged_connect(self.scan_sel_changed) + self.input.load_button.clicked_connect(self.load_scan_from_history) + self.input.load_from_folder_button.clicked_connect(self.load_scan_from_folder) + self.viewer.unload_button.clicked_connect(self.unload_all_scans) + self.input.open_fm_button.clicked_connect(self.open_in_file_manager) + + self.show_open_from_folder_infobox = True + + def apply_theme(self, theme: Literal["dark", "light"]): + """ + Apply the theme + + Args: + theme (str): Theme, either "dark" or "light" + """ + self.viewer.apply_theme(theme) + self.on_history_update() + + @SafeSlot() + def scan_sel_changed(self, *_, **kwargs): + """Updates the current row value of the scan selection list""" + self.current_row = kwargs["value"]().row() + + @SafeSlot() + def open_in_file_manager(self, *_): + """Open the scan folder in the systems default file manager""" + if len(self.history) > 0: + scan = self.history[self.current_row] + filepath = scan["file_components"][0].decode().rsplit("/", 1)[0] + subprocess.Popen( + ["xdg-open", filepath], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + + @SafeSlot() + def load_scan_from_history(self, *_): + """ + Loads a scan. Find all files within the scan folder, sort them and + then load the files in the scan view + """ + if len(self.history) > 0: + scan = self.history[self.current_row] + base_filepath = scan["file_components"][0].decode().rsplit("/", 1)[0] + self.load_scan(base_filepath) + + @SafeSlot() + def load_scan_from_folder(self, *_): + """ + Loads a scan from a folder. Find all files within the scan folder, sort them and + then load the files in the scan view + """ + + hostname = self.client._hostname + start = hostname.find("x") + if start != -1: + beamline = hostname[start : start + 5] + active_account = self.client.active_account + start_folder = f"/sls/{beamline}/data/{active_account}/raw" + else: + start_folder = "/sls" + + if self.show_open_from_folder_infobox: + self.show_open_from_folder_infobox = False + info_box = QMessageBox(self) + info_box.setIcon(QMessageBox.Icon.Information) + info_box.setWindowTitle("Select Scan Folder") + info_box.setText("Navigate to the scan folder and click 'Open' to load it.") + info_box.addButton("Got it!", QMessageBox.ButtonRole.AcceptRole) + info_box.exec_() + + base_filepath = QFileDialog.getExistingDirectory(self, "Open Scan Folder", start_folder) + + if base_filepath != "": + self.load_scan(base_filepath) + + def load_scan(self, base_filepath): + """ + Loads a scan from a base_filepath. Find all files within the scan folder, sort them and + then load the files in the scan view + """ + filenames = [ + f for f in os.listdir(base_filepath) if os.path.isfile(os.path.join(base_filepath, f)) + ] + + def sort_priority(name): + if "master" in name: + return 0 + if name.endswith(".h5"): + return 1 + return 2 + + sorted_files = [f"{base_filepath}/{name}" for name in sorted(filenames, key=sort_priority)] + self.viewer.load_files(sorted_files) + + @SafeSlot() + def unload_all_scans(self, *_): + """Removes all scans from the scan view""" + self.viewer.clear_files() + + def duration_formatted(self, start: str, end: str) -> str: + """ + Calculates the duration of a scan based on start end end time and + formats it as an easy readable string. + + Args: + start(str): start time in iso-format + end(str): end time in iso-format + + Returns: + str: Formatted duration, e.g. '1min 10s' or '1h 13min' + """ + start_dt = datetime.fromisoformat(start) + end_dt = datetime.fromisoformat(end) + seconds = abs(int((end_dt - start_dt).total_seconds())) + + days, remainder = divmod(seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days: + parts.append(f"{days}d") + if hours: + parts.append(f"{hours}h") + if minutes: + parts.append(f"{minutes}min") + if not days and not hours and minutes < 10: + parts.append(f"{seconds}s") + return " ".join(parts) if parts else "<1min" + + def time_formatted(self, iso_time: str) -> str: + """ + Formates a time as an easy readable string. + + Args: + iso_time(str): Time in iso-format + + Returns: + str: Time formatted with format '%d.%m.%Y %H:%M', e.g. '14.01.1995 08:12' + """ + dt = datetime.fromisoformat(iso_time) + return dt.strftime("%d.%m.%Y %H:%M") + + @SafeSlot() + def on_history_update(self, *_): + """Updates the scan list based on the bec scan history.""" + self.history = [] + self.input.scan_sel.clear() + if self.client.history is None: + return + max_scans = min(len(self.client.history), MAX_HIST_LEN) + for n in range(1, max_scans): # last scans, limited by MAX_HIST_LEN + start_time = self.client.history[-n].metadata["start_time"] # type: ignore + end_time = self.client.history[-n].metadata["end_time"] # type: ignore + scan_data = self.client.history[-n].metadata["bec"] # type: ignore + # logger.info(f"scan_data: {scan_data}") + scan_number = scan_data["scan_number"] + scan_name = scan_data["scan_name"] + + comment, sample_name = "", "" + user_metadata = scan_data.get("user_metadata", {}) + comment = user_metadata.get("comment", comment) + sample_name = user_metadata.get("sample_name", sample_name) + if comment == "": + metadata_user = scan_data.get("metadata", {}).get("user_metadata", {}) + comment = metadata_user.get("comment", comment) + if sample_name == "": + metadata_user = scan_data.get("metadata", {}).get("user_metadata", {}) + sample_name = metadata_user.get("sample_name", sample_name) + + status = scan_data["status"] + self.history.append( + { + "scan_number": scan_number, + "scan_name": scan_name, + "comment": comment, + "sample_name": sample_name, + "file_components": scan_data["file_components"], + "start_time": start_time, + "end_time": end_time, + "status": status, + } + ) + tags = [] + tags.append((scan_name, get_accent_colors().default.name())) + if sample_name != "": + tags.append((sample_name, get_accent_colors().highlight.name())) + if comment != "": + tags.append((comment, get_accent_colors().warning.name())) + if status == "closed": + tags.append((status, get_accent_colors().success.name())) + elif status == "halted" or status == "aborted": + tags.append((status, get_accent_colors().emergency.name())) + else: + tags.append((status, "#656365")) + tags.append((self.duration_formatted(start_time, end_time), "#656365")) + tags.append((self.time_formatted(start_time), "#656365")) + self.input.scan_sel.addTaggedItem(label=str(scan_number), tags=tags) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + apply_theme("light") + dispatcher = BECDispatcher(gui_id="data_viewer") + win = DataViewer() + win.show() + sys.exit(app.exec_()) diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer.pyproject b/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer.pyproject new file mode 100644 index 00000000..e6d7fe42 --- /dev/null +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer.pyproject @@ -0,0 +1 @@ +{'files': ['data_viewer.py']} \ No newline at end of file diff --git a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control_widget_plugin.py b/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer_plugin.py similarity index 70% rename from csaxs_bec/bec_widgets/widgets/slit_control/slit_control_widget_plugin.py rename to csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer_plugin.py index 8e28c5dd..8a3e7372 100644 --- a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control_widget_plugin.py +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/data_viewer_plugin.py @@ -5,17 +5,17 @@ from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtWidgets import QWidget from bec_widgets.utils.bec_designer import designer_material_icon -from csaxs_bec.bec_widgets.widgets.slit_control.slit_control import SlitControlWidget +from csaxs_bec.bec_widgets.widgets.data_viewer.data_viewer import DataViewer DOM_XML = """ - + """ -class SlitControlWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover +class DataViewerPlugin(QDesignerCustomWidgetInterface): # pragma: no cover def __init__(self): super().__init__() self._form_editor = None @@ -23,7 +23,7 @@ class SlitControlWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cov def createWidget(self, parent): if parent is None: return QWidget() - t = SlitControlWidget(parent) + t = DataViewer(parent) return t def domXml(self): @@ -33,10 +33,10 @@ class SlitControlWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cov return "" def icon(self): - return designer_material_icon(SlitControlWidget.ICON_NAME) + return designer_material_icon(DataViewer.ICON_NAME) def includeFile(self): - return "slit_control_widget" + return "data_viewer" def initialize(self, form_editor): self._form_editor = form_editor @@ -48,7 +48,7 @@ class SlitControlWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cov return self._form_editor is not None def name(self): - return "SlitControlWidget" + return "DataViewer" def toolTip(self): return "" diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/loaders.py b/csaxs_bec/bec_widgets/widgets/data_viewer/loaders.py new file mode 100644 index 00000000..5b260abd --- /dev/null +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/loaders.py @@ -0,0 +1,229 @@ +""" +File-format loader (HDF5, images). +""" + +import os +from abc import ABC, abstractmethod +from typing import Iterator, Literal, Optional + +import h5py +import numpy as np + + +class NodeInfo: + """Describes a single node (group or dataset) inside a loaded file.""" + + __slots__ = ("name", "path", "kind", "dtype", "shape") + + def __init__( + self, + name: str, + path: str, + kind: Literal["group", "dataset"], + dtype: str = "", + shape: tuple[int, ...] = (), + ): + self.name = name + self.path = path + self.kind = kind + self.dtype = dtype # e.g. "float", "int", "str", … + self.shape = shape # empty tuple for scalars / groups + + +class BaseFileLoader(ABC): + """ + Abstract base class for file-format loaders. + + Subclass this to add support for a new format. Three things are required: + + 1. ``EXTENSIONS`` — tuple of lowercase extensions this loader handles, + e.g. ``(".h5", ".hdf5")``. + + 2. ``open(filepath)`` — open the file and keep any handles alive. + + 3. ``iter_nodes(path)`` — yield ``NodeInfo`` objects for the direct + children of *path* (depth-1 walk; the tree widget calls this + recursively as the user expands nodes). + + 4. ``read_dataset(path)`` — return the dataset at *path* as a + ``numpy.ndarray``. + + 5. ``close()`` — release any open file handles. + + 6. ``child_count(path)`` — return the number of direct children of a + group node (used for the status label; override if cheap to compute). + """ + + EXTENSIONS: tuple[str, ...] = () + + @abstractmethod + def open(self, filepath: str) -> None: ... + + @abstractmethod + def iter_nodes(self, path: str) -> Iterator[NodeInfo]: ... + + @abstractmethod + def read_dataset(self, path: str) -> np.ndarray: ... + + @abstractmethod + def close(self) -> None: ... + + def child_count(self, path: str) -> int: + return sum(1 for _ in self.iter_nodes(path)) + + +class HDF5Loader(BaseFileLoader): + """Loader for HDF5 / NeXus files (.h5, .hdf5, .nxs, .nx).""" + + EXTENSIONS = (".h5", ".hdf5", ".hdf", ".nxs", ".nx") + + def __init__(self): + self._file: Optional[h5py.File] = None + + def open(self, filepath: str) -> None: + self._file = h5py.File(filepath, "r") + + def close(self) -> None: + if self._file is not None: + self._file.close() + self._file = None + + def iter_nodes(self, path: str) -> Iterator[NodeInfo]: + assert self._file is not None, "File not open" + obj = self._file[path] if path != "/" else self._file + + if not isinstance(obj, h5py.Group): + return + + for key in obj.keys(): + try: + child = obj[key] + except Exception: + continue + + child_path = child.name # h5py always gives the absolute path + + if isinstance(child, h5py.Group): + yield NodeInfo(name=key, path=child_path, kind="group") + + elif isinstance(child, h5py.Dataset): + shape_tuple = child.shape + d = child.dtype + dtype = "unknown" + if np.issubdtype(d, np.integer): + dtype = "int" + if np.issubdtype(d, np.floating): + dtype = "float" + if np.issubdtype(d, np.complexfloating): + dtype = "complex" + if d.kind in ("S", "U", "O"): + dtype = "str" + + yield NodeInfo( + name=key, path=child_path, kind="dataset", dtype=dtype, shape=shape_tuple + ) + + def read_dataset(self, path: str) -> np.ndarray: + assert self._file is not None, "File not open" + return self._file[path][()] + + def child_count(self, path: str) -> int: + assert self._file is not None, "File not open" + obj = self._file[path] if path != "/" else self._file + return len(obj) if isinstance(obj, h5py.Group) else 0 + + +class ImageLoader(BaseFileLoader): + """ + Loader for raster image files. + + The file is treated as a single, flat dataset. ``iter_nodes`` yields one + leaf node. + + Requires: Pillow (``pip install Pillow``) + """ + + EXTENSIONS = ( + ".jpg", + ".jpeg", + ".png", + ".gif", + ".tiff", + ".tif", + ".bmp", + ".webp", + ".ico", + ".ppm", + ".pgm", + ".pbm", + ) + + def __init__(self): + self._filepath: Optional[str] = None + + def open(self, filepath: str) -> None: + # Validate that Pillow can open it; keep only the path. + try: + from PIL import Image as _PILImage # noqa: F401 — existence check + + _PILImage.open(filepath).verify() + except Exception as exc: + raise OSError(f"Cannot open image {filepath!r}: {exc}") from exc + self._filepath = filepath + + def close(self) -> None: + self._filepath = None + + def iter_nodes(self, path: str) -> Iterator[NodeInfo]: + """Images have no internal hierarchy — yield a single leaf node.""" + if path != "/" or self._filepath is None: + return + from PIL import Image as _PILImage + + with _PILImage.open(self._filepath) as img: + w, h = img.size + mode = img.mode # e.g. "RGB", "RGBA", "L", … + + name = os.path.basename(self._filepath) + yield NodeInfo(name=name, path="/image", kind="dataset", dtype=mode, shape=(h, w)) + + def read_dataset(self, path: str) -> np.ndarray: + """Return the image as a uint8 numpy array (H x W x C or H x W).""" + from PIL import Image as _PILImage + + with _PILImage.open(self._filepath) as img: # type: ignore[arg-type] + # Animated GIF → first frame only + if hasattr(img, "n_frames") and img.n_frames > 1: + img.seek(0) + return np.asarray(img) + + def child_count(self, path: str) -> int: + return 1 if path == "/" else 0 + + +class LoaderRegistry: + """Maps file extensions to loader classes.""" + + def __init__(self): + self._registry: dict[str, type[BaseFileLoader]] = {} + + def register(self, loader_cls: type[BaseFileLoader]) -> None: + """Register a loader class for all extensions it declares.""" + for ext in loader_cls.EXTENSIONS: + self._registry[ext.lower()] = loader_cls + + def get_loader(self, filepath: str) -> Optional[BaseFileLoader]: + """Return a fresh loader instance for *filepath*, or None if unsupported.""" + ext = os.path.splitext(filepath)[1].lower() + cls = self._registry.get(ext) + return cls() if cls is not None else None + + @property + def supported_extensions(self) -> list[str]: + return sorted(self._registry) + + +# Default global registry — pre-populated with built-in loaders. +registry = LoaderRegistry() +registry.register(HDF5Loader) +registry.register(ImageLoader) diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/panels/__init__.py b/csaxs_bec/bec_widgets/widgets/data_viewer/panels/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/panels/data_view.py b/csaxs_bec/bec_widgets/widgets/data_viewer/panels/data_view.py new file mode 100644 index 00000000..e9b368c4 --- /dev/null +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/panels/data_view.py @@ -0,0 +1,393 @@ +""" +Data viewer displaying the data +""" + +from typing import Literal, Optional + +import numpy as np +import pyqtgraph as pg +from bec_lib import bec_logger +from bec_widgets.utils.colors import Colors +from qtpy.QtCore import Qt + +# pylint: disable=E0611 +from qtpy.QtGui import QFont, QPixmap + +# pylint: disable=E0611 +from qtpy.QtWidgets import ( + QAbstractItemView, + QApplication, + QGroupBox, + QHBoxLayout, + QHeaderView, + QLabel, + QRadioButton, + QSizePolicy, + QSlider, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, +) + +logger = bec_logger.logger + +MAX_ROWS = 2000 +MAX_COLS = 500 + + +class DataView(QWidget): + def __init__(self): + super().__init__() + self._layout = QVBoxLayout(self) + + header = QHBoxLayout() + self.path_label = QLabel("") + self.path_label.setObjectName("path_label") + self.info_label = QLabel("") + self.info_label.setObjectName("info_label") + header.addWidget(self.path_label, 1) + header.addWidget(self.info_label) + self._layout.addLayout(header) + + mode_box = QGroupBox("View mode") + mode_layout = QHBoxLayout(mode_box) + mode_layout.setContentsMargins(8, 4, 8, 4) + self.rb_auto = QRadioButton("Auto") + self.rb_plot = QRadioButton("Plot") + self.rb_image = QRadioButton("Image") + self.rb_table = QRadioButton("Table") + self.rb_auto.setChecked(True) + for rb in (self.rb_auto, self.rb_image, self.rb_plot, self.rb_table): + mode_layout.addWidget(rb) + rb.toggled.connect(self._on_mode_change) + mode_layout.addStretch() + self._layout.addWidget(mode_box) + + self.content = QWidget() + self.content_layout = QVBoxLayout(self.content) + self.content_layout.setContentsMargins(0, 0, 0, 0) + self._layout.addWidget(self.content, 1) + + self.plot_widget = None + self.image_widget = None + self._current_data = None + self.show_empty() + + def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None): + """ + Apply the theme + + Args: + theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None. + """ + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + if self.plot_widget is not None: + n_curves = len(self.plot_widget.listDataItems()) + colors = Colors.golden_angle_color( + colormap="plasma", num=max(10, n_curves + 1), format="HEX" + ) + for idx, curve in enumerate(self.plot_widget.listDataItems()): + curve.setPen(pg.mkPen(color=colors[idx])) + # Background + self.plot_widget.setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = self.plot_widget.getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + + if self.image_widget is not None: + self.image_widget.getView().setBackgroundColor(bg_color) + self.image_widget.ui.histogram.setBackground(bg_color) + + def _clear_stack(self): + while self.content_layout.count(): + item = self.content_layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + self.plot_widget = None + self.image_widget = None + + def show_empty(self): + """Empties the content area.""" + self._clear_stack() + empty_label = QLabel("No data selected") + empty_label.setObjectName("info_label") + empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.path_label.setText("") + self.info_label.setText("") + self.content_layout.addWidget(empty_label) + + def show_unsupported(self, path: str = "") -> None: + """Display a friendly 'not implemented' message for unknown file types.""" + self._clear_stack() + self._current_data = None + self.path_label.setText(path) + self.info_label.setText("") + lbl = QLabel("File type not supported") + lbl.setObjectName("info_label") + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.content_layout.addWidget(lbl) + + def _on_mode_change(self): + if self._current_data is not None: + self.display(self._current_data, self.path_label.text()) + + def _active_mode(self): + if self.rb_plot.isChecked(): + return "plot" + if self.rb_image.isChecked(): + return "image" + if self.rb_table.isChecked(): + return "table" + return "auto" + + def display(self, data, path: str = "") -> None: + """ + Render *data* in the panel. + + Parameters + ---------- + data: + A ``numpy.ndarray`` (or anything convertible to one). + path: + Human-readable label shown in the header. + """ + self._current_data = data + self.path_label.setText(path) + + if not isinstance(data, np.ndarray): + data = np.array(data) + + self.info_label.setText(f"shape {data.shape}, dtype {data.dtype}, {data.size} elements") + + mode = self._active_mode() + if mode == "auto": + if data.ndim <= 1 and data.size > 1: + mode = "plot" + elif data.ndim <= 4 and min(data.shape, default=0) > 1: + mode = "image" + else: + mode = "table" + + if mode == "plot": + self._show_plot_1d(data) + elif mode == "image": + self._show_image_2d(data) + else: + self._show_table(data) + + def _show_plot_1d(self, data): + self._clear_stack() + + is_2d = data.ndim == 2 + + if is_2d: + n_rows, _ = data.shape + row_data = data[0].astype(np.float32) + else: + row_data = data.reshape(-1).astype(np.float32) + + x = np.arange(row_data.size, dtype=np.float32) + + self.plot_widget = pg.PlotWidget() + plot_item = self.plot_widget.getPlotItem() + assert plot_item is not None, "PlotWidget has no PlotItem" + plot_item.showGrid(x=True, y=True, alpha=0.25) + self.plot_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + plot_item.setAutoVisible(y=False) # type: ignore[attr-defined] + + curve = pg.PlotDataItem( + x, row_data, pen=pg.mkPen(color="#2980b9", width=1.6), antialias=False + ) + self.plot_widget.addItem(curve) + + curve.setDownsampling(auto=True, method="peak") + curve.setClipToView(True) + curve.setSkipFiniteCheck(True) + + plot_item.enableAutoRange() # type: ignore[attr-defined] + + if is_2d: + slider = QSlider(Qt.Orientation.Vertical) + slider.setMinimum(0) + slider.setMaximum(n_rows - 1) + slider.setValue(0) + slider.setFixedWidth(32) + slider.setPageStep(1) + + current_row_label = QLabel("0") + current_row_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + current_row_label.setFixedWidth(32) + + max_row_label = QLabel(f"0:{n_rows-1}") + max_row_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + max_row_label.setFixedWidth(32) + + def on_row_changed(row): + current_row_label.setText(str(row)) + new_data = data[row].astype(np.float32) + new_x = np.arange(new_data.size, dtype=np.float32) + curve.setData(new_x, new_data) + plot_item.enableAutoRange() # type: ignore[attr-defined] + + slider.valueChanged.connect(on_row_changed) + + slider_col = QWidget() + slider_col.setFixedWidth(36) + col_layout = QVBoxLayout(slider_col) + col_layout.setContentsMargins(0, 0, 0, 0) + col_layout.setSpacing(2) + col_layout.addWidget(max_row_label) + col_layout.addWidget(slider) + col_layout.addWidget(current_row_label) + + container = QWidget() + h_layout = QHBoxLayout(container) + h_layout.setContentsMargins(0, 0, 0, 0) + h_layout.setSpacing(4) + h_layout.addWidget(slider_col) + h_layout.addWidget(self.plot_widget) + + self.content_layout.addWidget(container) + else: + self.content_layout.addWidget(self.plot_widget) + + self.apply_theme() + + def _show_image_2d(self, data): + self._clear_stack() + + stacked = False + n_images = 0 + rgb = False + img = data + if data.ndim == 3: + if data.shape[-1] in (3, 4): + rgb = True + else: + stacked = True + n_images = data.shape[0] + img = data[0, :] + elif data.ndim == 4: + if data.shape[-1] in (3, 4): + rgb = True + stacked = True + n_images = data.shape[0] + img = data[0, :] + + self.image_widget = pg.ImageView() + + self.image_widget.ui.roiBtn.hide() + self.image_widget.ui.menuBtn.hide() + + if not rgb: + self.image_widget.setColorMap(pg.colormap.get("inferno", source="matplotlib")) + + def set_image(img, autoLevels=True, autoHistogramRange=True): + if rgb: + self.image_widget.imageItem.setOpts(axisOrder="row-major") + self.image_widget.setImage(img) + else: + self.image_widget.setImage( + img.T, autoLevels=autoLevels, autoHistogramRange=autoHistogramRange + ) + + set_image(img) + self.image_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + if stacked: + slider = QSlider(Qt.Orientation.Vertical) + slider.setMinimum(0) + slider.setMaximum(n_images - 1) + slider.setValue(0) + slider.setFixedWidth(32) + slider.setPageStep(1) + + current_image_label = QLabel("0") + current_image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + current_image_label.setFixedWidth(32) + + max_image_label = QLabel(f"0:{n_images-1}") + max_image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + max_image_label.setFixedWidth(32) + + def on_image_changed(row): + current_image_label.setText(str(row)) + set_image(data[row, :], autoLevels=False, autoHistogramRange=False) + + slider.valueChanged.connect(on_image_changed) + + slider_col = QWidget() + slider_col.setFixedWidth(36) + col_layout = QVBoxLayout(slider_col) + col_layout.setContentsMargins(0, 0, 0, 0) + col_layout.setSpacing(2) + col_layout.addWidget(max_image_label) + col_layout.addWidget(slider) + col_layout.addWidget(current_image_label) + + container = QWidget() + h_layout = QHBoxLayout(container) + h_layout.setContentsMargins(0, 0, 0, 0) + h_layout.setSpacing(4) + h_layout.addWidget(slider_col) + h_layout.addWidget(self.image_widget) + + self.content_layout.addWidget(container) + else: + self.content_layout.addWidget(self.image_widget) + + self.apply_theme() + + def _show_table(self, data): + self._clear_stack() + + if data.ndim == 0: + flat = data.reshape(1, 1) + elif data.ndim == 1: + flat = data.reshape(-1, 1) + elif data.ndim == 2: + flat = data + else: + flat = data.reshape(-1, data.shape[-1]) + + rows, cols = flat.shape + show_rows = min(rows, MAX_ROWS) + show_cols = min(cols, MAX_COLS) + + if rows > MAX_ROWS or cols > MAX_COLS: + note = QLabel(f"⚠ Showing {show_rows}/{rows} rows x {show_cols}/{cols} columns") + note.setObjectName("info_label") + note.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.content_layout.addWidget(note) + + table = QTableWidget(show_rows, show_cols) + table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + table.setSelectionMode(QAbstractItemView.SelectionMode.ContiguousSelection) + table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) + + is_float = np.issubdtype(flat.dtype, np.floating) + is_complex = np.iscomplexobj(flat) + is_bytes = flat.dtype.kind == "S" + + for r in range(show_rows): + for c in range(show_cols): + val = flat[r, c] + txt = ( + f"{val:.6g}" + if is_float + else f"{val:.4g}" if is_complex else str(val.decode()) if is_bytes else str(val) + ) + cell = QTableWidgetItem(txt) + cell.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + table.setItem(r, c, cell) + + self.content_layout.addWidget(table) diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/panels/input_panel.py b/csaxs_bec/bec_widgets/widgets/data_viewer/panels/input_panel.py new file mode 100644 index 00000000..3d2088e1 --- /dev/null +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/panels/input_panel.py @@ -0,0 +1,40 @@ +# pylint: disable=E0611 +from qtpy.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QWidget + +# pylint: disable=E0402 +from ..widgets.qt_widgets import Button, Group, ListWidget + + +class InputPanel(QWidget): + """Panel for scan selection of the data viewer widget""" + + def __init__(self, parent=None): + super().__init__(parent) + self._layout = QHBoxLayout(self) + + # Scan selection + self.scan_sel = ListWidget("scan_sel") + self.load_button = Button(label_button="Load Dataset", enabled=True) + self.open_fm_button = Button(label_button="Open in File Manager", enabled=True) + + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setFrameShadow(QFrame.Sunken) + + self.load_from_folder_button = Button( + label_button="Load Dataset from File Manager", enabled=True + ) + + self._button_layout = QVBoxLayout() + self._button_layout.addWidget(self.load_button) + self._button_layout.addWidget(self.open_fm_button) + self._button_layout.addWidget(line) + self._button_layout.addWidget(self.load_from_folder_button) + self._button_layout.addStretch() + + # Assemble complete scan selection group + self.input_group = Group( + "Scan selection", [self._button_layout, self.scan_sel], orientation="horizontal" + ) + + self._layout.addWidget(self.input_group) diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/panels/scan_view.py b/csaxs_bec/bec_widgets/widgets/data_viewer/panels/scan_view.py new file mode 100644 index 00000000..9c730b13 --- /dev/null +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/panels/scan_view.py @@ -0,0 +1,215 @@ +""" +Scan viewer. Displays files of one or more scans in a tree view +""" + +import os +from typing import Literal, Optional + +from bec_lib import bec_logger +from bec_qthemes import material_icon +from bec_widgets.utils.colors import get_accent_colors +from qtpy.QtCore import Qt + +# pylint: disable=E0611 +from qtpy.QtGui import QBrush, QColor + +# pylint: disable=E0611 +from qtpy.QtWidgets import ( + QHBoxLayout, + QHeaderView, + QMainWindow, + QSplitter, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, +) + +# pylint: disable=E0402 +from ..loaders import BaseFileLoader, registry +from ..widgets.qt_widgets import Button, Group +from .data_view import DataView + +logger = bec_logger.logger + +ICON_SIZE = 20 + + +class ScanViewer(QMainWindow): + """ + Generic scan viewer. Supports any format registered in *registry*. + + Args: + filepath(str): Optional path to open on startup. + """ + + def __init__(self, filepath: Optional[str] = None): + super().__init__() + self.registry = registry + + self._open_files: dict[str, tuple[BaseFileLoader, str]] = {} + + central = QWidget() + self.setCentralWidget(central) + root_layout = QHBoxLayout(central) + + splitter = QSplitter(Qt.Orientation.Horizontal) + splitter.setChildrenCollapsible(False) + + left_pane = QWidget() + left_layout = QVBoxLayout(left_pane) + + self.unload_button = Button(label_button="Unload all", enabled=True) + self.tree = QTreeWidget() + self.tree.setMinimumWidth(250) + self.tree.setHeaderLabels(["Name", "Type", "Shape"]) + self.tree.header().setStretchLastSection(False) + self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self.tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + self.tree.header().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + self.tree.itemClicked.connect(self._on_item_clicked) + + left_layout.addWidget(self.unload_button) + left_layout.addWidget(self.tree, 1) + + self.data_panel = DataView() + + splitter.addWidget(left_pane) + splitter.addWidget(self.data_panel) + + splitter.setStretchFactor(0, 1) + splitter.setStretchFactor(1, 3) + splitter.setSizes([300, 900]) + splitter.setHandleWidth(6) + splitter.setChildrenCollapsible(False) + + self.scan_view_group = Group("Scan view", [splitter]) + + root_layout.addWidget(self.scan_view_group) + + if filepath: + self.load_files([filepath]) + + def apply_theme(self, theme: Literal["dark", "light"]): + """ + Apply the theme + + Args: + theme (str): Theme, either "dark" or "light" + """ + self.data_panel.apply_theme(theme) + + def load_files(self, filepaths: list[str]) -> None: + """Open one or more files and add each as a top-level tree node.""" + for fp in filepaths: + if fp in self._open_files: + continue # already loaded + + loader = self.registry.get_loader(fp) + if loader is None: + supported = ", ".join(self.registry.supported_extensions) + logger.warning("No loader found for %r. Supported extensions: %s", fp, supported) + continue + + try: + loader.open(fp) + except Exception as exc: + logger.error("Failed to open %r: %s", fp, exc) + continue + + display_name = os.path.basename(fp) + self._open_files[fp] = (loader, display_name) + self._add_file_to_tree(fp, loader, display_name) + + def clear_files(self) -> None: + """Close all open files and reset the tree.""" + for loader, _ in self._open_files.values(): + loader.close() + self._open_files.clear() + self.tree.clear() + self.data_panel.show_empty() + + def closeEvent(self, event): + """Close all""" + for loader, _ in self._open_files.values(): + loader.close() + self._open_files.clear() + super().closeEvent(event) + + def _add_file_to_tree(self, filepath: str, loader: BaseFileLoader, display_name: str) -> None: + """Add a single file as a new top-level node in the tree.""" + dataset_icon = material_icon( + "dataset", size=(ICON_SIZE, ICON_SIZE), color=get_accent_colors().default.name() + ) + root_item = QTreeWidgetItem(self.tree, [display_name, "Group", ""]) + root_item.setIcon(0, dataset_icon) + root_item.setData(0, Qt.ItemDataRole.UserRole, "/") # path + root_item.setData(0, Qt.ItemDataRole.UserRole + 1, "group") # kind + root_item.setData(0, Qt.ItemDataRole.UserRole + 2, filepath) # file key + + self._populate_tree(root_item, loader, "/") + self.tree.addTopLevelItem(root_item) + + # Expand first 2 levels by default + self.tree.expandItem(root_item) + for i in range(root_item.childCount()): + self.tree.expandItem(root_item.child(i)) + + self.tree.setCurrentItem(root_item) + + def _populate_tree( + self, parent_item: QTreeWidgetItem, loader: BaseFileLoader, path: str + ) -> None: + folder_icon = material_icon("folder", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + vector_icon = material_icon("show_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + array_icon = material_icon( + "stacked_line_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9" + ) + scalar_icon = material_icon("point_scan", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + str_icon = material_icon("text_snippet", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + + for node in loader.iter_nodes(path): + shape_str = "x".join(str(s) for s in node.shape) if node.shape else "scalar" + + if node.kind == "group": + item = QTreeWidgetItem(parent_item, [node.name, "Group", ""]) + item.setIcon(0, folder_icon) + item.setData(0, Qt.ItemDataRole.UserRole, node.path) + item.setData(0, Qt.ItemDataRole.UserRole + 1, "group") + self._populate_tree(item, loader, node.path) + + else: # dataset + if shape_str == "scalar": + icon = str_icon if node.dtype == "str" else scalar_icon + elif "x" in shape_str: + icon = array_icon + else: + icon = vector_icon + + item = QTreeWidgetItem(parent_item, [node.name, node.dtype, shape_str]) + item.setIcon(0, icon) + item.setData(0, Qt.ItemDataRole.UserRole, node.path) + item.setData(0, Qt.ItemDataRole.UserRole + 1, "dataset") + item.setForeground(1, QBrush(QColor(get_accent_colors().success.name()))) + item.setForeground(2, QBrush(QColor("#656365"))) + + def _get_filepath_for_item(self, item: QTreeWidgetItem) -> str: + """Walk up the tree to find the filepath stored on the root node.""" + node = item + while node.parent(): + node = node.parent() + return node.data(0, Qt.ItemDataRole.UserRole + 2) + + def _on_item_clicked(self, item: QTreeWidgetItem, _col: int) -> None: + path = item.data(0, Qt.ItemDataRole.UserRole) + kind = item.data(0, Qt.ItemDataRole.UserRole + 1) + filepath = self._get_filepath_for_item(item) + + if not path or not filepath: + return + + loader, _ = self._open_files[filepath] + + if kind == "dataset": + data = loader.read_dataset(path) + self.data_panel.display(data, path) diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/register_tomo_params.py b/csaxs_bec/bec_widgets/widgets/data_viewer/register_data_viewer.py similarity index 67% rename from csaxs_bec/bec_widgets/widgets/tomo_params/register_tomo_params.py rename to csaxs_bec/bec_widgets/widgets/data_viewer/register_data_viewer.py index dfd11909..fb1e5578 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/register_tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/register_data_viewer.py @@ -6,9 +6,9 @@ def main(): # pragma: no cover return from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection - from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params_plugin import TomoParamsPlugin + from csaxs_bec.bec_widgets.widgets.data_viewer.data_viewer_plugin import DataViewerPlugin - QPyDesignerCustomWidgetCollection.addCustomWidget(TomoParamsPlugin()) + QPyDesignerCustomWidgetCollection.addCustomWidget(DataViewerPlugin()) if __name__ == "__main__": # pragma: no cover diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/widgets/__init__.py b/csaxs_bec/bec_widgets/widgets/data_viewer/widgets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/csaxs_bec/bec_widgets/widgets/data_viewer/widgets/qt_widgets.py b/csaxs_bec/bec_widgets/widgets/data_viewer/widgets/qt_widgets.py new file mode 100644 index 00000000..7242c1e5 --- /dev/null +++ b/csaxs_bec/bec_widgets/widgets/data_viewer/widgets/qt_widgets.py @@ -0,0 +1,233 @@ +""" +Universal Qt widgets +""" + +from functools import partial + +from bec_widgets.utils.colors import get_accent_colors + +# pylint: disable=E0611 +from qtpy.QtCore import QPoint, QRect, QSize, Qt +from qtpy.QtGui import QColor, QFont, QFontMetrics, QPainter, QPen +from qtpy.QtWidgets import ( + QGroupBox, + QHBoxLayout, + QLabel, + QLayout, + QListWidget, + QListWidgetItem, + QPushButton, + QStyle, + QStyledItemDelegate, + QStyleOptionViewItem, + QVBoxLayout, + QWidget, +) + + +class Group(QGroupBox): + def __init__(self, label, objs, orientation="vertical"): + super().__init__(label) + if orientation == "vertical": + self._layout = QVBoxLayout(self) + elif orientation == "horizontal": + self._layout = QHBoxLayout(self) + else: + raise ValueError(f"Orientation {orientation} is not supported!") + for obj in objs: + if isinstance(obj, QWidget): + self._layout.addWidget(obj) + elif isinstance(obj, QLayout): + self._layout.addLayout(obj) + + +class Button(QWidget): + def __init__(self, label=None, label_button: str = "", enabled=False): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + if label is not None: + self.label = QLabel(label) + self.label.setFixedWidth(160) + layout.addWidget(self.label) + self.button = QPushButton(label_button) + self.enable_button(enabled) + layout.addWidget(self.button) + + def clicked_connect(self, func): + """Connect a function to the button press.""" + self.button.clicked.connect(func) + + def enable_button(self, enable: bool = False): + if enable: + self.button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + self.button.setEnabled(True) + else: # disabled + self.button.setStyleSheet( + "QPushButton {{background-color: rgb(120, 120, 120); color: white;}}" + ) + self.button.setDisabled(True) + + def setText(self, text): + self.button.setText(text) + + +class ListWidget(QWidget): + def __init__(self, identifier=""): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 0, 0, 0) + layout.setSpacing(0) + self.identifier = identifier + self.value = TaggedListWidget() + layout.addWidget(self.value) + + def clear(self): + self.value.clear() + + def addTaggedItem(self, label, tags): + self.value.addTaggedItem(label, tags) + + def setCurrentIndex(self, text): + self.value.setCurrentIndex(text) + + def currentItemChanged_connect(self, func): + """Connect a function to the Enter/Return key press.""" + self.value.currentItemChanged.connect( + partial( + func, + identifier=self.identifier, + value_obj=self.value, + value=lambda: self.value.currentIndex(), + ) + ) + + def setDisabled(self, disable): + self.value.setDisabled(disable) + + +class TaggedListWidget(QListWidget): + """QListWidget with label + coloured tag pills per row.""" + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.setItemDelegate(TaggedDelegate(self)) + self.setMouseTracking(True) # enables hover highlight + + def addTaggedItem(self, label: str, tags: list | None = None) -> QListWidgetItem: + """ + Add a row. tags is a list of (text, hex_color) pairs, e.g. + [("v1.26", "#2563EB"), ("stable", "#16A34A")] + """ + item = QListWidgetItem(str(label)) + if tags: + item.setData(_UserRole, list(tags)) + self.addItem(item) + return item + + def currentTags(self) -> list: + item = self.currentItem() + return item.data(_UserRole) or [] if item else [] + + +ITEM_HEIGHT = 30 +H_PAD = 12 +TAG_H_PAD = 7 +TAG_V_PAD = 3 +TAG_GAP = 5 +LABEL_TAG_GAP = 12 +CORNER_RADIUS = 4 +TAG_TEXT_COLOR = "#FFFFFF" +_DEMIBOLD = QFont.Weight.DemiBold +_MEDIUM = QFont.Weight.Medium +_AlignCenter = ( + Qt.AlignmentFlag.AlignCenter if hasattr(Qt, "AlignmentFlag") else Qt.AlignmentFlag.AlignCenter +) +_UserRole = Qt.ItemDataRole.UserRole if hasattr(Qt, "ItemDataRole") else Qt.ItemDataRole.UserRole +_NoPen = Qt.PenStyle.NoPen if hasattr(Qt, "PenStyle") else Qt.PenStyle.NoPen +_AA = ( + QPainter.RenderHint.Antialiasing + if hasattr(QPainter, "RenderHint") + else QPainter.RenderHint.Antialiasing +) +_State_Selected = QStyle.StateFlag.State_Selected +_State_MouseOver = QStyle.StateFlag.State_MouseOver + + +class TaggedDelegate(QStyledItemDelegate): + + def sizeHint(self, option: QStyleOptionViewItem, index) -> QSize: + return QSize(option.rect.width() or 300, ITEM_HEIGHT) + + def paint(self, painter: QPainter, option: QStyleOptionViewItem, index) -> None: + painter.save() + + is_selected = bool(option.state & _State_Selected) + is_hover = bool(option.state & _State_MouseOver) + + # Background + if is_selected: + painter.fillRect(option.rect, option.palette.highlight()) + elif is_hover: + painter.fillRect(option.rect, QColor("#F1F5F9")) + else: + painter.fillRect(option.rect, option.palette.base()) + + label_text = ( + index.data( + Qt.ItemDataRole.DisplayRole + if hasattr(Qt, "ItemDataRole") + else Qt.ItemDataRole.DisplayRole + ) + or "" + ) + tags: list = index.data(_UserRole) or [] + + # Label + label_font = painter.font() # inherit default font + label_font.setWeight(_DEMIBOLD) + painter.setFont(label_font) + label_color = ( + option.palette.highlightedText().color() + if is_selected + else option.palette.text().color() + ) + painter.setPen(QPen(label_color)) + + fm = QFontMetrics(label_font) + label_w = fm.horizontalAdvance(label_text) + label_y = option.rect.top() + (ITEM_HEIGHT - fm.height()) // 2 + fm.ascent() + painter.drawText(QPoint(option.rect.left() + H_PAD, label_y), label_text) + + # Tag pills + tag_font = painter.font() # inherit default font + tag_font.setWeight(_MEDIUM) + fm_tag = QFontMetrics(tag_font) + + x = option.rect.left() + H_PAD + label_w + LABEL_TAG_GAP + for tag_text, hex_color in tags: + tag_text = str(tag_text) + tw = fm_tag.horizontalAdvance(tag_text) + 2 * TAG_H_PAD + th = fm_tag.height() + 2 * TAG_V_PAD + ty = option.rect.top() + (ITEM_HEIGHT - th) // 2 + pill = QRect(x, ty, tw, th) + + fill = QColor(hex_color) + if is_selected: + fill = fill.lighter(140) + + painter.setRenderHint(_AA) + painter.setPen(_NoPen) + painter.setBrush(fill) + painter.drawRoundedRect(pill, CORNER_RADIUS, CORNER_RADIUS) + + painter.setFont(tag_font) + painter.setPen(QPen(QColor(TAG_TEXT_COLOR))) + painter.drawText(pill, _AlignCenter, tag_text) + + x += tw + TAG_GAP + + painter.restore() diff --git a/csaxs_bec/bec_widgets/widgets/designer_plugins.py b/csaxs_bec/bec_widgets/widgets/designer_plugins.py index b40f4002..ad5b6c7c 100644 --- a/csaxs_bec/bec_widgets/widgets/designer_plugins.py +++ b/csaxs_bec/bec_widgets/widgets/designer_plugins.py @@ -5,31 +5,27 @@ from __future__ import annotations # pylint: skip-file designer_plugins = { - "ConsoleButtonsWidget": ( - "csaxs_bec.bec_widgets.widgets.console_buttons.console_buttons", - "ConsoleButtonsWidget", - ), - "SAXSWidget": ("csaxs_bec.bec_widgets.widgets.saxs_widget.saxs_widget", "SAXSWidget"), - "SampleStorageWidget": ( + "DataViewer": ("csaxs_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"), + "OMNY_SampleStorage": ( "csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage", - "SampleStorageWidget", + "OMNY_SampleStorage", ), - "SlitControlWidget": ( - "csaxs_bec.bec_widgets.widgets.slit_control.slit_control", - "SlitControlWidget", + "OMNY_TomoParams": ("csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params", "OMNY_TomoParams"), + "OMNY_XRayEye": ("csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye", "OMNY_XRayEye"), + "SAXSWidget": ("csaxs_bec.bec_widgets.widgets.saxs_widget.saxs_widget", "SAXSWidget"), + "SlitControl": ("csaxs_bec.bec_widgets.widgets.slit_control.slit_control", "SlitControl"), + "z_ConsoleButtonsWidget": ( + "csaxs_bec.bec_widgets.widgets.console_buttons.console_buttons", + "z_ConsoleButtonsWidget", ), - "TomoParamsWidget": ( - "csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params", - "TomoParamsWidget", - ), - "XRayEye": ("csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye", "XRayEye"), } widget_icons = { - "ConsoleButtonsWidget": "widgets", + "DataViewer": "find_in_page", + "OMNY_SampleStorage": "widgets", + "OMNY_TomoParams": "widgets", + "OMNY_XRayEye": "widgets", "SAXSWidget": "table_chart", - "SampleStorageWidget": "widgets", - "SlitControlWidget": "widgets", - "TomoParamsWidget": "widgets", - "XRayEye": "widgets", + "SlitControl": "widgets", + "z_ConsoleButtonsWidget": "widgets", } diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/__init__.py b/csaxs_bec/bec_widgets/widgets/sample_storage/__init__.py index 7bddbc71..ce9647d2 100644 --- a/csaxs_bec/bec_widgets/widgets/sample_storage/__init__.py +++ b/csaxs_bec/bec_widgets/widgets/sample_storage/__init__.py @@ -1,3 +1,3 @@ -from .sample_storage import SampleStorageWidget +from .sample_storage import OMNY_SampleStorage -__all__ = ["SampleStorageWidget"] +__all__ = ["OMNY_SampleStorage"] diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_widget.pyproject b/csaxs_bec/bec_widgets/widgets/sample_storage/omny_sample_storage.pyproject similarity index 100% rename from csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_widget.pyproject rename to csaxs_bec/bec_widgets/widgets/sample_storage/omny_sample_storage.pyproject diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_widget_plugin.py b/csaxs_bec/bec_widgets/widgets/sample_storage/omny_sample_storage_plugin.py similarity index 74% rename from csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_widget_plugin.py rename to csaxs_bec/bec_widgets/widgets/sample_storage/omny_sample_storage_plugin.py index 84eaa4f4..79e8f8f4 100644 --- a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_widget_plugin.py +++ b/csaxs_bec/bec_widgets/widgets/sample_storage/omny_sample_storage_plugin.py @@ -5,17 +5,17 @@ from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtWidgets import QWidget from bec_widgets.utils.bec_designer import designer_material_icon -from csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage import SampleStorageWidget +from csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage import OMNY_SampleStorage DOM_XML = """ - + """ -class SampleStorageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover +class OMNY_SampleStoragePlugin(QDesignerCustomWidgetInterface): # pragma: no cover def __init__(self): super().__init__() self._form_editor = None @@ -23,7 +23,7 @@ class SampleStorageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no c def createWidget(self, parent): if parent is None: return QWidget() - t = SampleStorageWidget(parent) + t = OMNY_SampleStorage(parent) return t def domXml(self): @@ -33,10 +33,10 @@ class SampleStorageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no c return "" def icon(self): - return designer_material_icon(SampleStorageWidget.ICON_NAME) + return designer_material_icon(OMNY_SampleStorage.ICON_NAME) def includeFile(self): - return "sample_storage_widget" + return "omny_sample_storage" def initialize(self, form_editor): self._form_editor = form_editor @@ -48,7 +48,7 @@ class SampleStorageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no c return self._form_editor is not None def name(self): - return "SampleStorageWidget" + return "OMNY_SampleStorage" def toolTip(self): return "" diff --git a/csaxs_bec/bec_widgets/widgets/slit_control/register_slit_control_widget.py b/csaxs_bec/bec_widgets/widgets/sample_storage/register_omny_sample_storage.py similarity index 61% rename from csaxs_bec/bec_widgets/widgets/slit_control/register_slit_control_widget.py rename to csaxs_bec/bec_widgets/widgets/sample_storage/register_omny_sample_storage.py index 372ad0ac..f3f84f65 100644 --- a/csaxs_bec/bec_widgets/widgets/slit_control/register_slit_control_widget.py +++ b/csaxs_bec/bec_widgets/widgets/sample_storage/register_omny_sample_storage.py @@ -6,9 +6,9 @@ def main(): # pragma: no cover return from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection - from csaxs_bec.bec_widgets.widgets.slit_control.slit_control_widget_plugin import SlitControlWidgetPlugin + from csaxs_bec.bec_widgets.widgets.sample_storage.omny_sample_storage_plugin import OMNY_SampleStoragePlugin - QPyDesignerCustomWidgetCollection.addCustomWidget(SlitControlWidgetPlugin()) + QPyDesignerCustomWidgetCollection.addCustomWidget(OMNY_SampleStoragePlugin()) if __name__ == "__main__": # pragma: no cover diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/register_sample_storage.py b/csaxs_bec/bec_widgets/widgets/sample_storage/register_sample_storage.py deleted file mode 100644 index 511c6a68..00000000 --- a/csaxs_bec/bec_widgets/widgets/sample_storage/register_sample_storage.py +++ /dev/null @@ -1,18 +0,0 @@ -def main(): # pragma: no cover - """Register SampleStorageWidget with the Qt Designer. - - Mirrors register_tomo_params.py — invoked by the Qt Designer's plugin - discovery mechanism. - """ - from qtpy import QtDesigner - - from csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage_plugin import ( - SampleStorageWidgetPlugin, - ) - - QPyDesignerCustomWidgetCollection = QtDesigner.QPyDesignerCustomWidgetCollection - QPyDesignerCustomWidgetCollection.addCustomWidget(SampleStorageWidgetPlugin()) - - -if __name__ == "__main__": # pragma: no cover - main() diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/register_sample_storage_widget.py b/csaxs_bec/bec_widgets/widgets/sample_storage/register_sample_storage_widget.py deleted file mode 100644 index bc297c7b..00000000 --- a/csaxs_bec/bec_widgets/widgets/sample_storage/register_sample_storage_widget.py +++ /dev/null @@ -1,15 +0,0 @@ -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 csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage_widget_plugin import SampleStorageWidgetPlugin - - QPyDesignerCustomWidgetCollection.addCustomWidget(SampleStorageWidgetPlugin()) - - -if __name__ == "__main__": # pragma: no cover - main() diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py b/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py index e14d74cf..480c9576 100644 --- a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py +++ b/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py @@ -1,5 +1,5 @@ """ -SampleStorageWidget – BEC widget for viewing and correcting the FlOMNI +OMNY_SampleStorage – BEC widget for viewing and correcting the FlOMNI sample-storage bookkeeping. This widget is a GUI replacement for the ``ftransfer_show`` / @@ -103,7 +103,7 @@ class _SlotCell(QFrame): parent widget's mutation methods, which own all backend access. """ - def __init__(self, slot: int, owner: "SampleStorageWidget", border_color: str): + def __init__(self, slot: int, owner: "OMNY_SampleStorage", border_color: str): super().__init__() self._slot = slot self._owner = owner @@ -186,7 +186,7 @@ class _SlotCell(QFrame): # ── main widget ────────────────────────────────────────────────────────────── -class SampleStorageWidget(BECWidget, QWidget): +class OMNY_SampleStorage(BECWidget, QWidget): """ View and correct the FlOMNI sample-storage records. @@ -267,7 +267,7 @@ class SampleStorageWidget(BECWidget, QWidget): try: data = self._samples.read(cached=True) except Exception as exc: - logger.warning(f"SampleStorageWidget: bulk read failed: {exc}") + logger.warning(f"OMNY_SampleStorage: bulk read failed: {exc}") # keep whatever is currently displayed rather than blanking out return {} @@ -309,7 +309,7 @@ class SampleStorageWidget(BECWidget, QWidget): self._last_state.pop(slot, None) return True except Exception as exc: - logger.warning(f"SampleStorageWidget: write of slot {slot} failed: {exc}") + logger.warning(f"OMNY_SampleStorage: write of slot {slot} failed: {exc}") QMessageBox.critical( self, "Write failed", diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_plugin.py b/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_plugin.py deleted file mode 100644 index 38140c8d..00000000 --- a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage_plugin.py +++ /dev/null @@ -1,53 +0,0 @@ -# pylint: disable=missing-module-docstring, missing-function-docstring -# This file is auto-generated style boilerplate for the Qt Designer plugin, -# mirroring the pattern used by the other csaxs_bec widget plugins -# (e.g. tomo_params_plugin.py). It registers SampleStorageWidget with the -# Qt Designer so it can be dropped onto a form. - -from qtpy.QtDesigner import QDesignerCustomWidgetInterface - -from csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage import SampleStorageWidget - -DOM_XML = """ - - - - -""" - - -class SampleStorageWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover - def __init__(self): - super().__init__() - self._form_editor = None - - def initialize(self, form_editor): - self._form_editor = form_editor - - def isInitialized(self): - return self._form_editor is not None - - def createWidget(self, parent): - t = SampleStorageWidget(parent) - return t - - def name(self): - return "SampleStorageWidget" - - def group(self): - return "cSAXS FlOMNI" - - def toolTip(self): - return "View and correct the FlOMNI sample-storage records (bookkeeping only)." - - def whatsThis(self): - return self.toolTip() - - def isContainer(self): - return False - - def domXml(self): - return DOM_XML - - def includeFile(self): - return "csaxs_bec.bec_widgets.widgets.sample_storage.sample_storage" diff --git a/csaxs_bec/bec_widgets/widgets/slit_control/__init__.py b/csaxs_bec/bec_widgets/widgets/slit_control/__init__.py index a83c0378..9df1eb37 100644 --- a/csaxs_bec/bec_widgets/widgets/slit_control/__init__.py +++ b/csaxs_bec/bec_widgets/widgets/slit_control/__init__.py @@ -1,3 +1,3 @@ -from csaxs_bec.bec_widgets.widgets.slit_control.slit_control import SlitControlWidget +from csaxs_bec.bec_widgets.widgets.slit_control.slit_control import SlitControl -__all__ = ["SlitControlWidget"] +__all__ = ["SlitControl"] diff --git a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py b/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py index ad9960e6..7dccc230 100644 --- a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py +++ b/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py @@ -71,7 +71,7 @@ _KB_STEP = [ ] -class SlitControlWidget(BECWidget, QWidget): +class SlitControl(BECWidget, QWidget): """ Interactive GUI for cSAXS slit center and size control. @@ -505,7 +505,7 @@ class SlitControlWidget(BECWidget, QWidget): device.move(target) except Exception as exc: msg = str(exc) or type(exc).__name__ - logger.warning(f"SlitControlWidget: absolute move {dev_name} failed: {msg}") + logger.warning(f"SlitControl: absolute move {dev_name} failed: {msg}") self._show_status(f"{dev_name}: {msg}") def _stop_slit(self) -> None: @@ -516,7 +516,7 @@ class SlitControlWidget(BECWidget, QWidget): device.stop() except Exception as exc: msg = str(exc) or type(exc).__name__ - logger.warning(f"SlitControlWidget: stop {self._dev_name(suffix)} failed: {msg}") + logger.warning(f"SlitControl: stop {self._dev_name(suffix)} failed: {msg}") self._show_status(f"Stop failed: {msg}") def _show_status(self, msg: str, error: bool = True) -> None: @@ -658,7 +658,7 @@ class SlitControlWidget(BECWidget, QWidget): device.move(current + delta) except Exception as exc: msg = str(exc) or type(exc).__name__ - logger.warning(f"SlitControlWidget: failed to move {dev_name}: {msg}") + logger.warning(f"SlitControl: failed to move {dev_name}: {msg}") self._show_status(f"{dev_name}: {msg}") def set_step(self, value: float): diff --git a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control_widget.pyproject b/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.pyproject similarity index 100% rename from csaxs_bec/bec_widgets/widgets/slit_control/slit_control_widget.pyproject rename to csaxs_bec/bec_widgets/widgets/slit_control/slit_control.pyproject diff --git a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control_plugin.py b/csaxs_bec/bec_widgets/widgets/slit_control/slit_control_plugin.py index c8499007..11c5e82d 100644 --- a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control_plugin.py +++ b/csaxs_bec/bec_widgets/widgets/slit_control/slit_control_plugin.py @@ -1,15 +1,15 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from bec_widgets.utils.bec_designer import designer_material_icon from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtWidgets import QWidget -from csaxs_bec.bec_widgets.widgets.slit_control.slit_control import SlitControlWidget +from bec_widgets.utils.bec_designer import designer_material_icon +from csaxs_bec.bec_widgets.widgets.slit_control.slit_control import SlitControl DOM_XML = """ - + """ @@ -23,7 +23,8 @@ class SlitControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover def createWidget(self, parent): if parent is None: return QWidget() - return SlitControlWidget(parent) + t = SlitControl(parent) + return t def domXml(self): return DOM_XML @@ -32,7 +33,7 @@ class SlitControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover return "" def icon(self): - return designer_material_icon(SlitControlWidget.ICON_NAME) + return designer_material_icon(SlitControl.ICON_NAME) def includeFile(self): return "slit_control" @@ -47,10 +48,10 @@ class SlitControlPlugin(QDesignerCustomWidgetInterface): # pragma: no cover return self._form_editor is not None def name(self): - return "SlitControlWidget" + return "SlitControl" def toolTip(self): - return "SlitControlWidget" + return "" def whatsThis(self): return self.toolTip() diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/__init__.py b/csaxs_bec/bec_widgets/widgets/tomo_params/__init__.py index 6807c87e..fb40b8e9 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/__init__.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/__init__.py @@ -1,3 +1,3 @@ -from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params import TomoParamsWidget +from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params import OMNY_TomoParams -__all__ = ["TomoParamsWidget"] +__all__ = ["OMNY_TomoParams"] diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_widget.pyproject b/csaxs_bec/bec_widgets/widgets/tomo_params/omny_tomo_params.pyproject similarity index 100% rename from csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_widget.pyproject rename to csaxs_bec/bec_widgets/widgets/tomo_params/omny_tomo_params.pyproject diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_widget_plugin.py b/csaxs_bec/bec_widgets/widgets/tomo_params/omny_tomo_params_plugin.py similarity index 75% rename from csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_widget_plugin.py rename to csaxs_bec/bec_widgets/widgets/tomo_params/omny_tomo_params_plugin.py index ad26d47b..572b01a0 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_widget_plugin.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/omny_tomo_params_plugin.py @@ -5,17 +5,17 @@ from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtWidgets import QWidget from bec_widgets.utils.bec_designer import designer_material_icon -from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params import TomoParamsWidget +from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params import OMNY_TomoParams DOM_XML = """ - + """ -class TomoParamsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover +class OMNY_TomoParamsPlugin(QDesignerCustomWidgetInterface): # pragma: no cover def __init__(self): super().__init__() self._form_editor = None @@ -23,7 +23,7 @@ class TomoParamsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cove def createWidget(self, parent): if parent is None: return QWidget() - t = TomoParamsWidget(parent) + t = OMNY_TomoParams(parent) return t def domXml(self): @@ -33,10 +33,10 @@ class TomoParamsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cove return "" def icon(self): - return designer_material_icon(TomoParamsWidget.ICON_NAME) + return designer_material_icon(OMNY_TomoParams.ICON_NAME) def includeFile(self): - return "tomo_params_widget" + return "omny_tomo_params" def initialize(self, form_editor): self._form_editor = form_editor @@ -48,7 +48,7 @@ class TomoParamsWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cove return self._form_editor is not None def name(self): - return "TomoParamsWidget" + return "OMNY_TomoParams" def toolTip(self): return "" diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/register_tomo_params_widget.py b/csaxs_bec/bec_widgets/widgets/tomo_params/register_omny_tomo_params.py similarity index 62% rename from csaxs_bec/bec_widgets/widgets/tomo_params/register_tomo_params_widget.py rename to csaxs_bec/bec_widgets/widgets/tomo_params/register_omny_tomo_params.py index 3e7497e7..6fcae542 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/register_tomo_params_widget.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/register_omny_tomo_params.py @@ -6,9 +6,9 @@ def main(): # pragma: no cover return from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection - from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params_widget_plugin import TomoParamsWidgetPlugin + from csaxs_bec.bec_widgets.widgets.tomo_params.omny_tomo_params_plugin import OMNY_TomoParamsPlugin - QPyDesignerCustomWidgetCollection.addCustomWidget(TomoParamsWidgetPlugin()) + QPyDesignerCustomWidgetCollection.addCustomWidget(OMNY_TomoParamsPlugin()) if __name__ == "__main__": # pragma: no cover diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py index 0e457fd6..0c6341c6 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py @@ -1,5 +1,5 @@ """ -TomoParamsWidget – BEC widget for editing tomo scan parameters and managing +OMNY_TomoParams – BEC widget for editing tomo scan parameters and managing the tomo scan queue, for whichever cSAXS tomography setup (flomni, lamni, ...) is active in the current BEC session -- see ``SETUP_PROFILES``. @@ -239,7 +239,7 @@ def _hline() -> QFrame: # ── main widget ────────────────────────────────────────────────────────────── -class TomoParamsWidget(BECWidget, QWidget): +class OMNY_TomoParams(BECWidget, QWidget): """ Interactive GUI for tomo scan parameter editing and queue management, for whichever setup (see ``SETUP_PROFILES``) is active in this BEC session. @@ -303,7 +303,7 @@ class TomoParamsWidget(BECWidget, QWidget): self._on_queue_status, MessageEndpoints.scan_queue_status() ) except Exception as exc: - logger.warning(f"TomoParamsWidget: could not subscribe to scan queue: {exc}") + logger.warning(f"OMNY_TomoParams: could not subscribe to scan queue: {exc}") self._refresh_busy_banner() # ── setup detection ────────────────────────────────────────────────────── @@ -330,7 +330,7 @@ class TomoParamsWidget(BECWidget, QWidget): try: return self.client.get_global_var(key) except Exception as exc: - logger.warning(f"TomoParamsWidget: get_global_var({key!r}) failed: {exc}") + logger.warning(f"OMNY_TomoParams: get_global_var({key!r}) failed: {exc}") return None def _gv_set(self, key: str, value: Any) -> bool: @@ -338,7 +338,7 @@ class TomoParamsWidget(BECWidget, QWidget): self.client.set_global_var(key, value) return True except Exception as exc: - logger.warning(f"TomoParamsWidget: set_global_var({key!r}) failed: {exc}") + logger.warning(f"OMNY_TomoParams: set_global_var({key!r}) failed: {exc}") return False def _load_params(self) -> dict[str, Any]: @@ -1001,7 +1001,7 @@ class TomoParamsWidget(BECWidget, QWidget): return False return not all(item.status in _QUEUE_IDLE_STATUSES for item in queue_info) except Exception as exc: - logger.warning(f"TomoParamsWidget: scan-queue busy check failed: {exc}") + logger.warning(f"OMNY_TomoParams: scan-queue busy check failed: {exc}") return False def _beamline_busy_reason(self) -> Optional[str]: @@ -1339,7 +1339,7 @@ class TomoQueueDialog(QDialog): """ Non-modal popup window for managing the tomo scan queue. - Opened from the main TomoParamsWidget via the "Queue control…" button, + Opened from the main OMNY_TomoParams via the "Queue control…" button, which passes its own detected ``profile`` (see ``SETUP_PROFILES``) down so this dialog's "Add current params to queue" reads the right param names/defaults for whichever setup is active -- it never detects the @@ -1350,7 +1350,7 @@ class TomoQueueDialog(QDialog): _POLL_MS = 2000 - # Same heartbeat-staleness window as TomoParamsWidget._is_tomo_running(). + # Same heartbeat-staleness window as OMNY_TomoParams._is_tomo_running(). # A "running" job whose tomo_progress heartbeat is older than this (or # missing) means the process that was executing it died -- crash, # Ctrl-C, kernel restart -- before it had a chance to update the job's @@ -1712,7 +1712,7 @@ class TomoQueueDialog(QDialog): """True if there's no evidence a scan is genuinely active right now -- tomo_progress's heartbeat is missing or older than _RUNNING_STALE_AFTER_S. Same signal/window as - TomoParamsWidget._is_tomo_running().""" + OMNY_TomoParams._is_tomo_running().""" prog = self._gv_get("tomo_progress") if not isinstance(prog, dict): return True diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_plugin.py b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_plugin.py deleted file mode 100644 index e3a67741..00000000 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params_plugin.py +++ /dev/null @@ -1,53 +0,0 @@ -from bec_widgets.utils.bec_designer import designer_material_icon -from qtpy.QtDesigner import QDesignerCustomWidgetInterface -from qtpy.QtWidgets import QWidget - -from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params import TomoParamsWidget - -DOM_XML = """ - - - - -""" - - -class TomoParamsPlugin(QDesignerCustomWidgetInterface): # pragma: no cover - def __init__(self): - super().__init__() - self._form_editor = None - - def createWidget(self, parent): - if parent is None: - return QWidget() - return TomoParamsWidget(parent) - - def domXml(self): - return DOM_XML - - def group(self): - return "" - - def icon(self): - return designer_material_icon(TomoParamsWidget.ICON_NAME) - - def includeFile(self): - return "tomo_params" - - 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 "TomoParamsWidget" - - def toolTip(self): - return "TomoParamsWidget" - - def whatsThis(self): - return self.toolTip() diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.pyproject b/csaxs_bec/bec_widgets/widgets/xray_eye/omny_x_ray_eye.pyproject similarity index 100% rename from csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.pyproject rename to csaxs_bec/bec_widgets/widgets/xray_eye/omny_x_ray_eye.pyproject diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye_plugin.py b/csaxs_bec/bec_widgets/widgets/xray_eye/omny_x_ray_eye_plugin.py similarity index 68% rename from csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye_plugin.py rename to csaxs_bec/bec_widgets/widgets/xray_eye/omny_x_ray_eye_plugin.py index f611e471..0de5d2d9 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye_plugin.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/omny_x_ray_eye_plugin.py @@ -1,29 +1,29 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from bec_widgets.utils.bec_designer import designer_material_icon from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtWidgets import QWidget -from csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye import XRayEye +from bec_widgets.utils.bec_designer import designer_material_icon +from csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye import OMNY_XRayEye DOM_XML = """ - + """ -class XRayEyePlugin(QDesignerCustomWidgetInterface): # pragma: no cover +class OMNY_XRayEyePlugin(QDesignerCustomWidgetInterface): # pragma: no cover def __init__(self): super().__init__() self._form_editor = None def createWidget(self, parent): if parent is None: - return QWidget() - t = XRayEye(parent) + return QWidget() + t = OMNY_XRayEye(parent) return t def domXml(self): @@ -33,10 +33,10 @@ class XRayEyePlugin(QDesignerCustomWidgetInterface): # pragma: no cover return "" def icon(self): - return designer_material_icon(XRayEye.ICON_NAME) + return designer_material_icon(OMNY_XRayEye.ICON_NAME) def includeFile(self): - return "x_ray_eye" + return "omny_x_ray_eye" def initialize(self, form_editor): self._form_editor = form_editor @@ -48,10 +48,10 @@ class XRayEyePlugin(QDesignerCustomWidgetInterface): # pragma: no cover return self._form_editor is not None def name(self): - return "XRayEye" + return "OMNY_XRayEye" def toolTip(self): - return "XRayEye" + return "OMNY_XRayEye" def whatsThis(self): return self.toolTip() diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/register_x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/register_omny_x_ray_eye.py similarity index 64% rename from csaxs_bec/bec_widgets/widgets/xray_eye/register_x_ray_eye.py rename to csaxs_bec/bec_widgets/widgets/xray_eye/register_omny_x_ray_eye.py index 8fd0e39e..d3ef6a4c 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/register_x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/register_omny_x_ray_eye.py @@ -6,9 +6,9 @@ def main(): # pragma: no cover return from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection - from csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye_plugin import XRayEyePlugin + from csaxs_bec.bec_widgets.widgets.xray_eye.omny_x_ray_eye_plugin import OMNY_XRayEyePlugin - QPyDesignerCustomWidgetCollection.addCustomWidget(XRayEyePlugin()) + QPyDesignerCustomWidgetCollection.addCustomWidget(OMNY_XRayEyePlugin()) if __name__ == "__main__": # pragma: no cover diff --git a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py index fb5c4bad..6431f5ed 100644 --- a/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py +++ b/csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py @@ -46,7 +46,7 @@ class TargetCrosshair: view while the user works through subsequent steps. Position is expressed in the same image/data coordinate system used by - the widget's ROIs (see ``XRayEye.submit``), so values read back from + the widget's ROIs (see ``OMNY_XRayEye.submit``), so values read back from ``omny_xray_gui.xval_x_*`` / ``yval_y_*`` can be passed in directly. """ @@ -89,7 +89,7 @@ class ImageZoomControl(QWidget): Mouse-wheel zoom steps can be far too coarse to use precisely over a remote desktop connection, so this provides explicit zoom in/out buttons plus a "fit to view" button that resets pan/zoom to frame the current - image (the same operation as ``XRayEye.reset_zoom()``). + image (the same operation as ``OMNY_XRayEye.reset_zoom()``). """ # scaleBy() factor for one "zoom in" click; >1 (its inverse) zooms out. @@ -231,7 +231,7 @@ class XRayEye2DControl(BECWidget, QWidget): logger.warning(f"Unknown direction {direction} for move command.") -class XRayEye(BECWidget, QWidget): +class OMNY_XRayEye(BECWidget, QWidget): USER_ACCESS = [ "active_roi", "user_message", @@ -683,7 +683,7 @@ class XRayEye(BECWidget, QWidget): try: msg = self.client.connector.get(MessageEndpoints.scan_queue_status()) except Exception as exc: - logger.warning(f"Failed to fetch initial scan queue status for XRayEye: {exc}") + logger.warning(f"Failed to fetch initial scan queue status for OMNY_XRayEye: {exc}") return if msg is None: @@ -957,7 +957,7 @@ class XRayEye(BECWidget, QWidget): for signal in signals: signal.connect(self.on_dap_params) self._dap_params_forwarding_connected = True - logger.info("Enabled XRayEye DAP parameter forwarding.") + logger.info("Enabled OMNY_XRayEye DAP parameter forwarding.") return for signal in signals: @@ -966,7 +966,7 @@ class XRayEye(BECWidget, QWidget): except (TypeError, RuntimeError): pass self._dap_params_forwarding_connected = False - logger.info("Disabled XRayEye DAP parameter forwarding.") + logger.info("Disabled OMNY_XRayEye DAP parameter forwarding.") @SafeSlot(dict, dict) def on_dap_params(self, data, meta): @@ -1070,7 +1070,7 @@ if __name__ == "__main__": app = QApplication(sys.argv) apply_theme("light") dispatcher = BECDispatcher(gui_id="xray") - win = XRayEye() + win = OMNY_XRayEye() win.resize(1000, 800) win.show()