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
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
1"""
2Page operations mixin for pyPhotoAlbum
3"""
5from pyPhotoAlbum.decorators import ribbon_action, dialog_action
6from pyPhotoAlbum.dialogs import PageSetupDialog
7from pyPhotoAlbum.project import Page
8from pyPhotoAlbum.page_layout import PageLayout
11class PageOperationsMixin:
12 """Mixin providing page management operations"""
14 # Note: Previous/Next page navigation removed - now using scrollable multi-page view
15 # User can scroll through all pages vertically
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()
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
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)
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
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
55 # Insert the page at the calculated position
56 self.project.add_page(new_page, index=insert_index)
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()
65 self.update_view()
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")
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.
77 This method contains only business logic. UI presentation
78 is handled by PageSetupDialog and the dialog_action decorator.
80 Args:
81 values: Dictionary of values from the dialog
82 """
83 selected_page = values["selected_page"]
84 selected_index = values["selected_index"]
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"]
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"]
95 if was_cover != is_cover:
96 selected_page.is_cover = is_cover
97 self.project.has_cover = is_cover
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")
108 # Get new values
109 width_mm = values["width_mm"]
110 height_mm = values["height_mm"]
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
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)
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 )
147 # Update DPI settings
148 self.project.working_dpi = values["working_dpi"]
149 self.project.export_dpi = values["export_dpi"]
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")
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
170 self.update_view()
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)
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
190 # Try to get the most visible page in viewport, fallback to current_page_index
191 page_index = self._get_most_visible_page_index()
193 # Ensure index is valid
194 if page_index < 0 or page_index >= len(self.project.pages):
195 page_index = 0
197 current_page = self.project.pages[page_index]
199 # Toggle the state
200 is_double = not current_page.is_double_spread
201 current_page.is_double_spread = is_double
203 # Mark as manually sized when toggling spread
204 current_page.manually_sized = True
206 # Update the page layout width
207 current_width = current_page.layout.size[0]
208 current_height = current_page.layout.size[1]
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
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)
223 # Update display
224 self.update_view()
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")
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
239 # Get the most visible page in viewport
240 page_index = self._get_most_visible_page_index()
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
246 page_to_remove = self.project.pages[page_index]
247 page_name = self.project.get_page_display_name(page_to_remove)
249 # Remove the selected page
250 self.project.remove_page(page_to_remove)
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()
259 # Update display
260 self.update_view()
262 print(f"Removed {page_name}, now have {len(self.project.pages)} pages")