This commit is contained in:
Ignacio Serantes
2026-03-25 22:02:13 +01:00
parent 56ef674d4a
commit dfddfd17b3
10 changed files with 430 additions and 53 deletions

View File

@@ -14,7 +14,7 @@ Classes:
MainWindow: The main application window containing the thumbnail grid and docks.
"""
__appname__ = "BagheeraView"
__version__ = "0.9.12"
__version__ = "0.9.13"
__author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net"
__license__ = "LGPL"
@@ -55,7 +55,7 @@ from pathlib import Path
from constants import (
APP_CONFIG, CONFIG_PATH, CURRENT_LANGUAGE, DEFAULT_GLOBAL_SHORTCUTS,
DEFAULT_VIEWER_SHORTCUTS, GLOBAL_ACTIONS, HISTORY_PATH, ICON_THEME,
ICON_THEME_FALLBACK, IMAGE_MIME_TYPES, LAYOUTS_DIR, PROG_AUTHOR,
ICON_THEME_FALLBACK, IMAGE_MIME_TYPES, LAYOUTS_DIR, FAVORITES_PATH, PROG_AUTHOR,
PROG_NAME, PROG_VERSION, RATING_XATTR_NAME, SCANNER_GENERATE_SIZES,
SCANNER_SETTINGS_DEFAULTS, SUPPORTED_LANGUAGES, TAGS_MENU_MAX_ITEMS_DEFAULT,
THUMBNAILS_BG_COLOR_DEFAULT, THUMBNAILS_DEFAULT_SIZE, VIEWER_ACTIONS,
@@ -74,7 +74,8 @@ from imageviewer import ImageViewer
from propertiesdialog import PropertiesDialog
from widgets import (
CircularProgressBar,
TagEditWidget, LayoutsWidget, HistoryWidget, RatingWidget, CommentWidget
TagEditWidget, LayoutsWidget, HistoryWidget, RatingWidget, CommentWidget,
FavoritesWidget
)
from metadatamanager import XattrManager
@@ -255,16 +256,13 @@ class ShortcutHelpDialog(QDialog):
new_mods = new_key_combo.keyboardModifiers()
new_key_tuple = (int(new_key), new_mods)
# Check for conflicts in the same scope
if new_key_tuple in source_dict and new_key_tuple != original_key_combo:
# Handle different value structures
val = source_dict[new_key_tuple]
# Global: (action, ignore, desc, category), Viewer: (action, desc)
if len(val) == 4:
conflict_desc = val[2]
else:
conflict_desc = val[1]
# Check for conflicts globally
conflict_desc = self.main_win.shortcut_controller.check_conflict(
new_key, new_mods)
is_same = (new_key_tuple == original_key_combo)
if conflict_desc and not is_same:
QMessageBox.warning(self, UITexts.SHORTCUT_CONFLICT_TITLE,
UITexts.SHORTCUT_CONFLICT_TEXT.format(
new_sequence.toString(QKeySequence.NativeText),
@@ -300,6 +298,7 @@ class AppShortcutController(QObject):
self.main_win = main_win
self._actions = self._get_actions()
self._shortcuts = {}
self._favorite_shortcuts = {}
self.action_to_shortcut = {}
self._register_shortcuts()
@@ -317,6 +316,44 @@ class AppShortcutController(QObject):
key_tuple = (k, Qt.KeyboardModifiers(m))
self._shortcuts[key_tuple] = (act, ignore, desc, cat)
self.action_to_shortcut[act] = key_tuple
self.refresh_favorite_shortcuts()
def refresh_favorite_shortcuts(self):
"""Loads dynamic shortcuts assigned to favorite queries."""
self._favorite_shortcuts.clear()
if not os.path.exists(FAVORITES_PATH):
return
try:
with open(FAVORITES_PATH, 'r', encoding='utf-8') as f:
favorites = json.load(f)
for fav in favorites:
sc_str = fav.get('shortcut', '')
if sc_str:
seq = QKeySequence(sc_str)
if not seq.isEmpty():
self._favorite_shortcuts[
(seq[0].key(), seq[0].keyboardModifiers())
] = fav.get('query')
except (json.JSONDecodeError, OSError):
pass
def check_conflict(self, key, mods):
"""Checks if a shortcut is already assigned and returns its description."""
key_tuple = (int(key), mods)
# Global
if key_tuple in self._shortcuts:
return self._shortcuts[key_tuple][2]
# Viewer
if key_tuple in self.main_win.viewer_shortcuts:
return self.main_win.viewer_shortcuts[key_tuple][1]
# Favorites
if key_tuple in self._favorite_shortcuts:
return f"{UITexts.FAVORITES_TAB}: {self._favorite_shortcuts[key_tuple]}"
return None
def _get_actions(self):
"""Returns a dictionary mapping action strings to callable functions."""
@@ -375,6 +412,12 @@ class AppShortcutController(QObject):
if is_typing:
return False
# 1. Check Favorite Shortcuts FIRST (Priority Override)
if (key, mods) in self._favorite_shortcuts:
query = self._favorite_shortcuts[(key, mods)]
self.main_win.process_term(query)
return True
# Check if we have a handler for this combination
if (key, mods) in self._shortcuts:
action_name, ignore_if_typing, _, _ = self._shortcuts[(key, mods)]
@@ -1172,16 +1215,26 @@ class MainWindow(QMainWindow):
self.tags_tabs.addTab(self.filter_widget, UITexts.TAG_FILTER_TAB)
# Tab 4: Layouts
# Tab 4: Favorites
self.favorites_tab = FavoritesWidget(self)
self.tags_tabs.addTab(self.favorites_tab, UITexts.FAVORITES_TAB)
# Tab 5: Layouts
self.is_xcb = QApplication.platformName() == "xcb"
if self.is_xcb:
self.layouts_tab = LayoutsWidget(self)
self.tags_tabs.addTab(self.layouts_tab, UITexts.LAYOUTS_TAB)
# Tab 5: History
# Tab 6: History
self.history_tab = HistoryWidget(self)
self.tags_tabs.addTab(self.history_tab, UITexts.HISTORY_TAB)
# Initialize the shortcut controller
self.shortcut_controller = AppShortcutController(self)
self.favorites_tab.favorites_changed.connect(
self.shortcut_controller.refresh_favorite_shortcuts)
self.main_dock.setWidget(self.tags_tabs)
self.addDockWidget(Qt.RightDockWidgetArea, self.main_dock)
@@ -1233,6 +1286,11 @@ class MainWindow(QMainWindow):
self.load_config()
self.load_full_history()
# Initialize the shortcut controller (after config is loaded)
self.shortcut_controller = AppShortcutController(self)
self.favorites_tab.favorites_changed.connect(
self.shortcut_controller.refresh_favorite_shortcuts)
self._apply_global_stylesheet()
# Set the initial thumbnail generation tier based on the loaded config size
self._current_thumb_tier = self._get_tier_for_size(self.current_thumb_size)
@@ -1558,15 +1616,23 @@ class MainWindow(QMainWindow):
# Actions to show different tabs in the dock
show_tags_action = menu.addAction(QIcon.fromTheme("document-properties"),
UITexts.MENU_SHOW_TAGS)
show_tags_action.triggered.connect(lambda: self.open_sidebar_tab(0))
show_tags_action.triggered.connect(
lambda: self.open_sidebar_tab(self.tags_tabs.indexOf(self.tag_edit_widget)))
show_info_action = menu.addAction(QIcon.fromTheme("dialog-information"),
UITexts.MENU_SHOW_INFO)
show_info_action.triggered.connect(lambda: self.open_sidebar_tab(1))
show_info_action.triggered.connect(
lambda: self.open_sidebar_tab(self.tags_tabs.indexOf(self.info_widget)))
show_favorites_action = menu.addAction(QIcon.fromTheme("bookmarks"),
UITexts.MENU_SHOW_FAVORITES)
f_idx = self.tags_tabs.indexOf(self.favorites_tab)
show_favorites_action.triggered.connect(lambda: self.open_sidebar_tab(f_idx))
show_filter_action = menu.addAction(QIcon.fromTheme("view-filter"),
UITexts.MENU_SHOW_FILTER)
show_filter_action.triggered.connect(lambda: self.open_sidebar_tab(2))
show_filter_action.triggered.connect(
lambda: self.open_sidebar_tab(self.tags_tabs.indexOf(self.filter_widget)))
if self.is_xcb:
show_layouts_action = menu.addAction(QIcon.fromTheme("view-grid"),
@@ -2911,6 +2977,8 @@ class MainWindow(QMainWindow):
self.update_tag_list()
elif widget == self.info_widget:
self.update_info_widget()
elif widget == self.favorites_tab:
self.favorites_tab.refresh_list()
def update_tag_edit_widget(self):
"""Updates the tag editor widget with data from the currently selected files."""
@@ -3852,7 +3920,7 @@ class MainWindow(QMainWindow):
self.proxy_model.data(index_at_pos, ITEM_TYPE_ROLE) == 'header':
group_name = self.proxy_model.data(index_at_pos, GROUP_NAME_ROLE)
if group_name:
action_toggle = menu.addAction("Collapse/Expand Group")
action_toggle = menu.addAction(UITexts.COLLAPSE_EXPAND_GROUP)
action_toggle.triggered.connect(
lambda: self.toggle_group_collapse(group_name))
menu.exec(self.thumbnail_view.mapToGlobal(pos))
@@ -4006,7 +4074,7 @@ class MainWindow(QMainWindow):
self.on_high_res_generation_finished)
self.thumbnail_generator.progress.connect(
lambda p, t: self.status_lbl.setText(
f"Regenerating thumbnail: {p}/{t}")
UITexts.THUMBNAILS_REGENERATE_PROGRESS.format(p, t))
)
self.thumbnail_generator.start()
@@ -4397,9 +4465,7 @@ def main():
path = path[6:]
win = MainWindow(cache, args, thread_pool_manager)
shortcut_controller = AppShortcutController(win)
win.shortcut_controller = shortcut_controller
app.installEventFilter(shortcut_controller)
app.installEventFilter(win.shortcut_controller)
sys.exit(app.exec())

View File

@@ -63,6 +63,9 @@ 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.13 -
· Añadida la opción de favoritos.
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.

View File

@@ -29,7 +29,7 @@ if FORCE_X11:
# --- CONFIGURATION ---
PROG_NAME = "Bagheera Image Viewer"
PROG_ID = "bagheeraview"
PROG_VERSION = "0.9.12"
PROG_VERSION = "0.9.13"
PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS ---
@@ -49,6 +49,8 @@ CACHE_PATH = os.path.join(CONFIG_DIR, "thumbnails_lmdb")
HISTORY_FILE = "history.json"
HISTORY_PATH = os.path.join(CONFIG_DIR, HISTORY_FILE)
LAYOUTS_DIR = os.path.join(CONFIG_DIR, "layouts") # Layouts saving directory
FAVORITES_FILE = "favorites.json"
FAVORITES_PATH = os.path.join(CONFIG_DIR, FAVORITES_FILE)
def save_app_config():
@@ -463,6 +465,17 @@ _UI_TEXTS = {
"MENU_CLEAN_CACHE": "Clean up invalid cache entries",
"MENU_SHOW_TAGS": "Show Tags",
"MENU_SHOW_INFO": "Show Information",
"MENU_SHOW_FAVORITES": "Show Favorites",
"FAVORITES_TAB": "Favorites",
"FAVORITES_SEARCH_PLACEHOLDER": "Search favorites...",
"FAVORITES_TABLE_HEADER": ["Comment", "Query", "Shortcut"],
"ADD_FAVORITE_TOOLTIP": "Add current search to favorites",
"EDIT_COMMENT_TITLE": "Edit Comment",
"EDIT_COMMENT_TEXT": "Comment for '{}':",
"EDIT_SHORTCUT_TITLE": "Assign Shortcut",
"EDIT_SHORTCUT_TEXT": "Press keys for '{}':",
"MOVE_UP": "Move Up",
"MOVE_DOWN": "Move Down",
"MENU_SHOW_FILTER": "Show Filter",
"MENU_SHOW_LAYOUTS": "Show Layouts",
"MENU_SHOW_HISTORY": "Show History",
@@ -606,12 +619,26 @@ _UI_TEXTS = {
"MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Failed to download the MediaPipe model: {}",
"MENU_FILMSTRIP_POSITION": "Filmstrip Position",
"FILMSTRIP_BOTTOM": "Bottom",
"VIEWER_MENU_COMPARE": "Comparison Mode",
"FILMSTRIP_LEFT": "Left",
"FILMSTRIP_TOP": "Top",
"FILMSTRIP_RIGHT": "Right",
"FILMSTRIP_POS_CHANGED_INFO": "The new filmstrip position will be applied to "
"newly opened viewers.",
"MENU_SHOW_SHORTCUTS": "Configure Keyboard Shortcuts...",
"VIEWER_MENU_MANIPULATE": "Manipulate",
"VIEWER_MENU_ZOOM": "Zoom",
"SAVE_CROP_TITLE": "Save Cropped Image",
"COMPARE_LINKED": " [Linked]",
"COMPARE_UNLINKED": " [Unlinked]",
"CROP_INDICATOR": " [CROP]",
"OPEN_WITH_OTHER": "Open with other application...",
"COLLAPSE_EXPAND_GROUP": "Collapse/Expand Group",
"MENU_TOGGLE_MAIN_WINDOW": "Show/Hide Main Window",
"LOADING_DATA": "Loading data...",
"SETTINGS_PLACEHOLDER_TAGS": "tag1, tag2, tag3/subtag",
"THUMBNAILS_GENERATE_PROGRESS": "Generating {}px thumbnails: {}/{}",
"THUMBNAILS_REGENERATE_PROGRESS": "Regenerating thumbnail: {}/{}",
"SHORTCUTS_TITLE": "Keyboard Shortcuts",
"SHORTCUTS_ACTION": "Action",
"SHORTCUTS_KEY": "Shortcut",
@@ -620,6 +647,7 @@ _UI_TEXTS = {
"SHORTCUT_EDIT_LABEL": "Enter new shortcut for '{}'",
"SHORTCUT_CONFLICT_TITLE": "Shortcut Conflict",
"SHORTCUT_CONFLICT_TEXT": "The shortcut '{}' is already assigned to '{}'.",
"SHORTCUT_OVERRIDE_QUESTION": "Do you want to override it?",
"SHORTCUT_SEARCH_PLACEHOLDER": "Search shortcuts...",
"CACHE_CLEANING": "Cleaning cache...",
"CACHE_CLEANED": "Cache cleaned. Removed {} invalid entries.",
@@ -647,7 +675,7 @@ _UI_TEXTS = {
"RENAME_ERROR_EXISTS": "File '{}' already exists.",
"FILE_RENAMED": "File renamed to {}",
"ERROR_RENAME": "Could not rename file: {}",
"MAIN_DOCK_TITLE": "Main dock",
"MAIN_DOCK_TITLE": "",
"LAYOUTS_TAB": "Layouts",
"LAYOUTS_TABLE_HEADER": ["Name", "Last Modified"],
"SAVE_LAYOUT_TITLE": "Save Layout",
@@ -901,6 +929,17 @@ _UI_TEXTS = {
"MENU_CLEAN_CACHE": "Limpiar entradas de caché inválidas",
"MENU_SHOW_TAGS": "Mostrar Etiquetas",
"MENU_SHOW_INFO": "Mostrar Información",
"MENU_SHOW_FAVORITES": "Mostrar Favoritos",
"FAVORITES_TAB": "Favoritos",
"FAVORITES_SEARCH_PLACEHOLDER": "Buscar favoritos...",
"FAVORITES_TABLE_HEADER": ["Comentario", "Consulta", "Atajo"],
"ADD_FAVORITE_TOOLTIP": "Añadir búsqueda actual a favoritos",
"EDIT_COMMENT_TITLE": "Editar Comentario",
"EDIT_COMMENT_TEXT": "Comentario para '{}':",
"EDIT_SHORTCUT_TITLE": "Asignar Atajo",
"EDIT_SHORTCUT_TEXT": "Pulsa las teclas para '{}':",
"MOVE_UP": "Subir",
"MOVE_DOWN": "Bajar",
"MENU_SHOW_FILTER": "Mostrar Filtro",
"MENU_SHOW_LAYOUTS": "Mostrar Diseños",
"MENU_SHOW_HISTORY": "Mostrar Historial",
@@ -1056,6 +1095,7 @@ _UI_TEXTS = {
"MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Fallo al descargar el modelo de MediaPipe: "
"{}",
"MENU_VIEWER_SETTINGS": "Opciones del Visor",
"VIEWER_MENU_COMPARE": "Modo Comparación",
"MENU_FILMSTRIP_POSITION": "Posición de la Tira de Imágenes",
"FILMSTRIP_BOTTOM": "Abajo",
"FILMSTRIP_LEFT": "Izquierda",
@@ -1063,6 +1103,17 @@ _UI_TEXTS = {
"FILMSTRIP_RIGHT": "Derecha",
"FILMSTRIP_POS_CHANGED_INFO": "La nueva posición de la tira de imágenes se "
"aplicará a los nuevos visores que se abran.",
"SAVE_CROP_TITLE": "Guardar Imagen Recortada",
"COMPARE_LINKED": " [Vinculado]",
"COMPARE_UNLINKED": " [Desvinculado]",
"CROP_INDICATOR": " [RECORTE]",
"OPEN_WITH_OTHER": "Abrir con otra aplicación...",
"COLLAPSE_EXPAND_GROUP": "Contraer/Expandir Grupo",
"MENU_TOGGLE_MAIN_WINDOW": "Mostrar/Ocultar ventana principal",
"LOADING_DATA": "Cargando datos...",
"SETTINGS_PLACEHOLDER_TAGS": "etiqueta1, etiqueta2, carpeta/etiqueta",
"THUMBNAILS_GENERATE_PROGRESS": "Generando miniaturas de {}px: {}/{}",
"THUMBNAILS_REGENERATE_PROGRESS": "Regenerando miniatura: {}/{}",
"MENU_SHOW_SHORTCUTS": "Configurar Atajos de Teclado...",
"SHORTCUTS_TITLE": "Atajos de Teclado",
"SHORTCUTS_ACTION": "Acción",
@@ -1072,6 +1123,7 @@ _UI_TEXTS = {
"SHORTCUT_EDIT_LABEL": "Nuevo atajo para '{}'",
"SHORTCUT_CONFLICT_TITLE": "Conflicto de Atajos",
"SHORTCUT_CONFLICT_TEXT": "El atajo '{}' ya está asignado a '{}'.",
"SHORTCUT_OVERRIDE_QUESTION": "¿Deseas sobrescribirlo?",
"SHORTCUT_SEARCH_PLACEHOLDER": "Buscar atajos...",
"CACHE_CLEANING": "Limpiando caché...",
"CACHE_CLEANED": "Caché limpiada. Se eliminaron {} entradas inválidas.",
@@ -1349,6 +1401,17 @@ _UI_TEXTS = {
"MENU_CLEAN_CACHE": "Limpar entradas de caché inválidas",
"MENU_SHOW_TAGS": "Amosar Etiquetas",
"MENU_SHOW_INFO": "Amosar Información",
"MENU_SHOW_FAVORITES": "Amosar Favoritos",
"FAVORITES_TAB": "Favoritos",
"FAVORITES_SEARCH_PLACEHOLDER": "Buscar favoritos...",
"FAVORITES_TABLE_HEADER": ["Comentario", "Consulta", "Atallo"],
"ADD_FAVORITE_TOOLTIP": "Engadir busca actual a favoritos",
"EDIT_COMMENT_TITLE": "Editar Comentario",
"EDIT_COMMENT_TEXT": "Comentario para '{}':",
"EDIT_SHORTCUT_TITLE": "Asignar Atallo",
"EDIT_SHORTCUT_TEXT": "Preme as teclas para '{}':",
"MOVE_UP": "Subir",
"MOVE_DOWN": "Baixar",
"MENU_SHOW_FILTER": "Amosar Filtro",
"MENU_SHOW_LAYOUTS": "Amosar Deseños",
"MENU_SHOW_HISTORY": "Amosar Historial",
@@ -1504,6 +1567,7 @@ _UI_TEXTS = {
"MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Fallo ao descargar o modelo de MediaPipe: {}",
"MENU_VIEWER_SETTINGS": "Opcións do Visor",
"MENU_FILMSTRIP_POSITION": "Posición da Tira de Imaxes",
"VIEWER_MENU_COMPARE": "Modo Comparación",
"FILMSTRIP_BOTTOM": "Abaixo",
"FILMSTRIP_LEFT": "Esquerda",
"FILMSTRIP_TOP": "Arriba",
@@ -1511,6 +1575,17 @@ _UI_TEXTS = {
"FILMSTRIP_POS_CHANGED_INFO": "A nova posición da tira de imaxes aplicarase "
"aos novos visores que se abran.",
"MENU_SHOW_SHORTCUTS": "Configurar Atallos de Teclado...",
"COMPARE_LINKED": " [Vencellado]",
"COMPARE_UNLINKED": " [Desvencellado]",
"CROP_INDICATOR": " [RECORTE]",
"OPEN_WITH_OTHER": "Abrir con outra aplicación...",
"COLLAPSE_EXPAND_GROUP": "Contraer/Expandir Grupo",
"MENU_TOGGLE_MAIN_WINDOW": "Amosar/Ocultar xanela principal",
"LOADING_DATA": "Cargando datos...",
"SETTINGS_PLACEHOLDER_TAGS": "etiqueta1, etiqueta2, cartafol/etiqueta",
"THUMBNAILS_GENERATE_PROGRESS": "Xerando miniaturas de {}px: {}/{}",
"THUMBNAILS_REGENERATE_PROGRESS": "Rexerando miniatura: {}/{}",
"SAVE_CROP_TITLE": "Gardar Imaxe Recortada",
"SHORTCUTS_TITLE": "Atallos de Teclado",
"SHORTCUTS_ACTION": "Acción",
"SHORTCUTS_KEY": "Atallo",
@@ -1519,6 +1594,7 @@ _UI_TEXTS = {
"SHORTCUT_EDIT_LABEL": "Novo Atallo para '{}'",
"SHORTCUT_CONFLICT_TITLE": "Conflito de Atallos",
"SHORTCUT_CONFLICT_TEXT": "O atallo '{}' xa está asignado a '{}'.",
"SHORTCUT_OVERRIDE_QUESTION": "Desexas sobrescribilo?",
"SHORTCUT_SEARCH_PLACEHOLDER": "Buscar atallos...",
"CACHE_CLEANING": "Limpando caché...",
"CACHE_CLEANED": "Caché limpada. Elimináronse {} entradas inválidas.",

View File

@@ -1279,6 +1279,7 @@ class ThumbnailGenerator(QThread):
nonlocal processed_count
processed_count += 1
if processed_count % 5 == 0 or processed_count == total:
# Signal remains int, format in receiver
self.progress.emit(processed_count, total)
# Use a direct connection or queued connection depending on context,

View File

@@ -2184,11 +2184,12 @@ class ImageViewer(QWidget):
self.populate_filmstrip()
self.update_status_bar(index=new_index)
def _on_movie_frame(self):
"""Updates the view with the current frame from the movie."""
if self.movie and self.movie.isValid():
self.controller.pixmap_original = self.movie.currentPixmap()
self.update_view(resize_win=False)
def _on_movie_frame_for_pane(self, pane):
"""Updates the view with the current frame from the movie for a specific
pane."""
if pane.movie and pane.movie.isValid():
pane.controller.pixmap_original = pane.movie.currentPixmap()
pane.update_view(resize_win=False)
def toggle_animation_pause(self):
"""Pauses or resumes the current animation."""
@@ -2335,7 +2336,8 @@ class ImageViewer(QWidget):
if self.active_pane.crop_mode:
self.active_pane.canvas.setCursor(Qt.CrossCursor)
self.sb_info_label.setText(f"{self.sb_info_label.text()} [CROP]")
self.sb_info_label.setText(
f"{self.sb_info_label.text()}{UITexts.CROP_INDICATOR}")
else:
self.active_pane.canvas.setCursor(Qt.ArrowCursor)
self.update_status_bar()
@@ -2432,7 +2434,8 @@ class ImageViewer(QWidget):
info_text = f"{w} x {h} px | {zoom}%"
if len(self.panes) > 1:
info_text += " [Linked]" if self.panes_linked else " [Unlinked]"
info_text += UITexts.COMPARE_LINKED \
if self.panes_linked else UITexts.COMPARE_UNLINKED
self.sb_info_label.setText(info_text)
@@ -2891,7 +2894,7 @@ class ImageViewer(QWidget):
"action": "fullscreen", "icon": "view-fullscreen"
if not self.isFullScreen() else "view-restore"},
"separator",
{"text": "Show/hide main window",
{"text": UITexts.MENU_TOGGLE_MAIN_WINDOW,
"action": "toggle_visibility", "icon": "view-restore"},
"separator",
{"text": UITexts.CONTEXT_MENU_PROPERTIES,
@@ -3288,13 +3291,13 @@ class ImageViewer(QWidget):
Args:
event (QCloseEvent): The close event.
"""
if self.movie:
self.movie.stop()
for pane in self.panes:
pane.cleanup()
self.slideshow_manager.stop()
if self.filmstrip_loader and self.filmstrip_loader.isRunning():
self.filmstrip_loader.stop()
self.uninhibit_screensaver()
self.controller.cleanup()
# If we close the last viewer and the main window is hidden, quit.
if self.main_win and not self.main_win.isVisible():
# Check how many viewers are left

