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()
@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