178 lines
6.3 KiB
Python
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})")
|