This commit is contained in:
Ignacio Serantes
2026-04-12 08:39:07 +02:00
parent 07afab6ca3
commit 1508e629c0
7 changed files with 249 additions and 42 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.19" __version__ = "0.9.20"
__author__ = "Ignacio Serantes" __author__ = "Ignacio Serantes"
__email__ = "kde@aynoa.net" __email__ = "kde@aynoa.net"
__license__ = "LGPL" __license__ = "LGPL"
@@ -3998,14 +3998,14 @@ class MainWindow(QMainWindow):
self.thumbnail_view.setGridSize(QSize()) self.thumbnail_view.setGridSize(QSize())
else: else:
self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None)) self.thumbnail_view.setGridSize(self.delegate.sizeHint(None, None))
self.rebuild_view() self.rebuild_view(full_reset=True)
self.save_config() self.save_config()
self.setFocus() self.setFocus()
def on_sort_changed(self): def on_sort_changed(self):
"""Callback for when the sort order dropdown changes.""" """Callback for when the sort order dropdown changes."""
self.rebuild_view() self.rebuild_view(full_reset=True)
self.save_config() self.save_config()
if hasattr(self, 'history_tab'): if hasattr(self, 'history_tab'):
self.history_tab.refresh_list() self.history_tab.refresh_list()

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.19-dev" PROG_VERSION = "0.9.20"
PROG_AUTHOR = "Ignacio Serantes" PROG_AUTHOR = "Ignacio Serantes"
# --- CACHE SETTINGS --- # --- CACHE SETTINGS ---

View File

