From 5d21216f63549e9f78e72b6dfbf36f3a397db624 Mon Sep 17 00:00:00 2001 From: x01da Date: Mon, 10 Aug 2026 14:56:19 +0200 Subject: [PATCH] wip widget --- .../bec_widgets/widgets/edge_selector.py | 339 ++++++++++++++++++ .../widgets/scan_control_xas/qt_widgets.py | 34 ++ .../scan_control_xas/scan_control_xas.py | 107 ++++-- 3 files changed, 452 insertions(+), 28 deletions(-) create mode 100644 debye_bec/bec_widgets/widgets/edge_selector.py diff --git a/debye_bec/bec_widgets/widgets/edge_selector.py b/debye_bec/bec_widgets/widgets/edge_selector.py new file mode 100644 index 0000000..e578616 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/edge_selector.py @@ -0,0 +1,339 @@ +import sys +from typing import Literal, Optional + +import xraydb +from bec_lib import bec_logger +from bec_widgets.utils.bec_dispatcher import BECDispatcher +from bec_widgets.utils.colors import apply_theme, get_accent_colors +from bec_widgets.utils.error_popups import SafeSlot +from qtpy.QtCore import Signal + +# pylint: disable=E0611 +from qtpy.QtWidgets import ( + QApplication, + QComboBox, + QDialog, + QGridLayout, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QVBoxLayout, + QWidget, +) + +logger = bec_logger.logger + +ELEMENTS = { + # period 1 + "H": (1, 1), + "He": (1, 18), + # period 2 + "Li": (2, 1), + "Be": (2, 2), + "B": (2, 13), + "C": (2, 14), + "N": (2, 15), + "O": (2, 16), + "F": (2, 17), + "Ne": (2, 18), + # period 3 + "Na": (3, 1), + "Mg": (3, 2), + "Al": (3, 13), + "Si": (3, 14), + "P": (3, 15), + "S": (3, 16), + "Cl": (3, 17), + "Ar": (3, 18), + # period 4 + "K": (4, 1), + "Ca": (4, 2), + "Sc": (4, 3), + "Ti": (4, 4), + "V": (4, 5), + "Cr": (4, 6), + "Mn": (4, 7), + "Fe": (4, 8), + "Co": (4, 9), + "Ni": (4, 10), + "Cu": (4, 11), + "Zn": (4, 12), + "Ga": (4, 13), + "Ge": (4, 14), + "As": (4, 15), + "Se": (4, 16), + "Br": (4, 17), + "Kr": (4, 18), + # period 5 + "Rb": (5, 1), + "Sr": (5, 2), + "Y": (5, 3), + "Zr": (5, 4), + "Nb": (5, 5), + "Mo": (5, 6), + "Tc": (5, 7), + "Ru": (5, 8), + "Rh": (5, 9), + "Pd": (5, 10), + "Ag": (5, 11), + "Cd": (5, 12), + "In": (5, 13), + "Sn": (5, 14), + "Sb": (5, 15), + "Te": (5, 16), + "I": (5, 17), + "Xe": (5, 18), + # period 6 + "Cs": (6, 1), + "Ba": (6, 2), + "La": (6, 3), + # lanthanides (row 8) + "Ce": (8, 4), + "Pr": (8, 5), + "Nd": (8, 6), + "Pm": (8, 7), + "Sm": (8, 8), + "Eu": (8, 9), + "Gd": (8, 10), + "Tb": (8, 11), + "Dy": (8, 12), + "Ho": (8, 13), + "Er": (8, 14), + "Tm": (8, 15), + "Yb": (8, 16), + "Lu": (8, 17), + # period 6 continued + "Hf": (6, 4), + "Ta": (6, 5), + "W": (6, 6), + "Re": (6, 7), + "Os": (6, 8), + "Ir": (6, 9), + "Pt": (6, 10), + "Au": (6, 11), + "Hg": (6, 12), + "Tl": (6, 13), + "Pb": (6, 14), + "Bi": (6, 15), + "Po": (6, 16), + "At": (6, 17), + "Rn": (6, 18), + # period 7 + "Fr": (7, 1), + "Ra": (7, 2), + "Ac": (7, 3), + # actinides (row 9) + "Th": (9, 4), + "Pa": (9, 5), + "U": (9, 6), + "Np": (9, 7), + "Pu": (9, 8), + "Am": (9, 9), + "Cm": (9, 10), + "Bk": (9, 11), + "Cf": (9, 12), +} + + +class EdgeSelector(QDialog): + """BEC widget to display a selection dialog where the user can select and edge and an element""" + + def __init__(self, parent=None, llim: int | float = 0, hlim: int | float = 1e6): + super().__init__(parent) + self.setWindowTitle("Absorption Edge Selector") + self.buttons = {} + self.active_edge = None + self.active_element = None + self.active_energy = None + self.llim = llim + self.hlim = hlim + + layout = QVBoxLayout(self) + + text = QLabel("Select edge and element.") + + edge_layout = QHBoxLayout(self) + edge_label = QLabel("Edge") + self.edge = QComboBox() + self.edge.addItems(["K", "L1", "L2", "L3"]) + edge_layout.addWidget(edge_label) + edge_layout.addWidget(self.edge) + edge_layout.addStretch() + + grid_layout = QGridLayout(self) + grid_layout.setContentsMargins(0, 0, 0, 0) + grid_layout.setSpacing(1) + + for element, (row, col) in ELEMENTS.items(): + button = QPushButton(element) + button.setFixedSize(42, 32) + button.setCheckable(True) + button.setAutoDefault(False) + button.setDefault(False) + button.clicked.connect(lambda checked=False, e=element: self._element_changed(e)) + grid_layout.addWidget(button, row - 1, col - 1) + self.buttons[element] = button + + res_layout = QHBoxLayout(self) + self.res = QLabel("") + res_layout.addStretch() + res_layout.addWidget(self.res) + res_layout.addStretch() + + button_layout = QHBoxLayout() + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(self.reject) + self.cancel_button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + self.select_button = QPushButton("Select") + self.select_button.setEnabled(False) + self.select_button.clicked.connect(self.accept) + self.select_button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + + button_layout.addStretch() + button_layout.addWidget(self.cancel_button) + button_layout.addWidget(self.select_button) + button_layout.addStretch() + + layout.addWidget(text) + layout.addLayout(edge_layout) + layout.addLayout(grid_layout) + layout.addLayout(res_layout) + layout.addLayout(button_layout) + + self.edge.activated.connect(self._edge_changed) + self._edge_changed() + + @SafeSlot() + def _edge_changed(self, *_): + self.active_edge = self.edge.currentText() + for element, button in self.buttons.items(): + # logger.info(f"{element}") + if "L" in self.active_edge and element in ["H", "He"]: + button.setDisabled(True) + button.setChecked(False) + elif self.active_edge in ["L2", "L3"] and element == "Li": + button.setDisabled(True) + button.setChecked(False) + else: + energy = xraydb.xray_edge(element, self.active_edge, True) + if self.active_element == element: + self.active_energy = energy + if energy is not None: + if energy < self.llim or energy > self.hlim: + button.setChecked(False) + button.setDisabled(True) + else: + button.setEnabled(True) + else: + button.setChecked(False) + button.setDisabled(True) + self._set_select_text() + + @SafeSlot() + def _element_changed(self, element: str | None = None): + if element is None: + if self.active_element is None: + return + element = self.active_element + # Uncheck all other buttons so only the picked one stays highlighted + for e, btn in self.buttons.items(): + if e != element: + btn.setChecked(False) + self.buttons[element].setChecked(True) + self.active_element = element + self.active_energy = xraydb.xray_edge(element, self.active_edge, True) + self.select_button.setEnabled(True) + self._set_select_text() + + def _set_select_text(self): + if self.active_energy is not None: + if self.buttons[self.active_element].isChecked(): + self._enable_button(self.select_button, True) + self.select_button.setText( + f"Selected {self.active_element}, {self.active_edge}-edge, {self.active_energy:0.1f} eV" + ) + else: + self._enable_button(self.select_button, False) + self.select_button.setText("Select") + else: + self._enable_button(self.select_button, False) + + @staticmethod + def _enable_button(button: QPushButton, enable: bool): + if enable: + button.setEnabled(True) + button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + else: + button.setDisabled(True) + button.setStyleSheet( + "QPushButton {{background-color: rgb(120, 120, 120); color: white;}}" + ) + + +ROW_MARGINS = (4, 0, 4, 0) +ROW_SPACING = 6 + + +class ElementSelector(QWidget): + + dialogClosed = Signal() + + def __init__(self): + super().__init__() + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + layout = QVBoxLayout(self) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) + self.label = QLabel("Element") + layout_selection = QHBoxLayout(self) + self.button = QPushButton("Select") + self.button.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + layout_selection.addWidget(self.button) + layout.addWidget(self.label) + layout.addLayout(layout_selection) + self.current_element = "" + + self.button.clicked.connect(self.open_table) + + def open_table(self): + dialog = EdgeSelector(self, llim=4500, hlim=60000) + if dialog.exec_(): + # self.current_element = dialog.selected + self.dialogClosed.emit() + + def currentElement(self): + return self.current_element + + 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 + + self.button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + apply_theme("light") + dispatcher = BECDispatcher(gui_id="scan_control_advanced") + win = ElementSelector() + win.show() + sys.exit(app.exec_()) diff --git a/debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py b/debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py index d16d011..0f4f1ef 100644 --- a/debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/scan_control_xas/qt_widgets.py @@ -23,6 +23,40 @@ ROW_MARGINS = (4, 0, 4, 0) ROW_SPACING = 6 +class Button(QWidget): + def __init__(self, label=None, label_button: str = "", enabled=False): + super().__init__() + layout = QHBoxLayout(self) + layout.setContentsMargins(*ROW_MARGINS) + layout.setSpacing(ROW_SPACING) + if label is not None: + self.label = QLabel(label) + self.label.setFixedWidth(LABEL_WIDTH) + 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 ComboBox(QWidget): def __init__(self, identifier="", label="", enums=None): super().__init__() diff --git a/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.py b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.py index 5ac47bd..ee6f4a8 100644 --- a/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.py +++ b/debye_bec/bec_widgets/widgets/scan_control_xas/scan_control_xas.py @@ -11,7 +11,7 @@ import xraydb from bec_lib import bec_logger from bec_lib.endpoints import MessageEndpoints from bec_widgets.utils.bec_dispatcher import BECDispatcher -from bec_widgets.utils.colors import Colors, apply_theme +from bec_widgets.utils.colors import Colors, apply_theme, get_accent_colors from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.widgets.control.scan_control.scan_control import ScanControl from qtpy.QtCore import Qt @@ -19,9 +19,12 @@ from qtpy.QtCore import Qt # pylint: disable=E0611 from qtpy.QtWidgets import ( QApplication, + QCheckBox, QFrame, QGroupBox, QHBoxLayout, + QLabel, + QPushButton, QScrollArea, QSizePolicy, QVBoxLayout, @@ -30,7 +33,8 @@ from qtpy.QtWidgets import ( # pylint: disable=E0402 from ....devices.mo1_bragg.mo1_bragg_utils import compute_spline -from .qt_widgets import ComboBox, ElementSelector, TextIndicator +from ..edge_selector import EdgeSelector +from .qt_widgets import Button, ComboBox, ElementSelector, TextIndicator logger = bec_logger.logger @@ -141,9 +145,26 @@ class MotionProfileWidget(QWidget): outer_layout = QVBoxLayout(self) outer_layout.setContentsMargins(0, 0, 0, 0) - self.element = ElementSelector() - self.edge = ComboBox("Edge", "Edge", ["K", "L1", "L2", "L3"]) - self.edge_text = TextIndicator("", "Selected Edge", "No edge selected") + # self.element = ElementSelector() + # self.edge = ComboBox("Edge", "Edge", ["K", "L1", "L2", "L3"]) + # self.edge_text = TextIndicator("", "Selected Edge", "No edge selected") + + edge_selector_layout = QHBoxLayout(self) + edge_selector_label = QLabel("Absorption edge:") + self.edge_selector_button = QPushButton("Choose") + self.edge_label = QLabel("No edge selected") + edge_selector_layout.addWidget(edge_selector_label) + edge_selector_layout.addWidget(self.edge_label) + edge_selector_layout.addWidget(self.edge_selector_button) + edge_selector_layout.addStretch() + + scan_rng_layout = QHBoxLayout(self) + self.scan_rng_ckbox = QCheckBox(self) + self.scan_rng_ckbox.setCheckState(Qt.CheckState.Checked) + scan_rng_label = QLabel("Auto adjust scan range to absorption edge") + scan_rng_layout.addWidget(self.scan_rng_ckbox) + scan_rng_layout.addWidget(scan_rng_label) + scan_rng_layout.addStretch() plot_container = QWidget() plot_layout = QHBoxLayout(plot_container) @@ -169,20 +190,25 @@ class MotionProfileWidget(QWidget): scroll.setWidget(plot_container) self.edge_selection = QHBoxLayout() - self.edge_selection.addWidget(self.element) - self.edge_selection.addWidget(self.edge) - self.edge_selection.addWidget(self.edge_text) + # self.edge_selection.addWidget(self.element) + # self.edge_selection.addWidget(self.edge) + # self.edge_selection.addWidget(self.edge_text) + self.edge_selection.addLayout(edge_selector_layout) self.edge_selection.addStretch(1) outer_layout.addLayout(self.edge_selection) - + outer_layout.addLayout(scan_rng_layout) outer_layout.addWidget(scroll, stretch=1) self.scan_name = None self.scan_parameters = None self.d_spacing = None - self.element.dialogClosed.connect(self._update_edge_selection) - self.edge.activated_connect(self._update_edge_selection) + self.edge_selector_button.clicked.connect(self._update_edge) + self.scan_rng_ckbox.stateChanged.connect(self._update_auto_scan_rng) + # self.element.dialogClosed.connect(self._update_edge_selection) + # self.edge.activated_connect(self._update_edge_selection) + + self.apply_theme() def _make_plot(self, left_label: str, bottom_label: str) -> pg.PlotWidget: plot = pg.PlotWidget() @@ -217,27 +243,48 @@ class MotionProfileWidget(QWidget): ) plot.addItem(scatter, ignoreBounds=True) - text = f"{self.element.currentElement()}, {self.edge.currentText()}-edge, {self.edge_energy:0.1f} eV" - label = pg.TextItem(text, anchor=(0, 1.5), color="r") + label = pg.TextItem(self.edge_label.text(), anchor=(0, 1.5), color="r") label.setPos(x_marker, y_marker) plot.addItem(label, ignoreBounds=True) @SafeSlot() - def _update_edge_selection(self, *_, **__): - if self.element.currentElement() == "": - return - if self.edge.currentText() == "": - return - self.edge_energy = xraydb.xray_edge( - self.element.currentElement(), self.edge.currentText(), True - ) - if self.edge_energy is not None: - self.edge_text.setText( - f"{self.element.currentElement()}, {self.edge.currentText()}-edge, {self.edge_energy:0.1f} eV" + def _update_edge(self, *_): + dlg = EdgeSelector(self, llim=4500, hlim=60000) + if dlg.exec_(): + self.edge_energy = dlg.active_energy + self.edge_label.setText( + f"{dlg.active_element}, {dlg.active_edge}-edge, {dlg.active_energy:0.1f} eV" ) - else: - self.edge_text.setText("No valid edge selected!") - self.update_plot() + self._update_auto_scan_rng() + self.update_plot() + + @SafeSlot() + def _update_auto_scan_rng(self, *_): + if self.scan_rng_ckbox.isChecked() and self.edge_energy is not None: + self._parent_widget._restore_kwargs( + { + "start": self.edge_energy - 200, + "stop": self.edge_energy + 1000, + "e_kink": self.edge_energy + 1000, + } + ) + + # @SafeSlot() + # def _update_edge_selection(self, *_, **__): + # if self.element.currentElement() == "": + # return + # if self.edge.currentText() == "": + # return + # self.edge_energy = xraydb.xray_edge( + # self.element.currentElement(), self.edge.currentText(), True + # ) + # if self.edge_energy is not None: + # self.edge_text.setText( + # f"{self.element.currentElement()}, {self.edge.currentText()}-edge, {self.edge_energy:0.1f} eV" + # ) + # else: + # self.edge_text.setText("No valid edge selected!") + # self.update_plot() @SafeSlot() def update_plot(self, *_, scan_name=None, scan_parameters=None, d_spacing=None, **__): @@ -331,6 +378,10 @@ class MotionProfileWidget(QWidget): app = QApplication.instance() theme = app.theme.theme # type: ignore + self.edge_selector_button.setStyleSheet( + f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}" + ) + bg_color = pg.getConfigOption("background") fg_color = pg.getConfigOption("foreground") @@ -346,7 +397,7 @@ class MotionProfileWidget(QWidget): for curve, color in zip(curves, colors): if not isinstance(curve, pg.ScatterPlotItem): curve.setPen(pg.mkPen(color=color, width=2)) - self.element.apply_theme(theme) + # self.element.apply_theme(theme) def _energy_to_angle(self, energy: int | float): if self.d_spacing is None: