This commit is contained in:
Ignacio Serantes
2026-03-31 23:35:57 +02:00
parent ff7c1aa373
commit cb751b2970
14 changed files with 2431 additions and 119 deletions

View File

@@ -419,11 +419,22 @@ class FaceCanvas(QLabel):
self.edit_handle = None
self.edit_start_rect = QRect()
self.resize_margin = 8
# Zoom indicator
self.zoom_indicator_point = None
self.zoom_indicator_timer = QTimer(self)
self.zoom_indicator_timer.setSingleShot(True)
self.zoom_indicator_timer.setInterval(500) # Show for 500ms
self.zoom_indicator_timer.timeout.connect(self._clear_zoom_indicator)
self.crop_rect = QRect()
self.crop_handle = None
self.crop_start_pos = QPoint()
self.crop_start_rect = QRect()
def _clear_zoom_indicator(self):
self.zoom_indicator_point = None
self.update()
def map_from_source(self, face_data):
"""Maps original normalized face data to current canvas QRect."""
nx = face_data.get('x', 0)
@@ -623,6 +634,16 @@ class FaceCanvas(QLabel):
painter.drawRect(pt.x() - offset, pt.y() - offset,
handle_size, handle_size)
# Draw zoom indicator
if self.zoom_indicator_point:
painter.setPen(QPen(QColor(255, 255, 0), 2)) # Yellow crosshair
painter.drawLine(self.zoom_indicator_point.x() - 10,
self.zoom_indicator_point.y(),
self.zoom_indicator_point.x() + 10,
self.zoom_indicator_point.y())
painter.drawLine(self.zoom_indicator_point.x(), self.zoom_indicator_point.y() - 10,
self.zoom_indicator_point.x(), self.zoom_indicator_point.y() + 10)
def _hit_test(self, pos):
"""Determines if the mouse is over a name, handle, or body."""
if not self.controller.show_faces:
@@ -1122,18 +1143,59 @@ class ZoomManager(QObject):
super().__init__(viewer)
self.viewer = viewer
def zoom(self, factor, reset=False):
"""Applies zoom to the image."""
def zoom(self, factor=1.1, reset=False, focus_point=None, absolute_factor=None):
"""Applies zoom to the image, centering on focus_point if provided."""
if not self.viewer.controller or self.viewer.controller.pixmap_original.isNull():
return
c_point = None
if reset:
self.viewer.controller.zoom_factor = 1.0
self.viewer.update_view(resize_win=True)
if self.viewer.canvas:
c_point = self.viewer.canvas.rect().center()
elif absolute_factor is not None: # New: set absolute zoom factor
self.viewer.controller.zoom_factor = absolute_factor
self.viewer.update_view(resize_win=False) # Don't resize window for sync zoom
if focus_point is not None and self.viewer.canvas:
scroll_area = self.viewer.scroll_area
viewport = scroll_area.viewport()
v_point = viewport.mapFrom(self.viewer, focus_point)
c_point = self.viewer.canvas.mapFrom(viewport, v_point)
else:
# 1. Determinar el punto de enfoque en coordenadas del viewport
scroll_area = self.viewer.scroll_area
viewport = scroll_area.viewport()
if focus_point is None:
v_point = viewport.rect().center()
else:
# focus_point es relativo al widget self.viewer (ImageViewer o ImagePane)
v_point = viewport.mapFrom(self.viewer, focus_point)
# 2. Mapear el punto de enfoque a coordenadas del canvas antes del zoom
c_point = self.viewer.canvas.mapFrom(viewport, v_point)
self.viewer.controller.zoom_factor *= factor
self.viewer.update_view(resize_win=True)
# Aplicar la actualización (esto redimensiona el canvas)
self.viewer.update_view(resize_win=(not self.viewer.isFullScreen()))
# 3. Ajustar las barras de desplazamiento para mantener el píxel bajo el cursor
scroll_area.horizontalScrollBar().setValue(
int(c_point.x() * factor - v_point.x()))
scroll_area.verticalScrollBar().setValue(
int(c_point.y() * factor - v_point.y()))
# Notify the main window that the image (and possibly index) has changed
# so it can update its selection.
self.viewer.index_changed.emit(self.viewer.controller.index)
if focus_point is not None and self.viewer.canvas:
self.viewer.canvas.zoom_indicator_point = c_point
self.viewer.canvas.zoom_indicator_timer.start()
self.viewer.canvas.update()
self.zoomed.emit(self.viewer.controller.zoom_factor)
if hasattr(self.viewer, 'sync_filmstrip_selection'):
self.viewer.sync_filmstrip_selection(self.viewer.controller.index)
@@ -1645,16 +1707,21 @@ class ImageViewer(QWidget):
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)
# Re-apply relative scroll after zoom changes bounds
# We defer this to the next event loop iteration to ensure
# that QScrollArea has updated its scrollbar maximums.
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()
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
for pane in self.panes:
if pane != self.active_pane:
QTimer.singleShot(0, lambda p=pane, x=x_pct, y=y_pct: p.set_scroll_relative(x, y))
def update_grid_layout(self):
# Clear layout
@@ -1693,6 +1760,8 @@ class ImageViewer(QWidget):
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
if self.panes_linked and self.active_pane:
pane.controller.zoom_factor = self.active_pane.controller.zoom_factor
pane.load_and_fit_image()
else:
# Remove panes (keep active if possible, else keep first)
@@ -1710,10 +1779,13 @@ class ImageViewer(QWidget):
# sizing
QTimer.singleShot(
0, lambda: self.active_pane.update_view(resize_win=True))
self.adjustSize() # Ajustar el tamaño de la ventana después de añadir/eliminar paneles
def toggle_link_panes(self):
"""Toggles the synchronized zoom/scroll for comparison mode."""
self.panes_linked = not self.panes_linked
if self.panes_linked and self.active_pane:
self._sync_zoom(self.active_pane.controller.zoom_factor)
self.update_status_bar()
def update_highlight(self):
@@ -1731,6 +1803,9 @@ class ImageViewer(QWidget):
def reset_inactivity_timer(self):
"""Resets the inactivity timer and restores controls visibility."""
if self.active_pane and self.active_pane.canvas:
self.active_pane.canvas._clear_zoom_indicator()
if self.isFullScreen():
self.unsetCursor()
if self.main_win and self.main_win.show_viewer_status_bar:
@@ -2110,8 +2185,12 @@ class ImageViewer(QWidget):
available_h -= self.status_bar_container.sizeHint().height()
should_resize = True
self.zoom_manager.calculate_initial_zoom(available_w, available_h,
self.isFullScreen())
if self.panes_linked and self.active_pane and pane != self.active_pane:
# Inherit zoom from active pane instead of recalculating
pane.controller.zoom_factor = self.active_pane.controller.zoom_factor
else:
pane.zoom_manager.calculate_initial_zoom(available_w, available_h,
self.isFullScreen())
self.update_view(resize_win=should_resize)
else:
@@ -3219,10 +3298,11 @@ class ImageViewer(QWidget):
self.reset_inactivity_timer()
if event.modifiers() & Qt.ControlModifier:
# Zoom with Ctrl + Wheel
focus_pos = event.position().toPoint()
if event.angleDelta().y() > 0:
self.zoom_manager.zoom(1.1)
self.zoom_manager.zoom(1.1, focus_point=focus_pos)
else:
self.zoom_manager.zoom(0.9)
self.zoom_manager.zoom(0.9, focus_point=focus_pos)
else:
# Navigate next/previous based on configurable speed
speed = APP_CONFIG.get("viewer_wheel_speed", VIEWER_WHEEL_SPEED_DEFAULT)