Coverage for pyPhotoAlbum/mixins/operations/template_ops.py: 100%

180 statements  

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

1""" 

2Template operations mixin for pyPhotoAlbum 

3""" 

4 

5from PyQt6.QtWidgets import ( 

6 QInputDialog, 

7 QDialog, 

8 QVBoxLayout, 

9 QLabel, 

10 QComboBox, 

11 QRadioButton, 

12 QButtonGroup, 

13 QPushButton, 

14 QHBoxLayout, 

15 QDoubleSpinBox, 

16) 

17from pyPhotoAlbum.decorators import ribbon_action, undoable_operation 

18 

19 

20class TemplateOperationsMixin: 

21 """Mixin providing template-related operations""" 

22 

23 @ribbon_action( 

24 label="Save as Template", 

25 tooltip="Save current page as a reusable template", 

26 tab="Layout", 

27 group="Templates", 

28 requires_page=True, 

29 ) 

30 def save_page_as_template(self): 

31 """Save current page as a template""" 

32 current_page = self.get_current_page() 

33 if not current_page: 

34 return 

35 

36 # Check if page has any elements 

37 if not current_page.layout.elements: 

38 self.show_warning("Empty Page", "Cannot save an empty page as a template.") 

39 return 

40 

41 # Ask for template name 

42 name, ok = QInputDialog.getText( 

43 self, 

44 "Save Template", 

45 "Enter template name:", 

46 text=f"Template_{len(self.template_manager.list_templates()) + 1}", 

47 ) 

48 

49 if not ok or not name: 

50 return 

51 

52 # Ask for optional description 

53 description, ok = QInputDialog.getText(self, "Template Description", "Enter description (optional):") 

54 

55 if not ok: 

56 description = "" 

57 

58 try: 

59 # Create template from page 

60 template = self.template_manager.create_template_from_page(current_page, name, description) 

61 

62 # Save template 

63 self.template_manager.save_template(template) 

64 

65 self.show_info("Template Saved", f"Template '{name}' has been saved successfully.") 

66 

67 print(f"Saved template: {name}") 

68 

69 except Exception as e: 

70 self.show_error("Error", f"Failed to save template: {str(e)}") 

71 print(f"Error saving template: {e}") 

72 

73 @ribbon_action( 

74 label="New from Template", tooltip="Create a new page from a template", tab="Layout", group="Templates" 

75 ) 

76 def new_page_from_template(self): 

77 """Create a new page from a template""" 

78 # Get available templates 

79 templates = self.template_manager.list_templates() 

80 

81 if not templates: 

82 self.show_info( 

83 "No Templates", "No templates available. Create a template first by using 'Save as Template'." 

84 ) 

85 return 

86 

87 # Create dialog for template selection and options 

88 dialog = QDialog(self) 

89 dialog.setWindowTitle("New Page from Template") 

90 dialog.setMinimumWidth(400) 

91 

92 layout = QVBoxLayout() 

93 

94 # Template selection 

95 layout.addWidget(QLabel("Select Template:")) 

96 template_combo = QComboBox() 

97 template_combo.addItems(templates) 

98 layout.addWidget(template_combo) 

99 

100 layout.addSpacing(10) 

101 

102 # Margin/Spacing percentage 

103 layout.addWidget(QLabel("Margin/Spacing:")) 

104 margin_layout = QHBoxLayout() 

105 margin_spinbox = QDoubleSpinBox() 

106 margin_spinbox.setRange(0.0, 10.0) 

107 margin_spinbox.setValue(2.5) 

108 margin_spinbox.setSuffix("%") 

109 margin_spinbox.setDecimals(1) 

110 margin_spinbox.setSingleStep(0.5) 

111 margin_spinbox.setToolTip("Percentage of page size to use for margins and spacing") 

112 margin_layout.addWidget(margin_spinbox) 

113 margin_layout.addStretch() 

114 layout.addLayout(margin_layout) 

115 

116 layout.addSpacing(10) 

117 

