ui.dl_execution_page

  1import os
  2import subprocess
  3
  4import torch
  5from PyQt6 import QtWidgets, QtGui, QtCore
  6from PyQt6.QtGui import QFont
  7from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QLabel, QPushButton,
  8                             QScrollArea, QFrame, QGridLayout, QHBoxLayout,
  9                             QMessageBox, QGroupBox, QListWidget, QProgressBar,
 10                             QListWidgetItem, QTextEdit, QSplitter, QFileDialog,
 11                             QCheckBox)
 12from PyQt6.QtCore import Qt, QThread, QCoreApplication
 13
 14from components.circular_progress_bar import CircularProgress
 15from threads.dl_worker import DlWorker
 16from page import Page
 17from logger import get_logger
 18
 19log = get_logger()
 20
 21
 22class DlExecutionPage(Page):
 23    """Page for SynthStrip + Coregistration of selected NIfTI files."""
 24
 25    def __init__(self, context=None, previous_page=None):
 26        super().__init__()
 27        self.worker = None  # Worker thread instance
 28        self.current_file = None  # Currently processing file (if needed)
 29        self.context = context  # Shared application context (settings, file lists)
 30        self.previous_page = previous_page  # Reference to the previous page for navigation
 31        self.next_page = None  # Reference to the next page
 32
 33        # State flags
 34        self.processing = False  # Flag to indicate if processing is active
 35        self.processing_completed = False  # Flag to indicate if processing has finished
 36
 37        self._setup_ui()  # Initialize the user interface elements
 38
 39        self._translate_ui()  # Apply initial translations
 40        if context and "language_changed" in context:
 41            # Connect to the global language change signal if it exists
 42            context["language_changed"].connect(self._translate_ui)
 43
 44    def _setup_ui(self):
 45        """Sets up the user interface layout and widgets."""
 46        main_layout = QVBoxLayout(self)
 47
 48        # --- Header ---
 49        self.header = QLabel("Deep Learning Segmentation")
 50        self.header.setFont(QFont("Arial", 18, QFont.Weight.Bold))
 51        self.header.setAlignment(Qt.AlignmentFlag.AlignCenter)
 52        main_layout.addWidget(self.header)
 53
 54        # --- Info Label (Tool detection) ---
 55        self.info_label = QLabel("")
 56        self.info_label.setOpenExternalLinks(True)
 57        self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
 58
 59        self.info_label.setStyleSheet("""
 60                            font-size: 11px;
 61                            color: #666;
 62                            font-style: italic;
 63                            margin-bottom: 6px;
 64                        """)
 65        self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
 66        self.info_label.setWordWrap(True)
 67        main_layout.addWidget(self.info_label)
 68
 69        try:
 70            # Run the command with --help to check if it exists and works
 71            subprocess.run(
 72                ['mri_synthstrip', '--help'],
 73                stdout=subprocess.DEVNULL,  # Discard standard output
 74                stderr=subprocess.DEVNULL  # Discard standard error
 75            )
 76            self.has_freesurfer = True
 77            self.info_label.setText(
 78                "Using tool: <a href='https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/'>SynthStrip from Free Surfer</a>")
 79        except (FileNotFoundError, subprocess.CalledProcessError):
 80            # Handle case where FreeSurfer version is not found
 81            self.has_freesurfer = False
 82            self.info_label.setText(
 83                'Using tool: <a href="https://github.com/nipreps/synthstrip">SynthStrip (nipreps)</a><br>'
 84                'To use SynthStrip from FreeSurfer, follow the instructions at: '
 85                '<a href="https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/">SynthStrip official documentation</a>'
 86            )
 87
 88        self.info_label.setOpenExternalLinks(True)  # Enable opening links in browser
 89        self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
 90        self.info_label.setToolTip("Open link")
 91
 92        # --- Current Operation Status ---
 93        self.current_operation = QLabel("Ready to start")
 94        self.current_operation.setStyleSheet("""
 95            font-size: 13px;
 96            color: #7f8c8d;
 97            margin-top: 8px;
 98        """)
 99        self.current_operation.setAlignment(Qt.AlignmentFlag.AlignCenter)
