diff --git a/bagheeraview.py b/bagheeraview.py index a1c9c80..3ed19fb 100755 --- a/bagheeraview.py +++ b/bagheeraview.py @@ -34,7 +34,7 @@ from itertools import groupby from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QHBoxLayout, QLineEdit, QTextEdit, QPushButton, QFileDialog, QComboBox, QSlider, QMessageBox, QSizePolicy, - QMenu, QInputDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget, + QMenu, QInputDialog, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget, QProgressDialog, QDockWidget, QAbstractItemView, QRadioButton, QButtonGroup, QListView, QStyledItemDelegate, QStyle, QDialog, QKeySequenceEdit, QDialogButtonBox ) @@ -2010,6 +2010,9 @@ class MainWindow(QMainWindow): def perform_shutdown(self): """Performs cleanup operations before the application closes.""" + if getattr(self, '_is_shutting_down', False): + return + self._is_shutting_down = True self.is_cleaning = True # Save configuration early if visible, as per user request. @@ -2017,6 +2020,24 @@ class MainWindow(QMainWindow): if self.isVisible(): self.save_config() + # Stop all pending UI timers to prevent background tasks from triggering + self.hide_progress_timer.stop() + self.filter_input_timer.stop() + self.filter_refresh_timer.stop() + self.thumbnails_refresh_timer.stop() + self.rebuild_timer.stop() + self._model_update_timer.stop() + self.resume_scan_timer.stop() + + # Close all viewers first to stop their preloader threads + self.close_all_viewers() + + # Close duplicate manager if open to stop its preloader threads + if HAVE_IMAGEHASH: + for widget in QApplication.topLevelWidgets(): + if isinstance(widget, DuplicateManagerDialog): + widget.close() + self.fs_watcher.stop() # 1. Stop all worker threads interacting with the cache @@ -2053,7 +2074,19 @@ class MainWindow(QMainWindow): # Ensure all QRunnables in the shared thread pool are finished if self.thread_pool_manager: - self.thread_pool_manager.get_pool().waitForDone() + pool = self.thread_pool_manager.get_pool() + if pool.activeThreadCount() > 0: + progress = QProgressDialog(UITexts.SHUTTING_DOWN, None, 0, 0, self) + progress.setWindowTitle(PROG_NAME) + progress.setWindowModality(Qt.ApplicationModal) + progress.setMinimumDuration(0) + progress.show() + + # Wait in small increments to keep the UI responsive + while not pool.waitForDone(100): + if QApplication.instance(): + QApplication.processEvents() + progress.close() if self.duplicate_cache: self.duplicate_cache.lmdb_close() diff --git a/duplicatecache.py b/duplicatecache.py index 4655246..b4ace7b 100644 --- a/duplicatecache.py +++ b/duplicatecache.py @@ -571,6 +571,7 @@ class DuplicateDetector(QThread): def stop(self): self._is_running = False + self.wait() # Add this line def run(self): total_files = len(self.paths_to_scan) diff --git a/duplicatedialog.py b/duplicatedialog.py index 46e5f9e..6195c4f 100644 --- a/duplicatedialog.py +++ b/duplicatedialog.py @@ -143,6 +143,11 @@ class DuplicateManagerDialog(QDialog): self.main_win.fs_watcher.file_moved.disconnect(self._on_file_moved_externally) except (RuntimeError, TypeError): pass + + if hasattr(self, 'left_pane') and self.left_pane: + self.left_pane.cleanup() + if hasattr(self, 'right_pane') and self.right_pane: + self.right_pane.cleanup() super().closeEvent(event) def resizeEvent(self, event): diff --git a/imagecontroller.py b/imagecontroller.py index 4cc5f21..034b225 100644 --- a/imagecontroller.py +++ b/imagecontroller.py @@ -42,6 +42,7 @@ class ImagePreloader(QThread): def __init__(self): """Initializes the preloader thread.""" super().__init__() + self.setObjectName("ImagePreloaderThread") self.path = None self.index = -1 self.mutex = QMutex() diff --git a/imagescanner.py b/imagescanner.py index e6db0a0..2e71463 100644 --- a/imagescanner.py +++ b/imagescanner.py @@ -134,13 +134,11 @@ class ScannerWorker(QRunnable): sizes_to_check = self.target_sizes if self.target_sizes is not None \ else SCANNER_GENERATE_SIZES - if self._is_cancelled: - if self.semaphore: - self.semaphore.release() - return - - fd = None try: + if self._is_cancelled: + return + + fd = None # Optimize: Open file once to reuse FD for stat and xattrs fd = os.open(self.path, os.O_RDONLY) stat_res = os.fstat(fd) @@ -285,6 +283,7 @@ class CacheWriter(QThread): self._condition_new_data = QWaitCondition() self._condition_space_available = QWaitCondition() # Soft limit for blocking producers (background threads) + self.setObjectName("CacheWriterThread") # Add this line self._max_size = 50 self._running = True @@ -334,6 +333,7 @@ class CacheWriter(QThread): self._running = False # Do not clear the queue here; let the run loop drain it to prevent data loss. self._condition_new_data.wakeAll() + logger.debug(f"{self.objectName()} stop requested, waking all.") self._condition_space_available.wakeAll() self._mutex.unlock() @@ -380,6 +380,7 @@ class CacheWriter(QThread): self.cache._batch_write_to_lmdb(batch) except Exception as e: logger.error(f"CacheWriter batch write error: {e}") + logger.debug(f"{self.objectName()} run method exiting.") class CacheLoader(QThread): @@ -1328,6 +1329,7 @@ class CacheCleaner(QThread): def stop(self): """Signals the thread to stop.""" self._is_running = False + self.wait() def run(self): self.setPriority(QThread.IdlePriority) @@ -1914,3 +1916,4 @@ class ImageScanner(QThread): self.mutex.lock() self.condition.wakeAll() self.mutex.unlock() + self.wait() diff --git a/settings.py b/settings.py index 0347894..cc693ee 100644 --- a/settings.py +++ b/settings.py @@ -55,6 +55,7 @@ class DuplicateFileCounter(QThread): def stop(self): self._abort = True + self.wait() # Add this line def run(self): count = 0