pyPhotoAlbum/pyPhotoAlbum/mixins/interaction_undo.py
2025-10-29 20:30:57 +01:00

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()