annalyse profondement le log et le script ;
mapFromSource recois un sourceIndex valide en mode recherche et un SourceIndex invalide en mode normal
aussi il y a l'erreurLe chemin du dossier doit être une chaîne de caractères valide. lors de l'appel à update_file_list_view
réfléchis doucement à la stratégie pour résoudre les problèmes
Le chemin INDEX_DIR est valide.
Le chemin WATCHED_DIR est valide.
2024-10-23 15:49:44,285 - DEBUG - Schema defined with fields: ['creation_time', 'ext', 'modification_time', 'name', 'path', 'size']
2024-10-23 15:49:44,285 - DEBUG - Initializing FileIndexer with index_dir: C:\index_dir, rootPath: Y:\DocSoufiane
2024-10-23 15:49:44,286 - DEBUG - Beginning of init function in CustomFileSystemModelForList
2024-10-23 15:49:44,302 - DEBUG - ThumbnailCache: Initialisé avec une taille maximale de 5000 vignettes.
2024-10-23 15:49:44,312 - DEBUG - Successfully connected thumbnailReady signal to on_thumbnail_ready slot.
2024-10-23 15:49:44,312 - DEBUG - End of init function in CustomFileSystemModelForList
Le chemin . est valide.
2024-10-23 15:49:44,509 - DEBUG - Beginning of init function in CustomFileSystemModelForList
2024-10-23 15:49:44,512 - DEBUG - ThumbnailCache: Initialisé avec une taille maximale de 5000 vignettes.
2024-10-23 15:49:44,513 - DEBUG - Successfully connected thumbnailReady signal to on_thumbnail_ready slot.
2024-10-23 15:49:44,513 - DEBUG - End of init function in CustomFileSystemModelForList
2024-10-23 15:49:44,513 - DEBUG - Start of setup_tree_dock_widget function
2024-10-23 15:49:44,514 - INFO - Connection: tree_view.clicked -> on_tree_clicked
2024-10-23 15:49:44,514 - INFO - Tree root path:
2024-10-23 15:49:44,515 - DEBUG - Starting setup of the proxy model for the tree/Function setup_proxy_tree_model
2024-10-23 15:49:44,515 - INFO - Source model assigned to the tree proxy.
2024-10-23 15:49:44,516 - INFO - Filter of the tree proxy model invalidated to reflect the changes.
2024-10-23 15:49:44,516 - DEBUG - End of the setup of the proxy model for the tree/Function setup_proxy_tree_model
2024-10-23 15:49:44,518 - DEBUG - End of setup_tree_dock_widget function
2024-10-23 15:49:44,518 - DEBUG - Starting setup of the proxy model for the tree/Function setup_proxy_tree_model
2024-10-23 15:49:44,518 - DEBUG - Proxy model for the tree is already set up.
2024-10-23 15:49:44,518 - DEBUG - SearchResultsProxyModel: setSourceModel called
2024-10-23 15:49:44,519 - DEBUG - Starting setup of the proxy model for the file list./Function setup_file_list_proxy
2024-10-23 15:49:44,520 - INFO - Source model assigned to the file list proxy.
2024-10-23 15:49:44,520 - INFO - Filters applied for the file list proxy model: *.dxf *.jpg *.jpeg *.png *.bmp
2024-10-23 15:49:44,520 - INFO - Filter of the file list proxy model invalidated to reflect the changes.
2024-10-23 15:49:44,521 - DEBUG - End of the setup of the proxy model for the file list./Function setup_file_list_proxy
2024-10-23 15:49:44,521 - DEBUG - Starting the setup_ui function in FileViewer
2024-10-23 15:49:44,521 - DEBUG - Starting the setupTreeModelAndView function
2024-10-23 15:49:44,522 - DEBUG - End of the setupTreeModelAndView function
2024-10-23 15:49:44,522 - DEBUG - Starting the setupListModelAndView function
2024-10-23 15:49:44,528 - INFO - Initial root path for the file list model: (empty)
2024-10-23 15:49:44,528 - INFO - Proxy model configured with the file list model.
2024-10-23 15:49:44,528 - INFO - File list view configured with the proxy model.
2024-10-23 15:49:44,528 - INFO - File list view configured to display empty content.
2024-10-23 15:49:44,528 - INFO - Triggered showModelInfo:
2024-10-23 15:49:44,528 - DEBUG - End of the setupListModelAndView function
2024-10-23 15:49:44,528 - DEBUG - Start of setup_central_frame function
2024-10-23 15:49:44,530 - INFO - New icon size for QListView: PyQt5.QtCore.QSize(160, 120)
2024-10-23 15:49:44,531 - DEBUG - End of setup_central_frame function
2024-10-23 15:49:44,531 - DEBUG - Start of setup_preview_dock_widget function
2024-10-23 15:49:44,533 - DEBUG - End of setup_preview_dock_widget function
2024-10-23 15:49:44,533 - INFO - Connecting signals and slots:
2024-10-23 15:49:44,533 - DEBUG - [Init] Directory label initialized
2024-10-23 15:49:44,533 - DEBUG - [Layout] Directory label added to layout
2024-10-23 15:49:44,534 - DEBUG - End of the setup_ui function in FileViewer
2024-10-23 15:49:44,534 - INFO - Start setupMenu
2024-10-23 15:49:44,534 - DEBUG - Setting up the menu bar
2024-10-23 15:49:44,534 - DEBUG - Menu bar configured with File and View menus.
2024-10-23 15:49:44,534 - INFO - End setupMenu
2024-10-23 15:49:44,534 - INFO - Start setupToolbar
2024-10-23 15:49:44,535 - DEBUG - Setting up the toolbar
2024-10-23 15:49:44,536 - DEBUG - Toolbar configured
2024-10-23 15:49:44,536 - INFO - End setupToolbar
2024-10-23 15:49:44,536 - DEBUG - SearchWorker initialized with index_dir: C:\index_dir
2024-10-23 15:49:44,537 - INFO - Function update_directory_label start
2024-10-23 15:49:44,537 - DEBUG - Attempting to update directory label with text: Current folder path:
2024-10-23 15:49:44,537 - INFO - Updated directory label: Current folder path:
2024-10-23 15:49:44,537 - INFO - Function update_directory_label end
2024-10-23 15:49:44,538 - INFO - Connection: file_list.clicked -> display_selected_file
2024-10-23 15:49:44,538 - INFO - Connection: file_list.doubleClicked -> on_folder_double_clicked
2024-10-23 15:49:44,538 - DEBUG - SearchResultsProxyModel: setSourceModel called
2024-10-23 15:49:44,595 - INFO - Start of resizeEvent in CustomGraphicsView class
2024-10-23 15:49:44,595 - DEBUG - Resize event detected.
2024-10-23 15:49:44,595 - DEBUG - Initial view adjusted to show entire scene.
2024-10-23 15:49:44,595 - DEBUG - Signal connected to adjust view dynamically.
2024-10-23 15:49:44,595 - INFO - End of resizeEvent in CustomGraphicsView class
2024-10-23 15:49:44,595 - INFO - Start of showEvent in CustomGraphicsView class
2024-10-23 15:49:44,595 - DEBUG - Show event detected.
2024-10-23 15:49:44,595 - INFO - End of showEvent in CustomGraphicsView class
2024-10-23 15:49:53,324 - DEBUG - Start of on_tree_clicked
2024-10-23 15:49:53,325 - DEBUG - Start processing index: <PyQt5.QtCore.QModelIndex object at 0x000001D45EE21B00>
2024-10-23 15:49:53,325 - DEBUG - Directory path: Y:/Divers fichiers Dxf À TRIER/ARC
2024-10-23 15:49:53,325 - INFO - Updating file list model for directory: Y:/Divers fichiers Dxf À TRIER/ARC
2024-10-23 15:49:53,330 - INFO - Function update_directory_label start
2024-10-23 15:49:53,330 - DEBUG - Attempting to update directory label with text: Current folder path: Y:/Divers fichiers Dxf À TRIER/ARC
2024-10-23 15:49:53,330 - INFO - Updated directory label: Current folder path: Y:/Divers fichiers Dxf À TRIER/ARC
2024-10-23 15:49:53,330 - INFO - Function update_directory_label end
2024-10-23 15:49:53,331 - INFO - [on_tree_clicked] Current folder path updated to: Y:/Divers fichiers Dxf À TRIER/ARC
2024-10-23 15:49:53,331 - DEBUG - End of on_tree_clicked
2024-10-23 15:49:53,368 - DEBUG - ThumbnailCache: getitem appelé pour Y:/Divers fichiers Dxf À TRIER/ARC/REPARE LUM 4.5-6.5PO.DXF
2024-10-23 15:49:53,368 - DEBUG - Thumbnail retrieved from cache for: Y:/Divers fichiers Dxf À TRIER/ARC/REPARE LUM 4.5-6.5PO.DXF
2024-10-23 15:49:53,369 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D45EE229E0>
2024-10-23 15:49:53,369 - WARNING - FileListProxyModel.mapFromSource reçu un sourceIndex invalide.
2024-10-23 15:49:53,369 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D45EE22970>
2024-10-23 15:49:53,370 - WARNING - FileListProxyModel.mapFromSource reçu un sourceIndex invalide.
2024-10-23 15:49:53,370 - DEBUG - SearchResultsProxyModel: Skipping dataChanged emission, model is not active.
2024-10-23 15:49:53,370 - DEBUG - Fonction update_file_list_view: Début
2024-10-23 15:49:53,370 - ERROR - Le chemin du dossier doit être une chaîne de caractères valide.
2024-10-23 15:49:53,370 - DEBUG - ThumbnailCache: getitem appelé pour Y:/Divers fichiers Dxf À TRIER/ARC/REPARE LUM 4.5-6.5PO.DXF
2024-10-23 15:49:53,371 - DEBUG - Thumbnail retrieved from cache for: Y:/Divers fichiers Dxf À TRIER/ARC/REPARE LUM 4.5-6.5PO.DXF
2024-10-23 15:49:53,371 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D45EE22A50>
2024-10-23 15:49:53,371 - WARNING - FileListProxyModel.mapFromSource reçu un sourceIndex invalide.
2024-10-23 15:49:53,371 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D45EE22970>
2024-10-23 15:49:53,371 - WARNING - FileListProxyModel.mapFromSource reçu un sourceIndex invalide.
2024-10-23 15:49:53,371 - DEBUG - SearchResultsProxyModel: Skipping dataChanged emission, model is not active.
2024-10-23 15:49:53,372 - DEBUG - Fonction update_file_list_view: Début
2024-10-23 15:49:53,372 - ERROR - Le chemin du dossier doit être une chaîne de caractères valide.
2024-10-23 15:49:56,229 - DEBUG - Slot activated: launch_delayed_search
2024-10-23 15:49:56,230 - INFO - Search criteria modified. Waiting before launching search...
2024-10-23 15:49:56,230 - DEBUG - End of launch_delayed_search
2024-10-23 15:49:57,231 - DEBUG - Function launchSearch start
2024-10-23 15:49:57,231 - DEBUG - Search criteria: r, criteria_type: filename
2024-10-23 15:49:57,232 - DEBUG - Current root path: Y:/Divers fichiers Dxf À TRIER/ARC
2024-10-23 15:49:57,232 - DEBUG - WATCHED_DIR: Y:\DocSoufiane
2024-10-23 15:49:57,232 - DEBUG - Search criteria set: r (type: filename)
2024-10-23 15:49:57,232 - DEBUG - Date range set: 2024-10-23 - 2024-10-23
2024-10-23 15:49:57,232 - DEBUG - Base path for search set: Y:\Divers fichiers Dxf À TRIER\ARC
2024-10-23 15:49:57,232 - DEBUG - Function launchSearch end
2024-10-23 15:49:57,233 - INFO - Starting search.
2024-10-23 15:49:57,234 - DEBUG - Root path for search: Y:\Divers fichiers Dxf À TRIER\ARC
2024-10-23 15:49:57,235 - INFO - Using file system search.
2024-10-23 15:49:57,235 - INFO - Finalizing search.
2024-10-23 15:49:57,235 - INFO - Search completed with 1 results.
2024-10-23 15:49:57,235 - DEBUG - Search finished with success: True
2024-10-23 15:49:57,236 - DEBUG - Stopping search thread.
2024-10-23 15:49:57,236 - DEBUG - Search thread stopped.
2024-10-23 15:49:57,237 - DEBUG - Search thread stopped and resources cleaned up.
2024-10-23 15:49:57,237 - DEBUG - Function handle_search_completion start
2024-10-23 15:49:57,237 - INFO - Search completed successfully.
2024-10-23 15:49:57,237 - DEBUG - Beginning of displayResults function.
2024-10-23 15:49:57,237 - DEBUG - Updating proxy model with search results.
2024-10-23 15:49:57,237 - DEBUG - SearchResultsProxyModel.setSearchResults called with 1 results.
2024-10-23 15:49:57,237 - DEBUG - setSearchResults: Model reset completed.
2024-10-23 15:49:57,237 - DEBUG - SearchResultsProxyModel set as the current model for file_list.
2024-10-23 15:49:57,242 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D45EE22970>
2024-10-23 15:49:57,243 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D45EE22AC0>
2024-10-23 15:49:57,244 - DEBUG - Logging search results.
2024-10-23 15:49:57,244 - DEBUG - Beginning of logSearchResults function.
2024-10-23 15:49:57,244 - DEBUG - Search result: y:\divers fichiers dxf à trier\arc\REPARE LUM 4.5-6.5PO.DXF
2024-10-23 15:49:57,244 - DEBUG - End of logSearchResults function.
2024-10-23 15:49:57,244 - DEBUG - End of displayResults function.
2024-10-23 15:49:57,244 - DEBUG - Function handle_search_completion end
2024-10-23 15:49:57,244 - DEBUG - End of onSearchFinished function.
2024-10-23 15:49:57,257 - DEBUG - ThumbnailCache: getitem appelé pour Y:/divers fichiers dxf à trier/ARC/repare lum 4.5-6.5po.dxf
2024-10-23 15:49:57,257 - DEBUG - Thumbnail retrieved from cache for: Y:/divers fichiers dxf à trier/ARC/repare lum 4.5-6.5po.dxf
2024-10-23 15:49:57,258 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D4628301A0>
2024-10-23 15:49:57,258 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D462830130>
2024-10-23 15:49:57,259 - DEBUG - ThumbnailCache: getitem appelé pour Y:/divers fichiers dxf à trier/ARC/repare lum 4.5-6.5po.dxf
2024-10-23 15:49:57,259 - DEBUG - Thumbnail retrieved from cache for: Y:/divers fichiers dxf à trier/ARC/repare lum 4.5-6.5po.dxf
2024-10-23 15:49:57,259 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D462830210>
2024-10-23 15:49:57,259 - DEBUG - FileListProxyModel.mapFromSource appelé avec sourceIndex: <PyQt5.QtCore.QModelIndex object at 0x000001D462830130>
import logging, os, threading, time, sys, cProfile, pstats, io, re, math
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton, QTreeView, QFileSystemModel, QListView, QDockWidget, QComboBox, QLineEdit, QDateEdit, QGraphicsView,
QGraphicsScene, QGraphicsPixmapItem, QSplitter, QLabel, QFileDialog, QGraphicsPathItem, QGraphicsItemGroup, QGraphicsTextItem, QGraphicsLineItem, QGraphicsEllipseItem, QToolBar, QAction,QToolButton, QGraphicsPolygonItem,
QMenu,QMessageBox, QHeaderView, QGraphicsItem, QGraphicsRectItem, )
from PyQt5 import QtGui
from PyQt5.QtGui import QVector2D, QPixmap, QImageReader, QImage, QPainter, QIcon, QPainterPath, QTransform, QPolygonF, QPen, QBrush, QColor, QFont, QFontMetrics
from unittest.mock import MagicMock
from PyQt5.QtCore import (
QDir, Qt, QSize, QRectF, pyqtSignal, QLineF, QTimer, QPointF, QPoint, QModelIndex, QSortFilterProxyModel, QAbstractProxyModel,QItemSelectionModel, QRegularExpression, QAbstractListModel, QFileInfo,QObject,
QThread, QDate, QMetaObject, QSemaphore, QThreadPool, QRunnable, QFileSystemWatcher,Q_ARG, pyqtSlot, QEvent, QMutex, QMutexLocker,QRecursiveMutex, QVariant)
from PyQt5.QtChart import QChart, QSplineSeries, QChartView
from PIL import Image, ImageQt
import ezdxf
import ezdxf.query
from ezdxf.entities import DXFEntity
from math import sin, cos, radians
from uuid import uuid4
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from collections import OrderedDict, deque
import matplotlib.pyplot as plt
from enum import Enum
import whoosh
from whoosh.index import create_in, open_dir, exists_in, EmptyIndexError
from whoosh.fields import Schema, TEXT, ID, DATETIME, STORED
from whoosh.qparser import QueryParser
from whoosh.query import Term, Or, DateRange, And, Every, FuzzyTerm, Prefix
from whoosh.writing import AsyncWriter
from datetime import datetime
import win32api,win32con
import shutil

