1 """

2 Frame picker dialog for pyPhotoAlbum

3

4 Dialog for selecting decorative frames to apply to images.

5 """

6

7 from typing import Optional, Tuple

8 from PyQt6.QtWidgets import (

9 QDialog,

10 QVBoxLayout,

11 QHBoxLayout,

12 QLabel,

13 QPushButton,

14 QTabWidget,

15 QWidget,

16 QGridLayout,

17 QScrollArea,

18 QFrame,

19 QGroupBox,

20 QCheckBox,

21 )

22 from PyQt6.QtCore import Qt, pyqtSignal

23 from PyQt6.QtGui import QPainter, QColor, QPen

24

25 from pyPhotoAlbum.frame_manager import get_frame_manager, FrameCategory, FrameDefinition, FrameType

26

27

28 class FramePreviewWidget(QFrame):

29 """Widget that shows a preview of a frame"""

30

31 clicked = pyqtSignal(str) # Emits frame name when clicked

32

33 def __init__(self, frame: FrameDefinition, parent=None):

34 super().__init__(parent)

35 self.frame = frame

36 self.selected = False

37 self.setFixedSize(100, 100)

38 self.setFrameStyle(QFrame.Shape.Box)

39 self.setCursor(Qt.CursorShape.PointingHandCursor)

40

41 def paintEvent(self, event):

42 super().paintEvent(event)

43 painter = QPainter(self)

44 painter.setRenderHint(QPainter.RenderHint.Antialiasing)

45

46 # Background

47 if self.selected:

48 painter.fillRect(self.rect(), QColor(200, 220, 255))

49 else:

50 painter.fillRect(self.rect(), QColor(245, 245, 245))

51

52 # Draw a simple preview of the frame style

53 margin = 15

54 rect = self.rect().adjusted(margin, margin, -margin, -margin)

55

56 # Draw "photo" placeholder

57 painter.fillRect(rect, QColor(180, 200, 220))

58

59 # Draw frame preview based on type

60 pen = QPen(QColor(80, 80, 80))

61 pen.setWidth(2)

62 painter.setPen(pen)

63

64 if self.frame.frame_type.value == "corners":

65 # Draw corner decorations

66 corner_size = 12

67 x, y, w, h = rect.x(), rect.y(), rect.width(), rect.height()

68

69 # Top-left

70 painter.drawLine(x, y + corner_size, x, y)

71 painter.drawLine(x, y, x + corner_size, y)

72

73 # Top-right

74 painter.drawLine(x + w - corner_size, y, x + w, y)

75 painter.drawLine(x + w, y, x + w, y + corner_size)

76

77 # Bottom-right

78 painter.drawLine(x + w, y + h - corner_size, x + w, y + h)

79 painter.drawLine(x + w, y + h, x + w - corner_size, y + h)

80

81 # Bottom-left

82 painter.drawLine(x + corner_size, y + h, x, y + h)

83 painter.drawLine(x, y + h, x, y + h - corner_size)

84

85 else:

86 # Draw full border

87 painter.drawRect(rect.adjusted(-3, -3, 3, 3))

88 painter.drawRect(rect)

89

90 # Draw frame name

91 painter.setPen(QColor(0, 0, 0))

92 text_rect = self.rect().adjusted(0, 0, 0, 0)

93 text_rect.setTop(self.rect().bottom() - 20)

94 painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, self.frame.display_name)

95

96 def mousePressEvent(self, event):

97 self.clicked.emit(self.frame.name)

98

99 def set_selected(self, selected: bool):

100 self.selected = selected

101 self.update()

102

103

104 class FramePickerDialog(QDialog):

105 """Dialog for selecting a decorative frame"""

106

107 def __init__(

108 self,

109 parent,

110 current_frame: Optional[str] = None,

111 current_color: Tuple[int, int, int] = (0, 0, 0),

112 current_corners: Tuple[bool, bool, bool, bool] = (True, True, True, True),

113 ):

114 super().__init__(parent)

115 self.setWindowTitle("Select Frame")

116 self.setMinimumSize(500, 500)

117

118 self.selected_frame: Optional[str] = current_frame

119 self.frame_color = current_color

120 self.frame_corners = current_corners # (TL, TR, BR, BL)

