PaIRS-UniNa 0.2.8__cp312-cp312-win_amd64.whl → 0.2.9__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. PaIRS_UniNa/Calibration_Tab.py +15 -0
  2. PaIRS_UniNa/Changes.txt +9 -0
  3. PaIRS_UniNa/Explorer.py +3158 -3126
  4. PaIRS_UniNa/FolderLoop.py +561 -561
  5. PaIRS_UniNa/Input_Tab.py +826 -826
  6. PaIRS_UniNa/Input_Tab_tools.py +3016 -3019
  7. PaIRS_UniNa/PaIRS.py +17 -17
  8. PaIRS_UniNa/SPIVCalHelp.py +155 -0
  9. PaIRS_UniNa/Saving_tools.py +277 -277
  10. PaIRS_UniNa/_PaIRS_PIV.pyd +0 -0
  11. PaIRS_UniNa/__init__.py +2 -2
  12. PaIRS_UniNa/gPaIRS.py +3890 -3889
  13. PaIRS_UniNa/icons/information.png +0 -0
  14. PaIRS_UniNa/icons/information2.png +0 -0
  15. PaIRS_UniNa/icons/spiv_setup_no.png +0 -0
  16. PaIRS_UniNa/icons/spiv_setup_ok.png +0 -0
  17. PaIRS_UniNa/listLib.py +301 -301
  18. PaIRS_UniNa/parForMulti.py +433 -433
  19. PaIRS_UniNa/tabSplitter.py +606 -606
  20. PaIRS_UniNa/ui_Calibration_Tab.py +575 -542
  21. PaIRS_UniNa/ui_Custom_Top.py +294 -294
  22. PaIRS_UniNa/ui_Input_Tab.py +1098 -1098
  23. PaIRS_UniNa/ui_Input_Tab_CalVi.py +1280 -1280
  24. PaIRS_UniNa/ui_Log_Tab.py +261 -261
  25. PaIRS_UniNa/ui_Output_Tab.py +2360 -2360
  26. PaIRS_UniNa/ui_Process_Tab.py +3808 -3808
  27. PaIRS_UniNa/ui_Process_Tab_CalVi.py +1547 -1547
  28. PaIRS_UniNa/ui_Process_Tab_Disp.py +1139 -1139
  29. PaIRS_UniNa/ui_Process_Tab_Min.py +435 -435
  30. PaIRS_UniNa/ui_ResizePopup.py +203 -203
  31. PaIRS_UniNa/ui_Vis_Tab.py +1626 -1626
  32. PaIRS_UniNa/ui_Vis_Tab_CalVi.py +1249 -1249
  33. PaIRS_UniNa/ui_Whatsnew.py +131 -131
  34. PaIRS_UniNa/ui_gPairs.py +873 -873
  35. PaIRS_UniNa/ui_infoPaIRS.py +550 -550
  36. PaIRS_UniNa/whatsnew.txt +1 -1
  37. {pairs_unina-0.2.8.dist-info → pairs_unina-0.2.9.dist-info}/METADATA +4 -3
  38. {pairs_unina-0.2.8.dist-info → pairs_unina-0.2.9.dist-info}/RECORD +40 -36
  39. {pairs_unina-0.2.8.dist-info → pairs_unina-0.2.9.dist-info}/WHEEL +0 -0
  40. {pairs_unina-0.2.8.dist-info → pairs_unina-0.2.9.dist-info}/top_level.txt +0 -0
PaIRS_UniNa/FolderLoop.py CHANGED
@@ -1,562 +1,562 @@
1
- import sys
2
- from PySide6.QtWidgets import (
3
- QApplication, QDialog, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QSpacerItem, QTreeWidget, QTreeWidgetItem,
4
- QPushButton, QProgressBar, QSizePolicy,
5
- QFileDialog, QListView, QAbstractItemView, QTreeView, QFileSystemModel
6
- )
7
- from PySide6.QtGui import QPixmap, QFont, QIcon, QCursor
8
- from PySide6.QtCore import Qt, QTimer, QSize, QVariantAnimation
9
- from time import sleep as timesleep
10
- from pathlib import Path
11
- from typing import Optional, List
1
+ import sys
2
+ from PySide6.QtWidgets import (
3
+ QApplication, QDialog, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QSpacerItem, QTreeWidget, QTreeWidgetItem,
4
+ QPushButton, QProgressBar, QSizePolicy,
5
+ QFileDialog, QListView, QAbstractItemView, QTreeView, QFileSystemModel
6
+ )
7
+ from PySide6.QtGui import QPixmap, QFont, QIcon, QCursor
8
+ from PySide6.QtCore import Qt, QTimer, QSize, QVariantAnimation
9
+ from time import sleep as timesleep
10
+ from pathlib import Path
11
+ from typing import Optional, List
12
12
  from .PaIRS_pypacks import fontPixelSize, fontName, icons_path
