131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
"""
|
|
Element selection mixin for GLWidget - handles element selection and hit detection
|
|
"""
|
|
|
|
from typing import Optional, Set
|
|
from pyPhotoAlbum.models import BaseLayoutElement
|
|
|
|
|
|
class ElementSelectionMixin:
|
|
"""
|
|
Mixin providing element selection and hit detection functionality.
|
|
|
|
This mixin manages which elements are selected and provides methods to
|
|
detect which element or resize handle is at a given screen position.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Selection state - multi-select support
|
|
self.selected_elements: Set[BaseLayoutElement] = set()
|
|
|
|
@property
|
|
def selected_element(self) -> Optional[BaseLayoutElement]:
|
|
"""
|
|
For backward compatibility - returns first selected element or None.
|
|
|
|
Returns:
|
|
BaseLayoutElement or None: The first selected element, or None if no selection
|
|
"""
|
|
return next(iter(self.selected_elements)) if self.selected_elements else None
|
|
|
|
@selected_element.setter
|
|
def selected_element(self, value: Optional[BaseLayoutElement]):
|
|
"""
|
|
For backward compatibility - sets single element selection.
|
|
|
|
Args:
|
|
value: Element to select, or None to clear selection
|
|
"""
|
|
if value is None:
|
|
self.selected_elements.clear()
|
|
else:
|
|
self.selected_elements = {value}
|
|
|
|
def _get_element_at(self, x: float, y: float) -> Optional[BaseLayoutElement]:
|
|
"""
|
|
Get the element at the given screen position across all pages.
|
|
|
|
Args:
|
|
x: Screen X coordinate
|
|
y: Screen Y coordinate
|
|
|
|
Returns:
|
|
BaseLayoutElement or None: The topmost element at the position, or None
|
|
"""
|
|
if not hasattr(self, '_page_renderers') or not self._page_renderers:
|
|
return None
|
|
|
|
# Check each page from top to bottom (reverse z-order)
|
|
for renderer, page in reversed(self._page_renderers):
|
|
# Convert screen coordinates to page-local coordinates
|
|
# Do this for all pages, not just those where the click is within bounds
|
|
# This allows selecting elements that have moved off the page
|
|
page_x, page_y = renderer.screen_to_page(x, y)
|
|
|
|
# Check elements in this page (highest in list = on top, so check in reverse)
|
|
for element in reversed(page.layout.elements):
|
|
# Get element bounds
|
|
ex, ey = element.position
|
|
ew, eh = element.size
|
|
|
|
# Simple bounds check (no rotation transformation needed - images are already rotated)
|
|
if ex <= page_x <= ex + ew and ey <= page_y <= ey + eh:
|
|
# Store the renderer with the element for later use
|
|
element._page_renderer = renderer
|
|
element._parent_page = page
|
|
return element
|
|
|
|
return None
|
|
|
|
def _get_resize_handle_at(self, x: float, y: float) -> Optional[str]:
|
|
"""
|
|
Get the resize handle at the given screen position.
|
|
|
|
Only checks if there is a single selected element.
|
|
|
|
Args:
|
|
x: Screen X coordinate
|
|
y: Screen Y coordinate
|
|
|
|
Returns:
|
|
str or None: Handle name ('nw', 'ne', 'sw', 'se') or None
|
|
"""
|
|
if not self.selected_element:
|
|
return None
|
|
|
|
main_window = self.window()
|
|
if not hasattr(main_window, 'project') or not main_window.project or not main_window.project.pages:
|
|
return None
|
|
|
|
# Get the PageRenderer for this element (stored when element was selected)
|
|
if not hasattr(self.selected_element, '_page_renderer'):
|
|
return None
|
|
|
|
renderer = self.selected_element._page_renderer
|
|
|
|
# Get element position and size in page-local coordinates
|
|
elem_x, elem_y = self.selected_element.position
|
|
elem_w, elem_h = self.selected_element.size
|
|
handle_size = 8
|
|
|
|
# Convert to screen coordinates using PageRenderer
|
|
ex, ey = renderer.page_to_screen(elem_x, elem_y)
|
|
ew = elem_w * renderer.zoom
|
|
eh = elem_h * renderer.zoom
|
|
|
|
# Check handles (no rotation transformation needed - images are already rotated)
|
|
handles = {
|
|
'nw': (ex - handle_size/2, ey - handle_size/2),
|
|
'ne': (ex + ew - handle_size/2, ey - handle_size/2),
|
|
'sw': (ex - handle_size/2, ey + eh - handle_size/2),
|
|
'se': (ex + ew - handle_size/2, ey + eh - handle_size/2),
|
|
}
|
|
|
|
for name, (hx, hy) in handles.items():
|
|
if hx <= x <= hx + handle_size and hy <= y <= hy + handle_size:
|
|
return name
|
|
|
|
return None
|