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

1""" 

2Page Setup Dialog for pyPhotoAlbum 

3 

4Encapsulates all UI logic for page setup configuration, 

5separating presentation from business logic. 

6""" 

7 

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 

25 

26 

27class PageSetupDialog(QDialog): 

28 """ 

29 Dialog for configuring page settings. 

30 

31 This dialog handles all UI presentation logic for page setup, 

32 including page size, DPI settings, and cover configuration. 

33 """ 

34 

35 def __init__(self, parent, project: Project, initial_page_index: int = 0): 

36 """ 

37 Initialize the page setup dialog. 

38 

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 

47 

48 self._setup_ui() 

49 self._connect_signals() 

50 self._initialize_values() 

51 

52 def _setup_ui(self): 

53 """Create and layout all UI components.""" 

54 self.setWindowTitle("Page Setup") 

55 self.setMinimumWidth(450) 

56 

57 layout = QVBoxLayout() 

58 

59 # Page selection group 

60 self._page_select_group = self._create_page_selection_group() 

61 layout.addWidget(self._page_select_group) 

62 

63 # Cover settings group 

64 self._cover_group = self._create_cover_settings_group() 

65 layout.addWidget(self._cover_group) 

66 

67 # Page size group 

68 self._size_group = self._create_page_size_group() 

69 layout.addWidget(self._size_group) 

70 

71 # DPI settings group 

72 self._dpi_group = self._create_dpi_settings_group() 

73 layout.addWidget(self._dpi_group) 

74 

75 # Buttons 

76 button_layout = self._create_button_layout() 

77 layout.addLayout(button_layout) 

78 

79 self.setLayout(layout) 

80 

81 def _create_page_selection_group(self) -> QGroupBox: 

82 """Create the page selection group.""" 

83 group = QGroupBox("Select Page") 

84 layout = QVBoxLayout() 

85 

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) 

96 

97 # Info label 

98 info_label = QLabel("* = Manually sized page") 

99 info_label.setStyleSheet("font-size: 9pt; color: gray;") 

100 layout.addWidget(info_label) 

101 

102 group.setLayout(layout) 

103 return group 

104 

105 def _create_cover_settings_group(self) -> QGroupBox: 

106 """Create the cover settings group.""" 

107 group = QGroupBox("Cover Settings") 

108 layout = QVBoxLayout() 

109 

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) 

114 

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) 

126 

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) 

138 

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) 

144 

145 group.setLayout(layout) 

146 return group 

147 

148 def _create_page_size_group(self) -> QGroupBox: 

149 """Create the page size group.""" 

150 group = QGroupBox("Page Size") 

151 layout = QVBoxLayout() 

152 

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) 

161 

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) 

170 

171 # Apply scope radio buttons 

172 scope_label = QLabel("Apply to:") 

173 layout.addWidget(scope_label) 

174 

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) 

180 

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) 

184 

185 layout.addWidget(self.scope_page_only) 

186 layout.addWidget(self.scope_non_manual) 

187 layout.addWidget(self.scope_all_pages) 

188 

189 group.setLayout(layout) 

190 return group 

191 

192 def _create_dpi_settings_group(self) -> QGroupBox: 

193 """Create the DPI settings group.""" 

194 group = QGroupBox("DPI Settings") 

195 layout = QVBoxLayout() 

196 

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) 

205 

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) 

214 

215 group.setLayout(layout) 

216 return group 

217 

218 def _create_button_layout(self) -> QHBoxLayout: 

219 """Create dialog button layout.""" 

220 layout = QHBoxLayout() 

221 

222 cancel_btn = QPushButton("Cancel") 

223 cancel_btn.clicked.connect(self.reject) 

224 

225 ok_btn = QPushButton("OK") 

226 ok_btn.clicked.connect(self.accept) 

227 ok_btn.setDefault(True) 

228 

229 layout.addStretch() 

230 layout.addWidget(cancel_btn) 

231 layout.addWidget(ok_btn) 

232 

233 return layout 

234 

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) 

241 

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) 

247 

248 # Trigger initial page change to populate values 

249 self._on_page_changed(self.initial_page_index) 

250 

251 def _on_page_changed(self, index: int): 

252 """ 

253 Handle page selection change. 

254 

255 Args: 

256 index: Index of selected page 

257 """ 

258 if index < 0 or index >= len(self.project.pages): 

259 return 

260 

261 selected_page = self.project.pages[index] 

262 is_first_page = index == 0 

263 

264 # Show/hide cover settings based on page selection 

265 self._cover_group.setVisible(is_first_page) 

266 

267 # Update cover checkbox 

268 if is_first_page: 

269 self.cover_checkbox.setChecked(selected_page.is_cover) 

270 self._update_spine_info() 

271 

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] 

284 

285 self.width_spinbox.setValue(display_width) 

286 self.height_spinbox.setValue(selected_page.layout.size[1]) 

287 

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) 

296 

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 

304 

305 page_width = self.project.page_size_mm[0] 

306 total_width = (page_width * 2) + spine_width + (self.bleed_spinbox.value() * 2) 

307 

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("") 

318 

319 def get_values(self) -> Dict[str, Any]: 

320 """ 

321 Get dialog values. 

322 

323 Returns: 

324 Dictionary containing all dialog values 

325 """ 

326 selected_index = self.page_combo.currentData() 

327 selected_page = self.project.pages[selected_index] 

328 

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 }