13
-
14
- time_sleep_loop=0
15
-
16
- def choose_directories(base:Path = Path('.'),parent=None) -> Optional[List[str]]:
17
- """
18
- Open a dialogue to select multiple directories
19
- Args:
20
- base (Path): Starting directory to show when opening dialogue
21
- Returns:
22
- List[str]: List of paths that were selected, ``None`` if "cancel" selected"
23
- References:
24
- Mildly adapted from https://stackoverflow.com/a/28548773
25
- to use outside an exising Qt Application
26
- """
27
-
28
- file_dialog = QFileDialog(parent)
29
- file_dialog.setWindowTitle("Select folders for process loop")
30
- file_dialog.setWindowIcon(QIcon(icons_path + "process_loop.png"))
31
- file_dialog.setWindowIconText("Select folders for process loop")
32
- file_dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
33
- file_dialog.setFileMode(QFileDialog.Directory)
34
- for widget_type in (QListView, QTreeView):
35
- for view in file_dialog.findChildren(widget_type):
36
- if isinstance(view.model(), QFileSystemModel):
37
- view.setSelectionMode(
38
- QAbstractItemView.ExtendedSelection)
39
-
40
- paths=[]
41
- if file_dialog.exec():
42
- paths = file_dialog.selectedFiles()
43
- return paths
44
-
45
- class FolderLoopDialog(QDialog):
46
- def __init__(self, pixmap_list, name_list, flag_list, parent=None, func=lambda it, opt: print(f"Iteration {it}"), paths=[], process_name = 'Process', *args, **kwargs):
47
- super().__init__(parent=parent, *args, **kwargs)
48
-
49
- self.func=func
50
- self.nfolders=len(paths)
51
- self.paths=paths
52
-
53
- # Variables for easy customization
54
- self.setWindowTitle("Process loop over folders")
55
- self.setWindowIcon(QIcon(icons_path + "process_loop.png"))
56
- self.setWindowIconText("Process loop over folders")
57
- self.title_text = "Configure each step of the process"
58
- if parent is None:
59
- self.title_font = QFont(fontName, fontPixelSize+12, QFont.Bold)
60
- self.item_font = QFont(fontName, fontPixelSize+8)
61
- self.button_font = QFont(fontName, fontPixelSize+2)
62
- else:
63
- self.title_font:QFont = parent.font()
64
- self.title_font.setBold(True)
65
- self.title_font.setPixelSize(fontPixelSize+12)
66
- self.item_font:QFont = parent.font()
67
- self.item_font = parent.font()
68
- self.item_font.setPixelSize(fontPixelSize+8)
69
- self.button_font: QFont = parent.font()
70
- self.button_font.setPixelSize(fontPixelSize+2)
71
- self.title_height = 48
72
- self.icon_size = QSize(42, 42) # Icon size inside buttons
73
- self.icon_size_off = QSize(24, 24) # Icon size inside buttons when unchecked
74
- self.button_size = QSize(56, 56) # Button size
75
- self.row_spacing = 18 # Spacing between rows
76
- self.margin_size = 5 # Window margin size
77
- self.progress_bar_height = 36
78
-
79
- self.min_height = self.row_spacing + self.margin_size*2 + self.title_height *2 + self.progress_bar_height # min window height
80
- self.max_height = len(name_list) * (self.button_size.height() + self.row_spacing) + self.min_height # Max window height
81
-
82
- # Tooltip texts
83
- self.tooltips = {
84
- "copy": "Copy the item",
85
- "link": "Link the item",
86
- "change_folder": "Change the folder",
87
- "auto_purge": "Auto-remove missing-image pairs",
88
- "rescan": "Re-scan destination paths"
89
- }
90
-
91
- # Icons for the buttons
92
- self.icons = {
93
- "copy": [QIcon(icons_path + "copy_process.png"), QIcon(icons_path + "copy_process_off.png")],
94
- "link": [QIcon(icons_path + "link.png"), QIcon(icons_path + "unlink.png")],
95
- "change_folder": [QIcon(icons_path + "change_folder.png"), QIcon(icons_path + "change_folder_off.png")],
96
- "auto_purge": [QIcon(icons_path + "folder_loop_cleanup.png"), QIcon(icons_path + "folder_loop_cleanup_off.png")],
97
- "rescan": [QIcon(icons_path + "scan_path_loop.png"), QIcon(icons_path + "scan_path_loop_off.png")],
98
- }
99
- self.options_list=list(self.icons)
100
-
101
- # Main layout
102
- layout = QVBoxLayout()
103
- layout.setSpacing(self.row_spacing)
104
-
105
- # Left-aligned title
106
- header_layout = QHBoxLayout()
107
- header_layout.setContentsMargins(0,0,0,0)
108
- header_layout.setSpacing(10)
109
-
110
- self.header_icon = QLabel('')
111
- self.header_icon.setPixmap(QPixmap(icons_path + "process_loop.png"))
112
- self.header_icon.setScaledContents(True)
113
- self.header_icon.setFixedSize(self.icon_size)
114
- header_layout.addWidget(self.header_icon)
115
-
116
- header_text=process_name[:48]+f"{'...' if len(process_name)>50 else ''}"
117
- self.header_label = QLabel(header_text)
118
- self.header_label.setFont(self.title_font)
119
- self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
120
- self.header_label.setFixedHeight(self.title_height)
121
- header_layout.addWidget(self.header_label)
122
-
123
- layout.addLayout(header_layout)
124
-
125
- # Left-aligned title
126
- title_layout = QHBoxLayout()
127
- title_layout.setContentsMargins(0,0,0,0)
128
- title_layout.setSpacing(10)
129
-
130
- """
131
- self.title_icon = QLabel('')
132
- self.title_icon.setPixmap(QPixmap(icons_path + "process_loop.png"))
133
- self.title_icon.setScaledContents(True)
134
- self.title_icon.setFixedSize(self.icon_size)
135
- title_layout.addWidget(self.title_icon)
136
- """
137
-
138
- self.title_label = QLabel(self.title_text)
139
- self.title_label.setFont(self.title_font)
140
- self.title_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
141
- self.title_label.setFixedHeight(self.title_height)
142
- title_layout.addWidget(self.title_label)
143
-
144
- layout.addLayout(title_layout)
145
-
146
- # List to store button states (0, 1, 2)
147
- self.button_states = [0] * len(name_list)
148
-
149
- # Add the additional n elements
150
- self.buttons_group = []
151
- self.step_options = []
152
- self.widgets = []
153
- self.nstep=len(pixmap_list)
154
- for i in range(self.nstep):
155
- widget = QWidget()
156
- item_layout = QHBoxLayout()
157
- item_layout.setContentsMargins(0,0,0,0)
158
- widget.setLayout(item_layout)
159
- widget.setFixedHeight(self.button_size.height())
160
- self.widgets.append(widget)
161
-
162
- # Label with pixmap (fitted to 32x32 pixels)
163
- pixmap_label = QLabel(self)
164
- pixmap = QPixmap(pixmap_list[i])#.scaled(self.button_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
165
- pixmap_label.setPixmap(pixmap)
166
- pixmap_label.setScaledContents(True)
167
- pixmap_label.setFixedSize(self.button_size)
168
- item_layout.addWidget(pixmap_label)
169
-
170
- spacer=QSpacerItem(10, 0, QSizePolicy.Minimum, QSizePolicy.Minimum)
171
- item_layout.addItem(spacer)
172
-
173
- # Label with text from the name list (larger font)
174
- name_label = QLabel(name_list[i],self)
175
- name_label.setFont(self.item_font)
176
- name_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
177
- item_layout.addWidget(name_label)
178
-
179
- # Stretch the name label to fill the row
180
- item_layout.addStretch()
181
-
182
- # Three checkable buttons with icons
183
- self.step_options.append(-1)
184
- copy_button = self.create_icon_button(i, "copy")
185
- link_button = self.create_icon_button(i, "link")
186
- if flag_list[i]:
187
- folder_button = self.create_icon_button(i, "change_folder")
188
- else:
189
- folder_button = QLabel(self,text='')
190
- folder_button.setFixedWidth(self.button_size.width())
191
- self.buttons_group.append([copy_button, link_button, folder_button])
192
-
193
- item_layout.addWidget(copy_button)
194
- item_layout.addWidget(link_button)
195
- item_layout.addWidget(folder_button)
196
-
197
- layout.addWidget(widget)
198
-
199
- # --- Cleanup row ---
200
- cleanup_spacing = 5
201
- self.cleanup_widget = QWidget()
202
- cw_layout = QHBoxLayout(self.cleanup_widget)
203
- cw_layout.setContentsMargins(0, cleanup_spacing, 0, cleanup_spacing)
204
- cw_layout.setSpacing(5)
205
-
206
- self.cleanup_button = QPushButton(self)
207
- self.cleanup_button.setCheckable(True)
208
- self.cleanup_button.setChecked(False)
209
- self.cleanup_button.setIcon(self.icons["auto_purge"][1])
210
- self.cleanup_button.setFixedSize(self.button_size)
211
- self.cleanup_button.setStyleSheet("QPushButton{border: none;}")
212
- self.cleanup_button.setCursor(QCursor(Qt.PointingHandCursor))
213
- self.cleanup_button.setToolTip(self.tooltips["auto_purge"])
214
- self.cleanup_button.setIconSize(self.icon_size_off)
215
-
216
- self.cleanup_label = QLabel("", self)
217
- self.cleanup_label_font = parent.font()
218
- self.cleanup_label_font.setPixelSize(fontPixelSize)
219
- self.cleanup_label_font.setItalic(True)
220
- self.cleanup_label.setFont(self.cleanup_label_font)
221
- self.cleanup_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
222
-
223
- self.cleanup_button.toggled.connect(self.on_cleanup_toggled)
224
- cw_layout.addItem(QSpacerItem(10, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
225
- cw_layout.addWidget(self.cleanup_label)
226
- cw_layout.addWidget(self.cleanup_button)
227
-
228
-
229
- # --- Rescan row ---
230
- rescan_spacing = 5
231
- self.rescan_widget = QWidget()
232
- rw_layout = QHBoxLayout(self.rescan_widget)
233
- rw_layout.setContentsMargins(0, rescan_spacing, 0, rescan_spacing)
234
- rw_layout.setSpacing(5)
235
-
236
- self.rescan_button = QPushButton(self)
237
- self.rescan_button.setCheckable(True)
238
- self.rescan_button.setChecked(False)
239
- self.rescan_button.setIcon(self.icons["rescan"][1])
240
- self.rescan_button.setFixedSize(self.button_size)
241
- self.rescan_button.setStyleSheet("QPushButton{border: none;}")
242
- self.rescan_button.setCursor(QCursor(Qt.PointingHandCursor))
243
- self.rescan_button.setToolTip(self.tooltips["rescan"])
244
- self.rescan_button.setIconSize(self.icon_size_off)
245
-
246
- self.rescan_label = QLabel("", self)
247
- self.rescan_label_font = parent.font()
248
- self.rescan_label_font.setPixelSize(fontPixelSize)
249
- self.rescan_label_font.setItalic(True)
250
- self.rescan_label.setFont(self.rescan_label_font)
251
- self.rescan_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
252
-
253
- self.rescan_button.toggled.connect(self.on_rescan_toggled)
254
- rw_layout.addItem(QSpacerItem(10, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
255
- rw_layout.addWidget(self.rescan_label)
256
- rw_layout.addWidget(self.rescan_button)
257
-
258
- # --- Container for both rows ---
259
- self.extra_opts_widget = QWidget()
260
- eo_layout = QVBoxLayout(self.extra_opts_widget)
261
- eo_layout.setContentsMargins(0, 5, 0, 50) # top=10, bottom=10
262
- eo_layout.setSpacing(5)
263
- eo_layout.addWidget(self.rescan_widget)
264
- eo_layout.addWidget(self.cleanup_widget)
265
- self.extra_opts_widget.setFixedHeight(self.button_size.height()*2+60)
266
-
267
- self.extra_opts_widget.setVisible(False)
268
- self.widgets.append(self.extra_opts_widget)
269
-
270
- # Add container to parent layout
271
- layout.addWidget(self.extra_opts_widget)
272
-
273
- self.warnings=[False]*len(self.paths)
274
-
275
- # Progress bar and final buttons (Cancel, Proceed)
276
- progress_widget = QWidget()
277
- progress_layout = QHBoxLayout()
278
- progress_layout.setContentsMargins(0,0,0,0)
279
- progress_layout.setSpacing(10)
280
- progress_widget.setLayout(progress_layout)
281
- progress_widget.setFixedHeight(self.progress_bar_height)
282
- self.progress_bar = QProgressBar(self)
283
- self.progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
284
- self.progress_bar.setVisible(False) # Hidden initially
285
- self.title_label.adjustSize()
286
- self.progress_bar.setFixedWidth(self.title_label.width())
287
- progress_layout.addWidget(self.progress_bar)
288
-
289
- # Spacer to extend before the buttons
290
- progress_layout.addStretch()
291
-
292
- cancel_button_text = "Cancel"
293
- self.proceed_button_text = "Proceed"
294
- self.stop_button_text = "Stop"
295
-
296
- cancel_button = QPushButton(cancel_button_text,self)
297
- cancel_button.setFixedHeight(self.progress_bar_height)
298
- cancel_button.setFont(self.button_font)
299
- cancel_button.clicked.connect(self.cancel) # Closes the dialog without doing anything
300
- progress_layout.addWidget(cancel_button)
301
-
302
- self.proceed_button = QPushButton(self.proceed_button_text,self)
303
- self.proceed_button.setFixedHeight(self.progress_bar_height)
304
- self.proceed_button.setFont(self.button_font)
305
- self.proceed_button.clicked.connect(self.on_proceed)
306
- progress_layout.addWidget(self.proceed_button)
307
-
308
- layout.addWidget(progress_widget)
309
- self.setLayout(layout)
310
-
311
- # Set window maximum height and margins
312
- self.on_cleanup_toggled(self.cleanup_button.isChecked())
313
- self.on_rescan_toggled(self.rescan_button.isChecked())
314
- self.update_extraopts_visibility(FlagAnimation=False)
315
- self.setFixedHeight(self.max_height)
316
- self.setContentsMargins(self.margin_size*2, self.margin_size, self.margin_size*2, self.margin_size)
317
- self.setMinimumWidth(640)
318
- self.setMaximumWidth(800)
319
-
320
- # Timer for progress
321
- self.iteration = 0
322
- self.timer = QTimer(self)
323
- self.timer.timeout.connect(self.update_progress)
324
- self.timer.setSingleShot(True)
325
- self.loop_running = False
326
-
327
- self.animation=None
328
-
329
- def create_icon_button(self, index, button_type):
330
- """Create a checkable button with icon based on its type (copy, link, change_folder)."""
331
- button = QPushButton(self)
332
- button.setCheckable(True)
333
- button.setChecked((button_type=='change_folder' and index==self.nstep-1) or (button_type=='copy' and index<self.nstep-1))
334
- button.setIcon(self.icons[button_type][int(not button.isChecked())]) # Set initial 'off' icon
335
- button.setFixedSize(self.button_size) # Set button size to 32x32 pixels
336
- button.setStyleSheet("QPushButton{border: none;}") # Remove button borders
337
- button.setCursor(QCursor(Qt.PointingHandCursor)) # Set cursor to pointing hand on hover
338
- button.setToolTip(self.tooltips[button_type]) # Set tooltip
339
- button.clicked.connect(lambda: self.update_buttons(index, button_type))
340
- if button.isChecked():
341
- self.step_options[index]=self.options_list.index(button_type)
342
- button.setIconSize(self.icon_size)
343
- else:
344
- button.setIconSize(self.icon_size_off) # Set icon size to 28x28 pixels
345
- return button
346
-
347
- def update_buttons(self, index, button_type):
348
- """Update the icons and states of the buttons, ensuring only one is checked at a time."""
349
- for k, button in enumerate(self.buttons_group[index]):
350
- if not isinstance(button,QPushButton): continue
351
- if button_type!=self.options_list[k]:
352
- button.setChecked(False) # Uncheck all buttons
353
- button.setIconSize(self.icon_size_off)
354
- button.setIcon(self.icons[self.options_list[k]][1])
355
- else:
356
- button.setChecked(True)
357
- button.setIconSize(self.icon_size)
358
- button.setIcon(self.icons[self.options_list[k]][0]) # Checked state icon
359
- self.step_options[index]=k
360
- self.update_extraopts_visibility()
361
-
362
- def update_extraopts_visibility(self,FlagAnimation=True):
363
- """Show extra row if any row has 'change_folder' checked; hide otherwise."""
364
- try:
365
- idx_change = self.options_list.index("change_folder")
366
- except ValueError:
367
- idx_change = -1
368
-
369
- show = (idx_change >= 0) and any(
370
- (opt == idx_change) for opt in self.step_options if isinstance(opt, int)
371
- )
372
-
373
- if self.extra_opts_widget.isVisible() != show:
374
- self.extra_opts_widget.setVisible(show)
375
- extra_opts_widget = self.extra_opts_widget.height()
376
- maxHeight = self.nstep * (self.button_size.height() + self.row_spacing) + int(show)*extra_opts_widget + self.min_height
377
- if FlagAnimation:
378
- self.window_resize(maxHeight)
379
- self.max_height = maxHeight
380
-
381
- def on_cleanup_toggled(self, checked: bool):
382
- """Sync icon/size and label text when the extra toggle is changed."""
383
- self.cleanup_button.setIcon(self.icons["auto_purge"][0 if checked else 1])
384
- self.cleanup_button.setIconSize(self.icon_size if checked else self.icon_size_off)
385
- self.cleanup_label.setText(
386
- "Image-missing pairs will be removed automatically."
387
- if checked else
388
- "Image-missing pairs are kept; batch copy may raise warnings."
389
- )
390
- self.cleanup_label.setStyleSheet("color: none;" if checked else "color: rgb(125,125,125);")
391
- self.cleanup_enabled = bool(checked)
392
-
393
- def on_rescan_toggled(self, checked: bool):
394
- """Sync icon/size and label text when the rescan toggle is changed."""
395
- self.rescan_button.setIcon(self.icons["rescan"][0 if checked else 1])
396
- self.rescan_button.setIconSize(self.icon_size if checked else self.icon_size_off)
397
-
398
- self.rescan_label.setText(
399
- "The input folder will be re-scanned; input image list may differ or trigger warnings."
400
- if checked else
401
- "No folder scan; images identified in the master process will be used."
402
- )
403
- self.rescan_label.setStyleSheet("color: none;" if checked else "color: rgb(125,125,125);")
404
- self.rescan_enabled = bool(checked)
405
-
406
- def on_proceed(self):
407
- """Start or stop the progress loop depending on the button state."""
408
- if not self.loop_running:
409
- self.start_progress()
410
- else:
411
- self.stop_progress()
412
-
413
- def start_progress(self):
414
- """Start the progress and update button text to 'Stop'."""
415
- self.loop_running = True
416
- self.title_label.setText('Preparing copy...')
417
- self.title_label.setFont(self.item_font)
418
- self.proceed_button.setText(self.stop_button_text)
419
- self.proceed_button.setVisible(False)
420
- self.animate_window_resize()
421
- self.progress_bar.setVisible(True) # Show the progress bar when Proceed is clicked
422
- self.progress_bar.setMaximum(self.nfolders)
423
-
424
- def stop_progress(self):
425
- """Stop the progress and update button text to 'Proceed'."""
426
- self.loop_running = False
427
- self.proceed_button.setText(self.proceed_button_text)
428
- self.timer.stop()
429
-
430
- def update_progress(self):
431
- """Update the progress bar on each timer tick."""
432
- if self.iteration < self.progress_bar.maximum():
433
- timesleep(time_sleep_loop)
434
- title_text=self.paths[self.iteration][:48]+f"{'...' if len(self.paths[self.iteration])>50 else ''}"
435
- self.title_label.setText(title_text)
436
- self.warnings[self.iteration]=self.func(self.iteration,self.step_options,self.cleanup_enabled,self.rescan_enabled)
437
- self.progress_bar.setValue(self.iteration)
438
- self.iteration += 1
439
- if self.loop_running: self.timer.start(0)
440
- else:
441
- self.progress_bar.setValue(self.progress_bar.maximum())
442
- self.stop_progress()
443
- self.hide()
444
- self.show_batch_issues_dialog()
445
- if not self.animation: self.done(0) # Closes the dialog when complete
446
-
447
- def cancel(self):
448
- if self.loop_running: self.stop_progress()
449
- if not self.animation: self.done(0) # Closes the dialog when complete
450
-
451
- def animate_window_resize(self):
452
- for w in self.widgets:
453
- w:QWidget
454
- w.setVisible(False)
455
- self.animation = QVariantAnimation(self)
456
- self.animation.valueChanged.connect(self.window_resize)
457
- self.animation.setDuration(300)
458
- self.animation.setStartValue(self.max_height)
459
- self.animation.setEndValue(self.min_height)
460
- self.animation.finished.connect(self.finishedAnimation)
461
- self.animation.start()
462
-
463
- def finishedAnimation(self):
464
- self.animation=None
465
- self.timer.start(0) # Start or resume the timer
466
-
467
- def window_resize(self, h):
468
- self.setFixedHeight(h)
469
-
470
- def show_batch_issues_dialog(self):
471
- """Show a summary dialog listing all destination folders with warnings=True."""
472
- issues=[(i,p) for i,p in enumerate(self.paths) if i<len(self.warnings) and self.warnings[i]]
473
- if not issues: return
474
-
475
- dlg=QDialog(self); dlg.setWindowTitle("Batch copy issues")
476
- font=dlg.font()
477
- font.setPixelSize(fontPixelSize)
478
- dlg.setFont(font)
479
- lay=QVBoxLayout(dlg)
480
-
481
- # --- Warning header with icon ---
482
- icon_width=96
483
- warn_layout = QHBoxLayout()
484
- warn_icon = QLabel()
485
- warn_pix = QPixmap(icons_path + "warning.png").scaled(
486
- icon_width, icon_width, Qt.KeepAspectRatio, Qt.SmoothTransformation
487
- )
488
- warn_icon.setPixmap(warn_pix)
489
- warn_icon.setScaledContents(False)
490
- #warn_icon.setAlignment(Qt.AlignTop)
491
- warn_icon.setFixedWidth(icon_width)
492
-
493
- head = QLabel(
494
- f"{len(issues)} destination folder{'s' if len(issues)>1 else ''} could require attention: "
495
- "potential warnings during batch copy or image-set mismatch!\n\n"
496
- "Please note that the interface may not explicitly flag these cases. "
497
- "Copy the paths below if you wish to inspect the corresponding processes.\n",
498
- dlg
499
- )
500
- head.setWordWrap(True)
501
-
502
- warn_layout.addWidget(warn_icon)
503
- warn_layout.addWidget(head)
504
- lay.addLayout(warn_layout)
505
-
506
- tree=QTreeWidget(dlg); tree.setHeaderHidden(True)
507
- tree.header().setStretchLastSection(True)
508
- for _,p in issues:
509
- item=QTreeWidgetItem([p])
510
- item.setToolTip(0,p)
511
- tree.addTopLevelItem(item)
512
- lay.addWidget(tree)
513
-
514
- # actions
515
- btns=QHBoxLayout()
516
- def _copy():
517
- txt="\n".join(p for _,p in issues)
518
- QApplication.clipboard().setText(txt)
519
- copy_btn=QPushButton("Copy paths"); copy_btn.clicked.connect(_copy)
520
- close_btn=QPushButton("Close"); close_btn.clicked.connect(dlg.accept)
521
- btns.addWidget(copy_btn); btns.addStretch(1); btns.addWidget(close_btn)
522
- lay.addLayout(btns)
523
-
524
- dlg.resize(720, 360); dlg.exec()
525
-
526
- if __name__ == "__main__":
527
- import random
528
- import string
529
-
530
- # Function to generate a random string
531
- def generate_random_string(length):
532
- letters = string.ascii_letters # Includes uppercase and lowercase letters
533
- return ''.join(random.choice(letters) for i in range(length))
534
-
535
- # Number of strings and length of each string
536
- num_strings = 10 # Number of strings in the list
537
- string_length = 25 # Length of each string
538
-
539
- # Generate the list of random strings
540
- random_strings_list = [generate_random_string(string_length) for _ in range(num_strings)]
541
-
542
-
543
- app = QApplication(sys.argv)
544
- app.setStyle('Fusion')
545
-
546
- time_sleep_loop=0.01
547
-
548
- print(choose_directories())
549
-
550
- # Example lists
551
- pixmap_list = [icons_path + "cal_step.png",
552
- icons_path + "min_step.png",
553
- icons_path + "disp_step.png",
554
- icons_path + "piv_step.png"]
555
- flag_list = [False,True,True,True]
556
- name_list = ["Camera calibration",
557
- "Image pre-processing",
558
- "Disparity correction",
559
- "Stereoscopic PIV analysis"]
560
-
561
- dialog = FolderLoopDialog(pixmap_list, name_list, flag_list, paths=random_strings_list, process_name='Stereoscopic PIV process 1')
562
- sys.exit(dialog.exec())
13
+
14
+ time_sleep_loop=0
15
+
16
+ def choose_directories(base:Path = Path('.'),parent=None) -> Optional[List[str]]:
17
+ """
18
+ Open a dialogue to select multiple directories
19
+ Args:
20
+ base (Path): Starting directory to show when opening dialogue
21
+ Returns:
22
+ List[str]: List of paths that were selected, ``None`` if "cancel" selected"
23
+ References:
24
+ Mildly adapted from https://stackoverflow.com/a/28548773
25
+ to use outside an exising Qt Application
26
+ """
27
+
28
+ file_dialog = QFileDialog(parent)
29
+ file_dialog.setWindowTitle("Select folders for process loop")
30
+ file_dialog.setWindowIcon(QIcon(icons_path + "process_loop.png"))
31
+ file_dialog.setWindowIconText("Select folders for process loop")
32
+ file_dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
33
+ file_dialog.setFileMode(QFileDialog.Directory)
34
+ for widget_type in (QListView, QTreeView):
35
+ for view in file_dialog.findChildren(widget_type):
36
+ if isinstance(view.model(), QFileSystemModel):
37
+ view.setSelectionMode(
38
+ QAbstractItemView.ExtendedSelection)
39
+
40
+ paths=[]
41
+ if file_dialog.exec():
42
+ paths = file_dialog.selectedFiles()
43
+ return paths
44
+
45
+ class FolderLoopDialog(QDialog):
46
+ def __init__(self, pixmap_list, name_list, flag_list, parent=None, func=lambda it, opt: print(f"Iteration {it}"), paths=[], process_name = 'Process', *args, **kwargs):
47
+ super().__init__(parent=parent, *args, **kwargs)
48
+
49
+ self.func=func
50
+ self.nfolders=len(paths)
51
+ self.paths=paths
52
+
53
+ # Variables for easy customization
54
+ self.setWindowTitle("Process loop over folders")
55
+ self.setWindowIcon(QIcon(icons_path + "process_loop.png"))
56
+ self.setWindowIconText("Process loop over folders")
57
+ self.title_text = "Configure each step of the process"
58
+ if parent is None:
59
+ self.title_font = QFont(fontName, fontPixelSize+12, QFont.Bold)
60
+ self.item_font = QFont(fontName, fontPixelSize+8)
61
+ self.button_font = QFont(fontName, fontPixelSize+2)
62
+ else:
63
+ self.title_font:QFont = parent.font()
64
+ self.title_font.setBold(True)
65
+ self.title_font.setPixelSize(fontPixelSize+12)
66
+ self.item_font:QFont = parent.font()
67
+ self.item_font = parent.font()
68
+ self.item_font.setPixelSize(fontPixelSize+8)
69
+ self.button_font: QFont = parent.font()
70
+ self.button_font.setPixelSize(fontPixelSize+2)
71
+ self.title_height = 48
72
+ self.icon_size = QSize(42, 42) # Icon size inside buttons
73
+ self.icon_size_off = QSize(24, 24) # Icon size inside buttons when unchecked
74
+ self.button_size = QSize(56, 56) # Button size
75
+ self.row_spacing = 18 # Spacing between rows
76
+ self.margin_size = 5 # Window margin size
77
+ self.progress_bar_height = 36
78
+
79
+ self.min_height = self.row_spacing + self.margin_size*2 + self.title_height *2 + self.progress_bar_height # min window height
80
+ self.max_height = len(name_list) * (self.button_size.height() + self.row_spacing) + self.min_height # Max window height
81
+
82
+ # Tooltip texts
83
+ self.tooltips = {
84
+ "copy": "Copy the item",
85
+ "link": "Link the item",
86
+ "change_folder": "Change the folder",
87
+ "auto_purge": "Auto-remove missing-image pairs",
88
+ "rescan": "Re-scan destination paths"
89
+ }
90
+
91
+ # Icons for the buttons
92
+ self.icons = {
93
+ "copy": [QIcon(icons_path + "copy_process.png"), QIcon(icons_path + "copy_process_off.png")],
94
+ "link": [QIcon(icons_path + "link.png"), QIcon(icons_path + "unlink.png")],
95
+ "change_folder": [QIcon(icons_path + "change_folder.png"), QIcon(icons_path + "change_folder_off.png")],
96
+ "auto_purge": [QIcon(icons_path + "folder_loop_cleanup.png"), QIcon(icons_path + "folder_loop_cleanup_off.png")],
97
+ "rescan": [QIcon(icons_path + "scan_path_loop.png"), QIcon(icons_path + "scan_path_loop_off.png")],
98
+ }
99
+ self.options_list=list(self.icons)
100
+
101
+ # Main layout
102
+ layout = QVBoxLayout()
103
+ layout.setSpacing(self.row_spacing)
104
+
105
+ # Left-aligned title
106
+ header_layout = QHBoxLayout()
107
+ header_layout.setContentsMargins(0,0,0,0)
108
+ header_layout.setSpacing(10)
109
+
110
+ self.header_icon = QLabel('')
111
+ self.header_icon.setPixmap(QPixmap(icons_path + "process_loop.png"))
112
+ self.header_icon.setScaledContents(True)
113
+ self.header_icon.setFixedSize(self.icon_size)
114
+ header_layout.addWidget(self.header_icon)
115
+
116
+ header_text=process_name[:48]+f"{'...' if len(process_name)>50 else ''}"
117
+ self.header_label = QLabel(header_text)
118
+ self.header_label.setFont(self.title_font)
119
+ self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
120
+ self.header_label.setFixedHeight(self.title_height)
121
+ header_layout.addWidget(self.header_label)
122
+
123
+ layout.addLayout(header_layout)
124
+
125
+ # Left-aligned title
126
+ title_layout = QHBoxLayout()
127
+ title_layout.setContentsMargins(0,0,0,0)
128
+ title_layout.setSpacing(10)
129
+
130
+ """
131
+ self.title_icon = QLabel('')
132
+ self.title_icon.setPixmap(QPixmap(icons_path + "process_loop.png"))
133
+ self.title_icon.setScaledContents(True)
134
+ self.title_icon.setFixedSize(self.icon_size)
135
+ title_layout.addWidget(self.title_icon)
136
+ """
137
+
138
+ self.title_label = QLabel(self.title_text)
139
+ self.title_label.setFont(self.title_font)
140
+ self.title_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
141
+ self.title_label.setFixedHeight(self.title_height)
142
+ title_layout.addWidget(self.title_label)
143
+
144
+ layout.addLayout(title_layout)
145
+
146
+ # List to store button states (0, 1, 2)
147
+ self.button_states = [0] * len(name_list)
148
+
149
+ # Add the additional n elements
150
+ self.buttons_group = []
151
+ self.step_options = []
152
+ self.widgets = []
153
+ self.nstep=len(pixmap_list)
154
+ for i in range(self.nstep):
155
+ widget = QWidget()
156
+ item_layout = QHBoxLayout()
157
+ item_layout.setContentsMargins(0,0,0,0)
158
+ widget.setLayout(item_layout)
159
+ widget.setFixedHeight(self.button_size.height())
160
+ self.widgets.append(widget)
161
+
162
+ # Label with pixmap (fitted to 32x32 pixels)
163
+ pixmap_label = QLabel(self)
164
+ pixmap = QPixmap(pixmap_list[i])#.scaled(self.button_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
165
+ pixmap_label.setPixmap(pixmap)
166
+ pixmap_label.setScaledContents(True)
167
+ pixmap_label.setFixedSize(self.button_size)
168
+ item_layout.addWidget(pixmap_label)
169
+
170
+ spacer=QSpacerItem(10, 0, QSizePolicy.Minimum, QSizePolicy.Minimum)
171
+ item_layout.addItem(spacer)
172
+
173
+ # Label with text from the name list (larger font)
174
+ name_label = QLabel(name_list[i],self)
175
+ name_label.setFont(self.item_font)
176
+ name_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
177
+ item_layout.addWidget(name_label)
178
+
179
+ # Stretch the name label to fill the row
180
+ item_layout.addStretch()
181
+
182
+ # Three checkable buttons with icons
183
+ self.step_options.append(-1)
184
+ copy_button = self.create_icon_button(i, "copy")
185
+ link_button = self.create_icon_button(i, "link")
186
+ if flag_list[i]:
187
+ folder_button = self.create_icon_button(i, "change_folder")
188
+ else:
189
+ folder_button = QLabel(self,text='')
190
+ folder_button.setFixedWidth(self.button_size.width())
191
+ self.buttons_group.append([copy_button, link_button, folder_button])
192
+
193
+ item_layout.addWidget(copy_button)
194
+ item_layout.addWidget(link_button)
195
+ item_layout.addWidget(folder_button)
196
+
197
+ layout.addWidget(widget)
198
+
199
+ # --- Cleanup row ---
200
+ cleanup_spacing = 5
201
+ self.cleanup_widget = QWidget()
202
+ cw_layout = QHBoxLayout(self.cleanup_widget)
203
+ cw_layout.setContentsMargins(0, cleanup_spacing, 0, cleanup_spacing)
204
+ cw_layout.setSpacing(5)
205
+
206
+ self.cleanup_button = QPushButton(self)
207
+ self.cleanup_button.setCheckable(True)
208
+ self.cleanup_button.setChecked(False)
209
+ self.cleanup_button.setIcon(self.icons["auto_purge"][1])
210
+ self.cleanup_button.setFixedSize(self.button_size)
211
+ self.cleanup_button.setStyleSheet("QPushButton{border: none;}")
212
+ self.cleanup_button.setCursor(QCursor(Qt.PointingHandCursor))
213
+ self.cleanup_button.setToolTip(self.tooltips["auto_purge"])
214
+ self.cleanup_button.setIconSize(self.icon_size_off)
215
+
216
+ self.cleanup_label = QLabel("", self)
217
+ self.cleanup_label_font = parent.font()
218
+ self.cleanup_label_font.setPixelSize(fontPixelSize)
219
+ self.cleanup_label_font.setItalic(True)
220
+ self.cleanup_label.setFont(self.cleanup_label_font)
221
+ self.cleanup_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
222
+
223
+ self.cleanup_button.toggled.connect(self.on_cleanup_toggled)
224
+ cw_layout.addItem(QSpacerItem(10, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
225
+ cw_layout.addWidget(self.cleanup_label)
226
+ cw_layout.addWidget(self.cleanup_button)
227
+
228
+
229
+ # --- Rescan row ---
230
+ rescan_spacing = 5
231
+ self.rescan_widget = QWidget()
232
+ rw_layout = QHBoxLayout(self.rescan_widget)
233
+ rw_layout.setContentsMargins(0, rescan_spacing, 0, rescan_spacing)
234
+ rw_layout.setSpacing(5)
235
+
236
+ self.rescan_button = QPushButton(self)
237
+ self.rescan_button.setCheckable(True)
238
+ self.rescan_button.setChecked(False)
239
+ self.rescan_button.setIcon(self.icons["rescan"][1])
240
+ self.rescan_button.setFixedSize(self.button_size)
241
+ self.rescan_button.setStyleSheet("QPushButton{border: none;}")
242
+ self.rescan_button.setCursor(QCursor(Qt.PointingHandCursor))
243
+ self.rescan_button.setToolTip(self.tooltips["rescan"])
244
+ self.rescan_button.setIconSize(self.icon_size_off)
245
+
246
+ self.rescan_label = QLabel("", self)
247
+ self.rescan_label_font = parent.font()
248
+ self.rescan_label_font.setPixelSize(fontPixelSize)
249
+ self.rescan_label_font.setItalic(True)
250
+ self.rescan_label.setFont(self.rescan_label_font)
251
+ self.rescan_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
252
+
253
+ self.rescan_button.toggled.connect(self.on_rescan_toggled)
254
+ rw_layout.addItem(QSpacerItem(10, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
255
+ rw_layout.addWidget(self.rescan_label)
256
+ rw_layout.addWidget(self.rescan_button)
257
+
258
+ # --- Container for both rows ---
259
+ self.extra_opts_widget = QWidget()
260
+ eo_layout = QVBoxLayout(self.extra_opts_widget)
261
+ eo_layout.setContentsMargins(0, 5, 0, 50) # top=10, bottom=10
262
+ eo_layout.setSpacing(5)
263
+ eo_layout.addWidget(self.rescan_widget)
264
+ eo_layout.addWidget(self.cleanup_widget)
265
+ self.extra_opts_widget.setFixedHeight(self.button_size.height()*2+60)
266
+
267
+ self.extra_opts_widget.setVisible(False)
268
+ self.widgets.append(self.extra_opts_widget)
269
+
270
+ # Add container to parent layout
271
+ layout.addWidget(self.extra_opts_widget)
272
+
273
+ self.warnings=[False]*len(self.paths)
274
+
275
+ # Progress bar and final buttons (Cancel, Proceed)
276
+ progress_widget = QWidget()
277
+ progress_layout = QHBoxLayout()
278
+ progress_layout.setContentsMargins(0,0,0,0)
279
+ progress_layout.setSpacing(10)
280
+ progress_widget.setLayout(progress_layout)
281
+ progress_widget.setFixedHeight(self.progress_bar_height)
282
+ self.progress_bar = QProgressBar(self)
283
+ self.progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
284
+ self.progress_bar.setVisible(False) # Hidden initially
285
+ self.title_label.adjustSize()
286
+ self.progress_bar.setFixedWidth(self.title_label.width())
287
+ progress_layout.addWidget(self.progress_bar)
288
+
289
+ # Spacer to extend before the buttons
290
+ progress_layout.addStretch()
291
+
292
+ cancel_button_text = "Cancel"
293
+ self.proceed_button_text = "Proceed"
294
+ self.stop_button_text = "Stop"
295
+
296
+ cancel_button = QPushButton(cancel_button_text,self)
297
+ cancel_button.setFixedHeight(self.progress_bar_height)
298
+ cancel_button.setFont(self.button_font)
299
+ cancel_button.clicked.connect(self.cancel) # Closes the dialog without doing anything
300
+ progress_layout.addWidget(cancel_button)
301
+
302
+ self.proceed_button = QPushButton(self.proceed_button_text,self)
303
+ self.proceed_button.setFixedHeight(self.progress_bar_height)
304
+ self.proceed_button.setFont(self.button_font)
305
+ self.proceed_button.clicked.connect(self.on_proceed)
306
+ progress_layout.addWidget(self.proceed_button)
307
+
308
+ layout.addWidget(progress_widget)
309
+ self.setLayout(layout)
310
+
311
+ # Set window maximum height and margins
312
+ self.on_cleanup_toggled(self.cleanup_button.isChecked())
313
+ self.on_rescan_toggled(self.rescan_button.isChecked())
314
+ self.update_extraopts_visibility(FlagAnimation=False)
315
+ self.setFixedHeight(self.max_height)
316
+ self.setContentsMargins(self.margin_size*2, self.margin_size, self.margin_size*2, self.margin_size)
317
+ self.setMinimumWidth(640)
318
+ self.setMaximumWidth(800)
319
+
320
+ # Timer for progress
321
+ self.iteration = 0
322
+ self.timer = QTimer(self)
323
+ self.timer.timeout.connect(self.update_progress)
324
+ self.timer.setSingleShot(True)
325
+ self.loop_running = False
326
+
327
+ self.animation=None
328
+
329
+ def create_icon_button(self, index, button_type):
330
+ """Create a checkable button with icon based on its type (copy, link, change_folder)."""
331
+ button = QPushButton(self)
332
+ button.setCheckable(True)
333
+ button.setChecked((button_type=='change_folder' and index==self.nstep-1) or (button_type=='copy' and index<self.nstep-1))
334
+ button.setIcon(self.icons[button_type][int(not button.isChecked())]) # Set initial 'off' icon
335
+ button.setFixedSize(self.button_size) # Set button size to 32x32 pixels
336
+ button.setStyleSheet("QPushButton{border: none;}") # Remove button borders
337
+ button.setCursor(QCursor(Qt.PointingHandCursor)) # Set cursor to pointing hand on hover
338
+ button.setToolTip(self.tooltips[button_type]) # Set tooltip
339
+ button.clicked.connect(lambda: self.update_buttons(index, button_type))
340
+ if button.isChecked():
341
+ self.step_options[index]=self.options_list.index(button_type)
342
+ button.setIconSize(self.icon_size)
343
+ else:
344
+ button.setIconSize(self.icon_size_off) # Set icon size to 28x28 pixels
345
+ return button
346
+
347
+ def update_buttons(self, index, button_type):
348
+ """Update the icons and states of the buttons, ensuring only one is checked at a time."""
349
+ for k, button in enumerate(self.buttons_group[index]):
350
+ if not isinstance(button,QPushButton): continue
351
+ if button_type!=self.options_list[k]:
352
+ button.setChecked(False) # Uncheck all buttons
353
+ button.setIconSize(self.icon_size_off)
354
+ button.setIcon(self.icons[self.options_list[k]][1])
355
+ else:
356
+ button.setChecked(True)
357
+ button.setIconSize(self.icon_size)
358
+ button.setIcon(self.icons[self.options_list[k]][0]) # Checked state icon
359
+ self.step_options[index]=k
360
+ self.update_extraopts_visibility()
361
+
362
+ def update_extraopts_visibility(self,FlagAnimation=True):
363
+ """Show extra row if any row has 'change_folder' checked; hide otherwise."""
364
+ try:
365
+ idx_change = self.options_list.index("change_folder")
366
+ except ValueError:
367
+ idx_change = -1
368
+
369
+ show = (idx_change >= 0) and any(
370
+ (opt == idx_change) for opt in self.step_options if isinstance(opt, int)
371
+ )
372
+
373
+ if self.extra_opts_widget.isVisible() != show:
374
+ self.extra_opts_widget.setVisible(show)
375
+ extra_opts_widget = self.extra_opts_widget.height()
376
+ maxHeight = self.nstep * (self.button_size.height() + self.row_spacing) + int(show)*extra_opts_widget + self.min_height
377
+ if FlagAnimation:
378
+ self.window_resize(maxHeight)
379
+ self.max_height = maxHeight
380
+
381
+ def on_cleanup_toggled(self, checked: bool):
382
+ """Sync icon/size and label text when the extra toggle is changed."""
383
+ self.cleanup_button.setIcon(self.icons["auto_purge"][0 if checked else 1])
384
+ self.cleanup_button.setIconSize(self.icon_size if checked else self.icon_size_off)
385
+ self.cleanup_label.setText(
386
+ "Image-missing pairs will be removed automatically."
387
+ if checked else
388
+ "Image-missing pairs are kept; batch copy may raise warnings."
389
+ )
390
+ self.cleanup_label.setStyleSheet("color: none;" if checked else "color: rgb(125,125,125);")
391
+ self.cleanup_enabled = bool(checked)
392
+
393
+ def on_rescan_toggled(self, checked: bool):
394
+ """Sync icon/size and label text when the rescan toggle is changed."""
395
+ self.rescan_button.setIcon(self.icons["rescan"][0 if checked else 1])
396
+ self.rescan_button.setIconSize(self.icon_size if checked else self.icon_size_off)
397
+
398
+ self.rescan_label.setText(
399
+ "The input folder will be re-scanned; input image list may differ or trigger warnings."
400
+ if checked else
401
+ "No folder scan; images identified in the master process will be used."
402
+ )
403
+ self.rescan_label.setStyleSheet("color: none;" if checked else "color: rgb(125,125,125);")
404
+ self.rescan_enabled = bool(checked)
405
+
406
+ def on_proceed(self):
407
+ """Start or stop the progress loop depending on the button state."""
408
+ if not self.loop_running:
409
+ self.start_progress()
410
+ else:
411
+ self.stop_progress()
412
+
413
+ def start_progress(self):
414
+ """Start the progress and update button text to 'Stop'."""
415
+ self.loop_running = True
416
+ self.title_label.setText('Preparing copy...')
417
+ self.title_label.setFont(self.item_font)
418
+ self.proceed_button.setText(self.stop_button_text)
419
+ self.proceed_button.setVisible(False)
420
+ self.animate_window_resize()
421
+ self.progress_bar.setVisible(True) # Show the progress bar when Proceed is clicked
422
+ self.progress_bar.setMaximum(self.nfolders)
423
+
424
+ def stop_progress(self):
425
+ """Stop the progress and update button text to 'Proceed'."""
426
+ self.loop_running = False
427
+ self.proceed_button.setText(self.proceed_button_text)
428
+ self.timer.stop()
429
+
430
+ def update_progress(self):
431
+ """Update the progress bar on each timer tick."""
432
+ if self.iteration < self.progress_bar.maximum():
433
+ timesleep(time_sleep_loop)
434
+ title_text=self.paths[self.iteration][:48]+f"{'...' if len(self.paths[self.iteration])>50 else ''}"
435
+ self.title_label.setText(title_text)
436
+ self.warnings[self.iteration]=self.func(self.iteration,self.step_options,self.cleanup_enabled,self.rescan_enabled)
437
+ self.progress_bar.setValue(self.iteration)
438
+ self.iteration += 1
439
+ if self.loop_running: self.timer.start(0)
440
+ else:
441
+ self.progress_bar.setValue(self.progress_bar.maximum())
442
+ self.stop_progress()
443
+ self.hide()
444
+ self.show_batch_issues_dialog()
445
+ if not self.animation: self.done(0) # Closes the dialog when complete
446
+
447
+ def cancel(self):
448
+ if self.loop_running: self.stop_progress()
449
+ if not self.animation: self.done(0) # Closes the dialog when complete
450
+
451
+ def animate_window_resize(self):
452
+ for w in self.widgets:
453
+ w:QWidget
454
+ w.setVisible(False)
455
+ self.animation = QVariantAnimation(self)
456
+ self.animation.valueChanged.connect(self.window_resize)
457
+ self.animation.setDuration(300)
458
+ self.animation.setStartValue(self.max_height)
459
+ self.animation.setEndValue(self.min_height)
460
+ self.animation.finished.connect(self.finishedAnimation)
461
+ self.animation.start()
462
+
463
+ def finishedAnimation(self):
464
+ self.animation=None
465
+ self.timer.start(0) # Start or resume the timer
466
+
467
+ def window_resize(self, h):
468
+ self.setFixedHeight(h)
469
+
470
+ def show_batch_issues_dialog(self):
471
+ """Show a summary dialog listing all destination folders with warnings=True."""
472
+ issues=[(i,p) for i,p in enumerate(self.paths) if i<len(self.warnings) and self.warnings[i]]
473
+ if not issues: return
474
+
475
+ dlg=QDialog(self); dlg.setWindowTitle("Batch copy issues")
476
+ font=dlg.font()
477
+ font.setPixelSize(fontPixelSize)
478
+ dlg.setFont(font)
479
+ lay=QVBoxLayout(dlg)
480
+
481
+ # --- Warning header with icon ---
482
+ icon_width=96
483
+ warn_layout = QHBoxLayout()
484
+ warn_icon = QLabel()
485
+ warn_pix = QPixmap(icons_path + "warning.png").scaled(
486
+ icon_width, icon_width, Qt.KeepAspectRatio, Qt.SmoothTransformation
487
+ )
488
+ warn_icon.setPixmap(warn_pix)
489
+ warn_icon.setScaledContents(False)
490
+ #warn_icon.setAlignment(Qt.AlignTop)
491
+ warn_icon.setFixedWidth(icon_width)
492
+
493
+ head = QLabel(
494
+ f"{len(issues)} destination folder{'s' if len(issues)>1 else ''} could require attention: "
495
+ "potential warnings during batch copy or image-set mismatch!\n\n"
496
+ "Please note that the interface may not explicitly flag these cases. "
497
+ "Copy the paths below if you wish to inspect the corresponding processes.\n",
498
+ dlg
499
+ )
500
+ head.setWordWrap(True)
501
+
502
+ warn_layout.addWidget(warn_icon)
503
+ warn_layout.addWidget(head)
504
+ lay.addLayout(warn_layout)
505
+
506
+ tree=QTreeWidget(dlg); tree.setHeaderHidden(True)
507
+ tree.header().setStretchLastSection(True)
508
+ for _,p in issues:
509
+ item=QTreeWidgetItem([p])
510
+ item.setToolTip(0,p)
511
+ tree.addTopLevelItem(item)
512
+ lay.addWidget(tree)
513
+
514
+ # actions
515
+ btns=QHBoxLayout()
516
+ def _copy():
517
+ txt="\n".join(p for _,p in issues)
518
+ QApplication.clipboard().setText(txt)
519
+ copy_btn=QPushButton("Copy paths"); copy_btn.clicked.connect(_copy)
520
+ close_btn=QPushButton("Close"); close_btn.clicked.connect(dlg.accept)
521
+ btns.addWidget(copy_btn); btns.addStretch(1); btns.addWidget(close_btn)
522
+ lay.addLayout(btns)
523
+
524
+ dlg.resize(720, 360); dlg.exec()
525
+
526
+ if __name__ == "__main__":
527
+ import random
528
+ import string
529
+
530
+ # Function to generate a random string
531
+ def generate_random_string(length):
532
+ letters = string.ascii_letters # Includes uppercase and lowercase letters
533
+ return ''.join(random.choice(letters) for i in range(length))
534
+
535
+ # Number of strings and length of each string
536
+ num_strings = 10 # Number of strings in the list
537
+ string_length = 25 # Length of each string
538
+
539
+ # Generate the list of random strings
540
+ random_strings_list = [generate_random_string(string_length) for _ in range(num_strings)]
541
+
542
+
543
+ app = QApplication(sys.argv)
544
+ app.setStyle('Fusion')
545
+
546
+ time_sleep_loop=0.01
547
+
548
+ print(choose_directories())
549
+
550
+ # Example lists
551
+ pixmap_list = [icons_path + "cal_step.png",
552
+ icons_path + "min_step.png",
553
+ icons_path + "disp_step.png",
554
+ icons_path + "piv_step.png"]
555
+ flag_list = [False,True,True,True]
556
+ name_list = ["Camera calibration",
557
+ "Image pre-processing",
558
+ "Disparity correction",
559
+ "Stereoscopic PIV analysis"]
560
+
561
+ dialog = FolderLoopDialog(pixmap_list, name_list, flag_list, paths=random_strings_list, process_name='Stereoscopic PIV process 1')
562
+ sys.exit(dialog.exec())