Définition des constantes
MAX_RETRIES = 3 # Nombre maximum de tentatives pour traiter un fichier
INDEX_DIR = os.path.normpath(r"C:\index_dir") # Chemin d'accès centralisé à l'index Whoosh
if os.path.exists(INDEX_DIR):
print("Le chemin INDEX_DIR est valide.")
# Ajoutez votre logique pour surveiller le dossier ici
else:
print("Le chemin INDEX_DIR n'est pas valide.")
WATCHED_DIR = os.path.normpath(r"Y:\DocSoufiane")# Constante pour le répertoire spécifique à surveiller
if os.path.exists(WATCHED_DIR):
print("Le chemin WATCHED_DIR est valide.")
# Ajoutez votre logique pour surveiller le dossier ici
else:
print("Le chemin WATCHED_DIR n'est pas valide.")
ALLOWED_EXTENSIONS = ['.dxf', '.png', '.jpg', '.jpeg', '.bmp']

Setup logging
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

def handle_uncaught_exception(exc_type, exc_value, exc_traceback):
"""Global exception handler."""
logger.critical("Unhandled exception!", exc_info=(exc_type, exc_value, exc_traceback))
sys.excepthook(exc_type, exc_value, exc_traceback) # Call the default exception handler

Install the global exception handler
sys.excepthook = handle_uncaught_exception
class CancelledError(Exception):
pass
class RequestState(Enum):
PENDING = 'pending'
IN_PROGRESS = 'in_progress'
PROCESSED = 'processed'
class RequestThread(QThread):
"""Thread dédié à la gestion de la file d'attente des requêtes de vignettes."""
thumbnailRequested = pyqtSignal(str) # Signal pour transmettre les requêtes au ProcessingThread


def __init__(self, parent=None):
    super().__init__(parent)
    self.request_queue = deque()
    self.lock = QMutex()
def run(self):
    """Boucle principale du thread.
    Attend les demandes de vignettes et les transmet au thread de traitement.
    """
    while True:
        file_path = None
        with QMutexLocker(self.lock):  # Verrouiller l'accès à la file d'attente
            if self.request_queue:
                file_path = self.request_queue.popleft()

        if file_path:
            logger.debug(f"RequestThread: Sending thumbnail request for {file_path}")
            self.thumbnailRequested.emit(file_path)  # Émettre le signal pour demander la vignette
        else:
            time.sleep(0.1)  # Pause de 100 ms

@pyqtSlot(str)
def enqueue_request(self, file_path):
    """Ajoute une requête de vignette à la file d'attente.
    Args:
        file_path (str): Chemin du fichier pour lequel une vignette est demandée.
    """
    with QMutexLocker(self.lock):  # Verrouiller l'accès à la file d'attente
        if file_path not in self.request_queue:
            self.request_queue.append(file_path)
            logger.debug(f"RequestThread: Request queued for {file_path}. Queue: {list(self.request_queue)}")
class ProcessingThread(QThread):
"""Thread dédié au traitement des requêtes de vignettes et à la gestion du cache."""
thumbnailReady = pyqtSignal(str, QPixmap, bool) # Signal pour notifier la disponibilité des vignettes
requestProcessed = pyqtSignal(str, bool) # Signal pour signaler le traitement d'une requête


def __init__(self, thumbnail_manager, max_workers=10, parent=None):
    super().__init__(parent)
    self.thumbnail_manager = thumbnail_manager  # Référence au ThumbnailManager
    self.max_workers = max_workers
    self.request_queue = deque()
    self.lock = QMutex()
    self.pending_requests_lock = QMutex()  # Verrou spécifique pour pending_requests
    self.semaphore = QSemaphore(max_workers)  # Sémaphore pour limiter le nombre de workers
    self.thumbnail_manager.requestThumbnail.connect(self.handle_thumbnail_request)
    self.pending_requests = set() 

def run(self):
    """Boucle principale du thread.
    
    Traite les requêtes de vignettes en lançant des ThumbnailWorker.
    """
    while True:
        file_path = None
        with QMutexLocker(self.lock):  # Verrouiller l'accès à la file d'attente
            if self.request_queue:
                file_path = self.request_queue.popleft()

        if file_path:
            with QMutexLocker(self.pending_requests_lock):  # Protéger l'accès à pending_requests
                if file_path not in self.pending_requests:
                    self.pending_requests.add(file_path)
                    if self.semaphore.tryAcquire():  # Acquérir un sémaphore si disponible
                        logger.debug(f"ProcessingThread: Processing request for {file_path}")
                        worker = ThumbnailWorker(file_path, self.cache)  # Passer le cache ici
                        worker.signals.thumbnailReady.connect(self.thumbnailReady.emit)  # Connecter le signal du worker
                        worker.signals.finished.connect(self.worker_finished)
                        worker.signals.errorOccurred.connect(self.handle_worker_error)
                        self.thumbnail_manager.threadpool.start(worker)
                    else:
                        # Si aucun sémaphore n'est disponible, remettre la requête en file d'attente
                        with QMutexLocker(self.lock):
                            self.request_queue.appendleft(file_path)
                        logger.debug(f"ProcessingThread: No semaphore available, request for {file_path} requeued")
        else:
            # Pause pour éviter la surcharge CPU lorsque la file d'attente est vide
            time.sleep(0.1)

@pyqtSlot(str)
def handle_thumbnail_request(self, file_path):
    """Ajoute une requête de vignette à la file d'attente.     
    Args:
        file_path (str): Chemin du fichier pour lequel une vignette est demandée.
    """
    with QMutexLocker(self.lock):  # Verrouiller l'accès à la file d'attente
        # Vérifier si le fichier est déjà dans la file d'attente ou en cours de traitement
        with QMutexLocker(self.pending_requests_lock):  # Utiliser un verrou pour pending_requests
            if file_path not in self.request_queue and file_path not in self.pending_requests:
                self.request_queue.append(file_path)
                logger.debug(f"ProcessingThread: Request queued for {file_path}. Queue: {list(self.request_queue)}")

@pyqtSlot()
def worker_finished(self, file_path):
    """Appelée lorsque le travailleur a terminé, pour libérer le sémaphore et mettre à jour les requêtes en attente."""
    with QMutexLocker(self.pending_requests_lock):
        self.pending_requests.discard(file_path)
    self.semaphore.release()
@pyqtSlot(str, Exception)
def handle_worker_error(self, file_path, error):
    """Gérer les erreurs survenues dans le travailleur."""
    logger.error(f"Error processing {file_path}: {error}")
    # Retirer le fichier des requêtes en attente même en cas d'erreur
    with QMutexLocker(self.pending_requests_lock):
        self.pending_requests.discard(file_path)
    self.semaphore.release()
class ThumbnailManager(QObject):
"""Gère la coordination des vignettes, le cache et la communication entre les threads."""


thumbnailReady = pyqtSignal(str, QPixmap, bool)  # Chemin du fichier, vignette (ou None), succès
requestThumbnail = pyqtSignal(str)  

def __init__(self, parent=None, max_workers=10, cache_size=5000):
    super().__init__(parent)
    self.cache = ThumbnailCache(cache_size)
    self.request_thread = RequestThread()
    self.processing_thread = ProcessingThread(self)
    self.request_thread.thumbnailRequested.connect(self.processing_thread.handle_thumbnail_request)
    self.request_thread.start()
    self.processing_thread.start()

    self.threadpool = QThreadPool()
    self.threadpool.setMaxThreadCount(max_workers)
    self.lock = QMutex()  # Verrou pour protéger l'accès au cache et aux workers
    self.workers = {}  # Dictionnaire pour suivre les workers actifs

@pyqtSlot(str, QPixmap, bool)
def handle_thumbnail_ready(self, file_path, thumbnail, success):
    """Gère la réception des vignettes générées par les workers."""
    with QMutexLocker(self.lock):
        if success:
            #logger.debug(f"handle_thumbnail_ready: Mise en cache de la vignette pour {file_path}")
            self.cache[file_path] = thumbnail  # Mettre en cache la vignette
        if file_path in self.workers:
            del self.workers[file_path]  # Supprimer le worker terminé
            #logger.debug(f"handle_thumbnail_ready: Worker supprimé pour {file_path}")

    #logger.debug(f"handle_thumbnail_ready: Émission du signal thumbnailReady pour {file_path}")
    self.thumbnailReady.emit(file_path, thumbnail, success)  # Émettre le signal pour mettre à jour la vue

def start_worker(self, file_path):
    """Lance un worker pour générer une vignette pour le fichier donné."""
    with QMutexLocker(self.lock):
        if file_path in self.cache:  # Vérifier si la vignette est déjà dans le cache
            #logger.debug(f"ThumbnailManager: Thumbnail found in cache for {file_path}")
            self.thumbnailReady.emit(file_path, self.cache[file_path], True)
            return

        if file_path in self.workers:  # Vérifier si un worker est déjà en cours pour ce fichier
            logger.debug(f"ThumbnailManager: Thumbnail generation already in progress for {file_path}")
            return

        #logger.debug(f"ThumbnailManager: Starting thumbnail worker for {file_path}")
        worker = ThumbnailWorker(file_path, self.cache)
        worker.signals.thumbnailReady.connect(self.handle_thumbnail_ready)
        worker.signals.finished.connect(self.worker_finished)
        self.workers[file_path] = worker  # Ajouter le worker au dictionnaire
        self.threadpool.start(worker)

@pyqtSlot()
def worker_finished(self):
    """Slot appelé lorsqu'un worker a terminé."""
    worker = self.sender()
    if worker is not None and hasattr(worker, 'file_path'):  # Check if worker is valid and has file_path
        file_path = worker.file_path
        logger.debug(f"ThumbnailManager: Worker finished for {file_path}")
        with QMutexLocker(self.lock):
            if file_path in self.workers:
                del self.workers[file_path]
                logger.debug(f"worker_finished: Worker supprimé pour {file_path}")
            else:
                logger.warning("ThumbnailManager: Worker finished but sender is None or doesn't have file_path")
class ThumbnailCache(OrderedDict):
"""Cache pour les vignettes générées."""
def init(self, cache_size=5000):
super().init()
self.cache_size = cache_size
logger.debug(f"ThumbnailCache: Initialisé avec une taille maximale de {cache_size} vignettes.")


def __getitem__(self, key):
    logger.debug(f"ThumbnailCache: __getitem__ appelé pour {key}")
    try:
        value = super().__getitem__(key)
        self.move_to_end(key)
        logger.debug(f"Thumbnail retrieved from cache for: {key}")
        return value
    except KeyError:
        logger.warning(f"ThumbnailCache: Vignette non trouvée dans le cache pour {key}")
        raise  # Relève l'exception pour que le code appelant puisse la gérer

def get(self, key):
    #logger.debug(f"ThumbnailCache: get appelé pour {key}")
    value = super().pop(key, None)
    if value is not None:
        self[key] = value  # Remettre à la fin pour indiquer un accès récent
        #logger.debug(f"(ThumbnailCache get): Thumbnail found in cache for: {key}")
    #else:
        #logger.debug(f"(ThumbnailCache get): Thumbnail not found in cache for: {key}")
    return value

def __setitem__(self, key, value):
    #logger.debug(f"ThumbnailCache: __setitem__ appelé pour {key}")
    super().__setitem__(key, value)
    self.move_to_end(key)  # Déplacer l'élément à la fin (le plus récemment utilisé)
    if len(self) > self.cache_size:
        oldest = next(iter(self))
        logger.debug(f"ThumbnailCache: Suppression de l'élément le plus ancien {oldest}")
        self.pop(oldest)  # Supprimer l'élément le moins récemment utilisé
class CustomFileSystemModelForList(QFileSystemModel):
"""Modèle de système de fichiers personnalisé pour afficher les vignettes dans une vue de liste."""
searchFinished = pyqtSignal(list) # results: List[str]
rootPathChanged = pyqtSignal(str) # new_root_path: str
#dataChanged = pyqtSignal(QModelIndex, QModelIndex) # index: QModelIndex
modelUpdated = pyqtSignal(QModelIndex) # index: QModelIndex # Signal to notify that the model has been updated
directoryLoaded = pyqtSignal(str) # path: str
#thumbnailUpdated = pyqtSignal(str, QPixmap, bool) # file_path: str, thumbnail: QPixmap, success: bool
thumbnailRequested = pyqtSignal(str) #


