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 """)
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.
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.
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.
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.
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.
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.
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.
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.
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.