Coverage for pyPhotoAlbum/mixins/operations/size_ops.py: 98%
66 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"""
2Size operations mixin for pyPhotoAlbum
3"""
5from pyPhotoAlbum.decorators import ribbon_action
6from pyPhotoAlbum.alignment import AlignmentManager
7from pyPhotoAlbum.commands import ResizeElementsCommand
10class SizeOperationsMixin:
11 """Mixin providing element sizing operations"""
13 def _get_selected_elements_list(self):
14 """Get list of selected elements for size operations"""
15 return list(self.gl_widget.selected_elements) if self.gl_widget.selected_elements else []
17 def _execute_resize(self, resize_func, status_msg: str):
18 """
19 Execute a resize operation on multiple elements.
21 Args:
22 resize_func: AlignmentManager method to call with elements
23 status_msg: Status message format string (will receive element count)
24 """
25 elements = self._get_selected_elements_list()
26 if not self.require_selection(min_count=2): # type: ignore[attr-defined]
27 return
29 changes = resize_func(elements)
30 if changes:
31 cmd = ResizeElementsCommand(changes)
32 self.project.history.execute(cmd) # type: ignore[attr-defined]
33 self.update_view() # type: ignore[attr-defined]
34 self.show_status(status_msg.format(len(elements)), 2000) # type: ignore[attr-defined]
36 def _execute_fit_to_page(self, fit_func, status_msg: str):
37 """
38 Execute a fit-to-page operation on a single element.
40 Args:
41 fit_func: Function that takes (element, page) and returns a change tuple
42 status_msg: Status message to display on success
43 """
44 if not self.require_selection(min_count=1): # type: ignore[attr-defined]
45 return
47 page = self.get_current_page() # type: ignore[attr-defined]
48 if not page:
49 self.show_warning("No Page", "Please create a page first.") # type: ignore[attr-defined]
50 return
52 element = next(iter(self.gl_widget.selected_elements)) # type: ignore[attr-defined]
53 change = fit_func(element, page)
55 if change:
56 cmd = ResizeElementsCommand([change])
57 self.project.history.execute(cmd) # type: ignore[attr-defined]
58 self.update_view() # type: ignore[attr-defined]
59 self.show_status(status_msg, 2000) # type: ignore[attr-defined]
61 @ribbon_action(
62 label="Same Size",
63 tooltip="Make all selected elements the same size",
64 tab="Arrange",
65 group="Size",
66 requires_selection=True,
67 min_selection=2,
68 )
69 def make_same_size(self):
70 """Make all selected elements the same size"""
71 self._execute_resize(AlignmentManager.make_same_size, "Resized {} elements to same size")
73 @ribbon_action(
74 label="Same Width",
75 tooltip="Make all selected elements the same width",
76 tab="Arrange",
77 group="Size",
78 requires_selection=True,
79 min_selection=2,
80 )
81 def make_same_width(self):
82 """Make all selected elements the same width"""
83 self._execute_resize(AlignmentManager.make_same_width, "Resized {} elements to same width")
85 @ribbon_action(
86 label="Same Height",
87 tooltip="Make all selected elements the same height",
88 tab="Arrange",
89 group="Size",
90 requires_selection=True,
91 min_selection=2,
92 )
93 def make_same_height(self):
94 """Make all selected elements the same height"""
95 self._execute_resize(AlignmentManager.make_same_height, "Resized {} elements to same height")
97 @ribbon_action(
98 label="Fit Width",
99 tooltip="Fit selected element to page width",
100 tab="Arrange",
101 group="Size",
102 requires_selection=True,
103 min_selection=1,
104 )
105 def fit_to_width(self):
106 """Fit selected element to page width"""
107 self._execute_fit_to_page(
108 lambda elem, page: AlignmentManager.fit_to_page_width(elem, page.layout.size[0]),
109 "Fitted element to page width",
110 )
112 @ribbon_action(
113 label="Fit Height",
114 tooltip="Fit selected element to page height",
115 tab="Arrange",
116 group="Size",
117 requires_selection=True,
118 min_selection=1,
119 )
120 def fit_to_height(self):
121 """Fit selected element to page height"""
122 self._execute_fit_to_page(
123 lambda elem, page: AlignmentManager.fit_to_page_height(elem, page.layout.size[1]),
124 "Fitted element to page height",
125 )
127 @ribbon_action(
128 label="Fit to Page",
129 tooltip="Fit selected element to page dimensions",
130 tab="Arrange",
131 group="Size",
132 requires_selection=True,
133 min_selection=1,
134 )
135 def fit_to_page(self):
136 """Fit selected element to page dimensions"""
137 self._execute_fit_to_page(
138 lambda elem, page: AlignmentManager.fit_to_page(elem, page.layout.size[0], page.layout.size[1]),
139 "Fitted element to page",
140 )
142 @ribbon_action(
143 label="Expand Image",
144 tooltip="Expand selected image to fill available space until it reaches page edges or other elements",
145 tab="Arrange",
146 group="Size",
147 requires_selection=True,
148 min_selection=1,
149 )
150 def expand_image(self):
151 """Expand selected image to fill available space"""
152 if not self.require_selection(min_count=1):
153 return
155 page = self.get_current_page()
156 if not page:
157 self.show_warning("No Page", "Please create a page first.")
158 return
160 # Get the first selected element
161 element = next(iter(self.gl_widget.selected_elements))
163 # Get other elements on the same page (excluding the selected one)
164 other_elements = [e for e in page.layout.elements if e is not element]
166 # Use configurable min_gap (grid spacing from snapping system, default 10mm)
167 min_gap = getattr(page.layout.snapping_system, "grid_spacing", 10.0)
169 # Expand to bounds
170 page_width, page_height = page.layout.size
171 change = AlignmentManager.expand_to_bounds(element, (page_width, page_height), other_elements, min_gap)
173 if change:
174 cmd = ResizeElementsCommand([change])
175 self.project.history.execute(cmd)
176 self.update_view()
177 self.show_status(f"Expanded image with {min_gap}mm gap", 2000)