from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog ) from PyQt6.QtCore import Qt, QThread, pyqtSignal from os import walk, path as Path import os from app.core.main_manager import MainManager, NotificationType from app.utils.paths import get_current_dir from app.ui.widgets.drag_drop_frame import DragDropFrame from app.ui.widgets.loading_bar import LoadingBar class ImportWorker(QThread): progress_changed = pyqtSignal(int) phase_changed = pyqtSignal(str) import_finished = pyqtSignal() error_occurred = pyqtSignal(str) dicoms_imported = pyqtSignal(int) # Signal for number of imported DICOMs def __init__(self, dicom_manager, dicom_paths): super().__init__() self.dicom_manager = dicom_manager self.dicom_paths = dicom_paths self.current_progress = 0 def run(self): total_paths = len(self.dicom_paths) # Phase 1: Path processing (0-20%) self.phase_changed.emit("processing_paths") for i, dicom_path in enumerate(self.dicom_paths): self.current_progress = int((i / total_paths) * 20) self.progress_changed.emit(self.current_progress) # Phase 2: DICOM object creation (20-70%) - Now much faster self.phase_changed.emit("creating_dicoms") self.dicom_manager.error_occurred.connect(self.error_occurred.emit) successfully_added = 0 for i, dicom_path in enumerate(self.dicom_paths): try: dicom = self.dicom_manager.process_dicomdir(dicom_path) if dicom is not None and not dicom.is_empty(): self.dicom_manager.dicoms.append(dicom) successfully_added += 1 except Exception as e: self.error_occurred.emit("load_error") self.current_progress = 20 + int((i / total_paths) * 50) self.progress_changed.emit(self.current_progress) try: self.dicom_manager.error_occurred.disconnect() except: pass self.dicoms_imported.emit(successfully_added) # Phase 3: Background validation (70-90%) self.phase_changed.emit("validating_files") self._validate_files_in_background() # Phase 4: UI preparation (90-100%) self.phase_changed.emit("preparing_display") for i in range(10): self.current_progress = 90 + i + 1 self.progress_changed.emit(self.current_progress) self.msleep(10) self.phase_changed.emit("import_complete") self.import_finished.emit() def _validate_files_in_background(self): """Validate DICOM files in background without blocking UI""" all_files = [] # Collect all DicomFile objects for dicom in self.dicom_manager.dicoms: if dicom is None: continue all_files.extend(dicom.get_files()) for subfolder in dicom.get_subfolders(): all_files.extend(subfolder.get_files()) total_files = len(all_files) invalid_count = 0 for i, dicom_file in enumerate(all_files): # Trigger lazy loading/validation dicom_file._ensure_loaded() if not dicom_file.is_valid: invalid_count += 1 # Update progress if i % 10 == 0: progress = 70 + int((i / total_files) * 20) self.progress_changed.emit(progress) # Log validation results if needed if invalid_count > 0: print(f"Warning: {invalid_count} invalid DICOM files found during validation") class ImportWindow(QWidget): def __init__(self, parent=None): super().__init__(parent) self.main_manager = MainManager.get_instance() self.language_manager = self.main_manager.get_language_manager() self.dicom_manager = self.main_manager.get_dicom_manager() self.alert_manager = self.main_manager.get_alert_manager() self.observer_manager = self.main_manager.get_observer_manager() self.observer_manager.subscribe(NotificationType.LANGUAGE, self.update_texts) self.dicom_paths = None self.setup_ui() self.update_texts() self.import_worker = None def setup_ui(self): layout = QVBoxLayout(self) layout.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) self.drag_area = DragDropFrame(self) self.drag_area.setMinimumHeight(100) self.drag_area.setMaximumHeight(200) layout.addWidget(self.drag_area) self.file_btn = QPushButton() self.file_btn.clicked.connect(self.select_file) self.folder_btn = QPushButton() self.folder_btn.clicked.connect(self.select_folder) file_layout = QHBoxLayout() file_layout.addWidget(self.file_btn) file_layout.addWidget(self.folder_btn) layout.addLayout(file_layout) self.input_entry = QLabel() self.input_entry.setText(self.language_manager.get_text("path_placeholder")) layout.addWidget(self.input_entry) layout.addStretch() self.progress_bar = LoadingBar() layout.addWidget(self.progress_bar) layout.addStretch() self.import_btn = QPushButton() self.import_btn.clicked.connect(self.start_import) layout.addWidget(self.import_btn, alignment=Qt.AlignmentFlag.AlignCenter) def update_texts(self): self.drag_area.setDragText(self.language_manager.get_text("drag_drop")) if ":" not in self.input_entry.text(): self.input_entry.setText(self.language_manager.get_text("path_placeholder")) self.file_btn.setText(self.language_manager.get_text("select_image")) self.folder_btn.setText(self.language_manager.get_text("select_folder")) self.import_btn.setText(self.language_manager.get_text("import")) def start_import(self): self.find_dicoms_from_files(self.input_entry.text()) self.import_dicoms() def import_dicoms(self): if not self.dicom_paths: self.alert_manager.show_error("no_dicom_found") return # Build a comprehensive set of all existing file paths (normalized) existing_file_paths = set() existing_dicom_paths = set() for dicom in self.dicom_manager.get_dicoms(): if dicom is None: continue # Add the DICOM's main path (normalized) if dicom.get_path(): existing_dicom_paths.add(os.path.normpath(dicom.get_path())) # Add all individual file paths within this DICOM (normalized) files = dicom.get_files() for file in files: if hasattr(file, 'file_path') and file.file_path: existing_file_paths.add(os.path.normpath(file.file_path)) # Add files from subfolders subfolders = dicom.get_subfolders() for subfolder in subfolders: subfolder_files = subfolder.get_files() for file in subfolder_files: if hasattr(file, 'file_path') and file.file_path: existing_file_paths.add(os.path.normpath(file.file_path)) # Filter out duplicates dicom_paths_filtered = [] for path in self.dicom_paths: normalized_path = os.path.normpath(path) should_skip = False # Check if this exact path is already imported if normalized_path in existing_dicom_paths: continue # For single DCM files, check if already part of existing DICOM if path.endswith('.dcm'): if normalized_path in existing_file_paths: continue # Also check if parent directory is already imported as DICOM parent_dir = os.path.normpath(os.path.dirname(path)) if parent_dir in existing_dicom_paths: continue # For directories, check more thoroughly elif Path.isdir(path): # Check if this directory path is already imported if normalized_path in existing_dicom_paths: continue # Check if any parent directory is already imported current_path = normalized_path while current_path != os.path.dirname(current_path): # Until we reach root if current_path in existing_dicom_paths: should_skip = True break current_path = os.path.dirname(current_path) if should_skip: continue # Check if any child directories are already imported for existing_path in existing_dicom_paths: if existing_path.startswith(normalized_path + os.sep): should_skip = True break if should_skip: continue # If we get here, the path is not a duplicate dicom_paths_filtered.append(path) if not dicom_paths_filtered: self.alert_manager.show_info("Tous les DICOMs sont déjà importés") return # Show loading bar and start import self.progress_bar.set_label(self.language_manager.get_text("importing")) self.progress_bar.set_progress(0) # Disable buttons during import self.file_btn.setEnabled(False) self.folder_btn.setEnabled(False) self.import_btn.setEnabled(False) # Start import worker self.import_worker = ImportWorker(self.dicom_manager, dicom_paths_filtered) self.import_worker.progress_changed.connect(self.progress_bar.set_progress) self.import_worker.phase_changed.connect(self.update_phase_label) self.import_worker.error_occurred.connect(self.handle_import_error) self.import_worker.import_finished.connect(self.on_import_finished) self.import_worker.start() def handle_import_error(self, error_key): """Handle errors during import""" self.alert_manager.show_error(error_key) def update_phase_label(self, phase): """Update the loading bar label based on current phase""" self.progress_bar.set_label(self.language_manager.get_text(phase)) def on_import_finished(self): # Re-enable buttons self.import_btn.setEnabled(True) self.file_btn.setEnabled(True) self.folder_btn.setEnabled(True) # Reset paths self.dicom_paths = None self.input_entry.setText(self.language_manager.get_text("path_placeholder")) # Clean up worker if self.import_worker: self.import_worker.deleteLater() self.import_worker = None # Notify observers self.observer_manager.notify(NotificationType.DICOM) def select_file(self): path = QFileDialog.getOpenFileName(self, self.language_manager.get_text("select_image"), get_current_dir(), "DICOMDIR Files (DICOMDIR) ;; DICOM Files (*.dcm)")[0] if path: self.input_entry.setText(path) else: self.input_entry.setText(self.language_manager.get_text("path_placeholder")) def select_folder(self): path = QFileDialog.getExistingDirectory(self, self.language_manager.get_text("select_folder"), get_current_dir()) if path: self.input_entry.setText(path) else: self.input_entry.setText(self.language_manager.get_text("path_placeholder")) def find_dicoms_from_files(self, path): if isinstance(path, tuple): path = path[0] # Extract the file path from the tuple if needed if not path: return if Path.isdir(path): if path: fichiers_dicomdir = [] for racine, _, fichiers in walk(path): for fichier in fichiers: if fichier == 'DICOMDIR': chemin_complet = Path.join(racine, fichier) fichiers_dicomdir.append(chemin_complet) if fichier.endswith('.dcm'): chemin_complet = Path.join(racine, fichier) parent_folder = Path.dirname(chemin_complet) if parent_folder not in fichiers_dicomdir: fichiers_dicomdir.append(parent_folder) if fichiers_dicomdir: self.dicom_paths = fichiers_dicomdir self.input_entry.setText(path) else: if path.endswith('DICOMDIR'): self.dicom_paths = [path] self.input_entry.setText(path) elif path.endswith('.dcm'): # For single DCM file, use the file path directly, not the parent folder self.dicom_paths = [path] self.input_entry.setText(path)