components.folder_card

  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        """)
class FolderCard(PyQt6.QtWidgets.QWidget):
 14class FolderCard(QWidget):
 15    """
 16    A modern, self-contained widget representing a monitored output folder.
 17    It shows the folder name, file count, and allows the user to open or inspect
 18    newly generated files. Includes subtle visual animations for updates.
 19    """
 20
 21    # Signal emitted when the user requests to open the folder externally
 22    open_folder_requested = pyqtSignal(str)
 23
 24    def __init__(self, context, folder):
 25        """
 26        Initialize the FolderCard.
 27
 28        Args:
 29            context (dict): Shared application context (unused here, reserved for integration).
 30            folder (str): Path to the folder being represented by this card.
 31        """
 32        super().__init__()
 33
 34        self.folder = folder
 35        self.files = []  # Tracks new files added since the last check
 36        self.is_finished = False
 37        # self.existing_files = set(os.listdir(folder)) if os.path.isdir(folder) else set()
 38        self.existing_files = self._list_all_files(folder)
 39
 40        # Animation and visual properties
 41        self.pulse_animation = None
 42        self._scale = 1.0
 43        self._glow_opacity = 0.0
 44
 45        # Define a fixed height for uniform card size
 46        self.setFixedHeight(100)
 47
 48        # Build the UI
 49        self._setup_ui()
 50
 51    # -------------------------------------------------------------------------
 52    # UI setup
 53    # -------------------------------------------------------------------------
 54    def _setup_ui(self):
 55        """Configure the visual layout and styling for the folder card."""
 56        layout = QVBoxLayout(self)
 57        layout.setContentsMargins(8, 8, 8, 8)
 58
 59        # Main container frame with light elevation
 60        self.card_frame = QFrame()
 61        self.card_frame.setObjectName("outputsCard")
 62        self.card_frame.setStyleSheet("""
 63            QFrame#outputsCard {
 64                background-color: white;
 65                border-radius: 12px;
 66                border: 1px solid #e0e0e0;
 67            }
 68        """)
 69
 70        # Horizontal layout for card contents
 71        card_layout = QHBoxLayout(self.card_frame)
 72        card_layout.setContentsMargins(16, 12, 16, 12)
 73        card_layout.setSpacing(12)
 74
 75        # Left section: folder icon
 76        self.folder_icon = QLabel("📁")
 77        self.folder_icon.setStyleSheet("""
 78            font-size: 32px;
 79            padding: 8px;
 80            background-color: #f0f7ff;
 81            border-radius: 8px;
 82        """)
 83        self.folder_icon.setFixedSize(56, 56)
 84        self.folder_icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
 85        card_layout.addWidget(self.folder_icon)
 86
 87        # Center section: folder name and status
 88        info_layout = QVBoxLayout()
 89        info_layout.setSpacing(4)
 90
 91        # Folder name label
 92        self.folder_name = QLabel(os.path.basename(self.folder))
 93        self.folder_name.setStyleSheet("""
 94            font-size: 14px;
 95            font-weight: 600;
 96            color: #2c3e50;
 97        """)
 98        info_layout.addWidget(self.folder_name)
 99
100        # Status label (e.g., “Waiting for files…” or “3 new files”)
101        self.status_label = QLabel(QCoreApplication.translate("Components", "Waiting for files..."))
102        self.status_label.setStyleSheet("""
103            font-size: 12px;
104            color: #7f8c8d;
105        """)
106        info_layout.addWidget(self.status_label)
107
108        card_layout.addLayout(info_layout, stretch=1)
109
110        # Right section: action button
111        self.action_btn = QPushButton(QCoreApplication.translate("Components", "Visualize"))
112        self.action_btn.setStyleSheet("""
113            QPushButton {
114                background-color: #3498db;
115                color: white;
116                border: none;
117                border-radius: 8px;
118                padding: 8px 20px;
119                font-size: 13px;
120                font-weight: 600;
121            }
122            QPushButton:hover {
123                background-color: #2980b9;
124            }
125            QPushButton:pressed {
126                background-color: #21618c;
127            }
128            QPushButton:disabled {
129                background-color: #bdc3c7;
130                color: #7f8c8d;
131            }
132        """)
133        self.action_btn.clicked.connect(self.show_files_dialog)
134        self.action_btn.setEnabled(False)
135        card_layout.addWidget(self.action_btn)
136
137        layout.addWidget(self.card_frame)
138
139    # -------------------------------------------------------------------------
140    # File handling and state updates
141    # -------------------------------------------------------------------------
142    def add_files(self, new_files):
143        """
144        Add new files to the card, update the UI, and trigger a pulse animation.
145
146        Args:
147            new_files (list[str]): Names of newly detected files.
148        """
149        if self.is_finished:
150            return
151
152        self.files.extend(new_files)
153        file_count = len(self.files)
154
155        # Update visual state to show activity
156        self.status_label.setText(
157            QCoreApplication.translate("Components", "{file_count} new file").format(file_count=file_count)
158        )
159        self.status_label.setStyleSheet("""
160            font-size: 12px;
161            color: #27ae60;
162            font-weight: 600;
163        """)
164
165        self.action_btn.setEnabled(True)
166
167        # Change the icon and color scheme to indicate success
168        self.folder_icon.setText("✓")
169        self.folder_icon.setStyleSheet("""
170            font-size: 32px;
171            padding: 8px;
172            background-color: #d5f4e6;
173            border-radius: 8px;
174            color: #27ae60;
175        """)
176
177        # Update card border to highlight new content
178        self.card_frame.setStyleSheet("""
179            QFrame#outputsCard {
180                background-color: white;
181                border-radius: 12px;
182                border: 2px solid #27ae60;
183            }
184        """)
185
186        # Trigger pulse animation for visual feedback
187        self.start_pulse_animation()
188
189    def start_pulse_animation(self):
190        """
191        Perform a subtle pulsing height animation when new files are added.
192        """
193        if self.pulse_animation:
194            self.pulse_animation.stop()
195
196        self.pulse_animation = QSequentialAnimationGroup()
197
198        # Expand slightly
199        pulse1 = QPropertyAnimation(self.card_frame, b"maximumHeight")
200        pulse1.setDuration(300)
201        pulse1.setStartValue(100)
202        pulse1.setEndValue(104)
203        pulse1.setEasingCurve(QEasingCurve.Type.OutCubic)
204
205        # Contract back
206        pulse2 = QPropertyAnimation(self.card_frame, b"maximumHeight")
207        pulse2.setDuration(300)
208        pulse2.setStartValue(104)
209        pulse2.setEndValue(100)
210        pulse2.setEasingCurve(QEasingCurve.Type.InCubic)
211
212        self.pulse_animation.addAnimation(pulse1)
213        self.pulse_animation.addAnimation(pulse2)
214        self.pulse_animation.start()
215
216    def reset_state(self):
217        """
218        Reset the card’s appearance to its initial “waiting” state.
219        Called after viewing or clearing files.
220        """
221        self.files.clear()
222        self.is_finished = False  # Resetta lo stato
223        self.existing_files = self._list_all_files(self.folder)
224
225        self.status_label.setText(QCoreApplication.translate("Components", "Waiting for files..."))
226        self.status_label.setStyleSheet("""
227            font-size: 12px;
228            color: #7f8c8d;
229        """)
230
231        self.folder_icon.setText("📁")
232        self.folder_icon.setStyleSheet("""
233            font-size: 32px;
234            padding: 8px;
235            background-color: #f0f7ff;
236            border-radius: 8px;
237        """)
238
239        self.card_frame.setStyleSheet("""
240            QFrame#outputsCard {
241                background-color: white;
242                border-radius: 12px;
243                border: 1px solid #e0e0e0;
244            }
245        """)
246
247        self.action_btn.setEnabled(False)
248
249        if self.pulse_animation:
250            self.pulse_animation.stop()
251
252    # -------------------------------------------------------------------------
253    # Dialog for file inspection
254    # -------------------------------------------------------------------------
255    def show_files_dialog(self):
256        """
257        Open a dialog window displaying all new files detected for this folder.
258        Provides options to open the folder or close the dialog.
259        """
260        if not self.files:
261            return
262
263        dialog = QDialog(self)
264        dialog.setWindowTitle(
265            QCoreApplication.translate("Components", "Generated file - {0}")
266            .format(os.path.basename(self.folder))
267        )
268        dialog.setModal(True)
269        dialog.setMinimumSize(600, 500)
270
271        layout = QVBoxLayout(dialog)
272        layout.setContentsMargins(20, 20, 20, 20)
273        layout.setSpacing(16)
274
275        # Header with file count
276        header = QLabel(
277            QCoreApplication.translate("Components", "📊 {0} generated file").format(len(self.files))
278        )
279        header.setStyleSheet("""
280            font-size: 18px;
281            font-weight: bold;
282            color: #2c3e50;
283            padding: 8px;
284        """)
285        layout.addWidget(header)
286
287        # Folder path subtitle
288        path_label = QLabel(f"📁 {self.folder}")
289        path_label.setStyleSheet("""
290            font-size: 12px;
291            color: #7f8c8d;
292            padding: 0 8px 8px 8px;
293        """)
294        path_label.setWordWrap(True)
295        layout.addWidget(path_label)
296
297        # File list area
298        list_widget = QListWidget()
299        list_widget.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
300        list_widget.setFocusPolicy(Qt.FocusPolicy.NoFocus)
301        list_widget.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
302        list_widget.setStyleSheet("""
303            QListWidget {
304                border: 1px solid #e0e0e0;
305                border-radius: 8px;
306                background-color: #fafafa;
307                padding: 8px;
308            }
309            QListWidget::item {
310                padding: 12px;
311                border-radius: 6px;
312                margin: 2px;
313            }
314        """)
315
316        # Populate list with filenames
317        for f in sorted(self.files):
318            item = QListWidgetItem(f"📄  {f.replace(os.sep, '/')}")
319            item.setToolTip(os.path.join(self.folder, f))
320            list_widget.addItem(item)
321
322        layout.addWidget(list_widget)
323
324        # Footer buttons
325        button_layout = QHBoxLayout()
326        button_layout.setSpacing(12)
327
328        # Button: open containing folder
329        btn_open_folder = QPushButton(QCoreApplication.translate("Components", "📂 Open Folder"))
330        btn_open_folder.setStyleSheet("""
331            QPushButton {
332                background-color: #3498db;
333                color: white;
334                border: none;
335                border-radius: 8px;
336                padding: 12px 24px;
337                font-size: 14px;
338                font-weight: 600;
339            }
340            QPushButton:hover {
341                background-color: #2980b9;
342            }
343        """)
344        btn_open_folder.clicked.connect(lambda: self.open_folder_requested.emit(self.folder))
345
346        # Button: close dialog
347        btn_close = QPushButton(QCoreApplication.translate("Components", "Close"))
348        btn_close.setStyleSheet("""
349            QPushButton {
350                background-color: #ecf0f1;
351                color: #2c3e50;
352                border: 1px solid #bdc3c7;
353                border-radius: 8px;
354                padding: 12px 24px;
355                font-size: 14px;
356                font-weight: 600;
357            }
358            QPushButton:hover {
359                background-color: #e0e0e0;
360            }
361        """)
362        btn_close.clicked.connect(dialog.accept)
363
364        button_layout.addWidget(btn_open_folder)
365        button_layout.addStretch()
366        button_layout.addWidget(btn_close)
367
368        layout.addLayout(button_layout)
369
370        dialog.exec()
371
372        # After closing the dialog, clear and reset
373        self.files.clear()
374        self.reset_state()
375
376    # -------------------------------------------------------------------------
377    # Monitoring logic
378    # -------------------------------------------------------------------------
379    def check_new_files(self):
380        """
381        Check if new files have appeared in the monitored folder.
382        If new files are found, they are added and the UI is updated.
383        """
384        if self.is_finished:
385            return
386
387        if not os.path.isdir(self.folder):
388            return
389
390        current_files = self._list_all_files(self.folder)
391        new_files = current_files - self.existing_files
392
393        if new_files:
394            self.add_files(list(new_files))
395
396        self.existing_files = current_files
397
398    def _list_all_files(self, root_folder):
399        """Restituisce un set con tutti i file nella cartella e nelle sottocartelle."""
400        all_files = set()
401        if not os.path.isdir(root_folder):
402            return all_files
403        for dirpath, _, filenames in os.walk(root_folder):
404            for f in filenames:
405                # Salva percorsi relativi (rispetto alla root) per coerenza
406                relative_path = os.path.relpath(os.path.join(dirpath, f), root_folder)
407                all_files.add(relative_path)
408        return all_files
409
410    def set_finished_state(self):
411        """
412        Sets the card to a visual "Completed" state.
413        Performs a final file check and updates the interface.
414        """
415        if self.is_finished:
416            return
417
418        self.is_finished = True
419
420        self.check_new_files()
421
422        file_count = len(self.existing_files)
423
424        self.status_label.setText(
425            QCoreApplication.translate("Components", "Processing complete ({file_count} files)")
426            .format(file_count=file_count)
427        )
428        self.status_label.setStyleSheet("""
429            font-size: 12px;
430            color: #27ae60;
431            font-weight: 600;
432        """)
433
434        self.action_btn.setEnabled(False)
435
436        self.folder_icon.setText("✓")
437        self.folder_icon.setStyleSheet("""
438            font-size: 32px;
439            padding: 8px;
440            background-color: #d5f4e6;
441            border-radius: 8px;
442            color: #27ae60;
443        """)
444
445        self.card_frame.setStyleSheet("""
446            QFrame#outputsCard {
447                background-color: white;
448                border-radius: 12px;
449                border: 2px solid #27ae60;
450            }
451        """)

A modern, self-contained widget representing a monitored output folder. It shows the folder name, file count, and allows the user to open or inspect newly generated files. Includes subtle visual animations for updates.

FolderCard(context, folder)
24    def __init__(self, context, folder):
25        """
26        Initialize the FolderCard.
27
28        Args:
29            context (dict): Shared application context (unused here, reserved for integration).
30            folder (str): Path to the folder being represented by this card.
31        """
32        super().__init__()
33
34        self.folder = folder
35        self.files = []  # Tracks new files added since the last check
36        self.is_finished = False
37        # self.existing_files = set(os.listdir(folder)) if os.path.isdir(folder) else set()
38        self.existing_files = self._list_all_files(folder)
39
40        # Animation and visual properties
41        self.pulse_animation = None
42        self._scale = 1.0
43        self._glow_opacity = 0.0
44
45        # Define a fixed height for uniform card size
46        self.setFixedHeight(100)
47
48        # Build the UI
49        self._setup_ui()

Initialize the FolderCard.

Args: context (dict): Shared application context (unused here, reserved for integration). folder (str): Path to the folder being represented by this card.

def open_folder_requested(unknown):

pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

folder
files
is_finished
existing_files
pulse_animation
def add_files(self, new_files):
142    def add_files(self, new_files):
143        """
144        Add new files to the card, update the UI, and trigger a pulse animation.
145
146        Args:
147            new_files (list[str]): Names of newly detected files.
148        """
149        if self.is_finished:
150            return
151
152        self.files.extend(new_files)
153        file_count = len(self.files)
154
155        # Update visual state to show activity
156        self.status_label.setText(
157            QCoreApplication.translate("Components", "{file_count} new file").format(file_count=file_count)
158        )
159        self.status_label.setStyleSheet("""
160            font-size: 12px;
161            color: #27ae60;
162            font-weight: 600;
163        """)
164
165        self.action_btn.setEnabled(True)
166
167        # Change the icon and color scheme to indicate success
168        self.folder_icon.setText("✓")
169        self.folder_icon.setStyleSheet("""
170            font-size: 32px;
171            padding: 8px;
172            background-color: #d5f4e6;
173            border-radius: 8px;
174            color: #27ae60;
175        """)
176
177        # Update card border to highlight new content
178        self.card_frame.setStyleSheet("""
179            QFrame#outputsCard {
180                background-color: white;
181                border-radius: 12px;
182                border: 2px solid #27ae60;
183            }
184        """)
185
186        # Trigger pulse animation for visual feedback
187        self.start_pulse_animation()

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

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

def start_pulse_animation(self):
189    def start_pulse_animation(self):
190        """
191        Perform a subtle pulsing height animation when new files are added.
192        """
193        if self.pulse_animation:
194            self.pulse_animation.stop()
195
196        self.pulse_animation = QSequentialAnimationGroup()
197
198        # Expand slightly
199        pulse1 = QPropertyAnimation(self.card_frame, b"maximumHeight")
200        pulse1.setDuration(300)
201        pulse1.setStartValue(100)
202        pulse1.setEndValue(104)
203        pulse1.setEasingCurve(QEasingCurve.Type.OutCubic)
204
205        # Contract back
206        pulse2 = QPropertyAnimation(self.card_frame, b"maximumHeight")
207        pulse2.setDuration(300)
208        pulse2.setStartValue(104)
209        pulse2.setEndValue(100)
210        pulse2.setEasingCurve(QEasingCurve.Type.InCubic)
211
212        self.pulse_animation.addAnimation(pulse1)
213        self.pulse_animation.addAnimation(pulse2)
214        self.pulse_animation.start()

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

def reset_state(self):
216    def reset_state(self):
217        """
218        Reset the card’s appearance to its initial “waiting” state.
219        Called after viewing or clearing files.
220        """
221        self.files.clear()
222        self.is_finished = False  # Resetta lo stato
223        self.existing_files = self._list_all_files(self.folder)
224
225        self.status_label.setText(QCoreApplication.translate("Components", "Waiting for files..."))
226        self.status_label.setStyleSheet("""
227            font-size: 12px;
228            color: #7f8c8d;
229        """)
230
231        self.folder_icon.setText("📁")
232        self.folder_icon.setStyleSheet("""
233            font-size: 32px;
234            padding: 8px;
235            background-color: #f0f7ff;
236            border-radius: 8px;
237        """)
238
239        self.card_frame.setStyleSheet("""
240            QFrame#outputsCard {
241                background-color: white;
242                border-radius: 12px;
243                border: 1px solid #e0e0e0;
244            }
245        """)
246
247        self.action_btn.setEnabled(False)
248
249        if self.pulse_animation:
250            self.pulse_animation.stop()

Reset the card’s appearance to its initial “waiting” state. Called after viewing or clearing files.

def show_files_dialog(self):
255    def show_files_dialog(self):
256        """
257        Open a dialog window displaying all new files detected for this folder.
258        Provides options to open the folder or close the dialog.
259        """
260        if not self.files:
261            return
262
263        dialog = QDialog(self)
264        dialog.setWindowTitle(
265            QCoreApplication.translate("Components", "Generated file - {0}")
266            .format(os.path.basename(self.folder))
267        )
268        dialog.setModal(True)
269        dialog.setMinimumSize(600, 500)
270
271        layout = QVBoxLayout(dialog)
272        layout.setContentsMargins(20, 20, 20, 20)
273        layout.setSpacing(16)
274
275        # Header with file count
276        header = QLabel(
277            QCoreApplication.translate("Components", "📊 {0} generated file").format(len(self.files))
278        )
279        header.setStyleSheet("""
280            font-size: 18px;
281            font-weight: bold;
282            color: #2c3e50;
283            padding: 8px;
284        """)
285        layout.addWidget(header)
286
287        # Folder path subtitle
288        path_label = QLabel(f"📁 {self.folder}")
289        path_label.setStyleSheet("""
290            font-size: 12px;
291            color: #7f8c8d;
292            padding: 0 8px 8px 8px;
293        """)
294        path_label.setWordWrap(True)
295        layout.addWidget(path_label)
296
297        # File list area
298        list_widget = QListWidget()
299        list_widget.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
300        list_widget.setFocusPolicy(Qt.FocusPolicy.NoFocus)
301        list_widget.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
302        list_widget.setStyleSheet("""
303            QListWidget {
304                border: 1px solid #e0e0e0;
305                border-radius: 8px;
306                background-color: #fafafa;
307                padding: 8px;
308            }
309            QListWidget::item {
310                padding: 12px;
311                border-radius: 6px;
312                margin: 2px;
313            }
314        """)
315
316        # Populate list with filenames
317        for f in sorted(self.files):
318            item = QListWidgetItem(f"📄  {f.replace(os.sep, '/')}")
319            item.setToolTip(os.path.join(self.folder, f))
320            list_widget.addItem(item)
321
322        layout.addWidget(list_widget)
323
324        # Footer buttons
325        button_layout = QHBoxLayout()
326        button_layout.setSpacing(12)
327
328        # Button: open containing folder
329        btn_open_folder = QPushButton(QCoreApplication.translate("Components", "📂 Open Folder"))
330        btn_open_folder.setStyleSheet("""
331            QPushButton {
332                background-color: #3498db;
333                color: white;
334                border: none;
335                border-radius: 8px;
336                padding: 12px 24px;
337                font-size: 14px;
338                font-weight: 600;
339            }
340            QPushButton:hover {
341                background-color: #2980b9;
342            }
343        """)
344        btn_open_folder.clicked.connect(lambda: self.open_folder_requested.emit(self.folder))
345
346        # Button: close dialog
347        btn_close = QPushButton(QCoreApplication.translate("Components", "Close"))
348        btn_close.setStyleSheet("""
349            QPushButton {
350                background-color: #ecf0f1;
351                color: #2c3e50;
352                border: 1px solid #bdc3c7;
353                border-radius: 8px;
354                padding: 12px 24px;
355                font-size: 14px;
356                font-weight: 600;
357            }
358            QPushButton:hover {
359                background-color: #e0e0e0;
360            }
361        """)
362        btn_close.clicked.connect(dialog.accept)
363
364        button_layout.addWidget(btn_open_folder)
365        button_layout.addStretch()
366        button_layout.addWidget(btn_close)
367
368        layout.addLayout(button_layout)
369
370        dialog.exec()
371
372        # After closing the dialog, clear and reset
373        self.files.clear()
374        self.reset_state()

Open a dialog window displaying all new files detected for this folder. Provides options to open the folder or close the dialog.

def check_new_files(self):
379    def check_new_files(self):
380        """
381        Check if new files have appeared in the monitored folder.
382        If new files are found, they are added and the UI is updated.
383        """
384        if self.is_finished:
385            return
386
387        if not os.path.isdir(self.folder):
388            return
389
390        current_files = self._list_all_files(self.folder)
391        new_files = current_files - self.existing_files
392
393        if new_files:
394            self.add_files(list(new_files))
395
396        self.existing_files = current_files

Check if new files have appeared in the monitored folder. If new files are found, they are added and the UI is updated.

def set_finished_state(self):
410    def set_finished_state(self):
411        """
412        Sets the card to a visual "Completed" state.
413        Performs a final file check and updates the interface.
414        """
415        if self.is_finished:
416            return
417
418        self.is_finished = True
419
420        self.check_new_files()
421
422        file_count = len(self.existing_files)
423
424        self.status_label.setText(
425            QCoreApplication.translate("Components", "Processing complete ({file_count} files)")
426            .format(file_count=file_count)
427        )
428        self.status_label.setStyleSheet("""
429            font-size: 12px;
430            color: #27ae60;
431            font-weight: 600;
432        """)
433
434        self.action_btn.setEnabled(False)
435
436        self.folder_icon.setText("✓")
437        self.folder_icon.setStyleSheet("""
438            font-size: 32px;
439            padding: 8px;
440            background-color: #d5f4e6;
441            border-radius: 8px;
442            color: #27ae60;
443        """)
444
445        self.card_frame.setStyleSheet("""
446            QFrame#outputsCard {
447                background-color: white;
448                border-radius: 12px;
449                border: 2px solid #27ae60;
450            }
451        """)

Sets the card to a visual "Completed" state. Performs a final file check and updates the interface.