From 8f1e9068846a3bd8a097488b0ae70a4293cea03d Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Mon, 1 Dec 2025 20:40:11 +0100 Subject: [PATCH] justified test and two finger scrolling --- pyPhotoAlbum/gl_widget.py | 186 +++++++++++++++++++++++++++++++ pyPhotoAlbum/mixins/rendering.py | 2 + pyPhotoAlbum/pdf_exporter.py | 3 +- pyPhotoAlbum/text_edit_dialog.py | 2 +- tests/test_rendering_mixin.py | 2 +- tests/test_text_edit_dialog.py | 1 + 6 files changed, 193 insertions(+), 3 deletions(-) diff --git a/pyPhotoAlbum/gl_widget.py b/pyPhotoAlbum/gl_widget.py index ede094c..d010737 100644 --- a/pyPhotoAlbum/gl_widget.py +++ b/pyPhotoAlbum/gl_widget.py @@ -77,6 +77,12 @@ class GLWidget( self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.setFocus() + # Enable gesture support for pinch-to-zoom + self.grabGesture(Qt.GestureType.PinchGesture) + + # Track pinch gesture state + self._pinch_scale_factor = 1.0 + def window(self): """Override window() to return stored main_window reference. @@ -154,3 +160,183 @@ class GLWidget( else: super().keyPressEvent(event) + + def event(self, event): + """Handle gesture events for pinch-to-zoom""" + from PyQt6.QtCore import QEvent, Qt as QtCore + from PyQt6.QtWidgets import QPinchGesture + from PyQt6.QtGui import QNativeGestureEvent + + # Handle native touchpad gestures (Linux, macOS) + if event.type() == QEvent.Type.NativeGesture: + native_event = event + gesture_type = native_event.gestureType() + + print(f"DEBUG: Native gesture detected - type: {gesture_type}") + + # Check for zoom/pinch gesture + if gesture_type == QtCore.NativeGestureType.ZoomNativeGesture: + # Get zoom value (typically a delta around 0) + value = native_event.value() + print(f"DEBUG: Zoom value: {value}") + + # Convert to scale factor (value is typically small, like -0.1 to 0.1) + # Positive value = zoom in, negative = zoom out + scale_factor = 1.0 + value + + # Get the position of the gesture + pos = native_event.position() + mouse_x = pos.x() + mouse_y = pos.y() + + self._apply_zoom_at_point(mouse_x, mouse_y, scale_factor) + return True + + # Check for pan gesture (two-finger drag) + elif gesture_type == QtCore.NativeGestureType.PanNativeGesture: + # Get the pan delta + delta = native_event.delta() + dx = delta.x() + dy = delta.y() + + print(f"DEBUG: Pan delta: dx={dx}, dy={dy}") + + # Apply pan + self.pan_offset[0] += dx + self.pan_offset[1] += dy + + # Clamp pan offset to content bounds + if hasattr(self, "clamp_pan_offset"): + self.clamp_pan_offset() + + self.update() + + # Update scrollbars if available + main_window = self.window() + if hasattr(main_window, "update_scrollbars"): + main_window.update_scrollbars() + + return True + + # Handle Qt gesture events (fallback for other platforms) + elif event.type() == QEvent.Type.Gesture: + print("DEBUG: Qt Gesture event detected") + gesture_event = event + pinch = gesture_event.gesture(Qt.GestureType.PinchGesture) + + if pinch: + print(f"DEBUG: Pinch gesture detected - state: {pinch.state()}, scale: {pinch.totalScaleFactor()}") + self._handle_pinch_gesture(pinch) + return True + + return super().event(event) + + def _handle_pinch_gesture(self, pinch): + """Handle pinch gesture for zooming""" + from PyQt6.QtCore import Qt as QtCore + + # Check gesture state + state = pinch.state() + + if state == QtCore.GestureState.GestureStarted: + # Reset scale factor at gesture start + self._pinch_scale_factor = 1.0 + return + + elif state == QtCore.GestureState.GestureUpdated: + # Get current total scale factor + current_scale = pinch.totalScaleFactor() + + # Calculate incremental change from last update + if current_scale > 0: + scale_change = current_scale / self._pinch_scale_factor + self._pinch_scale_factor = current_scale + + # Get the center point of the pinch gesture + center_point = pinch.centerPoint() + mouse_x = center_point.x() + mouse_y = center_point.y() + + # Calculate world coordinates at the pinch center + world_x = (mouse_x - self.pan_offset[0]) / self.zoom_level + world_y = (mouse_y - self.pan_offset[1]) / self.zoom_level + + # Apply incremental zoom change + new_zoom = self.zoom_level * scale_change + + # Clamp zoom level to reasonable bounds + if 0.1 <= new_zoom <= 5.0: + old_pan_x = self.pan_offset[0] + old_pan_y = self.pan_offset[1] + + self.zoom_level = new_zoom + + # Adjust pan offset to keep the pinch center point fixed + self.pan_offset[0] = mouse_x - world_x * self.zoom_level + self.pan_offset[1] = mouse_y - world_y * self.zoom_level + + # If dragging, adjust drag_start_pos to account for pan_offset change + if hasattr(self, 'is_dragging') and self.is_dragging and hasattr(self, 'drag_start_pos') and self.drag_start_pos: + pan_delta_x = self.pan_offset[0] - old_pan_x + pan_delta_y = self.pan_offset[1] - old_pan_y + self.drag_start_pos = (self.drag_start_pos[0] + pan_delta_x, self.drag_start_pos[1] + pan_delta_y) + + # Clamp pan offset to content bounds + if hasattr(self, "clamp_pan_offset"): + self.clamp_pan_offset() + + self.update() + + # Update status bar + main_window = self.window() + if hasattr(main_window, "status_bar"): + main_window.status_bar.showMessage(f"Zoom: {int(self.zoom_level * 100)}%", 2000) + + # Update scrollbars if available + if hasattr(main_window, "update_scrollbars"): + main_window.update_scrollbars() + + elif state == QtCore.GestureState.GestureFinished or state == QtCore.GestureState.GestureCanceled: + # Reset on gesture end + self._pinch_scale_factor = 1.0 + + def _apply_zoom_at_point(self, mouse_x, mouse_y, scale_factor): + """Apply zoom centered at a specific point""" + # Calculate world coordinates at the zoom center + world_x = (mouse_x - self.pan_offset[0]) / self.zoom_level + world_y = (mouse_y - self.pan_offset[1]) / self.zoom_level + + # Apply zoom + new_zoom = self.zoom_level * scale_factor + + # Clamp zoom level to reasonable bounds + if 0.1 <= new_zoom <= 5.0: + old_pan_x = self.pan_offset[0] + old_pan_y = self.pan_offset[1] + + self.zoom_level = new_zoom + + # Adjust pan offset to keep the zoom center point fixed + self.pan_offset[0] = mouse_x - world_x * self.zoom_level + self.pan_offset[1] = mouse_y - world_y * self.zoom_level + + # If dragging, adjust drag_start_pos to account for pan_offset change + if hasattr(self, 'is_dragging') and self.is_dragging and hasattr(self, 'drag_start_pos') and self.drag_start_pos: + pan_delta_x = self.pan_offset[0] - old_pan_x + pan_delta_y = self.pan_offset[1] - old_pan_y + self.drag_start_pos = (self.drag_start_pos[0] + pan_delta_x, self.drag_start_pos[1] + pan_delta_y) + + # Clamp pan offset to content bounds + if hasattr(self, "clamp_pan_offset"): + self.clamp_pan_offset() + + self.update() + + # Update status bar + main_window = self.window() + if hasattr(main_window, "status_bar"): + main_window.status_bar.showMessage(f"Zoom: {int(self.zoom_level * 100)}%", 2000) + + # Update scrollbars if available + if hasattr(main_window, "update_scrollbars"): + main_window.update_scrollbars() diff --git a/pyPhotoAlbum/mixins/rendering.py b/pyPhotoAlbum/mixins/rendering.py index 4c47729..b131ee7 100644 --- a/pyPhotoAlbum/mixins/rendering.py +++ b/pyPhotoAlbum/mixins/rendering.py @@ -281,6 +281,8 @@ class RenderingMixin: alignment = Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop elif element.alignment == "right": alignment = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop + elif element.alignment == "justify": + alignment = Qt.AlignmentFlag.AlignJustify | Qt.AlignmentFlag.AlignTop text_flags = Qt.TextFlag.TextWordWrap diff --git a/pyPhotoAlbum/pdf_exporter.py b/pyPhotoAlbum/pdf_exporter.py index 3fc4e78..d609b80 100644 --- a/pyPhotoAlbum/pdf_exporter.py +++ b/pyPhotoAlbum/pdf_exporter.py @@ -10,7 +10,7 @@ from reportlab.pdfgen import canvas from reportlab.lib.utils import ImageReader from reportlab.platypus import Paragraph from reportlab.lib.styles import ParagraphStyle -from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY from PIL import Image import math from pyPhotoAlbum.models import ImageData, TextBoxData, PlaceholderData @@ -575,6 +575,7 @@ class PDFExporter: "left": TA_LEFT, "center": TA_CENTER, "right": TA_RIGHT, + "justify": TA_JUSTIFY, } text_alignment = alignment_map.get(text_element.alignment, TA_LEFT) diff --git a/pyPhotoAlbum/text_edit_dialog.py b/pyPhotoAlbum/text_edit_dialog.py index df74fe4..88c03eb 100644 --- a/pyPhotoAlbum/text_edit_dialog.py +++ b/pyPhotoAlbum/text_edit_dialog.py @@ -74,7 +74,7 @@ class TextEditDialog(QDialog): alignment_layout = QHBoxLayout() alignment_layout.addWidget(QLabel("Alignment:")) self.alignment_combo = QComboBox() - self.alignment_combo.addItems(["left", "center", "right"]) + self.alignment_combo.addItems(["left", "center", "right", "justify"]) alignment_layout.addWidget(self.alignment_combo) alignment_layout.addStretch() layout.addLayout(alignment_layout) diff --git a/tests/test_rendering_mixin.py b/tests/test_rendering_mixin.py index e6eba7d..e5c1eb4 100644 --- a/tests/test_rendering_mixin.py +++ b/tests/test_rendering_mixin.py @@ -496,7 +496,7 @@ class TestRenderTextOverlays: qtbot.addWidget(widget) widget.resize(1000, 800) - for alignment in ["left", "center", "right"]: + for alignment in ["left", "center", "right", "justify"]: # Create text element with alignment text_element = TextBoxData( x=50, y=50, width=200, height=100, diff --git a/tests/test_text_edit_dialog.py b/tests/test_text_edit_dialog.py index febf5f4..7978344 100644 --- a/tests/test_text_edit_dialog.py +++ b/tests/test_text_edit_dialog.py @@ -178,6 +178,7 @@ class TestTextEditDialogUI: assert "left" in options assert "center" in options assert "right" in options + assert "justify" in options def test_font_options(self, qtbot): """Test that font combo has expected fonts"""