A bunch of changes

This commit is contained in:
Ignacio Serantes
2026-03-23 21:53:19 +01:00
parent a402828d1a
commit 547bfbf760
9 changed files with 544 additions and 150 deletions

View File

@@ -579,8 +579,6 @@ class ThumbnailDelegate(QStyledItemDelegate):
thumb_size = self.main_win.current_thumb_size
path = index.data(PATH_ROLE)
mtime = index.data(MTIME_ROLE)
inode = index.data(INODE_ROLE)
device_id = index.data(DEVICE_ROLE)
# Optimization: Use QPixmapCache to avoid expensive QImage->QPixmap
# conversion on every paint event.
@@ -589,6 +587,8 @@ class ThumbnailDelegate(QStyledItemDelegate):
if not source_pixmap or source_pixmap.isNull():
# Not in UI cache, try to get from main thumbnail cache (Memory/LMDB)
inode = index.data(INODE_ROLE)
device_id = index.data(DEVICE_ROLE)
img, _ = self.main_win.cache.get_thumbnail(
path, requested_size=thumb_size, curr_mtime=mtime,
inode=inode, device_id=device_id, async_load=True)
@@ -863,20 +863,34 @@ class ThumbnailSortFilterProxyModel(QSortFilterProxyModel):
def lessThan(self, left, right):
"""Custom sorting logic for name and date."""
sort_role = self.sortRole()
left_data = self.sourceModel().data(left, sort_role)
right_data = self.sourceModel().data(right, sort_role)
if sort_role == MTIME_ROLE:
left = left_data if left_data is not None else 0
right = right_data if right_data is not None else 0
return left < right
left_data = self.sourceModel().data(left, sort_role)
right_data = self.sourceModel().data(right, sort_role)
# Treat None as 0 for safe comparison
left_val = left_data if left_data is not None else 0
right_val = right_data if right_data is not None else 0
return left_val < right_val
# Default (DisplayRole) is case-insensitive name sorting
# Handle None values safely
l_str = str(left_data) if left_data is not None else ""
r_str = str(right_data) if right_data is not None else ""
# Default (DisplayRole) is name sorting.
# Optimization: Use the pre-calculated lowercase name from the cache
# to avoid repeated string operations during sorting.
left_path = self.sourceModel().data(left, PATH_ROLE)
right_path = self.sourceModel().data(right, PATH_ROLE)
return l_str.lower() < r_str.lower()
# Fallback for non-thumbnail items (like headers) or if cache is missing
if not left_path or not right_path or not self._data_cache:
l_str = str(self.sourceModel().data(left, Qt.DisplayRole) or "")
r_str = str(self.sourceModel().data(right, Qt.DisplayRole) or "")
return l_str.lower() < r_str.lower()
# Get from cache, with a fallback just in case
_, left_name_lower = self._data_cache.get(
left_path, (None, os.path.basename(left_path).lower()))
_, right_name_lower = self._data_cache.get(
right_path, (None, os.path.basename(right_path).lower()))
return left_name_lower < right_name_lower
class MainWindow(QMainWindow):
@@ -908,6 +922,7 @@ class MainWindow(QMainWindow):
self.current_thumb_size = THUMBNAILS_DEFAULT_SIZE
self.face_names_history = []
self.pet_names_history = []
self.body_names_history = []
self.object_names_history = []
self.landmark_names_history = []
self.mru_tags = deque(maxlen=APP_CONFIG.get(
@@ -1466,6 +1481,10 @@ class MainWindow(QMainWindow):
if "geometry" in mw_data:
g = mw_data["geometry"]
self.setGeometry(g["x"], g["y"], g["w"], g["h"])
selected_path = mw_data.get("selected_path")
select_paths = [selected_path] if selected_path else None
if "window_state" in mw_data:
self.restoreState(
QByteArray.fromBase64(mw_data["window_state"].encode()))
@@ -1521,7 +1540,7 @@ class MainWindow(QMainWindow):
paths.append(d)
self.start_scan([p.strip() for p in paths if p.strip()
and os.path.exists(os.path.expanduser(p.strip()))],
select_path=mw_data.get("selected_path"))
select_paths=select_paths)
if search_text:
self.search_input.setEditText(search_text)
@@ -1643,6 +1662,11 @@ class MainWindow(QMainWindow):
if len(self.face_names_history) > new_max_faces:
self.face_names_history = self.face_names_history[:new_max_faces]
new_max_bodies = APP_CONFIG.get("body_menu_max_items",
constants.FACES_MENU_MAX_ITEMS_DEFAULT)
if len(self.body_names_history) > new_max_bodies:
self.body_names_history = self.body_names_history[:new_max_bodies]
new_bg_color = APP_CONFIG.get("thumbnails_bg_color",
constants.THUMBNAILS_BG_COLOR_DEFAULT)
self.thumbnail_view.setStyleSheet(f"background-color: {new_bg_color};")
@@ -1974,6 +1998,44 @@ class MainWindow(QMainWindow):
return False
def get_selected_paths(self):
"""Returns a list of all selected file paths."""
paths = []
seen = set()
for idx in self.thumbnail_view.selectedIndexes():
path = self.proxy_model.data(idx, PATH_ROLE)
if path and path not in seen:
paths.append(path)
seen.add(path)
return paths
def restore_selection(self, paths):
"""Restores selection for a list of paths."""
if not paths:
return
selection_model = self.thumbnail_view.selectionModel()
selection = QItemSelection()
first_valid_index = QModelIndex()
for path in paths:
if path in self._path_to_model_index:
persistent_index = self._path_to_model_index[path]
if persistent_index.isValid():
source_index = QModelIndex(persistent_index)
proxy_index = self.proxy_model.mapFromSource(source_index)
if proxy_index.isValid():
selection.select(proxy_index, proxy_index)
if not first_valid_index.isValid():
first_valid_index = proxy_index
if not selection.isEmpty():
selection_model.select(selection, QItemSelectionModel.ClearAndSelect)
if first_valid_index.isValid():
self.thumbnail_view.setCurrentIndex(first_valid_index)
self.thumbnail_view.scrollTo(
first_valid_index, QAbstractItemView.EnsureVisible)
def toggle_visibility(self):
"""Toggles the visibility of the main window, opening a viewer if needed."""
if self.isVisible():
@@ -2247,7 +2309,7 @@ class MainWindow(QMainWindow):
w.load_and_fit_image()
def start_scan(self, paths, sync_viewer=False, active_viewer=None,
select_path=None):
select_paths=None):
"""
Starts a new background scan for images.
@@ -2255,7 +2317,7 @@ class MainWindow(QMainWindow):
paths (list): A list of file paths or directories to scan.
sync_viewer (bool): If True, avoids clearing the grid.
active_viewer (ImageViewer): A viewer to sync with the scan results.
select_path (str): A path to select automatically after the scan finishes.
select_paths (list): A list of paths to select automatically.
"""
self.is_cleaning = True
self._suppress_updates = True
@@ -2299,11 +2361,11 @@ class MainWindow(QMainWindow):
self.scanner.progress_msg.connect(self.status_lbl.setText)
self.scanner.more_files_available.connect(self.more_files_available)
self.scanner.finished_scan.connect(
lambda n: self._on_scan_finished(n, select_path))
lambda n: self._on_scan_finished(n, select_paths))
self.scanner.start()
self._scan_all = False
def _on_scan_finished(self, n, select_path=None):
def _on_scan_finished(self, n, select_paths=None):
"""Slot for when the image scanner has finished."""
self._suppress_updates = False
self._scanner_last_index = self._scanner_total_files
@@ -2331,8 +2393,8 @@ class MainWindow(QMainWindow):
self.update_tag_edit_widget()
# Select a specific path if requested (e.g., after layout restore)
if select_path:
self.find_and_select_path(select_path)
if select_paths:
self.restore_selection(select_paths)
# Final rebuild to ensure all items are correctly placed
if self.rebuild_timer.isActive():
@@ -2573,7 +2635,7 @@ class MainWindow(QMainWindow):
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
# Preserve selection
selected_path = self.get_current_selected_path()
selected_paths = self.get_selected_paths()
mode = self.sort_combo.currentText()
rev = "" in mode
@@ -2628,7 +2690,7 @@ class MainWindow(QMainWindow):
self._suppress_updates = False
self.apply_filters()
self.thumbnail_view.setUpdatesEnabled(True)
self.find_and_select_path(selected_path)
self.restore_selection(selected_paths)
if self.main_dock.isVisible() and \
self.tags_tabs.currentWidget() == self.filter_widget:
@@ -2782,7 +2844,7 @@ class MainWindow(QMainWindow):
self._suppress_updates = False
self.apply_filters()
self.thumbnail_view.setUpdatesEnabled(True)
self.find_and_select_path(selected_path)
self.restore_selection(selected_paths)
if self.main_dock.isVisible() and \
self.tags_tabs.currentWidget() == self.filter_widget:
@@ -3064,7 +3126,7 @@ class MainWindow(QMainWindow):
return
# Preserve selection
selected_path = self.get_current_selected_path()
selected_paths = self.get_selected_paths()
# Gather filter criteria from the UI
include_tags = set()
@@ -3112,8 +3174,8 @@ class MainWindow(QMainWindow):
self.filtered_count_lbl.setText(UITexts.FILTERED_ZERO)
# Restore selection if it's still visible
if selected_path:
self.find_and_select_path(selected_path)
if selected_paths:
self.restore_selection(selected_paths)
# Sync open viewers with the new list of visible paths
visible_paths = self.get_visible_image_paths()
@@ -3163,13 +3225,18 @@ class MainWindow(QMainWindow):
target_list.append(current_path)
new_index = len(target_list) - 1
w.controller.update_list(
target_list, new_index if new_index != -1 else None)
# Check if we are preserving the image to pass correct metadata
tags_to_pass = None
rating_to_pass = 0
if new_index != -1 and new_index < len(target_list):
if target_list[new_index] == current_path_in_viewer:
tags_to_pass = viewer_tags
rating_to_pass = viewer_rating
# Pass current image's tags and rating to the controller
w.controller.update_list(
target_list, new_index if new_index != -1 else None,
viewer_tags, viewer_rating)
tags_to_pass, rating_to_pass)
if not w._is_persistent and not w.controller.image_list:
w.close()
continue
@@ -3468,16 +3535,16 @@ class MainWindow(QMainWindow):
if not self.history:
return
current_selection = self.get_current_selected_path()
current_selection = self.get_selected_paths()
term = self.history[0]
if term.startswith("file:/"):
path = term[6:]
if os.path.isfile(path):
self.start_scan([os.path.dirname(path)], select_path=current_selection)
self.start_scan([os.path.dirname(path)], select_paths=current_selection)
return
self.process_term(term, select_path=current_selection)
self.process_term(term, select_paths=current_selection)
def process_term(self, term, select_path=None):
def process_term(self, term, select_paths=None):
"""Processes a search term, file path, or layout directive."""
self.add_to_history(term)
self.update_search_input()
@@ -3529,7 +3596,7 @@ class MainWindow(QMainWindow):
else:
# If a directory or search term, start a scan
self.start_scan([path], select_path=select_path)
self.start_scan([path], select_paths=select_paths)
def update_search_input(self):
"""Updates the search input combo box with history items and icons."""
@@ -3607,6 +3674,7 @@ class MainWindow(QMainWindow):
self.tags_tabs.setCurrentIndex(d["active_dock_tab"])
self.face_names_history = d.get("face_names_history", [])
self.pet_names_history = d.get("pet_names_history", [])
self.body_names_history = d.get("body_names_history", [])
self.object_names_history = d.get("object_names_history", [])
self.landmark_names_history = d.get("landmark_names_history", [])
@@ -3674,6 +3742,7 @@ class MainWindow(QMainWindow):
APP_CONFIG["active_dock_tab"] = self.tags_tabs.currentIndex()
APP_CONFIG["face_names_history"] = self.face_names_history
APP_CONFIG["pet_names_history"] = self.pet_names_history
APP_CONFIG["body_names_history"] = self.body_names_history
APP_CONFIG["object_names_history"] = self.object_names_history
APP_CONFIG["landmark_names_history"] = self.landmark_names_history
APP_CONFIG["mru_tags"] = list(self.mru_tags)