components.nifti_file_dialog

  1from pathlib import Path
  2
  3from PyQt6.QtWidgets import (
  4    QDialog, QVBoxLayout, QGroupBox, QGridLayout, QLabel, QLineEdit, QComboBox,
  5    QCheckBox, QListWidget, QListWidgetItem, QPushButton, QDialogButtonBox,
  6    QMessageBox
  7)
  8from PyQt6.QtCore import Qt, QCoreApplication
  9from PyQt6.QtGui import QBrush, QColor
 10import os
 11from logger import get_logger
 12
 13log = get_logger()
 14
 15class NiftiFileDialog(QDialog):
 16    def __init__(self, context, allow_multiple=None, has_existing_func=None, label=None, forced_filters=None):
 17        super().__init__()
 18        self.label = label
 19        self.setWindowTitle(QCoreApplication.translate("Components", "Select NIfTI Files ({0})").format(self.label))
 20        self.resize(700, 650)
 21
 22        self.workspace_path = context["workspace_path"]
 23        self.allow_multiple = allow_multiple
 24        self.has_existing_func = has_existing_func or (lambda *_: False)
 25        self.forced_filters = forced_filters or {}
 26
 27        self.selected_files = []
 28        self.relative_to_absolute = {}
 29        self.files_with_flag = set()
 30
 31        self._build_ui()
 32        self._populate_files()
 33        self._connect_signals()
 34
 35    # === Public API ===
 36    @staticmethod
 37    def get_files(context, allow_multiple=True, has_existing_func=None, label="mask",forced_filters=None):
 38        dialog = NiftiFileDialog(context, allow_multiple, has_existing_func, label,forced_filters)
 39        if dialog.exec() == QDialog.DialogCode.Accepted:
 40            return dialog.selected_files
 41        return None
 42
 43    # === UI Construction ===
 44    def _build_ui(self):
 45        layout = QVBoxLayout(self)
 46
 47        # === Filters ===
 48        filter_group = QGroupBox(QCoreApplication.translate("Components", "Filters"))
 49        filter_layout = QGridLayout(filter_group)
 50
 51        self.search_bar = QLineEdit()
 52        self.search_bar.setPlaceholderText(QCoreApplication.translate("Components", "Search files (FLAIR, T1, T2, etc.)"))
 53        self.search_bar.setClearButtonEnabled(True)
 54        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Search:")), 0, 0)
 55        filter_layout.addWidget(self.search_bar, 0, 1, 1, 3)
 56
 57        self.subject_combo = QComboBox()
 58        self.subject_combo.addItem(QCoreApplication.translate("Components", "All subjects"))
 59        self.subject_combo.model().item(0)
 60        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Subject:")), 1, 0)
 61        filter_layout.addWidget(self.subject_combo, 1, 1)
 62
 63        self.session_combo = QComboBox()
 64        self.session_combo.addItem(QCoreApplication.translate("Components", "All sessions"))
 65        self.session_combo.model().item(0)
 66        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Session:")), 1, 2)
 67        filter_layout.addWidget(self.session_combo, 1, 3)
 68
 69        self.modality_combo = QComboBox()
 70        self.modality_combo.addItem(QCoreApplication.translate("Components", "All modalities"))
 71        self.modality_combo.model().item(0)
 72        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Modality:")), 2, 0)
 73        filter_layout.addWidget(self.modality_combo, 2, 1)
 74
 75        self.datatype_combo = QComboBox()
 76        self.datatype_combo.addItem(QCoreApplication.translate("Components", "All types"))
 77        self.datatype_combo.model().item(0)
 78        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Data type:")), 2, 2)
 79        filter_layout.addWidget(self.datatype_combo, 2, 3)
 80
 81        self.no_flag_checkbox = QCheckBox(QCoreApplication.translate("Components", "Show only files without existing {0}s").format(self.label))
 82        self.with_flag_checkbox = QCheckBox(QCoreApplication.translate("Components", "Show only files with existing {0}s").format(self.label))
 83        if self.label is None:
 84            self.no_flag_checkbox.setVisible(False)
 85            self.with_flag_checkbox.setVisible(False)
 86        filter_layout.addWidget(self.no_flag_checkbox, 3, 0, 1, 2)
 87        filter_layout.addWidget(self.with_flag_checkbox, 3, 2, 1, 2)
 88
 89        layout.addWidget(filter_group)
 90
 91        # === Info label ===
 92        self.info_label = QLabel()
 93        layout.addWidget(self.info_label)
 94
 95        # === File list ===
 96        self.file_list = QListWidget()
 97        self.file_list.setEditTriggers(QListWidget.EditTrigger.NoEditTriggers)
 98        self.file_list.setSelectionMode(
 99            QListWidget.SelectionMode.ExtendedSelection if self.allow_multiple else QListWidget.SelectionMode.SingleSelection
100        )
101        self.file_list.setAlternatingRowColors(True)
102        layout.addWidget(self.file_list)
103
104        # === Buttons ===
105        self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
106
107        reset_button = QPushButton(QCoreApplication.translate("Components", "Reset Filters"))
108        select_all_button = QPushButton(QCoreApplication.translate("Components", "Select All Visible"))
109        deselect_all_button = QPushButton(QCoreApplication.translate("Components", "Deselect All"))
110
111        self.buttons.addButton(reset_button, QDialogButtonBox.ButtonRole.ResetRole)
112        self.buttons.addButton(select_all_button, QDialogButtonBox.ButtonRole.ActionRole)
113        self.buttons.addButton(deselect_all_button, QDialogButtonBox.ButtonRole.ActionRole)
114
115        layout.addWidget(self.buttons)
116
117        # === Store buttons for handlers ===
118        self._reset_button = reset_button
119        self._select_all_button = select_all_button
120        self._deselect_all_button = deselect_all_button
121
122    # === Populate ===
123    def _populate_files(self):
124        self.all_nii_files = []
125        subjects_set, sessions_set, modalities_set, datatypes_set = set(), set(), set(), set()
126
127        for root, dirs, files in os.walk(self.workspace_path):
128            # dirs[:] = [d for d in dirs if d != "derivatives"]
129
130            for f in files:
131                if f.endswith((".nii", ".nii.gz")):
132                    full_path = os.path.join(root, f)
133                    relative_path = os.path.relpath(full_path, self.workspace_path)
134                    self.all_nii_files.append(relative_path)
135                    self.relative_to_absolute[relative_path] = full_path
136
137                    path_parts = relative_path.split(os.sep)
138
139                    # subject
140                    for part in path_parts:
141                        if part.startswith("sub-"):
142                            subjects_set.add(part)
143                            break
144
145                    # session
146                    for part in path_parts:
147                        if part.startswith("ses-"):
148                            sessions_set.add(part)
149                            break
150
151                    # modality
152                    filename_noext = os.path.splitext(os.path.splitext(f)[0])[0]
153                    modality = filename_noext.split("_")[-1] or QCoreApplication.translate("Components", "Unknown")
154                    modalities_set.add(modality)
155
156                    # existing flag (mask/skull strip)
157                    if self.has_existing_func(full_path, self.workspace_path):
158                        self.files_with_flag.add(relative_path)
159
160        # --- Cerca data type directories ---
161        for subject_dir in sorted(Path(self.workspace_path).glob("sub-*")):
162            if subject_dir.is_dir():
163                # Caso: sub-*/anat, sub-*/func, ecc.
164                for child in subject_dir.iterdir():
165                    if child.is_dir() and not child.name.startswith("ses-"):
166                        datatypes_set.add(child.name)
167
168                # Caso: sub-*/ses-*/anat, sub-*/ses-*/func, ecc.
169                for ses_dir in subject_dir.glob("ses-*"):
170                    if ses_dir.is_dir():
171                        for child in ses_dir.iterdir():
172                            if child.is_dir():
173                                datatypes_set.add(child.name)
174
175        self.subject_combo.addItems(sorted(subjects_set))
176        self.session_combo.addItems(sorted(sessions_set))
177        self.modality_combo.addItems(sorted(modalities_set))
178        self.datatype_combo.addItems(sorted(datatypes_set))
179
180        self._update_info_label(len(self.all_nii_files))
181        self._populate_file_list()
182
183        if self.forced_filters:
184            self._apply_forced_filters()
185
186    def _populate_file_list(self):
187        self.file_list.clear()
188        for relative_path in sorted(self.all_nii_files):
189            item = QListWidgetItem(relative_path)
190            if relative_path in self.files_with_flag:
191                item.setForeground(QBrush(QColor(255, 193, 7)))
192                item.setToolTip(QCoreApplication.translate("Components", "{0}\n✓ Existing {1}").format(self.relative_to_absolute[relative_path], self.label))
193            else:
194                item.setToolTip(QCoreApplication.translate("Components", "{0}\nâ—‹ No {1}").format(self.relative_to_absolute[relative_path], self.label))
195            self.file_list.addItem(item)
196
197    # === Helpers ===
198    def _update_info_label(self, visible_count):
199        info_text = QCoreApplication.translate("Components", "Showing {0} of {1} files").format(visible_count, len(self.all_nii_files))
200        if self.files_with_flag:
201            info_text += QCoreApplication.translate("Components", " ({0} with existing {1}s)").format(len(self.files_with_flag), self.label)
202        self.info_label.setText(info_text)
203        self.info_label.setStyleSheet("color: gray; font-size: 10px;")
204
205    def _apply_filters(self):
206        search_text = self.search_bar.text().lower()
207        selected_subject = self.subject_combo.currentText()
208        selected_session = self.session_combo.currentText()
209        selected_modality = self.modality_combo.currentText()
210        selected_datatype = self.datatype_combo.currentText()
211
212        show_only_no_flag = self.no_flag_checkbox.isChecked()
213        show_only_with_flag = self.with_flag_checkbox.isChecked()
214
215        visible_count = 0
216        for i in range(self.file_list.count()):
217            item = self.file_list.item(i)
218            relative_path = item.text()
219            should_show = True
220
221            if search_text and search_text not in relative_path.lower():
222                should_show = False
223            if selected_subject != QCoreApplication.translate("Components", "All subjects") and selected_subject not in relative_path:
224                should_show = False
225            if selected_session != QCoreApplication.translate("Components", "All sessions") and selected_session not in relative_path:
226                should_show = False
227            if selected_modality != QCoreApplication.translate("Components", "All modalities") and selected_modality not in relative_path:
228                should_show = False
229            if selected_datatype != QCoreApplication.translate("Components", "All types") and f"/{selected_datatype}/" not in relative_path and f"\\{selected_datatype}\\" not in relative_path:
230                should_show = False
231
232            has_flag = relative_path in self.files_with_flag
233            if show_only_no_flag and has_flag:
234                should_show = False
235            if show_only_with_flag and not has_flag:
236                should_show = False
237
238            item.setHidden(not should_show)
239            if should_show:
240                visible_count += 1
241
242        self._update_info_label(visible_count)
243
244    # === Signals ===
245    def _connect_signals(self):
246        self.search_bar.textChanged.connect(self._apply_filters)
247        self.subject_combo.currentTextChanged.connect(self._apply_filters)
248        self.session_combo.currentTextChanged.connect(self._apply_filters)
249        self.modality_combo.currentTextChanged.connect(self._apply_filters)
250        self.datatype_combo.currentTextChanged.connect(self._apply_filters)
251        self.no_flag_checkbox.toggled.connect(self._apply_filters)
252        self.with_flag_checkbox.toggled.connect(self._apply_filters)
253
254        def on_no_flag_toggled(checked):
255            if checked:
256                self.with_flag_checkbox.setChecked(False)
257        def on_with_flag_toggled(checked):
258            if checked:
259                self.no_flag_checkbox.setChecked(False)
260        self.no_flag_checkbox.toggled.connect(on_no_flag_toggled)
261        self.with_flag_checkbox.toggled.connect(on_with_flag_toggled)
262
263        self._reset_button.clicked.connect(self._reset_filters)
264        self._select_all_button.clicked.connect(self._select_all_visible)
265        self._deselect_all_button.clicked.connect(lambda: self.file_list.clearSelection())
266
267        self.buttons.accepted.connect(self._accept)
268        self.buttons.rejected.connect(self.reject)
269
270    def _reset_filters(self):
271        self.search_bar.clear()
272        self.subject_combo.setCurrentIndex(0)
273        self.session_combo.setCurrentIndex(0)
274        self.modality_combo.setCurrentIndex(0)
275        self.datatype_combo.setCurrentIndex(0)
276        self.no_flag_checkbox.setChecked(False)
277        self.with_flag_checkbox.setChecked(False)
278
279    def _select_all_visible(self):
280        for i in range(self.file_list.count()):
281            item = self.file_list.item(i)
282            if not item.isHidden():
283                item.setSelected(True)
284
285    def _accept(self):
286        selected_items = [item for item in self.file_list.selectedItems() if not item.isHidden()]
287        if not selected_items:
288            QMessageBox.warning(
289                self,
290                QCoreApplication.translate("Components", "No selection"),
291                QCoreApplication.translate("Components", "Please select at least one visible NIfTI file."))
292            log.info("No selection")
293            return
294
295        selected_files = []
296        warnings = []
297
298        for item in selected_items:
299            relative_path = item.text()
300            abs_path = self.relative_to_absolute[relative_path]
301            path_parts = abs_path.replace(self.workspace_path, '').strip(os.sep).split(os.sep)
302            subject = next((p for p in path_parts if p.startswith('sub-')), None)
303            session = next((p for p in path_parts if p.startswith('ses-')), None)
304            filename = os.path.basename(abs_path)
305            filename_noext = os.path.splitext(os.path.splitext(filename)[0])[0]
306            modality = filename_noext.split("_")[-1] or QCoreApplication.translate("Components", "Unknown")
307            datatype = next((p for p in path_parts if p in ["anat", "func", "dwi", "fmap", "perf"]), None)
308
309            has_flag = relative_path in self.files_with_flag
310            if has_flag:
311                warnings.append((abs_path, subject))
312
313            selected_files.append(abs_path)
314
315        # Show warnings if needed
316        if warnings:
317            unique_subjects = sorted({s for _, s in warnings if s})
318            msg = QMessageBox(self)
319            msg.setIcon(QMessageBox.Icon.Warning)
320            msg.setWindowTitle(QCoreApplication.translate("Components", "Existing {0} Detected").format(self.label.title()))
321            if len(unique_subjects) == 1:
322                subject_display = unique_subjects[0] or QCoreApplication.translate("Components", "this patient")
323                msg.setText(QCoreApplication.translate("Components", "A {0} already exists for {1}.").format(self.label, subject_display))
324            else:
325                msg.setText(QCoreApplication.translate("Components", "{0}s already exist for {1} patients: {2}").format(self.label.title(), len(unique_subjects), ', '.join(unique_subjects)))
326            msg.setInformativeText(QCoreApplication.translate("Components", "You can still proceed to create additional {0}s. Continue?").format(self.label))
327            msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
328            if msg.exec() == QMessageBox.StandardButton.No:
329                return
330
331        self.selected_files = selected_files
332        self.accept()
333
334    def _apply_forced_filters(self):
335        """Applica i filtri forzati definiti in self.forced_filters."""
336        if "search" in self.forced_filters:
337            self.search_bar.setText(self.forced_filters["search"])
338            self.search_bar.setEnabled(False)
339
340        def safe_set_combo_value(combo, key):
341            if key in self.forced_filters:
342                value = self.forced_filters[key]
343                index = combo.findText(value)
344                if index >= 0:
345                    combo.setCurrentIndex(index)
346                else:
347                    combo.addItem(value)
348                    combo.setCurrentIndex(combo.count() - 1)
349                combo.setEnabled(False)
350
351        safe_set_combo_value(self.subject_combo, "subject")
352        safe_set_combo_value(self.session_combo, "session")
353        safe_set_combo_value(self.modality_combo, "modality")
354        safe_set_combo_value(self.datatype_combo, "datatype")
355
356        if "no_flag" in self.forced_filters:
357            self.no_flag_checkbox.setChecked(bool(self.forced_filters["no_flag"]))
358            self.no_flag_checkbox.setEnabled(False)
359
360        if "with_flag" in self.forced_filters:
361            self.with_flag_checkbox.setChecked(bool(self.forced_filters["with_flag"]))
362            self.with_flag_checkbox.setEnabled(False)
363
364        # Riapplica i filtri
365        self._apply_filters()
log = <Logger GliAAns-UI (WARNING)>
class NiftiFileDialog(PyQt6.QtWidgets.QDialog):
 16class NiftiFileDialog(QDialog):
 17    def __init__(self, context, allow_multiple=None, has_existing_func=None, label=None, forced_filters=None):
 18        super().__init__()
 19        self.label = label
 20        self.setWindowTitle(QCoreApplication.translate("Components", "Select NIfTI Files ({0})").format(self.label))
 21        self.resize(700, 650)
 22
 23        self.workspace_path = context["workspace_path"]
 24        self.allow_multiple = allow_multiple
 25        self.has_existing_func = has_existing_func or (lambda *_: False)
 26        self.forced_filters = forced_filters or {}
 27
 28        self.selected_files = []
 29        self.relative_to_absolute = {}
 30        self.files_with_flag = set()
 31
 32        self._build_ui()
 33        self._populate_files()
 34        self._connect_signals()
 35
 36    # === Public API ===
 37    @staticmethod
 38    def get_files(context, allow_multiple=True, has_existing_func=None, label="mask",forced_filters=None):
 39        dialog = NiftiFileDialog(context, allow_multiple, has_existing_func, label,forced_filters)
 40        if dialog.exec() == QDialog.DialogCode.Accepted:
 41            return dialog.selected_files
 42        return None
 43
 44    # === UI Construction ===
 45    def _build_ui(self):
 46        layout = QVBoxLayout(self)
 47
 48        # === Filters ===
 49        filter_group = QGroupBox(QCoreApplication.translate("Components", "Filters"))
 50        filter_layout = QGridLayout(filter_group)
 51
 52        self.search_bar = QLineEdit()
 53        self.search_bar.setPlaceholderText(QCoreApplication.translate("Components", "Search files (FLAIR, T1, T2, etc.)"))
 54        self.search_bar.setClearButtonEnabled(True)
 55        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Search:")), 0, 0)
 56        filter_layout.addWidget(self.search_bar, 0, 1, 1, 3)
 57
 58        self.subject_combo = QComboBox()
 59        self.subject_combo.addItem(QCoreApplication.translate("Components", "All subjects"))
 60        self.subject_combo.model().item(0)
 61        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Subject:")), 1, 0)
 62        filter_layout.addWidget(self.subject_combo, 1, 1)
 63
 64        self.session_combo = QComboBox()
 65        self.session_combo.addItem(QCoreApplication.translate("Components", "All sessions"))
 66        self.session_combo.model().item(0)
 67        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Session:")), 1, 2)
 68        filter_layout.addWidget(self.session_combo, 1, 3)
 69
 70        self.modality_combo = QComboBox()
 71        self.modality_combo.addItem(QCoreApplication.translate("Components", "All modalities"))
 72        self.modality_combo.model().item(0)
 73        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Modality:")), 2, 0)
 74        filter_layout.addWidget(self.modality_combo, 2, 1)
 75
 76        self.datatype_combo = QComboBox()
 77        self.datatype_combo.addItem(QCoreApplication.translate("Components", "All types"))
 78        self.datatype_combo.model().item(0)
 79        filter_layout.addWidget(QLabel(QCoreApplication.translate("Components", "Data type:")), 2, 2)
 80        filter_layout.addWidget(self.datatype_combo, 2, 3)
 81
 82        self.no_flag_checkbox = QCheckBox(QCoreApplication.translate("Components", "Show only files without existing {0}s").format(self.label))
 83        self.with_flag_checkbox = QCheckBox(QCoreApplication.translate("Components", "Show only files with existing {0}s").format(self.label))
 84        if self.label is None:
 85            self.no_flag_checkbox.setVisible(False)
 86            self.with_flag_checkbox.setVisible(False)
 87        filter_layout.addWidget(self.no_flag_checkbox, 3, 0, 1, 2)
 88        filter_layout.addWidget(self.with_flag_checkbox, 3, 2, 1, 2)
 89
 90        layout.addWidget(filter_group)
 91
 92        # === Info label ===
 93        self.info_label = QLabel()
 94        layout.addWidget(self.info_label)
 95
 96        # === File list ===
 97        self.file_list = QListWidget()
 98        self.file_list.setEditTriggers(QListWidget.EditTrigger.NoEditTriggers)
 99        self.file_list.setSelectionMode(
100            QListWidget.SelectionMode.ExtendedSelection if self.allow_multiple else QListWidget.SelectionMode.SingleSelection
101        )
102        self.file_list.setAlternatingRowColors(True)
103        layout.addWidget(self.file_list)
104
105        # === Buttons ===
106        self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
107
108        reset_button = QPushButton(QCoreApplication.translate("Components", "Reset Filters"))
109        select_all_button = QPushButton(QCoreApplication.translate("Components", "Select All Visible"))
110        deselect_all_button = QPushButton(QCoreApplication.translate("Components", "Deselect All"))
111
112        self.buttons.addButton(reset_button, QDialogButtonBox.ButtonRole.ResetRole)
113        self.buttons.addButton(select_all_button, QDialogButtonBox.ButtonRole.ActionRole)
114        self.buttons.addButton(deselect_all_button, QDialogButtonBox.ButtonRole.ActionRole)
115
116        layout.addWidget(self.buttons)
117
118        # === Store buttons for handlers ===
119        self._reset_button = reset_button
120        self._select_all_button = select_all_button
121        self._deselect_all_button = deselect_all_button
122
123    # === Populate ===
124    def _populate_files(self):
125        self.all_nii_files = []
126        subjects_set, sessions_set, modalities_set, datatypes_set = set(), set(), set(), set()
127
128        for root, dirs, files in os.walk(self.workspace_path):
129            # dirs[:] = [d for d in dirs if d != "derivatives"]
130
131            for f in files:
132                if f.endswith((".nii", ".nii.gz")):
133                    full_path = os.path.join(root, f)
134                    relative_path = os.path.relpath(full_path, self.workspace_path)
135                    self.all_nii_files.append(relative_path)
136                    self.relative_to_absolute[relative_path] = full_path
137
138                    path_parts = relative_path.split(os.sep)
139
140                    # subject
141                    for part in path_parts:
142                        if part.startswith("sub-"):
143                            subjects_set.add(part)
144                            break
145
146                    # session
147                    for part in path_parts:
148                        if part.startswith("ses-"):
149                            sessions_set.add(part)
150                            break
151
152                    # modality
153                    filename_noext = os.path.splitext(os.path.splitext(f)[0])[0]
154                    modality = filename_noext.split("_")[-1] or QCoreApplication.translate("Components", "Unknown")
155                    modalities_set.add(modality)
156
157                    # existing flag (mask/skull strip)
158                    if self.has_existing_func(full_path, self.workspace_path):
159                        self.files_with_flag.add(relative_path)
160
161        # --- Cerca data type directories ---
162        for subject_dir in sorted(Path(self.workspace_path).glob("sub-*")):
163            if subject_dir.is_dir():
164                # Caso: sub-*/anat, sub-*/func, ecc.
165                for child in subject_dir.iterdir():
166                    if child.is_dir() and not child.name.startswith("ses-"):
167                        datatypes_set.add(child.name)
168
169                # Caso: sub-*/ses-*/anat, sub-*/ses-*/func, ecc.
170                for ses_dir in subject_dir.glob("ses-*"):
171                    if ses_dir.is_dir():
172                        for child in ses_dir.iterdir():
173                            if child.is_dir():
174                                datatypes_set.add(child.name)
175
176        self.subject_combo.addItems(sorted(subjects_set))
177        self.session_combo.addItems(sorted(sessions_set))
178        self.modality_combo.addItems(sorted(modalities_set))
179        self.datatype_combo.addItems(sorted(datatypes_set))
180
181        self._update_info_label(len(self.all_nii_files))
182        self._populate_file_list()
183
184        if self.forced_filters:
185            self._apply_forced_filters()
186
187    def _populate_file_list(self):
188        self.file_list.clear()
189        for relative_path in sorted(self.all_nii_files):
190            item = QListWidgetItem(relative_path)
191            if relative_path in self.files_with_flag:
192                item.setForeground(QBrush(QColor(255, 193, 7)))
193                item.setToolTip(QCoreApplication.translate("Components", "{0}\n✓ Existing {1}").format(self.relative_to_absolute[relative_path], self.label))
194            else:
195                item.setToolTip(QCoreApplication.translate("Components", "{0}\nâ—‹ No {1}").format(self.relative_to_absolute[relative_path], self.label))
196            self.file_list.addItem(item)
197
198    # === Helpers ===
199    def _update_info_label(self, visible_count):
200        info_text = QCoreApplication.translate("Components", "Showing {0} of {1} files").format(visible_count, len(self.all_nii_files))
201        if self.files_with_flag:
202            info_text += QCoreApplication.translate("Components", " ({0} with existing {1}s)").format(len(self.files_with_flag), self.label)
203        self.info_label.setText(info_text)
204        self.info_label.setStyleSheet("color: gray; font-size: 10px;")
205
206    def _apply_filters(self):
207        search_text = self.search_bar.text().lower()
208        selected_subject = self.subject_combo.currentText()
209        selected_session = self.session_combo.currentText()
210        selected_modality = self.modality_combo.currentText()
211        selected_datatype = self.datatype_combo.currentText()
212
213        show_only_no_flag = self.no_flag_checkbox.isChecked()
214        show_only_with_flag = self.with_flag_checkbox.isChecked()
215
216        visible_count = 0
217        for i in range(self.file_list.count()):
218            item = self.file_list.item(i)
219            relative_path = item.text()
220            should_show = True
221
222            if search_text and search_text not in relative_path.lower():
223                should_show = False
224            if selected_subject != QCoreApplication.translate("Components", "All subjects") and selected_subject not in relative_path:
225                should_show = False
226            if selected_session != QCoreApplication.translate("Components", "All sessions") and selected_session not in relative_path:
227                should_show = False
228            if selected_modality != QCoreApplication.translate("Components", "All modalities") and selected_modality not in relative_path:
229                should_show = False
230            if selected_datatype != QCoreApplication.translate("Components", "All types") and f"/{selected_datatype}/" not in relative_path and f"\\{selected_datatype}\\" not in relative_path:
231                should_show = False
232
233            has_flag = relative_path in self.files_with_flag
234            if show_only_no_flag and has_flag:
235                should_show = False
236            if show_only_with_flag and not has_flag:
237                should_show = False
238
239            item.setHidden(not should_show)
240            if should_show:
241                visible_count += 1
242
243        self._update_info_label(visible_count)
244
245    # === Signals ===
246    def _connect_signals(self):
247        self.search_bar.textChanged.connect(self._apply_filters)
248        self.subject_combo.currentTextChanged.connect(self._apply_filters)
249        self.session_combo.currentTextChanged.connect(self._apply_filters)
250        self.modality_combo.currentTextChanged.connect(self._apply_filters)
251        self.datatype_combo.currentTextChanged.connect(self._apply_filters)
252        self.no_flag_checkbox.toggled.connect(self._apply_filters)
253        self.with_flag_checkbox.toggled.connect(self._apply_filters)
254
255        def on_no_flag_toggled(checked):
256            if checked:
257                self.with_flag_checkbox.setChecked(False)
258        def on_with_flag_toggled(checked):
259            if checked:
260                self.no_flag_checkbox.setChecked(False)
261        self.no_flag_checkbox.toggled.connect(on_no_flag_toggled)
262        self.with_flag_checkbox.toggled.connect(on_with_flag_toggled)
263
264        self._reset_button.clicked.connect(self._reset_filters)
265        self._select_all_button.clicked.connect(self._select_all_visible)
266        self._deselect_all_button.clicked.connect(lambda: self.file_list.clearSelection())
267
268        self.buttons.accepted.connect(self._accept)
269        self.buttons.rejected.connect(self.reject)
270
271    def _reset_filters(self):
272        self.search_bar.clear()
273        self.subject_combo.setCurrentIndex(0)
274        self.session_combo.setCurrentIndex(0)
275        self.modality_combo.setCurrentIndex(0)
276        self.datatype_combo.setCurrentIndex(0)
277        self.no_flag_checkbox.setChecked(False)
278        self.with_flag_checkbox.setChecked(False)
279
280    def _select_all_visible(self):
281        for i in range(self.file_list.count()):
282            item = self.file_list.item(i)
283            if not item.isHidden():
284                item.setSelected(True)
285
286    def _accept(self):
287        selected_items = [item for item in self.file_list.selectedItems() if not item.isHidden()]
288        if not selected_items:
289            QMessageBox.warning(
290                self,
291                QCoreApplication.translate("Components", "No selection"),
292                QCoreApplication.translate("Components", "Please select at least one visible NIfTI file."))
293            log.info("No selection")
294            return
295
296        selected_files = []
297        warnings = []
298
299        for item in selected_items:
300            relative_path = item.text()
301            abs_path = self.relative_to_absolute[relative_path]
302            path_parts = abs_path.replace(self.workspace_path, '').strip(os.sep).split(os.sep)
303            subject = next((p for p in path_parts if p.startswith('sub-')), None)
304            session = next((p for p in path_parts if p.startswith('ses-')), None)
305            filename = os.path.basename(abs_path)
306            filename_noext = os.path.splitext(os.path.splitext(filename)[0])[0]
307            modality = filename_noext.split("_")[-1] or QCoreApplication.translate("Components", "Unknown")
308            datatype = next((p for p in path_parts if p in ["anat", "func", "dwi", "fmap", "perf"]), None)
309
310            has_flag = relative_path in self.files_with_flag
311            if has_flag:
312                warnings.append((abs_path, subject))
313
314            selected_files.append(abs_path)
315
316        # Show warnings if needed
317        if warnings:
318            unique_subjects = sorted({s for _, s in warnings if s})
319            msg = QMessageBox(self)
320            msg.setIcon(QMessageBox.Icon.Warning)
321            msg.setWindowTitle(QCoreApplication.translate("Components", "Existing {0} Detected").format(self.label.title()))
322            if len(unique_subjects) == 1:
323                subject_display = unique_subjects[0] or QCoreApplication.translate("Components", "this patient")
324                msg.setText(QCoreApplication.translate("Components", "A {0} already exists for {1}.").format(self.label, subject_display))
325            else:
326                msg.setText(QCoreApplication.translate("Components", "{0}s already exist for {1} patients: {2}").format(self.label.title(), len(unique_subjects), ', '.join(unique_subjects)))
327            msg.setInformativeText(QCoreApplication.translate("Components", "You can still proceed to create additional {0}s. Continue?").format(self.label))
328            msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
329            if msg.exec() == QMessageBox.StandardButton.No:
330                return
331
332        self.selected_files = selected_files
333        self.accept()
334
335    def _apply_forced_filters(self):
336        """Applica i filtri forzati definiti in self.forced_filters."""
337        if "search" in self.forced_filters:
338            self.search_bar.setText(self.forced_filters["search"])
339            self.search_bar.setEnabled(False)
340
341        def safe_set_combo_value(combo, key):
342            if key in self.forced_filters:
343                value = self.forced_filters[key]
344                index = combo.findText(value)
345                if index >= 0:
346                    combo.setCurrentIndex(index)
347                else:
348                    combo.addItem(value)
349                    combo.setCurrentIndex(combo.count() - 1)
350                combo.setEnabled(False)
351
352        safe_set_combo_value(self.subject_combo, "subject")
353        safe_set_combo_value(self.session_combo, "session")
354        safe_set_combo_value(self.modality_combo, "modality")
355        safe_set_combo_value(self.datatype_combo, "datatype")
356
357        if "no_flag" in self.forced_filters:
358            self.no_flag_checkbox.setChecked(bool(self.forced_filters["no_flag"]))
359            self.no_flag_checkbox.setEnabled(False)
360
361        if "with_flag" in self.forced_filters:
362            self.with_flag_checkbox.setChecked(bool(self.forced_filters["with_flag"]))
363            self.with_flag_checkbox.setEnabled(False)
364
365        # Riapplica i filtri
366        self._apply_filters()