def __init__(self, parent=None, indexer=None, max_workers=10, cache_size=5000): 
    logger.debug("Beginning of __init__ function in CustomFileSystemModelForList")
    super().__init__(parent)
    self.allowed_extensions = ALLOWED_EXTENSIONS
    self.setRootPath(os.path.normpath(WATCHED_DIR))  
    # Connexion des signaux pour le débogage
    #self.searchFinished.connect(lambda results: print(f"Search finished signal received with results: {results}"))
    #self.rootPathChanged.connect(lambda path: print(f"Root path changed signal received with path: {path}"))
    #self.dataChanged.connect(lambda top_left, bottom_right: print(f"Data changed signal received for indexes: {top_left}, {bottom_right}"))
    #self.rootPathChanged.connect(lambda path: logger.debug(f"Root path changed: {path}"))
    #self.modelUpdated.connect(lambda index: print(f"Model updated signal received for index: {index}"))

    self.thumbnailManager = ThumbnailManager(parent=self, max_workers=max_workers, cache_size=cache_size)
    # Connexion du signal thumbnailReady au slot correspondant de ThumbnailManager
    self.thumbnailManager.thumbnailReady.connect(self.on_thumbnail_ready)
    # Connexion explicite du signal thumbnailRequested
    self.thumbnailRequested.connect(self.thumbnailManager.start_worker)
    # Connexion du signal requestProcessed au slot correspondant de FileViewer
    #self.processing_thread.requestProcessed.connect(self.handle_request_processed)

    self.parent = parent
    self.file_list = QListView()
    self.file_list.setStyleSheet("QListView::item { margin: 5px; }")
    self.criteria = ""    
    self._search_timer = QTimer(self)
    self._search_timer.setSingleShot(True)
    self._last_criteria = ""
    self.indexer = indexer
    if self.indexer:
        # Connect the indexUpdated signal with an anonymous lambda function to provide rootPath
        self.indexer.indexUpdated.connect(lambda: QTimer.singleShot(1000,  # Délai de 1 seconde
                                                            self.add_files_to_watcher))
    # Cache resources
    script_directory = os.path.dirname(os.path.abspath(__file__))
    icon_path = os.path.join(script_directory, "dossier.png")
    self.folder_icon = QIcon(QPixmap(icon_path))
    if self.folder_icon.isNull():
        logger.error(f"Failed to load folder icon from {icon_path}")

    #self.thumbnailManager = ThumbnailManager(parent=self, max_workers=max_workers, cache_size=cache_size) # Créer une instance de ThumbnailManager
    self.thumbnailManager.thumbnailReady.connect(self.on_thumbnail_ready)
    logger.debug("Successfully connected thumbnailReady signal to on_thumbnail_ready slot.")
    self.file_watcher = QFileSystemWatcher(self)  # Initialize file_watcher
    self.directoryLoaded.connect(self.on_directory_loaded)
    #self.file_watcher.fileChanged.connect(self.handle_file_change) # Connect to the signal
    logger.debug("End of __init__ function in CustomFileSystemModelForList")

def event(self, event):
    return super().event(event)  # Revenir à la méthode par défaut

def rowCount(self, parent=QModelIndex()):
    #logger.debug(f"rowCount called with parent: {parent}")
    return super().rowCount(parent)

def index(self, *args, **kwargs):  
    #logger.debug(f"index called with row: {row}, column: {column}, parent: {parent}")
    try:
        #result = super().index(row, column, parent)
        #logger.debug(f"CustomFileSystemModelForList.index: Returning index: {result}")
        if len(args) == 1 and isinstance(args[0], str):
            # Appel avec path  
            path = args[0]  
            column = kwargs.get('column', 0)  
            #logger.debug(f"index called with path: {path}, column: {column}")  
            return super().index(path, column)  
            #return super().index(row, column, parent)
        elif len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], int):
            # Appel avec path et column  
            path = args[0]  
            column = args[1]  
            #logger.debug(f"index called with path: {path}, column: {column}")  
            return super().index(path, column)  
        else:
            # Appel avec row, column, parent  
            return super().index(*args, **kwargs)  

    except Exception as e:
        logger.error(f"CustomFileSystemModelForList.index: An error occurred: {e}")
        raise  # Remonte l'exception pour une gestion plus haut dans le code si nécessaire
def indexFromPath(self, path):
    """Helper function to get the index from a path."""
    #logger.debug(f"Entering CustomFileSystemModelForList.indexFromPath with path: {path}")
    result = super().index(path)
    #logger.debug(f"CustomFileSystemModelForList.indexFromPath: Returning index: {result}")
    return result
def add_files_to_watcher(self):
    """
    Ajoute les fichiers du répertoire surveillé (WATCHED_DIR) et de ses sous-répertoires 
    au `QFileSystemWatcher` pour surveiller les modifications.
    """
    logger.debug(f"Entering CustomFileSystemModelForList.add_files_to_watcher")
    try:
        if not self.file_watcher:
            logger.warning("file_watcher is not initialized.")
            return

        # Assurez-vous que le répertoire surveillé est correctement normalisé
        normalized_watched_dir = os.path.normpath(WATCHED_DIR)

        # Utiliser QDir pour lister les fichiers dans le répertoire surveillé
        dir = QDir(normalized_watched_dir)
        dir.setNameFilters([f"*{ext}" for ext in self.allowed_extensions])
        dir.setFilter(QDir.Files | QDir.NoSymLinks | QDir.NoDotAndDotDot)

        # Obtenir tous les fichiers correspondants dans le répertoire et ses sous-répertoires
        file_info_list = dir.entryInfoList(QDir.AllEntries | QDir.NoDotAndDotDot, QDir.Name)

        allowed_file_paths = []
        for file_info in file_info_list:
            file_path_str = file_info.absoluteFilePath()
            #logger.debug(f"Checking file path: {file_path_str}")

            # Vérifiez si le fichier est caché ou système
            if file_info.isHidden() or (win32api.GetFileAttributes(file_path_str) & win32con.FILE_ATTRIBUTE_SYSTEM):
                logger.debug(f"  - Skipped: Is hidden or system file")
                continue

            # Vérifier l'extension du fichier
            extension = os.path.splitext(file_path_str)[1].lower()
            if extension not in self.allowed_extensions:
                logger.debug(f"  - Skipped: Extension {extension} not allowed")
                continue

            allowed_file_paths.append(file_path_str)

        if not allowed_file_paths:
            logger.warning("No valid file paths found to add to QFileSystemWatcher.")
            logger.warning(f"  - Root path: {WATCHED_DIR}")
            logger.warning(f"  - Allowed extensions: {self.allowed_extensions}")
        else:
            self.file_watcher.addPaths(allowed_file_paths)
            logger.info(f"{len(allowed_file_paths)} files added to QFileSystemWatcher.")

    except Exception as e:
        logger.critical(f"Unhandled exception in add_files_to_watcher: {e}", exc_info=True)
    logger.debug("Exiting CustomFileSystemModelForList.add_files_to_watcher")

def on_directory_loaded(self, path):
    """
    Gère les actions à effectuer après le chargement d'un répertoire.
    """
    logger.info(f"on_directory_loaded / Répertoire chargé (CustomFileSystemModelForList) : {path}")
    if self.indexer: 
        normalized_path = os.path.normpath(path)
        if normalized_path.startswith(os.path.normpath(WATCHED_DIR)):
            self.indexer.buildIndex()
            logger.debug("add_files_to_watcher.on_directory_loaded(self, path) / self.indexer.buildIndex()  called")
        #self.add_files_to_watcher(path) 
    else:
        logger.warning("CustomFileSystemModelForList/ on_directory_loaded; Indexer non disponible dans CustomFileSystemModelForList.")

def data(self, index, role=Qt.DisplayRole):
    #logger.debug(f"CustomFileSystemModelForList.data called for index {index} with role {role}")  
    if role in (Qt.SizeHintRole, Qt.FontRole):
        return None
    try:
        if not index.isValid():
            return None
        if role not in [Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole]:
            return None
        return self.standard_data_method(index, role)
    except Exception as e:
        logger.exception(f"CustomFileSystemModelForList.data: Exception occurred: {e}")
        return None
    finally:
        pass
        #logger.debug(f"CustomFileSystemModelForList.data finished for index {index} with role {role}")

def standard_data_method(self, index, role):
    """Méthode standard pour fournir les données du modèle."""

    if role in (Qt.SizeHintRole, Qt.FontRole):  # Optimisation : ignorer ces rôles
        return None
    
    file_path = self.filePath(index)
    #logger.debug(f"CustomFileSystemModelForList.standard_data_method called for {file_path}, role: {role}")
    #logger.debug(f"standard_data_method: Current thread: {QThread.currentThread()}")
    try:
        if not index.isValid():
            return None
        #logger.debug(f"Entering role check for {file_path}")
        if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole):
           #logger.debug(f"Unsupported role {role} for {file_path}, returning None")
           return None 
        #logger.debug(f"Processing role {role} for {file_path}")
        if role == Qt.DisplayRole:
            filename = os.path.basename(file_path)
            #logger.debug(f"DisplayRole: Original filename {filename}")
            if file_path in QDir.drives():
                filename = file_path
                #logger.debug(f"DisplayRole: Filename is a drive {filename}")
            longueur_max_titre = 25
            if len(filename) > longueur_max_titre:
                filename = filename[:longueur_max_titre - 3] + "..."
            return filename

        elif role == Qt.ToolTipRole:
            #logger.debug(f"ToolTipRole: Returning file path {file_path}")
            return file_path

        elif role == Qt.DecorationRole and index.column() == 0:
            #logger.debug(f"CustomFileSystemModelForList: Standard data method called ; Requesting thumbnail for {file_path} , role: {role}")
            if os.path.isdir(file_path):
                #logger.debug(f"DecorationRole: {file_path} is a directory, returning folder icon")
                return self.folder_icon
            elif file_path.lower().endswith(('.dxf', '.png', '.jpg', '.jpeg')):
                thumbnail = self.thumbnailManager.cache.get(file_path)  # Vérifier le cache
                if thumbnail is None:
                    with QMutexLocker(self.thumbnailManager.processing_thread.pending_requests_lock):  # Utiliser le verrou spécifique
                        # Vérifier les requêtes en attente dans ThumbnailManager
                        if file_path not in self.thumbnailManager.processing_thread.pending_requests: 
                            self.thumbnailManager.processing_thread.pending_requests.add(file_path)
                            if QThread.currentThread() == QApplication.instance().thread():
                                self.thumbnailRequested.emit(file_path)
                            else:
                                # Émettre le signal depuis le thread principal en utilisant un timer
                                QTimer.singleShot(0, lambda: self.thumbnailRequested.emit(file_path))
                    return QPixmap()  # Retourner une image vide en attendant
                else:
                    return thumbnail.scaled(160, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation)  # Redimensionner la vignette
            else:
                return None  # Aucun icône pour les autres types de fichiers
    except Exception as e:
        logger.exception(f"CustomFileSystemModelForList.data: Exception occurred: {e}")
        return None

@pyqtSlot(str, QPixmap, bool)
def on_thumbnail_ready(self, file_path, thumbnail, success):
    """Slot appelé lorsque la vignette est prête."""
    #logger.debug(f"CustomFileSystemModelForList: on_thumbnail_ready() - file_path: {file_path}, success: {success}")
    if success:
        self.update_thumbnail_cache(file_path, thumbnail)
    else:
        logger.warning(f"on_thumbnail_ready: Échec de la génération de la vignette pour {file_path}")
@pyqtSlot(str, bool)
def handle_request_processed(self, file_path, success):
    """Gère le signal requestProcessed du ProcessingThread."""
    if not success:
        # Gérez l'échec de la génération de la vignette (réessayez, affichez un message d'erreur, etc.)
        logger.error(f"CustomFileSystemModelForList: Failed to generate thumbnail for {file_path}")
    else:
        logger.debug(f"CustomFileSystemModelForList: Thumbnail successfully generated for {file_path}")

@pyqtSlot(str, QPixmap)
def update_thumbnail_cache(self, file_path, thumbnail):
    """Met à jour le cache des vignettes et émet le signal dataChanged dans le thread principal."""
    #logger.debug(f"CustomFileSystemModelForList: update_thumbnail_cache() - Entrée avec file_path: {file_path}")
    
    # Utiliser QMutexLocker pour assurer le déverrouillage automatique
    with QMutexLocker(self.thumbnailManager.lock):
        #logger.debug(f"CustomFileSystemModelForList: update_thumbnail_cache() - Verrou acquis")
        if file_path not in self.thumbnailManager.cache or self.thumbnailManager.cache[file_path] != thumbnail:
            #logger.debug(f"CustomFileSystemModelForList: update_thumbnail_cache() - Vignette absente du cache ou différente, mise à jour en cours...")
            self.thumbnailManager.cache[file_path] = thumbnail
            #logger.debug(f"CustomFileSystemModelForList: update_thumbnail_cache() - Vignette mise à jour dans le cache pour {file_path}")
        else:
            logger.debug(f"CustomFileSystemModelForList: update_thumbnail_cache() - Vignette déjà présente dans le cache, aucune mise à jour nécessaire.")

    # Émettre le signal dataChanged pour indiquer que les données ont changé
    index = self.indexFromPath(file_path)
    if index.isValid():
        #logger.debug(f"CustomFileSystemModelForList: update_thumbnail_cache() - Émission du signal dataChanged pour {file_path}")  
        self.dataChanged.emit(index, index, [Qt.DecorationRole])  # Émettre le signal dataChanged pour l'élément spécifique
        #logger.debug(f"CustomFileSystemModelForList: update_thumbnail_cache() - Signal dataChanged émis pour {file_path}")
    else:
        logger.warning(f"CustomFileSystemModelForList: update_thumbnail_cache() - Index invalide pour {file_path}")
class CustomFileSystemModelForTree(QFileSystemModel):
def init(self, *args, **kwargs):
super().init(*args, **kwargs)
# Cache the folder icon for performance
script_directory = os.path.dirname(os.path.abspath(file))
icon_path = os.path.join(script_directory, "dossier.png")
self.folder_icon = QIcon(QPixmap(icon_path))
if self.folder_icon.isNull():
logger.error(f"Failed to load folder icon from {icon_path}")


def data(self, index, role):
    if role == Qt.DecorationRole and index.column() == 0:
        # Get the full path of the file or directory at the index
        file_path = self.filePath(index)
        # Check if it's a directory
        if os.path.isdir(file_path):
            return self.folder_icon
    elif role == Qt.DisplayRole:
        # Get the default display text
        original_text = super().data(index, role)
        return original_text
    return None
Background search worker class
class SearchWorker(QObject):
searchFinished = pyqtSignal(bool, list) # Signal to indicate search success


def __init__(self, indexer, index_dir):
    super().__init__()
    self.running = False
    self.running_mutex = QMutex()  # Mutex to protect self.running
    self.rootPath = ""
    self.criteria = ""
    self.criteria_type = "filename"
    self.start_date = None
    self.end_date = None
    self.allowed_extensions = ALLOWED_EXTENSIONS
    self.extension_pattern = re.compile(r".*(" + "|".join([re.escape(ext) + "$" for ext in self.allowed_extensions]) + ")", re.IGNORECASE)
    self.indexer = indexer  
    self.index_dir = os.path.normpath(index_dir)
    logger.debug(f"SearchWorker initialized with index_dir: {self.index_dir}")