100        main_layout.addWidget(self.current_operation)
101
102        # --- Content area (Grid) ---
103        content_layout = QGridLayout()
104        main_layout.addLayout(content_layout, stretch=1)
105
106        # --- Left Column (Row 0, Col 0): Progress Display ---
107        left_layout = QVBoxLayout()
108        self.progress_bar = CircularProgress()  # Custom circular progress widget
109        left_layout.addWidget(self.progress_bar, alignment=Qt.AlignmentFlag.AlignCenter)
110        content_layout.addLayout(left_layout, 0, 0)
111
112        # --- Right Column (Row 0, Col 1): File List ---
113        self.files_group = QGroupBox("Files to process")
114        files_layout = QVBoxLayout(self.files_group)
115
116        self.files_list = QListWidget()
117        self.files_list.setMaximumHeight(150)
118        files_layout.addWidget(self.files_list)
119
120        # Add the file list (QListWidget) directly to the grid
121        content_layout.addWidget(self.files_list, 0, 1)
122
123        # --- Grid Layout Stretch ---
124        # Columns: 1/3 for progress bar, 2/3 for scroll area
125        content_layout.setColumnStretch(0, 1)  # Left column (progress bar)
126        content_layout.setColumnStretch(1, 2)  # Right column (folder list)
127
128        # --- Bottom Row (Rows 1 & 2): Log Display ---
129        self.log_label = QLabel("Execution Log:")
130        self.log_label.setStyleSheet("""
131                    font-size: 16px;
132                    font-weight: bold;
133                    color: #2c3e50;
134                    margin-top: 15px;
135                    margin-bottom: 5px;
136                """)
137        # Add log label, spanning both columns (Row 1, Col 0, 1 row span, 2 col span)
138        content_layout.addWidget(self.log_label, 1, 0, 1, 2)
139
140        self.log_text = QTextEdit()
141        self.log_text.setStyleSheet("""
142                    QTextEdit {
143                        background-color: #2c3e50;
144                        color: #ecf0f1;
145                        font-family: 'Courier New', monospace;
146                        font-size: 11px;
147                        border: 2px solid #34495e;
148                        border-radius: 8px;
149                        padding: 8px;
150                    }
151                """)
152        self.log_text.setMaximumHeight(200)
153        self.log_text.setReadOnly(True)
154        # Add log text area, spanning both columns (Row 2, Col 0, 1 row span, 2 col span)
155        content_layout.addWidget(self.log_text, 2, 0, 1, 2)
156
157        # === Control Buttons ===
158
159        # --- Button Layout (Start/Stop) ---
160        button_frame = QFrame()  # Frame to hold the centered buttons
161        button_layout = QHBoxLayout(button_frame)
162        button_layout.setContentsMargins(0, 15, 0, 0)
163
164        self.start_button = QPushButton("Start deep learning")
165        button_layout.addStretch()  # Center the buttons
166        self.start_button.clicked.connect(self.start_processing)
167        self.start_button.setStyleSheet("""
168            QPushButton {
169                font-size: 14px;
170                font-weight: bold;
171                background-color: #27ae60;
172                color: white;
173                border-radius: 8px;
174                padding: 10px 20px;
175                min-width: 140px;
176            }
177            QPushButton:hover { 
178                background-color: #229954; 
179            }
180            QPushButton:disabled {
181                background-color: #bdc3c7;
182                color: #7f8c8d;
183            }
184        """)
185        button_layout.addWidget(self.start_button)
186
187        self.cancel_button = QPushButton("Stop processing")
188        self.cancel_button.setStyleSheet("""
189            QPushButton {
190                font-size: 14px;
191                font-weight: bold;
192                background-color: #e74c3c;
193                color: white;
194                border-radius: 8px;
195                padding: 10px 20px;
196                min-width: 140px;
197            }
198            QPushButton:hover { 
199                background-color: #c0392b; 
200            }
201            QPushButton:disabled {
202                background-color: #bdc3c7;
203                color: #7f8c8d;
204            }
205        """)
206        self.cancel_button.clicked.connect(self.cancel_processing)
207        self.cancel_button.setVisible(False)  # Initially hidden
208
209        button_layout.addWidget(self.cancel_button)
210        button_layout.addStretch()  # Center the buttons
211
212        main_layout.addWidget(button_frame)
213
214    def on_enter(self):
215        """Called when this page becomes active."""
216        self.reset_processing_state()  # Ensure UI is in a clean, default state
217
218        # Populate the file list if available from context
219        if self.context and "selected_segmentation_files" in self.context:
220            self.files_list.clear()
221            for file_path in self.context["selected_segmentation_files"]:
222                filename = os.path.basename(file_path)
223                # Add item with "Waiting..." status
224                self.files_list.addItem(
225                    QCoreApplication.translate("DlExecutionPage", "📄 {filename} - Waiting...").format(
226                        filename=filename))
227
228    def start_processing(self):
229        """Starts the deep learning processing workflow."""
230        if not self.context or "selected_segmentation_files" not in self.context:
231            QMessageBox.warning(self, QCoreApplication.translate("DlExecutionPage", "Error"),
232                                QCoreApplication.translate("DlExecutionPage", "No files selected for processing."))
233            return
234
235        selected_files = self.context["selected_segmentation_files"]
236        if not selected_files:
237            QMessageBox.warning(self, QCoreApplication.translate("DlExecutionPage", "Error"),
238                                QCoreApplication.translate("DlExecutionPage", "No files selected for processing."))
239            return
240
241        # --- Setup Worker Thread ---
242        self.worker = DlWorker(
243            input_files=selected_files,
244            workspace_path=self.context["workspace_path"],
245            has_freesurfer=self.has_freesurfer
246        )
247
248        # --- Connect Worker Signals ---
249        self.worker.progressbar_update.connect(self.update_progress)
250        self.worker.file_update.connect(self.update_file_status)
251        self.worker.log_update.connect(self.add_log_message)
252        self.worker.finished.connect(self.processing_finished)
253        # self.worker.finished.connect(self.worker.deleteLater) # Option to auto-delete worker
254
255        # --- Update UI for Processing State ---
256        self.processing = True
257        self.start_button.setVisible(False)
258        self.cancel_button.setVisible(True)
259        self.progress_bar.setVisible(True)
260        self.progress_bar.setValue(0)
261        self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Processing..."))
262        self.log_text.clear()
263
264        # --- Start Worker ---
265        self.worker.start()  # Start the QThread
266
267        self.add_log_message(
268            QCoreApplication.translate("DlExecutionPage", "Deep learning processing started for {0} file").format(
269                len(selected_files)), 'i')
270        log.info(f"Deep learning processing started for {len(selected_files)} file")
271
272    def update_progress(self, value):
273        """Updates the circular progress bar value."""
274        self.progress_bar.setValue(value)
275
276    def add_log_message(self, message, type):
277        """Appends a message to the UI log text area."""
278        timestamp = QtCore.QDateTime.currentDateTime().toString("hh:mm:ss")
279        self.log_text.append(f"[{timestamp}] {message}")
280
281        # Log to external file based on message type
282        if type == 'e':
283            log.error(f"[{timestamp}] {message}")
284        elif type == 'i':
285            log.info(f"[{timestamp}] {message}")
286        elif type == 'w':
287            log.warning(f"[{timestamp}] {message}")
288        else:
289            log.debug(f"[{timestamp}] {message}")
290
291        # Auto-scroll to the bottom
292        scrollbar = self.log_text.verticalScrollBar()
293        scrollbar.setValue(scrollbar.maximum())
294
295    def update_file_status(self, filename, status):
296        """Updates the status of a specific file in the QListWidget."""
297        for i in range(self.files_list.count()):
298            item = self.files_list.item(i)
299            if filename in item.text():  # Find the matching item
300                item.setText(f"📄 {filename} - {status}")
301                break  # Stop searching once found
302
303    def cancel_processing(self):
304        """Requests the worker thread to cancel processing."""
305        if self.worker:
306            self.worker.cancel_requested.emit()  # Emit the signal to request cancellation
307            # self.worker.cancel() # If you have a hard cancel method
308            self.add_log_message(QCoreApplication.translate("DlExecutionPage", "Cancellation requested..."), 'i')
309
310    def processing_finished(self, success, message):
311        """Slot called when the worker thread finishes."""
312        self.processing = False
313        self.processing_completed = True
314
315        # --- Update UI to Reflect Finished State ---
316        self.start_button.setVisible(True)
317        self.cancel_button.setVisible(False)
318        self.progress_bar.setVisible(False)
319
320        if success:
321            self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "✓ Processing completed!"))
322            self.current_operation.setStyleSheet("color: green; font-weight: bold;")
323            self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Reprocess"))
324        else:
325            self.current_operation.setText("✗ Processing failed!")
326            self.current_operation.setStyleSheet("color: #c0392b; font-weight: bold;")
327
328        self.add_log_message(QCoreApplication.translate("DlExecutionPage", "Final: {message}").format(message=message),
329                             'i')
330
331        # --- Update Context with Results ---
332        if success and "workspace_path" in self.context:
333            # If successful, store the output directory in the context
334            output_dir = os.path.join(self.context["workspace_path"], "outputs")
335            self.context["processing_output_dir"] = output_dir
336
337        # Notify main window to update its buttons (e.g., 'Next')
338        if "update_main_buttons" in self.context:
339            self.context["update_main_buttons"]()
340
341        # --- Show Final Message Box ---
342        if success:
343            QMessageBox.information(self, QCoreApplication.translate("DlExecutionPage", "Completed"), message)
344        else:
345            QMessageBox.critical(self, QCoreApplication.translate("DlExecutionPage", "Error"), message)
346
347    def reset_processing_state(self):
348        """Resets the page to its initial state."""
349        self.processing = False
350        self.processing_completed = False
351        self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Start deep learning"))
352        self.start_button.setVisible(True)
353        self.cancel_button.setVisible(False)
354        self.progress_bar.setVisible(False)
355        self.current_operation.setStyleSheet("")  # Reset color
356        self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Ready to start"))
357
358        if self.worker:
359            self.worker = None  # Clear worker reference
360
361    def back(self):
362        """Handles the 'Back' navigation request."""
363        if self.processing:
364            # Ask user for confirmation to stop
365            reply = QMessageBox.question(
366                self,
367                QCoreApplication.translate("DlExecutionPage", "Processing in progress"),
368                QCoreApplication.translate("DlExecutionPage",
369                                           "Processing is in progress. Do you really want to go back?\nProcessing will be interrupted."),
370                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
371                QMessageBox.StandardButton.No
372            )
373            if reply == QMessageBox.StandardButton.No:
374                return None  # Abort navigation
375
376            # --- Stop Processing ---
377            if self.worker:
378                self.worker.cancel_requested.emit()
379                # self.worker.cancel()
380
381        if self.previous_page:
382            self.previous_page.on_enter()  # Call on_enter of the previous page
383            return self.previous_page
384        return None  # No previous page
385
386    def next(self, context):
387        """Handles the 'Next' navigation request."""
388        return None  # This page is a terminal step
389
390    def is_ready_to_advance(self):
391        """Checks if the page is in a state to advance."""
392        return False  # Cannot advance from this page
393
394    def is_ready_to_go_back(self):
395        """Checks if the page is in a state to go back."""
396        return True  # Can always go back (with a warning if processing)
397
398    def _translate_ui(self):
399        """Updates all user-visible strings for translation."""
400        self.header.setText(QCoreApplication.translate("DlExecutionPage", "Deep Learning Segmentation"))
401
402        # Only reset 'Ready to start' if not processing or completed
403        if not self.processing and not self.processing_completed:
404            self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Ready to start"))
405
406        self.files_group.setTitle(QCoreApplication.translate("DlExecutionPage", "Files to process"))
407        self.log_label.setText(QCoreApplication.translate("DlExecutionPage", "Execution Log:"))
408
409        if not self.processing_completed:
410            self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Start deep learning"))
411        else:
412            self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Reprocess"))
413
414        self.cancel_button.setText(QCoreApplication.translate("DlExecutionPage", "Stop processing"))
415
416        # Repopulate the file list with translated "Waiting..." string
417        if self.context and "selected_segmentation_files" in self.context and not self.processing:
418            self.files_list.clear()
419            for file_path in self.context["selected_segmentation_files"]:
420                filename = os.path.basename(file_path)
421                self.files_list.addItem(
422                    QCoreApplication.translate("DlExecutionPage", "📄 {filename} - Waiting...").format(
423                        filename=filename))
log = <Logger GliAAns-UI (WARNING)>
class DlExecutionPage(page.Page):
 23class DlExecutionPage(Page):
 24    """Page for SynthStrip + Coregistration of selected NIfTI files."""
 25
 26    def __init__(self, context=None, previous_page=None):
 27        super().__init__()
 28        self.worker = None  # Worker thread instance
 29        self.current_file = None  # Currently processing file (if needed)
 30        self.context = context  # Shared application context (settings, file lists)
 31        self.previous_page = previous_page  # Reference to the previous page for navigation
 32        self.next_page = None  # Reference to the next page
 33
 34        # State flags
 35        self.processing = False  # Flag to indicate if processing is active
 36        self.processing_completed = False  # Flag to indicate if processing has finished
 37
 38        self._setup_ui()  # Initialize the user interface elements
 39
 40        self._translate_ui()  # Apply initial translations
 41        if context and "language_changed" in context:
 42            # Connect to the global language change signal if it exists
 43            context["language_changed"].connect(self._translate_ui)
 44
 45    def _setup_ui(self):
 46        """Sets up the user interface layout and widgets."""
 47        main_layout = QVBoxLayout(self)
 48
 49        # --- Header ---
 50        self.header = QLabel("Deep Learning Segmentation")
 51        self.header.setFont(QFont("Arial", 18, QFont.Weight.Bold))
 52        self.header.setAlignment(Qt.AlignmentFlag.AlignCenter)
 53        main_layout.addWidget(self.header)
 54
 55        # --- Info Label (Tool detection) ---
 56        self.info_label = QLabel("")
 57        self.info_label.setOpenExternalLinks(True)
 58        self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
 59
 60        self.info_label.setStyleSheet("""
 61                            font-size: 11px;
 62                            color: #666;
 63                            font-style: italic;
 64                            margin-bottom: 6px;
 65                        """)
 66        self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
 67        self.info_label.setWordWrap(True)
 68        main_layout.addWidget(self.info_label)
 69
 70        try:
 71            # Run the command with --help to check if it exists and works
 72            subprocess.run(
 73                ['mri_synthstrip', '--help'],
 74                stdout=subprocess.DEVNULL,  # Discard standard output
 75                stderr=subprocess.DEVNULL  # Discard standard error
 76            )
 77            self.has_freesurfer = True
 78            self.info_label.setText(
 79                "Using tool: <a href='https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/'>SynthStrip from Free Surfer</a>")
 80        except (FileNotFoundError, subprocess.CalledProcessError):
 81            # Handle case where FreeSurfer version is not found
 82            self.has_freesurfer = False
 83            self.info_label.setText(
 84                'Using tool: <a href="https://github.com/nipreps/synthstrip">SynthStrip (nipreps)</a><br>'
 85                'To use SynthStrip from FreeSurfer, follow the instructions at: '
 86                '<a href="https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/">SynthStrip official documentation</a>'
 87            )
 88
 89        self.info_label.setOpenExternalLinks(True)  # Enable opening links in browser
 90        self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
 91        self.info_label.setToolTip("Open link")
 92
 93        # --- Current Operation Status ---
 94        self.current_operation = QLabel("Ready to start")
 95        self.current_operation.setStyleSheet("""
 96            font-size: 13px;
 97            color: #7f8c8d;
 98            margin-top: 8px;
 99        """)
