GUI: added beamline_state_panel.py with station widgets to GUI
This commit is contained in:
@@ -23,6 +23,7 @@ from aare.gui.models.gui_state_manager import UIStateManager
|
||||
from aare.gui.panels.LogPanel import LogDock
|
||||
|
||||
from aare.gui.panels.beamline_controls import BeamlineControls
|
||||
from aare.gui.panels.beamline_state_panel import BeamlineStatePanel
|
||||
from aare.gui.panels.data_collection_settings import DataCollectionSettings
|
||||
from aare.gui.panels.developer_help_dialog import DeveloperHelpDialog
|
||||
from aare.gui.panels.beamline_recovery_panel import BeamlineRecoveryDialog
|
||||
@@ -166,17 +167,31 @@ class MainWindow(QMainWindow):
|
||||
|
||||
collection_controls_scroll = NoWheelScrollArea(top_widget)
|
||||
|
||||
self.left_column = QWidget(parent=top_widget)
|
||||
self.left_column_layout = QVBoxLayout(self.left_column)
|
||||
self.left_column_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.left_column_layout.setSpacing(8)
|
||||
|
||||
self.data_collection = DataCollectionSettings(s=geom,
|
||||
parent=top_widget,
|
||||
parent=self.left_column,
|
||||
raster_mgr=self.raster,
|
||||
diffraction=diffraction)
|
||||
|
||||
self.beamline_state_panel = BeamlineStatePanel(parent=self.left_column)
|
||||
|
||||
self.left_column_layout.addWidget(self.data_collection)
|
||||
self.left_column_layout.addWidget(self.beamline_state_panel)
|
||||
self.left_column_layout.addStretch()
|
||||
|
||||
top_widget_layout.addWidget(collection_controls_scroll)
|
||||
collection_controls_scroll.setWidget(self.data_collection)
|
||||
collection_controls_scroll.setWidget(self.left_column)
|
||||
collection_controls_scroll.setHorizontalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
||||
)
|
||||
collection_controls_scroll.setFixedWidth(self.data_collection.set_width + 10)
|
||||
collection_controls_scroll.setWidgetResizable(True)
|
||||
collection_controls_scroll.setFixedWidth(
|
||||
max(self.data_collection.set_width, self.beamline_state_panel.set_width) + 10
|
||||
)
|
||||
|
||||
self.video_tab = QTabWidget(parent=top_widget)
|
||||
|
||||
@@ -527,6 +542,11 @@ class MainWindow(QMainWindow):
|
||||
self.status_bar.close_shutter.connect(self.daq.close_shutter)
|
||||
self.status_bar.open_shutter.connect(self.daq.open_shutter)
|
||||
|
||||
self.beamline_state_panel.dewar_exchange.connect(self.daq.dewar_exchange)
|
||||
self.beamline_state_panel.sample_exchange.connect(self.daq.sample_exchange)
|
||||
self.beamline_state_panel.sample_alignment.connect(self.daq.sample_alignment)
|
||||
self.beamline_state_panel.beam_location.connect(self.daq.beam_location)
|
||||
|
||||
self.rotation.file_ready.connect(self.viewer.load_image)
|
||||
self.raster.image_selected.connect(self.viewer.load_image)
|
||||
|
||||
@@ -562,6 +582,7 @@ class MainWindow(QMainWindow):
|
||||
self.daq.update.connect(self.sample_logic.update_daq_status)
|
||||
self.daq.update.connect(self.manual_sample_panel.update_daq_status)
|
||||
self.daq.update.connect(self.fluor_panel.update_daq_status)
|
||||
self.daq.update.connect(self.beamline_state_panel.update_daq_status)
|
||||
self.daq.update.connect(self.update_daq_status)
|
||||
|
||||
self.daq.sample_missing.connect(self.show_sample_missing_dialog)
|
||||
|
||||
@@ -1,20 +1,540 @@
|
||||
from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
from PySide6.QtCore import QPoint, QRect, Qt, Signal, Slot, QSettings
|
||||
from PySide6.QtGui import QColor, QPainter, QPen
|
||||
from PySide6.QtWidgets import QFrame, QLabel, QPushButton
|
||||
|
||||
from aare.common.models import BeamlineStateEnum, DAQStatusModel
|
||||
|
||||
|
||||
class BeamlineStatePanel(QWidget):
|
||||
@dataclass(frozen=True)
|
||||
class StationSpec:
|
||||
state: BeamlineStateEnum
|
||||
label: str
|
||||
x: int
|
||||
y: int
|
||||
clickable: bool = False
|
||||
tooltip: str = ""
|
||||
|
||||
|
||||
class HoverableLabel(QLabel):
|
||||
hovered = Signal(object)
|
||||
unhovered = Signal()
|
||||
|
||||
def enterEvent(self, event) -> None:
|
||||
self.hovered.emit(getattr(self, "_beamline_state", None))
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event) -> None:
|
||||
self.unhovered.emit()
|
||||
super().leaveEvent(event)
|
||||
|
||||
|
||||
class HoverableButton(QPushButton):
|
||||
hovered = Signal(object)
|
||||
unhovered = Signal()
|
||||
|
||||
def enterEvent(self, event) -> None:
|
||||
self.hovered.emit(getattr(self, "_beamline_state", None))
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event) -> None:
|
||||
self.unhovered.emit()
|
||||
super().leaveEvent(event)
|
||||
|
||||
|
||||
class BeamlineStatePanel(QFrame):
|
||||
sample_exchange = Signal()
|
||||
sample_alignment = Signal()
|
||||
dewar_exchange = Signal()
|
||||
beam_location = Signal()
|
||||
data_collection = Signal()
|
||||
xtal_snapshot = Signal()
|
||||
xray_fluorescence = Signal()
|
||||
|
||||
set_width = 400
|
||||
map_height = 560
|
||||
station_radius = 8
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
self.setFixedWidth(self.set_width)
|
||||
self.setMinimumHeight(self.map_height)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Beamline state", self), 0, 0)
|
||||
self._settings = QSettings("PSI", "AareGUI")
|
||||
|
||||
self.sample_exchange = QPushButton("Sample exchange", parent=self)
|
||||
grid_layout.addWidget(self.sample_exchange, 1, 0)
|
||||
self._current_state: BeamlineStateEnum | None = None
|
||||
self._hovered_state: BeamlineStateEnum | None = None
|
||||
self._pending_target_state: BeamlineStateEnum | None = None
|
||||
self._last_stable_state: BeamlineStateEnum | None = None
|
||||
self._view_mode: str = self._settings.value("beamline_state_panel/view_mode", "metro", type=str)
|
||||
|
||||
self.sample_alignment = QPushButton("Sample alignment", parent=self)
|
||||
grid_layout.addWidget(self.sample_alignment, 2, 0)
|
||||
self._line_color = QColor(111, 129, 160)
|
||||
self._line_current = QColor(0, 126, 229)
|
||||
self._line_hover = QColor(244, 196, 48)
|
||||
|
||||
self.dewar_exchange = QPushButton("Load/unload samples", parent=self)
|
||||
grid_layout.addWidget(self.dewar_exchange, 3, 0)
|
||||
self._station_current = QColor(0, 126, 229)
|
||||
self._station_current_ring = QColor(120, 195, 255)
|
||||
self._station_hover = QColor(244, 196, 48)
|
||||
self._label_current_bg = "rgba(0, 126, 229, 0.12)"
|
||||
self._label_hover_bg = "rgba(244, 196, 48, 0.22)"
|
||||
|
||||
self._group_colors: dict[BeamlineStateEnum, QColor] = {
|
||||
BeamlineStateEnum.DewarTransfer: QColor(128, 90, 213), # Purple
|
||||
BeamlineStateEnum.SampleExchange: QColor(237, 137, 54), # Orange
|
||||
BeamlineStateEnum.RobotSampleExchange: QColor(237, 137, 54), # Orange
|
||||
BeamlineStateEnum.SampleAlignment: QColor(72, 187, 120), # Green
|
||||
BeamlineStateEnum.BeamLocation: QColor(72, 187, 120), # Green
|
||||
BeamlineStateEnum.DataCollection: QColor(236, 72, 153), # Pink
|
||||
BeamlineStateEnum.XtalSnapshot: QColor(236, 72, 153), # Pink
|
||||
BeamlineStateEnum.XrayFluorescence: QColor(236, 72, 153), # Pink
|
||||
}
|
||||
|
||||
self._group_label_colors: dict[BeamlineStateEnum, str] = {
|
||||
state: color.name() for state, color in self._group_colors.items()
|
||||
}
|
||||
|
||||
self._group_label_backgrounds: dict[BeamlineStateEnum, str] = {
|
||||
BeamlineStateEnum.DewarTransfer: "rgba(128, 90, 213, 0.14)",
|
||||
BeamlineStateEnum.SampleExchange: "rgba(237, 137, 54, 0.16)",
|
||||
BeamlineStateEnum.RobotSampleExchange: "rgba(237, 137, 54, 0.16)",
|
||||
BeamlineStateEnum.SampleAlignment: "rgba(72, 187, 120, 0.16)",
|
||||
BeamlineStateEnum.BeamLocation: "rgba(72, 187, 120, 0.16)",
|
||||
BeamlineStateEnum.DataCollection: "rgba(236, 72, 153, 0.14)",
|
||||
BeamlineStateEnum.XtalSnapshot: "rgba(236, 72, 153, 0.14)",
|
||||
BeamlineStateEnum.XrayFluorescence: "rgba(236, 72, 153, 0.14)",
|
||||
}
|
||||
|
||||
self._metro_stations = [
|
||||
StationSpec(BeamlineStateEnum.DewarTransfer, "Dewar transfer", 96, 126, True, "Dewar transfer mode"),
|
||||
StationSpec(BeamlineStateEnum.SampleExchange, "Manual sample exchange", 96, 186, True, "Manual sample exchange mode"),
|
||||
StationSpec(BeamlineStateEnum.RobotSampleExchange, "Robot sample exchange", 96, 246, False, "Robot-assisted sample exchange"),
|
||||
StationSpec(BeamlineStateEnum.SampleAlignment, "Sample alignment", 96, 316, True, "Sample centring and alignment mode"),
|
||||
StationSpec(BeamlineStateEnum.BeamLocation, "Beam location", 250, 316, True, "Beam location mode"),
|
||||
StationSpec(BeamlineStateEnum.DataCollection, "Data collection", 96, 396, True, "Measurement / collection mode"),
|
||||
StationSpec(BeamlineStateEnum.XtalSnapshot, "Crystal snapshot", 250, 396, True, "Crystal snapshot mode"),
|
||||
StationSpec(BeamlineStateEnum.XrayFluorescence, "XRF", 96, 476, True, "X-ray fluorescence mode"),
|
||||
]
|
||||
|
||||
self._line_stations = [
|
||||
StationSpec(BeamlineStateEnum.DewarTransfer, "Dewar transfer", 54, 170, True, "Dewar transfer mode"),
|
||||
StationSpec(BeamlineStateEnum.SampleExchange, "Manual sample exchange", 54, 220, True, "Manual sample exchange mode"),
|
||||
StationSpec(BeamlineStateEnum.RobotSampleExchange, "Robot sample exchange", 54, 270, False, "Robot-assisted sample exchange"),
|
||||
StationSpec(BeamlineStateEnum.SampleAlignment, "Sample alignment", 54, 320, True, "Sample centring and alignment mode"),
|
||||
StationSpec(BeamlineStateEnum.BeamLocation, "Beam location", 54, 370, True, "Beam location mode"),
|
||||
StationSpec(BeamlineStateEnum.DataCollection, "Data collection", 54, 420, True, "Measurement / collection mode"),
|
||||
StationSpec(BeamlineStateEnum.XtalSnapshot, "Crystal snapshot", 54, 470, True, "Crystal snapshot mode"),
|
||||
StationSpec(BeamlineStateEnum.XrayFluorescence, "XRF", 54, 520, True, "X-ray fluorescence mode"),
|
||||
]
|
||||
|
||||
self._segments = [
|
||||
(BeamlineStateEnum.DewarTransfer, BeamlineStateEnum.SampleExchange),
|
||||
(BeamlineStateEnum.SampleExchange, BeamlineStateEnum.RobotSampleExchange),
|
||||
(BeamlineStateEnum.RobotSampleExchange, BeamlineStateEnum.SampleAlignment),
|
||||
(BeamlineStateEnum.SampleAlignment, BeamlineStateEnum.BeamLocation),
|
||||
(BeamlineStateEnum.SampleAlignment, BeamlineStateEnum.DataCollection),
|
||||
(BeamlineStateEnum.DataCollection, BeamlineStateEnum.XtalSnapshot),
|
||||
(BeamlineStateEnum.DataCollection, BeamlineStateEnum.XrayFluorescence),
|
||||
]
|
||||
|
||||
self._graph = self._build_graph(self._segments)
|
||||
self._station_widgets: dict[BeamlineStateEnum, QLabel | QPushButton] = {}
|
||||
|
||||
self.title = QLabel(self)
|
||||
self.title.setText("<H3>Beamline state</H3>")
|
||||
self.title.setStyleSheet("background-color: #4B0082; color: #ffffff;")
|
||||
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.title.setFixedHeight(50)
|
||||
self.title.setGeometry(0, 0, self.set_width, 50)
|
||||
|
||||
self.current_label = QLabel("Current: —", self)
|
||||
self.current_label.setStyleSheet("""
|
||||
QLabel {
|
||||
color: rgb(30, 41, 59);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
padding-left: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
""")
|
||||
self.current_label.move(14, 58)
|
||||
self.current_label.adjustSize()
|
||||
|
||||
self.view_toggle = QPushButton(self)
|
||||
self.view_toggle.setCheckable(False)
|
||||
self.view_toggle.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: rgb(240, 244, 252);
|
||||
color: rgb(55, 67, 87);
|
||||
border: 1px solid rgb(160, 175, 200);
|
||||
border-radius: 8px;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: rgb(228, 236, 248);
|
||||
}
|
||||
""")
|
||||
self.view_toggle.clicked.connect(self._toggle_view_mode)
|
||||
self._update_toggle_text()
|
||||
|
||||
self._build_station_widgets()
|
||||
self._position_station_widgets()
|
||||
|
||||
@staticmethod
|
||||
def _canon_segment(a: BeamlineStateEnum, b: BeamlineStateEnum) -> tuple[BeamlineStateEnum, BeamlineStateEnum]:
|
||||
return tuple(sorted((a, b), key=lambda state: state.value))
|
||||
|
||||
def _build_graph(
|
||||
self,
|
||||
segments: list[tuple[BeamlineStateEnum, BeamlineStateEnum]],
|
||||
) -> dict[BeamlineStateEnum, set[BeamlineStateEnum]]:
|
||||
graph: dict[BeamlineStateEnum, set[BeamlineStateEnum]] = {}
|
||||
for a, b in segments:
|
||||
graph.setdefault(a, set()).add(b)
|
||||
graph.setdefault(b, set()).add(a)
|
||||
return graph
|
||||
|
||||
def _stations(self) -> list[StationSpec]:
|
||||
return self._metro_stations if self._view_mode == "metro" else self._line_stations
|
||||
|
||||
def _station_map(self) -> dict[BeamlineStateEnum, StationSpec]:
|
||||
return {station.state: station for station in self._stations()}
|
||||
|
||||
def _path_segments_between(
|
||||
self,
|
||||
start: BeamlineStateEnum | None,
|
||||
end: BeamlineStateEnum | None,
|
||||
) -> set[tuple[BeamlineStateEnum, BeamlineStateEnum]]:
|
||||
if start is None or end is None:
|
||||
return set()
|
||||
|
||||
if start == BeamlineStateEnum.Moving or end == BeamlineStateEnum.Moving:
|
||||
return set()
|
||||
|
||||
if start == end:
|
||||
return set()
|
||||
|
||||
queue = deque([start])
|
||||
previous: dict[BeamlineStateEnum, BeamlineStateEnum | None] = {start: None}
|
||||
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
if node == end:
|
||||
break
|
||||
|
||||
for neighbour in self._graph.get(node, set()):
|
||||
if neighbour in previous:
|
||||
continue
|
||||
previous[neighbour] = node
|
||||
queue.append(neighbour)
|
||||
|
||||
if end not in previous:
|
||||
return set()
|
||||
|
||||
path_segments: set[tuple[BeamlineStateEnum, BeamlineStateEnum]] = set()
|
||||
cursor = end
|
||||
while previous[cursor] is not None:
|
||||
parent = previous[cursor]
|
||||
path_segments.add(self._canon_segment(cursor, parent))
|
||||
cursor = parent
|
||||
|
||||
return path_segments
|
||||
|
||||
def _active_hover_route(self) -> set[tuple[BeamlineStateEnum, BeamlineStateEnum]]:
|
||||
if self._hovered_state is not None:
|
||||
route_source = self._last_stable_state if self._current_state == BeamlineStateEnum.Moving else self._current_state
|
||||
return self._path_segments_between(route_source, self._hovered_state)
|
||||
|
||||
if self._current_state == BeamlineStateEnum.Moving and self._pending_target_state is not None:
|
||||
return self._path_segments_between(self._last_stable_state, self._pending_target_state)
|
||||
|
||||
return set()
|
||||
|
||||
def _build_station_widgets(self) -> None:
|
||||
for station in self._metro_stations:
|
||||
if station.clickable:
|
||||
widget: QLabel | QPushButton = HoverableButton(station.label, self)
|
||||
widget.setFlat(True)
|
||||
widget.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
widget.clicked.connect(lambda _checked=False, state=station.state: self._emit_for_state(state))
|
||||
else:
|
||||
widget = HoverableLabel(station.label, self)
|
||||
|
||||
widget._beamline_state = station.state
|
||||
widget.hovered.connect(self._set_hovered_state)
|
||||
widget.unhovered.connect(self._clear_hovered_state)
|
||||
widget.setToolTip(station.tooltip or station.label)
|
||||
self._station_widgets[station.state] = widget
|
||||
|
||||
self._apply_station_highlight()
|
||||
|
||||
def _position_station_widgets(self) -> None:
|
||||
station_map = self._station_map()
|
||||
|
||||
for state, widget in self._station_widgets.items():
|
||||
station = station_map[state]
|
||||
widget.setText(station.label)
|
||||
widget.adjustSize()
|
||||
|
||||
label_x = station.x + 18
|
||||
label_y = station.y - 12
|
||||
|
||||
if self._view_mode == "metro":
|
||||
if state == BeamlineStateEnum.SampleAlignment:
|
||||
label_x = station.x + 24
|
||||
label_y = station.y + 8
|
||||
elif state == BeamlineStateEnum.DataCollection:
|
||||
label_x = station.x + 24
|
||||
label_y = station.y + 8
|
||||
elif state == BeamlineStateEnum.BeamLocation:
|
||||
label_y = station.y - 18
|
||||
elif state == BeamlineStateEnum.XtalSnapshot:
|
||||
label_y = station.y - 14
|
||||
elif state == BeamlineStateEnum.XrayFluorescence:
|
||||
label_y = station.y - 14
|
||||
else:
|
||||
label_x = station.x + 20
|
||||
label_y = station.y - 12
|
||||
|
||||
widget.move(label_x, label_y)
|
||||
widget.show()
|
||||
|
||||
self._apply_station_highlight()
|
||||
|
||||
def _update_toggle_text(self) -> None:
|
||||
self.view_toggle.setText("View: Metro" if self._view_mode == "metro" else "View: Line")
|
||||
self.view_toggle.adjustSize()
|
||||
self.view_toggle.move(self.set_width - 118, 58)
|
||||
|
||||
@Slot()
|
||||
def _toggle_view_mode(self) -> None:
|
||||
self._view_mode = "line" if self._view_mode == "metro" else "metro"
|
||||
self._settings.setValue("beamline_state_panel/view_mode", self._view_mode)
|
||||
self._update_toggle_text()
|
||||
self._position_station_widgets()
|
||||
self.update()
|
||||
|
||||
def _emit_for_state(self, state: BeamlineStateEnum) -> None:
|
||||
self._pending_target_state = state
|
||||
self._hovered_state = None
|
||||
self.update()
|
||||
|
||||
if state == BeamlineStateEnum.SampleExchange:
|
||||
self.sample_exchange.emit()
|
||||
elif state == BeamlineStateEnum.SampleAlignment:
|
||||
self.sample_alignment.emit()
|
||||
elif state == BeamlineStateEnum.DewarTransfer:
|
||||
self.dewar_exchange.emit()
|
||||
elif state == BeamlineStateEnum.BeamLocation:
|
||||
self.beam_location.emit()
|
||||
elif state == BeamlineStateEnum.DataCollection:
|
||||
self.data_collection.emit()
|
||||
elif state == BeamlineStateEnum.XtalSnapshot:
|
||||
self.xtal_snapshot.emit()
|
||||
elif state == BeamlineStateEnum.XrayFluorescence:
|
||||
self.xray_fluorescence.emit()
|
||||
|
||||
@Slot(object)
|
||||
def _set_hovered_state(self, state: BeamlineStateEnum | None) -> None:
|
||||
self._hovered_state = state
|
||||
self._apply_station_highlight()
|
||||
|
||||
@Slot()
|
||||
def _clear_hovered_state(self) -> None:
|
||||
self._hovered_state = None
|
||||
self._apply_station_highlight()
|
||||
|
||||
def _apply_station_highlight(self) -> None:
|
||||
for station in self._stations():
|
||||
widget = self._station_widgets[station.state]
|
||||
is_current = station.state == self._current_state
|
||||
is_hovered = station.state == self._hovered_state
|
||||
is_pending = station.state == self._pending_target_state and self._current_state == BeamlineStateEnum.Moving
|
||||
|
||||
label_color = self._group_label_colors.get(station.state, "rgb(55, 67, 87)")
|
||||
label_bg = self._group_label_backgrounds.get(station.state, "transparent")
|
||||
|
||||
if isinstance(widget, QPushButton):
|
||||
if is_current:
|
||||
widget.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: {self._label_current_bg};
|
||||
color: rgb(0, 92, 170);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
padding: 2px 6px 2px 8px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
color: rgb(0, 92, 170);
|
||||
}}
|
||||
""")
|
||||
elif is_hovered or is_pending:
|
||||
widget.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: {self._label_hover_bg};
|
||||
color: rgb(115, 88, 0);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
padding: 2px 6px 2px 8px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
color: rgb(115, 88, 0);
|
||||
}}
|
||||
""")
|
||||
else:
|
||||
widget.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: {label_bg};
|
||||
color: {label_color};
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 2px 6px 2px 8px;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
color: {label_color};
|
||||
}}
|
||||
""")
|
||||
else:
|
||||
if is_current:
|
||||
widget.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
border-radius: 10px;
|
||||
background: {self._label_current_bg};
|
||||
color: rgb(0, 92, 170);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px 2px 8px;
|
||||
}}
|
||||
""")
|
||||
elif is_hovered or is_pending:
|
||||
widget.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
border-radius: 10px;
|
||||
background: {self._label_hover_bg};
|
||||
color: rgb(115, 88, 0);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px 2px 8px;
|
||||
}}
|
||||
""")
|
||||
else:
|
||||
widget.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
border-radius: 10px;
|
||||
background: {label_bg};
|
||||
color: {label_color};
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px 2px 8px;
|
||||
}}
|
||||
""")
|
||||
|
||||
widget.adjustSize()
|
||||
|
||||
self.update()
|
||||
|
||||
def _station_center(self, state: BeamlineStateEnum) -> QPoint:
|
||||
station = self._station_map()[state]
|
||||
return QPoint(station.x, station.y)
|
||||
|
||||
def _segment_color(self, a: BeamlineStateEnum, b: BeamlineStateEnum) -> QColor:
|
||||
segment = self._canon_segment(a, b)
|
||||
|
||||
active_hover_route = self._active_hover_route()
|
||||
if segment in active_hover_route:
|
||||
return self._line_hover
|
||||
|
||||
current_path = self._path_segments_between(BeamlineStateEnum.DewarTransfer, self._last_stable_state or self._current_state)
|
||||
if segment in current_path:
|
||||
return self._line_current
|
||||
|
||||
return self._line_color
|
||||
|
||||
def _draw_segment(self, painter: QPainter, start: QPoint, end: QPoint, color: QColor) -> None:
|
||||
pen = QPen(color, 4)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
painter.setPen(pen)
|
||||
painter.drawLine(start, end)
|
||||
|
||||
def _station_base_color(self, state: BeamlineStateEnum) -> QColor:
|
||||
return self._group_colors.get(state, QColor(180, 190, 210))
|
||||
|
||||
def _draw_station(self, painter: QPainter, station: StationSpec) -> None:
|
||||
center = self._station_center(station.state)
|
||||
rect = QRect(
|
||||
center.x() - self.station_radius,
|
||||
center.y() - self.station_radius,
|
||||
self.station_radius * 2,
|
||||
self.station_radius * 2,
|
||||
)
|
||||
|
||||
if station.state == self._hovered_state or (
|
||||
self._current_state == BeamlineStateEnum.Moving and station.state == self._pending_target_state
|
||||
):
|
||||
painter.setPen(QPen(self._station_hover, 3))
|
||||
painter.setBrush(self._station_hover)
|
||||
elif station.state == self._current_state:
|
||||
painter.setPen(QPen(self._station_current_ring, 4))
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.drawEllipse(
|
||||
QRect(
|
||||
center.x() - self.station_radius - 4,
|
||||
center.y() - self.station_radius - 4,
|
||||
(self.station_radius + 4) * 2,
|
||||
(self.station_radius + 4) * 2,
|
||||
)
|
||||
)
|
||||
painter.setPen(QPen(self._station_current, 2))
|
||||
painter.setBrush(self._station_current)
|
||||
else:
|
||||
base_color = self._station_base_color(station.state)
|
||||
painter.setPen(QPen(base_color.darker(125), 2))
|
||||
painter.setBrush(base_color)
|
||||
|
||||
painter.drawEllipse(rect)
|
||||
|
||||
def paintEvent(self, event) -> None:
|
||||
super().paintEvent(event)
|
||||
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
|
||||
for start_state, end_state in self._segments:
|
||||
start = self._station_center(start_state)
|
||||
end = self._station_center(end_state)
|
||||
color = self._segment_color(start_state, end_state)
|
||||
self._draw_segment(painter, start, end, color)
|
||||
|
||||
for station in self._stations():
|
||||
self._draw_station(painter, station)
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, status: DAQStatusModel) -> None:
|
||||
self.set_current_state(status.state)
|
||||
|
||||
def set_current_state(self, state: BeamlineStateEnum | None) -> None:
|
||||
self._current_state = state
|
||||
|
||||
if state is not None and state != BeamlineStateEnum.Moving:
|
||||
self._last_stable_state = state
|
||||
if self._pending_target_state == state:
|
||||
self._pending_target_state = None
|
||||
|
||||
label = state.display_name() if state is not None else "—"
|
||||
self.current_label.setText(f"Current: {label}")
|
||||
self.current_label.adjustSize()
|
||||
self._apply_station_highlight()
|
||||
Reference in New Issue
Block a user