diff --git a/bagheeraview.py b/bagheeraview.py index 1a0c6f1..4857957 100755 --- a/bagheeraview.py +++ b/bagheeraview.py @@ -1037,6 +1037,7 @@ class MainWindow(QMainWindow): self._group_info_cache = {} self._visible_paths_cache = None # Cache for visible image paths self._path_to_model_index = {} + self._paths_being_modified_by_app = set() # For ignoring FS events # Keep references to open viewers to manage their lifecycle self.viewers = [] @@ -1349,6 +1350,10 @@ class MainWindow(QMainWindow): self.fs_watcher.monitoring_status_changed.connect( self.on_fs_watcher_status_changed) + # Set up callback for metadata managers + from metadatamanager import set_app_modified_callback + set_app_modified_callback(self._mark_path_as_app_modified) + # Batching for file creation events self._fs_created_queue = set() self._fs_created_timer = QTimer(self) @@ -4863,22 +4868,52 @@ class MainWindow(QMainWindow): if path not in self._known_paths: return # Not a file we're tracking - # Invalidate cache and trigger a refresh of its metadata and thumbnail - self.cache.invalidate_path(path) + # If this modification was initiated by the app, ignore it + if path in self._paths_being_modified_by_app: + return - # Re-read metadata and thumbnail - res = load_common_metadata(path) - mtime = os.path.getmtime(path) - stat_res = os.stat(path) - inode = stat_res.st_ino - dev = stat_res.st_dev + # External modification: check if it's metadata-only or content change + try: + new_stat = os.stat(path) + new_mtime = new_stat.st_mtime + new_size = new_stat.st_size - # Update internal data and model - self._update_internal_data(path, mtime=mtime, tags=res.tags, rating=res.rating, - inode=inode, dev=dev) - self.proxy_model.add_to_cache(path, res.tags) - self.rebuild_view() - self.status_lbl.setText(f"File modified: {os.path.basename(path)}") + # Find old data from internal list + old_item_data = next((item for item in self.found_items_data if item[0] == path), None) + old_mtime = old_item_data[2] if old_item_data else 0 + old_size = os.path.getsize(path) if old_item_data else 0 # Re-read size from disk for comparison + + if new_size == old_size and new_mtime != old_mtime: + # Likely metadata-only change (size unchanged, mtime changed) + res = load_common_metadata(path) + self._update_internal_data(path, mtime=new_mtime, tags=res.tags, rating=res.rating, + inode=new_stat.st_ino, dev=new_stat.st_dev) + self.proxy_model.add_to_cache(path, res.tags) + self.thumbnail_view.viewport().update() # Force repaint + self.status_lbl.setText(f"Metadata updated: {os.path.basename(path)}") + else: + # Content or size changed, invalidate thumbnail and rebuild view + self.cache.invalidate_path(path) + res = load_common_metadata(path) # Re-read metadata as well + self._update_internal_data(path, mtime=new_mtime, tags=res.tags, rating=res.rating, + inode=new_stat.st_ino, dev=new_stat.st_dev) + self.proxy_model.add_to_cache(path, res.tags) + self.rebuild_view() + self.status_lbl.setText(f"File modified: {os.path.basename(path)}") + except Exception: + # Fallback to full refresh if error occurs + self.refresh_content() + + def _mark_path_as_app_modified(self, path): + """Marks a path as being modified by the application to ignore FS events.""" + abs_path = os.path.abspath(path) + parent_path = os.path.dirname(abs_path) + self._paths_being_modified_by_app.add(abs_path) + self._paths_being_modified_by_app.add(parent_path) + + # Schedule removal after a delay to allow all FS events to propagate + QTimer.singleShot(1000, lambda: self._paths_being_modified_by_app.discard(abs_path)) + QTimer.singleShot(1000, lambda: self._paths_being_modified_by_app.discard(parent_path)) def on_fs_watcher_status_changed(self, is_monitoring): """Updates the UI indicator for the FileSystemWatcher.""" @@ -4893,6 +4928,9 @@ class MainWindow(QMainWindow): """Handles a directory being modified (e.g., new subfolder, mass changes).""" path = os.path.abspath(path) + if path in self._paths_being_modified_by_app: + return + # Trigger a debounced full refresh. This is useful for syncing large # external changes (bulk operations, directory deletions) that are # more robustly handled by a full scan than incremental updates. diff --git a/duplicatedialog.py b/duplicatedialog.py index e59ead7..addd3d4 100644 --- a/duplicatedialog.py +++ b/duplicatedialog.py @@ -1,16 +1,16 @@ import os from datetime import datetime from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame, + QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QSplitter, QWidget, QMessageBox, QApplication, QMenu, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView ) -from PySide6.QtGui import QPixmap, QIcon, QImageReader, QImage, QDesktopServices -from PySide6.QtCore import Qt, QSize, QTimer, QUrl +from PySide6.QtGui import QIcon, QImage, QDesktopServices +from PySide6.QtCore import Qt, QTimer, QUrl from imageviewer import ImagePane -from imagecontroller import ImageController -from constants import UITexts, APP_CONFIG from propertiesdialog import PropertiesDialog +from constants import APP_CONFIG, UITexts + class DuplicateManagerDialog(QDialog): """ @@ -18,7 +18,7 @@ class DuplicateManagerDialog(QDialog): """ def __init__(self, duplicates, duplicate_cache, main_win, review_mode=False): super().__init__(main_win) - self.duplicates = duplicates # List of DuplicateResult + self.duplicates = duplicates # List of DuplicateResult self.cache = duplicate_cache self.main_win = main_win self.review_mode = review_mode @@ -35,7 +35,8 @@ class DuplicateManagerDialog(QDialog): self._populate_list() if self.main_win and hasattr(self.main_win, 'fs_watcher'): - self.main_win.fs_watcher.file_deleted.connect(self._on_file_deleted_externally) + self.main_win.fs_watcher.file_deleted.connect( + self._on_file_deleted_externally) self.main_win.fs_watcher.file_moved.connect(self._on_file_moved_externally) if self.duplicates: @@ -59,15 +60,22 @@ class DuplicateManagerDialog(QDialog): self.table_widget = QTableWidget() if self.review_mode: self.table_widget.setColumnCount(3) - self.table_widget.setHorizontalHeaderLabels([UITexts.IGNORED_DATE, "%", UITexts.CONTEXT_MENU_OPEN]) - self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) - self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) - self.table_widget.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) + self.table_widget.setHorizontalHeaderLabels( + [UITexts.IGNORED_DATE, "%", UITexts.CONTEXT_MENU_OPEN]) + self.table_widget.horizontalHeader().setSectionResizeMode( + 0, QHeaderView.ResizeToContents) + self.table_widget.horizontalHeader().setSectionResizeMode( + 1, QHeaderView.ResizeToContents) + self.table_widget.horizontalHeader().setSectionResizeMode( + 2, QHeaderView.Stretch) else: self.table_widget.setColumnCount(2) - self.table_widget.setHorizontalHeaderLabels(["%", UITexts.CONTEXT_MENU_OPEN]) # Usamos una cadena existente o genérica - self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) - self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) + self.table_widget.setHorizontalHeaderLabels( + ["%", UITexts.CONTEXT_MENU_OPEN]) # Usamos una cadena existente o genérica + self.table_widget.horizontalHeader().setSectionResizeMode( + 0, QHeaderView.ResizeToContents) + self.table_widget.horizontalHeader().setSectionResizeMode( + 1, QHeaderView.Stretch) self.table_widget.verticalHeader().setVisible(False) self.table_widget.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -268,11 +276,20 @@ class DuplicateManagerDialog(QDialog): pane.zoom_manager.calculate_initial_zoom(w, h, True) self.update_view_for_pane(pane) - def reset_inactivity_timer(self): pass - def sync_filmstrip_selection(self, index): pass - def _get_clicked_face_for_pane(self, pane, pos): return None - def rename_face(self, face): pass - def toggle_fullscreen(self): pass + def reset_inactivity_timer(self): + pass + + def sync_filmstrip_selection(self, index): + pass + + def _get_clicked_face_for_pane(self, pane, pos): + return None + + def rename_face(self, face): + pass + + def toggle_fullscreen(self): + pass def _create_comparison_pane_widget(self): widget = QWidget() diff --git a/metadatamanager.py b/metadatamanager.py index 443f505..f089a1b 100644 --- a/metadatamanager.py +++ b/metadatamanager.py @@ -16,6 +16,9 @@ try: except ImportError: exiv2 = None HAVE_EXIV2 = False + +_app_modified_callback = None + from utils import preserve_mtime from constants import RATING_XATTR_NAME, XATTR_NAME @@ -23,6 +26,15 @@ MetadataResult = collections.namedtuple('MetadataResult', ['tags', 'rating']) EMPTY_METADATA = MetadataResult([], 0) +def set_app_modified_callback(callback): + global _app_modified_callback + _app_modified_callback = callback + +def mark_app_modified(path): + """Triggers the application-modified callback for a path.""" + if _app_modified_callback: + _app_modified_callback(path) + def notify_baloo(path): """ Notifies the Baloo file indexer about a file change using DBus. @@ -148,6 +160,7 @@ class XattrManager: return try: with preserve_mtime(file_path): + mark_app_modified(file_path) if value: os.setxattr(file_path, attr_name, str(value).encode('utf-8')) else: diff --git a/xmpmanager.py b/xmpmanager.py index debb66c..68c1c9e 100644 --- a/xmpmanager.py +++ b/xmpmanager.py @@ -15,10 +15,11 @@ Dependencies: - utils.preserve_mtime: A utility to prevent file modification times from changing during metadata writes. """ +from importlib.resources import path import os import re from utils import preserve_mtime -from metadatamanager import notify_baloo +from metadatamanager import notify_baloo, mark_app_modified try: import exiv2 except ImportError: @@ -38,8 +39,9 @@ class XmpManager: This method parses the XMP data structure for a `mwg-rs:RegionList`, extracts all regions of type 'Face', and returns them as a list of - dictionaries. Each dictionary contains the face's name and its - normalized coordinates (center x, center y, width, height). + dictionaries. + Each dictionary contains the face's name and its normalized coordinates + (center x, center y, width, height). Args: path (str): The path to the image file. @@ -161,7 +163,9 @@ class XmpManager: xmp[f"{area_base}/stArea:unit"] = 'normalized' img.writeMetadata() + notify_baloo(path) + mark_app_modified(path) return True except Exception as e: print(f"Error saving faces to XMP: {e}")