diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 422a6e63..a3fa57eb 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -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 ========== diff --git a/src/aare/gui/models/sample_queue_model.py b/src/aare/gui/models/sample_queue_model.py index 3c03968f..77e0569f 100644 --- a/src/aare/gui/models/sample_queue_model.py +++ b/src/aare/gui/models/sample_queue_model.py @@ -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", []) ] diff --git a/src/aare/gui/panels/compact_automation_panel.py b/src/aare/gui/panels/compact_automation_panel.py index 09b1c8a5..2597dec6 100644 --- a/src/aare/gui/panels/compact_automation_panel.py +++ b/src/aare/gui/panels/compact_automation_panel.py @@ -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()})" \ No newline at end of file diff --git a/src/aare/gui/panels/sample_queue_panel.py b/src/aare/gui/panels/sample_queue_panel.py index 97327779..8692be3f 100644 --- a/src/aare/gui/panels/sample_queue_panel.py +++ b/src/aare/gui/panels/sample_queue_panel.py @@ -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: diff --git a/src/aare/gui/panels/tell_sample_panel.py b/src/aare/gui/panels/tell_sample_panel.py index 253c8e3a..32228876 100644 --- a/src/aare/gui/panels/tell_sample_panel.py +++ b/src/aare/gui/panels/tell_sample_panel.py @@ -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)