def stop(self):
    """Stop the ongoing search gracefully."""
    self.running_mutex.lock()
    running = self.running
    self.running_mutex.unlock()

    if not running:
        logger.info("Search interrupted by user.")
        return  # Exit if the search was not running

    logger.info("Search stopped by user.")
    self.running = False  # This will cause the search loop to exit

def setCriteria(self, criteria, criteria_type="filename"):
    """Sets the search criteria and its type."""
    self.criteria = criteria.lower()  # Convert criteria to lowercase
    self.criteria_type = criteria_type
    logger.debug(f"Search criteria set: {criteria} (type: {criteria_type})")

def setBasePath(self, path):
    self.rootPath = os.path.normpath(path)
    logger.debug(f"Base path for search set: {self.rootPath}")

def setDateRange(self, start_date, end_date):
    self.start_date = start_date.toPyDate()
    self.end_date = end_date.toPyDate()
    logger.debug(f"Date range set: {self.start_date} - {self.end_date}")

    # Pre-calculate timestamps for date comparison
    self.start_date_timestamp = datetime.combine(self.start_date, datetime.min.time()).timestamp()
    self.end_date_timestamp = datetime.combine(self.end_date, datetime.max.time()).timestamp()

def performSearch(self, retry=False):
    logger.info("Starting search.")
    logger.debug(f"Root path for search: {self.rootPath}")

    # Verify root path
    if not self.rootPath or not os.path.exists(self.rootPath):
        logger.error("Base path not set or does not exist.")
        self.searchFinished.emit(False, [])
        return

    # Validate dates
    if self.start_date and self.end_date and self.start_date > self.end_date:
        logger.error("Start date must be before end date.")
        self.searchFinished.emit(False, [])
        return

    self.running_mutex.lock()  # Lock before modifying
    self.running = True
    self.running_mutex.unlock()  # Unlock after modification

    results = []
    criteria_lower = self.criteria.lower().strip()
    if not criteria_lower:
        logger.error("Search criteria is empty after processing. Aborting search.")
        self.searchFinished.emit(False, [])
        return

    try:
        # Check if within watched directory
        normalized_root_path = os.path.normcase(os.path.normpath(self.rootPath))
        normalized_watched_dir = os.path.normcase(os.path.normpath(WATCHED_DIR))

        if os.path.commonpath([normalized_root_path, normalized_watched_dir]) == normalized_watched_dir:
            logger.info("Using index-based search.")
            self.index_based_search(results, criteria_lower, retry)
        else:
            logger.info("Using file system search.")
            self.scan_directory(normalized_root_path, results, criteria_lower)

        #logger.debug(f"Final search results: {results}")
    except Exception as e:
        logger.error(f"Error during search: {e}")
        self.searchFinished.emit(False, [])
        return
    finally:
        logger.info("Finalizing search.")
        self.running_mutex.lock()
        self.running = False
        self.running_mutex.unlock()
        self.searchFinished.emit(True, results)
        logger.info(f"Search completed with {len(results)} results.")

def index_based_search(self, results, criteria_lower, retry):
    try:
        logger.info("Performing index-based search.")
        ix = open_dir(self.index_dir)
        with ix.searcher() as searcher:
            query_parts = []

            # Build query based on criteria type
            if self.criteria_type == "filename":
                if not criteria_lower.strip():
                    raise ValueError("Search criteria for filename is empty.")
                query_part = Prefix("name", criteria_lower)
                query_parts.append(query_part)
            elif self.criteria_type in ("creation_date", "modification_date"):
                date_field = "creation_time" if self.criteria_type == "creation_date" else "modification_time"
                query_parts.append(DateRange(date_field, self.start_date, self.end_date))

            # Filter by extensions using regex
            if self.allowed_extensions:  # Check if extensions are specified
                query_parts.append(self.extension_pattern)

            # Filter by path
            query_parts.append(Prefix("path", os.path.normcase(os.path.normpath(self.rootPath))))
            query = And(query_parts)

            # Execute the search
            hits = searcher.search(query, limit=None)
            for hit in hits:
                self.running_mutex.lock()  # Protect access to self.running
                running = self.running
                self.running_mutex.unlock()
                if not running:
                    break
                results.append(hit["path"])
    except EmptyIndexError:
        if not retry:
            logger.warning("Index directory is empty, creating index...")
            self.indexer.buildIndex()
            return self.performSearch(retry=True)  # Retry after rebuilding index
        else:
            logger.error("Index is still empty after rebuilding.")
            self.searchFinished.emit(False, [])
    except Exception as e:
        logger.error(f"Error during Whoosh search: {e}")
        self.searchFinished.emit(False, [])

def scan_directory(self, directory, results, criteria_lower):
    """Recursively scan the directory for files matching the criteria."""
    for entry in os.scandir(directory):
        if entry.is_dir():
            self.scan_directory(entry.path, results, criteria_lower)  # Recursive call for subdirectories
        elif entry.is_file():
            self.process_file(entry.path, entry.name, results, criteria_lower)

def process_file(self, fullPath, file, results, criteria_lower):
    try:
        file_lower = file.lower()
        _, extension = os.path.splitext(fullPath)

        # Check extension using regex
        if not self.extension_pattern.match(fullPath):
            return

        # Check criteria
        if self.criteria_type == "filename" and criteria_lower in file_lower:
            results.append(fullPath)
        elif self.criteria_type in ("creation_date", "modification_date"):
            date_to_check = os.path.getctime(fullPath) if self.criteria_type == "creation_date" else os.path.getmtime(fullPath)
            if self.compare_dates(date_to_check) :
                if criteria_lower in file_lower:
                    results.append(fullPath)
    except Exception as e:
        logger.error(f"Error processing file {fullPath}: {e}")

def compare_dates(self, file_timestamp):
    return self.start_date_timestamp <= file_timestamp <= self.end_date_timestamp
class TreeProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
index = self.sourceModel().index(source_row, 0, source_parent)
if self.sourceModel().isDir(index):
#logger.debug(f"Folder accepted for display: {index.data()}")
return True # Accept the entry as it is a folder
#logger.debug(f"Entry rejected (not a folder): {index.data()}")
return False # Reject the entry as it is not a folder

class FileListProxyModel(QSortFilterProxyModel):
def init(self, parent=None):
super(FileListProxyModel, self).init(parent)
self.filter_text = ""
self.filters = ['.dxf', '.png', '.jpg', '.jpeg']
self.recursiveSearchEnabled = True # Enabled by default
self.searchPaths = [] # Paths to use for filtering results


def setFilterText(self, text):
    """Set the filter text and invalidate the current filter."""
    self.filter_text = text.lower()
    self.invalidateFilter()
    logger.debug(f"Filter text set: {self.filter_text}")   

def setSearchPaths(self, paths):
    """Set the search paths for filtering."""
    self.searchPaths = paths
    self.invalidateFilter()
    logger.debug(f"Search paths set: {self.searchPaths}") 

def setRecursiveSearchEnabled(self, enabled):
    """Enable or disable recursive search."""
    self.recursiveSearchEnabled = enabled
    self.invalidateFilter()
    logger.debug(f"Recursive search {'enabled' if enabled else 'disabled'}.")

def filterAcceptsRow(self, sourceRow, sourceParent):
    sourceModel = self.sourceModel()
    if not sourceModel:
        return False

    sourceIndex = sourceModel.index(sourceRow, 0, sourceParent)
    if not sourceIndex.isValid():
        return False

    # Standard filtering logic
    filepath = sourceModel.filePath(sourceIndex).lower()
    isDirectory = sourceModel.isDir(sourceIndex)
    isAcceptableFile = isDirectory or any(filepath.endswith(ext) for ext in self.filters)
    filename = sourceModel.fileName(sourceIndex).lower()

    if self.searchPaths:
        if not any(filepath.startswith(path.lower()) for path in self.searchPaths):
            #logger.debug(f"Path {filepath} rejected by search paths.")
            return False

    if self.filter_text:
        filename = self.sourceModel().data(sourceIndex, Qt.DisplayRole).lower()
        textMatch = self.filter_text in filename or self.filter_text in filepath
    else:
        textMatch = True

    # Final decision for row acceptance
    if isAcceptableFile and textMatch:
        return True

    # If recursive search is enabled and the index is a directory, check subdirectories
    if self.recursiveSearchEnabled and isDirectory:
        rowCount = sourceModel.rowCount(sourceIndex)
        logger.debug(f"Recursive search enabled, number of subdirectories for {filepath}: {rowCount}")
        for childRow in range(rowCount):
            if self.filterAcceptsRow(childRow, sourceIndex):
                logger.debug(f"Recursive acceptance for {filepath} due to subdirectory at row {childRow}")
                return True

    #logger.debug(f"Rejected {filepath}")
    return False
class SearchResultsProxyModel(QAbstractProxyModel):
def init(self, parent=None):
super().init(parent)
self._search_results = [] # Stocke les chemins des fichiers trouvés
self._path_to_row = {} # Mapping from path to row index
self._cached_source_indexes = {} # Cache pour les index source


def setSearchResults(self, results):
    """Met à jour les résultats de la recherche et signale les changements au modèle."""
    logger.debug(f"SearchResultsProxyModel.setSearchResults called with {len(results)} results.")
    self.beginResetModel()
    self._search_results = [os.path.normcase(r) for r in results]
    self._path_to_row = {path: idx for idx, path in enumerate(self._search_results)}
    self._cached_source_indexes.clear()  # Vider le cache car les résultats ont changé
    self.endResetModel()
    logger.debug("setSearchResults: Model reset completed.") 

def rowCount(self, parent=QModelIndex()):
    """Retourne le nombre de résultats de recherche."""
    if parent.isValid():  # Si l'index parent est valide, on est dans un noeud enfant, donc 0 enfant
        return 0
    return len(self._search_results)

def filePath(self, index):
    """Retourne le chemin du fichier pour l'index donné, en déléguant au modèle source."""
    source_index = self.mapToSource(index)
    return self.sourceModel().filePath(source_index)

def columnCount(self, parent=QModelIndex()):
    """Retourne le nombre de colonnes du modèle source."""
    return self.sourceModel().columnCount(parent) if self.sourceModel() else 0

def index(self, row, column, parent=QModelIndex()):
    """Retourne l'index du proxy pour la ligne, la colonne et le parent donnés."""
    if not parent.isValid() and 0 <= row < len(self._search_results):
        source_index = self.sourceModel().indexFromPath(self._search_results[row])
        if source_index.isValid():
            return self.createIndex(row, column, source_index)
        else:
            logger.error(f"Invalid source index for path: {self._search_results[row]}")                     
    return QModelIndex()

def parent(self, index):
    """Retourne l'index parent, toujours invalide pour ce modèle."""
    return QModelIndex()

def mapFromSource(self, sourceIndex):
    """Retourne l'index du proxy correspondant à l'index source."""
    logger.debug(f"FileListProxyModel.mapFromSource appelé avec sourceIndex: {sourceIndex}")
    if sourceIndex.isValid():
        source_file_path = os.path.normcase(os.path.normpath(self.sourceModel().filePath(sourceIndex)))
        row = self._path_to_row.get(source_file_path)
        if row is not None:
            return self.createIndex(row, sourceIndex.column())
        else:
            logger.warning("FileListProxyModel.mapFromSource reçu un sourceIndex invalide.")
            return QModelIndex()
    return QModelIndex()

def mapToSource(self, proxyIndex):
    """Retourne l'index source correspondant à l'index du proxy."""
    #logger.debug(f"FileListProxyModel.mapToSource appelé avec proxyIndex: {proxyIndex}")
    if proxyIndex.isValid() and 0 <= proxyIndex.row() < len(self._search_results):
        source_path = self._search_results[proxyIndex.row()]
        if source_path in self._cached_source_indexes:
            return self._cached_source_indexes[source_path]
        source_index = self.sourceModel().indexFromPath(source_path)
        if source_index.isValid():
            self._cached_source_indexes[source_path] = source_index
        return source_index
    return QModelIndex()

def data(self, index, role=Qt.DisplayRole):
    """Retourne les données pour l'index donné et le rôle."""
    if not index.isValid():
        logger.debug("Invalid index in data method.")
        return None

    if not self.sourceModel():
        logger.error("Source model is not set.")
        return None

    source_index = self.mapToSource(index)  # Obtenir un nouvel index source valide
    if not source_index.isValid():
        logger.debug("Invalid source index in data method.")
        return None

    return self.sourceModel().data(source_index, role)
def setSourceModel(self, source_model):
    logger.debug("SearchResultsProxyModel: setSourceModel called")
    super().setSourceModel(source_model)
    if source_model is not None:
        source_model.dataChanged.connect(self.sourceDataChanged)

def sourceDataChanged(self, sourceTopLeft, sourceBottomRight, roles):
    #logger.debug(f"SearchResultsProxyModel: sourceDataChanged called with sourceTopLeft={sourceTopLeft}, sourceBottomRight={sourceBottomRight}, roles={roles}")
    # Mapper les index source vers les index du proxy
    proxyTopLeft = self.mapFromSource(sourceTopLeft)
    proxyBottomRight = self.mapFromSource(sourceBottomRight)
    #logger.debug(f"SearchResultsProxyModel: Mapped proxyTopLeft={proxyTopLeft}, proxyBottomRight={proxyBottomRight}")

    if proxyTopLeft.isValid() and proxyBottomRight.isValid():
        #logger.debug("SearchResultsProxyModel: Emitting dataChanged from proxy model")
        # Émettre le signal dataChanged du proxy avec les rôles appropriés
        self.dataChanged.emit(proxyTopLeft, proxyBottomRight, roles)
    else:
        logger.debug("SearchResultsProxyModel: Skipping dataChanged emission, model is not active.")
class ThumbnailWorkerSignals(QObject):
"""Classe pour regrouper les signaux du ThumbnailWorker."""
thumbnailReady = pyqtSignal(str, QPixmap, bool) # Chemin du fichier, vignette, succès
finished = pyqtSignal()
errorOccurred = pyqtSignal(str, Exception) # Chemin du fichier, exception

class ThumbnailWorker(QRunnable):
"""Worker pour générer les vignettes de manière asynchrone."""


def __init__(self, file_path, cache):
    super().__init__()
    self.signals = ThumbnailWorkerSignals()  # Instance pour gérer les signaux
    self.file_path = file_path
    self.cache = cache  # Référence au cache partagé
    self._cancel_requested = False

@pyqtSlot()
def cancel(self):
    """Annule la génération de la vignette si elle est en cours."""
    self._cancel_requested = True

