Coverage for main/components/folder_card.py: 92%

160 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-03 15:51 +0000

1import os 

2from PyQt6.QtCore import ( 

3 pyqtProperty, QPropertyAnimation, QSize, Qt, 

4 QEasingCurve, QSequentialAnimationGroup, QParallelAnimationGroup, pyqtSignal, QCoreApplication 

5) 

6from PyQt6.QtGui import QColor, QIcon, QPainter, QPen 

7from PyQt6.QtWidgets import ( 

8 QWidget, QVBoxLayout, QPushButton, QDialog, QListWidget, 

9 QListWidgetItem, QHBoxLayout, QLabel, QFrame, QScrollArea, QAbstractItemView 

10) 

11 

12 

13class FolderCard(QWidget): 

14 """ 

15 A modern, self-contained widget representing a monitored output folder. 

16 It shows the folder name, file count, and allows the user to open or inspect 

17 newly generated files. Includes subtle visual animations for updates. 

18 """ 

19 

20 # Signal emitted when the user requests to open the folder externally 

21 open_folder_requested = pyqtSignal(str) 

22 

23 def __init__(self, context, folder): 

24 """ 

25 Initialize the FolderCard. 

26 

27 Args: 

28 context (dict): Shared application context (unused here, reserved for integration). 

29 folder (str): Path to the folder being represented by this card. 

30 """ 

31 super().__init__() 

32 

33 self.folder = folder 

34 self.files = [] # Tracks new files added since the last check 

35 self.is_finished = False 

36 # self.existing_files = set(os.listdir(folder)) if os.path.isdir(folder) else set() 

37 self.existing_files = self._list_all_files(folder) 

38 

39 # Animation and visual properties 

40 self.pulse_animation = None 

41 self._scale = 1.0 

42 self._glow_opacity = 0.0 

43 

44 # Define a fixed height for uniform card size 

45 self.setFixedHeight(100) 

46 

47 # Build the UI 

48 self._setup_ui() 

49 

50 # ------------------------------------------------------------------------- 

51 # UI setup 

52 # ------------------------------------------------------------------------- 

53 def _setup_ui(self): 

54 """Configure the visual layout and styling for the folder card.""" 

55 layout = QVBoxLayout(self) 

56 layout.setContentsMargins(8, 8, 8, 8) 

57 

58 # Main container frame with light elevation 

59 self.card_frame = QFrame() 

60 self.card_frame.setObjectName("outputsCard") 

61 self.card_frame.setStyleSheet(""" 

62 QFrame#outputsCard { 

63 background-color: white; 

64 border-radius: 12px; 

65 border: 1px solid #e0e0e0; 

66 } 

67 """) 

68 

69 # Horizontal layout for card contents 

70 card_layout = QHBoxLayout(self.card_frame) 

71 card_layout.setContentsMargins(16, 12, 16, 12) 

72 card_layout.setSpacing(12) 

73 

74 # Left section: folder icon 

75 self.folder_icon = QLabel("📁") 

76 self.folder_icon.setStyleSheet(""" 

77 font-size: 32px; 

78 padding: 8px; 

79 background-color: #f0f7ff; 

80 border-radius: 8px; 

81 """) 

82 self.folder_icon.setFixedSize(56, 56) 

83 self.folder_icon.setAlignment(Qt.AlignmentFlag.AlignCenter) 

84 card_layout.addWidget(self.folder_icon) 

85 

86 # Center section: folder name and status 

87 info_layout = QVBoxLayout() 

88 info_layout.setSpacing(4) 

89 

90 # Folder name label 

91 self.folder_name = QLabel(os.path.basename(self.folder)) 

92 self.folder_name.setStyleSheet(""" 

93 font-size: 14px; 

94 font-weight: 600; 

95 color: #2c3e50; 

96 """) 

97 info_layout.addWidget(self.folder_name) 

98 

99 # Status label (e.g., “Waiting for files…” or “3 new files”) 

100 self.status_label = QLabel(QCoreApplication.translate("Components", "Waiting for files...")) 

101 self.status_label.setStyleSheet(""" 

102 font-size: 12px; 

103 color: #7f8c8d; 

104 """) 

105 info_layout.addWidget(self.status_label) 

106 

107 card_layout.addLayout(info_layout, stretch=1) 

108 

109 # Right section: action button 

110 self.action_btn = QPushButton(QCoreApplication.translate("Components", "Visualize")) 

111 self.action_btn.setStyleSheet(""" 

112 QPushButton { 

113 background-color: #3498db; 

114 color: white; 

115 border: none; 

116 border-radius: 8px; 

117 padding: 8px 20px; 

118 font-size: 13px; 

119 font-weight: 600; 

120 } 

121 QPushButton:hover { 

122 background-color: #2980b9; 

123 } 

124 QPushButton:pressed { 

125 background-color: #21618c; 

126 } 

127 QPushButton:disabled { 

128 background-color: #bdc3c7; 

129 color: #7f8c8d; 

130 } 

131 """) 

132 self.action_btn.clicked.connect(self.show_files_dialog) 

133 self.action_btn.setEnabled(False) 

134 card_layout.addWidget(self.action_btn) 

135 

136 layout.addWidget(self.card_frame) 

137 

138 # ------------------------------------------------------------------------- 

139 # File handling and state updates 

140 # ------------------------------------------------------------------------- 

141 def add_files(self, new_files): 

142 """ 

143 Add new files to the card, update the UI, and trigger a pulse animation. 

144 

145 Args: 

146 new_files (list[str]): Names of newly detected files. 

147 """ 

148 if self.is_finished: 

149 return 

150 

151 self.files.extend(new_files) 

152 file_count = len(self.files) 

153 

154 # Update visual state to show activity 

155 self.status_label.setText( 

156 QCoreApplication.translate("Components", "{file_count} new file").format(file_count=file_count) 

157 ) 

158 self.status_label.setStyleSheet(""" 

159 font-size: 12px; 

160 color: #27ae60; 

161 font-weight: 600; 

162 """) 

163 

164 self.action_btn.setEnabled(True) 

165 

166 # Change the icon and color scheme to indicate success 

167 self.folder_icon.setText("✓") 

168 self.folder_icon.setStyleSheet(""" 

169 font-size: 32px; 

170 padding: 8px; 

171 background-color: #d5f4e6; 

172 border-radius: 8px; 

173 color: #27ae60; 

174 """) 

175 

176 # Update card border to highlight new content 

177 self.card_frame.setStyleSheet(""" 

178 QFrame#outputsCard { 

179 background-color: white; 

180 border-radius: 12px; 

181 border: 2px solid #27ae60; 

182 } 

183 """) 

184 

185 # Trigger pulse animation for visual feedback 

186 self.start_pulse_animation() 

187 

188 def start_pulse_animation(self): 

189 """ 

190 Perform a subtle pulsing height animation when new files are added. 

191 """ 

192 if self.pulse_animation: 

193 self.pulse_animation.stop() 

194 

195 self.pulse_animation = QSequentialAnimationGroup() 

196 

197 # Expand slightly 

198 pulse1 = QPropertyAnimation(self.card_frame, b"maximumHeight") 

199 pulse1.setDuration(300) 

200 pulse1.setStartValue(100) 

201 pulse1.setEndValue(104) 

202 pulse1.setEasingCurve(QEasingCurve.Type.OutCubic) 

203 

204 # Contract back 

205 pulse2 = QPropertyAnimation(self.card_frame, b"maximumHeight") 

206 pulse2.setDuration(300) 

207 pulse2.setStartValue(104) 

208 pulse2.setEndValue(100) 

209 pulse2.setEasingCurve(QEasingCurve.Type.InCubic) 

210 

211 self.pulse_animation.addAnimation(pulse1) 

212 self.pulse_animation.addAnimation(pulse2) 

213 self.pulse_animation.start() 

214 

215 def reset_state(self): 

216 """ 

217 Reset the card’s appearance to its initial “waiting” state. 

218 Called after viewing or clearing files. 

219 """ 

220 self.files.clear() 

221 self.is_finished = False # Resetta lo stato 

222 self.existing_files = self._list_all_files(self.folder) 

223 

224 self.status_label.setText(QCoreApplication.translate("Components", "Waiting for files...")) 

225 self.status_label.setStyleSheet(""" 

226 font-size: 12px; 

227 color: #7f8c8d; 

228 """) 

229 

230 self.folder_icon.setText("📁") 

231 self.folder_icon.setStyleSheet(""" 

232 font-size: 32px; 

233 padding: 8px; 

234 background-color: #f0f7ff; 

235 border-radius: 8px; 

236 """) 

237 

238 self.card_frame.setStyleSheet(""" 

239 QFrame#outputsCard { 

240 background-color: white; 

241 border-radius: 12px; 

242 border: 1px solid #e0e0e0; 

243 } 

244 """) 

245 

246 self.action_btn.setEnabled(False) 

247 

248 if self.pulse_animation: 

249 self.pulse_animation.stop() 

250 

251 # ------------------------------------------------------------------------- 

252 # Dialog for file inspection 

253 # ------------------------------------------------------------------------- 

254 def show_files_dialog(self): 

255 """ 

256 Open a dialog window displaying all new files detected for this folder. 

257 Provides options to open the folder or close the dialog. 

258 """ 

259 if not self.files: 

260 return 

261 

262 dialog = QDialog(self) 

263 dialog.setWindowTitle( 

264 QCoreApplication.translate("Components", "Generated file - {0}") 

265 .format(os.path.basename(self.folder)) 

266 ) 

267 dialog.setModal(True) 

268 dialog.setMinimumSize(600, 500) 

269 

270 layout = QVBoxLayout(dialog) 

271 layout.setContentsMargins(20, 20, 20, 20) 

272 layout.setSpacing(16) 

273 

274 # Header with file count 

275 header = QLabel( 

276 QCoreApplication.translate("Components", "📊 {0} generated file").format(len(self.files)) 

277 ) 

278 header.setStyleSheet(""" 

279 font-size: 18px; 

280 font-weight: bold; 

281 color: #2c3e50; 

282 padding: 8px; 

283 """) 

284 layout.addWidget(header) 

285 

286 # Folder path subtitle 

287 path_label = QLabel(f"📁 {self.folder}") 

288 path_label.setStyleSheet(""" 

289 font-size: 12px; 

290 color: #7f8c8d; 

291 padding: 0 8px 8px 8px; 

292 """) 

293 path_label.setWordWrap(True) 

294 layout.addWidget(path_label) 

295 

296 # File list area 

297 list_widget = QListWidget() 

298 list_widget.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) 

299 list_widget.setFocusPolicy(Qt.FocusPolicy.NoFocus) 

300 list_widget.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) 

301 list_widget.setStyleSheet(""" 

302 QListWidget { 

303 border: 1px solid #e0e0e0; 

304 border-radius: 8px; 

305 background-color: #fafafa; 

306 padding: 8px; 

307 } 

308 QListWidget::item { 

309 padding: 12px; 

310 border-radius: 6px; 

311 margin: 2px; 

312 } 

313 """) 

314 

315 # Populate list with filenames 

316 for f in sorted(self.files): 

317 item = QListWidgetItem(f"📄 {f.replace(os.sep, '/')}") 

318 item.setToolTip(os.path.join(self.folder, f)) 

319 list_widget.addItem(item) 

320 

321 layout.addWidget(list_widget) 

322 

323 # Footer buttons 

324 button_layout = QHBoxLayout() 

325 button_layout.setSpacing(12) 

326 

327 # Button: open containing folder 

328 btn_open_folder = QPushButton(QCoreApplication.translate("Components", "📂 Open Folder")) 

329 btn_open_folder.setStyleSheet(""" 

330 QPushButton { 

331 background-color: #3498db; 

332 color: white; 

333 border: none; 

334 border-radius: 8px; 

335 padding: 12px 24px; 

336 font-size: 14px; 

337 font-weight: 600; 

338 } 

339 QPushButton:hover { 

340 background-color: #2980b9; 

341 } 

342 """) 

343 btn_open_folder.clicked.connect(lambda: self.open_folder_requested.emit(self.folder)) 

344 

345 # Button: close dialog 

346 btn_close = QPushButton(QCoreApplication.translate("Components", "Close")) 

347 btn_close.setStyleSheet(""" 

348 QPushButton { 

349 background-color: #ecf0f1; 

350 color: #2c3e50; 

351 border: 1px solid #bdc3c7; 

352 border-radius: 8px; 

353 padding: 12px 24px; 

354 font-size: 14px; 

355 font-weight: 600; 

356 } 

357 QPushButton:hover { 

358 background-color: #e0e0e0; 

359 } 

360 """) 

361 btn_close.clicked.connect(dialog.accept) 

362 

363 button_layout.addWidget(btn_open_folder) 

364 button_layout.addStretch() 

365 button_layout.addWidget(btn_close) 

366 

367 layout.addLayout(button_layout) 

368 

369 dialog.exec() 

370 

371 # After closing the dialog, clear and reset 

372 self.files.clear() 

373 self.reset_state() 

374 

375 # ------------------------------------------------------------------------- 

376 # Monitoring logic 

377 # ------------------------------------------------------------------------- 

378 def check_new_files(self): 

379 """ 

380 Check if new files have appeared in the monitored folder. 

381 If new files are found, they are added and the UI is updated. 

382 """ 

383 if self.is_finished: 

384 return 

385 

386 if not os.path.isdir(self.folder): 

387 return 

388 

389 current_files = self._list_all_files(self.folder) 

390 new_files = current_files - self.existing_files 

391 

392 if new_files: 

393 self.add_files(list(new_files)) 

394 

395 self.existing_files = current_files 

396 

397 def _list_all_files(self, root_folder): 

398 """Restituisce un set con tutti i file nella cartella e nelle sottocartelle.""" 

399 all_files = set() 

400 if not os.path.isdir(root_folder): 

401 return all_files 

402 for dirpath, _, filenames in os.walk(root_folder): 

403 for f in filenames: 

404 # Salva percorsi relativi (rispetto alla root) per coerenza 

405 relative_path = os.path.relpath(os.path.join(dirpath, f), root_folder) 

406 all_files.add(relative_path) 

407 return all_files 

408 

409 def set_finished_state(self): 

410 """ 

411 Sets the card to a visual "Completed" state. 

412 Performs a final file check and updates the interface. 

413 """ 

414 if self.is_finished: 

415 return 

416 

417 self.is_finished = True 

418 

419 self.check_new_files() 

420 

421 file_count = len(self.existing_files) 

422 

423 self.status_label.setText( 

424 QCoreApplication.translate("Components", "Processing complete ({file_count} files)") 

425 .format(file_count=file_count) 

426 ) 

427 self.status_label.setStyleSheet(""" 

428 font-size: 12px; 

429 color: #27ae60; 

430 font-weight: 600; 

431 """) 

432 

433 self.action_btn.setEnabled(False) 

434 

435 self.folder_icon.setText("✓") 

436 self.folder_icon.setStyleSheet(""" 

437 font-size: 32px; 

438 padding: 8px; 

439 background-color: #d5f4e6; 

440 border-radius: 8px; 

441 color: #27ae60; 

442 """) 

443 

444 self.card_frame.setStyleSheet(""" 

445 QFrame#outputsCard { 

446 background-color: white; 

447 border-radius: 12px; 

448 border: 2px solid #27ae60; 

449 } 

450 """)