1 """

2 Page operations mixin for pyPhotoAlbum

3 """

4

5 from pyPhotoAlbum.decorators import ribbon_action, dialog_action

6 from pyPhotoAlbum.dialogs import PageSetupDialog

7 from pyPhotoAlbum.project import Page

8 from pyPhotoAlbum.page_layout import PageLayout

9

10

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