121 self.frame_widgets: dict[str, FramePreviewWidget] = {}

122

123 self._setup_ui()

124

125 def _setup_ui(self):

126 layout = QVBoxLayout(self)

127

128 # Tab widget for categories

129 self.tab_widget = QTabWidget()

130

131 # All frames tab

132 all_tab = self._create_category_tab(None)

133 self.tab_widget.addTab(all_tab, "All")

134

135 # Category tabs

136 for category in FrameCategory:

137 tab = self._create_category_tab(category)

138 self.tab_widget.addTab(tab, category.value.title())

139

140 layout.addWidget(self.tab_widget)

141

142 # Selected frame info

143 info_group = QGroupBox("Selected Frame")

144 info_layout = QVBoxLayout(info_group)

145

146 # Frame name and color row

147 name_color_layout = QHBoxLayout()

148 self.selected_label = QLabel("None")

149 name_color_layout.addWidget(self.selected_label)

150

151 # Color button

152 from pyPhotoAlbum.dialogs.style_dialogs import ColorButton

153

154 name_color_layout.addWidget(QLabel("Color:"))

155 self.color_btn = ColorButton(self.frame_color)

156 name_color_layout.addWidget(self.color_btn)

157 name_color_layout.addStretch()

158 info_layout.addLayout(name_color_layout)

159

160 # Corner selection (for corner-type frames)

161 self.corners_group = QGroupBox("Corner Decorations")

162 corners_layout = QGridLayout(self.corners_group)

163

164 # Create a visual grid for corner checkboxes

165 self.corner_tl = QCheckBox("Top-Left")

166 self.corner_tl.setChecked(self.frame_corners[0])

167 self.corner_tl.stateChanged.connect(self._update_corners)

168

169 self.corner_tr = QCheckBox("Top-Right")

170 self.corner_tr.setChecked(self.frame_corners[1])

171 self.corner_tr.stateChanged.connect(self._update_corners)

172

173 self.corner_br = QCheckBox("Bottom-Right")

174 self.corner_br.setChecked(self.frame_corners[2])

175 self.corner_br.stateChanged.connect(self._update_corners)

176

177 self.corner_bl = QCheckBox("Bottom-Left")

178 self.corner_bl.setChecked(self.frame_corners[3])

179 self.corner_bl.stateChanged.connect(self._update_corners)

180

181 corners_layout.addWidget(self.corner_tl, 0, 0)

182 corners_layout.addWidget(self.corner_tr, 0, 1)

183 corners_layout.addWidget(self.corner_bl, 1, 0)

184 corners_layout.addWidget(self.corner_br, 1, 1)

185

186 # Quick selection buttons

187 quick_btns_layout = QHBoxLayout()

188 all_btn = QPushButton("All")

189 all_btn.clicked.connect(self._select_all_corners)

190 none_btn = QPushButton("None")

191 none_btn.clicked.connect(self._select_no_corners)

192 diag_btn = QPushButton("Diagonal")

193 diag_btn.clicked.connect(self._select_diagonal_corners)

194 quick_btns_layout.addWidget(all_btn)

195 quick_btns_layout.addWidget(none_btn)

196 quick_btns_layout.addWidget(diag_btn)

197 quick_btns_layout.addStretch()

198 corners_layout.addLayout(quick_btns_layout, 2, 0, 1, 2)

199

200 info_layout.addWidget(self.corners_group)

201

202 layout.addWidget(info_group)

203

204 # Update corners group visibility based on frame type

205 self._update_corners_visibility()

206

207 # Buttons

208 button_layout = QHBoxLayout()

209

210 clear_btn = QPushButton("No Frame")

211 clear_btn.clicked.connect(self._clear_selection)

212 button_layout.addWidget(clear_btn)

213

214 button_layout.addStretch()

215

216 ok_btn = QPushButton("OK")

217 ok_btn.clicked.connect(self.accept)

218 button_layout.addWidget(ok_btn)

219

220 cancel_btn = QPushButton("Cancel")

221 cancel_btn.clicked.connect(self.reject)

222 button_layout.addWidget(cancel_btn)

223

224 layout.addLayout(button_layout)

225

226 # Update selection display

227 self._update_selection_display()

228