100        self.current_operation.setAlignment(Qt.AlignmentFlag.AlignCenter)
101        main_layout.addWidget(self.current_operation)
102
103        # --- Content area (Grid) ---
104        content_layout = QGridLayout()
105        main_layout.addLayout(content_layout, stretch=1)
106
107        # --- Left Column (Row 0, Col 0): Progress Display ---
108        left_layout = QVBoxLayout()
109        self.progress_bar = CircularProgress()  # Custom circular progress widget
110        left_layout.addWidget(self.progress_bar, alignment=Qt.AlignmentFlag.AlignCenter)
111        content_layout.addLayout(left_layout, 0, 0)
112
113        # --- Right Column (Row 0, Col 1): File List ---
114        self.files_group = QGroupBox("Files to process")
115        files_layout = QVBoxLayout(self.files_group)
116
117        self.files_list = QListWidget()
118        self.files_list.setMaximumHeight(150)
119        files_layout.addWidget(self.files_list)
120
121        # Add the file list (QListWidget) directly to the grid
122        content_layout.addWidget(self.files_list, 0, 1)
123
124        # --- Grid Layout Stretch ---
125        # Columns: 1/3 for progress bar, 2/3 for scroll area
126        content_layout.setColumnStretch(0, 1)  # Left column (progress bar)
127        content_layout.setColumnStretch(1, 2)  # Right column (folder list)
128
129        # --- Bottom Row (Rows 1 & 2): Log Display ---
130        self.log_label = QLabel("Execution Log:")
131        self.log_label.setStyleSheet("""
132                    font-size: 16px;
133                    font-weight: bold;
134                    color: #2c3e50;
135                    margin-top: 15px;
136                    margin-bottom: 5px;
137                """)
138        # Add log label, spanning both columns (Row 1, Col 0, 1 row span, 2 col span)
139        content_layout.addWidget(self.log_label, 1, 0, 1, 2)
140
141        self.log_text = QTextEdit()
142        self.log_text.setStyleSheet("""
143                    QTextEdit {
144                        background-color: #2c3e50;
145                        color: #ecf0f1;
146                        font-family: 'Courier New', monospace;
147                        font-size: 11px;
148                        border: 2px solid #34495e;
149                        border-radius: 8px;
150                        padding: 8px;
151                    }
152                """)
153        self.log_text.setMaximumHeight(200)
154        self.log_text.setReadOnly(True)
155        # Add log text area, spanning both columns (Row 2, Col 0, 1 row span, 2 col span)
156        content_layout.addWidget(self.log_text, 2, 0, 1, 2)
157
158        # === Control Buttons ===
159
160        # --- Button Layout (Start/Stop) ---
161        button_frame = QFrame()  # Frame to hold the centered buttons
162        button_layout = QHBoxLayout(button_frame)
163        button_layout.setContentsMargins(0, 15, 0, 0)
164
165        self.start_button = QPushButton("Start deep learning")
166        button_layout.addStretch()  # Center the buttons
167        self.start_button.clicked.connect(self.start_processing)
168        self.start_button.setStyleSheet("""
169            QPushButton {
170                font-size: 14px;
171                font-weight: bold;
172                background-color: #27ae60;
173                color: white;
174                border-radius: 8px;
175                padding: 10px 20px;
176                min-width: 140px;
177            }
178            QPushButton:hover { 
179                background-color: #229954; 
180            }
181            QPushButton:disabled {
182                background-color: #bdc3c7;
183                color: #7f8c8d;
184            }
185        """)
186        button_layout.addWidget(self.start_button)
187
188        self.cancel_button = QPushButton("Stop processing")
189        self.cancel_button.setStyleSheet("""
190            QPushButton {
191                font-size: 14px;
192                font-weight: bold;
193                background-color: #e74c3c;
194                color: white;
195                border-radius: 8px;
196                padding: 10px 20px;
197                min-width: 140px;
198            }
199            QPushButton:hover { 
200                background-color: #c0392b; 
201            }
202            QPushButton:disabled {
203                background-color: #bdc3c7;
204                color: #7f8c8d;
205            }
206        """)
207        self.cancel_button.clicked.connect(self.cancel_processing)
208        self.cancel_button.setVisible(False)  # Initially hidden
209
210        button_layout.addWidget(self.cancel_button)
211        button_layout.addStretch()  # Center the buttons
212
213        main_layout.addWidget(button_frame)
214
215    def on_enter(self):
216        """Called when this page becomes active."""
217        self.reset_processing_state()  # Ensure UI is in a clean, default state
218
219        # Populate the file list if available from context
220        if self.context and "selected_segmentation_files" in self.context:
221            self.files_list.clear()
222            for file_path in self.context["selected_segmentation_files"]:
223                filename = os.path.basename(file_path)
224                # Add item with "Waiting..." status
225                self.files_list.addItem(
226                    QCoreApplication.translate("DlExecutionPage", "📄 {filename} - Waiting...").format(
227                        filename=filename))
228
229    def start_processing(self):
230        """Starts the deep learning processing workflow."""
231        if not self.context or "selected_segmentation_files" not in self.context:
232            QMessageBox.warning(self, QCoreApplication.translate("DlExecutionPage", "Error"),
233                                QCoreApplication.translate("DlExecutionPage", "No files selected for processing."))
234            return
235
236        selected_files = self.context["selected_segmentation_files"]
237        if not selected_files:
238            QMessageBox.warning(self, QCoreApplication.translate("DlExecutionPage", "Error"),
239                                QCoreApplication.translate("DlExecutionPage", "No files selected for processing."))
240            return
241
242        # --- Setup Worker Thread ---
243        self.worker = DlWorker(
244            input_files=selected_files,
245            workspace_path=self.context["workspace_path"],
246            has_freesurfer=self.has_freesurfer
247        )
248
249        # --- Connect Worker Signals ---
250        self.worker.progressbar_update.connect(self.update_progress)
251        self.worker.file_update.connect(self.update_file_status)
252        self.worker.log_update.connect(self.add_log_message)
253        self.worker.finished.connect(self.processing_finished)
254        # self.worker.finished.connect(self.worker.deleteLater) # Option to auto-delete worker
255
256        # --- Update UI for Processing State ---
257        self.processing = True
258        self.start_button.setVisible(False)
259        self.cancel_button.setVisible(True)
260        self.progress_bar.setVisible(True)
261        self.progress_bar.setValue(0)
262        self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Processing..."))
263        self.log_text.clear()
264
265        # --- Start Worker ---
266        self.worker.start()  # Start the QThread
267
268        self.add_log_message(
269            QCoreApplication.translate("DlExecutionPage", "Deep learning processing started for {0} file").format(
270                len(selected_files)), 'i')
271        log.info(f"Deep learning processing started for {len(selected_files)} file")
272
273    def update_progress(self, value):
274        """Updates the circular progress bar value."""
275        self.progress_bar.setValue(value)
276
277    def add_log_message(self, message, type):
278        """Appends a message to the UI log text area."""
279        timestamp = QtCore.QDateTime.currentDateTime().toString("hh:mm:ss")
280        self.log_text.append(f"[{timestamp}] {message}")
281
282        # Log to external file based on message type
283        if type == 'e':
284            log.error(f"[{timestamp}] {message}")
285        elif type == 'i':
286            log.info(f"[{timestamp}] {message}")
287        elif type == 'w':
288            log.warning(f"[{timestamp}] {message}")
289        else:
290            log.debug(f"[{timestamp}] {message}")
291
292        # Auto-scroll to the bottom
293        scrollbar = self.log_text.verticalScrollBar()
294        scrollbar.setValue(scrollbar.maximum())
295
296    def update_file_status(self, filename, status):
297        """Updates the status of a specific file in the QListWidget."""
298        for i in range(self.files_list.count()):
299            item = self.files_list.item(i)
300            if filename in item.text():  # Find the matching item
301                item.setText(f"📄 {filename} - {status}")
302                break  # Stop searching once found
303
304    def cancel_processing(self):
305        """Requests the worker thread to cancel processing."""
306        if self.worker:
307            self.worker.cancel_requested.emit()  # Emit the signal to request cancellation
308            # self.worker.cancel() # If you have a hard cancel method
309            self.add_log_message(QCoreApplication.translate("DlExecutionPage", "Cancellation requested..."), 'i')
310
311    def processing_finished(self, success, message):
312        """Slot called when the worker thread finishes."""
313        self.processing = False
314        self.processing_completed = True
315
316        # --- Update UI to Reflect Finished State ---
317        self.start_button.setVisible(True)
318        self.cancel_button.setVisible(False)
319        self.progress_bar.setVisible(False)
320
321        if success:
322            self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "✓ Processing completed!"))
323            self.current_operation.setStyleSheet("color: green; font-weight: bold;")
324            self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Reprocess"))
325        else:
326            self.current_operation.setText("✗ Processing failed!")
327            self.current_operation.setStyleSheet("color: #c0392b; font-weight: bold;")
328
329        self.add_log_message(QCoreApplication.translate("DlExecutionPage", "Final: {message}").format(message=message),
330                             'i')
331
332        # --- Update Context with Results ---
333        if success and "workspace_path" in self.context:
334            # If successful, store the output directory in the context
335            output_dir = os.path.join(self.context["workspace_path"], "outputs")
336            self.context["processing_output_dir"] = output_dir
337
338        # Notify main window to update its buttons (e.g., 'Next')
339        if "update_main_buttons" in self.context:
340            self.context["update_main_buttons"]()
341
342        # --- Show Final Message Box ---
343        if success:
344            QMessageBox.information(self, QCoreApplication.translate("DlExecutionPage", "Completed"), message)
345        else:
346            QMessageBox.critical(self, QCoreApplication.translate("DlExecutionPage", "Error"), message)
347
348    def reset_processing_state(self):
349        """Resets the page to its initial state."""
350        self.processing = False
351        self.processing_completed = False
352        self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Start deep learning"))
353        self.start_button.setVisible(True)
354        self.cancel_button.setVisible(False)
355        self.progress_bar.setVisible(False)
356        self.current_operation.setStyleSheet("")  # Reset color
357        self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Ready to start"))
358
359        if self.worker:
360            self.worker = None  # Clear worker reference
361
362    def back(self):
363        """Handles the 'Back' navigation request."""
364        if self.processing:
365            # Ask user for confirmation to stop
366            reply = QMessageBox.question(
367                self,
368                QCoreApplication.translate("DlExecutionPage", "Processing in progress"),
369                QCoreApplication.translate("DlExecutionPage",
370                                           "Processing is in progress. Do you really want to go back?\nProcessing will be interrupted."),
371                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
372                QMessageBox.StandardButton.No
373            )
374            if reply == QMessageBox.StandardButton.No:
375                return None  # Abort navigation
376
377            # --- Stop Processing ---
378            if self.worker:
379                self.worker.cancel_requested.emit()
380                # self.worker.cancel()
381
382        if self.previous_page:
383            self.previous_page.on_enter()  # Call on_enter of the previous page
384            return self.previous_page
385        return None  # No previous page
386
387    def next(self, context):
388        """Handles the 'Next' navigation request."""
389        return None  # This page is a terminal step
390
391    def is_ready_to_advance(self):
392        """Checks if the page is in a state to advance."""
393        return False  # Cannot advance from this page
394
395    def is_ready_to_go_back(self):
396        """Checks if the page is in a state to go back."""
397        return True  # Can always go back (with a warning if processing)
398
399    def _translate_ui(self):
400        """Updates all user-visible strings for translation."""
401        self.header.setText(QCoreApplication.translate("DlExecutionPage", "Deep Learning Segmentation"))
402
403        # Only reset 'Ready to start' if not processing or completed
404        if not self.processing and not self.processing_completed:
405            self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Ready to start"))
406
407        self.files_group.setTitle(QCoreApplication.translate("DlExecutionPage", "Files to process"))
408        self.log_label.setText(QCoreApplication.translate("DlExecutionPage", "Execution Log:"))
409
410        if not self.processing_completed:
411            self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Start deep learning"))
412        else:
413            self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Reprocess"))
414
415        self.cancel_button.setText(QCoreApplication.translate("DlExecutionPage", "Stop processing"))
416
417        # Repopulate the file list with translated "Waiting..." string
418        if self.context and "selected_segmentation_files" in self.context and not self.processing:
419            self.files_list.clear()
420            for file_path in self.context["selected_segmentation_files"]:
421                filename = os.path.basename(file_path)
422                self.files_list.addItem(
423                    QCoreApplication.translate("DlExecutionPage", "📄 {filename} - Waiting...").format(
424                        filename=filename))

