v0.9.12
This commit is contained in:
486
bagheeraview.py
486
bagheeraview.py
@@ -14,7 +14,7 @@ Classes:
|
||||
MainWindow: The main application window containing the thumbnail grid and docks.
|
||||
"""
|
||||
__appname__ = "BagheeraView"
|
||||
__version__ = "0.9.11"
|
||||
__version__ = "0.9.12"
|
||||
__author__ = "Ignacio Serantes"
|
||||
__email__ = "kde@aynoa.net"
|
||||
__license__ = "LGPL"
|
||||
@@ -41,10 +41,10 @@ from PySide6.QtWidgets import (
|
||||
from PySide6.QtGui import (
|
||||
QDesktopServices, QFont, QFontMetrics, QIcon, QTransform, QImageReader, QPalette,
|
||||
QStandardItemModel, QStandardItem, QColor, QPixmap, QPixmapCache, QPainter,
|
||||
QKeySequence, QAction, QActionGroup
|
||||
QKeySequence, QAction, QActionGroup, QImage
|
||||
)
|
||||
from PySide6.QtCore import (
|
||||
Qt, QPoint, QUrl, QObject, QEvent, QTimer, QMimeData, QByteArray,
|
||||
Qt, QPoint, QUrl, QObject, QEvent, QTimer, QByteArray,
|
||||
QItemSelection, QSortFilterProxyModel, QItemSelectionModel, QRect, QSize,
|
||||
QThread, QPersistentModelIndex, QModelIndex
|
||||
)
|
||||
@@ -1483,9 +1483,9 @@ class MainWindow(QMainWindow):
|
||||
mw_data = data.get("main_window", {})
|
||||
|
||||
# Restore main window geometry and state (including docks)
|
||||
if "geometry" in mw_data:
|
||||
g = mw_data["geometry"]
|
||||
self.setGeometry(g["x"], g["y"], g["w"], g["h"])
|
||||
# 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
|
||||
@@ -2621,242 +2621,247 @@ class MainWindow(QMainWindow):
|
||||
# layout properties (grid size, uniform items) are in sync with the
|
||||
# selected mode before any model rebuild.
|
||||
self._suppress_updates = True
|
||||
index = self.view_mode_combo.currentIndex()
|
||||
selected_paths = []
|
||||
try:
|
||||
index = self.view_mode_combo.currentIndex()
|
||||
|
||||
self._model_update_queue.clear()
|
||||
self._model_update_timer.stop()
|
||||
self._model_update_queue.clear()
|
||||
self._model_update_timer.stop()
|
||||
|
||||
# Update proxy model flags to ensure they match the UI state
|
||||
self.proxy_model.group_by_folder = (index == 1)
|
||||
self.proxy_model.group_by_day = (index == 2)
|
||||
self.proxy_model.group_by_week = (index == 3)
|
||||
self.proxy_model.group_by_month = (index == 4)
|
||||
self.proxy_model.group_by_year = (index == 5)
|
||||
self.proxy_model.group_by_rating = (index == 6)
|
||||
# Update proxy model flags to ensure they match the UI state
|
||||
self.proxy_model.group_by_folder = (index == 1)
|
||||
self.proxy_model.group_by_day = (index == 2)
|
||||
self.proxy_model.group_by_week = (index == 3)
|
||||
self.proxy_model.group_by_month = (index == 4)
|
||||
self.proxy_model.group_by_year = (index == 5)
|
||||
self.proxy_model.group_by_rating = (index == 6)
|
||||
|
||||
is_grouped = index > 0
|
||||
self.thumbnail_view.setUniformItemSizes(not is_grouped)
|
||||
if is_grouped:
|
||||
self.thumbnail_view.setGridSize(QSize())
|
||||
else:
|
||||
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
|
||||
is_grouped = index > 0
|
||||
self.thumbnail_view.setUniformItemSizes(not is_grouped)
|
||||
if is_grouped:
|
||||
self.thumbnail_view.setGridSize(QSize())
|
||||
else:
|
||||
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
|
||||
|
||||
# Preserve selection
|
||||
selected_paths = self.get_selected_paths()
|
||||
# Preserve selection
|
||||
selected_paths = self.get_selected_paths()
|
||||
|
||||
mode = self.sort_combo.currentText()
|
||||
rev = "↓" in mode
|
||||
sort_by_name = "Name" in mode
|
||||
mode = self.sort_combo.currentText()
|
||||
rev = "↓" in mode
|
||||
sort_by_name = "Name" in mode
|
||||
|
||||
# 2. Sort the collected data. Python's sort is stable, so we apply sorts
|
||||
# from least specific to most specific.
|
||||
# 2. Sort the collected data. Python's sort is stable, so we apply sorts
|
||||
# from least specific to most specific.
|
||||
|
||||
# First, sort by the user's preference (name or date).
|
||||
def user_sort_key(data_tuple):
|
||||
path, _, mtime, _, _, _, _ = data_tuple
|
||||
if sort_by_name:
|
||||
return os.path.basename(path).lower()
|
||||
# Handle None mtime safely for sort
|
||||
return mtime if mtime is not None else 0
|
||||
# First, sort by the user's preference (name or date).
|
||||
def user_sort_key(data_tuple):
|
||||
path, _, mtime, _, _, _, _ = data_tuple
|
||||
if sort_by_name:
|
||||
return os.path.basename(path).lower()
|
||||
# Handle None mtime safely for sort
|
||||
return mtime if mtime is not None else 0
|
||||
|
||||
self.found_items_data.sort(key=user_sort_key, reverse=rev)
|
||||
self.found_items_data.sort(key=user_sort_key, reverse=rev)
|
||||
|
||||
# 3. Rebuild the model. Disable view updates for a massive performance boost.
|
||||
self.thumbnail_view.setUpdatesEnabled(False)
|
||||
# 3. Rebuild the model. Disable view updates for a massive performance
|
||||
# boost.
|
||||
self.thumbnail_view.setUpdatesEnabled(False)
|
||||
|
||||
target_structure = []
|
||||
target_structure = []
|
||||
|
||||
if not is_grouped:
|
||||
# OPTIMIZATION: In Flat View, rely on Proxy Model for sorting.
|
||||
# This avoids expensive O(N) source model reshuffling/syncing on the main
|
||||
# thread.
|
||||
if not is_grouped:
|
||||
# OPTIMIZATION: In Flat View, rely on Proxy Model for sorting.
|
||||
# This avoids expensive O(N) source model reshuffling/syncing on the
|
||||
# main thread.
|
||||
|
||||
sort_role = Qt.DisplayRole if sort_by_name else MTIME_ROLE
|
||||
sort_order = Qt.DescendingOrder if rev else Qt.AscendingOrder
|
||||
self.proxy_model.setSortRole(sort_role)
|
||||
self.proxy_model.sort(0, sort_order)
|
||||
sort_role = Qt.DisplayRole if sort_by_name else MTIME_ROLE
|
||||
sort_order = Qt.DescendingOrder if rev else Qt.AscendingOrder
|
||||
self.proxy_model.setSortRole(sort_role)
|
||||
self.proxy_model.sort(0, sort_order)
|
||||
|
||||
# Only rebuild source if requested or desynchronized (e.g. first batch)
|
||||
# If items were added incrementally, count matches and we skip rebuild.
|
||||
if full_reset or \
|
||||
self.thumbnail_model.rowCount() != len(self.found_items_data):
|
||||
self.thumbnail_model.clear()
|
||||
self._path_to_model_index.clear()
|
||||
# Fast append of all items
|
||||
for item_data in self.found_items_data:
|
||||
# Only rebuild source if requested or desynchronized (e.g. first batch)
|
||||
# If items were added incrementally, count matches and we skip rebuild.
|
||||
if full_reset or \
|
||||
self.thumbnail_model.rowCount() != len(self.found_items_data):
|
||||
self.thumbnail_model.clear()
|
||||
self._path_to_model_index.clear()
|
||||
# Fast append of all items
|
||||
for item_data in self.found_items_data:
|
||||
# item structure: (path, qi, mtime, tags, rating, inode, dev)
|
||||
p, q, m, t, r, ino, d = item_data
|
||||
new_item = self._create_thumbnail_item(
|
||||
p, q, m, os.path.dirname(p), t, r, ino, d)
|
||||
self.thumbnail_model.appendRow(new_item)
|
||||
path = new_item.data(PATH_ROLE)
|
||||
source_index = self.thumbnail_model.indexFromItem(new_item)
|
||||
self._path_to_model_index[path] = \
|
||||
QPersistentModelIndex(source_index)
|
||||
|
||||
else:
|
||||
# For Grouped View, we must ensure source model order matches
|
||||
# groups/headers
|
||||
self.proxy_model.sort(-1) # Disable proxy sorting
|
||||
|
||||
if full_reset:
|
||||
self.thumbnail_model.clear()
|
||||
self._path_to_model_index.clear()
|
||||
|
||||
# Optimize grouped insertion: Decorate-Sort-Group
|
||||
# 1. Decorate: Calculate group info once per item
|
||||
decorated_data = []
|
||||
for item in self.found_items_data:
|
||||
# item structure: (path, qi, mtime, tags, rating, inode, dev)
|
||||
p, q, m, t, r, ino, d = item_data
|
||||
new_item = self._create_thumbnail_item(
|
||||
p, q, m, os.path.dirname(p), t, r, ino, d)
|
||||
self.thumbnail_model.appendRow(new_item)
|
||||
path = new_item.data(PATH_ROLE)
|
||||
source_index = self.thumbnail_model.indexFromItem(new_item)
|
||||
self._path_to_model_index[path] = \
|
||||
QPersistentModelIndex(source_index)
|
||||
stable_key, display_name = self._get_group_info(
|
||||
item[0], item[2], item[4])
|
||||
# Use empty string for None keys to ensure sortability
|
||||
sort_key = stable_key if stable_key is not None else ""
|
||||
decorated_data.append((sort_key, display_name, item))
|
||||
|
||||
# 2. Sort by group key (stable sort preserves previous user order)
|
||||
is_reverse_group = not self.proxy_model.group_by_folder
|
||||
decorated_data.sort(key=lambda x: x[0], reverse=is_reverse_group)
|
||||
|
||||
# Update master list to reflect the new group order
|
||||
self.found_items_data = [x[2] for x in decorated_data]
|
||||
|
||||
# 3. Group and Insert
|
||||
for _, group_iter in groupby(decorated_data, key=lambda x: x[0]):
|
||||
group_list = list(group_iter)
|
||||
if not group_list:
|
||||
continue
|
||||
|
||||
# Extract info from the first item in the group
|
||||
_, display_name_group, _ = group_list[0]
|
||||
count = len(group_list)
|
||||
|
||||
header_text = (UITexts.GROUP_HEADER_FORMAT_SINGULAR if count == 1
|
||||
else UITexts.GROUP_HEADER_FORMAT).format(
|
||||
group_name=display_name_group, count=count)
|
||||
|
||||
# ('HEADER', (key, header_text, count))
|
||||
target_structure.append(
|
||||
('HEADER', (display_name_group, header_text, count)))
|
||||
|
||||
# Add items from the group
|
||||
target_structure.extend([x[2] for x in group_list])
|
||||
|
||||
# 4. Synchronize model with target_structure
|
||||
model_idx = 0
|
||||
target_idx = 0
|
||||
total_targets = len(target_structure)
|
||||
new_items_batch = []
|
||||
|
||||
while target_idx < total_targets:
|
||||
target = target_structure[target_idx]
|
||||
current_item = self.thumbnail_model.item(model_idx)
|
||||
|
||||
if self._match_item(target, current_item):
|
||||
model_idx += 1
|
||||
target_idx += 1
|
||||
else:
|
||||
# Prepare new item
|
||||
if isinstance(target, tuple) and len(target) == 2 \
|
||||
and target[0] == 'HEADER':
|
||||
_, (group_name, header_text, _) = target
|
||||
new_item = QStandardItem()
|
||||
new_item.setData('header', ITEM_TYPE_ROLE)
|
||||
new_item.setData(header_text, DIR_ROLE)
|
||||
new_item.setData(group_name, GROUP_NAME_ROLE)
|
||||
new_item.setFlags(Qt.ItemIsEnabled)
|
||||
else:
|
||||
path, qi, mtime, tags, rating, inode, dev = target
|
||||
new_item = self._create_thumbnail_item(
|
||||
path, qi, mtime, os.path.dirname(path),
|
||||
tags, rating, inode, dev)
|
||||
|
||||
# Detect continuous block of new items for batch insertion
|
||||
new_items_batch = [new_item]
|
||||
target_idx += 1
|
||||
|
||||
# Look ahead to see if next items are also new (not in current
|
||||
# model)
|
||||
# This optimization drastically reduces proxy model
|
||||
# recalculations
|
||||
while target_idx < total_targets:
|
||||
next_target = target_structure[target_idx]
|
||||
# Check if next_target matches current model position
|
||||
# (re-sync)
|
||||
if self._match_item(
|
||||
next_target, self.thumbnail_model.item(model_idx)):
|
||||
break
|
||||
|
||||
# If not matching, it's another new item to insert
|
||||
if isinstance(next_target, tuple) \
|
||||
and len(next_target) == 2 and next_target[0] == 'HEADER':
|
||||
_, (h_group, h_text, _) = next_target
|
||||
n_item = QStandardItem()
|
||||
n_item.setData('header', ITEM_TYPE_ROLE)
|
||||
n_item.setData(h_text, DIR_ROLE)
|
||||
n_item.setData(h_group, GROUP_NAME_ROLE)
|
||||
n_item.setFlags(Qt.ItemIsEnabled)
|
||||
new_items_batch.append(n_item)
|
||||
else:
|
||||
p, q, m, t, r, ino, d = next_target
|
||||
n_item = self._create_thumbnail_item(
|
||||
p, q, m, os.path.dirname(p), t, r, ino, d)
|
||||
new_items_batch.append(n_item)
|
||||
target_idx += 1
|
||||
|
||||
# Perform batch insertion
|
||||
# Optimization: Use appendRow/insertRow with the item directly
|
||||
# to avoid double-signaling (rowsInserted + dataChanged) which
|
||||
# forces the ProxyModel to filter every row twice.
|
||||
if model_idx >= self.thumbnail_model.rowCount():
|
||||
for item in new_items_batch:
|
||||
self.thumbnail_model.appendRow(item)
|
||||
if item.data(ITEM_TYPE_ROLE) == 'thumbnail':
|
||||
path = item.data(PATH_ROLE)
|
||||
source_index = \
|
||||
self.thumbnail_model.indexFromItem(item)
|
||||
self._path_to_model_index[path] = \
|
||||
QPersistentModelIndex(source_index)
|
||||
else:
|
||||
for i, item in enumerate(new_items_batch):
|
||||
self.thumbnail_model.insertRow(model_idx + i, item)
|
||||
if item.data(ITEM_TYPE_ROLE) == 'thumbnail':
|
||||
path = item.data(PATH_ROLE)
|
||||
source_index = self.thumbnail_model.index(
|
||||
model_idx + i, 0)
|
||||
self._path_to_model_index[path] = \
|
||||
QPersistentModelIndex(source_index)
|
||||
|
||||
model_idx += len(new_items_batch)
|
||||
|
||||
# Remove any remaining trailing items in the model (e.g. if list shrank)
|
||||
if model_idx < self.thumbnail_model.rowCount():
|
||||
for row in range(model_idx, self.thumbnail_model.rowCount()):
|
||||
item = self.thumbnail_model.item(row)
|
||||
if item and item.data(ITEM_TYPE_ROLE) == 'thumbnail':
|
||||
path = item.data(PATH_ROLE)
|
||||
if path in self._path_to_model_index:
|
||||
# Only delete if it points to this specific row (stale)
|
||||
# otherwise we might delete the index for a newly
|
||||
# inserted item
|
||||
p_idx = self._path_to_model_index[path]
|
||||
if not p_idx.isValid() or p_idx.row() == row:
|
||||
del self._path_to_model_index[path]
|
||||
self.thumbnail_model.removeRows(
|
||||
model_idx, self.thumbnail_model.rowCount() - model_idx)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"Error in rebuild_view: {e}")
|
||||
finally:
|
||||
self._suppress_updates = False
|
||||
self.apply_filters()
|
||||
self.thumbnail_view.setUpdatesEnabled(True)
|
||||
self.restore_selection(selected_paths)
|
||||
if selected_paths:
|
||||
self.restore_selection(selected_paths)
|
||||
|
||||
if self.main_dock.isVisible() and \
|
||||
self.tags_tabs.currentWidget() == self.filter_widget:
|
||||
if not self.filter_refresh_timer.isActive():
|
||||
self.filter_refresh_timer.start()
|
||||
return
|
||||
else:
|
||||
# For Grouped View, we must ensure source model order matches groups/headers
|
||||
self.proxy_model.sort(-1) # Disable proxy sorting
|
||||
|
||||
if full_reset:
|
||||
self.thumbnail_model.clear()
|
||||
self._path_to_model_index.clear()
|
||||
|
||||
# Optimize grouped insertion: Decorate-Sort-Group
|
||||
# 1. Decorate: Calculate group info once per item
|
||||
decorated_data = []
|
||||
for item in self.found_items_data:
|
||||
# item structure: (path, qi, mtime, tags, rating, inode, dev)
|
||||
stable_key, display_name = self._get_group_info(
|
||||
item[0], item[2], item[4])
|
||||
# Use empty string for None keys to ensure sortability
|
||||
sort_key = stable_key if stable_key is not None else ""
|
||||
decorated_data.append((sort_key, display_name, item))
|
||||
|
||||
# 2. Sort by group key (stable sort preserves previous user order)
|
||||
is_reverse_group = not self.proxy_model.group_by_folder
|
||||
decorated_data.sort(key=lambda x: x[0], reverse=is_reverse_group)
|
||||
|
||||
# Update master list to reflect the new group order
|
||||
self.found_items_data = [x[2] for x in decorated_data]
|
||||
|
||||
# 3. Group and Insert
|
||||
for _, group_iter in groupby(decorated_data, key=lambda x: x[0]):
|
||||
group_list = list(group_iter)
|
||||
if not group_list:
|
||||
continue
|
||||
|
||||
# Extract info from the first item in the group
|
||||
_, display_name_group, _ = group_list[0]
|
||||
count = len(group_list)
|
||||
|
||||
header_text = (UITexts.GROUP_HEADER_FORMAT_SINGULAR if count == 1
|
||||
else UITexts.GROUP_HEADER_FORMAT).format(
|
||||
group_name=display_name_group, count=count)
|
||||
|
||||
# ('HEADER', (key, header_text, count))
|
||||
target_structure.append(
|
||||
('HEADER', (display_name_group, header_text, count)))
|
||||
|
||||
# Add items from the group
|
||||
target_structure.extend([x[2] for x in group_list])
|
||||
|
||||
# 4. Synchronize model with target_structure
|
||||
model_idx = 0
|
||||
target_idx = 0
|
||||
total_targets = len(target_structure)
|
||||
new_items_batch = []
|
||||
|
||||
while target_idx < total_targets:
|
||||
target = target_structure[target_idx]
|
||||
current_item = self.thumbnail_model.item(model_idx)
|
||||
|
||||
if self._match_item(target, current_item):
|
||||
model_idx += 1
|
||||
target_idx += 1
|
||||
else:
|
||||
# Prepare new item
|
||||
if isinstance(target, tuple) and len(target) == 2 \
|
||||
and target[0] == 'HEADER':
|
||||
_, (group_name, header_text, _) = target
|
||||
new_item = QStandardItem()
|
||||
new_item.setData('header', ITEM_TYPE_ROLE)
|
||||
new_item.setData(header_text, DIR_ROLE)
|
||||
new_item.setData(group_name, GROUP_NAME_ROLE)
|
||||
new_item.setFlags(Qt.ItemIsEnabled)
|
||||
else:
|
||||
path, qi, mtime, tags, rating, inode, dev = target
|
||||
new_item = self._create_thumbnail_item(
|
||||
path, qi, mtime, os.path.dirname(path),
|
||||
tags, rating, inode, dev)
|
||||
|
||||
# Detect continuous block of new items for batch insertion
|
||||
new_items_batch = [new_item]
|
||||
target_idx += 1
|
||||
|
||||
# Look ahead to see if next items are also new (not in current model)
|
||||
# This optimization drastically reduces proxy model recalculations
|
||||
while target_idx < total_targets:
|
||||
next_target = target_structure[target_idx]
|
||||
# Check if next_target matches current model position (re-sync)
|
||||
if self._match_item(
|
||||
next_target, self.thumbnail_model.item(model_idx)):
|
||||
break
|
||||
|
||||
# If not matching, it's another new item to insert
|
||||
if isinstance(next_target, tuple) and len(next_target) == 2 \
|
||||
and next_target[0] == 'HEADER':
|
||||
_, (h_group, h_text, _) = next_target
|
||||
n_item = QStandardItem()
|
||||
n_item.setData('header', ITEM_TYPE_ROLE)
|
||||
n_item.setData(h_text, DIR_ROLE)
|
||||
n_item.setData(h_group, GROUP_NAME_ROLE)
|
||||
n_item.setFlags(Qt.ItemIsEnabled)
|
||||
new_items_batch.append(n_item)
|
||||
else:
|
||||
p, q, m, t, r, ino, d = next_target
|
||||
n_item = self._create_thumbnail_item(
|
||||
p, q, m, os.path.dirname(p), t, r, ino, d)
|
||||
new_items_batch.append(n_item)
|
||||
target_idx += 1
|
||||
|
||||
# Perform batch insertion
|
||||
# Optimization: Use appendRow/insertRow with the item directly to avoid
|
||||
# double-signaling (rowsInserted + dataChanged) which forces the
|
||||
# ProxyModel to filter every row twice.
|
||||
if model_idx >= self.thumbnail_model.rowCount():
|
||||
for item in new_items_batch:
|
||||
self.thumbnail_model.appendRow(item)
|
||||
if item.data(ITEM_TYPE_ROLE) == 'thumbnail':
|
||||
path = item.data(PATH_ROLE)
|
||||
source_index = self.thumbnail_model.indexFromItem(item)
|
||||
self._path_to_model_index[path] = QPersistentModelIndex(
|
||||
source_index)
|
||||
else:
|
||||
for i, item in enumerate(new_items_batch):
|
||||
self.thumbnail_model.insertRow(model_idx + i, item)
|
||||
if item.data(ITEM_TYPE_ROLE) == 'thumbnail':
|
||||
path = item.data(PATH_ROLE)
|
||||
source_index = self.thumbnail_model.index(model_idx + i, 0)
|
||||
self._path_to_model_index[path] = QPersistentModelIndex(
|
||||
source_index)
|
||||
|
||||
model_idx += len(new_items_batch)
|
||||
|
||||
# Remove any remaining trailing items in the model (e.g. if list shrank)
|
||||
if model_idx < self.thumbnail_model.rowCount():
|
||||
for row in range(model_idx, self.thumbnail_model.rowCount()):
|
||||
item = self.thumbnail_model.item(row)
|
||||
if item and item.data(ITEM_TYPE_ROLE) == 'thumbnail':
|
||||
path = item.data(PATH_ROLE)
|
||||
if path in self._path_to_model_index:
|
||||
# Only delete if it points to this specific row (stale)
|
||||
# otherwise we might delete the index for a newly inserted item
|
||||
p_idx = self._path_to_model_index[path]
|
||||
if not p_idx.isValid() or p_idx.row() == row:
|
||||
del self._path_to_model_index[path]
|
||||
self.thumbnail_model.removeRows(
|
||||
model_idx, self.thumbnail_model.rowCount() - model_idx)
|
||||
|
||||
self._suppress_updates = False
|
||||
self.apply_filters()
|
||||
self.thumbnail_view.setUpdatesEnabled(True)
|
||||
self.restore_selection(selected_paths)
|
||||
|
||||
if self.main_dock.isVisible() and \
|
||||
self.tags_tabs.currentWidget() == self.filter_widget:
|
||||
if not self.filter_refresh_timer.isActive():
|
||||
self.filter_refresh_timer.start()
|
||||
|
||||
def _create_thumbnail_item(self, path, qi, mtime, dir_path,
|
||||
tags, rating, inode=None, dev=None):
|
||||
@@ -3452,8 +3457,6 @@ class MainWindow(QMainWindow):
|
||||
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
|
||||
self.rebuild_view(full_reset=True)
|
||||
|
||||
self.update_tag_list()
|
||||
|
||||
self.save_config()
|
||||
self.setFocus()
|
||||
|
||||
@@ -3954,9 +3957,15 @@ class MainWindow(QMainWindow):
|
||||
clipboard_menu = menu.addMenu(QIcon.fromTheme("edit-copy"),
|
||||
UITexts.CONTEXT_MENU_CLIPBOARD)
|
||||
|
||||
action_copy_url = clipboard_menu.addAction(QIcon.fromTheme("text-html"),
|
||||
UITexts.CONTEXT_MENU_COPY_URL)
|
||||
action_copy_url.triggered.connect(self.copy_file_url)
|
||||
action_copy_image = clipboard_menu.addAction(QIcon.fromTheme("image-x-generic"),
|
||||
UITexts.VIEWER_MENU_COPY_IMAGE)
|
||||
action_copy_image.triggered.connect(self.copy_image_to_clipboard)
|
||||
if len(selected_indexes) > 1:
|
||||
action_copy_image.setEnabled(False)
|
||||
|
||||
action_copy_path = clipboard_menu.addAction(
|
||||
QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
|
||||
action_copy_path.triggered.connect(self.copy_file_path_to_clipboard)
|
||||
|
||||
action_copy_dir = clipboard_menu.addAction(QIcon.fromTheme("folder"),
|
||||
UITexts.CONTEXT_MENU_COPY_DIR)
|
||||
@@ -4127,23 +4136,30 @@ class MainWindow(QMainWindow):
|
||||
msg.setArguments(["", QUrl.fromLocalFile(path).toString(), {"ask": True}])
|
||||
QDBusConnection.sessionBus().call(msg, QDBus.NoBlock)
|
||||
|
||||
def copy_file_url(self):
|
||||
"""Copies the file URL of the selected image to the clipboard."""
|
||||
def copy_image_to_clipboard(self):
|
||||
"""Copies the full image of the selected thumbnail to the clipboard."""
|
||||
path = self.get_current_selected_path()
|
||||
if not path:
|
||||
return
|
||||
url = QUrl.fromLocalFile(path)
|
||||
mime = QMimeData()
|
||||
mime.setUrls([url])
|
||||
mime.setText(url.toString())
|
||||
QApplication.clipboard().setMimeData(mime)
|
||||
# This is a disk read, but it's on user action.
|
||||
img = QImage(path)
|
||||
if not img.isNull():
|
||||
QApplication.clipboard().setImage(img)
|
||||
|
||||
def copy_file_path_to_clipboard(self):
|
||||
"""Copies the file path(s) of the selected image(s) to the clipboard."""
|
||||
paths = self.get_selected_paths()
|
||||
if not paths:
|
||||
return
|
||||
QApplication.clipboard().setText("\n".join(paths))
|
||||
|
||||
def copy_dir_path(self):
|
||||
"""Copies the directory path of the selected image to the clipboard."""
|
||||
path = self.get_current_selected_path()
|
||||
if not path:
|
||||
"""Copies the directory path(s) of the selected image(s) to the clipboard."""
|
||||
paths = self.get_selected_paths()
|
||||
if not paths:
|
||||
return
|
||||
QApplication.clipboard().setText(os.path.dirname(path))
|
||||
dir_paths = sorted(list(set(os.path.dirname(p) for p in paths)))
|
||||
QApplication.clipboard().setText("\n".join(dir_paths))
|
||||
|
||||
def show_properties(self):
|
||||
"""Shows the custom properties dialog for the selected file."""
|
||||
|
||||
Reference in New Issue
Block a user