QDialog(parent: QWidget|None = None, flags: Qt.WindowType = Qt.WindowFlags())

NiftiFileDialog( context, allow_multiple=None, has_existing_func=None, label=None, forced_filters=None)
17    def __init__(self, context, allow_multiple=None, has_existing_func=None, label=None, forced_filters=None):
18        super().__init__()
19        self.label = label
20        self.setWindowTitle(QCoreApplication.translate("Components", "Select NIfTI Files ({0})").format(self.label))
21        self.resize(700, 650)
22
23        self.workspace_path = context["workspace_path"]
24        self.allow_multiple = allow_multiple
25        self.has_existing_func = has_existing_func or (lambda *_: False)
26        self.forced_filters = forced_filters or {}
27
28        self.selected_files = []
29        self.relative_to_absolute = {}
30        self.files_with_flag = set()
31
32        self._build_ui()
33        self._populate_files()
34        self._connect_signals()
label
workspace_path
allow_multiple
has_existing_func
forced_filters
selected_files
relative_to_absolute
files_with_flag
@staticmethod
def get_files( context, allow_multiple=True, has_existing_func=None, label='mask', forced_filters=None):
37    @staticmethod
38    def get_files(context, allow_multiple=True, has_existing_func=None, label="mask",forced_filters=None):
39        dialog = NiftiFileDialog(context, allow_multiple, has_existing_func, label,forced_filters)
40        if dialog.exec() == QDialog.DialogCode.Accepted:
41            return dialog.selected_files
42        return None