ui.pipeline_review_page
1import json 2import os 3import glob 4 5from PyQt6.QtGui import QFont 6from PyQt6.QtWidgets import QWidget, QScrollArea, QVBoxLayout, QLabel 7from PyQt6.QtCore import Qt, QCoreApplication 8 9from components.collapsible_info_frame import CollapsibleInfoFrame 10from components.collapsible_patient_frame import CollapsiblePatientFrame 11from ui.pipeline_execution_page import PipelineExecutionPage 12from page import Page 13from logger import get_logger 14 15log = get_logger() 16 17 18class PipelineReviewPage(Page): 19 """ 20 GUI page that allows users to review and adjust the configuration 21 before running the medical imaging pipeline. 22 23 This page: 24 - Loads the latest configuration JSON file. 25 - Displays collapsible panels for each patient and data type. 26 - Allows editing, saving, and validating configurations. 27 - Transitions to the execution page once all data is ready. 28 """ 29 30 def __init__(self, context=None, previous_page=None): 31 """ 32 Initialize the review page and build its UI. 33 34 Args: 35 context (dict): Shared application context (workspace path, logger, signals, etc.). 36 previous_page (Page): The previous page in the workflow. 37 """ 38 super().__init__() 39 self.context = context 40 self.workspace_path = context["workspace_path"] 41 self.previous_page = previous_page 42 self.next_page = None 43 44 # Locate and load the latest configuration file 45 self.config_path = self._find_latest_config() 46 self.pipeline_config = self._load_config() 47 48 # Main layout container 49 self.main_layout = QVBoxLayout(self) 50 51 # Build interface 52 self._setup_ui() 53 54 # Apply translation and connect to language change signal (if available) 55 self._translate_ui() 56 if context and "language_changed" in context: 57 context["language_changed"].connect(self._translate_ui) 58 59 # ───────────────────────────────────────────── 60 # CONFIGURATION MANAGEMENT 61 # ───────────────────────────────────────────── 62 def _find_latest_config(self): 63 """ 64 Find the most recent pipeline configuration file in the workspace. 65 66 Returns: 67 str: Absolute path to the newest configuration JSON file. 68 """ 69 pipeline_dir = os.path.join(self.workspace_path, "pipeline") 70 71 # Return a default path if the directory does not exist 72 if not os.path.exists(pipeline_dir): 73 return os.path.join(pipeline_dir, "pipeline_config.json") 74 75 # Locate all files matching the *_config.json pattern 76 config_files = glob.glob(os.path.join(pipeline_dir, "*_config.json")) 77 78 if not config_files: 79 return os.path.join(pipeline_dir, "pipeline_config.json") 80 81 # Identify the latest config file by its numeric ID prefix 82 max_id = 0 83 latest_config = None 84 for config_file in config_files: 85 filename = os.path.basename(config_file) 86 try: 87 config_id = int(filename.split('_')[0]) # Extract numeric prefix 88 if config_id > max_id: 89 max_id = config_id 90 latest_config = config_file 91 except (ValueError, IndexError): 92 continue # Skip non-conforming filenames 93 94 if latest_config: 95 return latest_config 96 97 return os.path.join(pipeline_dir, "pipeline_config.json") 98 99 def _load_config_from_path(self, config_path): 100 """ 101 Load configuration data from a specific JSON file. 102 103 Args: 104 config_path (str): Path to the configuration file. 105 106 Returns: 107 dict: Parsed configuration data, or an empty dict if load fails. 108 """ 109 if not os.path.exists(config_path): 110 log.warning(f"Config file not found: {config_path}") 111 return {} 112 113 try: 114 with open(config_path, "r", encoding="utf-8") as f: 115 return json.load(f) 116 except (json.JSONDecodeError, IOError) as e: 117 log.error(f"Error loading config {config_path}: {e}") 118 return {} 119 120 def _load_config(self): 121 """Load the current configuration using the latest detected file.""" 122 return self._load_config_from_path(self.config_path) 123 124 # ───────────────────────────────────────────── 125 # UI SETUP AND LAYOUT 126 # ───────────────────────────────────────────── 127 def _setup_ui(self): 128 """Construct the entire user interface for the review page.""" 129 layout = self.main_layout 130 131 # Clear any existing widgets 132 while layout.count(): 133 item = layout.takeAt(0) 134 widget = item.widget() 135 if widget: 136 widget.deleteLater() 137 138 # ── Header ────────────────────────────── 139 self.header = QLabel("Pipeline Configuration Review") 140 self.header.setAlignment(Qt.AlignmentFlag.AlignCenter) 141 self.header.setFont(QFont("Arial", 18, QFont.Weight.Bold)) 142 layout.addWidget(self.header) 143 144 # ── Config file info ───────────────────── 145 self.config_info = QLabel(f"Reviewing: {os.path.basename(self.config_path)}") 146 self.config_info.setAlignment(Qt.AlignmentFlag.AlignCenter) 147 self.config_info.setWordWrap(True) 148 self.config_info.setStyleSheet(""" 149 font-size: 11px; 150 color: #666; 151 font-style: italic; 152 margin-bottom: 6px; 153 """) 154 layout.addWidget(self.config_info) 155 156 # ── Collapsible instructions ───────────── 157 info_frame = CollapsibleInfoFrame(self.context) 158 layout.addWidget(info_frame) 159 160 # ── Info label ────────────────────────── 161 self.info_label = QLabel( 162 "<strong>Click</strong> a frame to review file selections. " 163 "<strong>Save</strong> yellow frames after selection." 164 ) 165 self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 166 self.info_label.setWordWrap(True) 167 self.info_label.setStyleSheet(""" 168 color: #666; 169 font-size: 12px; 170 margin: 6px 0; 171 """) 172 layout.addWidget(self.info_label) 173 174 # ── Scroll area for patient panels ─────── 175 scroll = QScrollArea() 176 scroll.setWidgetResizable(True) 177 scroll.setStyleSheet("QScrollArea { border: none; }") 178 layout.addWidget(scroll) 179 180 content = QWidget() 181 content_layout = QVBoxLayout(content) 182 content_layout.setContentsMargins(6, 6, 6, 6) 183 content_layout.setSpacing(8) 184 scroll.setWidget(content) 185 186 # ── Patient frames ─────────────────────── 187 self.patient_widgets = {} 188 189 # Define expected data categories and file patterns 190 categories = { 191 "mri": [os.path.join(self.workspace_path, "{pid}", "anat", "*_flair.nii*")], 192 "mri_str": [os.path.join(self.workspace_path, "derivatives", "skullstrips", "{pid}", "anat", "*_brain.nii*")], 193 "pet": [os.path.join(self.workspace_path, "{pid}", "ses-01", "pet", "*_pet.nii*")], 194 "pet4d": [os.path.join(self.workspace_path, "{pid}", "ses-02", "pet", "*_pet.nii*")], 195 "tumor_mri": [ 196 os.path.join(self.workspace_path, "derivatives", "manual_masks", "{pid}", "anat", "*_mask.nii*"), 197 os.path.join(self.workspace_path, "derivatives", "deep_learning_seg", "{pid}", "anat", "*_seg.nii*"), 198 ], 199 } 200 201 # Create collapsible frames for each patient 202 for patient_id, files in self.pipeline_config.items(): 203 patient_patterns = { 204 cat: [pat.format(pid=patient_id) for pat in pats] 205 for cat, pats in categories.items() 206 } 207 multiple_choice = bool(files.get("need_revision", False)) 208 209 frame = CollapsiblePatientFrame( 210 self.context, 211 patient_id, 212 files, 213 patient_patterns, 214 multiple_choice, 215 save_callback=self._save_single_patient, 216 ) 217 218 self.patient_widgets[patient_id] = frame 219 content_layout.addWidget(frame) 220 221 content_layout.addStretch() 222 223 # ───────────────────────────────────────────── 224 # SAVE & UPDATE LOGIC 225 # ───────────────────────────────────────────── 226 def _save_single_patient(self, patient_id, files): 227 """ 228 Save the configuration for a single patient. 229 230 Args: 231 patient_id (str): Patient identifier. 232 files (dict): Updated file paths and metadata. 233 """ 234 self.pipeline_config[patient_id] = files 235 with open(self.config_path, "w", encoding="utf-8") as f: 236 json.dump(self.pipeline_config, f, indent=4) 237 238 def on_enter(self): 239 """ 240 Hook called when the page becomes visible. 241 242 Checks for updated configuration files and refreshes the UI 243 if any changes are detected. 244 """ 245 new_config_path = self._find_latest_config() 246 new_pipeline_config = self._load_config_from_path(new_config_path) 247 248 config_changed = ( 249 new_config_path != self.config_path or 250 new_pipeline_config != self.pipeline_config 251 ) 252 253 if config_changed: 254 log.info(f"Config changed, reloading UI: {os.path.basename(new_config_path)}") 255 self.config_path = new_config_path 256 self.pipeline_config = new_pipeline_config 257 self._setup_ui() 258 else: 259 log.debug("Config unchanged, keeping existing UI") 260 261 # ───────────────────────────────────────────── 262 # NAVIGATION HANDLERS 263 # ───────────────────────────────────────────── 264 def next(self, context): 265 """ 266 Proceed to the next page, executing the pipeline if all configurations are valid. 267 268 Args: 269 context (dict): Shared context. 270 271 Returns: 272 Page: The next page instance, or self if configuration is incomplete. 273 """ 274 # Check for patients requiring revision 275 unsaved_patients = [ 276 pid for pid, files in self.pipeline_config.items() 277 if files.get("need_revision", False) 278 ] 279 280 if unsaved_patients: 281 # Warn the user about unsaved patients 282 from PyQt6.QtWidgets import QMessageBox 283 msg = QMessageBox() 284 msg.setIcon(QMessageBox.Icon.Warning) 285 msg.setWindowTitle(QCoreApplication.translate("PipelineReviewPage", "Configuration Incomplete")) 286 msg.setText(QCoreApplication.translate("PipelineReviewPage", "Some patients still require configuration review.")) 287 msg.setInformativeText( 288 QCoreApplication.translate( 289 "PipelineReviewPage", 290 "Please review and save configuration for: {patients}" 291 ).format(patients=", ".join(unsaved_patients)) 292 ) 293 msg.exec() 294 return self # Stay on current page 295 296 # Proceed to pipeline execution page 297 if not self.next_page: 298 self.next_page = PipelineExecutionPage(context, self) 299 self.context["history"].append(self.next_page) 300 self.next_page.on_enter() 301 return self.next_page 302 303 def back(self): 304 """ 305 Navigate back to the previous page. 306 307 If the output folder for the current config does not exist, 308 the config file is deleted to keep the workspace clean. 309 """ 310 if os.path.exists(self.config_path): 311 try: 312 config_filename = os.path.basename(self.config_path) 313 config_id = config_filename.split('_config.json')[0] 314 pipeline_dir = os.path.dirname(self.config_path) 315 output_folder_path = os.path.join(pipeline_dir, f"{config_id}_output") 316 317 if not os.path.exists(output_folder_path): 318 os.remove(self.config_path) 319 log.info(f"Removed unused config file: {self.config_path}") 320 else: 321 log.info(f"Keeping config file: {self.config_path}") 322 except (OSError, IndexError, ValueError) as e: 323 log.error(f"Error processing config file {self.config_path}: {e}") 324 else: 325 log.error(f"Invalid config path: {self.config_path}") 326 327 if self.previous_page: 328 self.previous_page.on_enter() 329 return self.previous_page 330 return None 331 332 # ───────────────────────────────────────────── 333 # TRANSLATION 334 # ───────────────────────────────────────────── 335 def _translate_ui(self): 336 """Translate all static text for internationalization.""" 337 self.header.setText(QCoreApplication.translate("PipelineReviewPage", "Pipeline Configuration Review")) 338 self.config_info.setText( 339 QCoreApplication.translate("PipelineReviewPage", "Reviewing: {0}").format(os.path.basename(self.config_path)) 340 ) 341 self.info_label.setText( 342 QCoreApplication.translate( 343 "PipelineReviewPage", 344 "<strong>Click</strong> a frame to review file selections. <strong>Save</strong> yellow frames after selection." 345 ) 346 )
19class PipelineReviewPage(Page): 20 """ 21 GUI page that allows users to review and adjust the configuration 22 before running the medical imaging pipeline. 23 24 This page: 25 - Loads the latest configuration JSON file. 26 - Displays collapsible panels for each patient and data type. 27 - Allows editing, saving, and validating configurations. 28 - Transitions to the execution page once all data is ready. 29 """ 30 31 def __init__(self, context=None, previous_page=None): 32 """ 33 Initialize the review page and build its UI. 34 35 Args: 36 context (dict): Shared application context (workspace path, logger, signals, etc.). 37 previous_page (Page): The previous page in the workflow. 38 """ 39 super().__init__() 40 self.context = context 41 self.workspace_path = context["workspace_path"] 42 self.previous_page = previous_page 43 self.next_page = None 44 45 # Locate and load the latest configuration file 46 self.config_path = self._find_latest_config() 47 self.pipeline_config = self._load_config() 48 49 # Main layout container 50 self.main_layout = QVBoxLayout(self) 51 52 # Build interface 53 self._setup_ui() 54 55 # Apply translation and connect to language change signal (if available) 56 self._translate_ui() 57 if context and "language_changed" in context: 58 context["language_changed"].connect(self._translate_ui) 59 60 # ───────────────────────────────────────────── 61 # CONFIGURATION MANAGEMENT 62 # ───────────────────────────────────────────── 63 def _find_latest_config(self): 64 """ 65 Find the most recent pipeline configuration file in the workspace. 66 67 Returns: 68 str: Absolute path to the newest configuration JSON file. 69 """ 70 pipeline_dir = os.path.join(self.workspace_path, "pipeline") 71 72 # Return a default path if the directory does not exist 73 if not os.path.exists(pipeline_dir): 74 return os.path.join(pipeline_dir, "pipeline_config.json") 75 76 # Locate all files matching the *_config.json pattern 77 config_files = glob.glob(os.path.join(pipeline_dir, "*_config.json")) 78 79 if not config_files: 80 return os.path.join(pipeline_dir, "pipeline_config.json") 81 82 # Identify the latest config file by its numeric ID prefix 83 max_id = 0 84 latest_config = None 85 for config_file in config_files: 86 filename = os.path.basename(config_file) 87 try: 88 config_id = int(filename.split('_')[0]) # Extract numeric prefix 89 if config_id > max_id: 90 max_id = config_id 91 latest_config = config_file 92 except (ValueError, IndexError): 93 continue # Skip non-conforming filenames 94 95 if latest_config: 96 return latest_config 97 98 return os.path.join(pipeline_dir, "pipeline_config.json") 99 100 def _load_config_from_path(self, config_path): 101 """ 102 Load configuration data from a specific JSON file. 103 104 Args: 105 config_path (str): Path to the configuration file. 106 107 Returns: 108 dict: Parsed configuration data, or an empty dict if load fails. 109 """ 110 if not os.path.exists(config_path): 111 log.warning(f"Config file not found: {config_path}") 112 return {} 113 114 try: 115 with open(config_path, "r", encoding="utf-8") as f: 116 return json.load(f) 117 except (json.JSONDecodeError, IOError) as e: 118 log.error(f"Error loading config {config_path}: {e}") 119 return {} 120 121 def _load_config(self): 122 """Load the current configuration using the latest detected file.""" 123 return self._load_config_from_path(self.config_path) 124 125 # ───────────────────────────────────────────── 126 # UI SETUP AND LAYOUT 127 # ───────────────────────────────────────────── 128 def _setup_ui(self): 129 """Construct the entire user interface for the review page.""" 130 layout = self.main_layout 131 132 # Clear any existing widgets 133 while layout.count(): 134 item = layout.takeAt(0) 135 widget = item.widget() 136 if widget: 137 widget.deleteLater() 138 139 # ── Header ────────────────────────────── 140 self.header = QLabel("Pipeline Configuration Review") 141 self.header.setAlignment(Qt.AlignmentFlag.AlignCenter) 142 self.header.setFont(QFont("Arial", 18, QFont.Weight.Bold)) 143 layout.addWidget(self.header) 144 145 # ── Config file info ───────────────────── 146 self.config_info = QLabel(f"Reviewing: {os.path.basename(self.config_path)}") 147 self.config_info.setAlignment(Qt.AlignmentFlag.AlignCenter) 148 self.config_info.setWordWrap(True) 149 self.config_info.setStyleSheet(""" 150 font-size: 11px; 151 color: #666; 152 font-style: italic; 153 margin-bottom: 6px; 154 """) 155 layout.addWidget(self.config_info) 156 157 # ── Collapsible instructions ───────────── 158 info_frame = CollapsibleInfoFrame(self.context) 159 layout.addWidget(info_frame) 160 161 # ── Info label ────────────────────────── 162 self.info_label = QLabel( 163 "<strong>Click</strong> a frame to review file selections. " 164 "<strong>Save</strong> yellow frames after selection." 165 ) 166 self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 167 self.info_label.setWordWrap(True) 168 self.info_label.setStyleSheet(""" 169 color: #666; 170 font-size: 12px; 171 margin: 6px 0; 172 """) 173 layout.addWidget(self.info_label) 174 175 # ── Scroll area for patient panels ─────── 176 scroll = QScrollArea() 177 scroll.setWidgetResizable(True) 178 scroll.setStyleSheet("QScrollArea { border: none; }") 179 layout.addWidget(scroll) 180 181 content = QWidget() 182 content_layout = QVBoxLayout(content) 183 content_layout.setContentsMargins(6, 6, 6, 6) 184 content_layout.setSpacing(8) 185 scroll.setWidget(content) 186 187 # ── Patient frames ─────────────────────── 188 self.patient_widgets = {} 189 190 # Define expected data categories and file patterns 191 categories = { 192 "mri": [os.path.join(self.workspace_path, "{pid}", "anat", "*_flair.nii*")], 193 "mri_str": [os.path.join(self.workspace_path, "derivatives", "skullstrips", "{pid}", "anat", "*_brain.nii*")], 194 "pet": [os.path.join(self.workspace_path, "{pid}", "ses-01", "pet", "*_pet.nii*")], 195 "pet4d": [os.path.join(self.workspace_path, "{pid}", "ses-02", "pet", "*_pet.nii*")], 196 "tumor_mri": [ 197 os.path.join(self.workspace_path, "derivatives", "manual_masks", "{pid}", "anat", "*_mask.nii*"), 198 os.path.join(self.workspace_path, "derivatives", "deep_learning_seg", "{pid}", "anat", "*_seg.nii*"), 199 ], 200 } 201 202 # Create collapsible frames for each patient 203 for patient_id, files in self.pipeline_config.items(): 204 patient_patterns = { 205 cat: [pat.format(pid=patient_id) for pat in pats] 206 for cat, pats in categories.items() 207 } 208 multiple_choice = bool(files.get("need_revision", False)) 209 210 frame = CollapsiblePatientFrame( 211 self.context, 212 patient_id, 213 files, 214 patient_patterns, 215 multiple_choice, 216 save_callback=self._save_single_patient, 217 ) 218 219 self.patient_widgets[patient_id] = frame 220 content_layout.addWidget(frame) 221 222 content_layout.addStretch() 223 224 # ───────────────────────────────────────────── 225 # SAVE & UPDATE LOGIC 226 # ───────────────────────────────────────────── 227 def _save_single_patient(self, patient_id, files): 228 """ 229 Save the configuration for a single patient. 230 231 Args: 232 patient_id (str): Patient identifier. 233 files (dict): Updated file paths and metadata. 234 """ 235 self.pipeline_config[patient_id] = files 236 with open(self.config_path, "w", encoding="utf-8") as f: 237 json.dump(self.pipeline_config, f, indent=4) 238 239 def on_enter(self): 240 """ 241 Hook called when the page becomes visible. 242 243 Checks for updated configuration files and refreshes the UI 244 if any changes are detected. 245 """ 246 new_config_path = self._find_latest_config() 247 new_pipeline_config = self._load_config_from_path(new_config_path) 248 249 config_changed = ( 250 new_config_path != self.config_path or 251 new_pipeline_config != self.pipeline_config 252 ) 253 254 if config_changed: 255 log.info(f"Config changed, reloading UI: {os.path.basename(new_config_path)}") 256 self.config_path = new_config_path 257 self.pipeline_config = new_pipeline_config 258 self._setup_ui() 259 else: 260 log.debug("Config unchanged, keeping existing UI") 261 262 # ───────────────────────────────────────────── 263 # NAVIGATION HANDLERS 264 # ───────────────────────────────────────────── 265 def next(self, context): 266 """ 267 Proceed to the next page, executing the pipeline if all configurations are valid. 268 269 Args: 270 context (dict): Shared context. 271 272 Returns: 273 Page: The next page instance, or self if configuration is incomplete. 274 """ 275 # Check for patients requiring revision 276 unsaved_patients = [ 277 pid for pid, files in self.pipeline_config.items() 278 if files.get("need_revision", False) 279 ] 280 281 if unsaved_patients: 282 # Warn the user about unsaved patients 283 from PyQt6.QtWidgets import QMessageBox 284 msg = QMessageBox() 285 msg.setIcon(QMessageBox.Icon.Warning) 286 msg.setWindowTitle(QCoreApplication.translate("PipelineReviewPage", "Configuration Incomplete")) 287 msg.setText(QCoreApplication.translate("PipelineReviewPage", "Some patients still require configuration review.")) 288 msg.setInformativeText( 289 QCoreApplication.translate( 290 "PipelineReviewPage", 291 "Please review and save configuration for: {patients}" 292 ).format(patients=", ".join(unsaved_patients)) 293 ) 294 msg.exec() 295 return self # Stay on current page 296 297 # Proceed to pipeline execution page 298 if not self.next_page: 299 self.next_page = PipelineExecutionPage(context, self) 300 self.context["history"].append(self.next_page) 301 self.next_page.on_enter() 302 return self.next_page 303 304 def back(self): 305 """ 306 Navigate back to the previous page. 307 308 If the output folder for the current config does not exist, 309 the config file is deleted to keep the workspace clean. 310 """ 311 if os.path.exists(self.config_path): 312 try: 313 config_filename = os.path.basename(self.config_path) 314 config_id = config_filename.split('_config.json')[0] 315 pipeline_dir = os.path.dirname(self.config_path) 316 output_folder_path = os.path.join(pipeline_dir, f"{config_id}_output") 317 318 if not os.path.exists(output_folder_path): 319 os.remove(self.config_path) 320 log.info(f"Removed unused config file: {self.config_path}") 321 else: 322 log.info(f"Keeping config file: {self.config_path}") 323 except (OSError, IndexError, ValueError) as e: 324 log.error(f"Error processing config file {self.config_path}: {e}") 325 else: 326 log.error(f"Invalid config path: {self.config_path}") 327 328 if self.previous_page: 329 self.previous_page.on_enter() 330 return self.previous_page 331 return None 332 333 # ───────────────────────────────────────────── 334 # TRANSLATION 335 # ───────────────────────────────────────────── 336 def _translate_ui(self): 337 """Translate all static text for internationalization.""" 338 self.header.setText(QCoreApplication.translate("PipelineReviewPage", "Pipeline Configuration Review")) 339 self.config_info.setText( 340 QCoreApplication.translate("PipelineReviewPage", "Reviewing: {0}").format(os.path.basename(self.config_path)) 341 ) 342 self.info_label.setText( 343 QCoreApplication.translate( 344 "PipelineReviewPage", 345 "<strong>Click</strong> a frame to review file selections. <strong>Save</strong> yellow frames after selection." 346 ) 347 )
GUI page that allows users to review and adjust the configuration before running the medical imaging pipeline.
This page:
- Loads the latest configuration JSON file.
- Displays collapsible panels for each patient and data type.
- Allows editing, saving, and validating configurations.
- Transitions to the execution page once all data is ready.
31 def __init__(self, context=None, previous_page=None): 32 """ 33 Initialize the review page and build its UI. 34 35 Args: 36 context (dict): Shared application context (workspace path, logger, signals, etc.). 37 previous_page (Page): The previous page in the workflow. 38 """ 39 super().__init__() 40 self.context = context 41 self.workspace_path = context["workspace_path"] 42 self.previous_page = previous_page 43 self.next_page = None 44 45 # Locate and load the latest configuration file 46 self.config_path = self._find_latest_config() 47 self.pipeline_config = self._load_config() 48 49 # Main layout container 50 self.main_layout = QVBoxLayout(self) 51 52 # Build interface 53 self._setup_ui() 54 55 # Apply translation and connect to language change signal (if available) 56 self._translate_ui() 57 if context and "language_changed" in context: 58 context["language_changed"].connect(self._translate_ui)
Initialize the review page and build its UI.
Args: context (dict): Shared application context (workspace path, logger, signals, etc.). previous_page (Page): The previous page in the workflow.
239 def on_enter(self): 240 """ 241 Hook called when the page becomes visible. 242 243 Checks for updated configuration files and refreshes the UI 244 if any changes are detected. 245 """ 246 new_config_path = self._find_latest_config() 247 new_pipeline_config = self._load_config_from_path(new_config_path) 248 249 config_changed = ( 250 new_config_path != self.config_path or 251 new_pipeline_config != self.pipeline_config 252 ) 253 254 if config_changed: 255 log.info(f"Config changed, reloading UI: {os.path.basename(new_config_path)}") 256 self.config_path = new_config_path 257 self.pipeline_config = new_pipeline_config 258 self._setup_ui() 259 else: 260 log.debug("Config unchanged, keeping existing UI")
Hook called when the page becomes visible.
Checks for updated configuration files and refreshes the UI if any changes are detected.
265 def next(self, context): 266 """ 267 Proceed to the next page, executing the pipeline if all configurations are valid. 268 269 Args: 270 context (dict): Shared context. 271 272 Returns: 273 Page: The next page instance, or self if configuration is incomplete. 274 """ 275 # Check for patients requiring revision 276 unsaved_patients = [ 277 pid for pid, files in self.pipeline_config.items() 278 if files.get("need_revision", False) 279 ] 280 281 if unsaved_patients: 282 # Warn the user about unsaved patients 283 from PyQt6.QtWidgets import QMessageBox 284 msg = QMessageBox() 285 msg.setIcon(QMessageBox.Icon.Warning) 286 msg.setWindowTitle(QCoreApplication.translate("PipelineReviewPage", "Configuration Incomplete")) 287 msg.setText(QCoreApplication.translate("PipelineReviewPage", "Some patients still require configuration review.")) 288 msg.setInformativeText( 289 QCoreApplication.translate( 290 "PipelineReviewPage", 291 "Please review and save configuration for: {patients}" 292 ).format(patients=", ".join(unsaved_patients)) 293 ) 294 msg.exec() 295 return self # Stay on current page 296 297 # Proceed to pipeline execution page 298 if not self.next_page: 299 self.next_page = PipelineExecutionPage(context, self) 300 self.context["history"].append(self.next_page) 301 self.next_page.on_enter() 302 return self.next_page
Proceed to the next page, executing the pipeline if all configurations are valid.
Args: context (dict): Shared context.
Returns: Page: The next page instance, or self if configuration is incomplete.
304 def back(self): 305 """ 306 Navigate back to the previous page. 307 308 If the output folder for the current config does not exist, 309 the config file is deleted to keep the workspace clean. 310 """ 311 if os.path.exists(self.config_path): 312 try: 313 config_filename = os.path.basename(self.config_path) 314 config_id = config_filename.split('_config.json')[0] 315 pipeline_dir = os.path.dirname(self.config_path) 316 output_folder_path = os.path.join(pipeline_dir, f"{config_id}_output") 317 318 if not os.path.exists(output_folder_path): 319 os.remove(self.config_path) 320 log.info(f"Removed unused config file: {self.config_path}") 321 else: 322 log.info(f"Keeping config file: {self.config_path}") 323 except (OSError, IndexError, ValueError) as e: 324 log.error(f"Error processing config file {self.config_path}: {e}") 325 else: 326 log.error(f"Invalid config path: {self.config_path}") 327 328 if self.previous_page: 329 self.previous_page.on_enter() 330 return self.previous_page 331 return None
Navigate back to the previous page.
If the output folder for the current config does not exist, the config file is deleted to keep the workspace clean.