@pyqtSlot()  # Supprimer l'argument file_path
def run(self):  # Méthode exécutée dans un thread séparé pour générer la vignette
    #logger.debug(f"ThumbnailWorker: run() Démarrage pour {self.file_path}")
    if self._cancel_requested:
        logger.warning(f"ThumbnailWorker: run() cancelled for {self.file_path}")
        return

    # Vérifier si la vignette est déjà dans le cache
    if self.file_path in self.cache:
        #logger.debug(f"ThumbnailWorker: Thumbnail found in cache for {self.file_path}")
        self.signals.thumbnailReady.emit(self.file_path, self.cache[self.file_path], True)
        self.signals.finished.emit()
        return
    
    try:
        #logger.debug(f"ThumbnailWorker: Generating thumbnail for {self.file_path}")
        thumbnail = self.generate_thumbnail(self.file_path)
        success = thumbnail is not None and not thumbnail.isNull()
        #logger.debug(f"ThumbnailWorker: Thumbnail generated for {self.file_path}, success: {success}")

        if success:
            self.signals.thumbnailReady.emit(self.file_path, thumbnail, success)
    except (FileNotFoundError, ValueError, CancelledError) as e:
        logger.error(f"ThumbnailWorker: Error generating thumbnail for {self.file_path}: {e}")
        self.signals.errorOccurred.emit(self.file_path, e)
    finally:
        self.signals.finished.emit()

def generate_thumbnail(self, file_path):
    """Génère une vignette pour le fichier donné."""
    #logger.debug(f"ThumbnailWorker: generate_thumbnail called for {file_path}")
    try:
        if file_path.lower().endswith(('.png', '.jpg', '.jpeg')):
            thumbnail = self.generate_image_thumbnail(file_path)
        elif file_path.lower().endswith('.dxf'):
            thumbnail = self.generate_dxf_thumbnail(file_path)
        else:
            logger.warning(f"ThumbnailWorker: generate_thumbnail() - Unsupported file format for {file_path}")
            raise ValueError(f"Unsupported file format: {file_path}")
        return thumbnail
    except Exception as e:
        logger.error(f"ThumbnailWorker: Error generating thumbnail for {file_path}: {e}", exc_info=True)
        raise

def generate_image_thumbnail(self, image_path):
    """Génère une miniature à partir d'un fichier image."""
    #logger.debug(f"ThumbnailWorker: generate_image_thumbnail for image: {image_path}")
    try:
        if self._cancel_requested:
            logger.warning(f"ThumbnailWorker: generate_image_thumbnail - Annulé pour {image_path}")
            return None  # Return None if cancelle
        size = QSize(160, 120)
        reader = QImageReader(image_path)
        reader.setAutoTransform(True)
        image = reader.read()
        if image.isNull():
            logger.error(f"Impossible de lire l'image: {image_path}")
            return QPixmap()
        thumbnail = QPixmap.fromImage(image).scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
        return thumbnail
    except ValueError as ve:
        logger.error(f"ValueError creating thumbnail for {image_path}: {ve}", exc_info=True)
        return QPixmap()
    except Exception as e:
        logger.error(f"Error creating thumbnail for {image_path}: {e}", exc_info=True)
        return QPixmap()  # Return an empty QPixmap on failure
def generate_dxf_thumbnail(self, dxf_file):
    """Génère une miniature à partir d'un fichier DXF."""
    #logger.debug(f"ThumbnailWorker: generate_dxf_thumbnail Début de la génération de la miniature DXF pour {self.file_path}")
    painter = None  # Initialisation
    try:
        # Vérification d'annulation avant de commencer le traitement
        if self._cancel_requested:
            logger.warning(f"ThumbnailWorker: generate_dxf_thumbnail - Annulé pour {self.file_path}")
            return None
        doc = ezdxf.readfile(dxf_file)
        msp = doc.modelspace()
        # Log the entity types
        #entity_types = set(entity.dxftype() for entity in msp)
        #logger.info(f"Entity types in DXF file: {entity_types}")
        width_thumbnail, height_thumbnail = 120, 160
        thumbnail = QPixmap(width_thumbnail, height_thumbnail)
        thumbnail.fill(Qt.white)
        painter = QPainter(thumbnail)
        painter.setRenderHint(QPainter.Antialiasing)
        bounding_rect = self.calculate_bounding_rect(msp)
        #logger.debug(f"Bounding rect: {bounding_rect}")
        
        scale_factor, offset_x, offset_y = self.calculate_scale_factor_and_offset(bounding_rect, width_thumbnail, height_thumbnail)
        #logger.debug(f"Scale factor: {scale_factor}, Offset X: {offset_x}, Offset Y: {offset_y}")
        # Vérification d'annulation pendant le traitement
        if self._cancel_requested:
            logger.warning(f"ThumbnailWorker: generate_dxf_thumbnail - Annulé pendant le traitement de {self.file_path}")
            return None
        for ent in msp:
            if ent.dxftype() == 'LINE':
                self.add_line(painter, ent.dxf.start, ent.dxf.end, scale_factor, offset_x, offset_y)
            elif ent.dxftype() == 'POINT':
                # Handle points if needed
                pass
            elif ent.dxftype() == 'CIRCLE':
                self.add_circle(painter, ent.dxf.center, ent.dxf.radius, scale_factor, offset_x, offset_y)
            elif ent.dxftype() == 'ARC':
                self.add_arc(painter, ent.dxf.center, ent.dxf.radius, ent.dxf.start_angle, ent.dxf.end_angle, scale_factor, offset_x, offset_y)
            elif ent.dxftype() == 'SPLINE':
                self.add_spline(painter, ent, scale_factor, offset_x, offset_y)
            elif ent.dxftype() == 'ELLIPSE':
                self.add_ellipse(painter, ent, scale_factor, offset_x, offset_y)
            elif ent.dxftype() == 'TEXT':
                self.add_text(painter, ent, scale_factor, offset_x, offset_y)
            elif ent.dxftype() == 'LWPOLYLINE':
                self.add_wpolyline(painter, ent, scale_factor, offset_x, offset_y)
    
        painter.end()
        thumbnail = thumbnail.transformed(QTransform().rotate(-90))
        final_thumbnail = thumbnail.scaled(QSize(160, 120), Qt.KeepAspectRatio, Qt.SmoothTransformation)
        #logger.debug(f"ThumbnailWorker: Fin de la génération de la miniature DXF pour {self.file_path}")
        return final_thumbnail
        #return thumbnail
    except (ezdxf.DXFError, RecursionError) as e:
        # Gérer les erreurs spécifiques au format DXF et les erreurs de récursion
        logger.error(f"Erreur DXF ou RecursionError: {e}")
    except Exception as e: # Catch-all pour les autres exceptions
        logger.error(f"Unexpected error: {type(e).__name__} - {e}", exc_info=True)  # Logging avec traceback complet
    finally: 
        if painter and painter.isActive():
            painter.end()  # Ensure QPainter is ended in case of error
    return QPixmap()
# SUITE DE LA CLASS .... 
class FileViewer(QMainWindow):
folderChanged = pyqtSignal(str)
fileSelected = pyqtSignal(str)
rechercheChanged = pyqtSignal(str)
def init(self):
super().init()
self.scale_factor = 1 # Initialiser avec une valeur par défaut
self.indexeur = FileIndexer()
self.setWindowTitle("Souf Dxf Explorer")
self.setGeometry(100, 100, 800, 600)


    self.model = CustomFileSystemModelForList(self)
    self.isSearchThreadRunning = False
    self._in_search_mode = False

    # Search indicator
    self.search_indicator = QLabel("Recherche en cours...", self)
    self.statusBar().addWidget(self.search_indicator)
    self.search_indicator.hide()

    self.current_folder = os.path.normpath(os.path.expanduser("~")) 
    self.previous_folder = None
    #self.current_folders = [QDir.homePath()]  # Initialize the current_folders attribute
    #self.rootPaths = ["Y:/"]
    #logger.debug(f"[Init] Current folders initialized to: {self.current_folders}")
    self.setupModelsAndView()
    self.setupUIComponents()
    self.setupObservers()
    self.connect_signals()
    self.indexeur.indexUpdated.connect(self.on_indexing_finished) 
    self.indexeur.indexerError.connect(self.on_indexing_error) # connecter au slot
    self.indexeur.indexingProgress.connect(self.update_indexing_progress)  # Connecter le signal
    # Démarrer l'indexation asynchrone après l'initialisation de l'interface
    #QTimer.singleShot(3000, lambda: (self.indexeur.buildIndexAsync(), logger.info(" Fileviewer.__init__(self) (self.indexeur.buildIndexAsync())Lambda function in QTimer.singleShot executed")))
    #logger.info("FileViewer / Asynchronous indexing initiated.") 

def connect_signals(self):
    if not hasattr(self, 'signals_connected'):
        # Connexions pour la mise à jour de la vue de la liste de fichiers
        self.file_list.model().dataChanged.connect(self.update_file_list_view)
        self.model.modelUpdated.connect(self.update_file_list_view)
        logger.info("Connection: file_list.clicked -> display_selected_file")
        self.file_list.clicked.connect(self.display_selected_file)
        logger.info("Connection: file_list.doubleClicked -> on_folder_double_clicked")
        self.file_list.doubleClicked.connect(self.on_folder_double_clicked)

        # Connexions pour la recherche
        self.SearchWorker.searchFinished.connect(self.onSearchFinished)
        self.searchThread.started.connect(self.SearchWorker.performSearch)

        # Connexions pour les temporisateurs et les champs de texte
        self.search_timer.timeout.connect(self.launchSearch)
        self.search_box.textChanged.connect(self.launch_delayed_search)

        # Configuration du proxy de recherche
        self.search_results_proxy_model = SearchResultsProxyModel(self)
        self.search_results_proxy_model.setSourceModel(self.model)  # Définir le modèle source du proxy

        self.signals_connected = True

def setupUIComponents(self):
    self.pathLabel = QLabel("Path:")
    self.setup_ui()
    self.setupMenu()
    self.setupToolbar()

def setupModelsAndView(self):
    #self.indexeur = FileIndexer()
    self.rootPaths = [os.path.normpath("")]
    for path in self.rootPaths:
        if os.path.exists(path):
            print(f"Le chemin {path} est valide.")
        else:
            #print(f"Le chemin {path} n'est pas valide.")
            print(f"Le chemin path est:{path} ")
    self.tree_model = CustomFileSystemModelForTree()
    self.file_list_model = CustomFileSystemModelForList(self, self.indexeur)
    self.file_list = QListView()
    self.setup_tree_dock_widget()
    self.setup_proxy_tree_model()

    # Add SearchResultsProxyModel
    self.search_results_proxy_model = SearchResultsProxyModel(self)
    self.search_results_proxy_model.setSourceModel(self.file_list_model)

    # Connect signal modelReset
    self.search_results_proxy_model.modelReset.connect(self.file_list.reset)
    # Connect signals layoutChanged and dataChanged
    self.search_results_proxy_model.layoutChanged.connect(self.file_list.reset)
    self.search_results_proxy_model.dataChanged.connect(self.file_list.reset)

    # Configure FileListProxyModel to use SearchResultsProxyModel as source
    self.file_list_proxy = FileListProxyModel()
    self.setup_file_list_proxy()
    self.file_list_proxy.setSourceModel(self.search_results_proxy_model)

    # Configure list view to use the proxy file_list_proxy
    self.file_list.setModel(self.file_list_proxy)

    self.file_list_model.directoryLoaded.connect(self.file_list_model.on_directory_loaded)

def setupObservers(self):
    self.SearchWorker = SearchWorker(self.indexeur, self.indexeur.index_dir)
    self.searchThread = QThread()
    self.SearchWorker.moveToThread(self.searchThread)

    self.FileMonitor = FileMonitor(self.indexeur, self.SearchWorker.allowed_extensions) 
    self.observer = Observer()
    self.observer.schedule(self.FileMonitor, path=os.path.normpath(WATCHED_DIR), recursive=True)   # Surveiller WATCHED_DIR
    self.observer.start()

    self.search_timer = QTimer(self)
    self.search_timer.setSingleShot(True)

    self.update_directory_label(f"Current folder path: {self.tree_model.rootPath()}")

def displayResults(self, success, results):
    logger.debug("Beginning of displayResults function.")

    # Arrêter le profilage et afficher les résultats
    #if hasattr(self, 'profiler'):  # Vérifier si le profilage a été activé
        #self.profiler.disable()
        #s = io.StringIO()
        #ps = pstats.Stats(self.profiler, stream=s).sort_stats('cumulative')
        #ps.print_stats()
       # logger.debug(f"Profiling results:\n{s.getvalue()}")

    # Eviter les appels récursifs ou redondants
    if hasattr(self, 'display_results_called') and self.display_results_called:
        logger.debug("displayResults already called, skipping.")
        return

    self.display_results_called = True
    self.isSearchThreadRunning = False
    self.search_indicator.hide()

    # Gérer les différents cas de figure
    if not success:
        logger.debug("Handling search failure.")
        self.handleSearchFailure()
        self._in_search_mode = False  # Assurez de quitter le mode recherche en cas d'échec
    elif not results:
        logger.debug("Handling no results.")
        self.handleNoResults()
        self._in_search_mode = False
    elif not self.search_box.text().strip():
        logger.debug("Search box is empty, resetting list view.")
        self.resetListView()
        self._in_search_mode = False
    else:
        # Afficher les résultats de la recherche
        self._in_search_mode = True
        logger.debug("Updating proxy model with search results.")
        self.search_results_proxy_model.setSearchResults(results)  # Mettre à jour le proxy
        # Si le modèle actuel de la vue n'est pas le modèle de résultats, le définir
        if self.file_list.model() != self.search_results_proxy_model:
            self.file_list.setModel(self.search_results_proxy_model)
        logger.debug("SearchResultsProxyModel set as the current model for file_list.")
        # Rafraîchir la vue
        #self.file_list.reset()
        #logger.debug(f"displayResults: ListView reset. Number of items in view: {self.file_list.model().rowCount()} ")
        # Forcer le focus et mettre à jour la vue
        #self.file_list.setFocus()
        #self.file_list.update()
        #self.file_list.repaint()
        # (Facultatif) Sélectionner le premier élément si souhaité
        if results:
            first_result_index = self.search_results_proxy_model.index(0, 0)
            if first_result_index.isValid():
                self.file_list.setCurrentIndex(first_result_index)
        logger.debug("Logging search results.")
        self.logSearchResults(results)

    self.display_results_called = False
    logger.debug("End of displayResults function.")

def launch_delayed_search(self):
    logger.debug("Slot activated: launch_delayed_search")
    search_criteria = self.search_box.text().strip()
    if search_criteria:
        logger.info("Search criteria modified. Waiting before launching search...")
        if not self.previous_folder: 
            self.previous_folder = self.current_folder  # Sauvegarder le dossier actuel 
        self.search_timer.stop()  # Stop the existing timer before restarting
        if self.search_timer.isActive():
            self.search_timer.stop()
        self.search_timer.start(1000)  # Delay of 1000 ms
    else:
        logger.info("Search bar empty. Displaying current folder.")
        self.exit_search_mode()
    logger.debug("End of launch_delayed_search")
