A bunch of changes
This commit is contained in:
137
bagheeraview.py
137
bagheeraview.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user