From 675549c1c2cf3fbe93c615972167511519dfebd1 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Tue, 9 Jun 2026 13:37:33 +0200 Subject: [PATCH] GUI: added automation_progress.py and updated comapct automation view --- src/aare/gui/main_window.py | 7 + .../gui/panels/compact_automation_panel.py | 235 +++++++++++------- src/aare/gui/widgets/automation_progress.py | 181 ++++++++++++++ 3 files changed, 327 insertions(+), 96 deletions(-) create mode 100644 src/aare/gui/widgets/automation_progress.py diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index a3fa57eb..1af8a7f0 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -412,6 +412,9 @@ class MainWindow(QMainWindow): self.job_list_panel.step_through_changed.connect( self.compact_automation_panel.set_step_through ) + self.job_list_panel.samples_in_queue_changed.connect( + self.compact_automation_panel.set_samples_in_queue + ) self.job_list_panel.samples_in_queue_changed.connect( self._refresh_compact_queue_preview ) @@ -422,6 +425,9 @@ class MainWindow(QMainWindow): 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.compact_automation_panel.set_samples_in_queue( + len(self.job_list_panel.table_model.samples) + ) self._refresh_compact_queue_preview() # smargon trace panel @@ -515,6 +521,7 @@ class MainWindow(QMainWindow): self.daq.baton_response_result.connect(self._on_baton_response_result) self.daq.baton_timeout_checked.connect(self._on_baton_timeout_checked) self.daq.automation_progress.connect(self.automation_progress_panel.set_progress) + self.daq.automation_progress.connect(self.compact_automation_panel.set_progress) self.status_bar.baton_request_received.connect(self._show_baton_request_dialog) self.status_bar.baton_request_accepted.connect(self._accept_baton_request) diff --git a/src/aare/gui/panels/compact_automation_panel.py b/src/aare/gui/panels/compact_automation_panel.py index 2597dec6..3504ed77 100644 --- a/src/aare/gui/panels/compact_automation_panel.py +++ b/src/aare/gui/panels/compact_automation_panel.py @@ -10,7 +10,9 @@ from PySide6.QtWidgets import ( QWidget, ) +from aare.common.automation_models import AutomationProgress from aare.common.models import SampleShortInfo +from aare.gui.widgets.automation_progress import CompactAutomationProgressStrip class CompactAutomationPanel(QFrame): @@ -35,162 +37,169 @@ class CompactAutomationPanel(QFrame): self.setStyleSheet( """ QFrame#compactAutomationPanel { - background: #eef4ff; + background: #edf3fb; border: none; + border-radius: 18px; + } + + QFrame#compactTopCard, + QFrame#compactProgressCard, + QFrame#compactControlsCard { + background: #ffffff; + border: 1px solid #d9e4f2; border-radius: 16px; } - QFrame#compactAutomationHeader { - background: #1f3b63; - border-radius: 14px; + QFrame#compactQueueItem { + background: #f7faff; + border: 1px solid #dbe6f5; + border-radius: 12px; } - QLabel#compactAutomationTitle { - color: white; - font-size: 20px; + QLabel#compactQueueTitle { + color: #4b5563; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + } + + QLabel#compactQueueValue { + color: #0f172a; + font-size: 14px; font-weight: 700; } - QLabel#compactAutomationSubtitle { - color: #d7e6ff; + QLabel#compactDashboardLabel { + color: #334155; font-size: 12px; + font-weight: 700; } 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; + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + border-radius: 14px; + padding: 10px 14px; font-size: 18px; font-weight: 700; } QToolButton#compactMenuButton:hover { - background: rgba(255, 255, 255, 0.24); + background: #dbeafe; } - QFrame#compactInfoPanel { - background: white; - border: 1px solid #d5e0f2; - border-radius: 14px; + QFrame#compactAutomationProgressStrip { + background: transparent; + border: none; } - 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; + QLabel#compactProgressSummary { + color: #1e293b; font-size: 13px; + padding: 2px 2px 6px 2px; + } + + QLabel#compactProgressStep { + background: #f8fbff; + border: 1px solid #dbe6f5; + border-radius: 10px; + padding: 8px 6px; } QPushButton#compactPrimaryButton { - background: #2f6fed; + background: #2563eb; color: white; border: none; - border-radius: 12px; - padding: 12px 16px; - font-size: 14px; + border-radius: 14px; + padding: 14px 18px; + font-size: 15px; font-weight: 700; } QPushButton#compactPrimaryButton:hover { - background: #255fd0; + background: #1d4ed8; } QPushButton#compactSecondaryButton { - background: white; - color: #1f3b63; - border: 1px solid #c8d7ef; - border-radius: 12px; - padding: 12px 16px; + background: #ffffff; + color: #0f172a; + border: 1px solid #cbd5e1; + border-radius: 14px; + padding: 14px 18px; font-size: 14px; font-weight: 700; } QPushButton#compactSecondaryButton:hover, QToolButton#compactSecondaryButton:hover { - background: #f4f8ff; + background: #f8fafc; } QToolButton#compactSecondaryButton { - background: white; - color: #1f3b63; - border: 1px solid #c8d7ef; - border-radius: 12px; - padding: 12px 16px; - font-size: 16px; + background: #ffffff; + color: #0f172a; + border: 1px solid #cbd5e1; + border-radius: 14px; + padding: 14px 18px; + font-size: 14px; font-weight: 700; } """ ) main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(20, 20, 20, 20) - main_layout.setSpacing(14) + main_layout.setContentsMargins(18, 18, 18, 18) + main_layout.setSpacing(12) - header = QFrame(self) - header.setObjectName("compactAutomationHeader") - header_layout = QHBoxLayout(header) - header_layout.setContentsMargins(14, 12, 14, 12) - header_layout.setSpacing(10) + top_card = QFrame(self) + top_card.setObjectName("compactTopCard") + top_layout = QHBoxLayout(top_card) + top_layout.setContentsMargins(12, 12, 12, 12) + top_layout.setSpacing(10) + + self.current_card = self._build_queue_item("Current", "—") + self.next_card = self._build_queue_item("Next", "—") + self.next_next_card = self._build_queue_item("Then", "—") + + top_layout.addWidget(self.current_card["frame"], 2) + top_layout.addWidget(self.next_card["frame"], 2) + top_layout.addWidget(self.next_next_card["frame"], 2) 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.setToolTip("Return to main view") self.menu_button.clicked.connect(self.show_full_view_requested.emit) + top_layout.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop) - title_container = QWidget(self) - title_layout = QVBoxLayout(title_container) - title_layout.setContentsMargins(0, 0, 0, 0) - title_layout.setSpacing(2) - - self.title_label = QLabel("Automation View", self) - self.title_label.setObjectName("compactAutomationTitle") - - 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(top_card) main_layout.addWidget(camera_widget, 1) - info_panel = QFrame(self) - info_panel.setObjectName("compactInfoPanel") - info_layout = QVBoxLayout(info_panel) - info_layout.setContentsMargins(14, 14, 14, 14) - info_layout.setSpacing(8) + progress_card = QFrame(self) + progress_card.setObjectName("compactProgressCard") + progress_layout = QVBoxLayout(progress_card) + progress_layout.setContentsMargins(12, 10, 12, 10) + progress_layout.setSpacing(6) - info_title = QLabel("Queue preview", self) - info_title.setObjectName("compactSectionTitle") + progress_title = QLabel("Automation progress", self) + progress_title.setObjectName("compactDashboardLabel") + progress_layout.addWidget(progress_title) - self.current_label = QLabel("Current: —", self) - self.next_label = QLabel("Next: —", self) - self.next_next_label = QLabel("Then: —", self) + self.progress_strip = CompactAutomationProgressStrip(self) + progress_layout.addWidget(self.progress_strip) - for label in (self.current_label, self.next_label, self.next_next_label): - label.setObjectName("compactInfoLabel") - label.setWordWrap(True) + main_layout.addWidget(progress_card) - info_layout.addWidget(info_title) - info_layout.addWidget(self.current_label) - info_layout.addWidget(self.next_label) - info_layout.addWidget(self.next_next_label) + controls_card = QFrame(self) + controls_card.setObjectName("compactControlsCard") + controls_layout = QVBoxLayout(controls_card) + controls_layout.setContentsMargins(12, 12, 12, 12) + controls_layout.setSpacing(10) - main_layout.addWidget(info_panel) + controls_title = QLabel("Controls", self) + controls_title.setObjectName("compactDashboardLabel") + controls_layout.addWidget(controls_title) button_row = QHBoxLayout() button_row.setSpacing(10) @@ -226,12 +235,38 @@ class CompactAutomationPanel(QFrame): button_row.addWidget(self.step_button, 1) button_row.addWidget(self.annotation_button, 1) - main_layout.addLayout(button_row) + controls_layout.addLayout(button_row) + main_layout.addWidget(controls_card) + + def _build_queue_item(self, title: str, value: str) -> dict[str, QWidget | QLabel]: + frame = QFrame(self) + frame.setObjectName("compactQueueItem") + + layout = QVBoxLayout(frame) + layout.setContentsMargins(12, 10, 12, 10) + layout.setSpacing(4) + + title_label = QLabel(title, self) + title_label.setObjectName("compactQueueTitle") + + value_label = QLabel(value, self) + value_label.setObjectName("compactQueueValue") + value_label.setWordWrap(True) + + layout.addWidget(title_label) + layout.addWidget(value_label) + + return { + "frame": frame, + "title": title_label, + "value": value_label, + } @Slot(bool) def set_running(self, running: bool) -> None: self._running = bool(running) self.play_pause_button.setText("⏸ Pause" if self._running else "▶ Play") + self.progress_strip.set_running(running) @Slot(bool) def set_step_through(self, enabled: bool) -> None: @@ -240,6 +275,14 @@ class CompactAutomationPanel(QFrame): self.step_button.setChecked(self._step_through) self.step_button.blockSignals(False) + @Slot(int) + def set_samples_in_queue(self, count: int) -> None: + self.progress_strip.set_samples_in_queue(count) + + @Slot(object) + def set_progress(self, progress: AutomationProgress) -> None: + self.progress_strip.set_progress(progress) + @Slot(object, object, object) def set_samples( self, @@ -251,9 +294,9 @@ class CompactAutomationPanel(QFrame): self._next_sample = next_sample self._next_next_sample = next_next_sample - 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"Then: {self._format_sample(next_next_sample)}") + self.current_card["value"].setText(self._format_sample(current_sample)) + self.next_card["value"].setText(self._format_sample(next_sample)) + self.next_next_card["value"].setText(self._format_sample(next_next_sample)) def _on_step_toggled(self, checked: bool) -> None: self._step_through = checked diff --git a/src/aare/gui/widgets/automation_progress.py b/src/aare/gui/widgets/automation_progress.py new file mode 100644 index 00000000..2803fa0a --- /dev/null +++ b/src/aare/gui/widgets/automation_progress.py @@ -0,0 +1,181 @@ +from __future__ import annotations + +import time +from datetime import datetime + +from PySide6.QtCore import QTimer, Slot +from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QVBoxLayout, QWidget + +from aare.common.automation_models import AutomationProgress, StepStatus, WorkflowStateKind + + +class CompactAutomationProgressStrip(QFrame): + DEFAULT_SAMPLE_ESTIMATE_S = 150.0 + + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + self._progress: AutomationProgress | None = None + self._queue_count = 0 + self._running = False + self._step_labels: dict[WorkflowStateKind, QLabel] = {} + self._summary_label: QLabel | None = None + self._timer = QTimer(self) + self._timer.setInterval(1000) + self._timer.timeout.connect(self._refresh_live_view) + + self.setObjectName("compactAutomationProgressStrip") + + layout = QVBoxLayout(self) + layout.setContentsMargins(14, 12, 14, 12) + layout.setSpacing(10) + + self._summary_label = QLabel("Automation idle", self) + self._summary_label.setObjectName("compactProgressSummary") + self._summary_label.setWordWrap(True) + layout.addWidget(self._summary_label) + + steps_row = QHBoxLayout() + steps_row.setSpacing(8) + + for step in ( + WorkflowStateKind.MOUNT, + WorkflowStateKind.LOOP_CENTRE, + WorkflowStateKind.RASTER, + WorkflowStateKind.DATA_COLLECTION, + WorkflowStateKind.FINAL, + ): + label = QLabel(self) + label.setObjectName("compactProgressStep") + label.setMinimumWidth(90) + label.setWordWrap(True) + steps_row.addWidget(label, 1) + self._step_labels[step] = label + + layout.addLayout(steps_row) + self._apply_empty_state() + + def _apply_empty_state(self) -> None: + if self._summary_label is not None: + self._summary_label.setText("Automation idle") + for step, label in self._step_labels.items(): + label.setText(self._format_step_html(step, StepStatus.PENDING)) + + @staticmethod + def _step_title(step: WorkflowStateKind) -> str: + return { + WorkflowStateKind.MOUNT: "Mount", + WorkflowStateKind.LOOP_CENTRE: "Center", + WorkflowStateKind.RASTER: "Raster", + WorkflowStateKind.DATA_COLLECTION: "Collect", + WorkflowStateKind.FINAL: "Finish", + }.get(step, str(step.value)) + + @staticmethod + def _step_icon(status: StepStatus) -> str: + return { + StepStatus.PENDING: "○", + StepStatus.RUNNING: "◉", + StepStatus.SUCCESS: "✓", + StepStatus.FAILED: "✕", + StepStatus.SKIPPED: "↷", + StepStatus.PAUSED: "⏸", + }.get(status, "○") + + @staticmethod + def _step_color(status: StepStatus) -> str: + return { + StepStatus.PENDING: "#64748b", + StepStatus.RUNNING: "#2563eb", + StepStatus.SUCCESS: "#15803d", + StepStatus.FAILED: "#b91c1c", + StepStatus.SKIPPED: "#64748b", + StepStatus.PAUSED: "#c2410c", + }.get(status, "#64748b") + + def _format_step_html(self, step: WorkflowStateKind, status: StepStatus) -> str: + color = self._step_color(status) + icon = self._step_icon(status) + title = self._step_title(step) + return ( + f"
" + f"
{icon}
" + f"
{title}
" + f"
" + ) + + @staticmethod + def _format_duration(seconds: float | None) -> str: + if seconds is None or seconds <= 0: + return "0m 00s" + total = int(round(seconds)) + minutes, secs = divmod(total, 60) + if minutes < 60: + return f"{minutes}m {secs:02d}s" + hours, minutes = divmod(minutes, 60) + return f"{hours}h {minutes:02d}m" + + @staticmethod + def _format_eta(epoch_seconds: float | None) -> str: + if epoch_seconds is None: + return "N/A" + return datetime.fromtimestamp(epoch_seconds).strftime("%H:%M:%S") + + def _refresh_live_view(self) -> None: + if self._progress is not None: + self.set_progress(self._progress) + + @Slot(bool) + def set_running(self, running: bool) -> None: + self._running = bool(running) + if self._progress is not None: + self.set_progress(self._progress) + + @Slot(int) + def set_samples_in_queue(self, count: int) -> None: + self._queue_count = max(0, int(count)) + if self._progress is not None: + self.set_progress(self._progress) + + @Slot(object) + def set_progress(self, progress: AutomationProgress) -> None: + self._progress = progress + + any_running = any(step.status == StepStatus.RUNNING for step in progress.steps) + if any_running and self._running: + if not self._timer.isActive(): + self._timer.start() + else: + self._timer.stop() + + current_sample = progress.current_sample_name or "None" + avg_time = progress.avg_time_per_sample if progress.avg_time_per_sample > 0 else self.DEFAULT_SAMPLE_ESTIMATE_S + queue_remaining = avg_time * self._queue_count + eta = time.time() + queue_remaining if queue_remaining > 0 else None + + state_text = "Paused" + state_color = "#c2410c" + if progress.finished and progress.success is True: + state_text = "Completed" + state_color = "#15803d" + elif progress.finished and progress.success is False: + state_text = "Failed" + state_color = "#b91c1c" + elif self._running: + state_text = "Running" + state_color = "#2563eb" + elif not self._running: + state_text = "Paused" + state_color = "#c2410c" + + if self._summary_label is not None: + self._summary_label.setText( + f"Status: {state_text}" + f"    Current: {current_sample}" + f"    Queue: {self._queue_count}" + f"    ETA: {self._format_duration(queue_remaining)}" + f"    Done by: {self._format_eta(eta)}" + ) + + states = {step.step: step.status for step in progress.steps} + for step, label in self._step_labels.items(): + label.setText(self._format_step_html(step, states.get(step, StepStatus.PENDING)))