def launchSearch(self):
    logger.debug("Function launchSearch start")
    criteria = self.search_box.text().strip()
    criteria_type = self.search_criteria_type.currentText().lower().replace(" ", "_")
    logger.debug(f"Search criteria: {criteria}, criteria_type: {criteria_type}")
    logger.debug(f"Current root path: {self.file_list_model.rootPath()}")
    logger.debug(f"WATCHED_DIR: {WATCHED_DIR}")
    if not criteria and criteria_type == "filename":
        logger.debug("Search criteria is empty, exiting search.")
        #QMessageBox.warning(self, "Warning", "Search criteria cannot be empty for filename search.")
        self.exit_search_mode()
        return

    if self.isSearchThreadRunning:
        logger.info("Search already running, stopping current search.")
        self.SearchWorker.running = False  # Stop the thread gracefully
        self.searchThread.quit()
        self.searchThread.wait()
    # Sauvegarder le dossier actuel avant de lancer la recherche  
    if not self.previous_folder:  
        self.previous_folder = self.current_folder  
        logger.debug(f"Saved current folder to previous_folder: {self.previous_folder}") 

    self.isSearchThreadRunning = True
    self.SearchWorker.setCriteria(criteria, criteria_type)
    self.SearchWorker.setDateRange(self.start_date.date(), self.end_date.date())

    # Assurez-vous que rootPath est bien défini
    current_root_path = self.file_list_model.rootPath()
    if current_root_path:
        self.SearchWorker.setBasePath(current_root_path)
        #self.profiler = cProfile.Profile()
        #self.profiler.enable()
        #self.searchThread.started.connect(self.SearchWorker.performSearch)
        self.searchThread.start()
        self.search_indicator.show()
        logger.debug("Function launchSearch end")
    else:
        logger.error("Current root path is not set. Cannot start search.")
        self.isSearchThreadRunning = False
def onSearchFinished(self, success, results):
    logger.debug("Search finished with success: {}".format(success))
    self.isSearchThreadRunning = False
    if self.searchThread.isRunning():
        logger.debug("Stopping search thread.")
        self.searchThread.quit()
        self.searchThread.wait()  # Ensure the thread is fully stopped
        logger.debug("Search thread stopped.")
    logger.debug("Search thread stopped and resources cleaned up.")
    self.handle_search_completion(success, results)
    logger.debug("End of onSearchFinished function.")

def handle_search_completion(self, success, results):
    logger.debug("Function handle_search_completion start")
    if success:
        logger.info("Search completed successfully.")
        self.displayResults(success, results)
        # Connecter le proxy de recherche à la vue SI en mode recherche
        if self._in_search_mode:  
            self.file_list.setModel(self.search_results_proxy_model)
        else:
            # Revenir au proxy normal si on n'est plus en recherche
            self.file_list.setModel(self.file_list_proxy) 
    else:
        logger.error("Search failed, attempting to restart.")
        logger.debug(f"Search results (should be empty on failure): {results}")
        # Resetting the thread to allow for new searches
        self.searchThread.quit()
        self.searchThread.wait()  # Wait until the thread cleanly finishes
        self.searchThread = QThread()  # Recreate the thread for future use
        self.SearchWorker = SearchWorker(self.indexeur, self.indexeur.index_dir)
        self.SearchWorker.moveToThread(self.searchThread)
        self.searchThread.started.connect(self.SearchWorker.performSearch)
        self.SearchWorker.searchFinished.connect(self.onSearchFinished)

    logger.debug("Function handle_search_completion end")

def handleSearchFailure(self):
    logger.debug("Beginning of handleSearchFailure function.")
    QMessageBox.warning(self, "Search Failed", "The search failed or was stopped without results.")
    self.resetListView()
    logger.debug("End of handleSearchFailure function.")

def handleNoResults(self):
    logger.debug("Beginning of handleNoResults function.")
    QMessageBox.information(self, "No Results", "No results to display.")
    self.resetListView()
    logger.debug("End of handleNoResults function.")

def logSearchResults(self, results):
    logger.debug("Beginning of logSearchResults function.")
    for result in results[:min(5, len(results))]:
        logger.debug(f"Search result: {result}")
    logger.debug("End of logSearchResults function.")

def exit_search_mode(self):
    logger.debug("Function exit_search_mode start")
    self._in_search_mode = False
    self.file_list_proxy.invalidateFilter()
    if self.previous_folder:  
        logger.debug(f"[exit_search_mode] Previous folder: {self.previous_folder}") 
        self.current_folder = self.previous_folder  # Restaurer le dossier précédent  
        self.previous_folder = None  # Réinitialiser previous_folder  
        self.file_list_model.setRootPath(self.current_folder)
        root_index = self.file_list_model.indexFromPath(self.current_folder)
        if root_index.isValid():
            if hasattr(self, 'file_list_proxy'):
                self.file_list_proxy.setSourceModel(self.file_list_model)
                self.file_list.setModel(self.file_list_proxy)  
                proxy_root_index = self.file_list_proxy.mapFromSource(root_index)
                if proxy_root_index.isValid():
                   self.file_list.setRootIndex(proxy_root_index)
                else:
                    logger.error(f"Invalid proxy root index: {proxy_root_index}")
            else:
                self.file_list.setModel(self.file_list_model) 
                self.file_list.setRootIndex(root_index)
        else:
            logger.error(f"Invalid root index: {root_index}")
        self.update_directory_label(f"Current folder: {self.current_folder}")  
        logger.info(f"[exit_search_mode] Current folder updated to: {self.current_folder}") 
    else:
        logger.warning("[exit_search_mode] No previous folder stored. Using default path.")  
        default_folder = QDir.homePath()
        self.file_list_model.setRootPath(default_folder)  
        root_index = self.file_list_model.index(default_folder)

        if hasattr(self, 'file_list_proxy'):
            self.file_list_proxy.setSourceModel(self.file_list_model)
            proxy_root_index = self.file_list_proxy.mapFromSource(root_index)
            self.file_list.setRootIndex(proxy_root_index)
        else:
            self.file_list.setRootIndex(root_index)

        self.update_directory_label(f"Current folder: {default_folder}")
        logger.info(f"[Update] Current folder updated to: {default_folder}")

    logger.debug("Function exit_search_mode end")

def resetListView(self):
    """Réinitialise la vue de liste pour afficher le contenu du répertoire courant."""
    try:
        self._in_search_mode = False  # Sortir du mode recherche
        self.file_list.setModel(self.file_list_proxy)  # Rétablir le proxy principal

        # Vérifier si current_folder est valide avant de continuer
        if self.current_folder and os.path.exists(self.current_folder):
            logger.debug(f"[resetListView] Current folder: {self.current_folder}")
            self.updateFileListModel(self.current_folder)  
            self.update_file_list_view()
            self.checkDisplayedFiles(self.file_list_model)
            self.updateDirectoryLabel()
        else:
            self.handleNoCurrentFolder()

        self.file_list.reset()  # Rafraîchir la vue après les modifications

    except Exception as e:
        self.handleResetListViewError(e)
def updateFileListModel(self, folder_path):
    """Met à jour le chemin racine du modèle de liste de fichiers.

    Args:
        folder_path (str): Le nouveau chemin racine pour le modèle.
    """
    logger.debug("Fonction updateFileListModel: Début")
    if not isinstance(folder_path, str) or not folder_path:
        logger.error("Le chemin du dossier doit être une chaîne de caractères valide.")
        QMessageBox.warning(self, "Erreur", "Le chemin du dossier doit être une chaîne de caractères valide.")
        return
    if not os.path.isdir(folder_path):
        logger.error(f"Le chemin fourni n'est pas valide ou n'est pas un dossier: {folder_path}")
        QMessageBox.warning(self, "Erreur", "Le chemin fourni n'est pas un dossier valide.")
        return
    self.file_list_model.setRootPath(folder_path)
    logger.info(f"Chemin racine du modèle de liste de fichiers mis à jour: {folder_path}")
    logger.debug("Fonction updateFileListModel: Fin")

def update_file_list_view(self, folder_path=None, index=None):
    logger.debug("Fonction update_file_list_view: Début")
    try:
        root_index = None
        if folder_path is not None:
            if isinstance(folder_path, str):
                logger.debug(f"Chemin du dossier fourni: {folder_path}")
                self.updateFileListModel(folder_path)
                root_index = self.file_list_model.indexFromPath(folder_path)
                if not root_index.isValid():
                    logger.error("Index racine obtenu à partir du chemin est invalide.")
                    return
            else:
                logger.error("Le chemin du dossier doit être une chaîne de caractères valide.")
                return
        elif index is not None:
            logger.debug(f"Index fourni: {index}")
            if not index.isValid():
                logger.error("Index non valide reçu pour la mise à jour.")
                return
            root_index = index if not isinstance(index.model(), QSortFilterProxyModel) else index.model().mapToSource(index)
            if not root_index.isValid():
                logger.error("Index racine obtenu à partir de l'index fourni est invalide.")
                return
        else:
            if self.current_folder:  
                folder_path = self.current_folder  
                logger.debug(f"Utilisation du dossier actuel: {folder_path}")  
                self.updateFileListModel(folder_path)  
                root_index = self.file_list_model.indexFromPath(folder_path)
                if not root_index.isValid():
                    logger.error("Index racine obtenu à partir du dossier actuel est invalide.")
                    return
            else:
                logger.error("Aucun dossier courant défini.")  
                return  

        if hasattr(self, 'file_list_proxy'):
            self.file_list_proxy.setSourceModel(self.file_list_model)
            proxy_root_index = self.file_list_proxy.mapFromSource(root_index)
            if proxy_root_index.isValid():
                self.file_list.setRootIndex(proxy_root_index)
                logger.info(f"Vue de la liste mise à jour avec proxy. Index source: {root_index}, Index proxy: {proxy_root_index}")
            else:
                logger.error("Erreur lors de la mise à jour de la vue avec proxy: L'index proxy n'est pas valide.")
        else:
            if root_index.isValid():
                self.file_list.setRootIndex(root_index)
                logger.info(f"Vue de la liste mise à jour sans proxy. Index source: {root_index}")
            else:
                logger.error("Erreur lors de la mise à jour de la vue sans proxy: L'index racine n'est pas valide.")
    except Exception as e:
        logger.error(f"Erreur dans update_file_list_view: {e}")
        QMessageBox.warning(self, "Erreur", f"Une erreur s'est produite: {e}")

    logger.debug("Fonction update_file_list_view: Fin")

def showModelInfo(self):
    logger.debug("Beginning of showModelInfo function.")
    try:
        self.logModelContents(self.file_list_model)
    except Exception as e:
        logger.error(f"An error occurred while displaying model information: {str(e)}")
        QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}")
    logger.debug("End of showModelInfo function.")

def logModelContents(self, model):
    logger.debug("Beginning of logModelContents function.")
    root_path = model.rootPath() 
    root_index = model.indexFromPath(root_path)
    for row in range(min(5, model.rowCount(root_index))):
        child_index = model.index(row, 0, root_index)
        if child_index.isValid():
            file_path = model.filePath(child_index)
            logger.debug(f"Element at index {row}: {file_path}")
        else:
            logger.warning(f"Invalid index at row {row}")
    logger.debug("End of logModelContents function.")

def handleNoCurrentFolder(self):
    logger.info("No current folder set, not updating the file list.")
    self.current_folder = os.path.expanduser("~")  # Chemin par défaut vers le dossier utilisateur  
    self.updateFileListModel(self.current_folder)  
    self.updateDirectoryLabel()  
    self.update_file_list_view() 
    # You can add additional logic here, such as displaying a message to the user

def setCurrentFolderPath(self):
    default_folder = os.path.expanduser("~")  # Ou un autre chemin par défaut
    self.current_folder = default_folder
    logger.debug(f"Current folder path reset to default: {self.current_folder}")

def updateDirectoryLabel(self):
    self.update_directory_label(f"Current folder: {self.current_folder}")
    logger.debug("Directory label updated.")

def handleResetListViewError(self, error):
    logger.error(f"Error resetting list view: {error}")
    QMessageBox.warning(self, "Error", "Problem resetting list view.")
    logger.warning("Reset list view error handled.")

def checkDisplayedFiles(self, model):
    actual_files = [model.filePath(model.index(row, 0)) for row in range(model.rowCount())]
    logger.info("Files currently displayed in the view: " + str(actual_files))

@pyqtSlot(int, int)
def update_indexing_progress(self, processed_files, total_files):
    # Mettre à jour la barre de progression ou afficher un message de statut
    if total_files > 0:
        progress_percent = int(processed_files / total_files * 100)
        QMetaObject.invokeMethod(self, "update_status_bar", Qt.QueuedConnection,
                         Q_ARG(str, f"Indexing in progress... {progress_percent}%"))
    else:
        progress_percent = 0
@pyqtSlot(str)
def update_status_bar(self, message):
    self.statusBar().showMessage(message)
@pyqtSlot()
def on_indexing_finished(self):
    """Slot appelé lorsque l'indexation est terminée."""
    logger.info("FileViewer / Indexing finished successfully.")
    # Ajoutez ici le code que vous souhaitez exécuter à la fin de l'indexation
    #self.update_file_list_view(WATCHED_DIR)  # Mettre à jour la vue de liste après l'indexation
    # ... (autres actions)
    # Activez la recherche lorsque l'indexation est terminée
    self.search_button.setEnabled(True)
@pyqtSlot(str)
def on_indexing_error(self, error_message):
    """Slot appelé en cas d'erreur lors de l'indexation."""
    logger.error(f"FileViewer / Indexing error: {error_message}")
    # Gérez l'erreur ici (par exemple, affichez un message à l'utilisateur)
    self.is_indexing = False  # Reset the flag in case of an error
    QMessageBox.critical(self, "Erreur d'indexation", f"Une erreur est survenue lors de l'indexation : {error_message}")

def get_proxy_model_for_source_model(self, source_model):
    logger.debug("Function get_proxy_model_for_source_model start")
    if source_model == self.file_list_model:
        logger.info("Found matching proxy model for file list model.")
        return self.file_list_proxy
    elif source_model == self.tree_model:
        logger.info("Found matching proxy model for tree model.")
        return self.tree_proxy
    logger.warning("No matching proxy model found for given source model.")
    logger.debug("Function get_proxy_model_for_source_model end")
    return None

def get_view_for_model(self, model):
    logger.debug("Function get_view_for_model start")
    if model == self.file_list_model:
        logger.info("Found matching view for file list model.")
        return self.file_list
    elif model == self.tree_model:
        logger.info("Found matching view for tree model.")
        return self.tree_view
    logger.debug("Function get_view_for_model end")
    return None

def on_root_path_changed(self, new_path):
    #self.current_folder = new_path  
    logger.debug("Function on_root_path_changed start with new path: {}".format(new_path))
    self.file_list_proxy.setSourceModel(self.file_list_model)  # Re-attach source model if needed
    self.update_file_list_view()
    logger.debug("Function on_root_path_changed end")

