wip widget
This commit is contained in:
@@ -4,7 +4,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@@ -15,7 +14,7 @@ logger = bec_logger.logger
|
|||||||
_Widgets = {
|
_Widgets = {
|
||||||
"DataViewer": "DataViewer",
|
"DataViewer": "DataViewer",
|
||||||
"DigitalTwin": "DigitalTwin",
|
"DigitalTwin": "DigitalTwin",
|
||||||
"ScanControlAdvanced": "ScanControlAdvanced",
|
"ScanControlXAS": "ScanControlXAS",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -67,8 +66,8 @@ class DigitalTwin(RPCBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ScanControlAdvanced(RPCBase):
|
class ScanControlXAS(RPCBase):
|
||||||
_IMPORT_MODULE = "debye_bec.bec_widgets.widgets.scan_control_advanced.scan_control_advanced"
|
_IMPORT_MODULE = "debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas"
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def attach(self):
|
def attach(self):
|
||||||
|
|||||||
@@ -7,14 +7,10 @@ from __future__ import annotations
|
|||||||
designer_plugins = {
|
designer_plugins = {
|
||||||
"DataViewer": ("debye_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"),
|
"DataViewer": ("debye_bec.bec_widgets.widgets.data_viewer.data_viewer", "DataViewer"),
|
||||||
"DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"),
|
"DigitalTwin": ("debye_bec.bec_widgets.widgets.digital_twin.digital_twin", "DigitalTwin"),
|
||||||
"ScanControlAdvanced": (
|
"ScanControlXAS": (
|
||||||
"debye_bec.bec_widgets.widgets.scan_control_advanced.scan_control_advanced",
|
"debye_bec.bec_widgets.widgets.scan_control_xas.scan_control_xas",
|
||||||
"ScanControlAdvanced",
|
"ScanControlXAS",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
widget_icons = {
|
widget_icons = {"DataViewer": "find_in_page", "DigitalTwin": "lightbulb", "ScanControlXAS": "tune"}
|
||||||
"DataViewer": "find_in_page",
|
|
||||||
"DigitalTwin": "lightbulb",
|
|
||||||
"ScanControlAdvanced": "tune",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{'files': ['scan_control_advanced.py']}
|
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
from typing import Literal, Optional
|
||||||
|
|
||||||
|
from bec_widgets.utils.colors import 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
LABEL_WIDTH = 118
|
||||||
|
ROW_MARGINS = (4, 0, 4, 0)
|
||||||
|
ROW_SPACING = 6
|
||||||
|
|
||||||
|
|
||||||
|
class ComboBox(QWidget):
|
||||||
|
def __init__(self, identifier="", label="", enums=None):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(*ROW_MARGINS)
|
||||||
|
layout.setSpacing(ROW_SPACING)
|
||||||
|
|
||||||
|
self.identifier = identifier
|
||||||
|
|
||||||
|
self.label = QLabel(label)
|
||||||
|
self.label.setFixedWidth(LABEL_WIDTH)
|
||||||
|
self.label.setWordWrap(True)
|
||||||
|
self.label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
self.value = QComboBox()
|
||||||
|
self.value.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
|
|
||||||
|
for entry in enums or []:
|
||||||
|
self.value.addItem(entry)
|
||||||
|
|
||||||
|
layout.addWidget(self.value)
|
||||||
|
|
||||||
|
def set_current_text(self, text):
|
||||||
|
self.value.setCurrentText(text)
|
||||||
|
|
||||||
|
def currentText(self) -> str:
|
||||||
|
return self.value.currentText()
|
||||||
|
|
||||||
|
def has_focus(self) -> bool:
|
||||||
|
return QApplication.focusWidget() is self.value.view()
|
||||||
|
|
||||||
|
def activated_connect(self, func):
|
||||||
|
"""Connect a function to the Enter/Return key press."""
|
||||||
|
self.value.activated.connect(func)
|
||||||
|
|
||||||
|
def setDisabled(self, disable):
|
||||||
|
self.value.setDisabled(disable)
|
||||||
|
|
||||||
|
|
||||||
|
class TextIndicator(QWidget):
|
||||||
|
def __init__(self, identifier="", label="", text=""):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(*ROW_MARGINS)
|
||||||
|
layout.setSpacing(ROW_SPACING)
|
||||||
|
|
||||||
|
self.identifier = identifier
|
||||||
|
|
||||||
|
self.label = QLabel(label)
|
||||||
|
self.label.setFixedWidth(LABEL_WIDTH)
|
||||||
|
self.label.setWordWrap(True)
|
||||||
|
self.label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
||||||
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
|
self.value = QLabel(text)
|
||||||
|
self.value.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
|
|
||||||
|
layout.addWidget(self.value)
|
||||||
|
|
||||||
|
def setText(self, text):
|
||||||
|
self.value.setText(text)
|
||||||
|
|
||||||
|
def text(self) -> str:
|
||||||
|
return self.value.text()
|
||||||
|
|
||||||
|
|
||||||
|
ELEMENTS = {
|
||||||
|
# period 4
|
||||||
|
"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": (8, 3),
|
||||||
|
"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),
|
||||||
|
"Pb": (6, 14),
|
||||||
|
"Bi": (6, 15),
|
||||||
|
"Po": (6, 16),
|
||||||
|
"At": (6, 17),
|
||||||
|
"Rn": (6, 18),
|
||||||
|
# period 7
|
||||||
|
"Fr": (7, 1),
|
||||||
|
"Ra": (7, 2),
|
||||||
|
"Ac": (9, 3),
|
||||||
|
"Th": (9, 4),
|
||||||
|
"Pa": (9, 5),
|
||||||
|
"U": (9, 6),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PeriodicTableDialog(QDialog):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.selected = None
|
||||||
|
|
||||||
|
layout = QGridLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(1)
|
||||||
|
|
||||||
|
for element, (row, col) in ELEMENTS.items():
|
||||||
|
button = QPushButton(element)
|
||||||
|
button.setFixedSize(42, 32)
|
||||||
|
button.clicked.connect(lambda checked=False, e=element: self.select(e))
|
||||||
|
layout.addWidget(button, row - 1, col - 1)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def select(self, element):
|
||||||
|
self.selected = element
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
|
||||||
|
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 = PeriodicTableDialog(self)
|
||||||
|
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;}}"
|
||||||
|
)
|
||||||
+2
-2
@@ -6,9 +6,9 @@ def main(): # pragma: no cover
|
|||||||
return
|
return
|
||||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||||
|
|
||||||
from .scan_control_advanced_plugin import ScanControlAdvancedPlugin
|
from .scan_control_xas_plugin import ScanControlXASPlugin
|
||||||
|
|
||||||
QPyDesignerCustomWidgetCollection.addCustomWidget(ScanControlAdvancedPlugin())
|
QPyDesignerCustomWidgetCollection.addCustomWidget(ScanControlXASPlugin())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
+65
-261
@@ -1,5 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Scan Control XAS: Custom BEC widget for Scan Control XAS scans.
|
||||||
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from functools import partial
|
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -8,43 +11,41 @@ import xraydb
|
|||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||||
from bec_widgets.utils.colors import Colors, apply_theme, get_accent_colors
|
from bec_widgets.utils.colors import Colors, apply_theme
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
||||||
from qtpy.QtCore import Qt, Signal
|
from qtpy.QtCore import Qt
|
||||||
|
|
||||||
# pylint: disable=E0611
|
# pylint: disable=E0611
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QComboBox,
|
|
||||||
QDialog,
|
|
||||||
QFrame,
|
QFrame,
|
||||||
QGridLayout,
|
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
|
||||||
QPushButton,
|
|
||||||
QScrollArea,
|
QScrollArea,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=E0402
|
||||||
from ....devices.mo1_bragg.mo1_bragg_utils import compute_spline
|
from ....devices.mo1_bragg.mo1_bragg_utils import compute_spline
|
||||||
|
from .qt_widgets import ComboBox, ElementSelector, TextIndicator
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
SHOW_MOTION_PROFILE = [
|
ALLOWED_SCANS = [
|
||||||
"xas_simple_scan",
|
"xas_simple_scan",
|
||||||
"xas_simple_scan_with_xrd",
|
"xas_simple_scan_with_xrd",
|
||||||
"xas_advanced_scan",
|
"xas_advanced_scan",
|
||||||
"xas_advanced_scan_with_xrd",
|
"xas_advanced_scan_with_xrd",
|
||||||
]
|
]
|
||||||
|
|
||||||
# TODO: Theme change edge marker. Show edge energy next to edge selector. Bugfix edge selector
|
|
||||||
|
|
||||||
|
class ScanControlXAS(ScanControl):
|
||||||
class ScanControlAdvanced(ScanControl):
|
"""
|
||||||
|
Main widget of Scan Control XAS
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._motion_profile_group = QGroupBox("Motion Profile Preview")
|
self._motion_profile_group = QGroupBox("Motion Profile Preview")
|
||||||
@@ -52,51 +53,51 @@ class ScanControlAdvanced(ScanControl):
|
|||||||
self.motion_profile_widget = MotionProfileWidget(self)
|
self.motion_profile_widget = MotionProfileWidget(self)
|
||||||
motion_profile_layout.addWidget(self.motion_profile_widget)
|
motion_profile_layout.addWidget(self.motion_profile_widget)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
allowed_scans=SHOW_MOTION_PROFILE, default_scan="xas_simple_scan", *args, **kwargs
|
allowed_scans=ALLOWED_SCANS, default_scan="xas_simple_scan", *args, **kwargs
|
||||||
)
|
)
|
||||||
self.show_motion_profile(self._selected_scan)
|
self._show_motion_profile(self._selected_scan)
|
||||||
|
|
||||||
self.d_spacing = 0
|
self.d_spacing = 0
|
||||||
self.bec_dispatcher.connect_slot(
|
self.bec_dispatcher.connect_slot(
|
||||||
self.update_d_spacing, MessageEndpoints.device_readback("mo1_bragg")
|
self._update_d_spacing, MessageEndpoints.device_readback("mo1_bragg")
|
||||||
)
|
)
|
||||||
|
|
||||||
@SafeSlot(dict, dict)
|
@SafeSlot(dict, dict)
|
||||||
def update_d_spacing(self, msg: dict, metadata: dict):
|
def _update_d_spacing(self, msg: dict, _: dict):
|
||||||
d_spacing = msg["signals"].get("mo1_bragg_crystal_current_d_spacing")["value"]
|
d_spacing = msg["signals"].get("mo1_bragg_crystal_current_d_spacing")["value"]
|
||||||
if d_spacing != self.d_spacing:
|
if d_spacing != self.d_spacing:
|
||||||
self.d_spacing = d_spacing
|
self.d_spacing = d_spacing
|
||||||
self.motion_profile_widget.update_plot(d_spacing=d_spacing)
|
self.motion_profile_widget.update_plot(d_spacing=d_spacing)
|
||||||
|
|
||||||
def _add_metadata_form(self):
|
def _add_metadata_form(self):
|
||||||
self.layout.addWidget(self._motion_profile_group, stretch=1)
|
self.layout.addWidget(self._motion_profile_group, stretch=1) # type: ignore
|
||||||
super()._add_metadata_form()
|
super()._add_metadata_form()
|
||||||
self.connect_scan_parameter_signals()
|
self._connect_scan_parameter_signals()
|
||||||
|
|
||||||
def on_scan_selection_changed(self, scan_name: str):
|
def _on_scan_selection_changed(self, scan_name: str):
|
||||||
super().on_scan_selection_changed(scan_name)
|
super()._on_scan_selection_changed(scan_name)
|
||||||
self.show_motion_profile(scan_name)
|
self._show_motion_profile(scan_name)
|
||||||
if scan_name in SHOW_MOTION_PROFILE:
|
if scan_name in ALLOWED_SCANS:
|
||||||
self.connect_scan_parameter_signals()
|
self._connect_scan_parameter_signals()
|
||||||
self.scan_parameter_changed()
|
self._scan_parameter_changed()
|
||||||
|
|
||||||
def show_motion_profile(self, scan_name):
|
def _show_motion_profile(self, scan_name):
|
||||||
if scan_name in SHOW_MOTION_PROFILE:
|
if scan_name in ALLOWED_SCANS:
|
||||||
self.motion_profile_widget.setVisible(True)
|
self.motion_profile_widget.setVisible(True)
|
||||||
else:
|
else:
|
||||||
self.motion_profile_widget.setVisible(False)
|
self.motion_profile_widget.setVisible(False)
|
||||||
|
|
||||||
def connect_scan_parameter_signals(self):
|
def _connect_scan_parameter_signals(self):
|
||||||
for box in self.kwarg_boxes:
|
for box in self.kwarg_boxes:
|
||||||
for widget in box.findChildren(QWidget):
|
for widget in box.findChildren(QWidget):
|
||||||
try:
|
try:
|
||||||
if hasattr(widget, "valueChanged"):
|
if hasattr(widget, "valueChanged"):
|
||||||
widget.valueChanged.connect(
|
widget.valueChanged.connect(
|
||||||
self.scan_parameter_changed, Qt.ConnectionType.UniqueConnection
|
self._scan_parameter_changed, Qt.ConnectionType.UniqueConnection
|
||||||
)
|
)
|
||||||
elif hasattr(widget, "textChanged"):
|
elif hasattr(widget, "textChanged"):
|
||||||
widget.textChanged.connect(
|
widget.textChanged.connect(
|
||||||
self.scan_parameter_changed, Qt.ConnectionType.UniqueConnection
|
self._scan_parameter_changed, Qt.ConnectionType.UniqueConnection
|
||||||
)
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# Raised if a connection would not be unique anymore, i.e. if
|
# Raised if a connection would not be unique anymore, i.e. if
|
||||||
@@ -104,7 +105,8 @@ class ScanControlAdvanced(ScanControl):
|
|||||||
# the same kwarg_boxes
|
# the same kwarg_boxes
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def scan_parameter_changed(self, *_):
|
@SafeSlot()
|
||||||
|
def _scan_parameter_changed(self, *_):
|
||||||
# logger.info(f"Fe K edge: {xraydb.xray_edge("Fe", "K", energy_only=True)}")
|
# logger.info(f"Fe K edge: {xraydb.xray_edge("Fe", "K", energy_only=True)}")
|
||||||
params = self.get_scan_parameters()[1]
|
params = self.get_scan_parameters()[1]
|
||||||
# logger.info(f"Scan parameters: {params}")
|
# logger.info(f"Scan parameters: {params}")
|
||||||
@@ -127,147 +129,11 @@ H = 6.62606957e-34
|
|||||||
E = 1.602176634e-19
|
E = 1.602176634e-19
|
||||||
C = 299792458
|
C = 299792458
|
||||||
|
|
||||||
ELEMENTS = {
|
|
||||||
# period 4
|
|
||||||
"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": (8, 3),
|
|
||||||
"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),
|
|
||||||
"Pb": (6, 14),
|
|
||||||
"Bi": (6, 15),
|
|
||||||
"Po": (6, 16),
|
|
||||||
"At": (6, 17),
|
|
||||||
"Rn": (6, 18),
|
|
||||||
# period 7
|
|
||||||
"Fr": (7, 1),
|
|
||||||
"Ra": (7, 2),
|
|
||||||
"Ac": (9, 3),
|
|
||||||
"Th": (9, 4),
|
|
||||||
"Pa": (9, 5),
|
|
||||||
"U": (9, 6),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PeriodicTableDialog(QDialog):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.selected = None
|
|
||||||
|
|
||||||
layout = QGridLayout(self)
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
layout.setSpacing(1)
|
|
||||||
|
|
||||||
for element, (row, col) in ELEMENTS.items():
|
|
||||||
button = QPushButton(element)
|
|
||||||
button.setFixedSize(42, 32)
|
|
||||||
button.clicked.connect(lambda checked=False, e=element: self.select(e))
|
|
||||||
layout.addWidget(button, row - 1, col - 1)
|
|
||||||
|
|
||||||
def select(self, element):
|
|
||||||
self.selected = element
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
|
|
||||||
class ElementSelector(QWidget):
|
|
||||||
|
|
||||||
dialogClosed = Signal()
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.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.Fixed, QSizePolicy.Fixed)
|
|
||||||
self.button.setStyleSheet(
|
|
||||||
f"QPushButton {{background-color: {get_accent_colors().default.name()}; color: white;}}"
|
|
||||||
)
|
|
||||||
# self.selection = QLabel("")
|
|
||||||
layout_selection.addWidget(self.button)
|
|
||||||
# layout_selection.addWidget(self.selection)
|
|
||||||
layout.addWidget(self.label)
|
|
||||||
layout.addLayout(layout_selection)
|
|
||||||
self.current_element = ""
|
|
||||||
|
|
||||||
self.button.clicked.connect(self.open_table)
|
|
||||||
|
|
||||||
def open_table(self):
|
|
||||||
dialog = PeriodicTableDialog(self)
|
|
||||||
if dialog.exec_():
|
|
||||||
self.current_element = dialog.selected
|
|
||||||
# self.selection.setText(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;}}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MotionProfileWidget(QWidget):
|
class MotionProfileWidget(QWidget):
|
||||||
|
"""
|
||||||
|
Additional widget specifically for XAS scans
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -298,8 +164,8 @@ class MotionProfileWidget(QWidget):
|
|||||||
)
|
)
|
||||||
scroll = QScrollArea()
|
scroll = QScrollArea()
|
||||||
scroll.setWidgetResizable(True)
|
scroll.setWidgetResizable(True)
|
||||||
scroll.setFrameShape(QFrame.NoFrame)
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
scroll.setWidget(plot_container)
|
scroll.setWidget(plot_container)
|
||||||
|
|
||||||
self.edge_selection = QHBoxLayout()
|
self.edge_selection = QHBoxLayout()
|
||||||
@@ -318,8 +184,7 @@ class MotionProfileWidget(QWidget):
|
|||||||
self.element.dialogClosed.connect(self._update_edge_selection)
|
self.element.dialogClosed.connect(self._update_edge_selection)
|
||||||
self.edge.activated_connect(self._update_edge_selection)
|
self.edge.activated_connect(self._update_edge_selection)
|
||||||
|
|
||||||
@staticmethod
|
def _make_plot(self, left_label: str, bottom_label: str) -> pg.PlotWidget:
|
||||||
def _make_plot(left_label: str, bottom_label: str) -> pg.PlotWidget:
|
|
||||||
plot = pg.PlotWidget()
|
plot = pg.PlotWidget()
|
||||||
plot.setLabel("left", left_label)
|
plot.setLabel("left", left_label)
|
||||||
plot.setLabel("bottom", bottom_label)
|
plot.setLabel("bottom", bottom_label)
|
||||||
@@ -329,7 +194,7 @@ class MotionProfileWidget(QWidget):
|
|||||||
plot.getAxis("left").enableAutoSIPrefix(False)
|
plot.getAxis("left").enableAutoSIPrefix(False)
|
||||||
plot.getAxis("bottom").enableAutoSIPrefix(False)
|
plot.getAxis("bottom").enableAutoSIPrefix(False)
|
||||||
plot.setMinimumHeight(180)
|
plot.setMinimumHeight(180)
|
||||||
plot.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
plot.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
|
||||||
return plot
|
return plot
|
||||||
|
|
||||||
def _add_edge_marker(self, plot, x_arr, y_arr, energy_arr, edge_marker):
|
def _add_edge_marker(self, plot, x_arr, y_arr, energy_arr, edge_marker):
|
||||||
@@ -353,7 +218,7 @@ class MotionProfileWidget(QWidget):
|
|||||||
plot.addItem(scatter, ignoreBounds=True)
|
plot.addItem(scatter, ignoreBounds=True)
|
||||||
|
|
||||||
text = f"{self.element.currentElement()}, {self.edge.currentText()}-edge, {self.edge_energy:0.1f} eV"
|
text = f"{self.element.currentElement()}, {self.edge.currentText()}-edge, {self.edge_energy:0.1f} eV"
|
||||||
label = pg.TextItem(text, anchor=(0.5, 1.5), color="r")
|
label = pg.TextItem(text, anchor=(0, 1.5), color="r")
|
||||||
label.setPos(x_marker, y_marker)
|
label.setPos(x_marker, y_marker)
|
||||||
plot.addItem(label, ignoreBounds=True)
|
plot.addItem(label, ignoreBounds=True)
|
||||||
|
|
||||||
@@ -375,7 +240,18 @@ class MotionProfileWidget(QWidget):
|
|||||||
self.update_plot()
|
self.update_plot()
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def update_plot(self, scan_name=None, scan_parameters=None, d_spacing=None, *_, **__):
|
def update_plot(self, *_, scan_name=None, scan_parameters=None, d_spacing=None, **__):
|
||||||
|
"""Update the plots. Parameters which are not defined will default to None,
|
||||||
|
in which case it will use the internal values if available.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scan_name(str): Scan name, e.g. "xas_simple_scan"
|
||||||
|
Defaults to None
|
||||||
|
scan_parameters(dict): Scan parameters from ScanControl
|
||||||
|
Defaults to None
|
||||||
|
d_spacing(float): d-spacing in Angstrom
|
||||||
|
Defaults to None
|
||||||
|
"""
|
||||||
if scan_name is not None:
|
if scan_name is not None:
|
||||||
self.scan_name = scan_name
|
self.scan_name = scan_name
|
||||||
if scan_parameters is not None:
|
if scan_parameters is not None:
|
||||||
@@ -396,15 +272,17 @@ class MotionProfileWidget(QWidget):
|
|||||||
self.plot_pos.clear()
|
self.plot_pos.clear()
|
||||||
self.plot_vel.clear()
|
self.plot_vel.clear()
|
||||||
|
|
||||||
start_angle = self.energy_to_angle(self.scan_parameters["stop"])
|
start_angle = self._energy_to_angle(self.scan_parameters["stop"])
|
||||||
stop_angle = self.energy_to_angle(self.scan_parameters["start"])
|
stop_angle = self._energy_to_angle(self.scan_parameters["start"])
|
||||||
|
if start_angle == stop_angle:
|
||||||
|
return
|
||||||
if "xas_simple_scan" in self.scan_name:
|
if "xas_simple_scan" in self.scan_name:
|
||||||
amp = (stop_angle - start_angle) / 2
|
amp = (stop_angle - start_angle) / 2
|
||||||
two_pi_f = 2 * np.pi * 1 / (2 * self.scan_parameters["scan_time"])
|
two_pi_f = 2 * np.pi * 1 / (2 * self.scan_parameters["scan_time"])
|
||||||
pos = (start_angle + stop_angle) / 2 + amp * np.cos(x_time * two_pi_f)
|
pos = (start_angle + stop_angle) / 2 + amp * np.cos(x_time * two_pi_f)
|
||||||
vel = two_pi_f * amp * np.sin(x_time * two_pi_f)
|
vel = two_pi_f * amp * np.sin(x_time * two_pi_f)
|
||||||
energy = self.angle_to_energy(pos)
|
energy = self._angle_to_energy(pos)
|
||||||
vel_e = self.velocity_to_energy_per_s(pos, vel)
|
vel_e = self._velocity_to_energy_per_s(pos, vel)
|
||||||
|
|
||||||
self.plot_pos.plot(x_time, energy)
|
self.plot_pos.plot(x_time, energy)
|
||||||
self._add_edge_marker(self.plot_pos, x_time, energy, energy, "_")
|
self._add_edge_marker(self.plot_pos, x_time, energy, energy, "_")
|
||||||
@@ -420,7 +298,7 @@ class MotionProfileWidget(QWidget):
|
|||||||
return
|
return
|
||||||
if self.scan_parameters["scan_time"] == 0:
|
if self.scan_parameters["scan_time"] == 0:
|
||||||
return
|
return
|
||||||
e_kink_deg = self.energy_to_angle(self.scan_parameters["e_kink"])
|
e_kink_deg = self._energy_to_angle(self.scan_parameters["e_kink"])
|
||||||
pos, vel, t = compute_spline(
|
pos, vel, t = compute_spline(
|
||||||
start_angle,
|
start_angle,
|
||||||
stop_angle,
|
stop_angle,
|
||||||
@@ -429,8 +307,8 @@ class MotionProfileWidget(QWidget):
|
|||||||
self.scan_parameters["scan_time"],
|
self.scan_parameters["scan_time"],
|
||||||
)
|
)
|
||||||
x_time = np.cumsum(t) / 1000
|
x_time = np.cumsum(t) / 1000
|
||||||
energy = self.angle_to_energy(pos)
|
energy = self._angle_to_energy(pos)
|
||||||
vel_e = self.velocity_to_energy_per_s(pos, vel)
|
vel_e = self._velocity_to_energy_per_s(pos, vel)
|
||||||
|
|
||||||
time_flipped = -np.flip(x_time) + x_time[-1]
|
time_flipped = -np.flip(x_time) + x_time[-1]
|
||||||
energy_flipped = np.flip(energy)
|
energy_flipped = np.flip(energy)
|
||||||
@@ -470,7 +348,7 @@ class MotionProfileWidget(QWidget):
|
|||||||
curve.setPen(pg.mkPen(color=color, width=2))
|
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):
|
def _energy_to_angle(self, energy: int | float):
|
||||||
if self.d_spacing is None:
|
if self.d_spacing is None:
|
||||||
return 0
|
return 0
|
||||||
if energy <= 0:
|
if energy <= 0:
|
||||||
@@ -479,7 +357,7 @@ class MotionProfileWidget(QWidget):
|
|||||||
val = wl / (2 * self.d_spacing * 1e-10)
|
val = wl / (2 * self.d_spacing * 1e-10)
|
||||||
return np.asin(val) / np.pi * 180
|
return np.asin(val) / np.pi * 180
|
||||||
|
|
||||||
def angle_to_energy(self, angle: np.ndarray | int | float):
|
def _angle_to_energy(self, angle: np.ndarray | int | float):
|
||||||
if self.d_spacing is None:
|
if self.d_spacing is None:
|
||||||
return 0
|
return 0
|
||||||
if isinstance(angle, np.ndarray):
|
if isinstance(angle, np.ndarray):
|
||||||
@@ -491,7 +369,7 @@ class MotionProfileWidget(QWidget):
|
|||||||
wl = 2 * self.d_spacing * 1e-10 * np.sin(angle / 180 * np.pi)
|
wl = 2 * self.d_spacing * 1e-10 * np.sin(angle / 180 * np.pi)
|
||||||
return C * H / (E * wl)
|
return C * H / (E * wl)
|
||||||
|
|
||||||
def velocity_to_energy_per_s(self, angle, velocity):
|
def _velocity_to_energy_per_s(self, angle, velocity):
|
||||||
if self.d_spacing is None:
|
if self.d_spacing is None:
|
||||||
return 0
|
return 0
|
||||||
return (
|
return (
|
||||||
@@ -503,84 +381,10 @@ class MotionProfileWidget(QWidget):
|
|||||||
) * velocity
|
) * velocity
|
||||||
|
|
||||||
|
|
||||||
LABEL_WIDTH = 118
|
|
||||||
ROW_MARGINS = (4, 0, 4, 0)
|
|
||||||
ROW_SPACING = 6
|
|
||||||
|
|
||||||
|
|
||||||
class ComboBox(QWidget):
|
|
||||||
def __init__(self, identifier="", label="", enums=None):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
layout.setContentsMargins(*ROW_MARGINS)
|
|
||||||
layout.setSpacing(ROW_SPACING)
|
|
||||||
|
|
||||||
self.identifier = identifier
|
|
||||||
|
|
||||||
self.label = QLabel(label)
|
|
||||||
self.label.setFixedWidth(LABEL_WIDTH)
|
|
||||||
self.label.setWordWrap(True)
|
|
||||||
self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
|
|
||||||
layout.addWidget(self.label)
|
|
||||||
|
|
||||||
self.value = QComboBox()
|
|
||||||
self.value.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
||||||
|
|
||||||
for entry in enums or []:
|
|
||||||
self.value.addItem(entry)
|
|
||||||
|
|
||||||
layout.addWidget(self.value)
|
|
||||||
|
|
||||||
def set_current_text(self, text):
|
|
||||||
self.value.setCurrentText(text)
|
|
||||||
|
|
||||||
def currentText(self) -> str:
|
|
||||||
return self.value.currentText()
|
|
||||||
|
|
||||||
def has_focus(self) -> bool:
|
|
||||||
return QApplication.focusWidget() is self.value.view()
|
|
||||||
|
|
||||||
def activated_connect(self, func):
|
|
||||||
"""Connect a function to the Enter/Return key press."""
|
|
||||||
self.value.activated.connect(func)
|
|
||||||
|
|
||||||
def setDisabled(self, disable):
|
|
||||||
self.value.setDisabled(disable)
|
|
||||||
|
|
||||||
|
|
||||||
class TextIndicator(QWidget):
|
|
||||||
def __init__(self, identifier="", label="", text=""):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
layout = QVBoxLayout(self)
|
|
||||||
layout.setContentsMargins(*ROW_MARGINS)
|
|
||||||
layout.setSpacing(ROW_SPACING)
|
|
||||||
|
|
||||||
self.identifier = identifier
|
|
||||||
|
|
||||||
self.label = QLabel(label)
|
|
||||||
self.label.setFixedWidth(LABEL_WIDTH)
|
|
||||||
self.label.setWordWrap(True)
|
|
||||||
self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
|
|
||||||
layout.addWidget(self.label)
|
|
||||||
|
|
||||||
self.value = QLabel(text)
|
|
||||||
self.value.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
||||||
|
|
||||||
layout.addWidget(self.value)
|
|
||||||
|
|
||||||
def setText(self, text):
|
|
||||||
self.value.setText(text)
|
|
||||||
|
|
||||||
def text(self) -> str:
|
|
||||||
return self.value.text()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
apply_theme("light")
|
apply_theme("light")
|
||||||
dispatcher = BECDispatcher(gui_id="scan_control_advanced")
|
dispatcher = BECDispatcher(gui_id="scan_control_advanced")
|
||||||
win = ScanControlAdvanced()
|
win = ScanControlXAS()
|
||||||
win.show()
|
win.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{'files': ['scan_control_xas.py']}
|
||||||
+8
-8
@@ -5,17 +5,17 @@ from bec_widgets.utils.bec_designer import designer_material_icon
|
|||||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from .scan_control_advanced import ScanControlAdvanced
|
from .scan_control_xas import ScanControlXAS
|
||||||
|
|
||||||
DOM_XML = """
|
DOM_XML = """
|
||||||
<ui language='c++'>
|
<ui language='c++'>
|
||||||
<widget class='ScanControlAdvanced' name='scan_control_advanced'>
|
<widget class='ScanControlXAS' name='scan_control_xas'>
|
||||||
</widget>
|
</widget>
|
||||||
</ui>
|
</ui>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
class ScanControlXASPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._form_editor = None
|
self._form_editor = None
|
||||||
@@ -23,7 +23,7 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c
|
|||||||
def createWidget(self, parent):
|
def createWidget(self, parent):
|
||||||
if parent is None:
|
if parent is None:
|
||||||
return QWidget()
|
return QWidget()
|
||||||
t = ScanControlAdvanced(parent)
|
t = ScanControlXAS(parent)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
def domXml(self):
|
def domXml(self):
|
||||||
@@ -33,10 +33,10 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def icon(self):
|
def icon(self):
|
||||||
return designer_material_icon(ScanControlAdvanced.ICON_NAME)
|
return designer_material_icon(ScanControlXAS.ICON_NAME)
|
||||||
|
|
||||||
def includeFile(self):
|
def includeFile(self):
|
||||||
return "scan_control_advanced"
|
return "scan_control_xas"
|
||||||
|
|
||||||
def initialize(self, form_editor):
|
def initialize(self, form_editor):
|
||||||
self._form_editor = form_editor
|
self._form_editor = form_editor
|
||||||
@@ -48,10 +48,10 @@ class ScanControlAdvancedPlugin(QDesignerCustomWidgetInterface): # pragma: no c
|
|||||||
return self._form_editor is not None
|
return self._form_editor is not None
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
return "ScanControlAdvanced"
|
return "ScanControlXAS"
|
||||||
|
|
||||||
def toolTip(self):
|
def toolTip(self):
|
||||||
return "ScanControlAdvanced"
|
return "ScanControlXAS"
|
||||||
|
|
||||||
def whatsThis(self):
|
def whatsThis(self):
|
||||||
return self.toolTip()
|
return self.toolTip()
|
||||||
Reference in New Issue
Block a user