@@ -1,3 +1,14 @@
"""
Duplicate Management Dialog Module for Bagheera.
This module implements the DuplicateManagerDialog, a specialized interface
for comparing and resolving duplicate image pairs identified by the
DuplicateDetector. It provides side-by-side viewing with synchronized
zooming and scrolling.
Classes:
DuplicateManagerDialog: A dialog to review and manage duplicate image pairs.
"""
import os import os
from datetime import datetime from datetime import datetime
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -17,6 +28,15 @@ class DuplicateManagerDialog(QDialog):
A dialog to review and manage duplicate image pairs found by the detector. A dialog to review and manage duplicate image pairs found by the detector.
""" """
def __init__(self, duplicates, duplicate_cache, main_win, review_mode=False): def __init__(self, duplicates, duplicate_cache, main_win, review_mode=False):
"""
Initializes the DuplicateManagerDialog.
Args:
duplicates (list): List of DuplicateResult tuples.
duplicate_cache (DuplicateCache): The persistent hash/exception cache.
main_win (MainWindow): Reference to the main application window.
review_mode (bool, optional): If True, shows previously ignored duplicates.
"""
super().__init__(main_win) super().__init__(main_win)
self.duplicates = duplicates # List of DuplicateResult self.duplicates = duplicates # List of DuplicateResult
self.cache = duplicate_cache self.cache = duplicate_cache
@@ -164,7 +184,12 @@ class DuplicateManagerDialog(QDialog):
self.right_pane = self.right_pane_widget.pane self.right_pane = self.right_pane_widget.pane
def closeEvent(self, event): def closeEvent(self, event):
"""Disconnects signals and performs cleanup when closing.""" """
Handles the dialog close event.
Disconnects external signals from the file system watcher and
performs cleanup on the image panes to free resources.
"""
if self.main_win and hasattr(self.main_win, 'fs_watcher'): if self.main_win and hasattr(self.main_win, 'fs_watcher'):
try: try:
self.main_win.fs_watcher.file_deleted.disconnect( self.main_win.fs_watcher.file_deleted.disconnect(
@@ -181,7 +206,12 @@ class DuplicateManagerDialog(QDialog):
super().closeEvent(event) super().closeEvent(event)
def resizeEvent(self, event): def resizeEvent(self, event):
"""Resizes the images to fill available space when the dialog is resized.""" """
Handles the dialog resize event.
Triggers a recalculation of the image scaling to ensure they fit
optimally within the new available space.
"""
super().resizeEvent(event) # Call base class resizeEvent super().resizeEvent(event) # Call base class resizeEvent
self._apply_linked_scaling() self._apply_linked_scaling()
@@ -257,7 +287,12 @@ class DuplicateManagerDialog(QDialog):
self._is_syncing = False self._is_syncing = False
def wheelEvent(self, event): def wheelEvent(self, event):
"""Handles mouse wheel events for zooming (with Ctrl).""" """
Handles mouse wheel events for zooming.
If the Control modifier is held, zooms the active image pane
centered on the mouse cursor position.
"""
if event.modifiers() & Qt.ControlModifier and self.active_pane: if event.modifiers() & Qt.ControlModifier and self.active_pane:
# Calculate the focus point relative to the active pane. # Calculate the focus point relative to the active pane.
focus_pos = self.active_pane.mapFromGlobal(event.globalPosition().toPoint()) focus_pos = self.active_pane.mapFromGlobal(event.globalPosition().toPoint())
@@ -270,7 +305,12 @@ class DuplicateManagerDialog(QDialog):
super().wheelEvent(event) super().wheelEvent(event)
def keyPressEvent(self, event): def keyPressEvent(self, event):
"""Handles keyboard shortcuts for zooming and duplicate management.""" """
Handles keyboard shortcuts for navigation and actions.
Provides quick access to deletion (U/I), ignore (O), and skip (P),
as well as standard zoom controls (Z/+/ -).
"""
key = event.key() key = event.key()
if key == Qt.Key_U: if key == Qt.Key_U:
self._delete_left() self._delete_left()
@@ -306,18 +346,29 @@ class DuplicateManagerDialog(QDialog):
# --- Viewer API Implementation for ImagePane --- # --- Viewer API Implementation for ImagePane ---
def set_active_pane(self, pane): def set_active_pane(self, pane):
"""Sets the currently focused pane for synchronization reference.""" """
Sets the currently focused pane for synchronization reference.
Args:
pane (ImagePane): The pane that gained focus.
"""
self.active_pane = pane self.active_pane = pane
self.update_highlight() self.update_highlight()
def update_highlight(self): def update_highlight(self):
"""Visual feedback for the active pane.""" """Applies visual feedback (border) to the active pane."""
for pw in [self.left_pane_widget, self.right_pane_widget]: for pw in [self.left_pane_widget, self.right_pane_widget]:
pw.setStyleSheet("border: 2px solid #3498db;" if pw.pane == self.active_pane pw.setStyleSheet("border: 2px solid #3498db;" if pw.pane == self.active_pane
else "border: 2px solid transparent;") else "border: 2px solid transparent;")
def on_metadata_changed(self, path, metadata=None): def on_metadata_changed(self, path, metadata=None):
"""Updates labels when image metadata (like tags) is modified.""" """
Updates labels when image metadata is modified.
Args:
path (str): The file path.
metadata (dict, optional): The updated metadata.
"""
# Find the widget displaying this path and update its info # Find the widget displaying this path and update its info
for pw in [self.left_pane_widget, self.right_pane_widget]: for pw in [self.left_pane_widget, self.right_pane_widget]:
if pw.pane.controller.get_current_path() == path: if pw.pane.controller.get_current_path() == path:
@@ -331,18 +382,30 @@ class DuplicateManagerDialog(QDialog):
self.main_win.update_metadata_for_path(path, metadata) self.main_win.update_metadata_for_path(path, metadata)
def on_controller_list_updated(self, index): def on_controller_list_updated(self, index):
"""Required by ImagePane API, no-op in dialog context.""" """Required by ImagePane API, no-op in this dialog."""
pass pass
def update_view_for_pane(self, pane, resize_win=False): def update_view_for_pane(self, pane, resize_win=False):
"""Refreshes the canvas for a specific pane.""" """
Refreshes the canvas for a specific pane.
Args:
pane (ImagePane): The pane to update.
resize_win (bool): Ignored in this context.
"""
pixmap = pane.controller.get_display_pixmap() pixmap = pane.controller.get_display_pixmap()
if not pixmap.isNull(): if not pixmap.isNull():
pane.canvas.setPixmap(pixmap) pane.canvas.setPixmap(pixmap)
pane.canvas.adjustSize() pane.canvas.adjustSize()
def load_and_fit_image_for_pane(self, pane, restore_config=None): def load_and_fit_image_for_pane(self, pane, restore_config=None):
"""Loads and calculates initial zoom to fit the pane viewport.""" """
Loads and calculates initial zoom to fit the pane viewport.
Args:
pane (ImagePane): The target pane.
restore_config (dict, optional): Unused here.
"""
success, _ = pane.controller.load_image() success, _ = pane.controller.load_image()
if success: if success:
viewport = pane.scroll_area.viewport() viewport = pane.scroll_area.viewport()
@@ -355,21 +418,29 @@ class DuplicateManagerDialog(QDialog):
self.update_view_for_pane(pane) self.update_view_for_pane(pane)
def reset_inactivity_timer(self): def reset_inactivity_timer(self):
"""Required by ImagePane API, no-op in this dialog."""
pass pass
def sync_filmstrip_selection(self, index): def sync_filmstrip_selection(self, index):
"""Required by ImagePane API, no-op in this dialog."""
pass pass
def _get_clicked_face_for_pane(self, pane, pos): def _get_clicked_face_for_pane(self, pane, pos):
"""Required by ImagePane API, no-op in this dialog."""
return None return None
def rename_face(self, face): def rename_face(self, face):
"""Required by ImagePane API, no-op in this dialog."""
pass pass
def toggle_fullscreen(self): def toggle_fullscreen(self):
"""Required by ImagePane API, no-op in this dialog."""
pass pass
def _create_comparison_pane_widget(self): def _create_comparison_pane_widget(self):
"""
Factory method to create a pane widget containing an ImagePane and info labels.
"""
widget = QWidget() widget = QWidget()
v_layout = QVBoxLayout(widget) v_layout = QVBoxLayout(widget)
v_layout.setContentsMargins(0, 0, 0, 0) v_layout.setContentsMargins(0, 0, 0, 0)
@@ -450,10 +521,14 @@ class DuplicateManagerDialog(QDialog):
self.table_widget.sortItems(0, Qt.DescendingOrder) self.table_widget.sortItems(0, Qt.DescendingOrder)
def _on_cell_changed(self, row, col, prev_row, prev_col): def _on_cell_changed(self, row, col, prev_row, prev_col):
"""Slot triggered when the selected row in the pairs table changes."""
if row >= 0: if row >= 0:
self._load_pair(row) self._load_pair(row)
def _load_pair(self, row): def _load_pair(self, row):
"""
Loads the duplicate pair corresponding to the specified table row.
"""
if row < 0 or row >= self.table_widget.rowCount(): if row < 0 or row >= self.table_widget.rowCount():
return return
@@ -579,7 +654,20 @@ class DuplicateManagerDialog(QDialog):
def _set_pane_data(self, pane_widget, path, filename_color, dir_color, def _set_pane_data(self, pane_widget, path, filename_color, dir_color,
filename_text, dir_text) -> bool: filename_text, dir_text) -> bool:
"""Updates an ImagePane and its labels with file data.""" """
Updates an ImagePane and its labels with file data.
Args:
pane_widget (QWidget): The container widget for the pane.
path (str): File path to load.
filename_color (str): Hex color for filename label.
dir_color (str): Hex color for directory label.
filename_text (str): Name to display.
dir_text (str): Directory path to display.
Returns:
bool: True if linked scaling should be disabled (e.g., animation).
"""
pane = pane_widget.pane pane = pane_widget.pane
info_lbl = pane_widget.info_lbl info_lbl = pane_widget.info_lbl
filename_lbl = pane_widget.filename_lbl filename_lbl = pane_widget.filename_lbl
@@ -625,7 +713,12 @@ class DuplicateManagerDialog(QDialog):
return disable_linking return disable_linking
def _show_pane_context_menu(self, pos): def _show_pane_context_menu(self, pos):
"""Displays a context menu for the pane that requested it.""" """
Displays a context menu for the pane that requested it.
Args:
pos (QPoint): Local coordinates of the request.
"""
pane = self.sender() pane = self.sender()
path = pane.controller.get_current_path() path = pane.controller.get_current_path()
if not path or not os.path.exists(path): if not path or not os.path.exists(path):
@@ -685,7 +778,12 @@ class DuplicateManagerDialog(QDialog):
menu.exec(pane.mapToGlobal(pos)) menu.exec(pane.mapToGlobal(pos))
def _handle_permanent_delete(self, path): def _handle_permanent_delete(self, path):
"""Prompts for and executes permanent deletion of a file.""" """
Prompts for and executes permanent deletion of a file.
Args:
path (str): File path to delete.
"""
confirm = QMessageBox(self) confirm = QMessageBox(self)
confirm.setIcon(QMessageBox.Warning) confirm.setIcon(QMessageBox.Warning)
confirm.setText(UITexts.CONFIRM_DELETE_TEXT) confirm.setText(UITexts.CONFIRM_DELETE_TEXT)
@@ -697,7 +795,13 @@ class DuplicateManagerDialog(QDialog):
self._handle_action(delete_path=path, permanent=True) self._handle_action(delete_path=path, permanent=True)
def _show_properties(self, path, pane): def _show_properties(self, path, pane):
"""Shows the file properties dialog for a pane's image.""" """
Shows the file properties dialog for a pane's image.
Args:
path (str): File path.
pane (ImagePane): The pane containing the image.
"""
tags = pane.controller._current_tags tags = pane.controller._current_tags
rating = pane.controller._current_rating rating = pane.controller._current_rating
dlg = PropertiesDialog( dlg = PropertiesDialog(
@@ -705,7 +809,11 @@ class DuplicateManagerDialog(QDialog):
dlg.exec() dlg.exec()
def _on_pane_activated(self): def _on_pane_activated(self):
"""Handles pane activation to synchronize viewing state if linked.""" """
Handles pane activation to synchronize viewing state if linked.
Ensures that the activated pane becomes the leader for synchronization.
"""
# When a pane is activated, ensure its zoom/scroll is the reference for linking # When a pane is activated, ensure its zoom/scroll is the reference for linking
if self.panes_linked: if self.panes_linked:
active_pane = self.sender() # The pane that emitted activated signal active_pane = self.sender() # The pane that emitted activated signal
@@ -720,7 +828,13 @@ class DuplicateManagerDialog(QDialog):
other_pane.set_scroll_relative(x_pct, y_pct) other_pane.set_scroll_relative(x_pct, y_pct)
def _sync_scroll(self, x_pct, y_pct): def _sync_scroll(self, x_pct, y_pct):
"""Synchronizes scroll position between panes if linked.""" """
Synchronizes scroll position between panes if linked.
Args:
x_pct (float): Horizontal scroll percentage.
y_pct (float): Vertical scroll percentage.
"""
if not self.panes_linked: if not self.panes_linked:
return return
source_pane = self.sender() source_pane = self.sender()
@@ -730,7 +844,13 @@ class DuplicateManagerDialog(QDialog):
self.left_pane.set_scroll_relative(x_pct, y_pct) self.left_pane.set_scroll_relative(x_pct, y_pct)
def _sync_zoom(self, factor, source_pane=None): def _sync_zoom(self, factor, source_pane=None):
"""Synchronizes zoom factor between panes if linked.""" """
Synchronizes zoom factor between panes if linked.
Args:
factor (float): New zoom factor.
source_pane (ImagePane, optional): The leader pane.
"""
if not self.panes_linked or self._is_syncing: if not self.panes_linked or self._is_syncing:
return return
if source_pane is None: if source_pane is None:
@@ -788,7 +908,12 @@ class DuplicateManagerDialog(QDialog):
self._is_syncing = False self._is_syncing = False
def _format_size(self, size): def _format_size(self, size):
"""Formats a file size in bytes to a human-readable string.""" """
Formats a file size in bytes to a human-readable string.
Args:
size (int): Size in bytes.
"""
for unit in ['B', 'KiB', 'MiB', 'GiB']: for unit in ['B', 'KiB', 'MiB', 'GiB']:
if size < 1024: if size < 1024:
return f"{size:.1f} {unit}" return f"{size:.1f} {unit}"
@@ -808,7 +933,7 @@ class DuplicateManagerDialog(QDialog):
self._handle_action(delete_path=path_to_delete) self._handle_action(delete_path=path_to_delete)
def _toggle_link_panes(self): def _toggle_link_panes(self):
"""Toggles the link state between panes.""" """Toggles the synchronized viewing state between panes."""
self._user_link_preference = self.btn_link_panes.isChecked() self._user_link_preference = self.btn_link_panes.isChecked()
self.panes_linked = self._user_link_preference self.panes_linked = self._user_link_preference
if self.panes_linked: if self.panes_linked:
@@ -823,7 +948,12 @@ class DuplicateManagerDialog(QDialog):
self.right_pane.set_scroll_relative(x_pct, y_pct) self.right_pane.set_scroll_relative(x_pct, y_pct)
def _on_file_deleted_externally(self, path): def _on_file_deleted_externally(self, path):
"""Handles file deletion events from the FileSystemWatcher.""" """
Handles file deletion events from the FileSystemWatcher.
Args:
path (str): The deleted path.
"""
path = os.path.abspath(path) path = os.path.abspath(path)
# 1. Identify pairs to remove and clean up the pending DB # 1. Identify pairs to remove and clean up the pending DB
@@ -849,7 +979,13 @@ class DuplicateManagerDialog(QDialog):
self.table_widget.setCurrentCell(new_row, 0) self.table_widget.setCurrentCell(new_row, 0)
def _on_file_moved_externally(self, old_path, new_path): def _on_file_moved_externally(self, old_path, new_path):
"""Handles file move/rename events from the FileSystemWatcher.""" """
Handles file move/rename events from the FileSystemWatcher.
Args:
old_path (str): Original path.
new_path (str): Target path.
"""
old_path = os.path.abspath(old_path) old_path = os.path.abspath(old_path)
new_path = os.path.abspath(new_path) new_path = os.path.abspath(new_path)
@@ -901,10 +1037,12 @@ class DuplicateManagerDialog(QDialog):
def _handle_action(self, delete_path=None, skip=False, permanent=None): def _handle_action(self, delete_path=None, skip=False, permanent=None):
""" """
Handles management actions (delete, skip, keep) for duplicate pairs. Handles management actions (delete, skip, keep) for duplicate pairs.
Updates the local list and the persistent databases.
Args: Args:
delete_path: Path to delete, if any. delete_path (str, optional): Path to delete.
skip: Whether to skip the current pair. skip (bool): Whether to skip without ignoring permanently.
permanent (bool, optional): If True, deletes without trash.
""" """
current_row = self.table_widget.currentRow() current_row = self.table_widget.currentRow()
if current_row < 0: if current_row < 0:

View File

@@ -1,3 +1,14 @@
"""
File System Watcher Module for Bagheera Image Viewer.
This module provides functionality to monitor file system changes in real-time
using the watchdog library. It notifies the application about new, deleted, or
modified image files within watched directories, handling debouncing to ensure
stability during rapid file operations.
Classes:
FileSystemWatcher: Coordinates file system monitoring and emits Qt signals.
"""
import os import os
try: try:
from watchdog.observers import Observer from watchdog.observers import Observer
@@ -14,6 +25,10 @@ class FileSystemWatcher(QObject):
Monitors file system events (created, deleted, modified) for specified directories. Monitors file system events (created, deleted, modified) for specified directories.
Emits signals to notify the main application thread of changes. Emits signals to notify the main application thread of changes.
""" """
# Signals emitted to the rest of the application
# ---------------------------------------------
file_created = Signal(str) file_created = Signal(str)
file_deleted = Signal(str) file_deleted = Signal(str)
file_modified = Signal(str) file_modified = Signal(str)
@@ -24,10 +39,18 @@ class FileSystemWatcher(QObject):
directory_modified = Signal(str) # For changes that might not be specific files directory_modified = Signal(str) # For changes that might not be specific files
_modified_events_queue = {} # {path: QTimer} _modified_events_queue = {} # {path: QTimer}
"""Queue to manage debouncing of modification events."""
def __init__(self, parent=None): def __init__(self, parent=None):
"""
Initializes the FileSystemWatcher.
Args:
parent (QObject, optional): The parent object. Defaults to None.
"""
super().__init__(parent) super().__init__(parent)
self._watched_directories = set() self._watched_directories = set()
self._debounce_interval = 500 # milliseconds
if HAVE_WATCHDOG: if HAVE_WATCHDOG:
self._observer = Observer() self._observer = Observer()
@@ -36,16 +59,21 @@ class FileSystemWatcher(QObject):
else: else:
self._observer = None # Keep observer as None if watchdog is not available self._observer = None # Keep observer as None if watchdog is not available
# Debounce timer for modified events to avoid multiple signals for a single save
self._debounce_interval = 500 # milliseconds
# Connect the internal signal to the debouncing slot # Connect the internal signal to the debouncing slot
if HAVE_WATCHDOG: if HAVE_WATCHDOG:
self._file_modified_from_handler.connect(self._on_file_modified_debounced) self._file_modified_from_handler.connect(self._on_file_modified_debounced)
def _on_file_modified_debounced(self, path): def _on_file_modified_debounced(self, path):
"""Slot to handle modified events from the watchdog thread, debounced in the """
main thread.""" Slot to handle modified events from the watchdog thread.
Implements a debouncing mechanism: if multiple modification events
arrive for the same path within the interval, previous timers are
reset to avoid redundant UI updates or heavy disk operations.
Args:
path (str): The path of the modified file.
"""
# Debounce timer for modified events to avoid multiple signals for a single save # Debounce timer for modified events to avoid multiple signals for a single save
if path in self._modified_events_queue: if path in self._modified_events_queue:
self._modified_events_queue[path].stop() self._modified_events_queue[path].stop()
@@ -59,7 +87,12 @@ class FileSystemWatcher(QObject):
self._modified_events_queue[path].start() self._modified_events_queue[path].start()
def _emit_modified_after_debounce(self, path): def _emit_modified_after_debounce(self, path):
"""Emits the file_modified signal after the debounce period.""" """
Emits the file_modified signal after the debounce period.
Args:
path (str): The path of the modified file.
"""
self.file_modified.emit(path) self.file_modified.emit(path)
if path in self._modified_events_queue: if path in self._modified_events_queue:
# Safely delete the QTimer object when done # Safely delete the QTimer object when done
@@ -67,7 +100,16 @@ class FileSystemWatcher(QObject):
del self._modified_events_queue[path] del self._modified_events_queue[path]
def add_path(self, path): def add_path(self, path):
"""Adds a directory to be monitored.""" """
Adds a directory to be monitored.
This method ensures that redundant watches are avoided by checking if
the path is already covered by an existing watch or if it should
consolidate multiple sub-watches into a single parent watch.
Args:
path (str): The directory path to monitor.
"""
if not HAVE_WATCHDOG or self._observer is None: if not HAVE_WATCHDOG or self._observer is None:
return return
@@ -111,7 +153,12 @@ class FileSystemWatcher(QObject):
self.monitoring_status_changed.emit(True) self.monitoring_status_changed.emit(True)
def remove_path(self, path): def remove_path(self, path):
"""Removes a directory from monitoring.""" """
Removes a directory from monitoring.
Args:
path (str): The directory path to stop monitoring.
"""
if not HAVE_WATCHDOG or self._observer is None: if not HAVE_WATCHDOG or self._observer is None:
return return
abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path))) abs_path = os.path.normpath(os.path.abspath(os.path.expanduser(path)))
@@ -138,7 +185,9 @@ class FileSystemWatcher(QObject):
self.monitoring_status_changed.emit(False) self.monitoring_status_changed.emit(False)
def stop(self): def stop(self):
"""Stops the file system observer.""" """
Stops the file system observer and cleans up active timers.
"""
if HAVE_WATCHDOG and self._observer: if HAVE_WATCHDOG and self._observer:
self._observer.stop() self._observer.stop()
self._observer.join() self._observer.join()
@@ -150,10 +199,19 @@ class FileSystemWatcher(QObject):
if HAVE_WATCHDOG: if HAVE_WATCHDOG:
class _Handler(FileSystemEventHandler): class _Handler(FileSystemEventHandler):
"""
Custom event handler for watchdog events.
Translates low-level file system events into high-level application
signals, filtering for supported image types.
"""
# Signal to communicate to main thread # Signal to communicate to main thread
file_modified_from_thread = Signal(str) file_modified_from_thread = Signal(str)
"""Custom event handler for watchdog events."""
def __init__(self, watcher): def __init__(self, watcher):
"""
Initializes the handler with a reference to the main watcher.
"""
super().__init__() super().__init__()
self.watcher = watcher self.watcher = watcher
@@ -199,11 +257,21 @@ class FileSystemWatcher(QObject):
self.watcher._file_modified_from_handler.emit(event.src_path) self.watcher._file_modified_from_handler.emit(event.src_path)
def _emit_modified(self, path): def _emit_modified(self, path):
"""Internal helper to emit the modified signal.""" """
Internal helper to emit the modified signal.
Args:
path (str): The modified path.
"""
self.watcher.file_modified.emit(path) self.watcher.file_modified.emit(path)
if path in self.watcher._modified_events_queue: if path in self.watcher._modified_events_queue:
del self.watcher._modified_events_queue[path] del self.watcher._modified_events_queue[path]
def _is_image_file(self, path): def _is_image_file(self, path):
"""Checks if a given path has a supported image extension.""" """
Checks if a given path has a supported image extension.
Args:
path (str): The file path to check.
"""
return os.path.splitext(path)[1].lower() in IMAGE_EXTENSIONS return os.path.splitext(path)[1].lower() in IMAGE_EXTENSIONS

View File

@@ -3317,7 +3317,8 @@ class ImageViewer(QWidget):
# A standard tick is 120. We define a threshold based on speed. # A standard tick is 120. We define a threshold based on speed.
# Speed 1 (slowest) requires a full 120 delta. # Speed 1 (slowest) requires a full 120 delta.
# Speed 10 (fastest) requires 120/10 = 12 delta. # Speed 10 (fastest) requires 120/10 = 12 delta.
threshold = 120 / speed # Still too fast so speed / 2.
threshold = 120 / speed / 2
self._wheel_scroll_accumulator += event.angleDelta().y() self._wheel_scroll_accumulator += event.angleDelta().y()

View File

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

View File

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