View File

@@ -267,7 +267,7 @@ class PropertiesDialog(QDialog):
if exif_data is None:
# Loading state
self.exif_table.setRowCount(1)
item = QTableWidgetItem("Loading data...")
item = QTableWidgetItem(UITexts.LOADING_DATA)
item.setFlags(Qt.ItemIsEnabled)
self.exif_table.setItem(0, 0, item)
self.exif_table.blockSignals(False)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "bagheeraview"
version = "0.9.12"
version = "0.9.13"
authors = [
{ name = "Ignacio Serantes" }
]

View File

@@ -349,7 +349,7 @@ class SettingsDialog(QDialog):
faces_layout = QVBoxLayout(faces_tab)
# Faces Header
faces_header = QLabel("Faces")
faces_header = QLabel(UITexts.TYPE_FACE)
faces_header.setFont(QFont("Sans", 10, QFont.Bold))
faces_layout.addWidget(faces_header)
@@ -357,7 +357,7 @@ class SettingsDialog(QDialog):
person_tags_layout = QHBoxLayout()
person_tags_label = QLabel(UITexts.SETTINGS_PERSON_TAGS_LABEL)
self.person_tags_edit = QLineEdit()
self.person_tags_edit.setPlaceholderText("tag1, tag2, tag3/subtag")
self.person_tags_edit.setPlaceholderText(UITexts.SETTINGS_PLACEHOLDER_TAGS)
self.person_tags_edit.setClearButtonEnabled(True)
person_tags_layout.addWidget(person_tags_label)
person_tags_layout.addWidget(self.person_tags_edit)
@@ -411,14 +411,14 @@ class SettingsDialog(QDialog):
# --- Pets Section ---
faces_layout.addSpacing(10)
pets_header = QLabel("Pets")
pets_header = QLabel(UITexts.TYPE_PET)
pets_header.setFont(QFont("Sans", 10, QFont.Bold))
faces_layout.addWidget(pets_header)
pet_tags_layout = QHBoxLayout()
pet_tags_label = QLabel(UITexts.SETTINGS_PET_TAGS_LABEL)
self.pet_tags_edit = QLineEdit()
self.pet_tags_edit.setPlaceholderText("tag1, tag2, tag3/subtag")
self.pet_tags_edit.setPlaceholderText(UITexts.SETTINGS_PLACEHOLDER_TAGS)
self.pet_tags_edit.setClearButtonEnabled(True)
pet_tags_layout.addWidget(pet_tags_label)
pet_tags_layout.addWidget(self.pet_tags_edit)
@@ -467,14 +467,14 @@ class SettingsDialog(QDialog):
# --- Body Section ---
faces_layout.addSpacing(10)
body_header = QLabel("Body")
body_header = QLabel(UITexts.TYPE_BODY)
body_header.setFont(QFont("Sans", 10, QFont.Bold))
faces_layout.addWidget(body_header)
body_tags_layout = QHBoxLayout()
body_tags_label = QLabel(UITexts.SETTINGS_BODY_TAGS_LABEL)
self.body_tags_edit = QLineEdit()
self.body_tags_edit.setPlaceholderText("tag1, tag2, tag3/subtag")
self.body_tags_edit.setPlaceholderText(UITexts.SETTINGS_PLACEHOLDER_TAGS)
self.body_tags_edit.setClearButtonEnabled(True)
body_tags_layout.addWidget(body_tags_label)
body_tags_layout.addWidget(self.body_tags_edit)
@@ -514,14 +514,14 @@ class SettingsDialog(QDialog):
# --- Object Section ---
faces_layout.addSpacing(10)
object_header = QLabel("Object")
object_header = QLabel(UITexts.TYPE_OBJECT)
object_header.setFont(QFont("Sans", 10, QFont.Bold))
faces_layout.addWidget(object_header)
object_tags_layout = QHBoxLayout()
object_tags_label = QLabel(UITexts.SETTINGS_OBJECT_TAGS_LABEL)
self.object_tags_edit = QLineEdit()
self.object_tags_edit.setPlaceholderText("tag1, tag2, tag3/subtag")
self.object_tags_edit.setPlaceholderText(UITexts.SETTINGS_PLACEHOLDER_TAGS)
self.object_tags_edit.setClearButtonEnabled(True)
object_tags_layout.addWidget(object_tags_label)
object_tags_layout.addWidget(self.object_tags_edit)
@@ -560,14 +560,14 @@ class SettingsDialog(QDialog):
# --- Landmark Section ---
faces_layout.addSpacing(10)
landmark_header = QLabel("Landmark")
landmark_header = QLabel(UITexts.TYPE_LANDMARK)
landmark_header.setFont(QFont("Sans", 10, QFont.Bold))
faces_layout.addWidget(landmark_header)
landmark_tags_layout = QHBoxLayout()
landmark_tags_label = QLabel(UITexts.SETTINGS_LANDMARK_TAGS_LABEL)
self.landmark_tags_edit = QLineEdit()
self.landmark_tags_edit.setPlaceholderText("tag1, tag2, tag3/subtag")
self.landmark_tags_edit.setPlaceholderText(UITexts.SETTINGS_PLACEHOLDER_TAGS)
self.landmark_tags_edit.setClearButtonEnabled(True)
landmark_tags_layout.addWidget(landmark_tags_label)
landmark_tags_layout.addWidget(self.landmark_tags_edit)

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="bagheeraview",
version="0.9.12",
version="0.9.13",
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 "

