HoDA_Radio/app/ui/windows/import_window.py

338 lines
13 KiB
Python

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)