Page for SynthStrip + Coregistration of selected NIfTI files.

DlExecutionPage(context=None, previous_page=None)
26    def __init__(self, context=None, previous_page=None):
27        super().__init__()
28        self.worker = None  # Worker thread instance
29        self.current_file = None  # Currently processing file (if needed)
30        self.context = context  # Shared application context (settings, file lists)
31        self.previous_page = previous_page  # Reference to the previous page for navigation
32        self.next_page = None  # Reference to the next page
33
34        # State flags
35        self.processing = False  # Flag to indicate if processing is active
36        self.processing_completed = False  # Flag to indicate if processing has finished
37
38        self._setup_ui()  # Initialize the user interface elements
39
40        self._translate_ui()  # Apply initial translations
41        if context and "language_changed" in context:
42            # Connect to the global language change signal if it exists
43            context["language_changed"].connect(self._translate_ui)
worker
current_file
context
previous_page
next_page
processing
processing_completed
def on_enter(self):
215    def on_enter(self):
216        """Called when this page becomes active."""
217        self.reset_processing_state()  # Ensure UI is in a clean, default state
218
219        # Populate the file list if available from context
220        if self.context and "selected_segmentation_files" in self.context:
221            self.files_list.clear()
222            for file_path in self.context["selected_segmentation_files"]:
223                filename = os.path.basename(file_path)
224                # Add item with "Waiting..." status
225                self.files_list.addItem(
226                    QCoreApplication.translate("DlExecutionPage", "📄 {filename} - Waiting...").format(
227                        filename=filename))