View File

@@ -12,6 +12,7 @@ including:
import os
import glob
import shutil
import json
import lmdb
from datetime import datetime
from collections import deque
@@ -20,11 +21,11 @@ from PySide6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
QMessageBox, QSizePolicy, QInputDialog, QTableWidget, QTableWidgetItem,
QMenu, QHeaderView, QAbstractItemView, QTreeView, QLabel, QTextEdit,
QComboBox, QCompleter, QToolBar
QComboBox, QCompleter, QToolBar, QDialog
)
from PySide6.QtGui import (
QIcon, QStandardItemModel, QStandardItem, QColor, QPainter, QPen,
QPalette, QAction,
QPalette, QAction, QKeySequence
)
from PySide6.QtCore import (
Signal, QSortFilterProxyModel, Slot, QStringListModel, Qt
@@ -33,7 +34,7 @@ from PySide6.QtCore import (
from metadatamanager import XattrManager
from constants import (
LAYOUTS_DIR, RATING_XATTR_NAME, XATTR_COMMENT_NAME, XATTR_NAME, UITexts,
FACES_MENU_MAX_ITEMS_DEFAULT, APP_CONFIG
FACES_MENU_MAX_ITEMS_DEFAULT, APP_CONFIG, FAVORITES_PATH
)
@@ -513,7 +514,7 @@ class TagEditWidget(QWidget):
self.refresh_ui()
self.tags_updated.emit(updated_files_tags)
except Exception as e:
QMessageBox.critical(self, "Error", str(e))
QMessageBox.critical(self, UITexts.ERROR, str(e))
finally:
QApplication.restoreOverrideCursor()
@@ -898,6 +899,233 @@ class HistoryWidget(QWidget):
self.refresh_list()
class FavoritesWidget(QWidget):
"""A widget for managing favorite search queries."""
favorites_changed = Signal()
def __init__(self, main_win):
super().__init__()
self.main_win = main_win
layout = QVBoxLayout(self)
self.search_bar = QLineEdit()
self.search_bar.setPlaceholderText(UITexts.FAVORITES_SEARCH_PLACEHOLDER)
self.search_bar.setClearButtonEnabled(True)
self.search_bar.textChanged.connect(self.filter_favorites)
layout.addWidget(self.search_bar)
self.table = QTableWidget()
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(UITexts.FAVORITES_TABLE_HEADER)
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
self.table.horizontalHeader().setStretchLastSection(True)
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table.verticalHeader().setVisible(False)
self.table.doubleClicked.connect(self.load_selected)
layout.addWidget(self.table)
toolbar = QToolBar()
layout.addWidget(toolbar)
load_action = QAction(QIcon.fromTheme("system-run"), UITexts.LOAD, self)
load_action.triggered.connect(self.load_selected)
toolbar.addAction(load_action)
add_action = QAction(QIcon.fromTheme("list-add"), UITexts.CREATE, self)
add_action.setToolTip(UITexts.ADD_FAVORITE_TOOLTIP)
add_action.triggered.connect(self.add_favorite)
toolbar.addAction(add_action)
edit_action = QAction(QIcon.fromTheme("edit-rename"), UITexts.RENAME, self)
edit_action.triggered.connect(self.edit_comment)
toolbar.addAction(edit_action)
shortcut_action = QAction(
QIcon.fromTheme("preferences-desktop-keyboard-shortcuts"),
UITexts.SHORTCUTS_KEY, self)
shortcut_action.triggered.connect(self.edit_shortcut)
toolbar.addAction(shortcut_action)
delete_action = QAction(QIcon.fromTheme("edit-delete"), UITexts.DELETE, self)
delete_action.triggered.connect(self.delete_favorite)
toolbar.addAction(delete_action)
toolbar.addSeparator()
up_action = QAction(QIcon.fromTheme("go-up"), UITexts.MOVE_UP, self)
up_action.triggered.connect(self.move_up)
toolbar.addAction(up_action)
down_action = QAction(QIcon.fromTheme("go-down"), UITexts.MOVE_DOWN, self)
down_action.triggered.connect(self.move_down)
toolbar.addAction(down_action)
self.refresh_list()
def resizeEvent(self, event):
width = self.table.viewport().width()
self.table.setColumnWidth(0, int(width * 0.60))
self.table.setColumnWidth(2, int(width * 0.15))
super().resizeEvent(event)
def refresh_list(self):
self.table.setRowCount(0)
if not os.path.exists(FAVORITES_PATH):
return
try:
with open(FAVORITES_PATH, 'r', encoding='utf-8') as f:
favorites = json.load(f)
except (json.JSONDecodeError, OSError):
favorites = []
self.table.setRowCount(len(favorites))
for i, fav in enumerate(favorites):
query = fav.get('query', '')
comment = fav.get('comment', '')
shortcut = fav.get('shortcut', '')
self.table.setItem(i, 0, QTableWidgetItem(comment))
self.table.setItem(i, 1, QTableWidgetItem(query))
self.table.setItem(i, 2, QTableWidgetItem(shortcut))
def filter_favorites(self, text):
search_text = text.lower()
for row in range(self.table.rowCount()):
item = self.table.item(row, 0)
if item:
self.table.setRowHidden(row, search_text not in item.text().lower())
def save_favorites(self):
favorites = []
for i in range(self.table.rowCount()):
item_comment = self.table.item(i, 0)
item_query = self.table.item(i, 1)
item_shortcut = self.table.item(i, 2)
comment = item_comment.text() if item_comment else ""
query = item_query.text() if item_query else ""
shortcut = item_shortcut.text() if item_shortcut else ""
favorites.append({'query': query, 'comment': comment, 'shortcut': shortcut})
try:
with open(FAVORITES_PATH, 'w', encoding='utf-8') as f:
json.dump(favorites, f, indent=4)
self.favorites_changed.emit()
except OSError:
pass
def load_selected(self):
row = self.table.currentRow()
if row >= 0:
query = self.table.item(row, 1).text()
self.main_win.process_term(query)
def add_favorite(self):
query = self.main_win.search_input.currentText().strip()
if not query:
return
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(""))
self.table.setItem(row, 1, QTableWidgetItem(query))
self.table.setItem(row, 2, QTableWidgetItem(""))
self.table.setCurrentCell(row, 0)
self.save_favorites()
def edit_comment(self):
row = self.table.currentRow()
if row < 0:
return
comment_item = self.table.item(row, 0)
query = self.table.item(row, 1).text()
old_comment = comment_item.text() if comment_item else ""
new_comment, ok = QInputDialog.getText(
self, UITexts.EDIT_COMMENT_TITLE,
UITexts.EDIT_COMMENT_TEXT.format(query),
QLineEdit.Normal, old_comment)
if ok:
self.table.item(row, 0).setText(new_comment)
self.save_favorites()
def edit_shortcut(self):
row = self.table.currentRow()
if row < 0:
return
query = self.table.item(row, 1).text()
current_sc = self.table.item(row, 2).text()
dialog = QDialog(self)
dialog.setWindowTitle(UITexts.EDIT_SHORTCUT_TITLE)
dlg_layout = QVBoxLayout(dialog)
dlg_layout.addWidget(QLabel(UITexts.EDIT_SHORTCUT_TEXT.format(query)))
from PySide6.QtWidgets import QKeySequenceEdit, QDialogButtonBox
key_edit = QKeySequenceEdit(QKeySequence(current_sc))
dlg_layout.addWidget(key_edit)
buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
buttons.button(QDialogButtonBox.Reset).clicked.connect(
lambda: key_edit.setKeySequence(QKeySequence()))
dlg_layout.addWidget(buttons)
if dialog.exec() == QDialog.Accepted:
new_sequence = key_edit.keySequence()
new_sc_str = new_sequence.toString(QKeySequence.NativeText)
if not new_sequence.isEmpty():
new_key_combo = new_sequence[0]
new_key = new_key_combo.key()
new_mods = new_key_combo.keyboardModifiers()
conflict_desc = self.main_win.shortcut_controller.check_conflict(
new_key, new_mods)
if conflict_desc and new_sc_str != current_sc:
res = QMessageBox.question(self, UITexts.SHORTCUT_CONFLICT_TITLE,
UITexts.SHORTCUT_CONFLICT_TEXT.format(
new_sc_str, conflict_desc) +
"\n\n" +
UITexts.SHORTCUT_OVERRIDE_QUESTION,
QMessageBox.Yes | QMessageBox.No)
if res == QMessageBox.No:
return
self.table.item(row, 2).setText(new_sc_str)
self.save_favorites()
def delete_favorite(self):
row = self.table.currentRow()
if row >= 0:
self.table.removeRow(row)
self.save_favorites()
def move_up(self):
row = self.table.currentRow()
if row > 0:
self._swap_rows(row, row - 1)
self.table.setCurrentCell(row - 1, 0)
self.save_favorites()
def move_down(self):
row = self.table.currentRow()
if row >= 0 and row < self.table.rowCount() - 1:
self._swap_rows(row, row + 1)
self.table.setCurrentCell(row + 1, 0)
self.save_favorites()
def _swap_rows(self, row1, row2):
for col in range(self.table.columnCount()):
item1 = self.table.takeItem(row1, col)
item2 = self.table.takeItem(row2, col)
self.table.setItem(row1, col, item2)
self.table.setItem(row2, col, item1)
class RatingStar(QLabel):
"""An individual star label for the rating widget."""
# Emits the star index (1-5)