Fixed thumbnail reload on metadata change
This commit is contained in:
@@ -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
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
mtime = os.path.getmtime(path)
|
||||
stat_res = os.stat(path)
|
||||
inode = stat_res.st_ino
|
||||
dev = stat_res.st_dev
|
||||
|
||||
# Update internal data and model
|
||||
self._update_internal_data(path, mtime=mtime, tags=res.tags, rating=res.rating,
|
||||
inode=inode, dev=dev)
|
||||
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.
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user