This commit is contained in:
Ignacio Serantes
2026-03-28 07:54:59 +01:00
parent d4f3732aa4
commit ff7c1aa373
3 changed files with 143 additions and 43 deletions

View File

@@ -837,27 +837,70 @@ class ThumbnailSortFilterProxyModel(QSortFilterProxyModel):
self.group_by_rating = False
self.collapsed_groups = set()
# Optimization: Pre-calculate tag filter results
self._tag_matching_paths = set()
self._tag_filter_active = False
self._last_tag_criteria = None # (frozenset(inc), frozenset(exc), mode)
def prepare_filter(self):
"""Builds a cache of paths to tags and names for faster filtering."""
if self.main_win:
# found_items_data: list of (path, qi, mtime, tags, rating, inode, dev)
# We pre-calculate sets and lowercase names for O(1) access
self._data_cache = {
item[0]: (set(item[3]) if item[3] else set(),
os.path.basename(item[0]).lower())
for item in self.main_win.found_items_data
}
else:
"""Updates the filter matching set if criteria have changed."""
if not self.main_win:
self._data_cache = {}
return
# Optimization: Pre-calculate which paths match the current tag criteria.
current_criteria = (frozenset(self.include_tags),
frozenset(self.exclude_tags),
self.match_mode)
if current_criteria != self._last_tag_criteria:
# Criteria changed: Full O(N) re-evaluation required for the whole cache.
self._last_tag_criteria = current_criteria
self._tag_matching_paths.clear()
self._tag_filter_active = bool(self.include_tags or self.exclude_tags)
if self._tag_filter_active:
for path, (tags, _) in self._data_cache.items():
if self._matches_tags(tags):
self._tag_matching_paths.add(path)
def _matches_tags(self, tags):
"""Internal helper to check if a set of tags matches current criteria."""
show = False
if not self.include_tags:
show = True
elif self.match_mode == "AND":
show = self.include_tags.issubset(tags)
else: # OR mode
show = not self.include_tags.isdisjoint(tags)
if show and self.exclude_tags:
if not self.exclude_tags.isdisjoint(tags):
show = False
return show
def clear_cache(self):
"""Clears the internal filter data cache."""
self._data_cache = {}
self._tag_matching_paths.clear()
self._last_tag_criteria = None
def add_to_cache(self, path, tags):
"""Adds a single item to the filter cache incrementally."""
self._data_cache[path] = (set(tags) if tags else set(),
os.path.basename(path).lower())
t_set = set(tags) if tags else set()
self._data_cache[path] = (t_set, os.path.basename(path).lower())
# Incremental update of matching paths avoids full cache scan in prepare_filter
if self._tag_filter_active:
if self._matches_tags(t_set):
self._tag_matching_paths.add(path)
else:
self._tag_matching_paths.discard(path)
def remove_from_cache(self, path):
"""Removes an item from the cache and tracking sets."""
self._data_cache.pop(path, None)
self._tag_matching_paths.discard(path)
def filterAcceptsRow(self, source_row, source_parent):
"""Determines if a row should be visible based on current filters."""
@@ -872,9 +915,9 @@ class ThumbnailSortFilterProxyModel(QSortFilterProxyModel):
self.group_by_year or self.group_by_rating)
return False
# Use cached data if available, otherwise fallback to model data
tags, name_lower = self._data_cache.get(
path, (set(index.data(TAGS_ROLE) or []), os.path.basename(path).lower()))
# 1. Optimization: Check tags first using pre-calculated set (O(1) lookup)
if self._tag_filter_active and path not in self._tag_matching_paths:
return False
# Filter collapsed groups
if self.main_win and (self.group_by_folder or self.group_by_day or
@@ -886,25 +929,15 @@ class ThumbnailSortFilterProxyModel(QSortFilterProxyModel):
if group_name in self.collapsed_groups:
return False
# Get cached lowercase name for remaining checks
cached_data = self._data_cache.get(path)
name_lower = cached_data[1] if cached_data else os.path.basename(path).lower()
# Filter by filename
if self.name_filter and self.name_filter not in name_lower:
return False
# Filter by tags
show = False
if not self.include_tags:
show = True
elif self.match_mode == "AND":
show = self.include_tags.issubset(tags)
else: # OR mode
show = not self.include_tags.isdisjoint(tags)
# Apply exclusion filter
if show and self.exclude_tags:
if not self.exclude_tags.isdisjoint(tags):
show = False
return show
return True
def lessThan(self, left, right):
"""Custom sorting logic for name and date."""
@@ -3416,6 +3449,10 @@ class MainWindow(QMainWindow):
self.proxy_model.match_mode = "AND" \
if self.filter_mode_group.buttons()[0].isChecked() else "OR"
# Optimization: Warm the filter cache before invalidating.
# This ensures filterAcceptsRow uses O(1) lookups for all items.
self.proxy_model.prepare_filter()
# Invalidate the model to force a re-filter
self.proxy_model.invalidate()
self._visible_paths_cache = None
@@ -4519,7 +4556,7 @@ class MainWindow(QMainWindow):
self.found_items_data = [item for item in self.found_items_data
if item[0] != path]
self._known_paths.discard(path)
self.proxy_model._data_cache.pop(path, None)
self.proxy_model.remove_from_cache(path)
self.cache.invalidate_path(path) # Clear from cache
# Update any open viewers
@@ -4634,12 +4671,9 @@ class MainWindow(QMainWindow):
break
# Update proxy model cache to avoid stale entries
if old_path in self.proxy_model._data_cache:
del self.proxy_model._data_cache[old_path]
self.proxy_model.remove_from_cache(old_path)
if current_tags is not None:
self.proxy_model._data_cache[new_path] = (
set(current_tags) if current_tags else set(),
os.path.basename(new_path).lower())
self.proxy_model.add_to_cache(new_path, current_tags)
# Update the main model
if old_path in self._path_to_model_index: