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

1""" 

2Size operations mixin for pyPhotoAlbum 

3""" 

4 

5from pyPhotoAlbum.decorators import ribbon_action 

6from pyPhotoAlbum.alignment import AlignmentManager 

7from pyPhotoAlbum.commands import ResizeElementsCommand 

8 

9 

10class 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)