Coverage for pyPhotoAlbum/mixins/operations/page_ops.py: 96%

135 statements  

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

1""" 

2Page operations mixin for pyPhotoAlbum 

3""" 

4 

5from pyPhotoAlbum.decorators import ribbon_action, dialog_action 

6from pyPhotoAlbum.dialogs import PageSetupDialog 

7from pyPhotoAlbum.project import Page 

8from pyPhotoAlbum.page_layout import PageLayout 

9 

10 

11class PageOperationsMixin: 

12 """Mixin providing page management operations""" 

13 

14 # Note: Previous/Next page navigation removed - now using scrollable multi-page view 

15 # User can scroll through all pages vertically 

16 

17 @ribbon_action(label="Add Page", tooltip="Add a new page to the project", tab="Layout", group="Page") 

18 def add_page(self): 

19 """Add a new page to the project after the current page""" 

20 # Get the most visible page in viewport to determine insertion point 

21 current_page_index = self._get_most_visible_page_index() 

22 

23 # Ensure index is valid, default to end if not 

24 if current_page_index < 0 or current_page_index >= len(self.project.pages): 

25 insert_index = len(self.project.pages) 

26 else: 

27 # Insert after the current page 

28 insert_index = current_page_index + 1 

29 

30 # Create layout with project default size 

31 width_mm, height_mm = self.project.page_size_mm 

32 new_layout = PageLayout(width=width_mm, height=height_mm) 

33 

34 # Calculate proper page number for the new page 

35 # The page_number represents the logical page number in the book 

36 if insert_index == 0: 

37 # Inserting at the beginning 

38 new_page_number = 1 

39 elif insert_index >= len(self.project.pages): 

40 # Inserting at the end - calculate based on last page 

41 if self.project.pages: 

42 last_page = self.project.pages[-1] 

43 # Add the count of pages the last page represents 

44 new_page_number = last_page.page_number + last_page.get_page_count() 

45 else: 

46 new_page_number = 1 

47 else: 

48 # Inserting in the middle - take the page number of the page that will come after 

49 new_page_number = self.project.pages[insert_index].page_number 

50 

51 new_page = Page(layout=new_layout, page_number=new_page_number) 

52 # New pages are not manually sized - they use project defaults 

53 new_page.manually_sized = False 

54 

55 # Insert the page at the calculated position 

56 self.project.add_page(new_page, index=insert_index) 

57 

58 # Renumber all pages to ensure consistent numbering 

59 # Page numbers represent logical page numbers in the book 

60 current_page_num = 1 

61 for page in self.project.pages: 

62 page.page_number = current_page_num 

63 current_page_num += page.get_page_count() 

64 

65 self.update_view() 

66 

67 # Get display name for status message 

68 new_page_name = self.project.get_page_display_name(new_page) 

69 print(f"Added {new_page_name} at position {insert_index + 1} with size {width_mm}×{height_mm} mm") 

70 

71 @ribbon_action(label="Page Setup", tooltip="Configure page size and settings", tab="Layout", group="Page") 

72 @dialog_action(dialog_class=PageSetupDialog, requires_pages=True) 

73 def page_setup(self, values): 

74 """ 

75 Apply page setup configuration. 

76 

77 This method contains only business logic. UI presentation 

78 is handled by PageSetupDialog and the dialog_action decorator. 

79 

80 Args: 

81 values: Dictionary of values from the dialog 

82 """ 

83 selected_page = values["selected_page"] 

84 selected_index = values["selected_index"] 

85 

86 # Update project cover settings 

87 self.project.paper_thickness_mm = values["paper_thickness_mm"] 

88 self.project.cover_bleed_mm = values["cover_bleed_mm"] 

89 

90 # Handle cover designation (only for first page) 

91 if selected_index == 0: 

92 was_cover = selected_page.is_cover 

93 is_cover = values["is_cover"] 

94 

95 if was_cover != is_cover: 

96 selected_page.is_cover = is_cover 

97 self.project.has_cover = is_cover 

98 

99 if is_cover: 

100 # Calculate and set cover dimensions 

101 self.project.update_cover_dimensions() 

102 print(f"Page 1 designated as cover") 

103 else: 

104 # Restore normal page size 

105 selected_page.layout.size = self.project.page_size_mm 

106 print(f"Cover removed from page 1") 

107 

108 # Get new values 

109 width_mm = values["width_mm"] 

110 height_mm = values["height_mm"] 

111 

112 # Don't allow manual size changes for covers 

113 if not selected_page.is_cover: 

114 # Check if size actually changed 

115 # For double spreads, compare with base width 

116 if selected_page.is_double_spread: 

117 old_base_width = ( 

118 selected_page.layout.base_width 

119 if hasattr(selected_page.layout, "base_width") 

120 else selected_page.layout.size[0] / 2 

121 ) 

122 old_height = selected_page.layout.size[1] 

123 size_changed = old_base_width != width_mm or old_height != height_mm 

124 

125 if size_changed: 

126 # Update double spread 

127 selected_page.layout.base_width = width_mm 

128 selected_page.layout.size = (width_mm * 2, height_mm) 

129 selected_page.manually_sized = True 

