Fixed thumbnail reload on metadata change
This commit is contained in:
@@ -1037,6 +1037,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._group_info_cache = {}
|
self._group_info_cache = {}
|
||||||
self._visible_paths_cache = None # Cache for visible image paths
|
self._visible_paths_cache = None # Cache for visible image paths
|
||||||
self._path_to_model_index = {}
|
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
|
# Keep references to open viewers to manage their lifecycle
|
||||||
self.viewers = []
|
self.viewers = []
|
||||||
@@ -1349,6 +1350,10 @@ class MainWindow(QMainWindow):
|
|||||||
self.fs_watcher.monitoring_status_changed.connect(
|
self.fs_watcher.monitoring_status_changed.connect(
|
||||||
self.on_fs_watcher_status_changed)
|
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
|
# Batching for file creation events
|
||||||
self._fs_created_queue = set()
|
self._fs_created_queue = set()
|
||||||
self._fs_created_timer = QTimer(self)
|
self._fs_created_timer = QTimer(self)
|
||||||
@@ -4863,22 +4868,52 @@ class MainWindow(QMainWindow):
|
|||||||
if path not in self._known_paths:
|
if path not in self._known_paths:
|
||||||
return # Not a file we're tracking
|
return # Not a file we're tracking
|
||||||
|
|
||||||
# Invalidate cache and trigger a refresh of its metadata and thumbnail
|
# If this modification was initiated by the app, ignore it
|
||||||
self.cache.invalidate_path(path)
|
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)
|
res = load_common_metadata(path)
|
||||||
mtime = os.path.getmtime(path)
|
self._update_internal_data(path, mtime=new_mtime, tags=res.tags, rating=res.rating,
|
||||||
stat_res = os.stat(path)
|
inode=new_stat.st_ino, dev=new_stat.st_dev)
|
||||||
inode = stat_res.st_ino
|
self.proxy_model.add_to_cache(path, res.tags)
|
||||||
dev = stat_res.st_dev
|
self.thumbnail_view.viewport().update() # Force repaint
|
||||||
|
self.status_lbl.setText(f"Metadata updated: {os.path.basename(path)}")
|
||||||
# Update internal data and model
|
else:
|
||||||
self._update_internal_data(path, mtime=mtime, tags=res.tags, rating=res.rating,
|
# Content or size changed, invalidate thumbnail and rebuild view
|
||||||
inode=inode, dev=dev)
|
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.proxy_model.add_to_cache(path, res.tags)
|
||||||
self.rebuild_view()
|
self.rebuild_view()
|
||||||
self.status_lbl.setText(f"File modified: {os.path.basename(path)}")
|
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):
|
def on_fs_watcher_status_changed(self, is_monitoring):
|
||||||
"""Updates the UI indicator for the FileSystemWatcher."""
|
"""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)."""
|
"""Handles a directory being modified (e.g., new subfolder, mass changes)."""
|
||||||
path = os.path.abspath(path)
|
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
|
# Trigger a debounced full refresh. This is useful for syncing large
|
||||||
# external changes (bulk operations, directory deletions) that are
|
# external changes (bulk operations, directory deletions) that are
|
||||||
# more robustly handled by a full scan than incremental updates.
|
# more robustly handled by a full scan than incremental updates.
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame,
|
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
|
||||||
QSplitter, QWidget, QMessageBox, QApplication, QMenu,
|
QSplitter, QWidget, QMessageBox, QApplication, QMenu,
|
||||||
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView
|
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView
|
||||||
)
|
)
|
||||||
from PySide6.QtGui import QPixmap, QIcon, QImageReader, QImage, QDesktopServices
|
from PySide6.QtGui import QIcon, QImage, QDesktopServices
|
||||||
from PySide6.QtCore import Qt, QSize, QTimer, QUrl
|
from PySide6.QtCore import Qt, QTimer, QUrl
|
||||||
from imageviewer import ImagePane
|
from imageviewer import ImagePane
|
||||||
from imagecontroller import ImageController
|
|
||||||
from constants import UITexts, APP_CONFIG
|
|
||||||
from propertiesdialog import PropertiesDialog
|
from propertiesdialog import PropertiesDialog
|
||||||
|
from constants import APP_CONFIG, UITexts
|
||||||
|
|
||||||
|
|
||||||
class DuplicateManagerDialog(QDialog):
|
class DuplicateManagerDialog(QDialog):
|
||||||
"""
|
"""
|
||||||
@@ -35,7 +35,8 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
self._populate_list()
|
self._populate_list()
|
||||||
|
|
||||||
if self.main_win and hasattr(self.main_win, 'fs_watcher'):
|
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)
|
self.main_win.fs_watcher.file_moved.connect(self._on_file_moved_externally)
|
||||||
|
|
||||||
if self.duplicates:
|
if self.duplicates:
|
||||||
@@ -59,15 +60,22 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
self.table_widget = QTableWidget()
|
self.table_widget = QTableWidget()
|
||||||
if self.review_mode:
|
if self.review_mode:
|
||||||
self.table_widget.setColumnCount(3)
|
self.table_widget.setColumnCount(3)
|
||||||
self.table_widget.setHorizontalHeaderLabels([UITexts.IGNORED_DATE, "%", UITexts.CONTEXT_MENU_OPEN])
|
self.table_widget.setHorizontalHeaderLabels(
|
||||||
self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
[UITexts.IGNORED_DATE, "%", UITexts.CONTEXT_MENU_OPEN])
|
||||||
self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
self.table_widget.horizontalHeader().setSectionResizeMode(
|
||||||
self.table_widget.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
|
0, QHeaderView.ResizeToContents)
|
||||||
|
self.table_widget.horizontalHeader().setSectionResizeMode(
|
||||||
|
1, QHeaderView.ResizeToContents)
|
||||||
|
self.table_widget.horizontalHeader().setSectionResizeMode(
|
||||||
|
2, QHeaderView.Stretch)
|
||||||
else:
|
else:
|
||||||
self.table_widget.setColumnCount(2)
|
self.table_widget.setColumnCount(2)
|
||||||
self.table_widget.setHorizontalHeaderLabels(["%", UITexts.CONTEXT_MENU_OPEN]) # Usamos una cadena existente o genérica
|
self.table_widget.setHorizontalHeaderLabels(
|
||||||
self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
["%", UITexts.CONTEXT_MENU_OPEN]) # Usamos una cadena existente o genérica
|
||||||
self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
|
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.verticalHeader().setVisible(False)
|
||||||
self.table_widget.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.table_widget.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
@@ -268,11 +276,20 @@ class DuplicateManagerDialog(QDialog):
|
|||||||
pane.zoom_manager.calculate_initial_zoom(w, h, True)
|
pane.zoom_manager.calculate_initial_zoom(w, h, True)
|
||||||
self.update_view_for_pane(pane)
|
self.update_view_for_pane(pane)
|
||||||
|
|
||||||
def reset_inactivity_timer(self): pass
|
def reset_inactivity_timer(self):
|
||||||
def sync_filmstrip_selection(self, index): pass
|
pass
|
||||||
def _get_clicked_face_for_pane(self, pane, pos): return None
|
|
||||||
def rename_face(self, face): pass
|
def sync_filmstrip_selection(self, index):
|
||||||
def toggle_fullscreen(self): pass
|
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):
|
def _create_comparison_pane_widget(self):
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
exiv2 = None
|
exiv2 = None
|
||||||
HAVE_EXIV2 = False
|
HAVE_EXIV2 = False
|
||||||
|
|
||||||
|
_app_modified_callback = None
|
||||||
|
|
||||||
from utils import preserve_mtime
|
from utils import preserve_mtime
|
||||||
from constants import RATING_XATTR_NAME, XATTR_NAME
|
from constants import RATING_XATTR_NAME, XATTR_NAME
|
||||||
|
|
||||||
@@ -23,6 +26,15 @@ MetadataResult = collections.namedtuple('MetadataResult', ['tags', 'rating'])
|
|||||||
EMPTY_METADATA = MetadataResult([], 0)
|
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):
|
def notify_baloo(path):
|
||||||
"""
|
"""
|
||||||
Notifies the Baloo file indexer about a file change using DBus.
|
Notifies the Baloo file indexer about a file change using DBus.
|
||||||
@@ -148,6 +160,7 @@ class XattrManager:
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
with preserve_mtime(file_path):
|
with preserve_mtime(file_path):
|
||||||
|
mark_app_modified(file_path)
|
||||||
if value:
|
if value:
|
||||||
os.setxattr(file_path, attr_name, str(value).encode('utf-8'))
|
os.setxattr(file_path, attr_name, str(value).encode('utf-8'))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ Dependencies:
|
|||||||
- utils.preserve_mtime: A utility to prevent file modification times from
|
- utils.preserve_mtime: A utility to prevent file modification times from
|
||||||
changing during metadata writes.
|
changing during metadata writes.
|
||||||
"""
|
"""
|
||||||
|
from importlib.resources import path
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from utils import preserve_mtime
|
from utils import preserve_mtime
|
||||||
from metadatamanager import notify_baloo
|
from metadatamanager import notify_baloo, mark_app_modified
|
||||||
try:
|
try:
|
||||||
import exiv2
|
import exiv2
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -38,8 +39,9 @@ class XmpManager:
|
|||||||
|
|
||||||
This method parses the XMP data structure for a `mwg-rs:RegionList`,
|
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
|
extracts all regions of type 'Face', and returns them as a list of
|
||||||
dictionaries. Each dictionary contains the face's name and its
|
dictionaries.
|
||||||
normalized coordinates (center x, center y, width, height).
|
Each dictionary contains the face's name and its normalized coordinates
|
||||||
|
(center x, center y, width, height).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path (str): The path to the image file.
|
path (str): The path to the image file.
|
||||||
@@ -161,7 +163,9 @@ class XmpManager:
|
|||||||
xmp[f"{area_base}/stArea:unit"] = 'normalized'
|
xmp[f"{area_base}/stArea:unit"] = 'normalized'
|
||||||
|
|
||||||
img.writeMetadata()
|
img.writeMetadata()
|
||||||
|
|
||||||
notify_baloo(path)
|
notify_baloo(path)
|
||||||
|
mark_app_modified(path)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error saving faces to XMP: {e}")
|
print(f"Error saving faces to XMP: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user