Called when this page becomes active.

def start_processing(self):
229    def start_processing(self):
230        """Starts the deep learning processing workflow."""
231        if not self.context or "selected_segmentation_files" not in self.context:
232            QMessageBox.warning(self, QCoreApplication.translate("DlExecutionPage", "Error"),
233                                QCoreApplication.translate("DlExecutionPage", "No files selected for processing."))
234            return
235
236        selected_files = self.context["selected_segmentation_files"]
237        if not selected_files:
238            QMessageBox.warning(self, QCoreApplication.translate("DlExecutionPage", "Error"),
239                                QCoreApplication.translate("DlExecutionPage", "No files selected for processing."))
240            return
241
242        # --- Setup Worker Thread ---
243        self.worker = DlWorker(
244            input_files=selected_files,
245            workspace_path=self.context["workspace_path"],
246            has_freesurfer=self.has_freesurfer
247        )
248
249        # --- Connect Worker Signals ---
250        self.worker.progressbar_update.connect(self.update_progress)
251        self.worker.file_update.connect(self.update_file_status)
252        self.worker.log_update.connect(self.add_log_message)
253        self.worker.finished.connect(self.processing_finished)
254        # self.worker.finished.connect(self.worker.deleteLater) # Option to auto-delete worker
255
256        # --- Update UI for Processing State ---
257        self.processing = True
258        self.start_button.setVisible(False)
259        self.cancel_button.setVisible(True)
260        self.progress_bar.setVisible(True)
261        self.progress_bar.setValue(0)
262        self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Processing..."))
263        self.log_text.clear()
264
265        # --- Start Worker ---
266        self.worker.start()  # Start the QThread
267
268        self.add_log_message(
269            QCoreApplication.translate("DlExecutionPage", "Deep learning processing started for {0} file").format(
270                len(selected_files)), 'i')
271        log.info(f"Deep learning processing started for {len(selected_files)} file")

