Coverage for pyPhotoAlbum/mixins/interaction_undo.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-20 12:55 +0000

1""" 

2Mixin for automatic undo/redo handling in interactive mouse operations 

3""" 

4 

5from typing import Optional 

6from pyPhotoAlbum.models import BaseLayoutElement 

7from .interaction_command_factory import InteractionCommandFactory, InteractionState 

8 

9 

10class UndoableInteractionMixin: 

11 """ 

12 Mixin providing automatic undo/redo for interactive mouse operations. 

13 

14 This mixin tracks the state of elements before interactive operations 

15 (move, resize, rotate) and automatically creates appropriate Command 

16 objects when the interaction completes. 

17 """ 

18 

19 def __init__(self, *args, **kwargs): 

20 super().__init__(*args, **kwargs) 

21 

22 # Command factory for creating undo/redo commands 

23 self._command_factory = InteractionCommandFactory() 

24 

25 # Interaction state tracking 

26 self._interaction_state = InteractionState() 

27 

28 def _begin_move(self, element: BaseLayoutElement): 

29 """ 

30 Begin tracking a move operation. 

31 

32 Args: 

33 element: The element being moved 

34 """ 

35 self._interaction_state.element = element 

36 self._interaction_state.interaction_type = "move" 

37 self._interaction_state.position = element.position 

38 

39 def _begin_resize(self, element: BaseLayoutElement): 

40 """ 

41 Begin tracking a resize operation. 

42 

43 Args: 

44 element: The element being resized 

45 """ 

46 self._interaction_state.element = element 

47 self._interaction_state.interaction_type = "resize" 

48 self._interaction_state.position = element.position 

49 self._interaction_state.size = element.size 

50 

51 def _begin_rotate(self, element: BaseLayoutElement): 

52 """ 

53 Begin tracking a rotate operation. 

54 

55 Args: 

56 element: The element being rotated 

57 """ 

58 self._interaction_state.element = element 

59 self._interaction_state.interaction_type = "rotate" 

60 self._interaction_state.rotation = element.rotation 

61 

62 def _begin_image_pan(self, element): 

63 """ 

64 Begin tracking an image pan operation. 

65 

66 Args: 

67 element: The ImageData element being panned 

68 """ 

69 from pyPhotoAlbum.models import ImageData 

70 

71 if not isinstance(element, ImageData): 

72 return 

73 

74 self._interaction_state.element = element 

75 self._interaction_state.interaction_type = "image_pan" 

76 self._interaction_state.crop_info = element.crop_info 

77 

78 def _end_interaction(self): 

79 """ 

80 End the current interaction and create appropriate undo/redo command. 

81 

82 This method uses the command factory to create the appropriate 

83 Command object based on what changed during the interaction. 

84 """ 

85 # Validate interaction state 

86 if not self._interaction_state.is_valid(): 

87 self._clear_interaction_state() 

88 return 

89 

90 # Get main window to access project history 

91 main_window = self.window() 

92 if not hasattr(main_window, "project"): 

93 self._clear_interaction_state() 

94 return 

95 

96 # Use factory to create command based on interaction type and changes 

97 command = self._command_factory.create_command( 

98 interaction_type=self._interaction_state.interaction_type, 

99 element=self._interaction_state.element, 

100 start_state=self._interaction_state.to_dict(), 

101 ) 

102 

103 # Execute the command through history if one was created 

104 if command: 

105 main_window.project.history.execute(command) 

106 

107 # Clear interaction state 

108 self._clear_interaction_state() 

109 

110 def _clear_interaction_state(self): 

111 """Clear all interaction tracking state""" 

112 self._interaction_state.clear() 

113 

114 def _cancel_interaction(self): 

115 """Cancel the current interaction without creating a command""" 

116 self._clear_interaction_state()