130 print( 

131 f"{self.project.get_page_display_name(selected_page)} " 

132 f"(double spread) updated to {width_mm}×{height_mm} mm per page" 

133 ) 

134 else: 

135 old_size = selected_page.layout.size 

136 size_changed = old_size != (width_mm, height_mm) 

137 

138 if size_changed: 

139 # Update single page 

140 selected_page.layout.size = (width_mm, height_mm) 

141 selected_page.layout.base_width = width_mm 

142 selected_page.manually_sized = True 

143 print( 

144 f"{self.project.get_page_display_name(selected_page)} " f"updated to {width_mm}×{height_mm} mm" 

145 ) 

146 

147 # Update DPI settings 

148 self.project.working_dpi = values["working_dpi"] 

149 self.project.export_dpi = values["export_dpi"] 

150 

151 # Apply to other pages based on scope 

152 # 0 = page only, 1 = non-manual pages, 2 = all pages 

153 apply_scope = values.get("apply_scope", 0) 

154 if apply_scope in (1, 2): 

155 self.project.page_size_mm = (width_mm, height_mm) 

156 print(f"Project default page size set to {width_mm}×{height_mm} mm") 

157 

158 for page in self.project.pages: 

159 if page is selected_page or page.is_cover: 

160 continue 

161 if apply_scope == 1 and page.manually_sized: 

162 continue 

163 if page.is_double_spread: 

164 page.layout.base_width = width_mm 

165 page.layout.size = (width_mm * 2, height_mm) 

166 else: 

167 page.layout.size = (width_mm, height_mm) 

168 page.layout.base_width = width_mm 

169 

170 self.update_view() 

171 

172 # Build status message 

173 page_name = self.project.get_page_display_name(selected_page) 

174 if selected_page.is_cover: 

175 status_msg = f"{page_name} updated" 

176 else: 

177 status_msg = f"{page_name} size: {width_mm}×{height_mm} mm" 

178 if values.get("apply_scope", 0) in (1, 2): 

179 status_msg += " (set as default)" 

180 self.show_status(status_msg, 2000) 

181 

182 @ribbon_action( 

183 label="Toggle Spread", tooltip="Toggle double page spread for current page", tab="Layout", group="Page" 

184 ) 

185 def toggle_double_spread(self): 

186 """Toggle double spread for the current page""" 

187 if not self.project.pages: 

188 return 

189 

190 # Try to get the most visible page in viewport, fallback to current_page_index 

191 page_index = self._get_most_visible_page_index() 

192 

193 # Ensure index is valid 

194 if page_index < 0 or page_index >= len(self.project.pages): 

195 page_index = 0 

196 

197 current_page = self.project.pages[page_index] 

198 

199 # Toggle the state 

200 is_double = not current_page.is_double_spread 

201 current_page.is_double_spread = is_double 

202 

203 # Mark as manually sized when toggling spread 

204 current_page.manually_sized = True 

205 

206 # Update the page layout width 

207 current_width = current_page.layout.size[0] 

208 current_height = current_page.layout.size[1] 

209 

210 # Get base width (might already be doubled) 

211 if hasattr(current_page.layout, "base_width"): 

212 base_width = current_page.layout.base_width 

213 else: 

214 # Assume current width is single if not marked as facing 

215 base_width = current_width / 2 if current_page.layout.is_facing_page else current_width 

216 

217 # Set new width based on double spread state 

218 new_width = base_width * 2 if is_double else base_width 

219 current_page.layout.base_width = base_width 

220 current_page.layout.is_facing_page = is_double 

221 current_page.layout.size = (new_width, current_height) 

222 

223 # Update display 

224 self.update_view() 

225 

226 status = "enabled" if is_double else "disabled" 

227 page_name = self.project.get_page_display_name(current_page) 

228 self.show_status(f"{page_name}: Double spread {status}, width = {new_width:.0f}mm", 2000) 

229 print(f"{page_name}: Double spread {status}, width = {new_width}mm") 

230 

231 @ribbon_action(label="Remove Page", tooltip="Remove the currently selected page", tab="Layout", group="Page") 

232 def remove_page(self): 

233 """Remove the currently selected page""" 

234 if len(self.project.pages) <= 1: 

235 self.show_warning("Cannot Remove", "Must have at least one page") 

236 print("Cannot remove page - must have at least one page") 

237 return 

238 

239 # Get the most visible page in viewport 

240 page_index = self._get_most_visible_page_index() 

241 

242 # Ensure index is valid 

243 if page_index < 0 or page_index >= len(self.project.pages): 

244 page_index = len(self.project.pages) - 1 

245 

246 page_to_remove = self.project.pages[page_index] 

247 page_name = self.project.get_page_display_name(page_to_remove) 

248 

249 # Remove the selected page 

250 self.project.remove_page(page_to_remove) 

251 

252 # Renumber remaining pages to ensure consistent numbering 

253 # Page numbers represent logical page numbers in the book 

254 current_page_num = 1 

255 for page in self.project.pages: 

256 page.page_number = current_page_num 

257 current_page_num += page.get_page_count() 

258 

259 # Update display 

260 self.update_view() 

261 

262 print(f"Removed {page_name}, now have {len(self.project.pages)} pages")