Starts the deep learning processing workflow.

def update_progress(self, value):
273    def update_progress(self, value):
274        """Updates the circular progress bar value."""
275        self.progress_bar.setValue(value)

Updates the circular progress bar value.

def add_log_message(self, message, type):
277    def add_log_message(self, message, type):
278        """Appends a message to the UI log text area."""
279        timestamp = QtCore.QDateTime.currentDateTime().toString("hh:mm:ss")
280        self.log_text.append(f"[{timestamp}] {message}")
281
282        # Log to external file based on message type
283        if type == 'e':
284            log.error(f"[{timestamp}] {message}")
285        elif type == 'i':
286            log.info(f"[{timestamp}] {message}")
287        elif type == 'w':
288            log.warning(f"[{timestamp}] {message}")
289        else:
290            log.debug(f"[{timestamp}] {message}")
291
292        # Auto-scroll to the bottom
293        scrollbar = self.log_text.verticalScrollBar()
294        scrollbar.setValue(scrollbar.maximum())

Appends a message to the UI log text area.

def update_file_status(self, filename, status):
296    def update_file_status(self, filename, status):
297        """Updates the status of a specific file in the QListWidget."""
298        for i in range(self.files_list.count()):
299            item = self.files_list.item(i)
300            if filename in item.text():  # Find the matching item
301                item.setText(f"📄 {filename} - {status}")
302                break  # Stop searching once found

