pyPhotoAlbum/pyPhotoAlbum/mixins/element_manipulation.py
2026-01-01 17:47:58 +00:00

178 lines
6.3 KiB
Python

"""
Element manipulation mixin for GLWidget - handles element transformations
"""
from typing import TYPE_CHECKING, Any, Optional, Tuple
if TYPE_CHECKING:
from pyPhotoAlbum.models import BaseLayoutElement
from PyQt6.QtWidgets import QMainWindow
class ElementManipulationMixin:
# Type hints for expected attributes from mixing class
selected_element: Optional["BaseLayoutElement"]
drag_start_pos: Optional[Tuple[float, float]]
drag_start_element_pos: Optional[Tuple[float, float]]
def window(self) -> "QMainWindow":
"""Expected from QWidget"""
...
def __init__(self, *args, **kwargs):
"""
Initialize element manipulation mixin.
This mixin provides element transformation functionality including
resizing, rotating, moving elements, snapping support and cross-page
element transfers.
"""
super().__init__(*args, **kwargs)
# Resize state
self.resize_handle: Optional[str] = None # 'nw', 'ne', 'sw', 'se'
self.resize_start_pos: Optional[Tuple[float, float]] = None
self.resize_start_size: Optional[Tuple[float, float]] = None
# Rotation state
self.rotation_mode: bool = False # Toggle between move/resize and rotation modes
self.rotation_start_angle: Optional[float] = None
self.rotation_snap_angle: int = 15 # Default snap angle in degrees
# Snap state tracking
self.snap_state = {"is_snapped": False, "last_position": None, "last_size": None}
def _resize_element(self, dx: float, dy: float):
"""
Resize the element based on the resize handle.
Args:
dx: Delta X in page-local coordinates
dy: Delta Y in page-local coordinates
"""
if not self.selected_element or not self.resize_handle:
return
if not self.resize_start_pos or not self.resize_start_size:
return
# Get the snapping system from the element's parent page
main_window = self.window()
if not hasattr(self.selected_element, "_parent_page"):
self._resize_element_no_snap(dx, dy)
return
parent_page = self.selected_element._parent_page
snap_sys = parent_page.layout.snapping_system
# Get page size
page_size = parent_page.layout.size
dpi = main_window.project.working_dpi
# Apply snapping to resize
from pyPhotoAlbum.snapping import SnapResizeParams
params = SnapResizeParams(
position=self.resize_start_pos,
size=self.resize_start_size,
dx=dx,
dy=dy,
resize_handle=self.resize_handle,
page_size=page_size,
dpi=dpi,
project=main_window.project,
)
new_pos, new_size = snap_sys.snap_resize(params)
# Apply the snapped values
self.selected_element.position = new_pos
self.selected_element.size = new_size
# Ensure minimum size
min_size = 20
w, h = self.selected_element.size
if w < min_size or h < min_size:
w = max(w, min_size)
h = max(h, min_size)
self.selected_element.size = (w, h)
def _resize_element_no_snap(self, dx: float, dy: float):
"""
Resize element without snapping.
Args:
dx: Delta X in page-local coordinates
dy: Delta Y in page-local coordinates
"""
if not self.resize_start_pos or not self.resize_start_size:
return
start_x, start_y = self.resize_start_pos
start_w, start_h = self.resize_start_size
if self.resize_handle == "nw":
self.selected_element.position = (start_x + dx, start_y + dy)
self.selected_element.size = (start_w - dx, start_h - dy)
elif self.resize_handle == "ne":
self.selected_element.position = (start_x, start_y + dy)
self.selected_element.size = (start_w + dx, start_h - dy)
elif self.resize_handle == "sw":
self.selected_element.position = (start_x + dx, start_y)
self.selected_element.size = (start_w - dx, start_h + dy)
elif self.resize_handle == "se":
self.selected_element.size = (start_w + dx, start_h + dy)
# Ensure minimum size
min_size = 20
w, h = self.selected_element.size
if w < min_size:
self.selected_element.size = (min_size, h)
if h < min_size:
w, _ = self.selected_element.size
self.selected_element.size = (w, min_size)
def _transfer_element_to_page(
self, element, source_page, target_page, mouse_x: float, mouse_y: float, target_renderer
):
"""
Transfer an element from one page to another during drag operation.
Args:
element: The element to transfer
source_page: Source page object
target_page: Target page object
mouse_x: Current mouse X position in screen coordinates
mouse_y: Current mouse Y position in screen coordinates
target_renderer: PageRenderer for the target page
"""
# Convert mouse position to target page coordinates
new_page_x, new_page_y = target_renderer.screen_to_page(mouse_x, mouse_y)
# Get element size
elem_w, elem_h = element.size
# Center the element on the mouse position
new_x = new_page_x - elem_w / 2
new_y = new_page_y - elem_h / 2
# Remove element from source page
if element in source_page.layout.elements:
source_page.layout.elements.remove(element)
print(f"Removed element from page {source_page.page_number}")
# Update element position to new page coordinates
element.position = (new_x, new_y)
# Add element to target page
target_page.layout.add_element(element)
# Update element's parent page reference
element._parent_page = target_page
element._page_renderer = target_renderer
# Update drag start position and element position for continued dragging
self.drag_start_pos = (mouse_x, mouse_y)
self.drag_start_element_pos = element.position
print(f"Transferred element to page {target_page.page_number} at ({new_x:.1f}, {new_y:.1f})")