118 # Scaling selection 

119 layout.addWidget(QLabel("Scaling:")) 

120 scale_group = QButtonGroup(dialog) 

121 

122 proportional_radio = QRadioButton("Proportional (maintain aspect ratio)") 

123 scale_group.addButton(proportional_radio, 0) 

124 layout.addWidget(proportional_radio) 

125 

126 stretch_radio = QRadioButton("Stretch to fit") 

127 stretch_radio.setChecked(True) 

128 scale_group.addButton(stretch_radio, 1) 

129 layout.addWidget(stretch_radio) 

130 

131 center_radio = QRadioButton("Center (no scaling)") 

132 scale_group.addButton(center_radio, 2) 

133 layout.addWidget(center_radio) 

134 

135 layout.addSpacing(20) 

136 

137 # Buttons 

138 button_layout = QHBoxLayout() 

139 cancel_btn = QPushButton("Cancel") 

140 cancel_btn.clicked.connect(dialog.reject) 

141 create_btn = QPushButton("Create") 

142 create_btn.clicked.connect(dialog.accept) 

143 create_btn.setDefault(True) 

144 

145 button_layout.addStretch() 

146 button_layout.addWidget(cancel_btn) 

147 button_layout.addWidget(create_btn) 

148 layout.addLayout(button_layout) 

149 

150 dialog.setLayout(layout) 

151 

152 # Show dialog 

153 if dialog.exec() != QDialog.DialogCode.Accepted: 

154 return 

155 

156 # Get selections 

157 template_name = template_combo.currentText() 

158 scale_id = scale_group.checkedId() 

159 margin_percent = margin_spinbox.value() 

160 scale_mode = ["proportional", "stretch", "center"][scale_id] 

161 

162 try: 

163 # Load template 

164 template = self.template_manager.load_template(template_name) 

165 

166 # Create new page from template 

167 new_page_number = len(self.project.pages) + 1 

168 new_page = self.template_manager.create_page_from_template( 

169 template, 

170 page_number=new_page_number, 

171 target_size_mm=self.project.page_size_mm, 

172 scale_mode=scale_mode, 

173 margin_percent=margin_percent, 

174 ) 

175 

176 # Add to project 

177 self.project.add_page(new_page) 

178 

179 # Switch to new page 

180 self.gl_widget.current_page_index = len(self.project.pages) - 1 

181 self.update_view() 

182 

183 self.show_status(f"Created page {new_page_number} from template '{template_name}'", 3000) 

184 print(f"Created page from template: {template_name} with scale_mode={scale_mode}, margin={margin_percent}%") 

185 

186 except Exception as e: 

187 self.show_error("Error", f"Failed to create page from template: {str(e)}") 

188 print(f"Error creating page from template: {e}") 

189 

190 @ribbon_action( 

191 label="Apply Template", 

192 tooltip="Apply a template layout to current page", 

193 tab="Layout", 

194 group="Templates", 

195 requires_page=True, 

196 ) 

197 @undoable_operation(capture="page_elements", description="Apply Template") 

198 def apply_template_to_page(self): 

199 """Apply a template to the current page""" 

200 current_page = self.get_current_page() 

201 if not current_page: 

202 return 

203 

204 # Get available templates 

205 templates = self.template_manager.list_templates() 

206 

207 if not templates: 

208 self.show_info( 

209 "No Templates", "No templates available. Create a template first by using 'Save as Template'." 

210 ) 

211 return 

212 

213 # Create dialog for template application options 

214 dialog = QDialog(self) 

215 dialog.setWindowTitle("Apply Template") 

216 dialog.setMinimumWidth(400) 

217 

218 layout = QVBoxLayout() 

219 

220 # Template selection 

221 layout.addWidget(QLabel("Select Template:")) 

222 template_combo = QComboBox() 

223 template_combo.addItems(templates) 

224 layout.addWidget(template_combo) 

225 

226 layout.addSpacing(10) 

227 

228 # Mode selection 

229 layout.addWidget(QLabel("Mode:")) 

