v0.9.12
This commit is contained in:
120
bagheeraview.py
120
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,6 +2621,8 @@ class MainWindow(QMainWindow):
|
||||
# layout properties (grid size, uniform items) are in sync with the
|
||||
# selected mode before any model rebuild.
|
||||
self._suppress_updates = True
|
||||
selected_paths = []
|
||||
try:
|
||||
index = self.view_mode_combo.currentIndex()
|
||||
|
||||
self._model_update_queue.clear()
|
||||
@@ -2661,15 +2663,16 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.found_items_data.sort(key=user_sort_key, reverse=rev)
|
||||
|
||||
# 3. Rebuild the model. Disable view updates for a massive performance boost.
|
||||
# 3. Rebuild the model. Disable view updates for a massive performance
|
||||
# boost.
|
||||
self.thumbnail_view.setUpdatesEnabled(False)
|
||||
|
||||
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.
|
||||
# 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
|
||||
@@ -2694,18 +2697,9 @@ class MainWindow(QMainWindow):
|
||||
self._path_to_model_index[path] = \
|
||||
QPersistentModelIndex(source_index)
|
||||
|
||||
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()
|
||||
return
|
||||
else:
|
||||
# For Grouped View, we must ensure source model order matches groups/headers
|
||||
# For Grouped View, we must ensure source model order matches
|
||||
# groups/headers
|
||||
self.proxy_model.sort(-1) # Disable proxy sorting
|
||||
|
||||
if full_reset:
|
||||
@@ -2784,18 +2778,21 @@ class MainWindow(QMainWindow):
|
||||
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
|
||||
# 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)
|
||||
# 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':
|
||||
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)
|
||||
@@ -2811,25 +2808,27 @@ class MainWindow(QMainWindow):
|
||||
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.
|
||||
# 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)
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -2841,16 +2840,22 @@ class MainWindow(QMainWindow):
|
||||
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
|
||||
# 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)
|
||||
if selected_paths:
|
||||
self.restore_selection(selected_paths)
|
||||
|
||||
if self.main_dock.isVisible() and \
|
||||
@@ -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."""
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
v0.9.11 -
|
||||
· Filmstrip fixed
|
||||
· Añadida una nueva área llamada Body.
|
||||
· Refactorizaciones, optimizaciones y cambios a saco.
|
||||
· Image viewer tiene comparisonb
|
||||
|
||||
Implement a bulk rename feature for the selected pet or face tags.
|
||||
Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text.
|
||||
|
||||
Me gustaría que el submenú de "Manipular" también incluyera una opción para "Escalar" la imagen, ¿cómo podría implementarlo?
|
||||
|
||||
Ensure that apply_filters doesn't trigger redundant updates if rebuild_view already called it.
|
||||
|
||||
Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text.
|
||||
Add a `shutdown` signal or method to `ScannerWorker` to allow cleaner cancellation of long-running tasks like `generate_thumbnail`.
|
||||
Implement a mechanism to dynamically adjust the thread pool size based on system load or user activity.
|
||||
@@ -30,10 +28,6 @@ Implement a "Comparison" mode to view 2 or 4 images side-by-side in the viewer.
|
||||
· Si quisiera distribuir mi aplicación como un AppImage, ¿cómo empaquetaría estos plugins de KDE para que funcionen en otros sistemas?
|
||||
· Solucionar el problema de las ventanas de diálogo nativas, tan simple como usar PySide nativo.
|
||||
|
||||
¿Cómo puedo añadir una opción para limitar el número de hilos que `ImageScanner` puede usar para la generación de miniaturas?
|
||||
|
||||
Check if the `_suppress_updates` flag correctly prevents potential race conditions in `update_tag_list` when switching view modes rapidly.
|
||||
|
||||
Verify if `find_and_select_path` logic in `on_view_mode_changed` handles cases where the selected item is filtered out after the view mode change.
|
||||
|
||||
cuando se hace una nueva búsqueda que no se refresquen los tags, ni filtros, ni nada hasta que venga la primera imagen de la búsqueda nueva. Actualizar algo que se está destruyendo no tiene sentido. Lo mismo aplica si se cambia la agrupación, paramos las actualizaciones y luego, cuando acabe la agrupación activamos de nuevo los tags y los filtros y todo lo que implique un refresco de pantalla.
|
||||
@@ -69,6 +63,16 @@ Ahora que la carga es rápida, ¿cómo puedo implementar una precarga inteligent
|
||||
¿Cómo puedo hacer que la selección de archivos sea persistente incluso después de recargar o filtrar la vista?
|
||||
|
||||
|
||||
v0.9.12 -
|
||||
· Al restaurar el layout no se restaura la posición y dimensiones de los thumbnails.
|
||||
· Mejoras en los menús de contexto.
|
||||
|
||||
v0.9.11 -
|
||||
· Filmstrip fixed
|
||||
· Añadida una nueva área llamada Body.
|
||||
· Refactorizaciones, optimizaciones y cambios a saco.
|
||||
· Image viewer tiene comparisonb
|
||||
|
||||
v0.9.10 - Eleven step to 1.0
|
||||
· Slideshow inverso
|
||||
· Más mejoras de rendimiento y seguridad
|
||||
|
||||
93
constants.py
93
constants.py
@@ -29,7 +29,7 @@ if FORCE_X11:
|
||||
# --- CONFIGURATION ---
|
||||
PROG_NAME = "Bagheera Image Viewer"
|
||||
PROG_ID = "bagheeraview"
|
||||
PROG_VERSION = "0.9.11"
|
||||
PROG_VERSION = "0.9.12"
|
||||
PROG_AUTHOR = "Ignacio Serantes"
|
||||
|
||||
# --- CACHE SETTINGS ---
|
||||
@@ -96,7 +96,12 @@ KWINOUTPUTCONFIG_PATH = os.path.join(os.path.expanduser("~"),
|
||||
|
||||
# --- EXTERNAL TOOLS ---
|
||||
# Command definitions for external search tools.
|
||||
|
||||
try:
|
||||
from bagheera_search_lib import BagheeraSearcher
|
||||
HAVE_BAGHEERASEARCH_LIB = True
|
||||
except ImportError:
|
||||
HAVE_BAGHEERASEARCH_LIB = False
|
||||
pass
|
||||
BALOOSEARCH_EXEC = shutil.which("baloosearch") or shutil.which("baloosearch6")
|
||||
SEARCH_CMD = [BALOOSEARCH_EXEC, "--type", "image"] if BALOOSEARCH_EXEC else None
|
||||
|
||||
@@ -167,8 +172,6 @@ if importlib.util.find_spec("mediapipe") is not None:
|
||||
pass
|
||||
HAVE_FACE_RECOGNITION = importlib.util.find_spec("face_recognition") is not None
|
||||
|
||||
HAVE_BAGHEERASEARCH_LIB = True
|
||||
|
||||
MEDIAPIPE_FACE_MODEL_PATH = os.path.join(CONFIG_DIR,
|
||||
"blaze_face_short_range.tflite")
|
||||
MEDIAPIPE_FACE_MODEL_URL = (
|
||||
@@ -291,6 +294,8 @@ VIEWER_ACTIONS = {
|
||||
"toggle_visibility": ("Show/Hide Main Window", "Window"),
|
||||
"toggle_crop": ("Toggle Crop Mode", "Edit"),
|
||||
"save_crop": ("Save Cropped Image", "File"),
|
||||
"copy_image": ("Copy Image to Clipboard", "Edit"),
|
||||
"copy_path": ("Copy File Path", "Edit"),
|
||||
"compare_1": ("Single View", "View"),
|
||||
"compare_2": ("Compare 2 Images", "View"),
|
||||
"compare_4": ("Compare 4 Images", "View"),
|
||||
@@ -451,8 +456,8 @@ _UI_TEXTS = {
|
||||
"MENU_ABOUT": "About",
|
||||
"MENU_ABOUT_TITLE": "About {}",
|
||||
"MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>A simple image viewer and manager "
|
||||
"for KDE with Baloo support.<br><br>Created by {2} with the help of AI, and "
|
||||
"mostly the good people at KDE and Qt.",
|
||||
"for KDE with Baloo support.<br><br>Created by {2} with the help of AI, but "
|
||||
"mostly thanks to the job of the good people at KDE and Qt.",
|
||||
"MENU_CACHE": "Cache",
|
||||
"MENU_CLEAR_CACHE": "Clear cache ({} items, {:.1f} MB, {:.1f} MB on disk)",
|
||||
"MENU_CLEAN_CACHE": "Clean up invalid cache entries",
|
||||
@@ -527,7 +532,7 @@ _UI_TEXTS = {
|
||||
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Thumbnails filename font size:",
|
||||
"SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Thumbnails tags font size:",
|
||||
"SETTINGS_SCAN_THREADS_LABEL": "Generation threads:",
|
||||
"SETTINGS_SCAN_THREADS_TOOLTIP": "Maximum number of simultaneous threads to"
|
||||
"SETTINGS_SCAN_THREADS_TOOLTIP": "Maximum number of simultaneous threads to "
|
||||
"generate thumbnails.",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_LABEL": "Scan Max Level:",
|
||||
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Scan Batch Size:",
|
||||
@@ -742,6 +747,15 @@ _UI_TEXTS = {
|
||||
"VIEWER_MENU_TAGS": "Quick tags",
|
||||
"VIEWER_MENU_CROP": "Crop Mode",
|
||||
"VIEWER_MENU_SAVE_CROP": "Save Selection...",
|
||||
"VIEWER_MENU_COPY_PATH": "Copy File Path",
|
||||
"VIEWER_MENU_COPY_IMAGE": "Copy Image to Clipboard",
|
||||
"VIEWER_MENU_DETECT_AREAS": "Detect areas",
|
||||
"VIEWER_MENU_DETECT_FACES": "Faces",
|
||||
"VIEWER_MENU_DETECT_PETS": "Pets",
|
||||
"VIEWER_MENU_MANIPULATE": "Manipulate",
|
||||
"VIEWER_MENU_ZOOM": "Zoom",
|
||||
"VIEWER_MENU_ZOOM_IN": "Zoom In",
|
||||
"VIEWER_MENU_ZOOM_OUT": "Zoom Out",
|
||||
"SAVE_CROP_TITLE": "Save Cropped Image",
|
||||
"VIEWER_MENU_COMPARE": "Comparison Mode",
|
||||
"VIEWER_MENU_COMPARE_1": "Single View",
|
||||
@@ -762,7 +776,7 @@ _UI_TEXTS = {
|
||||
"CONTEXT_MENU_ROTATE_RIGHT": "Right",
|
||||
"CONTEXT_MENU_TRASH": "Move to Trash",
|
||||
"CONTEXT_MENU_CLIPBOARD": "Clipboard",
|
||||
"CONTEXT_MENU_COPY_URL": "Copy File URL",
|
||||
"CONTEXT_MENU_COPY_FILE": "Copy File URL",
|
||||
"CONTEXT_MENU_COPY_DIR": "Copy Directory Path",
|
||||
"CONTEXT_MENU_PROPERTIES": "Properties",
|
||||
"CONTEXT_MENU_NO_APPS_FOUND": "No apps found",
|
||||
@@ -881,7 +895,7 @@ _UI_TEXTS = {
|
||||
"MENU_ABOUT_TITLE": "Acerca de {}",
|
||||
"MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>Un visor y gestor de imágenes "
|
||||
"simple para KDE con soporte para Baloo.<br><br>Creado por {2} con la ayuda de "
|
||||
"la IA, y mayormente la buena gente de KDE y Qt.",
|
||||
"la IA, pero mayormente gracias al trabajo de la buena gente de KDE y Qt.",
|
||||
"MENU_CACHE": "Caché",
|
||||
"MENU_CLEAR_CACHE": "Limpiar caché ({} ítems, {:.1f} MB, {:.1f} MB en disco)",
|
||||
"MENU_CLEAN_CACHE": "Limpiar entradas de caché inválidas",
|
||||
@@ -898,11 +912,11 @@ _UI_TEXTS = {
|
||||
"SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persona:",
|
||||
"SETTINGS_FACE_ENGINE_LABEL": "Motor de detección de caras:",
|
||||
"SETTINGS_FACE_COLOR_LABEL": "Color del recuadro de cara:",
|
||||
"SETTINGS_MRU_TAGS_COUNT_LABEL": "Máx etiquetas recientes:",
|
||||
"SETTINGS_MRU_TAGS_COUNT_LABEL": "Máximo número de etiquetas recientes:",
|
||||
"SETTINGS_PET_TAGS_LABEL": "Etiquetas de mascota:",
|
||||
"SETTINGS_PET_ENGINE_LABEL": "Motor de detección de mascotas:",
|
||||
"SETTINGS_PET_COLOR_LABEL": "Color del recuadro de mascota:",
|
||||
"SETTINGS_PET_HISTORY_COUNT_LABEL": "Máx historial mascotas:",
|
||||
"SETTINGS_PET_HISTORY_COUNT_LABEL": "Máximo historial de mascotas:",
|
||||
"SETTINGS_PET_TAGS_TOOLTIP": "Etiquetas predeterminadas para mascotas, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_PET_ENGINE_TOOLTIP": "Librería utilizada para la detección de "
|
||||
@@ -919,7 +933,7 @@ _UI_TEXTS = {
|
||||
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de cuerpo:",
|
||||
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de cuerpos:",
|
||||
"SETTINGS_BODY_COLOR_LABEL": "Color del recuadro de cuerpo:",
|
||||
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial cuerpos:",
|
||||
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máximo historial de cuerpos:",
|
||||
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para cuerpos, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_BODY_ENGINE_TOOLTIP": "Librería utilizada para la detección de "
|
||||
@@ -931,7 +945,7 @@ _UI_TEXTS = {
|
||||
"SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de objeto:",
|
||||
"SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de objetos:",
|
||||
"SETTINGS_OBJECT_COLOR_LABEL": "Color del recuadro de objeto:",
|
||||
"SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máx historial objetos:",
|
||||
"SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máximo historial de objetos:",
|
||||
"SETTINGS_OBJECT_TAGS_TOOLTIP": "Etiquetas predeterminadas para objetos, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_OBJECT_ENGINE_TOOLTIP": "Librería utilizada para la detección "
|
||||
@@ -943,7 +957,7 @@ _UI_TEXTS = {
|
||||
"SETTINGS_LANDMARK_TAGS_LABEL": "Etiquetas de lugar:",
|
||||
"SETTINGS_LANDMARK_ENGINE_LABEL": "Motor de detección de lugares:",
|
||||
"SETTINGS_LANDMARK_COLOR_LABEL": "Color del recuadro de lugar:",
|
||||
"SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máx historial lugares:",
|
||||
"SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máximo historial de lugares:",
|
||||
"SETTINGS_LANDMARK_TAGS_TOOLTIP": "Etiquetas predeterminadas para "
|
||||
"lugares/monumentos, separadas por comas.",
|
||||
"SETTINGS_LANDMARK_ENGINE_TOOLTIP": "Librería utilizada para la detección "
|
||||
@@ -952,7 +966,7 @@ _UI_TEXTS = {
|
||||
"alrededor de los lugares.",
|
||||
"SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nombres de lugares "
|
||||
"usados recientemente para recordar.",
|
||||
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máx historial caras:",
|
||||
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máximo historial de caras:",
|
||||
"SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):",
|
||||
"SETTINGS_THUMBS_BG_COLOR_LABEL": "Color de fondo de miniaturas:",
|
||||
"SETTINGS_THUMBS_FILENAME_COLOR_LABEL": "Color del nombre de fichero:",
|
||||
@@ -960,12 +974,15 @@ _UI_TEXTS = {
|
||||
"SETTINGS_THUMBS_RATING_COLOR_LABEL": "Color de valoración de miniaturas:",
|
||||
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Tamaño de fuente del nombre de "
|
||||
"fichero:",
|
||||
"SETTINGS_SCAN_THREADS_LABEL": "Hilos de generación:",
|
||||
"SETTINGS_SCAN_THREADS_TOOLTIP": "Número máximo de hilos simultaneos para "
|
||||
"generar miniaturas.",
|
||||
"SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Tamaño de fuente de las etiquetas:",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_LABEL": "Nivel Máximo de Escaneo:",
|
||||
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño de Lote de Escaneo:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de búsqueda de archivos:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_TOOLTIP": "Motor a usar para buscar archivos. "
|
||||
"'Bagheera' usa la librería de BagheeraSearch. 'Baloo0 usa el commando "
|
||||
"'Bagheera' usa la librería de BagheeraSearch. 'Baloo' usa el commando "
|
||||
"'baloosearch'",
|
||||
"SETTINGS_SCAN_FULL_ON_START_LABEL": "Escanear Todo al Inicio:",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_TOOLTIP": "Profundidad máxima de directorio para "
|
||||
@@ -1177,6 +1194,15 @@ _UI_TEXTS = {
|
||||
"VIEWER_MENU_TAGS": "Etiquetas rápidas",
|
||||
"VIEWER_MENU_CROP": "Modo Recorte",
|
||||
"VIEWER_MENU_SAVE_CROP": "Guardar Selección...",
|
||||
"VIEWER_MENU_COPY_PATH": "Copiar Ruta del Archivo",
|
||||
"VIEWER_MENU_COPY_IMAGE": "Copiar Imagen al Portapapeles",
|
||||
"VIEWER_MENU_DETECT_AREAS": "Detectar áreas",
|
||||
"VIEWER_MENU_DETECT_FACES": "Rostros",
|
||||
"VIEWER_MENU_DETECT_PETS": "Mascotas",
|
||||
"VIEWER_MENU_MANIPULATE": "Manipular",
|
||||
"VIEWER_MENU_ZOOM": "Zoom",
|
||||
"VIEWER_MENU_ZOOM_IN": "Acercar",
|
||||
"VIEWER_MENU_ZOOM_OUT": "Alejar",
|
||||
"VIEWER_MENU_COMPARE": "Modo Comparación",
|
||||
"VIEWER_MENU_COMPARE_1": "Vista Única",
|
||||
"VIEWER_MENU_COMPARE_2": "2 Imágenes",
|
||||
@@ -1197,7 +1223,7 @@ _UI_TEXTS = {
|
||||
"CONTEXT_MENU_ROTATE_RIGHT": "Derecha",
|
||||
"CONTEXT_MENU_TRASH": "Mover a la Papelera",
|
||||
"CONTEXT_MENU_CLIPBOARD": "Portapapeles",
|
||||
"CONTEXT_MENU_COPY_URL": "Copiar URL del Archivo",
|
||||
"CONTEXT_MENU_COPY_FILE": "Copiar URL del Archivo",
|
||||
"CONTEXT_MENU_COPY_DIR": "Copiar Ruta del Directorio",
|
||||
"CONTEXT_MENU_PROPERTIES": "Propiedades",
|
||||
"CONTEXT_MENU_NO_APPS_FOUND": "No se encontraron aplicaciones",
|
||||
@@ -1316,7 +1342,7 @@ _UI_TEXTS = {
|
||||
"MENU_ABOUT_TITLE": "Acerca de {}",
|
||||
"MENU_ABOUT_TEXT": "<b>{0}</b> v{1}<br><br>Un visor e xestor de imaxes "
|
||||
"sinxelo para KDE con soporte para Baloo.<br><br>Creado por {2} coa axuda da "
|
||||
" IA, e maiormente a boa xente de KDE e Qt.",
|
||||
" IA, pero maiormente gracias ó traballo da boa xente de KDE e Qt.",
|
||||
"MENU_CACHE": "Caché",
|
||||
"MENU_CLEAR_CACHE": "Limpar caché ({} elementos, {:.1f} MB, {:.1f} MB en "
|
||||
"disco)",
|
||||
@@ -1328,17 +1354,17 @@ _UI_TEXTS = {
|
||||
"MENU_SHOW_HISTORY": "Amosar Historial",
|
||||
"MENU_SETTINGS": "Opcións",
|
||||
"SETTINGS_GROUP_SCANNER": "Escáner",
|
||||
"SETTINGS_GROUP_AREAS": "´áreas",
|
||||
"SETTINGS_GROUP_AREAS": "Áreas",
|
||||
"SETTINGS_GROUP_THUMBNAILS": "Miniaturas",
|
||||
"SETTINGS_GROUP_VIEWER": "Visor de Imaxes",
|
||||
"SETTINGS_PERSON_TAGS_LABEL": "Etiquetas de persoa:",
|
||||
"SETTINGS_FACE_ENGINE_LABEL": "Motor de detección de caras:",
|
||||
"SETTINGS_FACE_COLOR_LABEL": "Cor do cadro de cara:",
|
||||
"SETTINGS_MRU_TAGS_COUNT_LABEL": "Máx etiquetas recentes:",
|
||||
"SETTINGS_MRU_TAGS_COUNT_LABEL": "Máximo número de etiquetas recentes:",
|
||||
"SETTINGS_PET_TAGS_LABEL": "Etiquetas de mascota:",
|
||||
"SETTINGS_PET_ENGINE_LABEL": "Motor de detección de mascotas:",
|
||||
"SETTINGS_PET_COLOR_LABEL": "Cor do cadro de mascota:",
|
||||
"SETTINGS_PET_HISTORY_COUNT_LABEL": "Máx historial mascotas:",
|
||||
"SETTINGS_PET_HISTORY_COUNT_LABEL": "Máximo historial de mascotas:",
|
||||
"SETTINGS_PET_TAGS_TOOLTIP": "Etiquetas predeterminadas para mascotas, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_PET_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
|
||||
@@ -1355,7 +1381,7 @@ _UI_TEXTS = {
|
||||
"SETTINGS_BODY_TAGS_LABEL": "Etiquetas de corpo:",
|
||||
"SETTINGS_BODY_ENGINE_LABEL": "Motor de detección de corpos:",
|
||||
"SETTINGS_BODY_COLOR_LABEL": "Cor do cadro de corpo:",
|
||||
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máx historial corpos:",
|
||||
"SETTINGS_BODY_HISTORY_COUNT_LABEL": "Máximo historial de corpos:",
|
||||
"SETTINGS_BODY_TAGS_TOOLTIP": "Etiquetas predeterminadas para corpos, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_BODY_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
|
||||
@@ -1367,7 +1393,7 @@ _UI_TEXTS = {
|
||||
"SETTINGS_OBJECT_TAGS_LABEL": "Etiquetas de obxecto:",
|
||||
"SETTINGS_OBJECT_ENGINE_LABEL": "Motor de detección de obxectos:",
|
||||
"SETTINGS_OBJECT_COLOR_LABEL": "Cor do cadro de obxecto:",
|
||||
"SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máx historial obxectos:",
|
||||
"SETTINGS_OBJECT_HISTORY_COUNT_LABEL": "Máximo historial de obxectos:",
|
||||
"SETTINGS_OBJECT_TAGS_TOOLTIP": "Etiquetas predeterminadas para obxectos, "
|
||||
"separadas por comas.",
|
||||
"SETTINGS_OBJECT_ENGINE_TOOLTIP": "Libraría utilizada para a detección de "
|
||||
@@ -1379,7 +1405,7 @@ _UI_TEXTS = {
|
||||
"SETTINGS_LANDMARK_TAGS_LABEL": "Etiquetas de lugar:",
|
||||
"SETTINGS_LANDMARK_ENGINE_LABEL": "Motor de detección de lugares:",
|
||||
"SETTINGS_LANDMARK_COLOR_LABEL": "Cor do cadro de lugar:",
|
||||
"SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máx historial lugares:",
|
||||
"SETTINGS_LANDMARK_HISTORY_COUNT_LABEL": "Máximo historial de lugares:",
|
||||
"SETTINGS_LANDMARK_TAGS_TOOLTIP": "Etiquetas predeterminadas para "
|
||||
"lugares/monumentos, separadas por comas.",
|
||||
"SETTINGS_LANDMARK_ENGINE_TOOLTIP": "Libraría utilizada para a detección "
|
||||
@@ -1388,7 +1414,7 @@ _UI_TEXTS = {
|
||||
"arredor dos lugares.",
|
||||
"SETTINGS_LANDMARK_HISTORY_TOOLTIP": "Número máximo de nomes de lugares "
|
||||
"usados recentemente para lembrar.",
|
||||
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máx historial caras:",
|
||||
"SETTINGS_FACE_HISTORY_COUNT_LABEL": "Máximo historial de caras:",
|
||||
"SETTINGS_THUMBS_REFRESH_LABEL": "Intervalo refresco miniaturas (ms):",
|
||||
"SETTINGS_THUMBS_BG_COLOR_LABEL": "Cor de fondo de miniaturas:",
|
||||
"SETTINGS_THUMBS_FILENAME_COLOR_LABEL": "Cor do nome de ficheiro:",
|
||||
@@ -1396,10 +1422,10 @@ _UI_TEXTS = {
|
||||
"SETTINGS_THUMBS_RATING_COLOR_LABEL": "Cor da valoración das miniaturas:",
|
||||
"SETTINGS_THUMBS_FILENAME_FONT_SIZE_LABEL": "Tamaño da fonte do nome de "
|
||||
"ficheiro:",
|
||||
"SETTINGS_SCAN_THREADS_LABEL": "Fios de xeración:",
|
||||
"SETTINGS_SCAN_THREADS_TOOLTIP": "Número máximo de fios simultaneos para "
|
||||
"xerar miniaturas.",
|
||||
"SETTINGS_THUMBS_TAGS_FONT_SIZE_LABEL": "Tamaño da fonte das etiquetas:",
|
||||
"SETTINGS_SCAN_THREADS_LABEL": "Hilos de generación:",
|
||||
"SETTINGS_SCAN_THREADS_TOOLTIP": "Número máximo de hilos simultáneos para "
|
||||
"generar miniaturas.",
|
||||
"SETTINGS_SCAN_MAX_LEVEL_LABEL": "Nivel Máximo de Escaneo:",
|
||||
"SETTINGS_SCAN_BATCH_SIZE_LABEL": "Tamaño do Lote de Escaneo:",
|
||||
"SETTINGS_SCANNER_SEARCH_ENGINE_LABEL": "Motor de busca de ficheiros:",
|
||||
@@ -1615,6 +1641,15 @@ _UI_TEXTS = {
|
||||
"VIEWER_MENU_TAGS": "Etiquetas rápidas",
|
||||
"VIEWER_MENU_CROP": "Modo Recorte",
|
||||
"VIEWER_MENU_SAVE_CROP": "Gardar Selección...",
|
||||
"VIEWER_MENU_COPY_PATH": "Copiar Ruta do Ficheiro",
|
||||
"VIEWER_MENU_COPY_IMAGE": "Copiar Imaxe ao Portapapeis",
|
||||
"VIEWER_MENU_DETECT_AREAS": "Detectar áreas",
|
||||
"VIEWER_MENU_DETECT_FACES": "Rostros",
|
||||
"VIEWER_MENU_DETECT_PETS": "Mascotas",
|
||||
"VIEWER_MENU_MANIPULATE": "Manipular",
|
||||
"VIEWER_MENU_ZOOM": "Zoom",
|
||||
"VIEWER_MENU_ZOOM_IN": "Achegar",
|
||||
"VIEWER_MENU_ZOOM_OUT": "Afastar",
|
||||
"VIEWER_MENU_COMPARE": "Modo Comparación",
|
||||
"VIEWER_MENU_COMPARE_1": "Vista Única",
|
||||
"VIEWER_MENU_COMPARE_2": "2 Imaxes",
|
||||
@@ -1636,7 +1671,7 @@ _UI_TEXTS = {
|
||||
"CONTEXT_MENU_ROTATE_RIGHT": "Dereita",
|
||||
"CONTEXT_MENU_TRASH": "Mover á Papeleira",
|
||||
"CONTEXT_MENU_CLIPBOARD": "Portapapeis",
|
||||
"CONTEXT_MENU_COPY_URL": "Copiar URL do Ficheiro",
|
||||
"CONTEXT_MENU_COPY_FILE": "Copiar URL do Ficheiro",
|
||||
"CONTEXT_MENU_COPY_DIR": "Copiar Ruta do Directorio",
|
||||
"CONTEXT_MENU_PROPERTIES": "Propiedades",
|
||||
"CONTEXT_MENU_NO_APPS_FOUND": "Non se atoparon aplicacións",
|
||||
|
||||
@@ -36,19 +36,16 @@ from PySide6.QtGui import QImage, QImageReader, QImageIOHandler
|
||||
|
||||
from constants import (
|
||||
APP_CONFIG, CACHE_PATH, CACHE_MAX_SIZE, CONFIG_DIR, DISK_CACHE_MAX_BYTES,
|
||||
IMAGE_EXTENSIONS, SEARCH_CMD, THUMBNAIL_SIZES, RATING_XATTR_NAME, XATTR_NAME,
|
||||
UITexts, SCANNER_SETTINGS_DEFAULTS, HAVE_BAGHEERASEARCH_LIB
|
||||
HAVE_BAGHEERASEARCH_LIB, IMAGE_EXTENSIONS, RATING_XATTR_NAME,
|
||||
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, THUMBNAIL_SIZES, XATTR_NAME,
|
||||
UITexts
|
||||
)
|
||||
|
||||
from imageviewer import ImageViewer
|
||||
from metadatamanager import XattrManager
|
||||
|
||||
if HAVE_BAGHEERASEARCH_LIB:
|
||||
try:
|
||||
from bagheera_search_lib import BagheeraSearcher
|
||||
except ImportError:
|
||||
HAVE_BAGHEERASEARCH_LIB = False
|
||||
pass
|
||||
|
||||
# Set up logging for better debugging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
387
imageviewer.py
387
imageviewer.py
@@ -172,14 +172,14 @@ class FastTagManager:
|
||||
def __init__(self, viewer):
|
||||
self.viewer = viewer
|
||||
self.main_win = viewer.main_win
|
||||
self.controller = viewer.controller
|
||||
|
||||
def show_menu(self):
|
||||
"""Builds and shows a context menu for quickly adding/removing tags."""
|
||||
if not self.main_win or not self.controller.get_current_path():
|
||||
controller = self.viewer.controller
|
||||
if not self.main_win or not controller or not controller.get_current_path():
|
||||
return
|
||||
|
||||
current_path = self.controller.get_current_path()
|
||||
current_path = controller.get_current_path()
|
||||
try:
|
||||
raw_tags = os.getxattr(current_path, XATTR_NAME).decode('utf-8')
|
||||
current_tags = {t.strip() for t in raw_tags.split(',') if t.strip()}
|
||||
@@ -233,10 +233,11 @@ class FastTagManager:
|
||||
return
|
||||
tag_name = action.text()
|
||||
is_checked = action.isChecked()
|
||||
current_path = self.controller.get_current_path()
|
||||
controller = self.viewer.controller
|
||||
current_path = controller.get_current_path() if controller else None
|
||||
if not current_path:
|
||||
return
|
||||
self.controller.toggle_tag(tag_name, is_checked)
|
||||
controller.toggle_tag(tag_name, is_checked)
|
||||
self.viewer.update_status_bar()
|
||||
if self.main_win:
|
||||
if is_checked:
|
||||
@@ -289,18 +290,23 @@ class FilmStripWidget(QListWidget):
|
||||
allowing the user to quickly jump to an image by clicking its thumbnail.
|
||||
It also supports dragging files out of the application.
|
||||
"""
|
||||
def __init__(self, controller, parent=None):
|
||||
def __init__(self, viewer, parent=None):
|
||||
"""
|
||||
Initializes the FilmStripWidget.
|
||||
|
||||
Args:
|
||||
controller (ImageController): The controller managing the image list.
|
||||
viewer (ImageViewer): The viewer that owns this filmstrip.
|
||||
parent (QWidget, optional): The parent widget. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.controller = controller
|
||||
self.viewer = viewer
|
||||
self.setDragEnabled(True)
|
||||
|
||||
@property
|
||||
def controller(self):
|
||||
"""Returns the controller of the active pane in the viewer."""
|
||||
return self.viewer.controller
|
||||
|
||||
def startDrag(self, supportedActions):
|
||||
"""
|
||||
Initiates a drag-and-drop operation for the selected image(s).
|
||||
@@ -312,7 +318,7 @@ class FilmStripWidget(QListWidget):
|
||||
urls = []
|
||||
for item in items:
|
||||
row = self.row(item)
|
||||
if 0 <= row < len(self.controller.image_list):
|
||||
if self.controller and 0 <= row < len(self.controller.image_list):
|
||||
path = self.controller.image_list[row]
|
||||
urls.append(QUrl.fromLocalFile(path))
|
||||
|
||||
@@ -332,6 +338,63 @@ class FilmStripWidget(QListWidget):
|
||||
|
||||
drag.exec(Qt.CopyAction)
|
||||
|
||||
def _get_selected_paths(self):
|
||||
"""Helper to get paths of all selected items."""
|
||||
return [item.data(Qt.UserRole)
|
||||
for item in self.selectedItems() if item.data(Qt.UserRole)]
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""Shows a context menu for the selected items."""
|
||||
selected_items = self.selectedItems()
|
||||
if not selected_items:
|
||||
return
|
||||
|
||||
menu = QMenu(self)
|
||||
|
||||
# Clipboard Submenu
|
||||
clipboard_menu = menu.addMenu(
|
||||
QIcon.fromTheme("edit-copy"), UITexts.CONTEXT_MENU_CLIPBOARD)
|
||||
|
||||
# Copy Image
|
||||
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_items) > 1:
|
||||
action_copy_image.setEnabled(False)
|
||||
|
||||
# Copy Path
|
||||
action_copy_path = clipboard_menu.addAction(
|
||||
QIcon.fromTheme("document-properties"), UITexts.VIEWER_MENU_COPY_PATH)
|
||||
action_copy_path.triggered.connect(self._copy_path_to_clipboard)
|
||||
|
||||
# Copy Directory Path
|
||||
action_copy_dir = clipboard_menu.addAction(
|
||||
QIcon.fromTheme("folder"), UITexts.CONTEXT_MENU_COPY_DIR)
|
||||
action_copy_dir.triggered.connect(self._copy_dir_to_clipboard)
|
||||
|
||||
menu.exec(event.globalPos())
|
||||
|
||||
def _copy_image_to_clipboard(self):
|
||||
"""Copies the currently selected image to the system clipboard."""
|
||||
paths = self._get_selected_paths()
|
||||
if len(paths) == 1 and paths[0] and os.path.exists(paths[0]):
|
||||
img = QImage(paths[0])
|
||||
if not img.isNull():
|
||||
QApplication.clipboard().setImage(img)
|
||||
|
||||
def _copy_path_to_clipboard(self):
|
||||
"""Copies the file path(s) of the selected image(s) to the clipboard."""
|
||||
paths = self._get_selected_paths()
|
||||
if paths:
|
||||
QApplication.clipboard().setText("\n".join(paths))
|
||||
|
||||
def _copy_dir_to_clipboard(self):
|
||||
"""Copies the directory path(s) of the selected image(s) to the clipboard."""
|
||||
paths = self._get_selected_paths()
|
||||
if paths:
|
||||
dir_paths = sorted(list(set(os.path.dirname(p) for p in paths)))
|
||||
QApplication.clipboard().setText("\n".join(dir_paths))
|
||||
|
||||
|
||||
class FaceCanvas(QLabel):
|
||||
"""
|
||||
@@ -1222,8 +1285,8 @@ class ImagePane(QWidget):
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setAlignment(Qt.AlignCenter)
|
||||
self.scroll_area.setStyleSheet("background-color: #000; border: none;")
|
||||
# self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
# self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.scroll_area.setWidget(self.canvas)
|
||||
layout.addWidget(self.scroll_area)
|
||||
|
||||
@@ -1413,7 +1476,7 @@ class ImageViewer(QWidget):
|
||||
# self.canvas = FaceCanvas(self) ... Moved to ImagePane
|
||||
# self.scroll_area.setWidget(self.canvas)
|
||||
|
||||
self.filmstrip = FilmStripWidget(self.controller)
|
||||
self.filmstrip = FilmStripWidget(self)
|
||||
self.filmstrip.setSpacing(2)
|
||||
self.filmstrip.itemClicked.connect(self.on_filmstrip_clicked)
|
||||
|
||||
@@ -1564,7 +1627,6 @@ class ImageViewer(QWidget):
|
||||
pane.scrolled.connect(self._sync_scroll)
|
||||
pane.zoom_manager.zoomed.connect(self._sync_zoom)
|
||||
|
||||
self.filmstrip.controller = pane.controller
|
||||
self.populate_filmstrip()
|
||||
self.sync_filmstrip_selection(pane.controller.index)
|
||||
self.update_status_bar()
|
||||
@@ -1618,6 +1680,10 @@ class ImageViewer(QWidget):
|
||||
if count == current_panes:
|
||||
return
|
||||
|
||||
if count > 1 and self.slideshow_manager.is_running():
|
||||
self.slideshow_manager.stop()
|
||||
self.update_title()
|
||||
|
||||
if count > current_panes:
|
||||
# Add panes
|
||||
base_controller = self.active_pane.controller
|
||||
@@ -1726,6 +1792,9 @@ class ImageViewer(QWidget):
|
||||
"properties": self.show_properties,
|
||||
"toggle_visibility": self.toggle_main_window_visibility,
|
||||
"toggle_crop": self.toggle_crop_mode,
|
||||
"copy_image": self.copy_image_to_clipboard,
|
||||
"copy_path": self.copy_file_path_to_clipboard,
|
||||
"copy_dir_path": self.copy_dir_path_to_clipboard,
|
||||
"save_crop": self.save_cropped_image,
|
||||
"compare_1": lambda: self.set_comparison_mode(1),
|
||||
"compare_2": lambda: self.set_comparison_mode(2),
|
||||
@@ -1735,6 +1804,11 @@ class ImageViewer(QWidget):
|
||||
|
||||
def _execute_action(self, action):
|
||||
"""Executes the method corresponding to the action name."""
|
||||
if self.slideshow_manager.is_running():
|
||||
allowed_actions = ('slideshow', 'slideshow_reverse', 'close', 'fullscreen')
|
||||
if action not in allowed_actions:
|
||||
return
|
||||
|
||||
if action in self._actions:
|
||||
self._actions[action]()
|
||||
|
||||
@@ -2383,6 +2457,81 @@ class ImageViewer(QWidget):
|
||||
if self.main_win:
|
||||
self.main_win.update_metadata_for_path(path, metadata)
|
||||
|
||||
def copy_image_to_clipboard(self):
|
||||
"""Copies the currently displayed image to the system clipboard."""
|
||||
if self.controller and not self.controller.pixmap_original.isNull():
|
||||
QApplication.clipboard().setImage(self.controller.pixmap_original.toImage())
|
||||
|
||||
def copy_file_path_to_clipboard(self):
|
||||
"""Copies the current image's file path to the system clipboard."""
|
||||
if self.controller:
|
||||
path = self.controller.get_current_path()
|
||||
if path:
|
||||
QApplication.clipboard().setText(path)
|
||||
|
||||
def copy_dir_path_to_clipboard(self):
|
||||
"""Copies the directory path of the current image to the system clipboard."""
|
||||
if self.controller:
|
||||
path = self.controller.get_current_path()
|
||||
if path:
|
||||
QApplication.clipboard().setText(os.path.dirname(path))
|
||||
|
||||
def _populate_open_with_menu(self, menu):
|
||||
"""Populates the 'Open With' submenu."""
|
||||
if self.main_win:
|
||||
path = self.controller.get_current_path()
|
||||
if path:
|
||||
self.main_win.populate_open_with_submenu(menu, path)
|
||||
|
||||
def _build_menu_from_data(self, target_menu, items):
|
||||
"""Builds a menu or submenu from a list of dictionary items."""
|
||||
for item in items:
|
||||
if item == "separator":
|
||||
target_menu.addSeparator()
|
||||
continue
|
||||
|
||||
# Handle dynamic submenus that need to be populated by a function
|
||||
if "dynamic_submenu" in item:
|
||||
icon = QIcon.fromTheme(item.get("icon", ""))
|
||||
submenu = target_menu.addMenu(icon, item["text"])
|
||||
item["dynamic_submenu"](submenu)
|
||||
continue
|
||||
|
||||
action_name = item.get("action")
|
||||
display_text = item["text"]
|
||||
|
||||
# Add shortcut string to display text if available
|
||||
if action_name and "submenu" not in item and \
|
||||
action_name in self.action_to_shortcut:
|
||||
key, mods = self.action_to_shortcut[action_name]
|
||||
try:
|
||||
mod_val = int(mods)
|
||||
except TypeError:
|
||||
mod_val = mods.value
|
||||
seq = QKeySequence(mod_val | key)
|
||||
shortcut_str = seq.toString(QKeySequence.NativeText)
|
||||
if shortcut_str:
|
||||
display_text += f"\t{shortcut_str}"
|
||||
|
||||
icon = QIcon.fromTheme(item.get("icon", ""))
|
||||
|
||||
if "submenu" in item:
|
||||
submenu = target_menu.addMenu(icon, item["text"])
|
||||
self._build_menu_from_data(submenu, item["submenu"])
|
||||
else:
|
||||
action = target_menu.addAction(icon, display_text)
|
||||
slot = item.get("slot")
|
||||
if action_name:
|
||||
action.triggered.connect(
|
||||
lambda checked=False, name=action_name:
|
||||
self._execute_action(name))
|
||||
elif slot:
|
||||
action.triggered.connect(slot)
|
||||
|
||||
if item.get("checkable"):
|
||||
action.setCheckable(True)
|
||||
action.setChecked(item.get("checked", False))
|
||||
|
||||
def restore_scroll_for_pane(self, pane, config):
|
||||
"""
|
||||
Applies the saved scrollbar positions from a layout configuration.
|
||||
@@ -2451,11 +2600,15 @@ class ImageViewer(QWidget):
|
||||
|
||||
def toggle_slideshow(self):
|
||||
"""Starts or stops the automatic slideshow timer."""
|
||||
if len(self.panes) > 1:
|
||||
return
|
||||
self.slideshow_manager.toggle(reverse=False)
|
||||
self.update_view(resize_win=False)
|
||||
|
||||
def toggle_slideshow_reverse(self):
|
||||
"""Starts or stops the automatic reverse slideshow timer."""
|
||||
if len(self.panes) > 1:
|
||||
return
|
||||
self.slideshow_manager.toggle(reverse=True)
|
||||
self.update_view(resize_win=False)
|
||||
|
||||
@@ -2604,53 +2757,94 @@ class ImageViewer(QWidget):
|
||||
"""Builds and returns the general viewer context menu."""
|
||||
menu = QMenu(self)
|
||||
|
||||
# Add "Open With" submenu
|
||||
if self.main_win:
|
||||
path = self.controller.get_current_path()
|
||||
if path:
|
||||
open_submenu = menu.addMenu(QIcon.fromTheme("document-open"),
|
||||
UITexts.CONTEXT_MENU_OPEN)
|
||||
self.main_win.populate_open_with_submenu(open_submenu, path)
|
||||
menu.addSeparator()
|
||||
menu_structure = []
|
||||
if self.slideshow_manager.is_running():
|
||||
is_fwd = self.slideshow_manager.is_forward()
|
||||
is_rev = self.slideshow_manager.is_reverse()
|
||||
menu_structure = [
|
||||
{"text": UITexts.VIEWER_MENU_STOP_SLIDESHOW if is_fwd
|
||||
else UITexts.VIEWER_MENU_START_SLIDESHOW, "action": "slideshow",
|
||||
"icon": "media-playback-stop" if is_fwd else "media-playback-start"},
|
||||
{"text": UITexts.VIEWER_MENU_STOP_REVERSE_SLIDESHOW if is_rev
|
||||
else UITexts.VIEWER_MENU_START_REVERSE_SLIDESHOW,
|
||||
"action": "slideshow_reverse",
|
||||
"icon": "media-playback-stop" if is_rev else "media-seek-backward"},
|
||||
{"text": UITexts.VIEWER_MENU_SET_INTERVAL,
|
||||
"slot": self.set_slideshow_interval, "icon": "preferences-system-time"}
|
||||
]
|
||||
|
||||
menu_items = [
|
||||
{"text": UITexts.VIEWER_MENU_TAGS, "action": "fast_tag",
|
||||
"icon": "document-properties"},
|
||||
else:
|
||||
# Build the normal menu structure
|
||||
is_fwd = self.slideshow_manager.is_forward()
|
||||
is_rev = self.slideshow_manager.is_reverse()
|
||||
|
||||
slideshow_submenu = [
|
||||
{"text": UITexts.VIEWER_MENU_STOP_SLIDESHOW if is_fwd
|
||||
else UITexts.VIEWER_MENU_START_SLIDESHOW, "action": "slideshow",
|
||||
"icon": "media-playback-stop" if is_fwd else "media-playback-start"},
|
||||
{"text": UITexts.VIEWER_MENU_STOP_REVERSE_SLIDESHOW if is_rev
|
||||
else UITexts.VIEWER_MENU_START_REVERSE_SLIDESHOW,
|
||||
"action": "slideshow_reverse",
|
||||
"icon": "media-playback-stop" if is_rev else "media-seek-backward"},
|
||||
{"text": UITexts.VIEWER_MENU_SET_INTERVAL,
|
||||
"slot": self.set_slideshow_interval, "icon": "preferences-system-time"}
|
||||
]
|
||||
|
||||
menu_structure = [
|
||||
{"text": UITexts.CONTEXT_MENU_OPEN, "icon": "document-open",
|
||||
"dynamic_submenu": self._populate_open_with_menu},
|
||||
"separator",
|
||||
{"text": UITexts.DETECT_FACES, "action": "detect_faces",
|
||||
"icon": "edit-image-face-recognize"},
|
||||
{"text": UITexts.VIEWER_MENU_TAGS,
|
||||
"action": "fast_tag", "icon": "document-properties"},
|
||||
{"text": UITexts.VIEWER_MENU_DETECT_AREAS,
|
||||
"icon": "edit-image-face-recognize", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_DETECT_FACES,
|
||||
"action": "detect_faces"},
|
||||
{"text": UITexts.VIEWER_MENU_DETECT_PETS, "action": "detect_pets"},
|
||||
]},
|
||||
"separator",
|
||||
{"text": UITexts.DETECT_PETS, "action": "detect_pets",
|
||||
"icon": "edit-image-face-recognize"},
|
||||
"separator",
|
||||
{"text": UITexts.DETECT_BODIES, "action": "detect_bodies",
|
||||
"icon": "edit-image-face-recognize"},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_ROTATE, "icon": "transform-rotate",
|
||||
"submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_MANIPULATE,
|
||||
"icon": "transform", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_ROTATE,
|
||||
"icon": "transform-rotate", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_ROTATE_LEFT,
|
||||
"action": "rotate_left", "icon": "object-rotate-left"},
|
||||
{"text": UITexts.VIEWER_MENU_ROTATE_RIGHT,
|
||||
"action": "rotate_right", "icon": "object-rotate-right"}
|
||||
]},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_FLIP, "icon": "transform-flip", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_FLIP,
|
||||
"icon": "transform-flip", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_FLIP_H,
|
||||
"action": "flip_horizontal",
|
||||
"icon": "object-flip-horizontal"},
|
||||
"action": "flip_horizontal", "icon": "object-flip-horizontal"},
|
||||
{"text": UITexts.VIEWER_MENU_FLIP_V,
|
||||
"action": "flip_vertical", "icon": "object-flip-vertical"}
|
||||
]}
|
||||
]},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_RENAME, "action": "rename",
|
||||
"icon": "edit-rename"},
|
||||
{"text": UITexts.VIEWER_MENU_ZOOM, "icon": "zoom", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_ZOOM_IN,
|
||||
"action": "zoom_in", "icon": "zoom-in"},
|
||||
{"text": UITexts.VIEWER_MENU_ZOOM_OUT,
|
||||
"action": "zoom_out", "icon": "zoom-out"},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_FIT_SCREEN,
|
||||
"slot": self.zoom_manager.toggle_fit_to_screen,
|
||||
"icon": "zoom-fit-best"},
|
||||
]},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_CROP, "action": "toggle_crop",
|
||||
"icon": "transform-crop", "checkable": True}, # checked updated later
|
||||
{"text": UITexts.VIEWER_MENU_RENAME,
|
||||
"action": "rename", "icon": "edit-rename"},
|
||||
{"text": UITexts.CONTEXT_MENU_CLIPBOARD,
|
||||
"icon": "edit-copy", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_COPY_IMAGE,
|
||||
"action": "copy_image", "icon": "image-x-generic"},
|
||||
{"text": UITexts.VIEWER_MENU_COPY_PATH,
|
||||
"action": "copy_path", "icon": "document-properties"},
|
||||
{"text": UITexts.CONTEXT_MENU_COPY_DIR,
|
||||
"action": "copy_dir_path", "icon": "folder"},
|
||||
]},
|
||||
{"text": UITexts.VIEWER_MENU_CROP,
|
||||
"action": "toggle_crop", "icon": "transform-crop", "checkable": True,
|
||||
"checked": self.crop_mode},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_COMPARE, "icon": "view-grid", "submenu": [
|
||||
{"text": UITexts.VIEWER_MENU_COMPARE_1,
|
||||
@@ -2668,104 +2862,43 @@ class ImageViewer(QWidget):
|
||||
]
|
||||
|
||||
if self.movie:
|
||||
is_paused = self.movie.state() == QMovie.Paused_
|
||||
pause_text = (UITexts.VIEWER_MENU_RESUME_ANIMATION if is_paused
|
||||
else UITexts.VIEWER_MENU_PAUSE_ANIMATION)
|
||||
pause_icon = ("media-playback-start" if is_paused
|
||||
else "media-playback-pause")
|
||||
menu_items.append({"text": pause_text, "action": "toggle_animation",
|
||||
is_paused = self.movie.state() == QMovie.Paused
|
||||
pause_text = UITexts.VIEWER_MENU_RESUME_ANIMATION \
|
||||
if is_paused else UITexts.VIEWER_MENU_PAUSE_ANIMATION
|
||||
pause_icon = "media-playback-start" \
|
||||
if is_paused else "media-playback-pause"
|
||||
menu_structure.append(
|
||||
{"text": pause_text, "action": "toggle_animation",
|
||||
"icon": pause_icon})
|
||||
|
||||
is_fwd_slideshow = self.slideshow_manager.is_forward()
|
||||
is_rev_slideshow = self.slideshow_manager.is_reverse()
|
||||
|
||||
slideshow_submenu = [
|
||||
{"text": UITexts.VIEWER_MENU_STOP_SLIDESHOW if is_fwd_slideshow
|
||||
else UITexts.VIEWER_MENU_START_SLIDESHOW, "action": "slideshow",
|
||||
"icon": "media-playback-stop" if is_fwd_slideshow
|
||||
else "media-playback-start"},
|
||||
{"text": UITexts.VIEWER_MENU_STOP_REVERSE_SLIDESHOW if is_rev_slideshow
|
||||
else UITexts.VIEWER_MENU_START_REVERSE_SLIDESHOW,
|
||||
"action": "slideshow_reverse",
|
||||
"icon": "media-playback-stop" if is_rev_slideshow
|
||||
else "media-seek-backward"},
|
||||
{"text": UITexts.VIEWER_MENU_SET_INTERVAL,
|
||||
"slot": self.set_slideshow_interval, "icon": "preferences-system-time"}
|
||||
]
|
||||
|
||||
menu_items.extend([
|
||||
{"text": UITexts.VIEWER_MENU_SLIDESHOW, "icon": "view-presentation",
|
||||
"submenu": slideshow_submenu},
|
||||
menu_structure.extend([
|
||||
{"text": UITexts.VIEWER_MENU_SLIDESHOW,
|
||||
"icon": "view-presentation", "submenu": slideshow_submenu},
|
||||
"separator",
|
||||
{"text": UITexts.SHOW_FACES, "action": "toggle_faces",
|
||||
"icon": "edit-image-face-show",
|
||||
"checkable": True, "checked": self.controller.show_faces},
|
||||
{"text": UITexts.SHOW_FACES,
|
||||
"action": "toggle_faces", "icon": "edit-image-face-show",
|
||||
"checkable": True,
|
||||
"checked": self.controller.show_faces if self.controller else False},
|
||||
{"text": UITexts.VIEWER_MENU_SHOW_FILMSTRIP,
|
||||
"action": "toggle_filmstrip", "icon": "view-filmstrip", "checkable": True,
|
||||
"checked": self.filmstrip.isVisible()},
|
||||
"action": "toggle_filmstrip", "icon": "view-filmstrip",
|
||||
"checkable": True, "checked": self.filmstrip.isVisible()},
|
||||
{"text": UITexts.VIEWER_MENU_SHOW_STATUSBAR,
|
||||
"action": "toggle_statusbar", "icon": "view-bottom-panel",
|
||||
"checkable": True,
|
||||
"checked": self.status_bar_container.isVisible()},
|
||||
"checkable": True, "checked": self.status_bar_container.isVisible()},
|
||||
"separator",
|
||||
{"text": UITexts.VIEWER_MENU_EXIT_FULLSCREEN
|
||||
if self.isFullScreen() else UITexts.VIEWER_MENU_ENTER_FULLSCREEN,
|
||||
"action": "fullscreen",
|
||||
"icon": "view-fullscreen" if not self.isFullScreen() else "view-restore"},
|
||||
"action": "fullscreen", "icon": "view-fullscreen"
|
||||
if not self.isFullScreen() else "view-restore"},
|
||||
"separator",
|
||||
{"text": "Show/hide main window",
|
||||
"action": "toggle_visibility",
|
||||
"icon": "view-restore"},
|
||||
"action": "toggle_visibility", "icon": "view-restore"},
|
||||
"separator",
|
||||
{"text": UITexts.CONTEXT_MENU_PROPERTIES, "action": "properties",
|
||||
"icon": "document-properties"}
|
||||
{"text": UITexts.CONTEXT_MENU_PROPERTIES,
|
||||
"action": "properties", "icon": "document-properties"}
|
||||
])
|
||||
|
||||
def build_actions(target_menu, items):
|
||||
for item in items:
|
||||
if item == "separator":
|
||||
target_menu.addSeparator()
|
||||
continue
|
||||
|
||||
action_name = item.get("action")
|
||||
display_text = item["text"]
|
||||
|
||||
# Only add shortcut to final actions, not to submenus
|
||||
if action_name and "submenu" not in item and \
|
||||
action_name in self.action_to_shortcut:
|
||||
key, mods = self.action_to_shortcut[action_name]
|
||||
# Handle both Qt.KeyboardModifier (enum) and Qt.KeyboardModifiers
|
||||
# (flags) by ensuring we have an integer value for the modifier
|
||||
# before the bitwise OR.
|
||||
try:
|
||||
mod_val = int(mods)
|
||||
except TypeError:
|
||||
mod_val = mods.value
|
||||
seq = QKeySequence(mod_val | key)
|
||||
shortcut_str = seq.toString(QKeySequence.NativeText)
|
||||
if shortcut_str:
|
||||
display_text += f"\t{shortcut_str}"
|
||||
|
||||
icon = QIcon.fromTheme(item.get("icon", ""))
|
||||
|
||||
if "submenu" in item:
|
||||
submenu = target_menu.addMenu(icon, item["text"])
|
||||
build_actions(submenu, item["submenu"])
|
||||
else:
|
||||
action = target_menu.addAction(icon, display_text)
|
||||
slot = item.get("slot")
|
||||
if action_name:
|
||||
action.triggered.connect(
|
||||
lambda checked=False, name=action_name:
|
||||
self._execute_action(name))
|
||||
elif slot:
|
||||
action.triggered.connect(slot)
|
||||
|
||||
if item.get("checkable"):
|
||||
action.setCheckable(True)
|
||||
action.setChecked(item.get("checked", False))
|
||||
|
||||
build_actions(menu, menu_items)
|
||||
self._build_menu_from_data(menu, menu_structure)
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "bagheeraview"
|
||||
version = "0.9.11"
|
||||
version = "0.9.12"
|
||||
authors = [
|
||||
{ name = "Ignacio Serantes" }
|
||||
]
|
||||
|
||||
@@ -27,6 +27,7 @@ from constants import (
|
||||
FACES_MENU_MAX_ITEMS_DEFAULT, MEDIAPIPE_FACE_MODEL_PATH, MEDIAPIPE_FACE_MODEL_URL,
|
||||
AVAILABLE_PET_ENGINES, DEFAULT_BODY_BOX_COLOR,
|
||||
MEDIAPIPE_OBJECT_MODEL_PATH, MEDIAPIPE_OBJECT_MODEL_URL,
|
||||
HAVE_BAGHEERASEARCH_LIB,
|
||||
SCANNER_SETTINGS_DEFAULTS, SEARCH_CMD, TAGS_MENU_MAX_ITEMS_DEFAULT,
|
||||
THUMBNAILS_FILENAME_LINES_DEFAULT,
|
||||
THUMBNAILS_REFRESH_INTERVAL_DEFAULT, THUMBNAILS_BG_COLOR_DEFAULT,
|
||||
@@ -35,7 +36,7 @@ from constants import (
|
||||
THUMBNAILS_TOOLTIP_FG_COLOR_DEFAULT, THUMBNAILS_FILENAME_FONT_SIZE_DEFAULT,
|
||||
THUMBNAILS_TAGS_LINES_DEFAULT, THUMBNAILS_TAGS_FONT_SIZE_DEFAULT,
|
||||
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT, VIEWER_WHEEL_SPEED_DEFAULT,
|
||||
UITexts, save_app_config, HAVE_BAGHEERASEARCH_LIB
|
||||
UITexts, save_app_config
|
||||
)
|
||||
|
||||
|
||||
|
||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="bagheeraview",
|
||||
version="0.9.11",
|
||||
version="0.9.12",
|
||||
author="Ignacio Serantes",
|
||||
description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind",
|
||||
long_description="A fast image viewer built with PySide6, featuring search and "
|
||||
|
||||
@@ -398,7 +398,7 @@ class TagEditWidget(QWidget):
|
||||
if not full_path:
|
||||
return ""
|
||||
words = full_path.replace('/', ' ').split()
|
||||
search_terms = [f"tags:{word}" for word in words if word]
|
||||
search_terms = [f"tags:'{word}'" for word in words if word]
|
||||
return " ".join(search_terms)
|
||||
|
||||
def _get_current_query_text(self):
|
||||
|
||||
Reference in New Issue
Block a user