comapct automation panel updates
This commit is contained in:
+119
-18
@@ -10,7 +10,9 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QMessageBox,
|
||||
QDockWidget,
|
||||
QTabWidget)
|
||||
QTabWidget,
|
||||
QStackedWidget
|
||||
)
|
||||
|
||||
#Common imports
|
||||
from aare.common.auth_models import BatonStatus
|
||||
@@ -166,6 +168,9 @@ class MainWindow(QMainWindow):
|
||||
self.alert_banner_secondary = AlertBanner(parent=root_widget)
|
||||
root_layout.addWidget(self.alert_banner_secondary)
|
||||
|
||||
self.content_stack = QStackedWidget(parent=root_widget)
|
||||
root_layout.addWidget(self.content_stack, 1)
|
||||
|
||||
top_widget = QWidget(parent=root_widget)
|
||||
top_widget_layout = QHBoxLayout(top_widget)
|
||||
top_widget.setLayout(top_widget_layout)
|
||||
@@ -280,11 +285,24 @@ class MainWindow(QMainWindow):
|
||||
self.compact_sample_camera = SampleCameraImageLabel(
|
||||
geom=geom,
|
||||
raster=self.raster,
|
||||
parent=top_widget,
|
||||
parent=root_widget,
|
||||
default_image=default_image,
|
||||
)
|
||||
self.compact_automation_panel = CompactAutomationPanel(self.compact_sample_camera, parent=top_widget)
|
||||
self.video_tab.addTab(self.compact_automation_panel, "Automation")
|
||||
self.compact_automation_panel = CompactAutomationPanel(self.compact_sample_camera, parent=root_widget)
|
||||
|
||||
self.compact_automation_page = QWidget(parent=root_widget)
|
||||
self.compact_automation_page.setObjectName("compactAutomationPage")
|
||||
self.compact_automation_page.setStyleSheet(
|
||||
"""
|
||||
QWidget#compactAutomationPage {
|
||||
background-color: #e8eefc;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.compact_automation_page_layout = QVBoxLayout(self.compact_automation_page)
|
||||
self.compact_automation_page_layout.setContentsMargins(18, 18, 18, 18)
|
||||
self.compact_automation_page_layout.setSpacing(0)
|
||||
self.compact_automation_page_layout.addWidget(self.compact_automation_panel)
|
||||
|
||||
top_widget_layout.addWidget(self.video_tab)
|
||||
self._start_axis_camera_threads()
|
||||
@@ -301,7 +319,13 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s=[]))
|
||||
self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s=[]))
|
||||
self.job_list_panel = SampleQueuePanel()
|
||||
self.job_list_panel = SampleQueuePanel(show_user=self.__decoded_token.staff)
|
||||
|
||||
self.compact_automation_panel.play_pause_clicked.connect(self.job_list_panel.run)
|
||||
self.compact_automation_panel.skip_clicked.connect(self.job_list_panel.skip_current_sample)
|
||||
self.compact_automation_panel.step_through_toggled.connect(self.job_list_panel.set_step_through)
|
||||
self.compact_automation_panel.show_full_view_requested.connect(self._return_from_compact_automation_view)
|
||||
self.compact_automation_panel.annotation_selected.connect(self._handle_compact_annotation)
|
||||
|
||||
self.tell_samples_dock = QDockWidget("Sample List", self)
|
||||
self.tell_samples_dock.setObjectName("tell_samples_dock")
|
||||
@@ -373,17 +397,15 @@ class MainWindow(QMainWindow):
|
||||
self.tabifyDockWidget(self.manual_sample_dock, self.automation_progress_dock)
|
||||
self.tabifyDockWidget(self.automation_progress_dock, self.log_dock)
|
||||
|
||||
self.compact_automation_panel.play_pause_clicked.connect(self.job_list_panel.run)
|
||||
self.compact_automation_panel.skip_clicked.connect(self.job_list_panel.skip_current_sample)
|
||||
self.compact_automation_panel.step_through_toggled.connect(self.job_list_panel.set_step_through)
|
||||
self.compact_automation_panel.show_full_view_requested.connect(self.restore_default_view)
|
||||
|
||||
self.job_list_panel.samples_in_queue_changed.connect(
|
||||
self.automation_progress_panel.set_samples_in_queue
|
||||
)
|
||||
self.job_list_panel.automation_running_changed.connect(
|
||||
self.automation_progress_panel.set_running
|
||||
)
|
||||
self.job_list_panel.automation_running_changed.connect(
|
||||
self._on_automation_running_changed
|
||||
)
|
||||
self.job_list_panel.automation_running_changed.connect(
|
||||
self.compact_automation_panel.set_running
|
||||
)
|
||||
@@ -393,10 +415,14 @@ class MainWindow(QMainWindow):
|
||||
self.job_list_panel.samples_in_queue_changed.connect(
|
||||
self._refresh_compact_queue_preview
|
||||
)
|
||||
|
||||
self.automation_progress_panel.set_samples_in_queue(
|
||||
len(self.job_list_panel.table_model.samples)
|
||||
)
|
||||
self.automation_progress_panel.set_running(self.job_list_panel.is_running())
|
||||
self.compact_automation_panel.set_running(self.job_list_panel.is_running())
|
||||
self.compact_automation_panel.set_step_through(self.job_list_panel.is_step_through())
|
||||
self._refresh_compact_queue_preview()
|
||||
|
||||
# smargon trace panel
|
||||
self.smargon_trace_panel = SmargonTracePanel()
|
||||
@@ -437,7 +463,11 @@ class MainWindow(QMainWindow):
|
||||
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.prediction_metrics_dock)
|
||||
self.prediction_metrics_dock.hide()
|
||||
|
||||
root_layout.addWidget(top_widget)
|
||||
self.content_stack.addWidget(top_widget)
|
||||
self.content_stack.addWidget(self.compact_automation_page)
|
||||
self.content_stack.setCurrentWidget(top_widget)
|
||||
self._standard_main_page = top_widget
|
||||
|
||||
self.setCentralWidget(root_widget)
|
||||
|
||||
self.setWindowTitle("AareGUI")
|
||||
@@ -873,7 +903,7 @@ class MainWindow(QMainWindow):
|
||||
self.gonio_camera_thread.start()
|
||||
|
||||
def _all_tell_samples_in_default_order(self) -> list:
|
||||
samples = list(self.tell_samples.table_model.samples)
|
||||
samples = list(getattr(self.tell_samples.table_model, "samples", []))
|
||||
return sorted(
|
||||
[sample for sample in samples if sample.location is not None],
|
||||
key=lambda sample: sample.loc_str_sort(),
|
||||
@@ -885,7 +915,30 @@ class MainWindow(QMainWindow):
|
||||
self._all_tell_samples_in_default_order()
|
||||
)
|
||||
self._refresh_compact_queue_preview()
|
||||
self.video_tab.setCurrentWidget(self.compact_automation_panel)
|
||||
|
||||
self.tell_samples_dock.setVisible(False)
|
||||
self.job_list_dock.setVisible(False)
|
||||
self.manual_sample_dock.setVisible(False)
|
||||
self.automation_progress_dock.setVisible(False)
|
||||
self.face_panel_dock.setVisible(False)
|
||||
self.fluor_panel_dock.setVisible(False)
|
||||
self.smargon_trace_dock.setVisible(False)
|
||||
self.target_stability_dock.setVisible(False)
|
||||
self.prediction_metrics_dock.setVisible(False)
|
||||
self.log_dock.setVisible(False)
|
||||
|
||||
if self.__decoded_token.staff:
|
||||
self.ref_tools_dock.setVisible(False)
|
||||
|
||||
self.content_stack.setCurrentWidget(self.compact_automation_page)
|
||||
|
||||
@Slot()
|
||||
def _return_from_compact_automation_view(self) -> None:
|
||||
self.content_stack.setCurrentWidget(self._standard_main_page)
|
||||
self.restore_default_view()
|
||||
self.job_list_dock.setVisible(True)
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.job_list_dock.raise_()
|
||||
|
||||
@Slot()
|
||||
def _refresh_compact_queue_preview(self) -> None:
|
||||
@@ -896,6 +949,52 @@ class MainWindow(QMainWindow):
|
||||
next_next_sample,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _annotation_token(annotation: str) -> str:
|
||||
mapping = {
|
||||
"Heart": "❤️",
|
||||
"Thumbs Up": "👍",
|
||||
"Thumbs Down": "👎",
|
||||
"Eyes": "👀",
|
||||
"Scan Again": "scan again",
|
||||
}
|
||||
return mapping.get(annotation, str(annotation).strip())
|
||||
|
||||
@staticmethod
|
||||
def _append_annotation_to_comment(existing_comment: str | None, annotation: str) -> str:
|
||||
token = MainWindow._annotation_token(annotation)
|
||||
current = str(existing_comment or "").strip()
|
||||
|
||||
if not current:
|
||||
return token
|
||||
|
||||
current_tokens = [part.strip() for part in current.split(" | ") if part.strip()]
|
||||
if token in current_tokens:
|
||||
return current
|
||||
|
||||
return f"{current} | {token}"
|
||||
|
||||
@Slot(str)
|
||||
def _handle_compact_annotation(self, annotation: str) -> None:
|
||||
current_sample, _, _ = self.job_list_panel.queue_preview()
|
||||
if current_sample is None:
|
||||
self.status_bar.show_connection_message("No sample selected for annotation.", True)
|
||||
return
|
||||
|
||||
updated_comment = self._append_annotation_to_comment(
|
||||
getattr(current_sample, "comment", None),
|
||||
annotation,
|
||||
)
|
||||
|
||||
self.job_list_panel.annotate_sample_comment(current_sample.db_id, updated_comment)
|
||||
self.tell_samples.annotate_sample_comment(current_sample.db_id, updated_comment)
|
||||
self._refresh_compact_queue_preview()
|
||||
|
||||
self.status_bar.show_connection_message(
|
||||
f"Annotation added: {self._annotation_token(annotation)} — {current_sample.sample_name}",
|
||||
False,
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def refresh_axis_cameras(self) -> None:
|
||||
logger.info("Refreshing Axis camera threads")
|
||||
@@ -1032,7 +1131,7 @@ class MainWindow(QMainWindow):
|
||||
beamline_combined_tab_action.triggered.connect(lambda: self.video_tab.setCurrentIndex(3))
|
||||
view_menu.addAction(beamline_combined_tab_action)
|
||||
|
||||
compact_automation_action = QAction("Compact Automation View", self)
|
||||
compact_automation_action = QAction("Automation View", self)
|
||||
compact_automation_action.setShortcut(QKeySequence("Ctrl+5"))
|
||||
compact_automation_action.triggered.connect(self.enter_compact_automation_view)
|
||||
view_menu.addAction(compact_automation_action)
|
||||
@@ -1087,6 +1186,8 @@ class MainWindow(QMainWindow):
|
||||
if self._default_window_state is not None:
|
||||
self.restoreState(self._default_window_state)
|
||||
|
||||
self.content_stack.setCurrentWidget(self._standard_main_page)
|
||||
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.job_list_dock.setVisible(True)
|
||||
self.manual_sample_dock.setVisible(True)
|
||||
@@ -1389,6 +1490,8 @@ class MainWindow(QMainWindow):
|
||||
s.geom.beam_location_pxl.y,
|
||||
)
|
||||
|
||||
self._refresh_compact_queue_preview()
|
||||
|
||||
current_session = int(getattr(self.__decoded_token, "session", -1))
|
||||
for gui in getattr(s, "open_guis", []) or []:
|
||||
try:
|
||||
@@ -1405,14 +1508,12 @@ class MainWindow(QMainWindow):
|
||||
if self._remote_close_deadline_ts is not None:
|
||||
self._clear_remote_close_request()
|
||||
|
||||
self._refresh_compact_queue_preview()
|
||||
|
||||
if not self.__mounting and s.state == BeamlineStateEnum.RobotSampleExchange:
|
||||
self.__mounting = True
|
||||
self.video_tab.setCurrentIndex(3)
|
||||
self.video_tab.setCurrentWidget(self.beamline_combined_panel)
|
||||
elif self.__mounting and s.state != BeamlineStateEnum.RobotSampleExchange:
|
||||
self.__mounting = False
|
||||
self.video_tab.setCurrentIndex(0)
|
||||
self.video_tab.setCurrentWidget(self.sample_camera)
|
||||
|
||||
|
||||
# ========== BATON DIALOG HANDLING ==========
|
||||
|
||||
@@ -4,31 +4,44 @@ from PySide6.QtGui import QBrush, QColor
|
||||
from aare.common.models import SampleShortInfo, SampleShortInfoList
|
||||
|
||||
|
||||
def get_entry(sample: SampleShortInfo, column: int):
|
||||
if column == 0:
|
||||
return sample.user
|
||||
elif column == 1:
|
||||
return sample.puck_name
|
||||
elif column == 2:
|
||||
return sample.sample_name
|
||||
def get_entry(sample: SampleShortInfo, column: int, *, show_user: bool = False):
|
||||
if show_user:
|
||||
if column == 0:
|
||||
return sample.user
|
||||
elif column == 1:
|
||||
return sample.dewar_name
|
||||
elif column == 2:
|
||||
return sample.puck_name
|
||||
elif column == 3:
|
||||
return sample.sample_name
|
||||
else:
|
||||
if column == 0:
|
||||
return sample.dewar_name
|
||||
elif column == 1:
|
||||
return sample.puck_name
|
||||
elif column == 2:
|
||||
return sample.sample_name
|
||||
return ""
|
||||
|
||||
|
||||
class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
samples: list[SampleShortInfo] | None = None
|
||||
samples: list[SampleShortInfo] | None = None,
|
||||
show_user: bool = False,
|
||||
):
|
||||
super().__init__(parent)
|
||||
if samples is None:
|
||||
samples = []
|
||||
self.__running = False
|
||||
self._show_user = bool(show_user)
|
||||
self.samples: list[SampleShortInfo] = samples
|
||||
self.header = [
|
||||
"User",
|
||||
"Puck",
|
||||
"Sample",
|
||||
]
|
||||
self.header = (
|
||||
["User", "Dewar", "Puck", "Sample"]
|
||||
if self._show_user
|
||||
else ["Dewar", "Puck", "Sample"]
|
||||
)
|
||||
|
||||
def flags(self, index):
|
||||
default_flags = super().flags(index)
|
||||
@@ -42,7 +55,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
return get_entry(self.samples[index.row()], index.column())
|
||||
return get_entry(self.samples[index.row()], index.column(), show_user=self._show_user)
|
||||
elif role == Qt.ItemDataRole.TextAlignmentRole:
|
||||
return Qt.AlignmentFlag.AlignCenter
|
||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
||||
@@ -119,16 +132,27 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
self.samples = []
|
||||
self.endResetModel()
|
||||
|
||||
def annotate_sample(self, db_id: int, comment: str) -> None:
|
||||
self.beginResetModel()
|
||||
updated_samples: list[SampleShortInfo] = []
|
||||
for sample in self.samples:
|
||||
if sample.db_id == db_id:
|
||||
updated_samples.append(sample.model_copy(update={"comment": comment}))
|
||||
else:
|
||||
updated_samples.append(sample)
|
||||
self.samples = updated_samples
|
||||
self.endResetModel()
|
||||
|
||||
def to_state(self) -> dict:
|
||||
return {
|
||||
"samples": [s.to_dict() for s in self.samples],
|
||||
"samples": [s.model_dump() for s in self.samples],
|
||||
}
|
||||
|
||||
def from_state(self, state: dict):
|
||||
self.beginResetModel()
|
||||
|
||||
self.samples = [
|
||||
SampleShortInfo.from_dict(d)
|
||||
SampleShortInfo.model_validate(d)
|
||||
for d in state.get("samples", [])
|
||||
]
|
||||
|
||||
|
||||
@@ -30,41 +30,162 @@ class CompactAutomationPanel(QFrame):
|
||||
self._next_sample: SampleShortInfo | None = None
|
||||
self._next_next_sample: SampleShortInfo | None = None
|
||||
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.setObjectName("compactAutomationPanel")
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QFrame#compactAutomationPanel {
|
||||
background: #eef4ff;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
QFrame#compactAutomationHeader {
|
||||
background: #1f3b63;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QLabel#compactAutomationTitle {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QLabel#compactAutomationSubtitle {
|
||||
color: #d7e6ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
QToolButton#compactMenuButton {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border-radius: 18px;
|
||||
padding: 8px 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QToolButton#compactMenuButton:hover {
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
QFrame#compactInfoPanel {
|
||||
background: white;
|
||||
border: 1px solid #d5e0f2;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
QLabel#compactSectionTitle {
|
||||
color: #1f3b63;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QLabel#compactInfoLabel {
|
||||
background: #f6f9ff;
|
||||
border: 1px solid #dde7f6;
|
||||
border-radius: 10px;
|
||||
color: #24364d;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
QPushButton#compactPrimaryButton {
|
||||
background: #2f6fed;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QPushButton#compactPrimaryButton:hover {
|
||||
background: #255fd0;
|
||||
}
|
||||
|
||||
QPushButton#compactSecondaryButton {
|
||||
background: white;
|
||||
color: #1f3b63;
|
||||
border: 1px solid #c8d7ef;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QPushButton#compactSecondaryButton:hover,
|
||||
QToolButton#compactSecondaryButton:hover {
|
||||
background: #f4f8ff;
|
||||
}
|
||||
|
||||
QToolButton#compactSecondaryButton {
|
||||
background: white;
|
||||
color: #1f3b63;
|
||||
border: 1px solid #c8d7ef;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(12, 12, 12, 12)
|
||||
main_layout.setSpacing(10)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
main_layout.setSpacing(14)
|
||||
|
||||
top_bar = QHBoxLayout()
|
||||
top_bar.setSpacing(8)
|
||||
header = QFrame(self)
|
||||
header.setObjectName("compactAutomationHeader")
|
||||
header_layout = QHBoxLayout(header)
|
||||
header_layout.setContentsMargins(14, 12, 14, 12)
|
||||
header_layout.setSpacing(10)
|
||||
|
||||
self.menu_button = QToolButton(self)
|
||||
self.menu_button.setObjectName("compactMenuButton")
|
||||
self.menu_button.setText("☰")
|
||||
self.menu_button.setToolTip("Return to full view")
|
||||
self.menu_button.clicked.connect(self.show_full_view_requested.emit)
|
||||
|
||||
self.title_label = QLabel("Automation mode", self)
|
||||
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title_container = QWidget(self)
|
||||
title_layout = QVBoxLayout(title_container)
|
||||
title_layout.setContentsMargins(0, 0, 0, 0)
|
||||
title_layout.setSpacing(2)
|
||||
|
||||
top_bar.addWidget(self.menu_button)
|
||||
top_bar.addWidget(self.title_label, 1)
|
||||
self.title_label = QLabel("Automation View", self)
|
||||
self.title_label.setObjectName("compactAutomationTitle")
|
||||
|
||||
main_layout.addLayout(top_bar)
|
||||
self.subtitle_label = QLabel("Focused queue control and sample camera", self)
|
||||
self.subtitle_label.setObjectName("compactAutomationSubtitle")
|
||||
|
||||
title_layout.addWidget(self.title_label)
|
||||
title_layout.addWidget(self.subtitle_label)
|
||||
|
||||
header_layout.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop)
|
||||
header_layout.addWidget(title_container, 1)
|
||||
|
||||
main_layout.addWidget(header)
|
||||
main_layout.addWidget(camera_widget, 1)
|
||||
|
||||
info_panel = QFrame(self)
|
||||
info_panel.setObjectName("compactInfoPanel")
|
||||
info_layout = QVBoxLayout(info_panel)
|
||||
info_layout.setContentsMargins(10, 10, 10, 10)
|
||||
info_layout.setSpacing(6)
|
||||
info_layout.setContentsMargins(14, 14, 14, 14)
|
||||
info_layout.setSpacing(8)
|
||||
|
||||
info_title = QLabel("Queue preview", self)
|
||||
info_title.setObjectName("compactSectionTitle")
|
||||
|
||||
self.current_label = QLabel("Current: —", self)
|
||||
self.next_label = QLabel("Next: —", self)
|
||||
self.next_next_label = QLabel("Next next: —", self)
|
||||
self.next_next_label = QLabel("Then: —", self)
|
||||
|
||||
for label in (self.current_label, self.next_label, self.next_next_label):
|
||||
label.setObjectName("compactInfoLabel")
|
||||
label.setWordWrap(True)
|
||||
|
||||
info_layout.addWidget(info_title)
|
||||
info_layout.addWidget(self.current_label)
|
||||
info_layout.addWidget(self.next_label)
|
||||
info_layout.addWidget(self.next_next_label)
|
||||
@@ -75,17 +196,21 @@ class CompactAutomationPanel(QFrame):
|
||||
button_row.setSpacing(10)
|
||||
|
||||
self.play_pause_button = QPushButton("▶ Play", self)
|
||||
self.play_pause_button.setObjectName("compactPrimaryButton")
|
||||
self.play_pause_button.clicked.connect(self.play_pause_clicked.emit)
|
||||
|
||||
self.skip_button = QPushButton("⏭ Skip", self)
|
||||
self.skip_button.setObjectName("compactSecondaryButton")
|
||||
self.skip_button.clicked.connect(self.skip_clicked.emit)
|
||||
|
||||
self.step_button = QPushButton("Step Through", self)
|
||||
self.step_button.setObjectName("compactSecondaryButton")
|
||||
self.step_button.setCheckable(True)
|
||||
self.step_button.toggled.connect(self._on_step_toggled)
|
||||
|
||||
self.annotation_button = QToolButton(self)
|
||||
self.annotation_button.setText("+")
|
||||
self.annotation_button.setObjectName("compactSecondaryButton")
|
||||
self.annotation_button.setText("Annotate")
|
||||
self.annotation_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
||||
|
||||
annotation_menu = QMenu(self.annotation_button)
|
||||
@@ -96,10 +221,10 @@ class CompactAutomationPanel(QFrame):
|
||||
)
|
||||
self.annotation_button.setMenu(annotation_menu)
|
||||
|
||||
button_row.addWidget(self.play_pause_button, 1)
|
||||
button_row.addWidget(self.play_pause_button, 2)
|
||||
button_row.addWidget(self.skip_button, 1)
|
||||
button_row.addWidget(self.step_button, 1)
|
||||
button_row.addWidget(self.annotation_button)
|
||||
button_row.addWidget(self.annotation_button, 1)
|
||||
|
||||
main_layout.addLayout(button_row)
|
||||
|
||||
@@ -128,7 +253,7 @@ class CompactAutomationPanel(QFrame):
|
||||
|
||||
self.current_label.setText(f"Current: {self._format_sample(current_sample)}")
|
||||
self.next_label.setText(f"Next: {self._format_sample(next_sample)}")
|
||||
self.next_next_label.setText(f"Next next: {self._format_sample(next_next_sample)}")
|
||||
self.next_next_label.setText(f"Then: {self._format_sample(next_next_sample)}")
|
||||
|
||||
def _on_step_toggled(self, checked: bool) -> None:
|
||||
self._step_through = checked
|
||||
@@ -138,4 +263,4 @@ class CompactAutomationPanel(QFrame):
|
||||
def _format_sample(sample: SampleShortInfo | None) -> str:
|
||||
if sample is None:
|
||||
return "—"
|
||||
return f"{sample.sample_name} ({sample.loc_str()})"
|
||||
return f"{sample.sample_name} ({sample.loc_str()})"
|
||||
@@ -25,13 +25,14 @@ class SampleQueuePanel(QFrame):
|
||||
automation_running_changed = Signal(bool)
|
||||
step_through_changed = Signal(bool)
|
||||
|
||||
def __init__(self, parent=None, samples: SampleShortInfoList | None = None):
|
||||
def __init__(self, parent=None, samples: SampleShortInfoList | None = None, show_user: bool = False):
|
||||
super().__init__(parent)
|
||||
self.__baton_holder = None
|
||||
self.__busy = None
|
||||
self.__beamline_state = None
|
||||
self.__pause = True
|
||||
self.__set_to_pause = False
|
||||
self.__warning_msg_box = None
|
||||
self._current_db_id: int | None = None
|
||||
self._step_through = False
|
||||
self.ring_current = None
|
||||
@@ -49,26 +50,18 @@ class SampleQueuePanel(QFrame):
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(TitleLabel("Sample queue", self))
|
||||
# Create the table
|
||||
self.table_view = QTableView(self)
|
||||
|
||||
# Create the custom table model
|
||||
self.table_model = SampleQueueSpreadsheet()
|
||||
# Create the QTableView and set the model
|
||||
self.table_model = SampleQueueSpreadsheet(show_user=show_user)
|
||||
self.table_view.setModel(self.table_model)
|
||||
# Enable automatic column resizing
|
||||
header = self.table_view.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # Columns stretch to fill the table
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
# Set the table's size policy to expand
|
||||
self.table_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.table_view.setAcceptDrops(True)
|
||||
self.table_view.setDropIndicatorShown(True)
|
||||
|
||||
# Enable selection of multiple rows
|
||||
self.table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||
|
||||
# Set up keyboard shortcut for delete
|
||||
self.delete_shortcut = QShortcut(QKeySequence.StandardKey.Delete, self.table_view)
|
||||
self.delete_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
||||
self.delete_shortcut.activated.connect(self.remove_selected_samples)
|
||||
@@ -77,9 +70,8 @@ class SampleQueuePanel(QFrame):
|
||||
self.run_pause_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
||||
self.run_pause_shortcut.activated.connect(self.run)
|
||||
|
||||
layout.addWidget(self.table_view) # Add table to the layout
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
# Create a horizontal layout for the buttons
|
||||
button_layout = QHBoxLayout()
|
||||
self.play_button = QPushButton("▶ Run", self)
|
||||
self.play_button.clicked.connect(self.run)
|
||||
@@ -98,7 +90,6 @@ class SampleQueuePanel(QFrame):
|
||||
button_layout.addWidget(self.clear_button)
|
||||
button_layout.addWidget(self.park_and_dry_when_cleared)
|
||||
|
||||
# Add the button layout to the main layout
|
||||
layout.addLayout(button_layout)
|
||||
self._emit_samples_in_queue_changed()
|
||||
self.automation_running_changed.emit(False)
|
||||
@@ -149,6 +140,9 @@ class SampleQueuePanel(QFrame):
|
||||
)
|
||||
self.queue_samples(ordered, replace=True)
|
||||
|
||||
def annotate_sample_comment(self, db_id: int, comment: str) -> None:
|
||||
self.table_model.annotate_sample(db_id, comment)
|
||||
|
||||
def skip_current_sample(self) -> None:
|
||||
if not self.table_model.samples:
|
||||
return
|
||||
@@ -171,16 +165,12 @@ class SampleQueuePanel(QFrame):
|
||||
self._emit_samples_in_queue_changed()
|
||||
|
||||
def remove_selected_samples(self):
|
||||
"""Remove selected samples from the queue."""
|
||||
selected_indexes = self.table_view.selectionModel().selectedRows()
|
||||
if not selected_indexes:
|
||||
return
|
||||
|
||||
# Get the row numbers and sort them in descending order
|
||||
# This ensures we remove from bottom to top to avoid index shifting
|
||||
rows = sorted([index.row() for index in selected_indexes], reverse=True)
|
||||
|
||||
# Remove samples by their database IDs
|
||||
for row in rows:
|
||||
if 0 <= row < len(self.table_model.samples):
|
||||
sample = self.table_model.samples[row]
|
||||
@@ -191,7 +181,7 @@ class SampleQueuePanel(QFrame):
|
||||
def ring_current_check(self):
|
||||
if not ring_current_low_check(self, self.ring_current):
|
||||
self.table_model.set_running(False)
|
||||
self.set_to_pause = True
|
||||
self.__set_to_pause = True
|
||||
self.__pause = True
|
||||
self.play_button.setText("▶ Run")
|
||||
self._emit_samples_in_queue_changed()
|
||||
@@ -199,7 +189,7 @@ class SampleQueuePanel(QFrame):
|
||||
return True
|
||||
|
||||
def resume_automation(self):
|
||||
if hasattr(self, '__warning_msg_box') and self.__warning_msg_box:
|
||||
if self.__warning_msg_box:
|
||||
self.__warning_msg_box.done(0)
|
||||
self.__warning_msg_box = None
|
||||
|
||||
@@ -218,14 +208,15 @@ class SampleQueuePanel(QFrame):
|
||||
self.__warning_msg_box.setText("TELL reported a warning. Please see console for details.")
|
||||
self.__warning_msg_box.setInformativeText(
|
||||
"Automation paused for 10 minutes to allow sufficient dry time."
|
||||
"\nClick 'Continue Now' to resume immediately, or wait for auto-resume.")
|
||||
"\nClick 'Continue Now' to resume immediately, or wait for auto-resume."
|
||||
)
|
||||
|
||||
continue_btn = self.__warning_msg_box.addButton("Continue Now", QMessageBox.ButtonRole.AcceptRole)
|
||||
self.__warning_msg_box.setWindowModality(Qt.WindowModality.NonModal)
|
||||
continue_btn.clicked.connect(self.manual_resume_from_warning)
|
||||
self.__warning_msg_box.show()
|
||||
|
||||
def show_error_dialog(self, title:str, msg:str, info:str = None):
|
||||
def show_error_dialog(self, title: str, msg: str, info: str | None = None):
|
||||
self.__warning_msg_box = QMessageBox(self)
|
||||
self.__warning_msg_box.setIcon(QMessageBox.Icon.Warning)
|
||||
self.__warning_msg_box.setWindowTitle(title)
|
||||
@@ -250,24 +241,25 @@ class SampleQueuePanel(QFrame):
|
||||
def run(self):
|
||||
self.__recovery_timer.stop()
|
||||
if CHECK_ENABLED:
|
||||
if hasattr(self, '__warning_msg_box') and self.__warning_msg_box:
|
||||
if self.__warning_msg_box:
|
||||
self.__warning_msg_box.done(0)
|
||||
self.__warning_msg_box = None
|
||||
|
||||
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
|
||||
self.show_error_dialog(title="wrong beamline state",
|
||||
msg="Cannot run automation while beamline is not in sample alignment state",
|
||||
info="Please change the state at the bottom of the menu")
|
||||
self.show_error_dialog(
|
||||
title="wrong beamline state",
|
||||
msg="Cannot run automation while beamline is not in sample alignment state",
|
||||
info="Please change the state at the bottom of the menu",
|
||||
)
|
||||
return
|
||||
|
||||
if self.__pause:
|
||||
|
||||
if self.__busy:
|
||||
self.show_error_dialog(
|
||||
title="Beamline is busy",
|
||||
msg="Cannot run automation while beamline is busy",
|
||||
info="Please wait until beamline is idle "
|
||||
"or contact your local contact for support"
|
||||
"or contact your local contact for support",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -276,7 +268,7 @@ class SampleQueuePanel(QFrame):
|
||||
title="Session is vacant",
|
||||
msg="Starting automation while session is vacant is not currently implemented",
|
||||
info="Please grab the baton before continuing "
|
||||
"or contact your local contact for support"
|
||||
"or contact your local contact for support",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -286,7 +278,7 @@ class SampleQueuePanel(QFrame):
|
||||
msg="You do not hold the baton.",
|
||||
info="Please request the baton if it is your shift."
|
||||
"If your baton request is denied and it should be the start of your shift,"
|
||||
"please contact your local contact for support"
|
||||
"please contact your local contact for support",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -323,12 +315,13 @@ class SampleQueuePanel(QFrame):
|
||||
self.show_error_dialog(
|
||||
title="No Samples in Queue",
|
||||
msg="Cannot run automation as there are no samples in the queue.",
|
||||
info="Please add samples to the queue."
|
||||
info="Please add samples to the queue.",
|
||||
)
|
||||
return
|
||||
else:
|
||||
self.__set_to_pause = True
|
||||
self.__pause = True
|
||||
self.table_model.set_running(False)
|
||||
self.play_button.setText("▶ Run")
|
||||
self._emit_samples_in_queue_changed()
|
||||
self.automation_running_changed.emit(False)
|
||||
@@ -356,16 +349,13 @@ class SampleQueuePanel(QFrame):
|
||||
self.automation_running_changed.emit(False)
|
||||
|
||||
@Slot(int, bool)
|
||||
def automated_scan_done(self, db_id: int, success: bool, reply:str):
|
||||
def automated_scan_done(self, db_id: int, success: bool, reply: str):
|
||||
if self._current_db_id is not None and db_id != self._current_db_id:
|
||||
return
|
||||
|
||||
if CHECK_ENABLED and (self.ring_current is None or (self.ring_current < LOW_CURRENT_THRESHOLD)):
|
||||
# Pause UI state
|
||||
self.pause_automation(set_id_to_None=False)
|
||||
|
||||
# This dialog should auto-accept when current_ok() returns True.
|
||||
# No user button press is required to continue.
|
||||
def current_ok() -> bool:
|
||||
return (self.ring_current is not None) and (self.ring_current >= LOW_CURRENT_THRESHOLD)
|
||||
|
||||
@@ -403,7 +393,6 @@ class SampleQueuePanel(QFrame):
|
||||
self._current_db_id = None
|
||||
self.pause_automation()
|
||||
logger.warning("TELL Warning: Pausing for 10 minutes.")
|
||||
# recovery timer of ten minutes to allow Tell to dry. However user should be able to interupt
|
||||
self.__recovery_timer.start()
|
||||
self.show_warning_recovery_dialog()
|
||||
|
||||
@@ -425,7 +414,6 @@ class SampleQueuePanel(QFrame):
|
||||
else:
|
||||
self.table_model.remove_sample(db_id)
|
||||
self._current_db_id = None
|
||||
# Missing or other errors -> continue to next sample
|
||||
if not self.__pause and len(self.table_model.samples) > 0:
|
||||
next_item = self.table_model.samples[0]
|
||||
if self._step_through:
|
||||
|
||||
@@ -52,15 +52,12 @@ class TellSamplePanel(QFrame):
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 0)
|
||||
|
||||
# Create the custom table model
|
||||
self.table_model = UserSampleSpreadsheet(samples=samples.s)
|
||||
# Create the QTableView and set the model
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Stretch
|
||||
)
|
||||
|
||||
# Enable the custom context menu
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table_view.customContextMenuRequested.connect(self.context_menu)
|
||||
@@ -88,26 +85,34 @@ class TellSamplePanel(QFrame):
|
||||
def new_sample_list(self, samples: SampleShortInfoList):
|
||||
self.table_model.updateData(samples=samples.s)
|
||||
|
||||
def annotate_sample_comment(self, db_id: int, comment: str) -> None:
|
||||
samples = list(self.table_model.samples)
|
||||
updated_samples: list[SampleShortInfo] = []
|
||||
|
||||
for sample in samples:
|
||||
if sample.db_id == db_id:
|
||||
updated_samples.append(sample.model_copy(update={"comment": comment}))
|
||||
else:
|
||||
updated_samples.append(sample)
|
||||
|
||||
self.table_model.updateData(samples=updated_samples)
|
||||
|
||||
def context_menu(self, position):
|
||||
# Determine the row where the user clicked
|
||||
index = self.table_view.indexAt(position)
|
||||
if not index.isValid():
|
||||
return # Do nothing if user didn't click on a valid item
|
||||
return
|
||||
|
||||
row = index.row()
|
||||
sample = self.table_model.get_id(row)
|
||||
|
||||
if sample.location is None:
|
||||
return # Do nothing if sample not mounted
|
||||
return
|
||||
|
||||
# Create the context menu
|
||||
menu = QMenu()
|
||||
mount_action = menu.addAction("Mount") # Add "Mount" to the menu
|
||||
mount_action = menu.addAction("Mount")
|
||||
|
||||
# Display the menu at the cursor position
|
||||
action = menu.exec_(self.table_view.viewport().mapToGlobal(position))
|
||||
|
||||
# Handle the result of the selected action
|
||||
if action == mount_action:
|
||||
self.mount.emit(sample)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user