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

54 statements  

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

1""" 

2Factory for creating interaction commands based on interaction type. 

3 

4This implements the Strategy pattern, allowing different command builders 

5to be registered and used based on the interaction type. 

6""" 

7 

8from typing import Optional, Dict, Any 

9from pyPhotoAlbum.models import BaseLayoutElement 

10from .interaction_command_builders import ( 

11 CommandBuilder, 

12 MoveCommandBuilder, 

13 ResizeCommandBuilder, 

14 RotateCommandBuilder, 

15 ImagePanCommandBuilder, 

16) 

17 

18 

19class InteractionCommandFactory: 

20 """ 

21 Factory for creating commands from interaction data. 

22 

23 Uses the Strategy pattern to delegate command creation to 

24 specialized builder classes based on interaction type. 

25 """ 

26 

27 def __init__(self): 

28 """Initialize factory with default builders.""" 

29 self._builders: Dict[str, CommandBuilder] = {} 

30 self._register_default_builders() 

31 

32 def _register_default_builders(self): 

33 """Register the default command builders.""" 

34 self.register_builder("move", MoveCommandBuilder()) 

35 self.register_builder("resize", ResizeCommandBuilder()) 

36 self.register_builder("rotate", RotateCommandBuilder()) 

37 self.register_builder("image_pan", ImagePanCommandBuilder()) 

38 

39 def register_builder(self, interaction_type: str, builder: CommandBuilder): 

40 """ 

41 Register a command builder for an interaction type. 

42 

43 Args: 

44 interaction_type: The type of interaction (e.g., 'move', 'resize') 

45 builder: The CommandBuilder instance to handle this type 

46 """ 

47 self._builders[interaction_type] = builder 

48 

49 def create_command( 

50 self, interaction_type: str, element: BaseLayoutElement, start_state: dict, **kwargs 

51 ) -> Optional[Any]: 

52 """ 

53 Create a command based on interaction type and state changes. 

54 

55 Args: 

56 interaction_type: Type of interaction ('move', 'resize', etc.) 

57 element: The element that was interacted with 

58 start_state: Dictionary containing initial state values 

59 **kwargs: Additional context for command creation 

60 

61 Returns: 

62 Command object if changes warrant it, None otherwise 

63 """ 

64 builder = self._builders.get(interaction_type) 

65 

66 if builder is None: 

67 print(f"Warning: No builder registered for interaction type '{interaction_type}'") 

68 return None 

69 

70 if not builder.can_build(element, start_state, **kwargs): 

71 return None 

72 

73 return builder.build(element, start_state, **kwargs) 

74 

75 def has_builder(self, interaction_type: str) -> bool: 

76 """Check if a builder is registered for the given interaction type.""" 

77 return interaction_type in self._builders 

78 

79 def get_supported_types(self) -> list: 

80 """Get list of supported interaction types.""" 

81 return list(self._builders.keys()) 

82 

83 

84class InteractionState: 

85 """ 

86 Value object representing the state of an interaction. 

87 

88 This simplifies passing interaction data around and makes 

89 the code more maintainable. 

90 """ 

91 

92 def __init__( 

93 self, 

94 element: Optional[BaseLayoutElement] = None, 

95 interaction_type: Optional[str] = None, 

96 position: Optional[tuple] = None, 

97 size: Optional[tuple] = None, 

98 rotation: Optional[float] = None, 

99 crop_info: Optional[tuple] = None, 

100 ): 

101 """ 

102 Initialize interaction state. 

103 

104 Args: 

105 element: The element being interacted with 

106 interaction_type: Type of interaction 

107 position: Initial position 

108 size: Initial size 

109 rotation: Initial rotation 

110 crop_info: Initial crop info (for images) 

111 """ 

112 self.element = element 

113 self.interaction_type = interaction_type 

114 self.position = position 

115 self.size = size 

116 self.rotation = rotation 

117 self.crop_info = crop_info 

118 

119 def to_dict(self) -> dict: 

120 """ 

121 Convert state to dictionary for command builders. 

122 

123 Returns: 

124 Dict with non-None state values 

125 """ 

126 state: Dict[str, Any] = {} 

127 if self.position is not None: 

128 state["position"] = self.position 

129 if self.size is not None: 

130 state["size"] = self.size 

131 if self.rotation is not None: 

132 state["rotation"] = self.rotation 

133 if self.crop_info is not None: 

134 state["crop_info"] = self.crop_info 

135 return state 

136 

137 def is_valid(self) -> bool: 

138 """Check if state has required fields for command creation.""" 

139 return self.element is not None and self.interaction_type is not None 

140 

141 def clear(self): 

142 """Clear all state values.""" 

143 self.element = None 

144 self.interaction_type = None 

145 self.position = None 

146 self.size = None 

147 self.rotation = None 

148 self.crop_info = None