def update_directory_label(self, text=None):
    logger.info("Function update_directory_label start")
    if text is None:  
        text = f"Current folder: {self.current_folder}" 
    logger.debug(f"Attempting to update directory label with text: {text}") 
    if hasattr(self, 'directory_label'):
        self.directory_label.setText(text)
        logger.info(f"Updated directory label: {text}")
    else:
        logger.error("Error: Directory label is not initialized.")
        # Optionally initialize the label here if needed
        self.directory_label = QLabel(text)
        logger.info(f"Initialized and updated directory label: {text}")
    logger.info("Function update_directory_label end")

def onRecursiveSearchToggled(self, checked):
    logger.debug("Function onRecursiveSearchToggled start")
    self.file_list_proxy.setRecursiveSearchEnabled(checked)
    logger.info(f"Recursive search {'enabled' if checked else 'disabled'}.")
    logger.debug("Function onRecursiveSearchToggled end")

def setup_proxy_tree_model(self):
    logger.debug("Starting setup of the proxy model for the tree/Function setup_proxy_tree_model")
    if self.tree_model is None:
        logger.error("Error: The source model (tree_model) is not initialized.")
        return
    if hasattr(self, 'tree_proxy_initialized') and self.tree_proxy_initialized:
        logger.debug("Proxy model for the tree is already set up.")
        return
    self.tree_proxy = TreeProxyModel() 
    self.tree_proxy.setSourceModel(self.tree_model)
    logger.info("Source model assigned to the tree proxy.")
    tree_filters = ['*']  # Specific filters for the tree
    self.tree_proxy.setFilterWildcard(' '.join(tree_filters))
    self.tree_proxy.invalidateFilter()
    logger.info("Filter of the tree proxy model invalidated to reflect the changes.")
    self.tree_view.setModel(self.tree_proxy)
    self.tree_proxy_initialized = True
    logger.debug("End of the setup of the proxy model for the tree/Function setup_proxy_tree_model")

def setup_file_list_proxy(self):
    logger.debug("Starting setup of the proxy model for the file list./Function setup_file_list_proxy")
    if self.file_list_model is None:
        logger.error("Error: The source model (file_list_model) is not initialized.")
        return
    try:
        self.file_list_proxy = FileListProxyModel() 
        self.file_list_proxy.setSourceModel(self.file_list_model)
        logger.info("Source model assigned to the file list proxy.")
        # Configuring filters
        file_list_filters = ['*.dxf', '*.jpg', '*.jpeg', '*.png', '*.bmp']  # Filters for the file list view
        self.file_list_proxy.setFilterWildcard(' '.join(file_list_filters))
        logger.info(f"Filters applied for the file list proxy model: {' '.join(file_list_filters)}")
        # Applying filters and updating the view
        self.file_list_proxy.invalidateFilter()
        logger.info("Filter of the file list proxy model invalidated to reflect the changes.")
        self.file_list.setModel(self.file_list_proxy)
    except Exception as e:
        logger.error(f"Error during the configuration of the proxy model: {str(e)}")
    logger.debug("End of the setup of the proxy model for the file list./Function setup_file_list_proxy")

def setup_ui(self):
    logger.debug("Starting the setup_ui function in FileViewer")
    self.setupTreeModelAndView()
    self.setupListModelAndView()
    self.central_layout = QVBoxLayout()
    self.main_splitter = QSplitter(Qt.Horizontal)
    self.setCentralWidget(self.main_splitter)
    self.tree_dock_widget.setFeatures(QDockWidget.NoDockWidgetFeatures)
    self.setup_central_frame()  
    self.setup_preview_dock_widget()
    self.main_splitter.setSizes([445])

    logger.info("Connecting signals and slots:")
    self.central_layout.addWidget(self.file_list)  
    self.directory_label = QLabel("Current folder path:") 
    logger.debug("[Init] Directory label initialized") 
    self.central_layout.addWidget(self.directory_label) 
    logger.debug("[Layout] Directory label added to layout")
    self.file_list.setContextMenuPolicy(Qt.CustomContextMenu)
    self.file_list.customContextMenuRequested.connect(self.open_context_menu)   
    logger.debug("End of the setup_ui function in FileViewer")

def handle_root_path_changed(self, new_root_path):
    logger.debug("Starting the handle_root_path_changed function")
    logger.info(f"New root path: {new_root_path}")
    current_root_path = self.file_list_model.rootPath()
    if new_root_path != current_root_path:
        self.current_folder = new_root_path  # Mettre à jour current_folder 
        self.update_file_list_view(new_root_path) 
    logger.debug("End of the handle_root_path_changed function")

def setupTreeModelAndView(self):
    logger.debug("Starting the setupTreeModelAndView function")
    self.tree_model = CustomFileSystemModelForTree()
    self.tree_model.setRootPath("")
    self.tree_proxy = TreeProxyModel()
    self.tree_proxy.setSourceModel(self.tree_model)
    self.tree_view.setModel(self.tree_proxy)
    logger.debug("End of the setupTreeModelAndView function")

def setupListModelAndView(self):
    logger.debug("Starting the setupListModelAndView function")
    self.file_list_model.setRootPath(self.current_folder)
    logger.info(f"Initial root path for the file list model: (empty)")
    # Configuring the proxy model
    self.file_list_proxy = FileListProxyModel()
    self.file_list_proxy.setSourceModel(self.file_list_model)
    logger.info("Proxy model configured with the file list model.")
    self.file_list_proxy.setFilterKeyColumn(0)
    self.file_list_proxy.setFilterWildcard("*.dxf *.jpg *.jpeg *.png *.bmp")
    # Associating the proxy model with the view
    self.file_list.setModel(self.file_list_proxy)
    logger.info("File list view configured with the proxy model.")
    self.file_list.setRootIndex(QModelIndex())
    logger.info("File list view configured to display empty content.")
    #self.showModelInfo()
    logger.info("Triggered showModelInfo:")
    logger.debug("End of the setupListModelAndView function")

def setup_tree_dock_widget(self):
    logger.debug("Start of setup_tree_dock_widget function")
    self.tree_dock_widget = QDockWidget("Tree")
    self.tree_view = QTreeView(self.tree_dock_widget)
    self.tree_dock_widget.setWidget(self.tree_view)
    self.tree_dock_widget.setMinimumWidth(455)
    self.tree_view.clicked.connect(self.on_tree_clicked)
    logger.info("Connection: tree_view.clicked -> on_tree_clicked")
    #self.tree_model = CustomFileSystemModelForTree() 
    self.tree_model.setRootPath("")
    root_path = self.tree_model.rootPath()
    logger.info(f"Tree root path: {root_path}")

    self.setup_proxy_tree_model()
    header = self.tree_view.header()    
    header.setSectionHidden(1, True) 
    header.setSectionHidden(2, True) 
    header.setSectionHidden(3, True) 
    self.tree_view.setHeaderHidden(False)  # Hide header if desired
    self.addDockWidget(Qt.LeftDockWidgetArea, self.tree_dock_widget)
    logger.debug("End of setup_tree_dock_widget function")

def setup_preview_dock_widget(self):
    logger.debug("Start of setup_preview_dock_widget function")
    self.preview_dock_widget = QDockWidget("Preview")
    self.preview_dock_widget.setFixedHeight(455)
    self.preview_dock_widget.setMinimumWidth(455)

    self.preview_scene = QGraphicsScene()
    self.preview_view = CustomGraphicsView(self.preview_scene)  
    self.preview_dock_widget.setWidget(self.preview_view)
    self.addDockWidget(Qt.LeftDockWidgetArea, self.preview_dock_widget)

    # Barre d'outils pour le zoom et les modes de mesure
    self.toolbar = QToolBar("Zoom and Measure Tools", self)
    self.addToolBar(Qt.TopToolBarArea, self.toolbar)
    
    zoom_in_action = QAction("Zoom In", self)
    zoom_in_action.triggered.connect(self.preview_view.zoom_in)
    self.toolbar.addAction(zoom_in_action)

    zoom_out_action = QAction("Zoom Out", self)
    zoom_out_action.triggered.connect(self.preview_view.zoom_out)
    self.toolbar.addAction(zoom_out_action)
    # Séparateur visuel
    self.toolbar.addSeparator()
    # Actions pour les modes de mesure
    action_measure_distance = QAction(QIcon("iconDistance.png"), "Distance Mode", self)
    action_measure_distance.triggered.connect(lambda: self.preview_view.set_measure_mode(MeasureMode.DISTANCE))
    self.toolbar.addAction(action_measure_distance)

    action_measure_angle = QAction(QIcon("iconAngle.png"), "Angle Mode", self)
    action_measure_angle.triggered.connect(lambda: self.preview_view.set_measure_mode(MeasureMode.ANGLE))
    self.toolbar.addAction(action_measure_angle)
    # Configuration de la vue et de la scène
    self.canvas = CustomGraphicsView()
    self.preview_view.setScene(self.preview_scene)
    self.preview_view.fitInView(self.preview_scene.sceneRect(), Qt.KeepAspectRatio)

    self.scene = QGraphicsScene()
    self.canvas.setScene(self.scene)

    logger.debug("End of setup_preview_dock_widget function")
def setup_central_frame(self):
    logger.debug("Start of setup_central_frame function")
    self.central_frame = QWidget()
    self.central_layout = QVBoxLayout(self.central_frame)

    self.label_files = QLabel("File List:")
    self.central_layout.addWidget(self.label_files)

    self.search_box = QLineEdit()
    self.search_box.setPlaceholderText("Search files...")
    self.central_layout.addWidget(self.search_box)

    self.search_criteria_type = QComboBox(self)
    self.search_criteria_type.addItems(["Filename", "Creation Date", "Modification Date"])
    self.central_layout.addWidget(self.search_criteria_type)

    self.start_date = QDateEdit(self)
    self.start_date.setCalendarPopup(True)
    self.start_date.setDate(QDate.currentDate())
    self.central_layout.addWidget(self.start_date)

    self.end_date = QDateEdit(self)
    self.end_date.setCalendarPopup(True)
    self.end_date.setDate(QDate.currentDate())
    self.central_layout.addWidget(self.end_date)

    self.search_button = QPushButton("Search", self)
    self.central_layout.addWidget(self.search_button)

    #self.file_list = QListView()
    self.file_list.setIconSize(QSize(160, 120))
    logger.info(f"New icon size for QListView: {self.file_list.iconSize()}")
    self.file_list.setGridSize(QSize(175, 135))
    self.file_list.setModel(self.file_list_proxy )  # Use the file list proxy model here
    self.file_list.setViewMode(QListView.IconMode)
    #self.file_list_model.setRootPath(QDir.rootPath()) 
    root_index_model_files = self.file_list_proxy.sourceModel().index(0, 0) 
    proxy_root_index = self.file_list_proxy.mapFromSource(root_index_model_files)
    self.file_list.setRootIndex(proxy_root_index)
    
    self.central_layout.addWidget(self.file_list)
    self.main_splitter.addWidget(self.central_frame)

    logger.debug("End of setup_central_frame function")
def on_signal_emitted(self, index):
    logger.debug(f"Start on_signal_emitted (self.update_file_list(index))")
    logger.debug(f"Signal emitted. Index: {index}")
    if index.isValid():
        folder_path = self.file_list_model.filePath(index)
        self.current_folder = folder_path  
        self.update_file_list_view(folder_path)
    else:
        logger.error("Invalid index received in on_signal_emitted")
    logger.debug(f"End on_signal_emitted")
def on_folder_double_clicked(self, index):
    logger.debug("Start on_folder_double_clicked")
    if not index.isValid():
        logger.error("The index is not valid.")
        QMessageBox.warning(self, "Error", "Invalid index selected.")
        return
    # Détermination du modèle de base et de l'indice source
    if isinstance(index.model(), QSortFilterProxyModel):
        source_index = index.model().mapToSource(index)
        if index.model() == self.file_list_proxy:
            base_model = self.file_list_model
            logger.info("Index from file list proxy")
        elif index.model() == self.tree_proxy:
            base_model = self.tree_model
            logger.info("Index from tree proxy")
        else:
            QMessageBox.warning(self, "Error", "Unknown model type")
            return
    else:
        source_index = index
        base_model = index.model()
    folder_path = base_model.filePath(source_index)
    if os.path.isdir(folder_path) and os.access(folder_path, os.R_OK):
        logger.info(f"Selected folder path: {folder_path}")
        self.current_folder = folder_path  
        logger.debug(f"[on_folder_double_clicked] Current folder updated to: {self.current_folder}")  
        self.file_list_model.setRootPath(folder_path)
        logger.info(f"Root path of the file list model updated: {folder_path}")
        # Mise à jour de l'indice root de la vue
        updated_root_index = self.file_list_model.indexFromPath(folder_path)
        if hasattr(self, 'file_list_proxy'):
            proxy_root_index = self.file_list_proxy.mapFromSource(updated_root_index)
            if proxy_root_index.isValid():
               self.file_list.setRootIndex(proxy_root_index)
            else:
                logger.error("Invalid proxy root index after mapping.")
                QMessageBox.warning(None, "Mapping Error", "Invalid proxy root index after mapping.")
        else:
            self.file_list.setRootIndex(updated_root_index)
        logger.info("List view updated to display the content of the selected folder.")
        self.update_directory_label(f"Current folder path: {self.current_folder}")  
    else:
        msg = "The selected path is not a directory." if not os.path.isdir(folder_path) else "The directory is not accessible."
        logger.error(f"{msg}: {folder_path}")
        QMessageBox.warning(self, "Error", msg)
    logger.debug("End on_folder_double_clicked")

def on_tree_clicked(self, index):
    logger.debug("Start of on_tree_clicked")
    
    if self._in_search_mode:
        logger.info("Exiting search mode after tree click.")
        self._in_search_mode = False
        self.file_list.setModel(self.file_list_proxy)
        self.search_box.clear()
    logger.debug(f"Start processing index: {index}")
    if not index.isValid():
        logger.error("The provided index is invalid.")
        return

    # Map the proxy index to the source index
    source_index = self.tree_proxy.mapToSource(index) if isinstance(index.model(), QSortFilterProxyModel) else index
    if not source_index.isValid():
        logger.error("Index mapping failed. Attempting reset.")
        return

    # Get the directory path
    try:
        directory_path = self.tree_model.filePath(source_index)
        self.current_folder = directory_path  
    except IndexError:
        logger.error(f"Error getting directory path from index: {source_index}")
        return
    assert os.path.isdir(directory_path), f"Invalid directory path: {directory_path}"
    logger.debug(f"Directory path: {directory_path}")

    #Only update if the directory is different
    if directory_path == self.file_list_model.rootPath():
        logger.debug("Selected directory is already the active directory. Skipping update.")
        return

    # Update the file list model 
    logger.info(f"Updating file list model for directory: {directory_path}")
    self.file_list_model.setRootPath(directory_path)

    # Directly update the list view after setting the root path
    list_root_index = self.file_list_model.indexFromPath(directory_path)
    self.update_view_index(self.file_list_model, self.file_list, list_root_index)

    self.update_directory_label(f"Current folder path: {self.current_folder}")  
    logger.info(f"[on_tree_clicked] Current folder path updated to: {directory_path}")
    logger.debug("End of on_tree_clicked")

