pyPhotoAlbum/pyPhotoAlbum/mixins/element_selection.py
Duncan Tourolle 5de3384c35
Some checks failed
Python CI / test (push) Successful in 1m19s
Lint / lint (push) Successful in 1m21s
Tests / test (3.10) (push) Failing after 1m2s
Tests / test (3.11) (push) Failing after 57s
Tests / test (3.9) (push) Failing after 59s
Many improvements and fixes
2025-11-21 22:35:47 +01:00

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