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:

  1. Main or Derivatives
  2. Subject selection (e.g., sub-001)
  3. 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)
subj
role
main
workspace_path
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.

def update_ok_button(self):
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)

Enable OK only when all required selections are made.