def update_view_index(self, model, view, root_index):
    """Updates the view's index based on the model and root index."""
    if hasattr(self, 'file_list_proxy') and model is self.file_list_model:
        index_proxy = self.file_list_proxy.mapFromSource(root_index)
        if index_proxy.isValid():
            view.setRootIndex(index_proxy)
        else:
            logger.error("Root proxy index is not valid after mapping.")
    else:
        view.setRootIndex(root_index)
def load_folder(self):
    """Loads a folder selected by the user."""
    logger.info("Start of load_folder function")
    folder_path = QFileDialog.getExistingDirectory(self, "Choose a folder")
    if folder_path:
        try:
            # Check if the path is accessible (read and write)
            if not os.access(folder_path, os.R_OK | os.W_OK):
                raise OSError("The selected folder is not accessible for read or write.")

            logger.info(f"Selected folder path: {folder_path}")
            self.current_folder = folder_path
            self.update_directory_label(f"Current folder path: {self.current_folder}")
            logger.debug(f"[load_folder] Current folder updated to: {self.current_folder}")

            # Update path in the list model
            self.file_list_model.setRootPath(self.current_folder)

            # Update list view index
            list_root_index = self.file_list_model.indexFromPath(self.current_folder)
            self.update_view_index(self.file_list_model, self.file_list, list_root_index)

            self.folderChanged.emit(folder_path)

        except OSError as e:
            QMessageBox.warning(self, "Error", str(e))
            logger.error(f"Error accessing folder: {e}")
    else:
        logger.warning("No folder selected.")
    logger.info("End of load_folder function")

def navigate_up(self):
    logger.info("Start navigate_up")
    logger.debug("Attempting to navigate up one level in the file tree.")
    current_index = self.file_list.rootIndex()
    index_parent = self.file_list_proxy.mapToSource(current_index).parent()  # Use the correct proxy model here
    if index_parent.isValid():
        self.file_list.setRootIndex(self.file_list_proxy.mapFromSource(index_parent))
        folder_path = self.file_list_model.filePath(index_parent)
        self.current_folder = folder_path  
        self.update_directory_label(f"Current folder path: {self.current_folder}") 
        logger.info(f"Current directory updated to: {self.current_folder}")  
    else:
        logger.warning("Unable to navigate up, no valid parent found.")
    logger.info("End navigate_up")

def on_tree_selection_changed(self, selected, deselected):
    logger.debug("Function on_tree_selection_changed start")
    index = self.tree_view.currentIndex()
    logger.debug(f"Current index in the tree: {index}")
    self.navigate_folder(index)
    logger.debug("Function on_tree_selection_changed end")

def on_list_selection_changed(self, selected, deselected):
    logger.debug("Function on_list_selection_changed start")
    index_source = self.file_list.currentIndex()
    logger.debug(f"Current source index in the list: {index_source}")
    self.navigate_folder(index_source)
    logger.debug("Function on_list_selection_changed end")

def navigate_folder(self, index):
    logger.debug("Function navigate_folder start")
    if not index.isValid():
        logger.error("Error: Invalid index provided for navigation.")
        return
    folder_path = self.file_list_model.filePath(index)
    if not os.path.exists(folder_path):
        logger.error(f"Error: Folder path does not exist - {folder_path}")
        return
    self.current_folder = folder_path  # Mise à jour du dossier courant  
    self.file_list_model.setRootPath(self.current_folder)
    logger.info(f"Updated root path of the file list model to: {self.current_folder}")
    self.update_file_list_view(folder_path)
    self.update_directory_label(f"Current folder: {self.current_folder}")
    logger.debug("Function navigate_folder end")

def change_list_view(self):
    logger.info("Start change_list_view")
    logger.info("Switching view to List View")
    self.file_list.setViewMode(QListView.ListMode)
    logger.debug("File view switched to list.")
    logger.info("End change_list_view")

def change_thumbnail_view(self):
    logger.info("Start change_thumbnail_view")
    logger.info("Switching view to Thumbnail View")
    self.file_list.setViewMode(QListView.IconMode)
    logger.debug("File view switched to thumbnails.")
    logger.info("End change_thumbnail_view")

def setupMenu(self):
    logger.info("Start setupMenu")
    logger.debug("Setting up the menu bar")
    menuBar = self.menuBar()
    fileMenu = menuBar.addMenu("&File")
    openAction = QAction("&Open", self)
    openAction.setShortcut("Ctrl+O")
    openAction.triggered.connect(self.load_folder)  # Assuming `load_folder` is your method to open a folder
    exitAction = QAction("&Exit", self)
    exitAction.setShortcut("Ctrl+Q")
    exitAction.triggered.connect(self.close)
    fileMenu.addAction(openAction)
    fileMenu.addSeparator()
    fileMenu.addAction(exitAction)
    viewMenu = menuBar.addMenu("&View")
    listViewAction = QAction("&List View", self)
    listViewAction.triggered.connect(self.change_list_view)
    viewMenu.addAction(listViewAction)
    thumbnailViewAction = QAction("&Thumbnail View", self)
    thumbnailViewAction.triggered.connect(self.change_thumbnail_view)
    viewMenu.addAction(thumbnailViewAction)
    logger.debug("Menu bar configured with File and View menus.")
    logger.info("End setupMenu")

def setupToolbar(self):
    logger.info("Start setupToolbar")
    logger.debug("Setting up the toolbar")
    toolbar = self.addToolBar("Main Toolbar")
    returnAction = QAction(QIcon("iconReturn.png"), "Return", self) 
    toolbar.setIconSize(QSize(24, 24))
    returnAction.triggered.connect(self.navigate_up)
    toolbar.addAction(returnAction)
    logger.debug("Toolbar configured")
    logger.info("End setupToolbar")

def closeEvent(self, event):
    logger.info("Function closeEvent start")
    logger.info("Closing application, stopping threads and observer.")
    try:
        if hasattr(self, 'observer') and self.observer:
            self.observer.stop()
            self.observer.join()
        if hasattr(self, 'searchThread') and self.searchThread.isRunning():
            self.SearchWorker.running = False  # Assurez-vous que SearchWorker peut être arrêté proprement
            #self.SearchWorker.stop() 
            self.searchThread.quit()
            self.searchThread.wait()  # Wait up to 0 seconds for the thread to cleanly finish
    except Exception as e:
        logger.error(f"Error during closing: {e}")
    finally:
        super().closeEvent(event)
    logger.info("Function closeEvent end")

def display_file(self, file):
    logger.debug(f"Start of display_file function with file: {file}")
    try:
        if os.path.isdir(file):
            self.display_folder(file)
        elif file.lower().endswith(('.png', '.jpg', '.jpeg')):
            self.display_image(file)
        elif file.lower().endswith('.dxf'):
            self.display_dxf(file)
        else:
            self.display_default_message()
    except Exception as e:
        logger.error(f"Error displaying file {file}: {e}")
        self.display_default_message()
    logger.info(f"End of display_file function")

def display_folder(self, folder):
    logger.info(f" Function display_folder / Displaying folder: {folder}")
    self.current_folder = folder  # Mise à jour du dossier courant  
    self.update_directory_label(f"Current folder: {self.current_folder}")  
    # Ajoutez le code nécessaire pour afficher le contenu du dossier si nécessaire  

def display_selected_file(self, index):
    """Affiche le fichier sélectionné dans le dock de prévisualisation."""
    logger.info("Start of display_selected_file")
    logger.debug(f"Function display_selected_file with index: {index}")
    logger.debug(f"État du thread avant la sélection: {QThread.currentThread()}") 
    try:

        # Déterminer le modèle à utiliser en fonction du mode de recherche
        if self._in_search_mode:
            model = self.search_results_proxy_model
            logger.debug("Mode recherche activé. Utilisation de search_results_proxy_model.")
        else:
            model = self.file_list_proxy 
            logger.debug("Mode recherche désactivé. Utilisation de file_list_proxy.")
        
        if not model:
            logger.error("Model is not initialized")
            return

        # Convertir l'index du proxy en index du modèle source
        source_index = model.mapToSource(index)

        if not source_index.isValid():
            logger.error("Invalid index for file selection")
            return

        file_path = self.file_list_model.filePath(source_index)
        if not file_path:
            logger.error("File path could not be retrieved")
            return

        logger.debug(f"Ready to open: {file_path}")
        logger.info(f"Displaying file for index {index}: {file_path}")
        logger.debug(f"FileViewer.display_selected_file() - Appel de display_file pour {file_path}")
        self.display_file(file_path)
        self.fileSelected.emit(file_path)
        logger.info(f"Emitted fileSelected signal for: {file_path}")
    except Exception as e:
        logger.error(f"Error in display_selected_file: {e}")
    finally:
        logger.info("End of display_selected_file")

def display_image(self, image_path):
    logger.info(f"Start display_image")
    pixmap = QPixmap(image_path)
    self.preview_scene.clear()
    item = QGraphicsPixmapItem(pixmap)
    item.setTransform(QTransform()) 
    self.preview_scene.addItem(item)
    rectF = QRectF(pixmap.rect())
    self.preview_scene.setSceneRect(rectF)
    self.preview_view.setTransform(QTransform())
    self.preview_view.fitInView(rectF, Qt.KeepAspectRatio)
    self.preview_view.viewport().update()
    #self.preview_view.update() 
    logger.info(f"Image displayed in preview: {image_path}")
    logger.info(f"End display_image")

def display_dxf(self, file):
    logger.debug("Starting function display_dxf")
    try:
        self.preview_scene.clear()
        doc = ezdxf.readfile(file)
        msp = doc.modelspace()
        window_size = self.preview_view.size()
        scale_factor = self.calculate_scale_factor(msp, window_size)
        self.scale_factor = scale_factor  # Stocker le facteur d'échelle
        entities_to_add = []  # Use a list to batch add if possible
        for ent in msp:
            if ent.dxftype() == 'LINE':
                entities_to_add.append(self.add_line(ent, scale_factor))
            elif ent.dxftype() == 'CIRCLE':
                entities_to_add.append(self.add_circle(ent, scale_factor))
            elif ent.dxftype() == 'SPLINE':
                entities_to_add.append(self.add_spline(ent, scale_factor))
            elif ent.dxftype() == 'POINT':
                entities_to_add.append(self.add_point(ent, scale_factor))
            elif ent.dxftype() == 'ARC':
                entities_to_add.append(self.add_arc(ent, scale_factor))
            elif ent.dxftype() == 'ELLIPSE':
                entities_to_add.append(self.add_ellipse(ent, scale_factor))
            elif ent.dxftype() == 'TEXT':
                entities_to_add.append(self.add_text(ent, scale_factor)) 
            #elif ent.dxftype() == 'MTEXT':
                #entities_to_add.append(self.add_mtext(ent, scale_factor))
            elif ent.dxftype() == '3DFACE':
                entities_to_add.append(self.add_3dface(ent, scale_factor))
            elif ent.dxftype() == 'SOLID':
                entities_to_add.append(self.add_solid(ent, scale_factor))
            elif ent.dxftype() == 'POLYLINE':
                entities_to_add.extend(self.add_polyline(ent, scale_factor))
            elif ent.dxftype() == 'INSERT':
                entities_to_add.extend(self.add_insertion(ent, doc.blocks, scale_factor))
            elif ent.dxftype() == 'LWPOLYLINE':
                entities_to_add.append(self.add_lwpolyline(ent, scale_factor))
        
        # Adjust the view to fit the scene
        self.preview_view.setTransform(QtGui.QTransform().scale(1, -1).rotate(0))
        self.preview_scene.setSceneRect(self.preview_scene.itemsBoundingRect())
        self.preview_view.fitInView(self.preview_scene.sceneRect(), Qt.KeepAspectRatio)
        #self.preview_view.resetCachedContent()
        self.preview_view.viewport().update()
        #self.preview_view.update()
        logger.debug("Successfully displayed DXF.")
    except ezdxf.DXFError as dxf_error:
        logger.error(f"DXF Error: {dxf_error}")
        self.preview_scene.clear()
        self.preview_scene.addText("Error reading DXF file")
        #self.preview_scene.addText("Error reading DXF file")
    except Exception as e:
        logger.error(f"Unexpected Error: {type(e).__name__} - {e}")
        self.preview_scene.clear()
        self.preview_scene.addText("Unexpected error while displaying DXF file")
        #self.preview_scene.addText("Unexpected error while displaying DXF file")
        #SUITE DU SCRIPT ...

Pour résoudre les problèmes mentionnés dans votre script, analysons chaque problème séparément et développons une stratégie :

Problème avec mapFromSource recevant un sourceIndex invalide en mode normal : Analyse : Il semble que la méthode mapFromSource reçoive des indices invalides dans certains cas. Cela peut provenir de différences entre le modèle source et le modèle de proxy ou d'erreurs dans la gestion des indices lors de la transition entre les différents modes (comme le mode recherche et le mode normal). Stratégie de résolution :
Assurez-vous que les chemins des fichiers sont correctement normalisés et correspondants entre le modèle source et les modèles de proxy.
Ajoutez des vérifications pour valider sourceIndex avant son utilisation dans mapFromSource.
Vérifiez que l'index source utilisé correspond correctement à un chemin de fichier connu dans les résultats de recherche ou dans le répertoire actuel.
Assurez-vous que le modèle source de la vue est correctement défini lors de chaque changement de mode.
Erreur sur "Le chemin du dossier doit être une chaîne de caractères valide" lors de l'appel de update_file_list_view : Analyse : Cette erreur indique que update_file_list_view est appelé avec un chemin qui n'est pas valide ou n'est pas une chaîne. Cela peut se produire lorsque update_file_list_view est appelé sans arguments corrects, ou lorsqu'un index invalide est passé et que le modèle ne peut pas obtenir le chemin correspondant. Stratégie de résolution :
Assurez-vous que update_file_list_view est toujours appelé avec un argument valide pour le chemin ou l'index.
Ajoutez des vérifications au début de update_file_list_view pour détecter et traiter les valeurs incorrectes avant de procéder.
Vérifiez l'intégrité des données (current_folder, previous_folder, etc.) pour vous assurer qu'elles contiennent bien des chemins valides avant d'appeler cette fonction.
Problèmes généraux d'intégrité des chemins et des indices : Stratégie générale :
Revisitez toutes les transitions et les conversions entre les indices des modèles et assurez-vous que chaque conversion (ex. : mapFromSource, mapToSource) est correcte et attendu.
Documentez et organisez mieux le flux d'information entre les méthodes et les objets pour s'assurer que chaque fonction a toutes les informations nécessaires à l'exécution correcte.
Ajoutez un logging de diagnostic plus détaillé (en utilisant différents niveaux log, comme DEBUG ou INFO) pour vérifier l'état interne et détecter les valeurs incorrectes dès que possible.