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. MainWindow: The main application window containing the thumbnail grid and docks.
""" """
__appname__ = "BagheeraView" __appname__ = "BagheeraView"
__version__ = "0.9.12" __version__ = "0.9.13"
__author__ = "Ignacio Serantes" __author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net" __email__ = "kde@aynoa.net"
__license__ = "LGPL" __license__ = "LGPL"
@@ -55,7 +55,7 @@ from pathlib import Path
from constants import ( from constants import (
APP_CONFIG, CONFIG_PATH, CURRENT_LANGUAGE, DEFAULT_GLOBAL_SHORTCUTS, APP_CONFIG, CONFIG_PATH, CURRENT_LANGUAGE, DEFAULT_GLOBAL_SHORTCUTS,
DEFAULT_VIEWER_SHORTCUTS, GLOBAL_ACTIONS, HISTORY_PATH, ICON_THEME, 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, PROG_NAME, PROG_VERSION, RATING_XATTR_NAME, SCANNER_GENERATE_SIZES,
SCANNER_SETTINGS_DEFAULTS, SUPPORTED_LANGUAGES, TAGS_MENU_MAX_ITEMS_DEFAULT, SCANNER_SETTINGS_DEFAULTS, SUPPORTED_LANGUAGES, TAGS_MENU_MAX_ITEMS_DEFAULT,
THUMBNAILS_BG_COLOR_DEFAULT, THUMBNAILS_DEFAULT_SIZE, VIEWER_ACTIONS, THUMBNAILS_BG_COLOR_DEFAULT, THUMBNAILS_DEFAULT_SIZE, VIEWER_ACTIONS,
@@ -74,7 +74,8 @@ from imageviewer import ImageViewer
from propertiesdialog import PropertiesDialog from propertiesdialog import PropertiesDialog
from widgets import ( from widgets import (
CircularProgressBar, CircularProgressBar,
TagEditWidget, LayoutsWidget, HistoryWidget, RatingWidget, CommentWidget TagEditWidget, LayoutsWidget, HistoryWidget, RatingWidget, CommentWidget,
FavoritesWidget
) )
from metadatamanager import XattrManager from metadatamanager import XattrManager
@@ -255,16 +256,13 @@ class ShortcutHelpDialog(QDialog):
new_mods = new_key_combo.keyboardModifiers() new_mods = new_key_combo.keyboardModifiers()
new_key_tuple = (int(new_key), new_mods) new_key_tuple = (int(new_key), new_mods)
# Check for conflicts in the same scope # Check for conflicts globally
if new_key_tuple in source_dict and new_key_tuple != original_key_combo: conflict_desc = self.main_win.shortcut_controller.check_conflict(
# Handle different value structures new_key, new_mods)
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]
is_same = (new_key_tuple == original_key_combo)
if conflict_desc and not is_same:
QMessageBox.warning(self, UITexts.SHORTCUT_CONFLICT_TITLE, QMessageBox.warning(self, UITexts.SHORTCUT_CONFLICT_TITLE,
UITexts.SHORTCUT_CONFLICT_TEXT.format( UITexts.SHORTCUT_CONFLICT_TEXT.format(
new_sequence.toString(QKeySequence.NativeText), new_sequence.toString(QKeySequence.NativeText),
@@ -300,6 +298,7 @@ class AppShortcutController(QObject):
self.main_win = main_win self.main_win = main_win
self._actions = self._get_actions() self._actions = self._get_actions()
self._shortcuts = {} self._shortcuts = {}
self._favorite_shortcuts = {}
self.action_to_shortcut = {} self.action_to_shortcut = {}
self._register_shortcuts() self._register_shortcuts()
@@ -317,6 +316,44 @@ class AppShortcutController(QObject):
key_tuple = (k, Qt.KeyboardModifiers(m)) key_tuple = (k, Qt.KeyboardModifiers(m))
self._shortcuts[key_tuple] = (act, ignore, desc, cat) self._shortcuts[key_tuple] = (act, ignore, desc, cat)
self.action_to_shortcut[act] = key_tuple 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): def _get_actions(self):
"""Returns a dictionary mapping action strings to callable functions.""" """Returns a dictionary mapping action strings to callable functions."""
@@ -375,6 +412,12 @@ class AppShortcutController(QObject):
if is_typing: if is_typing:
return False 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 # Check if we have a handler for this combination
if (key, mods) in self._shortcuts: if (key, mods) in self._shortcuts:
action_name, ignore_if_typing, _, _ = self._shortcuts[(key, mods)] 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) 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" self.is_xcb = QApplication.platformName() == "xcb"
if self.is_xcb: if self.is_xcb:
self.layouts_tab = LayoutsWidget(self) self.layouts_tab = LayoutsWidget(self)
self.tags_tabs.addTab(self.layouts_tab, UITexts.LAYOUTS_TAB) self.tags_tabs.addTab(self.layouts_tab, UITexts.LAYOUTS_TAB)
# Tab 5: History # Tab 6: History
self.history_tab = HistoryWidget(self) self.history_tab = HistoryWidget(self)
self.tags_tabs.addTab(self.history_tab, UITexts.HISTORY_TAB) 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.main_dock.setWidget(self.tags_tabs)
self.addDockWidget(Qt.RightDockWidgetArea, self.main_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.main_dock)
@@ -1233,6 +1286,11 @@ class MainWindow(QMainWindow):
self.load_config() self.load_config()
self.load_full_history() 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() self._apply_global_stylesheet()
# Set the initial thumbnail generation tier based on the loaded config size # 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) 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 # Actions to show different tabs in the dock
show_tags_action = menu.addAction(QIcon.fromTheme("document-properties"), show_tags_action = menu.addAction(QIcon.fromTheme("document-properties"),
UITexts.MENU_SHOW_TAGS) 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"), show_info_action = menu.addAction(QIcon.fromTheme("dialog-information"),
UITexts.MENU_SHOW_INFO) 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"), show_filter_action = menu.addAction(QIcon.fromTheme("view-filter"),
UITexts.MENU_SHOW_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: if self.is_xcb:
show_layouts_action = menu.addAction(QIcon.fromTheme("view-grid"), show_layouts_action = menu.addAction(QIcon.fromTheme("view-grid"),
@@ -2911,6 +2977,8 @@ class MainWindow(QMainWindow):
self.update_tag_list() self.update_tag_list()
elif widget == self.info_widget: elif widget == self.info_widget:
self.update_info_widget() self.update_info_widget()
elif widget == self.favorites_tab:
self.favorites_tab.refresh_list()
def update_tag_edit_widget(self): def update_tag_edit_widget(self):
"""Updates the tag editor widget with data from the currently selected files.""" """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': self.proxy_model.data(index_at_pos, ITEM_TYPE_ROLE) == 'header':
group_name = self.proxy_model.data(index_at_pos, GROUP_NAME_ROLE) group_name = self.proxy_model.data(index_at_pos, GROUP_NAME_ROLE)
if group_name: if group_name:
action_toggle = menu.addAction("Collapse/Expand Group") action_toggle = menu.addAction(UITexts.COLLAPSE_EXPAND_GROUP)
action_toggle.triggered.connect( action_toggle.triggered.connect(
lambda: self.toggle_group_collapse(group_name)) lambda: self.toggle_group_collapse(group_name))
menu.exec(self.thumbnail_view.mapToGlobal(pos)) menu.exec(self.thumbnail_view.mapToGlobal(pos))
@@ -4006,7 +4074,7 @@ class MainWindow(QMainWindow):
self.on_high_res_generation_finished) self.on_high_res_generation_finished)
self.thumbnail_generator.progress.connect( self.thumbnail_generator.progress.connect(
lambda p, t: self.status_lbl.setText( lambda p, t: self.status_lbl.setText(
f"Regenerating thumbnail: {p}/{t}") UITexts.THUMBNAILS_REGENERATE_PROGRESS.format(p, t))
) )
self.thumbnail_generator.start() self.thumbnail_generator.start()
@@ -4397,9 +4465,7 @@ def main():
path = path[6:] path = path[6:]
win = MainWindow(cache, args, thread_pool_manager) win = MainWindow(cache, args, thread_pool_manager)
shortcut_controller = AppShortcutController(win) app.installEventFilter(win.shortcut_controller)
win.shortcut_controller = shortcut_controller
app.installEventFilter(shortcut_controller)
sys.exit(app.exec()) 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? ¿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 - v0.9.12 -
· Al restaurar el layout no se restaura la posición y dimensiones de los thumbnails. · Al restaurar el layout no se restaura la posición y dimensiones de los thumbnails.
· Mejoras en los menús de contexto. · Mejoras en los menús de contexto.

View File

@@ -29,7 +29,7 @@ if FORCE_X11:
# --- CONFIGURATION --- # --- CONFIGURATION ---
PROG_NAME = "Bagheera Image Viewer" PROG_NAME = "Bagheera Image Viewer"
PROG_ID = "bagheeraview" PROG_ID = "bagheeraview"
PROG_VERSION = "0.9.12" PROG_VERSION = "0.9.13"
PROG_AUTHOR = "Ignacio Serantes" PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS --- # --- CACHE SETTINGS ---
@@ -49,6 +49,8 @@ CACHE_PATH = os.path.join(CONFIG_DIR, "thumbnails_lmdb")
HISTORY_FILE = "history.json" HISTORY_FILE = "history.json"
HISTORY_PATH = os.path.join(CONFIG_DIR, HISTORY_FILE) HISTORY_PATH = os.path.join(CONFIG_DIR, HISTORY_FILE)
LAYOUTS_DIR = os.path.join(CONFIG_DIR, "layouts") # Layouts saving directory 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(): def save_app_config():
@@ -463,6 +465,17 @@ _UI_TEXTS = {
"MENU_CLEAN_CACHE": "Clean up invalid cache entries", "MENU_CLEAN_CACHE": "Clean up invalid cache entries",
"MENU_SHOW_TAGS": "Show Tags", "MENU_SHOW_TAGS": "Show Tags",
"MENU_SHOW_INFO": "Show Information", "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_FILTER": "Show Filter",
"MENU_SHOW_LAYOUTS": "Show Layouts", "MENU_SHOW_LAYOUTS": "Show Layouts",
"MENU_SHOW_HISTORY": "Show History", "MENU_SHOW_HISTORY": "Show History",
@@ -606,12 +619,26 @@ _UI_TEXTS = {
"MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Failed to download the MediaPipe model: {}", "MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Failed to download the MediaPipe model: {}",
"MENU_FILMSTRIP_POSITION": "Filmstrip Position", "MENU_FILMSTRIP_POSITION": "Filmstrip Position",
"FILMSTRIP_BOTTOM": "Bottom", "FILMSTRIP_BOTTOM": "Bottom",
"VIEWER_MENU_COMPARE": "Comparison Mode",
"FILMSTRIP_LEFT": "Left", "FILMSTRIP_LEFT": "Left",
"FILMSTRIP_TOP": "Top", "FILMSTRIP_TOP": "Top",
"FILMSTRIP_RIGHT": "Right", "FILMSTRIP_RIGHT": "Right",
"FILMSTRIP_POS_CHANGED_INFO": "The new filmstrip position will be applied to " "FILMSTRIP_POS_CHANGED_INFO": "The new filmstrip position will be applied to "
"newly opened viewers.", "newly opened viewers.",
"MENU_SHOW_SHORTCUTS": "Configure Keyboard Shortcuts...", "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_TITLE": "Keyboard Shortcuts",
"SHORTCUTS_ACTION": "Action", "SHORTCUTS_ACTION": "Action",
"SHORTCUTS_KEY": "Shortcut", "SHORTCUTS_KEY": "Shortcut",
@@ -620,6 +647,7 @@ _UI_TEXTS = {
"SHORTCUT_EDIT_LABEL": "Enter new shortcut for '{}'", "SHORTCUT_EDIT_LABEL": "Enter new shortcut for '{}'",
"SHORTCUT_CONFLICT_TITLE": "Shortcut Conflict", "SHORTCUT_CONFLICT_TITLE": "Shortcut Conflict",
"SHORTCUT_CONFLICT_TEXT": "The shortcut '{}' is already assigned to '{}'.", "SHORTCUT_CONFLICT_TEXT": "The shortcut '{}' is already assigned to '{}'.",
"SHORTCUT_OVERRIDE_QUESTION": "Do you want to override it?",
"SHORTCUT_SEARCH_PLACEHOLDER": "Search shortcuts...", "SHORTCUT_SEARCH_PLACEHOLDER": "Search shortcuts...",
"CACHE_CLEANING": "Cleaning cache...", "CACHE_CLEANING": "Cleaning cache...",
"CACHE_CLEANED": "Cache cleaned. Removed {} invalid entries.", "CACHE_CLEANED": "Cache cleaned. Removed {} invalid entries.",
@@ -647,7 +675,7 @@ _UI_TEXTS = {
"RENAME_ERROR_EXISTS": "File '{}' already exists.", "RENAME_ERROR_EXISTS": "File '{}' already exists.",
"FILE_RENAMED": "File renamed to {}", "FILE_RENAMED": "File renamed to {}",
"ERROR_RENAME": "Could not rename file: {}", "ERROR_RENAME": "Could not rename file: {}",
"MAIN_DOCK_TITLE": "Main dock", "MAIN_DOCK_TITLE": "",
"LAYOUTS_TAB": "Layouts", "LAYOUTS_TAB": "Layouts",
"LAYOUTS_TABLE_HEADER": ["Name", "Last Modified"], "LAYOUTS_TABLE_HEADER": ["Name", "Last Modified"],
"SAVE_LAYOUT_TITLE": "Save Layout", "SAVE_LAYOUT_TITLE": "Save Layout",
@@ -901,6 +929,17 @@ _UI_TEXTS = {
"MENU_CLEAN_CACHE": "Limpiar entradas de caché inválidas", "MENU_CLEAN_CACHE": "Limpiar entradas de caché inválidas",
"MENU_SHOW_TAGS": "Mostrar Etiquetas", "MENU_SHOW_TAGS": "Mostrar Etiquetas",
"MENU_SHOW_INFO": "Mostrar Información", "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_FILTER": "Mostrar Filtro",
"MENU_SHOW_LAYOUTS": "Mostrar Diseños", "MENU_SHOW_LAYOUTS": "Mostrar Diseños",
"MENU_SHOW_HISTORY": "Mostrar Historial", "MENU_SHOW_HISTORY": "Mostrar Historial",
@@ -1056,6 +1095,7 @@ _UI_TEXTS = {
"MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Fallo al descargar el modelo de MediaPipe: " "MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Fallo al descargar el modelo de MediaPipe: "
"{}", "{}",
"MENU_VIEWER_SETTINGS": "Opciones del Visor", "MENU_VIEWER_SETTINGS": "Opciones del Visor",
"VIEWER_MENU_COMPARE": "Modo Comparación",
"MENU_FILMSTRIP_POSITION": "Posición de la Tira de Imágenes", "MENU_FILMSTRIP_POSITION": "Posición de la Tira de Imágenes",
"FILMSTRIP_BOTTOM": "Abajo", "FILMSTRIP_BOTTOM": "Abajo",
"FILMSTRIP_LEFT": "Izquierda", "FILMSTRIP_LEFT": "Izquierda",
@@ -1063,6 +1103,17 @@ _UI_TEXTS = {
"FILMSTRIP_RIGHT": "Derecha", "FILMSTRIP_RIGHT": "Derecha",
"FILMSTRIP_POS_CHANGED_INFO": "La nueva posición de la tira de imágenes se " "FILMSTRIP_POS_CHANGED_INFO": "La nueva posición de la tira de imágenes se "
"aplicará a los nuevos visores que se abran.", "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...", "MENU_SHOW_SHORTCUTS": "Configurar Atajos de Teclado...",
"SHORTCUTS_TITLE": "Atajos de Teclado", "SHORTCUTS_TITLE": "Atajos de Teclado",
"SHORTCUTS_ACTION": "Acción", "SHORTCUTS_ACTION": "Acción",
@@ -1072,6 +1123,7 @@ _UI_TEXTS = {
"SHORTCUT_EDIT_LABEL": "Nuevo atajo para '{}'", "SHORTCUT_EDIT_LABEL": "Nuevo atajo para '{}'",
"SHORTCUT_CONFLICT_TITLE": "Conflicto de Atajos", "SHORTCUT_CONFLICT_TITLE": "Conflicto de Atajos",
"SHORTCUT_CONFLICT_TEXT": "El atajo '{}' ya está asignado a '{}'.", "SHORTCUT_CONFLICT_TEXT": "El atajo '{}' ya está asignado a '{}'.",
"SHORTCUT_OVERRIDE_QUESTION": "¿Deseas sobrescribirlo?",
"SHORTCUT_SEARCH_PLACEHOLDER": "Buscar atajos...", "SHORTCUT_SEARCH_PLACEHOLDER": "Buscar atajos...",
"CACHE_CLEANING": "Limpiando caché...", "CACHE_CLEANING": "Limpiando caché...",
"CACHE_CLEANED": "Caché limpiada. Se eliminaron {} entradas inválidas.", "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_CLEAN_CACHE": "Limpar entradas de caché inválidas",
"MENU_SHOW_TAGS": "Amosar Etiquetas", "MENU_SHOW_TAGS": "Amosar Etiquetas",
"MENU_SHOW_INFO": "Amosar Información", "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_FILTER": "Amosar Filtro",
"MENU_SHOW_LAYOUTS": "Amosar Deseños", "MENU_SHOW_LAYOUTS": "Amosar Deseños",
"MENU_SHOW_HISTORY": "Amosar Historial", "MENU_SHOW_HISTORY": "Amosar Historial",
@@ -1504,6 +1567,7 @@ _UI_TEXTS = {
"MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Fallo ao descargar o modelo de MediaPipe: {}", "MEDIAPIPE_DOWNLOAD_ERROR_TEXT": "Fallo ao descargar o modelo de MediaPipe: {}",
"MENU_VIEWER_SETTINGS": "Opcións do Visor", "MENU_VIEWER_SETTINGS": "Opcións do Visor",
"MENU_FILMSTRIP_POSITION": "Posición da Tira de Imaxes", "MENU_FILMSTRIP_POSITION": "Posición da Tira de Imaxes",
"VIEWER_MENU_COMPARE": "Modo Comparación",
"FILMSTRIP_BOTTOM": "Abaixo", "FILMSTRIP_BOTTOM": "Abaixo",
"FILMSTRIP_LEFT": "Esquerda", "FILMSTRIP_LEFT": "Esquerda",
"FILMSTRIP_TOP": "Arriba", "FILMSTRIP_TOP": "Arriba",
@@ -1511,6 +1575,17 @@ _UI_TEXTS = {
"FILMSTRIP_POS_CHANGED_INFO": "A nova posición da tira de imaxes aplicarase " "FILMSTRIP_POS_CHANGED_INFO": "A nova posición da tira de imaxes aplicarase "
"aos novos visores que se abran.", "aos novos visores que se abran.",
"MENU_SHOW_SHORTCUTS": "Configurar Atallos de Teclado...", "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_TITLE": "Atallos de Teclado",
"SHORTCUTS_ACTION": "Acción", "SHORTCUTS_ACTION": "Acción",
"SHORTCUTS_KEY": "Atallo", "SHORTCUTS_KEY": "Atallo",
@@ -1519,6 +1594,7 @@ _UI_TEXTS = {
"SHORTCUT_EDIT_LABEL": "Novo Atallo para '{}'", "SHORTCUT_EDIT_LABEL": "Novo Atallo para '{}'",
"SHORTCUT_CONFLICT_TITLE": "Conflito de Atallos", "SHORTCUT_CONFLICT_TITLE": "Conflito de Atallos",
"SHORTCUT_CONFLICT_TEXT": "O atallo '{}' xa está asignado a '{}'.", "SHORTCUT_CONFLICT_TEXT": "O atallo '{}' xa está asignado a '{}'.",
"SHORTCUT_OVERRIDE_QUESTION": "Desexas sobrescribilo?",
"SHORTCUT_SEARCH_PLACEHOLDER": "Buscar atallos...", "SHORTCUT_SEARCH_PLACEHOLDER": "Buscar atallos...",
"CACHE_CLEANING": "Limpando caché...", "CACHE_CLEANING": "Limpando caché...",
"CACHE_CLEANED": "Caché limpada. Elimináronse {} entradas inválidas.", "CACHE_CLEANED": "Caché limpada. Elimináronse {} entradas inválidas.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="bagheeraview", name="bagheeraview",
version="0.9.12", version="0.9.13",
author="Ignacio Serantes", author="Ignacio Serantes",
description="Bagheera Image Viewer - An image viewer for KDE with Baloo in mind", 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 " long_description="A fast image viewer built with PySide6, featuring search and "

View File

@@ -12,6 +12,7 @@ including:
import os import os
import glob import glob
import shutil import shutil
import json
import lmdb import lmdb
from datetime import datetime from datetime import datetime
from collections import deque from collections import deque
@@ -20,11 +21,11 @@ from PySide6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
QMessageBox, QSizePolicy, QInputDialog, QTableWidget, QTableWidgetItem, QMessageBox, QSizePolicy, QInputDialog, QTableWidget, QTableWidgetItem,
QMenu, QHeaderView, QAbstractItemView, QTreeView, QLabel, QTextEdit, QMenu, QHeaderView, QAbstractItemView, QTreeView, QLabel, QTextEdit,
QComboBox, QCompleter, QToolBar QComboBox, QCompleter, QToolBar, QDialog
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
QIcon, QStandardItemModel, QStandardItem, QColor, QPainter, QPen, QIcon, QStandardItemModel, QStandardItem, QColor, QPainter, QPen,
QPalette, QAction, QPalette, QAction, QKeySequence
) )
from PySide6.QtCore import ( from PySide6.QtCore import (
Signal, QSortFilterProxyModel, Slot, QStringListModel, Qt Signal, QSortFilterProxyModel, Slot, QStringListModel, Qt
@@ -33,7 +34,7 @@ from PySide6.QtCore import (
from metadatamanager import XattrManager from metadatamanager import XattrManager
from constants import ( from constants import (
LAYOUTS_DIR, RATING_XATTR_NAME, XATTR_COMMENT_NAME, XATTR_NAME, UITexts, 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.refresh_ui()
self.tags_updated.emit(updated_files_tags) self.tags_updated.emit(updated_files_tags)
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Error", str(e)) QMessageBox.critical(self, UITexts.ERROR, str(e))
finally: finally:
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
@@ -898,6 +899,233 @@ class HistoryWidget(QWidget):
self.refresh_list() 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): class RatingStar(QLabel):
"""An individual star label for the rating widget.""" """An individual star label for the rating widget."""
# Emits the star index (1-5) # Emits the star index (1-5)