From dfddfd17b38a8cd44b97d9da24d909e3f3e33c8d Mon Sep 17 00:00:00 2001 From: Ignacio Serantes Date: Wed, 25 Mar 2026 22:02:13 +0100 Subject: [PATCH] v0.9.13 --- bagheeraview.py | 110 ++++++++++++++++----- changelog.txt | 3 + constants.py | 80 ++++++++++++++- imagescanner.py | 1 + imageviewer.py | 27 ++--- propertiesdialog.py | 2 +- pyproject.toml | 2 +- settings.py | 20 ++-- setup.py | 2 +- widgets.py | 236 +++++++++++++++++++++++++++++++++++++++++++- 10 files changed, 430 insertions(+), 53 deletions(-) diff --git a/bagheeraview.py b/bagheeraview.py index dc6199c..5cc6910 100755 --- a/bagheeraview.py +++ b/bagheeraview.py @@ -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()) diff --git a/changelog.txt b/changelog.txt index 24f90e4..e73edf1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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. diff --git a/constants.py b/constants.py index b2675cc..8659ab8 100644 --- a/constants.py +++ b/constants.py @@ -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.", diff --git a/imagescanner.py b/imagescanner.py index 0739b9c..b183868 100644 --- a/imagescanner.py +++ b/imagescanner.py @@ -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, diff --git a/imageviewer.py b/imageviewer.py index a0af76e..1925f84 100644 --- a/imageviewer.py +++ b/imageviewer.py @@ -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) @@ -2841,7 +2844,7 @@ class ImageViewer(QWidget): "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}, @@ -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 diff --git a/propertiesdialog.py b/propertiesdialog.py index a134c43..0e0f6c4 100644 --- a/propertiesdialog.py +++ b/propertiesdialog.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index e0dc3e1..8e4f91c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bagheeraview" -version = "0.9.12" +version = "0.9.13" authors = [ { name = "Ignacio Serantes" } ] diff --git a/settings.py b/settings.py index f1ca725..ab71d76 100644 --- a/settings.py +++ b/settings.py @@ -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) diff --git a/setup.py b/setup.py index bb62ef0..da0df5f 100644 --- a/setup.py +++ b/setup.py @@ -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 " diff --git a/widgets.py b/widgets.py index 3ac8b42..2011cb0 100644 --- a/widgets.py +++ b/widgets.py @@ -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)