229 def _create_category_tab(self, category: Optional[FrameCategory]) -> QWidget:

230 """Create a tab for a frame category"""

231 widget = QWidget()

232 layout = QVBoxLayout(widget)

233

234 scroll = QScrollArea()

235 scroll.setWidgetResizable(True)

236 scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)

237

238 content = QWidget()

239 grid = QGridLayout(content)

240 grid.setSpacing(10)

241

242 frame_manager = get_frame_manager()

243

244 if category:

245 frames = frame_manager.get_frames_by_category(category)

246 else:

247 frames = frame_manager.get_all_frames()

248

249 row, col = 0, 0

250 max_cols = 4

251

252 for frame in frames:

253 preview = FramePreviewWidget(frame)

254 preview.clicked.connect(self._on_frame_clicked)

255

256 if frame.name == self.selected_frame:

257 preview.set_selected(True)

258

259 grid.addWidget(preview, row, col)

260 self.frame_widgets[frame.name] = preview

261

262 col += 1

263 if col >= max_cols:

264 col = 0

265 row += 1

266

267 # Add stretch at the bottom

268 grid.setRowStretch(row + 1, 1)

269

270 scroll.setWidget(content)

271 layout.addWidget(scroll)

272

273 return widget

274

275 def _on_frame_clicked(self, frame_name: str):

276 """Handle frame selection"""

277 # Deselect previous

278 if self.selected_frame and self.selected_frame in self.frame_widgets:

279 self.frame_widgets[self.selected_frame].set_selected(False)

280

281 # Select new

282 self.selected_frame = frame_name

283 if frame_name in self.frame_widgets:

284 self.frame_widgets[frame_name].set_selected(True)

285

286 self._update_selection_display()

287 self._update_corners_visibility()

288

289 def _clear_selection(self):

290 """Clear frame selection"""

291 if self.selected_frame and self.selected_frame in self.frame_widgets:

292 self.frame_widgets[self.selected_frame].set_selected(False)

293 self.selected_frame = None

294 self._update_selection_display()

295 self._update_corners_visibility()

296

297 def _update_selection_display(self):

298 """Update the selected frame label"""

299 if self.selected_frame:

300 frame = get_frame_manager().get_frame(self.selected_frame)

301 if frame:

302 self.selected_label.setText(f"{frame.display_name} - {frame.description}")

303 else:

304 self.selected_label.setText(self.selected_frame)

305 else:

306 self.selected_label.setText("None")

307

308 def _update_corners(self):

309 """Update corner selection from checkboxes"""

310 self.frame_corners = (

311 self.corner_tl.isChecked(),

312 self.corner_tr.isChecked(),

313 self.corner_br.isChecked(),

314 self.corner_bl.isChecked(),

315 )

316

317 def _update_corners_visibility(self):

318 """Show/hide corners group based on selected frame type"""

319 if self.selected_frame:

320 frame = get_frame_manager().get_frame(self.selected_frame)

321 if frame and frame.frame_type == FrameType.CORNERS:

322 self.corners_group.setVisible(True)

323 return

324 self.corners_group.setVisible(False)

325

326 def _select_all_corners(self):

327 """Select all corners"""

328 self.corner_tl.setChecked(True)

329 self.corner_tr.setChecked(True)

330 self.corner_br.setChecked(True)

331 self.corner_bl.setChecked(True)

332 self._update_corners()

333

334 def _select_no_corners(self):

335 """Deselect all corners"""

336 self.corner_tl.setChecked(False)

337 self.corner_tr.setChecked(False)

338 self.corner_br.setChecked(False)

339 self.corner_bl.setChecked(False)

340 self._update_corners()

341

342 def _select_diagonal_corners(self):

343 """Select diagonal corners (TL and BR)"""

344 self.corner_tl.setChecked(True)

345 self.corner_tr.setChecked(False)

346 self.corner_br.setChecked(True)

347 self.corner_bl.setChecked(False)

348 self._update_corners()

349

350 def get_values(self) -> Tuple[Optional[str], Tuple[int, int, int], Tuple[bool, bool, bool, bool]]:

351 """Get selected frame name, color, and corner configuration"""

352 return self.selected_frame, self.color_btn.get_color(), self.frame_corners