Coverage for pyPhotoAlbum/dialogs/page_setup_dialog.py: 100%
183 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 Setup Dialog for pyPhotoAlbum
4Encapsulates all UI logic for page setup configuration,
5separating presentation from business logic.
6"""
8import math
9from typing import Optional, Dict, Any
10from PyQt6.QtWidgets import (
11 QDialog,
12 QVBoxLayout,
13 QHBoxLayout,
14 QLabel,
15 QDoubleSpinBox,
16 QSpinBox,
17 QPushButton,
18 QGroupBox,
19 QComboBox,
20 QCheckBox,
21 QRadioButton,
22 QButtonGroup,
23)
24from pyPhotoAlbum.project import Project
27class PageSetupDialog(QDialog):
28 """
29 Dialog for configuring page settings.
31 This dialog handles all UI presentation logic for page setup,
32 including page size, DPI settings, and cover configuration.
33 """
35 def __init__(self, parent, project: Project, initial_page_index: int = 0):
36 """
37 Initialize the page setup dialog.
39 Args:
40 parent: Parent widget
41 project: Project instance containing pages and settings
42 initial_page_index: Index of page to initially select
43 """
44 super().__init__(parent)
45 self.project = project
46 self.initial_page_index = initial_page_index
48 self._setup_ui()
49 self._connect_signals()
50 self._initialize_values()
52 def _setup_ui(self):
53 """Create and layout all UI components."""
54 self.setWindowTitle("Page Setup")
55 self.setMinimumWidth(450)
57 layout = QVBoxLayout()
59 # Page selection group
60 self._page_select_group = self._create_page_selection_group()
61 layout.addWidget(self._page_select_group)
63 # Cover settings group
64 self._cover_group = self._create_cover_settings_group()
65 layout.addWidget(self._cover_group)
67 # Page size group
68 self._size_group = self._create_page_size_group()
69 layout.addWidget(self._size_group)
71 # DPI settings group
72 self._dpi_group = self._create_dpi_settings_group()
73 layout.addWidget(self._dpi_group)
75 # Buttons
76 button_layout = self._create_button_layout()
77 layout.addLayout(button_layout)
79 self.setLayout(layout)
81 def _create_page_selection_group(self) -> QGroupBox:
82 """Create the page selection group."""
83 group = QGroupBox("Select Page")
84 layout = QVBoxLayout()
86 # Page combo box
87 self.page_combo = QComboBox()
88 for i, page in enumerate(self.project.pages):
89 page_label = self.project.get_page_display_name(page)
90 if page.is_double_spread and not page.is_cover:
91 page_label += " (Double Spread)"
92 if page.manually_sized:
93 page_label += " *"
94 self.page_combo.addItem(page_label, i)
95 layout.addWidget(self.page_combo)
97 # Info label
98 info_label = QLabel("* = Manually sized page")
99 info_label.setStyleSheet("font-size: 9pt; color: gray;")
100 layout.addWidget(info_label)
102 group.setLayout(layout)
103 return group
105 def _create_cover_settings_group(self) -> QGroupBox:
106 """Create the cover settings group."""
107 group = QGroupBox("Cover Settings")
108 layout = QVBoxLayout()
110 # Cover checkbox
111 self.cover_checkbox = QCheckBox("Designate as Cover")
112 self.cover_checkbox.setToolTip("Mark this page as the book cover with wrap-around front/spine/back")
113 layout.addWidget(self.cover_checkbox)
115 # Paper thickness
116 thickness_layout = QHBoxLayout()
117 thickness_layout.addWidget(QLabel("Paper Thickness:"))
118 self.thickness_spinbox = QDoubleSpinBox()
119 self.thickness_spinbox.setRange(0.05, 1.0)
120 self.thickness_spinbox.setSingleStep(0.05)
121 self.thickness_spinbox.setValue(self.project.paper_thickness_mm)
122 self.thickness_spinbox.setSuffix(" mm")
123 self.thickness_spinbox.setToolTip("Thickness of paper for spine calculation")
124 thickness_layout.addWidget(self.thickness_spinbox)
125 layout.addLayout(thickness_layout)
127 # Bleed margin
128 bleed_layout = QHBoxLayout()
129 bleed_layout.addWidget(QLabel("Bleed Margin:"))
130 self.bleed_spinbox = QDoubleSpinBox()
131 self.bleed_spinbox.setRange(0, 10)
132 self.bleed_spinbox.setSingleStep(0.5)
133 self.bleed_spinbox.setValue(self.project.cover_bleed_mm)
134 self.bleed_spinbox.setSuffix(" mm")
135 self.bleed_spinbox.setToolTip("Extra margin around cover for printing bleed")
136 bleed_layout.addWidget(self.bleed_spinbox)
137 layout.addLayout(bleed_layout)
139 # Calculated spine width display
140 self.spine_info_label = QLabel()
141 self.spine_info_label.setStyleSheet("font-size: 9pt; color: #0066cc; padding: 5px;")
142 self.spine_info_label.setWordWrap(True)
143 layout.addWidget(self.spine_info_label)
145 group.setLayout(layout)
146 return group
148 def _create_page_size_group(self) -> QGroupBox:
149 """Create the page size group."""
150 group = QGroupBox("Page Size")
151 layout = QVBoxLayout()
153 # Width
154 width_layout = QHBoxLayout()
155 width_layout.addWidget(QLabel("Width:"))
156 self.width_spinbox = QDoubleSpinBox()
157 self.width_spinbox.setRange(10, 1000)
158 self.width_spinbox.setSuffix(" mm")
159 width_layout.addWidget(self.width_spinbox)
160 layout.addLayout(width_layout)
162 # Height
163 height_layout = QHBoxLayout()
164 height_layout.addWidget(QLabel("Height:"))
165 self.height_spinbox = QDoubleSpinBox()
166 self.height_spinbox.setRange(10, 1000)
167 self.height_spinbox.setSuffix(" mm")
168 height_layout.addWidget(self.height_spinbox)
169 layout.addLayout(height_layout)
171 # Apply scope radio buttons
172 scope_label = QLabel("Apply to:")
173 layout.addWidget(scope_label)
175 self._apply_scope_group = QButtonGroup(self)
176 self.scope_page_only = QRadioButton("This page only")
177 self.scope_non_manual = QRadioButton("All non-manual pages")
178 self.scope_all_pages = QRadioButton("All pages (override manual sizing)")
179 self.scope_page_only.setChecked(True)
181 self._apply_scope_group.addButton(self.scope_page_only, 0)
182 self._apply_scope_group.addButton(self.scope_non_manual, 1)
183 self._apply_scope_group.addButton(self.scope_all_pages, 2)
185 layout.addWidget(self.scope_page_only)
186 layout.addWidget(self.scope_non_manual)
187 layout.addWidget(self.scope_all_pages)
189 group.setLayout(layout)
190 return group
192 def _create_dpi_settings_group(self) -> QGroupBox:
193 """Create the DPI settings group."""
194 group = QGroupBox("DPI Settings")
195 layout = QVBoxLayout()
197 # Working DPI
198 working_dpi_layout = QHBoxLayout()
199 working_dpi_layout.addWidget(QLabel("Working DPI:"))
200 self.working_dpi_spinbox = QSpinBox()
201 self.working_dpi_spinbox.setRange(72, 1200)
202 self.working_dpi_spinbox.setValue(self.project.working_dpi)
203 working_dpi_layout.addWidget(self.working_dpi_spinbox)
204 layout.addLayout(working_dpi_layout)
206 # Export DPI
207 export_dpi_layout = QHBoxLayout()
208 export_dpi_layout.addWidget(QLabel("Export DPI:"))
209 self.export_dpi_spinbox = QSpinBox()
210 self.export_dpi_spinbox.setRange(72, 1200)
211 self.export_dpi_spinbox.setValue(self.project.export_dpi)
212 export_dpi_layout.addWidget(self.export_dpi_spinbox)
213 layout.addLayout(export_dpi_layout)
215 group.setLayout(layout)
216 return group
218 def _create_button_layout(self) -> QHBoxLayout:
219 """Create dialog button layout."""
220 layout = QHBoxLayout()
222 cancel_btn = QPushButton("Cancel")
223 cancel_btn.clicked.connect(self.reject)
225 ok_btn = QPushButton("OK")
226 ok_btn.clicked.connect(self.accept)
227 ok_btn.setDefault(True)
229 layout.addStretch()
230 layout.addWidget(cancel_btn)
231 layout.addWidget(ok_btn)
233 return layout
235 def _connect_signals(self):
236 """Connect widget signals to handlers."""
237 self.page_combo.currentIndexChanged.connect(self._on_page_changed)
238 self.cover_checkbox.stateChanged.connect(self._update_spine_info)
239 self.thickness_spinbox.valueChanged.connect(self._update_spine_info)
240 self.bleed_spinbox.valueChanged.connect(self._update_spine_info)
242 def _initialize_values(self):
243 """Initialize dialog values based on current page."""
244 # Set initial page selection
245 if 0 <= self.initial_page_index < len(self.project.pages):
246 self.page_combo.setCurrentIndex(self.initial_page_index)
248 # Trigger initial page change to populate values
249 self._on_page_changed(self.initial_page_index)
251 def _on_page_changed(self, index: int):
252 """
253 Handle page selection change.
255 Args:
256 index: Index of selected page
257 """
258 if index < 0 or index >= len(self.project.pages):
259 return
261 selected_page = self.project.pages[index]
262 is_first_page = index == 0
264 # Show/hide cover settings based on page selection
265 self._cover_group.setVisible(is_first_page)
267 # Update cover checkbox
268 if is_first_page:
269 self.cover_checkbox.setChecked(selected_page.is_cover)
270 self._update_spine_info()
272 # Get display width (accounting for double spreads and covers)
273 if selected_page.is_cover:
274 # For covers, show the full calculated width
275 display_width = selected_page.layout.size[0]
276 elif selected_page.is_double_spread:
277 display_width = (
278 selected_page.layout.base_width
279 if hasattr(selected_page.layout, "base_width")
280 else selected_page.layout.size[0] / 2
281 )
282 else:
283 display_width = selected_page.layout.size[0]
285 self.width_spinbox.setValue(display_width)
286 self.height_spinbox.setValue(selected_page.layout.size[1])
288 # Disable size editing for covers (auto-calculated)
289 is_cover = selected_page.is_cover
290 self.width_spinbox.setEnabled(not is_cover)
291 self.height_spinbox.setEnabled(not is_cover)
292 self.scope_non_manual.setEnabled(not is_cover)
293 self.scope_all_pages.setEnabled(not is_cover)
294 if is_cover:
295 self.scope_page_only.setChecked(True)
297 def _update_spine_info(self):
298 """Update the spine information display."""
299 if self.cover_checkbox.isChecked():
300 # Calculate spine width with current settings
301 content_pages = sum(p.get_page_count() for p in self.project.pages if not p.is_cover)
302 sheets = math.ceil(content_pages / 4)
303 spine_width = sheets * self.thickness_spinbox.value() * 2
305 page_width = self.project.page_size_mm[0]
306 total_width = (page_width * 2) + spine_width + (self.bleed_spinbox.value() * 2)
308 self.spine_info_label.setText(
309 f"Cover Layout: Front ({page_width:.0f}mm) + "
310 f"Spine ({spine_width:.2f}mm) + "
311 f"Back ({page_width:.0f}mm) + "
312 f"Bleed ({self.bleed_spinbox.value():.1f}mm × 2)\n"
313 f"Total Width: {total_width:.1f}mm | "
314 f"Content Pages: {content_pages} | Sheets: {sheets}"
315 )
316 else:
317 self.spine_info_label.setText("")
319 def get_values(self) -> Dict[str, Any]:
320 """
321 Get dialog values.
323 Returns:
324 Dictionary containing all dialog values
325 """
326 selected_index = self.page_combo.currentData()
327 selected_page = self.project.pages[selected_index]
329 return {
330 "selected_index": selected_index,
331 "selected_page": selected_page,
332 "is_cover": self.cover_checkbox.isChecked(),
333 "paper_thickness_mm": self.thickness_spinbox.value(),
334 "cover_bleed_mm": self.bleed_spinbox.value(),
335 "width_mm": self.width_spinbox.value(),
336 "height_mm": self.height_spinbox.value(),
337 "working_dpi": self.working_dpi_spinbox.value(),
338 "export_dpi": self.export_dpi_spinbox.value(),
339 "apply_scope": self._apply_scope_group.checkedId(),
340 }