230 mode_group = QButtonGroup(dialog) 

231 

232 replace_radio = QRadioButton("Replace with placeholders") 

233 replace_radio.setChecked(True) 

234 replace_radio.setToolTip("Clear page and add template placeholders") 

235 mode_group.addButton(replace_radio, 0) 

236 layout.addWidget(replace_radio) 

237 

238 reflow_radio = QRadioButton("Reflow existing content") 

239 reflow_radio.setToolTip("Keep existing images and reposition to template slots") 

240 mode_group.addButton(reflow_radio, 1) 

241 layout.addWidget(reflow_radio) 

242 

243 layout.addSpacing(10) 

244 

245 # Margin/Spacing percentage 

246 layout.addWidget(QLabel("Margin/Spacing:")) 

247 margin_layout = QHBoxLayout() 

248 margin_spinbox = QDoubleSpinBox() 

249 margin_spinbox.setRange(0.0, 10.0) 

250 margin_spinbox.setValue(2.5) 

251 margin_spinbox.setSuffix("%") 

252 margin_spinbox.setDecimals(1) 

253 margin_spinbox.setSingleStep(0.5) 

254 margin_spinbox.setToolTip("Percentage of page size to use for margins and spacing") 

255 margin_layout.addWidget(margin_spinbox) 

256 margin_layout.addStretch() 

257 layout.addLayout(margin_layout) 

258 

259 layout.addSpacing(10) 

260 

261 # Scaling selection 

262 layout.addWidget(QLabel("Scaling:")) 

263 scale_group = QButtonGroup(dialog) 

264 

265 proportional_radio = QRadioButton("Proportional (maintain aspect ratio)") 

266 scale_group.addButton(proportional_radio, 0) 

267 layout.addWidget(proportional_radio) 

268 

269 stretch_radio = QRadioButton("Stretch to fit") 

270 stretch_radio.setChecked(True) 

271 scale_group.addButton(stretch_radio, 1) 

272 layout.addWidget(stretch_radio) 

273 

274 center_radio = QRadioButton("Center (no scaling)") 

275 scale_group.addButton(center_radio, 2) 

276 layout.addWidget(center_radio) 

277 

278 layout.addSpacing(20) 

279 

280 # Buttons 

281 button_layout = QHBoxLayout() 

282 cancel_btn = QPushButton("Cancel") 

283 cancel_btn.clicked.connect(dialog.reject) 

284 apply_btn = QPushButton("Apply") 

285 apply_btn.clicked.connect(dialog.accept) 

286 apply_btn.setDefault(True) 

287 

288 button_layout.addStretch() 

289 button_layout.addWidget(cancel_btn) 

290 button_layout.addWidget(apply_btn) 

291 layout.addLayout(button_layout) 

292 

293 dialog.setLayout(layout) 

294 

295 # Show dialog 

296 if dialog.exec() != QDialog.DialogCode.Accepted: 

297 return 

298 

299 # Get selections 

300 template_name = template_combo.currentText() 

301 mode_id = mode_group.checkedId() 

302 scale_id = scale_group.checkedId() 

303 margin_percent = margin_spinbox.value() 

304 

305 mode = "replace" if mode_id == 0 else "reflow" 

306 scale_mode = ["proportional", "stretch", "center"][scale_id] 

307 

308 try: 

309 # Load template 

310 template = self.template_manager.load_template(template_name) 

311 

312 # Apply template to page 

313 self.template_manager.apply_template_to_page( 

314 template, current_page, mode=mode, scale_mode=scale_mode, margin_percent=margin_percent 

315 ) 

316 

317 # Update display 

318 self.update_view() 

319 

320 self.show_status(f"Applied template '{template_name}' to current page", 3000) 

321 print(f"Applied template '{template_name}' with mode={mode}, scale_mode={scale_mode}") 

322 

323 except Exception as e: 

324 self.show_error("Error", f"Failed to apply template: {str(e)}") 

325 print(f"Error applying template: {e}")