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
« 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)
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 """
20 # Signal emitted when the user requests to open the folder externally
21 open_folder_requested = pyqtSignal(str)
23 def __init__(self, context, folder):
24 """
25 Initialize the FolderCard.
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__()
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)
39 # Animation and visual properties
40 self.pulse_animation = None
41 self._scale = 1.0
42 self._glow_opacity = 0.0
44 # Define a fixed height for uniform card size
45 self.setFixedHeight(100)
47 # Build the UI
48 self._setup_ui()
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)
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 """)
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)
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)
86 # Center section: folder name and status
87 info_layout = QVBoxLayout()
88 info_layout.setSpacing(4)
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)
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)
107 card_layout.addLayout(info_layout, stretch=1)
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)
136 layout.addWidget(self.card_frame)
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.
145 Args:
146 new_files (list[str]): Names of newly detected files.
147 """
148 if self.is_finished:
149 return
151 self.files.extend(new_files)
152 file_count = len(self.files)
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 """)
164 self.action_btn.setEnabled(True)
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 """)
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 """)
185 # Trigger pulse animation for visual feedback
186 self.start_pulse_animation()
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()
195 self.pulse_animation = QSequentialAnimationGroup()
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)
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)
211 self.pulse_animation.addAnimation(pulse1)
212 self.pulse_animation.addAnimation(pulse2)
213 self.pulse_animation.start()
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)
224 self.status_label.setText(QCoreApplication.translate("Components", "Waiting for files..."))
225 self.status_label.setStyleSheet("""
226 font-size: 12px;
227 color: #7f8c8d;
228 """)
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 """)
238 self.card_frame.setStyleSheet("""
239 QFrame#outputsCard {
240 background-color: white;
241 border-radius: 12px;
242 border: 1px solid #e0e0e0;
243 }
244 """)
246 self.action_btn.setEnabled(False)
248 if self.pulse_animation:
249 self.pulse_animation.stop()
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
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)
270 layout = QVBoxLayout(dialog)
271 layout.setContentsMargins(20, 20, 20, 20)
272 layout.setSpacing(16)
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)
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)
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 """)
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)
321 layout.addWidget(list_widget)
323 # Footer buttons
324 button_layout = QHBoxLayout()
325 button_layout.setSpacing(12)
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))
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)
363 button_layout.addWidget(btn_open_folder)
364 button_layout.addStretch()
365 button_layout.addWidget(btn_close)
367 layout.addLayout(button_layout)
369 dialog.exec()
371 # After closing the dialog, clear and reset
372 self.files.clear()
373 self.reset_state()
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
386 if not os.path.isdir(self.folder):
387 return
389 current_files = self._list_all_files(self.folder)
390 new_files = current_files - self.existing_files
392 if new_files:
393 self.add_files(list(new_files))
395 self.existing_files = current_files
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
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
417 self.is_finished = True
419 self.check_new_files()
421 file_count = len(self.existing_files)
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 """)
433 self.action_btn.setEnabled(False)
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 """)
444 self.card_frame.setStyleSheet("""
445 QFrame#outputsCard {
446 background-color: white;
447 border-radius: 12px;
448 border: 2px solid #27ae60;
449 }
450 """)