Updates the status of a specific file in the QListWidget.

def cancel_processing(self):
304    def cancel_processing(self):
305        """Requests the worker thread to cancel processing."""
306        if self.worker:
307            self.worker.cancel_requested.emit()  # Emit the signal to request cancellation
308            # self.worker.cancel() # If you have a hard cancel method
309            self.add_log_message(QCoreApplication.translate("DlExecutionPage", "Cancellation requested..."), 'i')

Requests the worker thread to cancel processing.

def processing_finished(self, success, message):
311    def processing_finished(self, success, message):
312        """Slot called when the worker thread finishes."""
313        self.processing = False
314        self.processing_completed = True
315
316        # --- Update UI to Reflect Finished State ---
317        self.start_button.setVisible(True)
318        self.cancel_button.setVisible(False)
319        self.progress_bar.setVisible(False)
320
321        if success:
322            self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "✓ Processing completed!"))
323            self.current_operation.setStyleSheet("color: green; font-weight: bold;")
324            self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Reprocess"))
325        else:
326            self.current_operation.setText("✗ Processing failed!")
327            self.current_operation.setStyleSheet("color: #c0392b; font-weight: bold;")
328
329        self.add_log_message(QCoreApplication.translate("DlExecutionPage", "Final: {message}").format(message=message),
330                             'i')
331
332        # --- Update Context with Results ---
333        if success and "workspace_path" in self.context:
334            # If successful, store the output directory in the context
335            output_dir = os.path.join(self.context["workspace_path"], "outputs")
336            self.context["processing_output_dir"] = output_dir
337
338        # Notify main window to update its buttons (e.g., 'Next')
339        if "update_main_buttons" in self.context:
340            self.context["update_main_buttons"]()
341
342        # --- Show Final Message Box ---
343        if success:
344            QMessageBox.information(self, QCoreApplication.translate("DlExecutionPage", "Completed"), message)
345        else:
346            QMessageBox.critical(self, QCoreApplication.translate("DlExecutionPage", "Error"), message)

