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
« 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.
4This implements the Strategy pattern, allowing different command builders
5to be registered and used based on the interaction type.
6"""
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)
19class InteractionCommandFactory:
20 """
21 Factory for creating commands from interaction data.
23 Uses the Strategy pattern to delegate command creation to
24 specialized builder classes based on interaction type.
25 """
27 def __init__(self):
28 """Initialize factory with default builders."""
29 self._builders: Dict[str, CommandBuilder] = {}
30 self._register_default_builders()
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())
39 def register_builder(self, interaction_type: str, builder: CommandBuilder):
40 """
41 Register a command builder for an interaction type.
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
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.
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
61 Returns:
62 Command object if changes warrant it, None otherwise
63 """
64 builder = self._builders.get(interaction_type)
66 if builder is None:
67 print(f"Warning: No builder registered for interaction type '{interaction_type}'")
68 return None
70 if not builder.can_build(element, start_state, **kwargs):
71 return None
73 return builder.build(element, start_state, **kwargs)
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
79 def get_supported_types(self) -> list:
80 """Get list of supported interaction types."""
81 return list(self._builders.keys())
84class InteractionState:
85 """
86 Value object representing the state of an interaction.
88 This simplifies passing interaction data around and makes
89 the code more maintainable.
90 """
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.
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
119 def to_dict(self) -> dict:
120 """
121 Convert state to dictionary for command builders.
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
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
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