206 lines
8.5 KiB
Python
206 lines
8.5 KiB
Python
"""
|
|
Mixin for automatic undo/redo handling in interactive mouse operations
|
|
"""
|
|
|
|
from typing import Optional
|
|
from pyPhotoAlbum.models import BaseLayoutElement
|
|
|
|
|
|
class UndoableInteractionMixin:
|
|
"""
|
|
Mixin providing automatic undo/redo for interactive mouse operations.
|
|
|
|
This mixin tracks the state of elements before interactive operations
|
|
(move, resize, rotate) and automatically creates appropriate Command
|
|
objects when the interaction completes.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Interaction tracking state
|
|
self._interaction_element: Optional[BaseLayoutElement] = None
|
|
self._interaction_type: Optional[str] = None
|
|
self._interaction_start_pos: Optional[tuple] = None
|
|
self._interaction_start_size: Optional[tuple] = None
|
|
self._interaction_start_rotation: Optional[float] = None
|
|
|
|
def _begin_move(self, element: BaseLayoutElement):
|
|
"""
|
|
Begin tracking a move operation.
|
|
|
|
Args:
|
|
element: The element being moved
|
|
"""
|
|
self._interaction_element = element
|
|
self._interaction_type = 'move'
|
|
self._interaction_start_pos = element.position
|
|
self._interaction_start_size = None
|
|
self._interaction_start_rotation = None
|
|
|
|
def _begin_resize(self, element: BaseLayoutElement):
|
|
"""
|
|
Begin tracking a resize operation.
|
|
|
|
Args:
|
|
element: The element being resized
|
|
"""
|
|
self._interaction_element = element
|
|
self._interaction_type = 'resize'
|
|
self._interaction_start_pos = element.position
|
|
self._interaction_start_size = element.size
|
|
self._interaction_start_rotation = None
|
|
|
|
def _begin_rotate(self, element: BaseLayoutElement):
|
|
"""
|
|
Begin tracking a rotate operation.
|
|
|
|
Args:
|
|
element: The element being rotated
|
|
"""
|
|
self._interaction_element = element
|
|
self._interaction_type = 'rotate'
|
|
self._interaction_start_pos = None
|
|
self._interaction_start_size = None
|
|
self._interaction_start_rotation = element.rotation
|
|
|
|
def _begin_image_pan(self, element):
|
|
"""
|
|
Begin tracking an image pan operation.
|
|
|
|
Args:
|
|
element: The ImageData element being panned
|
|
"""
|
|
from pyPhotoAlbum.models import ImageData
|
|
if not isinstance(element, ImageData):
|
|
return
|
|
|
|
self._interaction_element = element
|
|
self._interaction_type = 'image_pan'
|
|
self._interaction_start_pos = None
|
|
self._interaction_start_size = None
|
|
self._interaction_start_rotation = None
|
|
self._interaction_start_crop_info = element.crop_info
|
|
|
|
def _end_interaction(self):
|
|
"""
|
|
End the current interaction and create appropriate undo/redo command.
|
|
|
|
This method checks what changed during the interaction and creates
|
|
the appropriate Command object (MoveElementCommand, ResizeElementCommand,
|
|
or RotateElementCommand).
|
|
"""
|
|
if not self._interaction_element or not self._interaction_type:
|
|
self._clear_interaction_state()
|
|
return
|
|
|
|
element = self._interaction_element
|
|
|
|
# Get main window to access project history
|
|
main_window = self.window()
|
|
if not hasattr(main_window, 'project'):
|
|
self._clear_interaction_state()
|
|
return
|
|
|
|
# Create appropriate command based on interaction type
|
|
command = None
|
|
|
|
if self._interaction_type == 'move':
|
|
# Check if position actually changed
|
|
new_pos = element.position
|
|
if self._interaction_start_pos and new_pos != self._interaction_start_pos:
|
|
# Check for significant change (> 0.1 units)
|
|
dx = abs(new_pos[0] - self._interaction_start_pos[0])
|
|
dy = abs(new_pos[1] - self._interaction_start_pos[1])
|
|
if dx > 0.1 or dy > 0.1:
|
|
from pyPhotoAlbum.commands import MoveElementCommand
|
|
command = MoveElementCommand(
|
|
element,
|
|
self._interaction_start_pos,
|
|
new_pos
|
|
)
|
|
print(f"Move command created: {self._interaction_start_pos} → {new_pos}")
|
|
|
|
elif self._interaction_type == 'resize':
|
|
# Check if position or size actually changed
|
|
new_pos = element.position
|
|
new_size = element.size
|
|
if self._interaction_start_pos and self._interaction_start_size:
|
|
pos_changed = new_pos != self._interaction_start_pos
|
|
size_changed = new_size != self._interaction_start_size
|
|
|
|
if pos_changed or size_changed:
|
|
# Check for significant change
|
|
dx = abs(new_pos[0] - self._interaction_start_pos[0])
|
|
dy = abs(new_pos[1] - self._interaction_start_pos[1])
|
|
dw = abs(new_size[0] - self._interaction_start_size[0])
|
|
dh = abs(new_size[1] - self._interaction_start_size[1])
|
|
|
|
if dx > 0.1 or dy > 0.1 or dw > 0.1 or dh > 0.1:
|
|
from pyPhotoAlbum.commands import ResizeElementCommand
|
|
command = ResizeElementCommand(
|
|
element,
|
|
self._interaction_start_pos,
|
|
self._interaction_start_size,
|
|
new_pos,
|
|
new_size
|
|
)
|
|
print(f"Resize command created: {self._interaction_start_size} → {new_size}")
|
|
|
|
elif self._interaction_type == 'rotate':
|
|
# Check if rotation actually changed
|
|
new_rotation = element.rotation
|
|
if self._interaction_start_rotation is not None:
|
|
if abs(new_rotation - self._interaction_start_rotation) > 0.1:
|
|
from pyPhotoAlbum.commands import RotateElementCommand
|
|
command = RotateElementCommand(
|
|
element,
|
|
self._interaction_start_rotation,
|
|
new_rotation
|
|
)
|
|
print(f"Rotation command created: {self._interaction_start_rotation:.1f}° → {new_rotation:.1f}°")
|
|
|
|
elif self._interaction_type == 'image_pan':
|
|
# Check if crop_info actually changed
|
|
from pyPhotoAlbum.models import ImageData
|
|
if isinstance(element, ImageData):
|
|
new_crop_info = element.crop_info
|
|
if hasattr(self, '_interaction_start_crop_info') and self._interaction_start_crop_info is not None:
|
|
# Check if crop changed significantly (more than 0.001 in any coordinate)
|
|
if new_crop_info != self._interaction_start_crop_info:
|
|
old_crop = self._interaction_start_crop_info
|
|
significant_change = any(
|
|
abs(new_crop_info[i] - old_crop[i]) > 0.001
|
|
for i in range(4)
|
|
)
|
|
|
|
if significant_change:
|
|
from pyPhotoAlbum.commands import AdjustImageCropCommand
|
|
command = AdjustImageCropCommand(
|
|
element,
|
|
self._interaction_start_crop_info,
|
|
new_crop_info
|
|
)
|
|
print(f"Image pan command created: {self._interaction_start_crop_info} → {new_crop_info}")
|
|
|
|
# Execute the command through history if one was created
|
|
if command:
|
|
main_window.project.history.execute(command)
|
|
|
|
# Clear interaction state
|
|
self._clear_interaction_state()
|
|
|
|
def _clear_interaction_state(self):
|
|
"""Clear all interaction tracking state"""
|
|
self._interaction_element = None
|
|
self._interaction_type = None
|
|
self._interaction_start_pos = None
|
|
self._interaction_start_size = None
|
|
self._interaction_start_rotation = None
|
|
if hasattr(self, '_interaction_start_crop_info'):
|
|
self._interaction_start_crop_info = None
|
|
|
|
def _cancel_interaction(self):
|
|
"""Cancel the current interaction without creating a command"""
|
|
self._clear_interaction_state()
|