Slot called when the worker thread finishes.

def reset_processing_state(self):
348    def reset_processing_state(self):
349        """Resets the page to its initial state."""
350        self.processing = False
351        self.processing_completed = False
352        self.start_button.setText(QCoreApplication.translate("DlExecutionPage", "Start deep learning"))
353        self.start_button.setVisible(True)
354        self.cancel_button.setVisible(False)
355        self.progress_bar.setVisible(False)
356        self.current_operation.setStyleSheet("")  # Reset color
357        self.current_operation.setText(QCoreApplication.translate("DlExecutionPage", "Ready to start"))
358
359        if self.worker:
360            self.worker = None  # Clear worker reference

Resets the page to its initial state.

def back(self):
362    def back(self):
363        """Handles the 'Back' navigation request."""
364        if self.processing:
365            # Ask user for confirmation to stop
366            reply = QMessageBox.question(
367                self,
368                QCoreApplication.translate("DlExecutionPage", "Processing in progress"),
369                QCoreApplication.translate("DlExecutionPage",
370                                           "Processing is in progress. Do you really want to go back?\nProcessing will be interrupted."),
371                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
372                QMessageBox.StandardButton.No
373            )
374            if reply == QMessageBox.StandardButton.No:
375                return None  # Abort navigation
376
377            # --- Stop Processing ---
378            if self.worker:
379                self.worker.cancel_requested.emit()
380                # self.worker.cancel()
381
382        if self.previous_page:
383            self.previous_page.on_enter()  # Call on_enter of the previous page
384            return self.previous_page
385        return None  # No previous page

Handles the 'Back' navigation request.

def next(self, context):
387    def next(self, context):
388        """Handles the 'Next' navigation request."""
389        return None  # This page is a terminal step

Handles the 'Next' navigation request.

def is_ready_to_advance(self):
391    def is_ready_to_advance(self):
392        """Checks if the page is in a state to advance."""
393        return False  # Cannot advance from this page

Checks if the page is in a state to advance.

def is_ready_to_go_back(self):
395    def is_ready_to_go_back(self):
396        """Checks if the page is in a state to go back."""
397        return True  # Can always go back (with a warning if processing)

Checks if the page is in a state to go back.