1 """

2 Size operations mixin for pyPhotoAlbum

3 """

4

5 from pyPhotoAlbum.decorators import ribbon_action

6 from pyPhotoAlbum.alignment import AlignmentManager

7 from pyPhotoAlbum.commands import ResizeElementsCommand

8

9

10 class SizeOperationsMixin:

11 """Mixin providing element sizing operations"""

12

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 []

16

17 def _execute_resize(self, resize_func, status_msg: str):

18 """

19 Execute a resize operation on multiple elements.

20

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

28

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]

35

36 def _execute_fit_to_page(self, fit_func, status_msg: str):

37 """

38 Execute a fit-to-page operation on a single element.

39

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

46

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

51

52 element = next(iter(self.gl_widget.selected_elements)) # type: ignore[attr-defined]

53 change = fit_func(element, page)

54

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]

60

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

72

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

84

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

96

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 )

111

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 )

126

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 )

141

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

154

155 page = self.get_current_page()

156 if not page:

157 self.show_warning("No Page", "Please create a page first.")

158 return

159

160 # Get the first selected element

161 element = next(iter(self.gl_widget.selected_elements))

162

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]

165

166 # Use configurable min_gap (grid spacing from snapping system, default 10mm)

167 min_gap = getattr(page.layout.snapping_system, "grid_spacing", 10.0)

168

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)

172

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)