components.file_role_dialog
1import os 2import re 3 4from PyQt6.QtCore import QCoreApplication 5from PyQt6.QtWidgets import ( 6 QDialog, QVBoxLayout, QWidget, QLabel, QRadioButton, 7 QButtonGroup, QFrame, QGroupBox, QComboBox, QDialogButtonBox 8) 9 10 11class FileRoleDialog(QDialog): 12 """ 13 A multistep dialog for assigning a file's logical role within a workspace. 14 15 Structure: 16 1. Main or Derivatives 17 2. Subject selection (e.g., sub-001) 18 3. Role selection (e.g., anat, ses-01, ses-02) 19 20 Dynamically adapts based on the provided arguments: 21 - `main`, `subj`, and `role` can be pre-filled to skip steps. 22 """ 23 24 def __init__(self, workspace_path=None, subj=None, role=None, main=None, parent=None): 25 super().__init__(parent) 26 27 # Store initial state and references 28 self.subj = subj 29 self.role = role 30 self.main = main 31 self.workspace_path = workspace_path 32 33 # Dialog setup 34 self.setWindowTitle(QCoreApplication.translate("Components", "File role")) 35 layout = QVBoxLayout(self) 36 37 # ---------------------------------------------------------------------- 38 # LEVEL 1 — MAIN / DERIVATIVES SELECTION 39 # ---------------------------------------------------------------------- 40 if main is None and subj is None: 41 # Widget container for level 1 42 self.level1_widget = QWidget() 43 level1_layout = QVBoxLayout(self.level1_widget) 44 45 # Label for level 1 46 self.pos_label = QLabel(QCoreApplication.translate("Components", "Position:")) 47 level1_layout.addWidget(self.pos_label) 48 49 # Two radio options: main or derivatives 50 self.opt_main = QRadioButton(QCoreApplication.translate("Components", "main subject files")) 51 self.opt_derivatives = QRadioButton("derivatives") 52 level1_layout.addWidget(self.opt_main) 53 level1_layout.addWidget(self.opt_derivatives) 54 55 # Group radio buttons to make them mutually exclusive 56 self.button_first_group = QButtonGroup(self) 57 self.button_first_group.addButton(self.opt_main) 58 self.button_first_group.addButton(self.opt_derivatives) 59 60 layout.addWidget(self.level1_widget) 61 62 # When toggled, show/hide derivative options 63 self.button_first_group.buttonToggled.connect(self.first_level_toggled) 64 65 # --- Additional frame for choosing derivative type (hidden by default) 66 self.derivative_extra_frame = QFrame() 67 derivative_extra_layout = QVBoxLayout(self.derivative_extra_frame) 68 self.derivative_extra_label = QLabel(QCoreApplication.translate("Components", "What derivative:")) 69 derivative_extra_layout.addWidget(self.derivative_extra_label) 70 71 # Create sub-options for derivatives 72 self.derivative_extra_button_group = QButtonGroup(self) 73 self.skull_strip_btn = QRadioButton("skullstrips") 74 derivative_extra_layout.addWidget(self.skull_strip_btn) 75 self.derivative_extra_button_group.addButton(self.skull_strip_btn) 76 77 self.manual_mask_btn = QRadioButton("manual_masks") 78 derivative_extra_layout.addWidget(self.manual_mask_btn) 79 self.derivative_extra_button_group.addButton(self.manual_mask_btn) 80 81 self.deep_learning_mask = QRadioButton("deep_learning_seg") 82 derivative_extra_layout.addWidget(self.deep_learning_mask) 83 self.derivative_extra_button_group.addButton(self.deep_learning_mask) 84 85 # Initially hidden (only shown when “derivatives” is selected) 86 self.derivative_extra_frame.hide() 87 layout.addWidget(self.derivative_extra_frame) 88 89 elif main == "derivatives": 90 # When `main` is pre-set to “derivatives”, show only derivative subtype 91 self.derivative_extra_frame = QFrame(self) 92 derivative_extra_layout = QVBoxLayout(self.derivative_extra_frame) 93 self.derivative_extra_label = QLabel(QCoreApplication.translate("Components", "What derivative:")) 94 derivative_extra_layout.addWidget(self.derivative_extra_label) 95 96 self.derivative_extra_button_group = QButtonGroup(self) 97 self.skull_strip_btn = QRadioButton("skullstrips") 98 derivative_extra_layout.addWidget(self.skull_strip_btn) 99 self.derivative_extra_button_group.addButton(self.skull_strip_btn) 100 101 self.manual_mask_btn = QRadioButton("manual_masks") 102 derivative_extra_layout.addWidget(self.manual_mask_btn) 103 self.derivative_extra_button_group.addButton(self.manual_mask_btn) 104 105 self.deep_learning_mask = QRadioButton("deep_learning_seg") 106 derivative_extra_layout.addWidget(self.deep_learning_mask) 107 self.derivative_extra_button_group.addButton(self.deep_learning_mask) 108 109 layout.addWidget(self.derivative_extra_frame) 110 self.button_first_group = None # no top-level main/derivatives group in this case 111 112 else: 113 # If `main` is not relevant, skip both 114 self.button_first_group = None 115 self.derivative_extra_button_group = None 116 117 # ---------------------------------------------------------------------- 118 # LEVEL 2 — SUBJECT SELECTION 119 # ---------------------------------------------------------------------- 120 if subj is None: 121 # Group box for subject selection 122 self.level2_widget = QGroupBox(QCoreApplication.translate("Components", "Subject")) 123 level2_layout = QVBoxLayout(self.level2_widget) 124 125 # Gather all available subjects (folders starting with "sub-") 126 subjects = [os.path.basename(p) for p in self._find_patient_dirs()] 127 128 # Use a dropdown instead of multiple radio buttons 129 self.subj_combo = QComboBox() 130 self.subj_combo.addItems(subjects) 131 level2_layout.addWidget(self.subj_combo) 132 133 # Placeholder for backward compatibility 134 self.subj_buttons = [] 135 self.button_second_group = None 136 137 layout.addWidget(self.level2_widget) 138 else: 139 self.button_second_group = None 140 141 # ---------------------------------------------------------------------- 142 # LEVEL 3 — ROLE / SESSION SELECTION 143 # ---------------------------------------------------------------------- 144 if role is None: 145 self.level3_widget = QWidget() 146 level3_layout = QVBoxLayout(self.level3_widget) 147 self.role_label = QLabel(QCoreApplication.translate("Components", "Role:")) 148 level3_layout.addWidget(self.role_label) 149 150 # Possible role choices 151 self.button_third_group = QButtonGroup(self) 152 self.anat_button = QRadioButton("anat") 153 self.button_third_group.addButton(self.anat_button) 154 level3_layout.addWidget(self.anat_button) 155 156 self.ses_1_button = QRadioButton("ses-01") 157 self.button_third_group.addButton(self.ses_1_button) 158 level3_layout.addWidget(self.ses_1_button) 159 160 self.ses_2_button = QRadioButton("ses-02") 161 self.button_third_group.addButton(self.ses_2_button) 162 level3_layout.addWidget(self.ses_2_button) 163 164 layout.addWidget(self.level3_widget) 165 else: 166 self.button_third_group = None 167 168 # ---------------------------------------------------------------------- 169 # DIALOG BUTTONS (OK / CANCEL) 170 # ---------------------------------------------------------------------- 171 buttons = QDialogButtonBox( 172 QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel 173 ) 174 buttons.accepted.connect(self.accept) 175 buttons.rejected.connect(self.reject) 176 layout.addWidget(buttons) 177 178 # Keep reference to OK button so we can enable/disable it 179 self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) 180 self.ok_button.setEnabled(False) # initially disabled 181 182 # ---------------------------------------------------------------------- 183 # SIGNAL CONNECTIONS FOR VALIDATION 184 # ---------------------------------------------------------------------- 185 if self.button_first_group: 186 self.button_first_group.buttonToggled.connect(self.update_ok_button) 187 188 if self.derivative_extra_button_group: 189 self.derivative_extra_button_group.buttonToggled.connect(self.update_ok_button) 190 191 if hasattr(self, "subj_combo"): 192 self.subj_combo.currentIndexChanged.connect(self.update_ok_button) 193 194 if self.button_third_group: 195 self.button_third_group.buttonToggled.connect(self.update_ok_button) 196 197 # ====================================================================== 198 # SUPPORT METHODS 199 # ====================================================================== 200 201 def filter_subjects(self, text): 202 """Live filter for subjects (used if implemented with QListWidget).""" 203 if hasattr(self, "subj_list"): 204 for i in range(self.subj_list.count()): 205 item = self.subj_list.item(i) 206 item.setHidden(text.lower() not in item.text().lower()) 207 208 def get_selections(self): 209 """Return all selected values from each level as a dictionary.""" 210 selections = {} 211 212 # --- Level 1: Main / Derivatives --- 213 if self.button_first_group: 214 btn = self.button_first_group.checkedButton() 215 selections["main"] = btn.text() if btn else None 216 217 if self.derivative_extra_button_group: 218 btn = self.derivative_extra_button_group.checkedButton() 219 selections["derivative"] = btn.text() if btn else None 220 221 # --- Level 2: Subject --- 222 if self.button_second_group: 223 btn = self.button_second_group.checkedButton() 224 selections["subj"] = btn.text() if btn else None 225 elif hasattr(self, "subj_combo"): 226 selections["subj"] = self.subj_combo.currentText() 227 228 # --- Level 3: Role --- 229 if self.button_third_group: 230 btn = self.button_third_group.checkedButton() 231 selections["role"] = btn.text() if btn else None 232 233 return selections 234 235 def get_relative_path(self): 236 """ 237 Build a relative path string (BIDS-like) from user selections. 238 Example: 239 derivatives/manual_masks/sub-001/ses-01/pet 240 """ 241 parts = [] 242 selections = self.get_selections() 243 244 main = selections.get("main") 245 subj = selections.get("subj") 246 role = selections.get("role") 247 derivative = selections.get("derivative") 248 249 # Handle derivatives hierarchy 250 if main == "derivatives": 251 parts.append("derivatives") 252 if derivative: 253 parts.append(derivative) 254 255 # Add subject 256 if subj: 257 parts.append(subj) 258 259 # Add role (special handling for session) 260 if role: 261 if re.match(r"^ses-\d+$", role): 262 parts.append(role) 263 parts.append("pet") # convention for PET session directories 264 else: 265 parts.append(role) 266 267 return os.path.join(*parts) if parts else None 268 269 def _find_patient_dirs(self): 270 """Return list of patient directories (sub-xxx), excluding 'derivatives'.""" 271 patient_dirs = [] 272 273 for root, dirs, files in os.walk(self.workspace_path): 274 # Exclude derivatives from search 275 if "derivatives" in dirs: 276 dirs.remove("derivatives") 277 278 # Collect dirs starting with 'sub-' 279 for dir_name in dirs: 280 if dir_name.startswith("sub-"): 281 full_path = os.path.join(root, dir_name) 282 patient_dirs.append(full_path) 283 284 return patient_dirs 285 286 # ---------------------------------------------------------------------- 287 # UI DYNAMICS 288 # ---------------------------------------------------------------------- 289 def first_level_toggled(self, button, checked): 290 """Show/hide derivative subtype section depending on selection.""" 291 if not checked: 292 return 293 if button == self.opt_main: 294 self.derivative_extra_frame.hide() 295 self.adjustSize() 296 if button == self.opt_derivatives: 297 self.derivative_extra_frame.show() 298 self.adjustSize() 299 300 def update_ok_button(self): 301 """Enable OK only when all required selections are made.""" 302 selections = self.get_selections() 303 enable = True 304 305 # Require main selection 306 if not selections.get("main"): 307 enable = False 308 309 # Require derivative type if derivatives is selected 310 if selections.get("main") == "derivatives" and not selections.get("derivative"): 311 enable = False 312 313 # Require subject selection 314 if not selections.get("subj"): 315 enable = False 316 317 # Require role selection 318 if not selections.get("role"): 319 enable = False 320 321 # Update OK button state 322 self.ok_button.setEnabled(enable)
class
FileRoleDialog(PyQt6.QtWidgets.QDialog):
12class FileRoleDialog(QDialog): 13 """ 14 A multistep dialog for assigning a file's logical role within a workspace. 15 16 Structure: 17 1. Main or Derivatives 18 2. Subject selection (e.g., sub-001) 19 3. Role selection (e.g., anat, ses-01, ses-02) 20 21 Dynamically adapts based on the provided arguments: 22 - `main`, `subj`, and `role` can be pre-filled to skip steps. 23 """ 24 25 def __init__(self, workspace_path=None, subj=None, role=None, main=None, parent=None): 26 super().__init__(parent) 27 28 # Store initial state and references 29 self.subj = subj 30 self.role = role 31 self.main = main 32 self.workspace_path = workspace_path 33 34 # Dialog setup 35 self.setWindowTitle(QCoreApplication.translate("Components", "File role")) 36 layout = QVBoxLayout(self) 37 38 # ---------------------------------------------------------------------- 39 # LEVEL 1 — MAIN / DERIVATIVES SELECTION 40 # ---------------------------------------------------------------------- 41 if main is None and subj is None: 42 # Widget container for level 1 43 self.level1_widget = QWidget() 44 level1_layout = QVBoxLayout(self.level1_widget) 45 46 # Label for level 1 47 self.pos_label = QLabel(QCoreApplication.translate("Components", "Position:")) 48 level1_layout.addWidget(self.pos_label) 49 50 # Two radio options: main or derivatives 51 self.opt_main = QRadioButton(QCoreApplication.translate("Components", "main subject files")) 52 self.opt_derivatives = QRadioButton("derivatives") 53 level1_layout.addWidget(self.opt_main) 54 level1_layout.addWidget(self.opt_derivatives) 55 56 # Group radio buttons to make them mutually exclusive 57 self.button_first_group = QButtonGroup(self) 58 self.button_first_group.addButton(self.opt_main) 59 self.button_first_group.addButton(self.opt_derivatives) 60 61 layout.addWidget(self.level1_widget) 62 63 # When toggled, show/hide derivative options 64 self.button_first_group.buttonToggled.connect(self.first_level_toggled) 65 66 # --- Additional frame for choosing derivative type (hidden by default) 67 self.derivative_extra_frame = QFrame() 68 derivative_extra_layout = QVBoxLayout(self.derivative_extra_frame) 69 self.derivative_extra_label = QLabel(QCoreApplication.translate("Components", "What derivative:")) 70 derivative_extra_layout.addWidget(self.derivative_extra_label) 71 72 # Create sub-options for derivatives 73 self.derivative_extra_button_group = QButtonGroup(self) 74 self.skull_strip_btn = QRadioButton("skullstrips") 75 derivative_extra_layout.addWidget(self.skull_strip_btn) 76 self.derivative_extra_button_group.addButton(self.skull_strip_btn) 77 78 self.manual_mask_btn = QRadioButton("manual_masks") 79 derivative_extra_layout.addWidget(self.manual_mask_btn) 80 self.derivative_extra_button_group.addButton(self.manual_mask_btn) 81 82 self.deep_learning_mask = QRadioButton("deep_learning_seg") 83 derivative_extra_layout.addWidget(self.deep_learning_mask) 84 self.derivative_extra_button_group.addButton(self.deep_learning_mask) 85 86 # Initially hidden (only shown when “derivatives” is selected) 87 self.derivative_extra_frame.hide() 88 layout.addWidget(self.derivative_extra_frame) 89 90 elif main == "derivatives": 91 # When `main` is pre-set to “derivatives”, show only derivative subtype 92 self.derivative_extra_frame = QFrame(self) 93 derivative_extra_layout = QVBoxLayout(self.derivative_extra_frame) 94 self.derivative_extra_label = QLabel(QCoreApplication.translate("Components", "What derivative:")) 95 derivative_extra_layout.addWidget(self.derivative_extra_label) 96 97 self.derivative_extra_button_group = QButtonGroup(self) 98 self.skull_strip_btn = QRadioButton("skullstrips") 99 derivative_extra_layout.addWidget(self.skull_strip_btn) 100 self.derivative_extra_button_group.addButton(self.skull_strip_btn) 101 102 self.manual_mask_btn = QRadioButton("manual_masks") 103 derivative_extra_layout.addWidget(self.manual_mask_btn) 104 self.derivative_extra_button_group.addButton(self.manual_mask_btn) 105 106 self.deep_learning_mask = QRadioButton("deep_learning_seg") 107 derivative_extra_layout.addWidget(self.deep_learning_mask) 108 self.derivative_extra_button_group.addButton(self.deep_learning_mask) 109 110 layout.addWidget(self.derivative_extra_frame) 111 self.button_first_group = None # no top-level main/derivatives group in this case 112 113 else: 114 # If `main` is not relevant, skip both 115 self.button_first_group = None 116 self.derivative_extra_button_group = None 117 118 # ---------------------------------------------------------------------- 119 # LEVEL 2 — SUBJECT SELECTION 120 # ---------------------------------------------------------------------- 121 if subj is None: 122 # Group box for subject selection 123 self.level2_widget = QGroupBox(QCoreApplication.translate("Components", "Subject")) 124 level2_layout = QVBoxLayout(self.level2_widget) 125 126 # Gather all available subjects (folders starting with "sub-") 127 subjects = [os.path.basename(p) for p in self._find_patient_dirs()] 128 129 # Use a dropdown instead of multiple radio buttons 130 self.subj_combo = QComboBox() 131 self.subj_combo.addItems(subjects) 132 level2_layout.addWidget(self.subj_combo) 133 134 # Placeholder for backward compatibility 135 self.subj_buttons = [] 136 self.button_second_group = None 137 138 layout.addWidget(self.level2_widget) 139 else: 140 self.button_second_group = None 141 142 # ---------------------------------------------------------------------- 143 # LEVEL 3 — ROLE / SESSION SELECTION 144 # ---------------------------------------------------------------------- 145 if role is None: 146 self.level3_widget = QWidget() 147 level3_layout = QVBoxLayout(self.level3_widget) 148 self.role_label = QLabel(QCoreApplication.translate("Components", "Role:")) 149 level3_layout.addWidget(self.role_label) 150 151 # Possible role choices 152 self.button_third_group = QButtonGroup(self) 153 self.anat_button = QRadioButton("anat") 154 self.button_third_group.addButton(self.anat_button) 155 level3_layout.addWidget(self.anat_button) 156 157 self.ses_1_button = QRadioButton("ses-01") 158 self.button_third_group.addButton(self.ses_1_button) 159 level3_layout.addWidget(self.ses_1_button) 160 161 self.ses_2_button = QRadioButton("ses-02") 162 self.button_third_group.addButton(self.ses_2_button) 163 level3_layout.addWidget(self.ses_2_button) 164 165 layout.addWidget(self.level3_widget) 166 else: 167 self.button_third_group = None 168 169 # ---------------------------------------------------------------------- 170 # DIALOG BUTTONS (OK / CANCEL) 171 # ---------------------------------------------------------------------- 172 buttons = QDialogButtonBox( 173 QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel 174 ) 175 buttons.accepted.connect(self.accept) 176 buttons.rejected.connect(self.reject) 177 layout.addWidget(buttons) 178 179 # Keep reference to OK button so we can enable/disable it 180 self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) 181 self.ok_button.setEnabled(False) # initially disabled 182 183 # ---------------------------------------------------------------------- 184 # SIGNAL CONNECTIONS FOR VALIDATION 185 # ---------------------------------------------------------------------- 186 if self.button_first_group: 187 self.button_first_group.buttonToggled.connect(self.update_ok_button) 188 189 if self.derivative_extra_button_group: 190 self.derivative_extra_button_group.buttonToggled.connect(self.update_ok_button) 191 192 if hasattr(self, "subj_combo"): 193 self.subj_combo.currentIndexChanged.connect(self.update_ok_button) 194 195 if self.button_third_group: 196 self.button_third_group.buttonToggled.connect(self.update_ok_button) 197 198 # ====================================================================== 199 # SUPPORT METHODS 200 # ====================================================================== 201 202 def filter_subjects(self, text): 203 """Live filter for subjects (used if implemented with QListWidget).""" 204 if hasattr(self, "subj_list"): 205 for i in range(self.subj_list.count()): 206 item = self.subj_list.item(i) 207 item.setHidden(text.lower() not in item.text().lower()) 208 209 def get_selections(self): 210 """Return all selected values from each level as a dictionary.""" 211 selections = {} 212 213 # --- Level 1: Main / Derivatives --- 214 if self.button_first_group: 215 btn = self.button_first_group.checkedButton() 216 selections["main"] = btn.text() if btn else None 217 218 if self.derivative_extra_button_group: 219 btn = self.derivative_extra_button_group.checkedButton() 220 selections["derivative"] = btn.text() if btn else None 221 222 # --- Level 2: Subject --- 223 if self.button_second_group: 224 btn = self.button_second_group.checkedButton() 225 selections["subj"] = btn.text() if btn else None 226 elif hasattr(self, "subj_combo"): 227 selections["subj"] = self.subj_combo.currentText() 228 229 # --- Level 3: Role --- 230 if self.button_third_group: 231 btn = self.button_third_group.checkedButton() 232 selections["role"] = btn.text() if btn else None 233 234 return selections 235 236 def get_relative_path(self): 237 """ 238 Build a relative path string (BIDS-like) from user selections. 239 Example: 240 derivatives/manual_masks/sub-001/ses-01/pet 241 """ 242 parts = [] 243 selections = self.get_selections() 244 245 main = selections.get("main") 246 subj = selections.get("subj") 247 role = selections.get("role") 248 derivative = selections.get("derivative") 249 250 # Handle derivatives hierarchy 251 if main == "derivatives": 252 parts.append("derivatives") 253 if derivative: 254 parts.append(derivative) 255 256 # Add subject 257 if subj: 258 parts.append(subj) 259 260 # Add role (special handling for session) 261 if role: 262 if re.match(r"^ses-\d+$", role): 263 parts.append(role) 264 parts.append("pet") # convention for PET session directories 265 else: 266 parts.append(role) 267 268 return os.path.join(*parts) if parts else None 269 270 def _find_patient_dirs(self): 271 """Return list of patient directories (sub-xxx), excluding 'derivatives'.""" 272 patient_dirs = [] 273 274 for root, dirs, files in os.walk(self.workspace_path): 275 # Exclude derivatives from search 276 if "derivatives" in dirs: 277 dirs.remove("derivatives") 278 279 # Collect dirs starting with 'sub-' 280 for dir_name in dirs: 281 if dir_name.startswith("sub-"): 282 full_path = os.path.join(root, dir_name) 283 patient_dirs.append(full_path) 284 285 return patient_dirs 286 287 # ---------------------------------------------------------------------- 288 # UI DYNAMICS 289 # ---------------------------------------------------------------------- 290 def first_level_toggled(self, button, checked): 291 """Show/hide derivative subtype section depending on selection.""" 292 if not checked: 293 return 294 if button == self.opt_main: 295 self.derivative_extra_frame.hide() 296 self.adjustSize() 297 if button == self.opt_derivatives: 298 self.derivative_extra_frame.show() 299 self.adjustSize() 300 301 def update_ok_button(self): 302 """Enable OK only when all required selections are made.""" 303 selections = self.get_selections() 304 enable = True 305 306 # Require main selection 307 if not selections.get("main"): 308 enable = False 309 310 # Require derivative type if derivatives is selected 311 if selections.get("main") == "derivatives" and not selections.get("derivative"): 312 enable = False 313 314 # Require subject selection 315 if not selections.get("subj"): 316 enable = False 317 318 # Require role selection 319 if not selections.get("role"): 320 enable = False 321 322 # Update OK button state 323 self.ok_button.setEnabled(enable)
A multistep dialog for assigning a file's logical role within a workspace.
Structure:
- Main or Derivatives
- Subject selection (e.g., sub-001)
- Role selection (e.g., anat, ses-01, ses-02)
Dynamically adapts based on the provided arguments:
- main, subj, and role can be pre-filled to skip steps.
FileRoleDialog(workspace_path=None, subj=None, role=None, main=None, parent=None)
25 def __init__(self, workspace_path=None, subj=None, role=None, main=None, parent=None): 26 super().__init__(parent) 27 28 # Store initial state and references 29 self.subj = subj 30 self.role = role 31 self.main = main 32 self.workspace_path = workspace_path 33 34 # Dialog setup 35 self.setWindowTitle(QCoreApplication.translate("Components", "File role")) 36 layout = QVBoxLayout(self) 37 38 # ---------------------------------------------------------------------- 39 # LEVEL 1 — MAIN / DERIVATIVES SELECTION 40 # ---------------------------------------------------------------------- 41 if main is None and subj is None: 42 # Widget container for level 1 43 self.level1_widget = QWidget() 44 level1_layout = QVBoxLayout(self.level1_widget) 45 46 # Label for level 1 47 self.pos_label = QLabel(QCoreApplication.translate("Components", "Position:")) 48 level1_layout.addWidget(self.pos_label) 49 50 # Two radio options: main or derivatives 51 self.opt_main = QRadioButton(QCoreApplication.translate("Components", "main subject files")) 52 self.opt_derivatives = QRadioButton("derivatives") 53 level1_layout.addWidget(self.opt_main) 54 level1_layout.addWidget(self.opt_derivatives) 55 56 # Group radio buttons to make them mutually exclusive 57 self.button_first_group = QButtonGroup(self) 58 self.button_first_group.addButton(self.opt_main) 59 self.button_first_group.addButton(self.opt_derivatives) 60 61 layout.addWidget(self.level1_widget) 62 63 # When toggled, show/hide derivative options 64 self.button_first_group.buttonToggled.connect(self.first_level_toggled) 65 66 # --- Additional frame for choosing derivative type (hidden by default) 67 self.derivative_extra_frame = QFrame() 68 derivative_extra_layout = QVBoxLayout(self.derivative_extra_frame) 69 self.derivative_extra_label = QLabel(QCoreApplication.translate("Components", "What derivative:")) 70 derivative_extra_layout.addWidget(self.derivative_extra_label) 71 72 # Create sub-options for derivatives 73 self.derivative_extra_button_group = QButtonGroup(self) 74 self.skull_strip_btn = QRadioButton("skullstrips") 75 derivative_extra_layout.addWidget(self.skull_strip_btn) 76 self.derivative_extra_button_group.addButton(self.skull_strip_btn) 77 78 self.manual_mask_btn = QRadioButton("manual_masks") 79 derivative_extra_layout.addWidget(self.manual_mask_btn) 80 self.derivative_extra_button_group.addButton(self.manual_mask_btn) 81 82 self.deep_learning_mask = QRadioButton("deep_learning_seg") 83 derivative_extra_layout.addWidget(self.deep_learning_mask) 84 self.derivative_extra_button_group.addButton(self.deep_learning_mask) 85 86 # Initially hidden (only shown when “derivatives” is selected) 87 self.derivative_extra_frame.hide() 88 layout.addWidget(self.derivative_extra_frame) 89 90 elif main == "derivatives": 91 # When `main` is pre-set to “derivatives”, show only derivative subtype 92 self.derivative_extra_frame = QFrame(self) 93 derivative_extra_layout = QVBoxLayout(self.derivative_extra_frame) 94 self.derivative_extra_label = QLabel(QCoreApplication.translate("Components", "What derivative:")) 95 derivative_extra_layout.addWidget(self.derivative_extra_label) 96 97 self.derivative_extra_button_group = QButtonGroup(self) 98 self.skull_strip_btn = QRadioButton("skullstrips") 99 derivative_extra_layout.addWidget(self.skull_strip_btn) 100 self.derivative_extra_button_group.addButton(self.skull_strip_btn) 101 102 self.manual_mask_btn = QRadioButton("manual_masks") 103 derivative_extra_layout.addWidget(self.manual_mask_btn) 104 self.derivative_extra_button_group.addButton(self.manual_mask_btn) 105 106 self.deep_learning_mask = QRadioButton("deep_learning_seg") 107 derivative_extra_layout.addWidget(self.deep_learning_mask) 108 self.derivative_extra_button_group.addButton(self.deep_learning_mask) 109 110 layout.addWidget(self.derivative_extra_frame) 111 self.button_first_group = None # no top-level main/derivatives group in this case 112 113 else: 114 # If `main` is not relevant, skip both 115 self.button_first_group = None 116 self.derivative_extra_button_group = None 117 118 # ---------------------------------------------------------------------- 119 # LEVEL 2 — SUBJECT SELECTION 120 # ---------------------------------------------------------------------- 121 if subj is None: 122 # Group box for subject selection 123 self.level2_widget = QGroupBox(QCoreApplication.translate("Components", "Subject")) 124 level2_layout = QVBoxLayout(self.level2_widget) 125 126 # Gather all available subjects (folders starting with "sub-") 127 subjects = [os.path.basename(p) for p in self._find_patient_dirs()] 128 129 # Use a dropdown instead of multiple radio buttons 130 self.subj_combo = QComboBox() 131 self.subj_combo.addItems(subjects) 132 level2_layout.addWidget(self.subj_combo) 133 134 # Placeholder for backward compatibility 135 self.subj_buttons = [] 136 self.button_second_group = None 137 138 layout.addWidget(self.level2_widget) 139 else: 140 self.button_second_group = None 141 142 # ---------------------------------------------------------------------- 143 # LEVEL 3 — ROLE / SESSION SELECTION 144 # ---------------------------------------------------------------------- 145 if role is None: 146 self.level3_widget = QWidget() 147 level3_layout = QVBoxLayout(self.level3_widget) 148 self.role_label = QLabel(QCoreApplication.translate("Components", "Role:")) 149 level3_layout.addWidget(self.role_label) 150 151 # Possible role choices 152 self.button_third_group = QButtonGroup(self) 153 self.anat_button = QRadioButton("anat") 154 self.button_third_group.addButton(self.anat_button) 155 level3_layout.addWidget(self.anat_button) 156 157 self.ses_1_button = QRadioButton("ses-01") 158 self.button_third_group.addButton(self.ses_1_button) 159 level3_layout.addWidget(self.ses_1_button) 160 161 self.ses_2_button = QRadioButton("ses-02") 162 self.button_third_group.addButton(self.ses_2_button) 163 level3_layout.addWidget(self.ses_2_button) 164 165 layout.addWidget(self.level3_widget) 166 else: 167 self.button_third_group = None 168 169 # ---------------------------------------------------------------------- 170 # DIALOG BUTTONS (OK / CANCEL) 171 # ---------------------------------------------------------------------- 172 buttons = QDialogButtonBox( 173 QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel 174 ) 175 buttons.accepted.connect(self.accept) 176 buttons.rejected.connect(self.reject) 177 layout.addWidget(buttons) 178 179 # Keep reference to OK button so we can enable/disable it 180 self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) 181 self.ok_button.setEnabled(False) # initially disabled 182 183 # ---------------------------------------------------------------------- 184 # SIGNAL CONNECTIONS FOR VALIDATION 185 # ---------------------------------------------------------------------- 186 if self.button_first_group: 187 self.button_first_group.buttonToggled.connect(self.update_ok_button) 188 189 if self.derivative_extra_button_group: 190 self.derivative_extra_button_group.buttonToggled.connect(self.update_ok_button) 191 192 if hasattr(self, "subj_combo"): 193 self.subj_combo.currentIndexChanged.connect(self.update_ok_button) 194 195 if self.button_third_group: 196 self.button_third_group.buttonToggled.connect(self.update_ok_button)
def
filter_subjects(self, text):
202 def filter_subjects(self, text): 203 """Live filter for subjects (used if implemented with QListWidget).""" 204 if hasattr(self, "subj_list"): 205 for i in range(self.subj_list.count()): 206 item = self.subj_list.item(i) 207 item.setHidden(text.lower() not in item.text().lower())
Live filter for subjects (used if implemented with QListWidget).
def
get_selections(self):
209 def get_selections(self): 210 """Return all selected values from each level as a dictionary.""" 211 selections = {} 212 213 # --- Level 1: Main / Derivatives --- 214 if self.button_first_group: 215 btn = self.button_first_group.checkedButton() 216 selections["main"] = btn.text() if btn else None 217 218 if self.derivative_extra_button_group: 219 btn = self.derivative_extra_button_group.checkedButton() 220 selections["derivative"] = btn.text() if btn else None 221 222 # --- Level 2: Subject --- 223 if self.button_second_group: 224 btn = self.button_second_group.checkedButton() 225 selections["subj"] = btn.text() if btn else None 226 elif hasattr(self, "subj_combo"): 227 selections["subj"] = self.subj_combo.currentText() 228 229 # --- Level 3: Role --- 230 if self.button_third_group: 231 btn = self.button_third_group.checkedButton() 232 selections["role"] = btn.text() if btn else None 233 234 return selections
Return all selected values from each level as a dictionary.
def
get_relative_path(self):
236 def get_relative_path(self): 237 """ 238 Build a relative path string (BIDS-like) from user selections. 239 Example: 240 derivatives/manual_masks/sub-001/ses-01/pet 241 """ 242 parts = [] 243 selections = self.get_selections() 244 245 main = selections.get("main") 246 subj = selections.get("subj") 247 role = selections.get("role") 248 derivative = selections.get("derivative") 249 250 # Handle derivatives hierarchy 251 if main == "derivatives": 252 parts.append("derivatives") 253 if derivative: 254 parts.append(derivative) 255 256 # Add subject 257 if subj: 258 parts.append(subj) 259 260 # Add role (special handling for session) 261 if role: 262 if re.match(r"^ses-\d+$", role): 263 parts.append(role) 264 parts.append("pet") # convention for PET session directories 265 else: 266 parts.append(role) 267 268 return os.path.join(*parts) if parts else None
Build a relative path string (BIDS-like) from user selections. Example: derivatives/manual_masks/sub-001/ses-01/pet
def
first_level_toggled(self, button, checked):
290 def first_level_toggled(self, button, checked): 291 """Show/hide derivative subtype section depending on selection.""" 292 if not checked: 293 return 294 if button == self.opt_main: 295 self.derivative_extra_frame.hide() 296 self.adjustSize() 297 if button == self.opt_derivatives: 298 self.derivative_extra_frame.show() 299 self.adjustSize()
Show/hide derivative subtype section depending on selection.