299 lines
12 KiB
Python
299 lines
12 KiB
Python
"""
|
|
Mouse interaction mixin for GLWidget - coordinates all mouse events
|
|
"""
|
|
|
|
from PyQt6.QtCore import Qt
|
|
from pyPhotoAlbum.models import ImageData
|
|
|
|
|
|
class MouseInteractionMixin:
|
|
"""
|
|
Mixin providing mouse event handling and coordination.
|
|
|
|
This mixin routes mouse events to appropriate other mixins based on
|
|
the current interaction state.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Mouse interaction state
|
|
self.drag_start_pos = None
|
|
self.drag_start_element_pos = None
|
|
self.is_dragging = False
|
|
self.is_panning = False
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Handle mouse press events"""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
x, y = event.position().x(), event.position().y()
|
|
ctrl_pressed = event.modifiers() & Qt.KeyboardModifier.ControlModifier
|
|
|
|
# Check if clicking on ghost page button
|
|
if self._check_ghost_page_click(x, y):
|
|
return
|
|
|
|
# Update current_page_index based on where user clicked
|
|
page, page_index, renderer = self._get_page_at(x, y)
|
|
if page_index >= 0:
|
|
self.current_page_index = page_index
|
|
|
|
if len(self.selected_elements) == 1 and self.selected_element:
|
|
if self.rotation_mode:
|
|
# In rotation mode, start rotation tracking
|
|
self._begin_rotate(self.selected_element)
|
|
self.drag_start_pos = (x, y)
|
|
self.rotation_start_angle = self.selected_element.rotation
|
|
self.is_dragging = True
|
|
return
|
|
else:
|
|
# In normal mode, check for resize handles
|
|
handle = self._get_resize_handle_at(x, y)
|
|
if handle:
|
|
self._begin_resize(self.selected_element)
|
|
self.resize_handle = handle
|
|
self.drag_start_pos = (x, y)
|
|
self.resize_start_pos = self.selected_element.position
|
|
self.resize_start_size = self.selected_element.size
|
|
self.is_dragging = True
|
|
return
|
|
|
|
element = self._get_element_at(x, y)
|
|
if element:
|
|
# Check if Control is pressed and element is ImageData - enter image pan mode
|
|
if ctrl_pressed and isinstance(element, ImageData) and not self.rotation_mode:
|
|
# Enter image pan mode - pan image within frame
|
|
self.selected_elements = {element}
|
|
self.drag_start_pos = (x, y)
|
|
self.image_pan_mode = True
|
|
self.image_pan_start_crop = element.crop_info
|
|
self._begin_image_pan(element)
|
|
self.is_dragging = True
|
|
self.setCursor(Qt.CursorShape.SizeAllCursor)
|
|
print(f"Entered image pan mode for {element}")
|
|
elif ctrl_pressed:
|
|
# Multi-select mode
|
|
if element in self.selected_elements:
|
|
self.selected_elements.remove(element)
|
|
else:
|
|
self.selected_elements.add(element)
|
|
else:
|
|
# Normal drag mode
|
|
self.selected_elements = {element}
|
|
self.drag_start_pos = (x, y)
|
|
self.drag_start_element_pos = element.position
|
|
if not self.rotation_mode:
|
|
self._begin_move(element)
|
|
self.is_dragging = True
|
|
else:
|
|
if not ctrl_pressed:
|
|
self.selected_elements.clear()
|
|
|
|
self.update()
|
|
|
|
elif event.button() == Qt.MouseButton.MiddleButton:
|
|
self.is_panning = True
|
|
self.drag_start_pos = (event.position().x(), event.position().y())
|
|
self.setCursor(Qt.CursorShape.ClosedHandCursor)
|
|
|
|
def mouseMoveEvent(self, event):
|
|
"""Handle mouse move events"""
|
|
x, y = event.position().x(), event.position().y()
|
|
|
|
# Update status bar with page information
|
|
self._update_page_status(x, y)
|
|
|
|
if self.is_panning and self.drag_start_pos:
|
|
dx = x - self.drag_start_pos[0]
|
|
dy = y - self.drag_start_pos[1]
|
|
|
|
self.pan_offset[0] += dx
|
|
self.pan_offset[1] += dy
|
|
|
|
self.drag_start_pos = (x, y)
|
|
self.update()
|
|
return
|
|
|
|
if not self.is_dragging or not self.drag_start_pos:
|
|
return
|
|
|
|
if self.selected_element:
|
|
if self.image_pan_mode:
|
|
# Image pan mode - delegate to ImagePanMixin
|
|
self._handle_image_pan_move(x, y, self.selected_element)
|
|
|
|
elif self.rotation_mode:
|
|
# Rotation mode
|
|
import math
|
|
|
|
if not hasattr(self.selected_element, '_page_renderer'):
|
|
return
|
|
|
|
renderer = self.selected_element._page_renderer
|
|
elem_x, elem_y = self.selected_element.position
|
|
elem_w, elem_h = self.selected_element.size
|
|
|
|
center_page_x = elem_x + elem_w / 2
|
|
center_page_y = elem_y + elem_h / 2
|
|
screen_center_x, screen_center_y = renderer.page_to_screen(center_page_x, center_page_y)
|
|
|
|
dx = x - screen_center_x
|
|
dy = y - screen_center_y
|
|
angle = math.degrees(math.atan2(dy, dx))
|
|
|
|
angle = round(angle / self.rotation_snap_angle) * self.rotation_snap_angle
|
|
angle = angle % 360
|
|
|
|
self.selected_element.rotation = angle
|
|
|
|
main_window = self.window()
|
|
if hasattr(main_window, 'show_status'):
|
|
main_window.show_status(f"Rotation: {angle:.1f}°", 100)
|
|
|
|
elif self.resize_handle:
|
|
# Resize mode
|
|
screen_dx = x - self.drag_start_pos[0]
|
|
screen_dy = y - self.drag_start_pos[1]
|
|
|
|
total_dx = screen_dx / self.zoom_level
|
|
total_dy = screen_dy / self.zoom_level
|
|
|
|
if self.selected_element.rotation != 0:
|
|
import math
|
|
angle_rad = -math.radians(self.selected_element.rotation)
|
|
cos_a = math.cos(angle_rad)
|
|
sin_a = math.sin(angle_rad)
|
|
|
|
rotated_dx = total_dx * cos_a - total_dy * sin_a
|
|
rotated_dy = total_dx * sin_a + total_dy * cos_a
|
|
|
|
total_dx = rotated_dx
|
|
total_dy = rotated_dy
|
|
|
|
self._resize_element(total_dx, total_dy)
|
|
else:
|
|
# Move mode
|
|
current_page, current_page_index, current_renderer = self._get_page_at(x, y)
|
|
|
|
if current_page and hasattr(self.selected_element, '_parent_page'):
|
|
source_page = self.selected_element._parent_page
|
|
|
|
if current_page is not source_page:
|
|
self._transfer_element_to_page(self.selected_element, source_page, current_page, x, y, current_renderer)
|
|
else:
|
|
total_dx = (x - self.drag_start_pos[0]) / self.zoom_level
|
|
total_dy = (y - self.drag_start_pos[1]) / self.zoom_level
|
|
|
|
new_x = self.drag_start_element_pos[0] + total_dx
|
|
new_y = self.drag_start_element_pos[1] + total_dy
|
|
|
|
main_window = self.window()
|
|
snap_sys = source_page.layout.snapping_system
|
|
page_size = source_page.layout.size
|
|
dpi = main_window.project.working_dpi
|
|
|
|
snapped_pos = snap_sys.snap_position(
|
|
position=(new_x, new_y),
|
|
size=self.selected_element.size,
|
|
page_size=page_size,
|
|
dpi=dpi
|
|
)
|
|
|
|
self.selected_element.position = snapped_pos
|
|
else:
|
|
total_dx = (x - self.drag_start_pos[0]) / self.zoom_level
|
|
total_dy = (y - self.drag_start_pos[1]) / self.zoom_level
|
|
|
|
new_x = self.drag_start_element_pos[0] + total_dx
|
|
new_y = self.drag_start_element_pos[1] + total_dy
|
|
|
|
self.selected_element.position = (new_x, new_y)
|
|
|
|
self.update()
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
"""Handle mouse release events"""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
self._end_interaction()
|
|
|
|
self.is_dragging = False
|
|
self.drag_start_pos = None
|
|
self.drag_start_element_pos = None
|
|
self.resize_handle = None
|
|
self.rotation_start_angle = None
|
|
self.image_pan_mode = False
|
|
self.image_pan_start_crop = None
|
|
self.snap_state = {
|
|
'is_snapped': False,
|
|
'last_position': None,
|
|
'last_size': None
|
|
}
|
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
|
|
elif event.button() == Qt.MouseButton.MiddleButton:
|
|
self.is_panning = False
|
|
self.drag_start_pos = None
|
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
|
|
def mouseDoubleClickEvent(self, event):
|
|
"""Handle mouse double-click events"""
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
x, y = event.position().x(), event.position().y()
|
|
element = self._get_element_at(x, y)
|
|
|
|
from pyPhotoAlbum.models import TextBoxData
|
|
if isinstance(element, TextBoxData):
|
|
self._edit_text_element(element)
|
|
return
|
|
|
|
super().mouseDoubleClickEvent(event)
|
|
|
|
def wheelEvent(self, event):
|
|
"""Handle mouse wheel events for scrolling or zooming (with Ctrl)"""
|
|
delta = event.angleDelta().y()
|
|
ctrl_pressed = event.modifiers() & Qt.KeyboardModifier.ControlModifier
|
|
|
|
if ctrl_pressed:
|
|
# Ctrl + Wheel: Zoom centered on mouse position
|
|
mouse_x = event.position().x()
|
|
mouse_y = event.position().y()
|
|
|
|
world_x = (mouse_x - self.pan_offset[0]) / self.zoom_level
|
|
world_y = (mouse_y - self.pan_offset[1]) / self.zoom_level
|
|
|
|
zoom_factor = 1.1 if delta > 0 else 0.9
|
|
new_zoom = self.zoom_level * zoom_factor
|
|
|
|
if 0.1 <= new_zoom <= 5.0:
|
|
self.zoom_level = new_zoom
|
|
|
|
self.pan_offset[0] = mouse_x - world_x * self.zoom_level
|
|
self.pan_offset[1] = mouse_y - world_y * self.zoom_level
|
|
|
|
self.update()
|
|
|
|
main_window = self.window()
|
|
if hasattr(main_window, 'status_bar'):
|
|
main_window.status_bar.showMessage(f"Zoom: {int(self.zoom_level * 100)}%", 2000)
|
|
else:
|
|
# Regular wheel: Vertical scroll
|
|
scroll_amount = delta * 0.5
|
|
self.pan_offset[1] += scroll_amount
|
|
self.update()
|
|
|
|
def _edit_text_element(self, text_element):
|
|
"""Open dialog to edit text element"""
|
|
from pyPhotoAlbum.text_edit_dialog import TextEditDialog
|
|
|
|
dialog = TextEditDialog(text_element, self)
|
|
if dialog.exec() == TextEditDialog.DialogCode.Accepted:
|
|
values = dialog.get_values()
|
|
|
|
text_element.text_content = values['text_content']
|
|
text_element.font_settings = values['font_settings']
|
|
text_element.alignment = values['alignment']
|
|
|
|
self.update()
|
|
|
|
print(f"Updated text element: {values['text_content'][:50]}...")
|