A bunch of changes

This commit is contained in:
Ignacio Serantes
2026-03-24 09:06:37 +01:00
parent 144ad665e4
commit 20e5318a53
4 changed files with 518 additions and 135 deletions

View File

@@ -2,7 +2,12 @@ v0.9.11 -
· Filmstrip fixed · Filmstrip fixed
· Añadida una nueva área llamada Body. · Añadida una nueva área llamada Body.
· Refactorizaciones, optimizaciones y cambios a saco. · Refactorizaciones, optimizaciones y cambios a saco.
· Image viewer tiene comparisonb
Implement a bulk rename feature for the selected pet or face tags.
Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text.
Add a visual indicator (e.g., an icon in the status bar) for the "Link Panes" status instead of text.
Add a `shutdown` signal or method to `ScannerWorker` to allow cleaner cancellation of long-running tasks like `generate_thumbnail`. Add a `shutdown` signal or method to `ScannerWorker` to allow cleaner cancellation of long-running tasks like `generate_thumbnail`.
Implement a mechanism to dynamically adjust the thread pool size based on system load or user activity. Implement a mechanism to dynamically adjust the thread pool size based on system load or user activity.

View File

@@ -167,7 +167,7 @@ if importlib.util.find_spec("mediapipe") is not None:
pass pass
HAVE_FACE_RECOGNITION = importlib.util.find_spec("face_recognition") is not None HAVE_FACE_RECOGNITION = importlib.util.find_spec("face_recognition") is not None
HAVE_BAGHEERASEARCH_LIB = False HAVE_BAGHEERASEARCH_LIB = True
MEDIAPIPE_FACE_MODEL_PATH = os.path.join(CONFIG_DIR, MEDIAPIPE_FACE_MODEL_PATH = os.path.join(CONFIG_DIR,
"blaze_face_short_range.tflite") "blaze_face_short_range.tflite")
@@ -291,6 +291,10 @@ VIEWER_ACTIONS = {
"toggle_visibility": ("Show/Hide Main Window", "Window"), "toggle_visibility": ("Show/Hide Main Window", "Window"),
"toggle_crop": ("Toggle Crop Mode", "Edit"), "toggle_crop": ("Toggle Crop Mode", "Edit"),
"save_crop": ("Save Cropped Image", "File"), "save_crop": ("Save Cropped Image", "File"),
"compare_1": ("Single View", "View"),
"compare_2": ("Compare 2 Images", "View"),
"compare_4": ("Compare 4 Images", "View"),
"link_panes": ("Link Panes", "View"),
} }
DEFAULT_VIEWER_SHORTCUTS = { DEFAULT_VIEWER_SHORTCUTS = {
@@ -319,6 +323,10 @@ DEFAULT_VIEWER_SHORTCUTS = {
"toggle_visibility": (Qt.Key_H, Qt.ControlModifier), "toggle_visibility": (Qt.Key_H, Qt.ControlModifier),
"toggle_crop": (Qt.Key_C, Qt.NoModifier), "toggle_crop": (Qt.Key_C, Qt.NoModifier),
"save_crop": (Qt.Key_S, Qt.ControlModifier), "save_crop": (Qt.Key_S, Qt.ControlModifier),
"compare_1": (Qt.Key_1, Qt.AltModifier),
"compare_2": (Qt.Key_2, Qt.AltModifier),
"compare_4": (Qt.Key_4, Qt.AltModifier),
"link_panes": (Qt.Key_L, Qt.AltModifier),
} }
@@ -735,6 +743,11 @@ _UI_TEXTS = {
"VIEWER_MENU_CROP": "Crop Mode", "VIEWER_MENU_CROP": "Crop Mode",
"VIEWER_MENU_SAVE_CROP": "Save Selection...", "VIEWER_MENU_SAVE_CROP": "Save Selection...",
"SAVE_CROP_TITLE": "Save Cropped Image", "SAVE_CROP_TITLE": "Save Cropped Image",
"VIEWER_MENU_COMPARE": "Comparison Mode",
"VIEWER_MENU_COMPARE_1": "Single View",
"VIEWER_MENU_COMPARE_2": "2 Images",
"VIEWER_MENU_COMPARE_4": "4 Images",
"VIEWER_MENU_LINK_PANES": "Link Panes",
"SAVE_CROP_FILTER": "Images (*.jpg *.jpeg *.png *.bmp *.webp)", "SAVE_CROP_FILTER": "Images (*.jpg *.jpeg *.png *.bmp *.webp)",
"SLIDESHOW_INTERVAL_TITLE": "Slideshow Interval", "SLIDESHOW_INTERVAL_TITLE": "Slideshow Interval",
"SLIDESHOW_INTERVAL_TEXT": "Seconds:", "SLIDESHOW_INTERVAL_TEXT": "Seconds:",
@@ -1164,6 +1177,11 @@ _UI_TEXTS = {
"VIEWER_MENU_TAGS": "Etiquetas rápidas", "VIEWER_MENU_TAGS": "Etiquetas rápidas",
"VIEWER_MENU_CROP": "Modo Recorte", "VIEWER_MENU_CROP": "Modo Recorte",
"VIEWER_MENU_SAVE_CROP": "Guardar Selección...", "VIEWER_MENU_SAVE_CROP": "Guardar Selección...",
"VIEWER_MENU_COMPARE": "Modo Comparación",
"VIEWER_MENU_COMPARE_1": "Vista Única",
"VIEWER_MENU_COMPARE_2": "2 Imágenes",
"VIEWER_MENU_COMPARE_4": "4 Imágenes",
"VIEWER_MENU_LINK_PANES": "Vincular Paneles",
"SAVE_CROP_TITLE": "Guardar Imagen Recortada", "SAVE_CROP_TITLE": "Guardar Imagen Recortada",
"SAVE_CROP_FILTER": "Imágenes (*.jpg *.jpeg *.png *.bmp *.webp)", "SAVE_CROP_FILTER": "Imágenes (*.jpg *.jpeg *.png *.bmp *.webp)",
"SLIDESHOW_INTERVAL_TITLE": "Intervalo de Presentación", "SLIDESHOW_INTERVAL_TITLE": "Intervalo de Presentación",
@@ -1597,6 +1615,11 @@ _UI_TEXTS = {
"VIEWER_MENU_TAGS": "Etiquetas rápidas", "VIEWER_MENU_TAGS": "Etiquetas rápidas",
"VIEWER_MENU_CROP": "Modo Recorte", "VIEWER_MENU_CROP": "Modo Recorte",
"VIEWER_MENU_SAVE_CROP": "Gardar Selección...", "VIEWER_MENU_SAVE_CROP": "Gardar Selección...",
"VIEWER_MENU_COMPARE": "Modo Comparación",
"VIEWER_MENU_COMPARE_1": "Vista Única",
"VIEWER_MENU_COMPARE_2": "2 Imaxes",
"VIEWER_MENU_COMPARE_4": "4 Imaxes",
"VIEWER_MENU_LINK_PANES": "Vincular Paneis",
"SAVE_CROP_TITLE": "Gardar Imaxe Recortada", "SAVE_CROP_TITLE": "Gardar Imaxe Recortada",
"SAVE_CROP_FILTER": "Imaxes (*.jpg *.jpeg *.png *.bmp *.webp)", "SAVE_CROP_FILTER": "Imaxes (*.jpg *.jpeg *.png *.bmp *.webp)",
"SLIDESHOW_INTERVAL_TITLE": "Intervalo da Presentación", "SLIDESHOW_INTERVAL_TITLE": "Intervalo da Presentación",

View File

@@ -44,7 +44,11 @@ from imageviewer import ImageViewer
from metadatamanager import XattrManager from metadatamanager import XattrManager
if HAVE_BAGHEERASEARCH_LIB: if HAVE_BAGHEERASEARCH_LIB:
from bagheera_search_lib import BagheeraSearcher try:
from bagheera_search_lib import BagheeraSearcher
except ImportError:
HAVE_BAGHEERASEARCH_LIB = False
pass
# Set up logging for better debugging # Set up logging for better debugging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -371,11 +375,8 @@ class CacheWriter(QThread):
# Gather a batch of items # Gather a batch of items
# Adaptive batch size: if queue is backing up, increase transaction size # Adaptive batch size: if queue is backing up, increase transaction size
# to improve throughput. # to improve throughput.
if not self._running: # Respect max size even during shutdown to avoid OOM or huge transactions
# Flush everything if stopping batch_limit = self._max_size
batch_limit = len(self._queue)
else:
batch_limit = self._max_size
batch = [] batch = []
while self._queue and len(batch) < batch_limit: while self._queue and len(batch) < batch_limit:

View File

@@ -15,7 +15,7 @@ import json
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea, QLabel, QHBoxLayout, QListWidget, QWidget, QVBoxLayout, QScrollArea, QLabel, QHBoxLayout, QListWidget,
QListWidgetItem, QAbstractItemView, QMenu, QInputDialog, QDialog, QDialogButtonBox, QListWidgetItem, QAbstractItemView, QMenu, QInputDialog, QDialog, QDialogButtonBox, QGridLayout,
QApplication, QMessageBox, QLineEdit, QFileDialog QApplication, QMessageBox, QLineEdit, QFileDialog
) )
from PySide6.QtGui import ( from PySide6.QtGui import (
@@ -39,6 +39,13 @@ from imagecontroller import ImageController
from widgets import FaceNameInputWidget from widgets import FaceNameInputWidget
from propertiesdialog import PropertiesDialog from propertiesdialog import PropertiesDialog
class HighlightWidget(QWidget):
"""Widget to show a highlight border around the active pane."""
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.setStyleSheet("border: 2px solid #3498db; background: transparent;")
self.hide()
class FaceNameDialog(QDialog): class FaceNameDialog(QDialog):
"""A dialog to get a face name using the FaceNameInputWidget.""" """A dialog to get a face name using the FaceNameInputWidget."""
@@ -646,7 +653,8 @@ class FaceCanvas(QLabel):
def mousePressEvent(self, event): def mousePressEvent(self, event):
"""Handles mouse press for drawing new faces or panning.""" """Handles mouse press for drawing new faces or panning."""
self.viewer.reset_inactivity_timer() if hasattr(self.viewer, 'reset_inactivity_timer'):
self.viewer.reset_inactivity_timer()
if self.viewer.crop_mode and event.button() == Qt.LeftButton: if self.viewer.crop_mode and event.button() == Qt.LeftButton:
handle = self._hit_test_crop(event.position().toPoint()) handle = self._hit_test_crop(event.position().toPoint())
if handle: if handle:
@@ -663,6 +671,10 @@ class FaceCanvas(QLabel):
event.accept() event.accept()
return return
# Activate the pane on click
if hasattr(self.viewer, 'activate'):
self.viewer.activate()
if self.controller.show_faces and event.button() == Qt.LeftButton: if self.controller.show_faces and event.button() == Qt.LeftButton:
self.start_pos = event.position().toPoint() self.start_pos = event.position().toPoint()
@@ -697,7 +709,8 @@ class FaceCanvas(QLabel):
def mouseMoveEvent(self, event): def mouseMoveEvent(self, event):
"""Handles mouse move for drawing new faces or panning.""" """Handles mouse move for drawing new faces or panning."""
self.viewer.reset_inactivity_timer() if hasattr(self.viewer, 'reset_inactivity_timer'):
self.viewer.reset_inactivity_timer()
if self.viewer.crop_mode: if self.viewer.crop_mode:
curr_pos = event.position().toPoint() curr_pos = event.position().toPoint()
@@ -963,6 +976,10 @@ class FaceCanvas(QLabel):
self.viewer.zoom_manager.zoom_to_rect(clicked_face) self.viewer.zoom_manager.zoom_to_rect(clicked_face)
event.accept() event.accept()
return return
# Double click to toggle fullscreen if handled by viewer/pane
if hasattr(self.viewer, 'toggle_fullscreen'):
self.viewer.toggle_fullscreen()
# If no face was double-clicked, pass the event on # If no face was double-clicked, pass the event on
super().mouseDoubleClickEvent(event) super().mouseDoubleClickEvent(event)
@@ -1033,6 +1050,8 @@ class ZoomManager(QObject):
""" """
Manages zoom calculations and state for the ImageViewer. Manages zoom calculations and state for the ImageViewer.
""" """
zoomed = Signal(float)
def __init__(self, viewer): def __init__(self, viewer):
super().__init__(viewer) super().__init__(viewer)
self.viewer = viewer self.viewer = viewer
@@ -1049,7 +1068,9 @@ class ZoomManager(QObject):
# so it can update its selection. # so it can update its selection.
self.viewer.index_changed.emit(self.viewer.controller.index) self.viewer.index_changed.emit(self.viewer.controller.index)
self.viewer.sync_filmstrip_selection(self.viewer.controller.index) self.zoomed.emit(self.viewer.controller.zoom_factor)
if hasattr(self.viewer, 'sync_filmstrip_selection'):
self.viewer.sync_filmstrip_selection(self.viewer.controller.index)
def toggle_fit_to_screen(self): def toggle_fit_to_screen(self):
""" """
@@ -1083,6 +1104,7 @@ class ZoomManager(QObject):
return return
self.viewer.controller.zoom_factor = min(w_avail / img_w, h_avail / img_h) self.viewer.controller.zoom_factor = min(w_avail / img_w, h_avail / img_h)
self.zoomed.emit(self.viewer.controller.zoom_factor)
self.viewer.update_view(resize_win=False) self.viewer.update_view(resize_win=False)
def calculate_initial_zoom(self, available_w, available_h, is_fullscreen): def calculate_initial_zoom(self, available_w, available_h, is_fullscreen):
@@ -1099,6 +1121,8 @@ class ZoomManager(QObject):
else: else:
self.viewer.controller.zoom_factor = 1.0 self.viewer.controller.zoom_factor = 1.0
self.zoomed.emit(self.viewer.controller.zoom_factor)
def zoom_to_rect(self, face_rect): def zoom_to_rect(self, face_rect):
"""Zooms and pans the view to center on a given normalized rectangle.""" """Zooms and pans the view to center on a given normalized rectangle."""
if self.viewer.controller.pixmap_original.isNull(): if self.viewer.controller.pixmap_original.isNull():
@@ -1131,6 +1155,7 @@ class ZoomManager(QObject):
new_zoom = min(zoom_w, zoom_h) new_zoom = min(zoom_w, zoom_h)
self.viewer.controller.zoom_factor = new_zoom self.viewer.controller.zoom_factor = new_zoom
self.zoomed.emit(new_zoom)
self.viewer.update_view(resize_win=False) self.viewer.update_view(resize_win=False)
# Defer centering until after the view has been updated # Defer centering until after the view has been updated
@@ -1157,6 +1182,153 @@ class ZoomManager(QObject):
self.viewer.scroll_area.verticalScrollBar().setValue(int(scroll_y)) self.viewer.scroll_area.verticalScrollBar().setValue(int(scroll_y))
class ImagePane(QWidget):
"""
A single image viewport containing the canvas, scroll area, and controller.
Used within ImageViewer to support comparison modes.
"""
activated = Signal()
index_changed = Signal(int)
scrolled = Signal(float, float)
def __init__(self, parent_viewer, cache, image_list, index, initial_tags=None,
initial_rating=0):
super().__init__(parent_viewer)
self.viewer = parent_viewer # Reference to main ImageViewer
self.main_win = parent_viewer.main_win
self.cache = cache
self.controller = ImageController(image_list, index, initial_tags,
initial_rating)
if self.main_win:
self.controller.show_faces = self.main_win.show_faces
# Connect signals
self.controller.metadata_changed.connect(self.viewer.on_metadata_changed)
self.controller.list_updated.connect(self.viewer.on_controller_list_updated)
self.zoom_manager = ZoomManager(self)
self.canvas = FaceCanvas(self)
self.movie = None
self.crop_mode = False
# Layout
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.scroll_area = QScrollArea()
self.scroll_area.setAlignment(Qt.AlignCenter)
self.scroll_area.setStyleSheet("background-color: #000; border: none;")
# self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll_area.setWidget(self.canvas)
layout.addWidget(self.scroll_area)
self.scroll_area.horizontalScrollBar().valueChanged.connect(self._on_scroll)
self.scroll_area.verticalScrollBar().valueChanged.connect(self._on_scroll)
self._suppress_scroll_signal = False
def activate(self):
"""Sets this pane as the active one in the viewer."""
self.viewer.set_active_pane(self)
self.activated.emit()
def reset_inactivity_timer(self):
"""Delegates to parent viewer."""
self.viewer.reset_inactivity_timer()
def sync_filmstrip_selection(self, index):
"""Delegates to parent viewer if this is the active pane."""
if self.viewer.active_pane == self:
self.viewer.sync_filmstrip_selection(index)
def load_and_fit_image(self, restore_config=None):
"""Loads image using shared logic, adapted for Pane."""
# reuse logic from ImageViewer (now moved/adapted)
self.viewer.load_and_fit_image_for_pane(self, restore_config)
def update_view(self, resize_win=False):
"""Updates this pane's view."""
self.viewer.update_view_for_pane(self, resize_win)
def _on_scroll(self):
if self._suppress_scroll_signal:
return
h_bar = self.scroll_area.horizontalScrollBar()
v_bar = self.scroll_area.verticalScrollBar()
h_max = h_bar.maximum()
v_max = v_bar.maximum()
x_pct = h_bar.value() / h_max if h_max > 0 else 0
y_pct = v_bar.value() / v_max if v_max > 0 else 0
self.scrolled.emit(x_pct, y_pct)
def set_scroll_relative(self, x_pct, y_pct):
self._suppress_scroll_signal = True
h_bar = self.scroll_area.horizontalScrollBar()
v_bar = self.scroll_area.verticalScrollBar()
h_bar.setValue(int(x_pct * h_bar.maximum()))
v_bar.setValue(int(y_pct * v_bar.maximum()))
self._suppress_scroll_signal = False
def toggle_fullscreen(self):
self.viewer.toggle_fullscreen()
def next_image(self):
self.controller.next()
self.index_changed.emit(self.controller.index)
self.load_and_fit_image()
def prev_image(self):
self.controller.prev()
self.index_changed.emit(self.controller.index)
self.load_and_fit_image()
def first_image(self):
self.controller.first()
self.index_changed.emit(self.controller.index)
self.load_and_fit_image()
def last_image(self):
self.controller.last()
self.index_changed.emit(self.controller.index)
self.load_and_fit_image()
def _get_clicked_face(self, pos):
return self.viewer._get_clicked_face_for_pane(self, pos)
def rename_face(self, face):
self.viewer.rename_face(face)
def cleanup(self):
if self.movie:
self.movie.stop()
self.controller.cleanup()
# Event handlers specific to the pane surface (e.g. drop) can go here
def mousePressEvent(self, event):
self.activate()
super().mousePressEvent(event)
def update_image_list(self, new_list):
# Logic similar to ImageViewer.update_image_list but for this controller
current_path = self.controller.get_current_path()
if not current_path and new_list:
self.controller.update_list(new_list, 0)
self.load_and_fit_image()
return
if current_path in new_list:
idx = new_list.index(current_path)
self.controller.update_list(new_list, idx)
else:
self.controller.update_list(new_list)
if self.controller.get_current_path() != current_path:
self.load_and_fit_image()
class ImageViewer(QWidget): class ImageViewer(QWidget):
""" """
A standalone window for viewing and manipulating a single image. A standalone window for viewing and manipulating a single image.
@@ -1204,13 +1376,11 @@ class ImageViewer(QWidget):
self._wheel_scroll_accumulator = 0 self._wheel_scroll_accumulator = 0
self.filmstrip_loader = None self.filmstrip_loader = None
self.movie = None # Pane management
self.controller = ImageController(image_list, current_index, self.panes = []
initial_tags, initial_rating) self.active_pane = None
if self.main_win: self.panes_linked = True
self.controller.show_faces = self.main_win.show_faces
self.controller.metadata_changed.connect(self.on_metadata_changed)
self.controller.list_updated.connect(self.on_controller_list_updated)
self.fast_tag_manager = FastTagManager(self) self.fast_tag_manager = FastTagManager(self)
self._setup_shortcuts() self._setup_shortcuts()
self._setup_actions() self._setup_actions()
@@ -1230,14 +1400,16 @@ class ImageViewer(QWidget):
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0) self.layout.setSpacing(0)
self.scroll_area = QScrollArea() # Container for panes (Grid)
self.scroll_area.setAlignment(Qt.AlignCenter) self.view_container = QWidget()
self.scroll_area.setStyleSheet("background-color: #000; border: none;") self.grid_layout = QGridLayout(self.view_container)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.grid_layout.setContentsMargins(0, 0, 0, 0)
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.grid_layout.setSpacing(2)
# self.scroll_area = QScrollArea() ... Moved to ImagePane
# self.canvas = FaceCanvas(self) ... Moved to ImagePane
# self.scroll_area.setWidget(self.canvas)
self.canvas = FaceCanvas(self)
self.scroll_area.setWidget(self.canvas)
self.filmstrip = FilmStripWidget(self.controller) self.filmstrip = FilmStripWidget(self.controller)
self.filmstrip.setSpacing(2) self.filmstrip.setSpacing(2)
@@ -1262,7 +1434,7 @@ class ImageViewer(QWidget):
center_layout = QVBoxLayout(center_pane) center_layout = QVBoxLayout(center_pane)
center_layout.setContentsMargins(0, 0, 0, 0) center_layout.setContentsMargins(0, 0, 0, 0)
center_layout.setSpacing(0) center_layout.setSpacing(0)
center_layout.addWidget(self.scroll_area) center_layout.addWidget(self.view_container)
center_layout.addWidget(self.status_bar_container) center_layout.addWidget(self.status_bar_container)
self.filmstrip.setFixedWidth(120) self.filmstrip.setFixedWidth(120)
@@ -1304,10 +1476,10 @@ class ImageViewer(QWidget):
if filmstrip_position == 'top': if filmstrip_position == 'top':
self.layout.addWidget(self.filmstrip) self.layout.addWidget(self.filmstrip)
self.layout.addWidget(self.scroll_area) self.layout.addWidget(self.view_container)
self.layout.addWidget(self.status_bar_container) self.layout.addWidget(self.status_bar_container)
else: # bottom else: # bottom
self.layout.addWidget(self.scroll_area) self.layout.addWidget(self.view_container)
self.layout.addWidget(self.filmstrip) self.layout.addWidget(self.filmstrip)
self.layout.addWidget(self.status_bar_container) self.layout.addWidget(self.status_bar_container)
@@ -1330,6 +1502,16 @@ class ImageViewer(QWidget):
self.slideshow_manager = SlideshowManager(self) self.slideshow_manager = SlideshowManager(self)
self.zoom_manager = ZoomManager(self) self.zoom_manager = ZoomManager(self)
# Connect viewer-level zoom manager (triggered by shortcuts) to sync
self.zoom_manager.zoomed.connect(self._sync_zoom)
# Highlight frame for active pane
self.highlight = HighlightWidget(self.view_container)
# Initialize first pane
self.add_pane(image_list, current_index, initial_tags, initial_rating)
self.set_active_pane(self.panes[0])
# Load image # Load image
if restore_config: if restore_config:
# If restoring a layout, don't auto-fit to screen. Instead, use # If restoring a layout, don't auto-fit to screen. Instead, use
@@ -1341,6 +1523,140 @@ class ImageViewer(QWidget):
self.populate_filmstrip() self.populate_filmstrip()
self.load_and_fit_image() self.load_and_fit_image()
@property
def controller(self):
return self.active_pane.controller if self.active_pane else None
@property
def canvas(self):
return self.active_pane.canvas if self.active_pane else None
@property
def scroll_area(self):
return self.active_pane.scroll_area if self.active_pane else None
@property
def movie(self):
return self.active_pane.movie if self.active_pane else None
def add_pane(self, image_list, index, initial_tags, initial_rating):
pane = ImagePane(self, self.cache, image_list, index, initial_tags, initial_rating)
self.panes.append(pane)
self.update_grid_layout()
return pane
def set_active_pane(self, pane):
if pane in self.panes:
# Disconnect signals from previous active pane to avoid double-syncing
if self.active_pane:
try:
self.active_pane.scrolled.disconnect(self._sync_scroll)
self.active_pane.zoom_manager.zoomed.disconnect(self._sync_zoom)
except RuntimeError:
pass # Signal wasn't connected
self.active_pane = pane
# Connect new active pane signals
pane.scrolled.connect(self._sync_scroll)
pane.zoom_manager.zoomed.connect(self._sync_zoom)
self.filmstrip.controller = pane.controller
self.populate_filmstrip()
self.sync_filmstrip_selection(pane.controller.index)
self.update_status_bar()
self.update_highlight()
def _sync_scroll(self, x_pct, y_pct):
if len(self.panes) > 1 and self.panes_linked:
for pane in self.panes:
if pane != self.active_pane:
pane.set_scroll_relative(x_pct, y_pct)
def _sync_zoom(self, factor):
if len(self.panes) > 1 and self.panes_linked:
for pane in self.panes:
if pane != self.active_pane:
pane.controller.zoom_factor = factor
pane.update_view(resize_win=False)
# Re-apply relative scroll after zoom changes bounds
if self.active_pane:
h_bar = self.active_pane.scroll_area.horizontalScrollBar()
v_bar = self.active_pane.scroll_area.verticalScrollBar()
h_max = h_bar.maximum()
v_max = v_bar.maximum()
if h_max > 0 or v_max > 0:
x_pct = h_bar.value() / h_max if h_max > 0 else 0
y_pct = v_bar.value() / v_max if v_max > 0 else 0
pane.set_scroll_relative(x_pct, y_pct)
def update_grid_layout(self):
# Clear layout
for i in reversed(range(self.grid_layout.count())):
self.grid_layout.itemAt(i).widget().setParent(None)
count = len(self.panes)
if count == 1:
self.grid_layout.addWidget(self.panes[0], 0, 0)
elif count == 2:
self.grid_layout.addWidget(self.panes[0], 0, 0)
self.grid_layout.addWidget(self.panes[1], 0, 1)
elif count >= 3:
# 2x2 grid
for i, pane in enumerate(self.panes):
row = i // 2
col = i % 2
self.grid_layout.addWidget(pane, row, col)
self.update_highlight()
def set_comparison_mode(self, count):
current_panes = len(self.panes)
if count == current_panes:
return
if count > current_panes:
# Add panes
base_controller = self.active_pane.controller
start_idx = base_controller.index
img_list = base_controller.image_list
for i in range(count - current_panes):
new_idx = (start_idx + i + 1) % len(img_list)
pane = self.add_pane(img_list, new_idx, None, 0) # Metadata will load
pane.load_and_fit_image()
else:
# Remove panes (keep active if possible, else keep first)
while len(self.panes) > count:
# Remove the last one
pane = self.panes.pop()
pane.cleanup()
if pane == self.active_pane:
self.set_active_pane(self.panes[0])
self.update_grid_layout()
# Restore default behavior (auto-resize) if we go back to single view
if count == 1 and self.active_pane:
# Allow layout to settle before resizing window to ensure accurate sizing
QTimer.singleShot(0, lambda: self.active_pane.update_view(resize_win=True))
def toggle_link_panes(self):
"""Toggles the synchronized zoom/scroll for comparison mode."""
self.panes_linked = not self.panes_linked
self.update_status_bar()
def update_highlight(self):
if len(self.panes) > 1 and self.active_pane:
self.highlight.show()
self.highlight.raise_()
# Adjust geometry to active pane
self.highlight.setGeometry(self.active_pane.geometry())
else:
self.highlight.hide()
def resizeEvent(self, event):
super().resizeEvent(event)
self.update_highlight()
def reset_inactivity_timer(self): def reset_inactivity_timer(self):
"""Resets the inactivity timer and restores controls visibility.""" """Resets the inactivity timer and restores controls visibility."""
if self.isFullScreen(): if self.isFullScreen():
@@ -1406,6 +1722,10 @@ class ImageViewer(QWidget):
"toggle_visibility": self.toggle_main_window_visibility, "toggle_visibility": self.toggle_main_window_visibility,
"toggle_crop": self.toggle_crop_mode, "toggle_crop": self.toggle_crop_mode,
"save_crop": self.save_cropped_image, "save_crop": self.save_cropped_image,
"compare_1": lambda: self.set_comparison_mode(1),
"compare_2": lambda: self.set_comparison_mode(2),
"compare_4": lambda: self.set_comparison_mode(4),
"link_panes": self.toggle_link_panes,
} }
def _execute_action(self, action): def _execute_action(self, action):
@@ -1563,6 +1883,7 @@ class ImageViewer(QWidget):
""" """
kwinoutputconfig.json kwinoutputconfig.json
""" """
return self.screen().availableGeometry().width(), self.screen().availableGeometry().height()
# We run kscreen-doctor and look for the primary monitor line. # We run kscreen-doctor and look for the primary monitor line.
if FORCE_X11: if FORCE_X11:
if os.path.exists(KWINOUTPUTCONFIG_PATH): if os.path.exists(KWINOUTPUTCONFIG_PATH):
@@ -1621,59 +1942,55 @@ class ImageViewer(QWidget):
screen_geo = self.screen().availableGeometry() screen_geo = self.screen().availableGeometry()
return screen_geo.width(), screen_geo.height() return screen_geo.width(), screen_geo.height()
def load_and_fit_image(self, restore_config=None): def load_and_fit_image_for_pane(self, pane, restore_config=None):
""" """
Loads the current image and calculates an appropriate initial zoom level. Logic for loading image into a specific pane.
If restoring from a config, it applies the saved zoom and scroll.
Otherwise, it fits the image to the screen, respecting a defined ratio.
Args:
restore_config (dict, optional): State dictionary to restore from.
""" """
success, reloaded = self.controller.load_image() success, reloaded = pane.controller.load_image()
if not success: if not success:
if self.movie: if pane.movie:
self.movie.stop() pane.movie.stop()
self.movie = None pane.movie = None
self.canvas.setPixmap(QPixmap()) pane.canvas.setPixmap(QPixmap())
self.update_status_bar() if pane == self.active_pane:
self.update_status_bar()
return return
path = self.controller.get_current_path() path = pane.controller.get_current_path()
if reloaded: if reloaded:
if self.movie: if pane.movie:
self.movie.stop() pane.movie.stop()
self.movie = None pane.movie = None
self.canvas.crop_rect = QRect() # Clear crop rect on new image pane.canvas.crop_rect = QRect() # Clear crop rect on new image
if path: if path:
reader = QImageReader(path) reader = QImageReader(path)
if reader.supportsAnimation() and reader.imageCount() > 1: if reader.supportsAnimation() and reader.imageCount() > 1:
self.movie = QMovie(path) pane.movie = QMovie(path)
self.movie.setCacheMode(QMovie.CacheAll) pane.movie.setCacheMode(QMovie.CacheAll)
self.movie.frameChanged.connect(self._on_movie_frame) pane.movie.frameChanged.connect(
self.movie.start() lambda: self._on_movie_frame_for_pane(pane))
pane.movie.start()
self.reset_inactivity_timer() self.reset_inactivity_timer()
if restore_config: if restore_config:
self.controller.zoom_factor = restore_config.get("zoom", 1.0) pane.controller.zoom_factor = restore_config.get("zoom", 1.0)
self.controller.rotation = restore_config.get("rotation", 0) pane.controller.rotation = restore_config.get("rotation", 0)
self.controller.show_faces = restore_config.get( pane.controller.show_faces = restore_config.get(
"show_faces", self.controller.show_faces) "show_faces", pane.controller.show_faces)
self.status_bar_container.setVisible( self.status_bar_container.setVisible(
restore_config.get("status_bar_visible", False)) restore_config.get("status_bar_visible", False))
self.filmstrip.setVisible( self.filmstrip.setVisible(
restore_config.get("filmstrip_visible", False)) restore_config.get("filmstrip_visible", False))
if self.filmstrip.isVisible(): if self.filmstrip.isVisible():
self.populate_filmstrip() self.populate_filmstrip()
self.update_view(resize_win=False) pane.update_view(resize_win=False)
QTimer.singleShot(0, lambda: self.restore_scroll(restore_config)) QTimer.singleShot(0, lambda: self.restore_scroll_for_pane(pane, restore_config))
elif reloaded: elif reloaded:
# Calculate zoom to fit the image on the screen # Calculate zoom to fit the image on the screen
if self.isFullScreen(): if self.isFullScreen():
viewport = self.scroll_area.viewport() viewport = pane.scroll_area.viewport()
available_w = viewport.width() available_w = viewport.width()
available_h = viewport.height() available_h = viewport.height()
should_resize = False should_resize = False
@@ -1687,7 +2004,7 @@ class ImageViewer(QWidget):
else: else:
# Tried to guess # Tried to guess
screen_width, screen_height = self.get_desktop_resolution() screen_width, screen_height = self.get_desktop_resolution()
self._first_load = False if pane == self.panes[0]: self._first_load = False
else: else:
screen_geo = self.screen().availableGeometry() screen_geo = self.screen().availableGeometry()
screen_width = screen_geo.width() screen_width = screen_geo.width()
@@ -1720,8 +2037,14 @@ class ImageViewer(QWidget):
self.update_view(resize_win=False) self.update_view(resize_win=False)
# Defer sync to ensure layout and scroll area are ready, fixing navigation sync # Defer sync to ensure layout and scroll area are ready, fixing navigation sync
QTimer.singleShot( if pane == self.active_pane:
0, lambda: self.sync_filmstrip_selection(self.controller.index)) QTimer.singleShot(
0, lambda: self.sync_filmstrip_selection(pane.controller.index))
def load_and_fit_image(self, restore_config=None):
"""Proxy method for active pane."""
if self.active_pane:
self.active_pane.load_and_fit_image(restore_config)
@Slot(list) @Slot(list)
def update_image_list(self, new_list): def update_image_list(self, new_list):
@@ -1803,30 +2126,29 @@ class ImageViewer(QWidget):
if self.controller.pixmap_original.isNull(): if self.controller.pixmap_original.isNull():
return return
self.controller.rotate(rotation) self.controller.rotate(rotation)
self.update_view(resize_win) if self.active_pane:
self.active_pane.update_view(resize_win)
def update_view(self, resize_win=False): def update_view_for_pane(self, pane, resize_win=False):
""" """
Updates the canvas with the current pixmap, applying zoom and rotation. Updates the canvas with the current pixmap for a specific pane.
This is the main rendering method. It gets the transformed pixmap from
the controller and displays it.
Args:
resize_win (bool): If True, the window resizes to fit the image.
""" """
pixmap = self.controller.get_display_pixmap() pixmap = pane.controller.get_display_pixmap()
if pixmap.isNull(): if pixmap.isNull():
return return
self.canvas.setPixmap(pixmap) pane.canvas.setPixmap(pixmap)
self.canvas.adjustSize() pane.canvas.adjustSize()
# Disable resizing window in comparison mode (more than 1 pane)
if len(self.panes) > 1:
resize_win = False
if resize_win and APP_CONFIG.get("viewer_auto_resize_window", if resize_win and APP_CONFIG.get("viewer_auto_resize_window",
VIEWER_AUTO_RESIZE_WINDOW_DEFAULT): VIEWER_AUTO_RESIZE_WINDOW_DEFAULT):
# Adjust window size to content # Adjust window size to content
content_w = self.canvas.width() content_w = pane.canvas.width()
content_h = self.canvas.height() content_h = pane.canvas.height()
filmstrip_position = self.main_win.filmstrip_position \ filmstrip_position = self.main_win.filmstrip_position \
if self.main_win else 'bottom' if self.main_win else 'bottom'
@@ -1859,8 +2181,13 @@ class ImageViewer(QWidget):
target_h = min(target_h, avail_geo.height()) target_h = min(target_h, avail_geo.height())
self.resize(target_w, target_h) self.resize(target_w, target_h)
self.update_title() if pane == self.active_pane:
self.update_status_bar() self.update_title()
self.update_status_bar()
def update_view(self, resize_win=False):
if self.active_pane:
self.active_pane.update_view(resize_win)
def rename_current_image(self): def rename_current_image(self):
""" """
@@ -1919,16 +2246,17 @@ class ImageViewer(QWidget):
def toggle_crop_mode(self): def toggle_crop_mode(self):
"""Toggles the crop selection mode.""" """Toggles the crop selection mode."""
self.crop_mode = not self.crop_mode if self.active_pane:
self.canvas.crop_rect = QRect() self.active_pane.crop_mode = not self.active_pane.crop_mode
self.canvas.update() self.active_pane.canvas.crop_rect = QRect()
self.active_pane.canvas.update()
if self.crop_mode: if self.active_pane.crop_mode:
self.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()} [CROP]")
else: else:
self.setCursor(Qt.ArrowCursor) self.active_pane.canvas.setCursor(Qt.ArrowCursor)
self.update_status_bar() self.update_status_bar()
def show_crop_menu(self, global_pos): def show_crop_menu(self, global_pos):
"""Shows a context menu for the crop selection.""" """Shows a context menu for the crop selection."""
@@ -1940,19 +2268,20 @@ class ImageViewer(QWidget):
if res == save_action: if res == save_action:
self.save_cropped_image() self.save_cropped_image()
elif res == cancel_action: elif res == cancel_action:
self.canvas.crop_rect = QRect() if self.active_pane:
self.canvas.update() self.active_pane.canvas.crop_rect = QRect()
self.active_pane.canvas.update()
def save_cropped_image(self): def save_cropped_image(self):
"""Saves the area currently selected in crop mode as a new image.""" """Saves the area currently selected in crop mode as a new image."""
if not self.crop_mode or self.canvas.crop_rect.isNull(): if not self.active_pane or not self.active_pane.crop_mode or self.active_pane.canvas.crop_rect.isNull():
return return
# Get normalized coordinates from the canvas rect # Get normalized coordinates from the canvas rect
nx, ny, nw, nh = self.canvas.map_to_source(self.canvas.crop_rect) nx, ny, nw, nh = self.active_pane.canvas.map_to_source(self.active_pane.canvas.crop_rect)
# Use original pixmap to extract high-quality crop # Use original pixmap to extract high-quality crop
orig = self.controller.pixmap_original orig = self.active_pane.controller.pixmap_original
if orig.isNull(): if orig.isNull():
return return
@@ -1983,8 +2312,8 @@ class ImageViewer(QWidget):
if file_name: if file_name:
cropped.save(file_name) cropped.save(file_name)
# Optionally stay in crop mode or exit # Optionally stay in crop mode or exit
self.canvas.crop_rect = QRect() self.active_pane.canvas.crop_rect = QRect()
self.canvas.update() self.active_pane.canvas.update()
def update_title(self): def update_title(self):
"""Updates the window title with the current image name.""" """Updates the window title with the current image name."""
@@ -2016,7 +2345,12 @@ class ImageViewer(QWidget):
w = self.controller.pixmap_original.width() w = self.controller.pixmap_original.width()
h = self.controller.pixmap_original.height() h = self.controller.pixmap_original.height()
zoom = int(self.controller.zoom_factor * 100) zoom = int(self.controller.zoom_factor * 100)
self.sb_info_label.setText(f"{w} x {h} px | {zoom}%") info_text = f"{w} x {h} px | {zoom}%"
if len(self.panes) > 1:
info_text += " [Linked]" if self.panes_linked else " [Unlinked]"
self.sb_info_label.setText(info_text)
# Use tags from metadata if provided (priority to avoid race conditions), # Use tags from metadata if provided (priority to avoid race conditions),
# otherwise fallback to controller's internal state. # otherwise fallback to controller's internal state.
@@ -2039,15 +2373,15 @@ class ImageViewer(QWidget):
if self.main_win: if self.main_win:
self.main_win.update_metadata_for_path(path, metadata) self.main_win.update_metadata_for_path(path, metadata)
def restore_scroll(self, config): def restore_scroll_for_pane(self, pane, config):
""" """
Applies the saved scrollbar positions from a layout configuration. Applies the saved scrollbar positions from a layout configuration.
Args: Args:
config (dict): The layout configuration dictionary. config (dict): The layout configuration dictionary.
""" """
self.scroll_area.horizontalScrollBar().setValue(config.get("scroll_x", 0)) pane.scroll_area.horizontalScrollBar().setValue(config.get("scroll_x", 0))
self.scroll_area.verticalScrollBar().setValue(config.get("scroll_y", 0)) pane.scroll_area.verticalScrollBar().setValue(config.get("scroll_y", 0))
def get_state(self): def get_state(self):
""" """
@@ -2069,8 +2403,8 @@ class ImageViewer(QWidget):
"show_faces": self.controller.show_faces, "show_faces": self.controller.show_faces,
"flip_h": self.controller.flip_h, "flip_h": self.controller.flip_h,
"flip_v": self.controller.flip_v, "flip_v": self.controller.flip_v,
"scroll_x": self.scroll_area.horizontalScrollBar().value(), "scroll_x": self.scroll_area.horizontalScrollBar().value() if self.scroll_area else 0,
"scroll_y": self.scroll_area.verticalScrollBar().value(), "scroll_y": self.scroll_area.verticalScrollBar().value() if self.scroll_area else 0,
"status_bar_visible": self.status_bar_container.isVisible(), "status_bar_visible": self.status_bar_container.isVisible(),
"filmstrip_visible": self.filmstrip.isVisible() "filmstrip_visible": self.filmstrip.isVisible()
} }
@@ -2142,10 +2476,10 @@ class ImageViewer(QWidget):
"""Re-loads shortcuts from the main window configuration.""" """Re-loads shortcuts from the main window configuration."""
self._setup_shortcuts() self._setup_shortcuts()
def _get_clicked_face(self, pos): def _get_clicked_face_for_pane(self, pane, pos):
"""Checks if a click position is inside any face bounding box.""" """Checks if a click position is inside any face bounding box."""
for face in self.controller.faces: for face in pane.controller.faces:
rect = self.canvas.map_from_source(face) rect = pane.canvas.map_from_source(face)
if rect.contains(pos): if rect.contains(pos):
return face return face
return None return None
@@ -2158,8 +2492,8 @@ class ImageViewer(QWidget):
if not self.controller.show_faces: if not self.controller.show_faces:
return False return False
pos = self.canvas.mapFromGlobal(event.globalPos()) pos = self.canvas.mapFromGlobal(event.globalPos()) if self.canvas else QPoint()
clicked_face = self._get_clicked_face(pos) clicked_face = self.active_pane._get_clicked_face(pos) if self.active_pane else None
if not clicked_face: if not clicked_face:
return False return False
@@ -2177,7 +2511,8 @@ class ImageViewer(QWidget):
for f in self.controller.faces) for f in self.controller.faces)
if not has_other: if not has_other:
self.controller.toggle_tag(face_name, False) self.controller.toggle_tag(face_name, False)
self.canvas.update() if self.canvas:
self.canvas.update()
elif res == action_ren: elif res == action_ren:
self.rename_face(clicked_face) self.rename_face(clicked_face)
return True return True
@@ -2234,7 +2569,8 @@ class ImageViewer(QWidget):
# Save changes and add new tag # Save changes and add new tag
self.controller.save_faces() self.controller.save_faces()
self.controller.toggle_tag(new_full_tag, True) self.controller.toggle_tag(new_full_tag, True)
self.canvas.update() if self.canvas:
self.canvas.update()
def toggle_main_window_visibility(self): def toggle_main_window_visibility(self):
"""Toggles the visibility of the main window.""" """Toggles the visibility of the main window."""
@@ -2301,8 +2637,15 @@ class ImageViewer(QWidget):
"icon": "zoom-fit-best"}, "icon": "zoom-fit-best"},
"separator", "separator",
{"text": UITexts.VIEWER_MENU_CROP, "action": "toggle_crop", {"text": UITexts.VIEWER_MENU_CROP, "action": "toggle_crop",
"icon": "transform-crop", "checkable": True, "icon": "transform-crop", "checkable": True}, # checked updated later
"checked": self.crop_mode}, "separator",
{"text": UITexts.VIEWER_MENU_COMPARE, "icon": "view-grid", "submenu": [
{"text": UITexts.VIEWER_MENU_COMPARE_1, "action": "compare_1", "icon": "view-restore"},
{"text": UITexts.VIEWER_MENU_COMPARE_2, "action": "compare_2", "icon": "view-split-left-right"},
{"text": UITexts.VIEWER_MENU_COMPARE_4, "action": "compare_4", "icon": "view-grid"},
"separator",
{"text": UITexts.VIEWER_MENU_LINK_PANES, "action": "link_panes", "icon": "object-link", "checkable": True, "checked": self.panes_linked}
]},
"separator", "separator",
] ]
@@ -2465,9 +2808,9 @@ class ImageViewer(QWidget):
Args: Args:
event (QContextMenuEvent): The context menu event. event (QContextMenuEvent): The context menu event.
""" """
if self.crop_mode and not self.canvas.crop_rect.isNull(): if self.active_pane and self.active_pane.crop_mode and not self.active_pane.canvas.crop_rect.isNull():
pos = self.canvas.mapFromGlobal(event.globalPos()) pos = self.active_pane.canvas.mapFromGlobal(event.globalPos())
if self.canvas.crop_rect.contains(pos): if self.active_pane.canvas.crop_rect.contains(pos):
self.show_crop_menu(event.globalPos()) self.show_crop_menu(event.globalPos())
return return
@@ -2505,12 +2848,14 @@ class ImageViewer(QWidget):
self.toggle_faces() self.toggle_faces()
self.controller.faces.append(new_face) self.controller.faces.append(new_face)
self.canvas.update() if self.canvas:
self.canvas.update()
w = self.canvas.width() w = self.canvas.width() if self.canvas else 0
h = self.canvas.height() h = self.canvas.height() if self.canvas else 0
self.scroll_area.ensureVisible(int(new_face.get('x', 0) * w), if self.scroll_area:
int(new_face.get('y', 0) * h), 50, 50) self.scroll_area.ensureVisible(int(new_face.get('x', 0) * w),
int(new_face.get('y', 0) * h), 50, 50)
QApplication.processEvents() QApplication.processEvents()
history = self.main_win.face_names_history if self.main_win else [] history = self.main_win.face_names_history if self.main_win else []
@@ -2526,7 +2871,8 @@ class ImageViewer(QWidget):
else: else:
# If user cancels, remove the face that was temporarily added # If user cancels, remove the face that was temporarily added
self.controller.faces.pop() self.controller.faces.pop()
self.canvas.update() if self.canvas:
self.canvas.update()
if added_count > 0: if added_count > 0:
self.controller.save_faces() self.controller.save_faces()
@@ -2559,12 +2905,14 @@ class ImageViewer(QWidget):
self.toggle_faces() self.toggle_faces()
self.controller.faces.append(new_pet) self.controller.faces.append(new_pet)
self.canvas.update() if self.canvas:
self.canvas.update()
w = self.canvas.width() w = self.canvas.width() if self.canvas else 0
h = self.canvas.height() h = self.canvas.height() if self.canvas else 0
self.scroll_area.ensureVisible(int(new_pet.get('x', 0) * w), if self.scroll_area:
int(new_pet.get('y', 0) * h), 50, 50) self.scroll_area.ensureVisible(int(new_pet.get('x', 0) * w),
int(new_pet.get('y', 0) * h), 50, 50)
QApplication.processEvents() QApplication.processEvents()
history = self.main_win.pet_names_history if self.main_win else [] history = self.main_win.pet_names_history if self.main_win else []
@@ -2579,7 +2927,8 @@ class ImageViewer(QWidget):
added_count += 1 added_count += 1
else: else:
self.controller.faces.pop() self.controller.faces.pop()
self.canvas.update() if self.canvas:
self.canvas.update()
if added_count > 0: if added_count > 0:
self.controller.save_faces() self.controller.save_faces()
@@ -2612,12 +2961,14 @@ class ImageViewer(QWidget):
self.toggle_faces() self.toggle_faces()
self.controller.faces.append(new_body) self.controller.faces.append(new_body)
self.canvas.update() if self.canvas:
self.canvas.update()
w = self.canvas.width() w = self.canvas.width() if self.canvas else 0
h = self.canvas.height() h = self.canvas.height() if self.canvas else 0
self.scroll_area.ensureVisible(int(new_body.get('x', 0) * w), if self.scroll_area:
int(new_body.get('y', 0) * h), 50, 50) self.scroll_area.ensureVisible(int(new_body.get('x', 0) * w),
int(new_body.get('y', 0) * h), 50, 50)
QApplication.processEvents() QApplication.processEvents()
# For bodies, we typically don't ask for a name immediately unless desired # For bodies, we typically don't ask for a name immediately unless desired
@@ -2634,7 +2985,8 @@ class ImageViewer(QWidget):
added_count += 1 added_count += 1
else: else:
self.controller.faces.pop() self.controller.faces.pop()
self.canvas.update() if self.canvas:
self.canvas.update()
if added_count > 0: if added_count > 0:
self.controller.save_faces() self.controller.save_faces()
@@ -2659,11 +3011,14 @@ class ImageViewer(QWidget):
def toggle_faces(self): def toggle_faces(self):
"""Toggles the display of face regions.""" """Toggles the display of face regions."""
self.controller.show_faces = not self.controller.show_faces for pane in self.panes:
pane.controller.show_faces = not pane.controller.show_faces
pane.canvas.update()
if self.main_win: if self.main_win:
self.main_win.show_faces = self.controller.show_faces if self.active_pane:
self.main_win.show_faces = self.active_pane.controller.show_faces
self.main_win.save_config() self.main_win.save_config()
self.canvas.update()
def show_fast_tag_menu(self): def show_fast_tag_menu(self):
"""Shows a context menu for quickly adding/removing tags.""" """Shows a context menu for quickly adding/removing tags."""
@@ -2697,10 +3052,9 @@ class ImageViewer(QWidget):
if event.modifiers() & Qt.ControlModifier: if event.modifiers() & Qt.ControlModifier:
# Zoom with Ctrl + Wheel # Zoom with Ctrl + Wheel
if event.angleDelta().y() > 0: if event.angleDelta().y() > 0:
self.controller.zoom_factor *= 1.1 self.zoom_manager.zoom(1.1)
else: else:
self.controller.zoom_factor *= 0.9 self.zoom_manager.zoom(0.9)
self.update_view(resize_win=True)
else: else:
# Navigate next/previous based on configurable speed # Navigate next/previous based on configurable speed
speed = APP_CONFIG.get("viewer_wheel_speed", VIEWER_WHEEL_SPEED_DEFAULT) speed = APP_CONFIG.get("viewer_wheel_speed", VIEWER_WHEEL_SPEED_DEFAULT)