PaIRS-UniNa 0.2.5__cp312-cp312-win_amd64.whl → 0.2.8__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 (54) hide show
  1. PaIRS_UniNa/Changes.txt +45 -2
  2. PaIRS_UniNa/Explorer.py +3126 -3059
  3. PaIRS_UniNa/FolderLoop.py +561 -371
  4. PaIRS_UniNa/Input_Tab.py +826 -714
  5. PaIRS_UniNa/Input_Tab_CalVi.py +15 -17
  6. PaIRS_UniNa/Input_Tab_tools.py +3019 -3017
  7. PaIRS_UniNa/Output_Tab.py +2 -4
  8. PaIRS_UniNa/PaIRS.py +17 -17
  9. PaIRS_UniNa/PaIRS_pypacks.py +227 -56
  10. PaIRS_UniNa/Process_Tab.py +2 -2
  11. PaIRS_UniNa/Process_Tab_Disp.py +1 -1
  12. PaIRS_UniNa/Saving_tools.py +277 -277
  13. PaIRS_UniNa/TabTools.py +7 -4
  14. PaIRS_UniNa/Vis_Tab.py +129 -60
  15. PaIRS_UniNa/Whatsnew.py +15 -3
  16. PaIRS_UniNa/_PaIRS_PIV.pyd +0 -0
  17. PaIRS_UniNa/__init__.py +4 -4
  18. PaIRS_UniNa/addwidgets_ps.py +28 -20
  19. PaIRS_UniNa/calibView.py +7 -0
  20. PaIRS_UniNa/gPaIRS.py +3889 -3745
  21. PaIRS_UniNa/icons/flaticon_PaIRS_download_warning.png +0 -0
  22. PaIRS_UniNa/icons/folder_loop_cleanup.png +0 -0
  23. PaIRS_UniNa/icons/folder_loop_cleanup_off.png +0 -0
  24. PaIRS_UniNa/icons/pencil_bw.png +0 -0
  25. PaIRS_UniNa/icons/scan_path_loop.png +0 -0
  26. PaIRS_UniNa/icons/scan_path_loop_off.png +0 -0
  27. PaIRS_UniNa/listLib.py +301 -301
  28. PaIRS_UniNa/parForMulti.py +433 -433
  29. PaIRS_UniNa/pivParFor.py +1 -1
  30. PaIRS_UniNa/procTools.py +51 -3
  31. PaIRS_UniNa/rqrdpckgs.txt +6 -5
  32. PaIRS_UniNa/stereoPivParFor.py +1 -1
  33. PaIRS_UniNa/tabSplitter.py +606 -606
  34. PaIRS_UniNa/ui_Calibration_Tab.py +542 -542
  35. PaIRS_UniNa/ui_Custom_Top.py +294 -294
  36. PaIRS_UniNa/ui_Input_Tab.py +1098 -1098
  37. PaIRS_UniNa/ui_Input_Tab_CalVi.py +1280 -1280
  38. PaIRS_UniNa/ui_Log_Tab.py +261 -261
  39. PaIRS_UniNa/ui_Output_Tab.py +2360 -2360
  40. PaIRS_UniNa/ui_Process_Tab.py +3808 -3808
  41. PaIRS_UniNa/ui_Process_Tab_CalVi.py +1547 -1547
  42. PaIRS_UniNa/ui_Process_Tab_Disp.py +1139 -1139
  43. PaIRS_UniNa/ui_Process_Tab_Min.py +435 -435
  44. PaIRS_UniNa/ui_ResizePopup.py +203 -203
  45. PaIRS_UniNa/ui_Vis_Tab.py +1626 -1626
  46. PaIRS_UniNa/ui_Vis_Tab_CalVi.py +1249 -1249
  47. PaIRS_UniNa/ui_Whatsnew.py +131 -131
  48. PaIRS_UniNa/ui_gPairs.py +873 -867
  49. PaIRS_UniNa/ui_infoPaIRS.py +550 -550
  50. PaIRS_UniNa/whatsnew.txt +4 -4
  51. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.8.dist-info}/METADATA +31 -17
  52. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.8.dist-info}/RECORD +54 -48
  53. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.8.dist-info}/WHEEL +0 -0
  54. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.8.dist-info}/top_level.txt +0 -0
PaIRS_UniNa/FolderLoop.py CHANGED
@@ -1,372 +1,562 @@
1
- import sys
2
- from PySide6.QtWidgets import (
3
- QApplication, QDialog, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QSpacerItem,
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
- }
88
-
89
- # Icons for the buttons
90
- self.icons = {
91
- "copy": [QIcon(icons_path + "copy_process.png"), QIcon(icons_path + "copy_process_off.png")],
92
- "link": [QIcon(icons_path + "link.png"), QIcon(icons_path + "unlink.png")],
93
- "change_folder": [QIcon(icons_path + "change_folder.png"), QIcon(icons_path + "change_folder_off.png")]
94
- }
95
- self.options_list=list(self.icons)
96
-
97
- # Main layout
98
- layout = QVBoxLayout()
99
- layout.setSpacing(self.row_spacing)
100
-
101
- # Left-aligned title
102
- header_layout = QHBoxLayout()
103
- header_layout.setContentsMargins(0,0,0,0)
104
- header_layout.setSpacing(10)
105
-
106
- self.header_icon = QLabel('')
107
- self.header_icon.setPixmap(QPixmap(icons_path + "process_loop.png"))
108
- self.header_icon.setScaledContents(True)
109
- self.header_icon.setFixedSize(self.icon_size)
110
- header_layout.addWidget(self.header_icon)
111
-
112
- self.header_label = QLabel(process_name)
113
- self.header_label.setFont(self.title_font)
114
- self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
115
- self.header_label.setFixedHeight(self.title_height)
116
- header_layout.addWidget(self.header_label)
117
-
118
- layout.addLayout(header_layout)
119
-
120
- # Left-aligned title
121
- title_layout = QHBoxLayout()
122
- title_layout.setContentsMargins(0,0,0,0)
123
- title_layout.setSpacing(10)
124
-
125
- """
126
- self.title_icon = QLabel('')
127
- self.title_icon.setPixmap(QPixmap(icons_path + "process_loop.png"))
128
- self.title_icon.setScaledContents(True)
129
- self.title_icon.setFixedSize(self.icon_size)
130
- title_layout.addWidget(self.title_icon)
131
- """
132
-
133
- self.title_label = QLabel(self.title_text)
134
- self.title_label.setFont(self.title_font)
135
- self.title_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
136
- self.title_label.setFixedHeight(self.title_height)
137
- title_layout.addWidget(self.title_label)
138
-
139
- layout.addLayout(title_layout)
140
-
141
- # List to store button states (0, 1, 2)
142
- self.button_states = [0] * len(name_list)
143
-
144
- # Add the additional n elements
145
- self.buttons_group = []
146
- self.step_options = []
147
- self.widgets = []
148
- self.nstep=len(pixmap_list)
149
- for i in range(self.nstep):
150
- widget = QWidget()
151
- item_layout = QHBoxLayout()
152
- item_layout.setContentsMargins(0,0,0,0)
153
- widget.setLayout(item_layout)
154
- widget.setFixedHeight(self.button_size.height())
155
- self.widgets.append(widget)
156
-
157
- # Label with pixmap (fitted to 32x32 pixels)
158
- pixmap_label = QLabel(self)
159
- pixmap = QPixmap(pixmap_list[i])#.scaled(self.button_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
160
- pixmap_label.setPixmap(pixmap)
161
- pixmap_label.setScaledContents(True)
162
- pixmap_label.setFixedSize(self.button_size)
163
- item_layout.addWidget(pixmap_label)
164
-
165
- spacer=QSpacerItem(10, 0, QSizePolicy.Minimum, QSizePolicy.Minimum)
166
- item_layout.addItem(spacer)
167
-
168
- # Label with text from the name list (larger font)
169
- name_label = QLabel(name_list[i],self)
170
- name_label.setFont(self.item_font)
171
- name_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
172
- item_layout.addWidget(name_label)
173
-
174
- # Stretch the name label to fill the row
175
- item_layout.addStretch()
176
-
177
- # Three checkable buttons with icons
178
- self.step_options.append(-1)
179
- copy_button = self.create_icon_button(i, "copy")
180
- link_button = self.create_icon_button(i, "link")
181
- if flag_list[i]:
182
- folder_button = self.create_icon_button(i, "change_folder")
183
- else:
184
- folder_button = QLabel(self,text='')
185
- folder_button.setFixedWidth(self.button_size.width())
186
- self.buttons_group.append([copy_button, link_button, folder_button])
187
-
188
- item_layout.addWidget(copy_button)
189
- item_layout.addWidget(link_button)
190
- item_layout.addWidget(folder_button)
191
-
192
- layout.addWidget(widget)
193
-
194
- # Progress bar and final buttons (Cancel, Proceed)
195
- progress_widget = QWidget()
196
- progress_layout = QHBoxLayout()
197
- progress_layout.setContentsMargins(0,0,0,0)
198
- progress_layout.setSpacing(10)
199
- progress_widget.setLayout(progress_layout)
200
- progress_widget.setFixedHeight(self.progress_bar_height)
201
- self.progress_bar = QProgressBar(self)
202
- self.progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
203
- self.progress_bar.setVisible(False) # Hidden initially
204
- self.title_label.adjustSize()
205
- self.progress_bar.setFixedWidth(self.title_label.width())
206
- progress_layout.addWidget(self.progress_bar)
207
-
208
- # Spacer to extend before the buttons
209
- progress_layout.addStretch()
210
-
211
- cancel_button_text = "Cancel"
212
- self.proceed_button_text = "Proceed"
213
- self.stop_button_text = "Stop"
214
-
215
- cancel_button = QPushButton(cancel_button_text,self)
216
- cancel_button.setFixedHeight(self.progress_bar_height)
217
- cancel_button.setFont(self.button_font)
218
- cancel_button.clicked.connect(self.cancel) # Closes the dialog without doing anything
219
- progress_layout.addWidget(cancel_button)
220
-
221
- self.proceed_button = QPushButton(self.proceed_button_text,self)
222
- self.proceed_button.setFixedHeight(self.progress_bar_height)
223
- self.proceed_button.setFont(self.button_font)
224
- self.proceed_button.clicked.connect(self.on_proceed)
225
- progress_layout.addWidget(self.proceed_button)
226
-
227
- layout.addWidget(progress_widget)
228
- self.setLayout(layout)
229
-
230
- # Set window maximum height and margins
231
- self.setFixedHeight(self.max_height)
232
- self.setContentsMargins(self.margin_size*2, self.margin_size, self.margin_size*2, self.margin_size)
233
-
234
- # Timer for progress
235
- self.iteration = 0
236
- self.timer = QTimer(self)
237
- self.timer.timeout.connect(self.update_progress)
238
- self.timer.setSingleShot(True)
239
- self.loop_running = False
240
-
241
- self.animation=None
242
-
243
- def create_icon_button(self, index, button_type):
244
- """Create a checkable button with icon based on its type (copy, link, change_folder)."""
245
- button = QPushButton(self)
246
- button.setCheckable(True)
247
- button.setChecked((button_type=='change_folder' and index==self.nstep-1) or (button_type=='copy' and index<self.nstep-1))
248
- button.setIcon(self.icons[button_type][int(not button.isChecked())]) # Set initial 'off' icon
249
- button.setFixedSize(self.button_size) # Set button size to 32x32 pixels
250
- button.setStyleSheet("QPushButton{border: none;}") # Remove button borders
251
- button.setCursor(QCursor(Qt.PointingHandCursor)) # Set cursor to pointing hand on hover
252
- button.setToolTip(self.tooltips[button_type]) # Set tooltip
253
- button.clicked.connect(lambda: self.update_buttons(index, button_type))
254
- if button.isChecked():
255
- self.step_options[index]=self.options_list.index(button_type)
256
- button.setIconSize(self.icon_size)
257
- else:
258
- button.setIconSize(self.icon_size_off) # Set icon size to 28x28 pixels
259
- return button
260
-
261
- def update_buttons(self, index, button_type):
262
- """Update the icons and states of the buttons, ensuring only one is checked at a time."""
263
- for k, button in enumerate(self.buttons_group[index]):
264
- if not isinstance(button,QPushButton): continue
265
- if button_type!=self.options_list[k]:
266
- button.setChecked(False) # Uncheck all buttons
267
- button.setIconSize(self.icon_size_off)
268
- button.setIcon(self.icons[self.options_list[k]][1])
269
- else:
270
- button.setChecked(True)
271
- button.setIconSize(self.icon_size)
272
- button.setIcon(self.icons[self.options_list[k]][0]) # Checked state icon
273
- self.step_options[index]=k
274
-
275
- def on_proceed(self):
276
- """Start or stop the progress loop depending on the button state."""
277
- if not self.loop_running:
278
- self.start_progress()
279
- else:
280
- self.stop_progress()
281
-
282
- def start_progress(self):
283
- """Start the progress and update button text to 'Stop'."""
284
- self.loop_running = True
285
- self.title_label.setText('Preparing copy...')
286
- self.title_label.setFont(self.item_font)
287
- self.proceed_button.setText(self.stop_button_text)
288
- self.proceed_button.setVisible(False)
289
- self.animate_window_resize()
290
- self.progress_bar.setVisible(True) # Show the progress bar when Proceed is clicked
291
- self.progress_bar.setMaximum(self.nfolders)
292
-
293
- def stop_progress(self):
294
- """Stop the progress and update button text to 'Proceed'."""
295
- self.loop_running = False
296
- self.proceed_button.setText(self.proceed_button_text)
297
- self.timer.stop()
298
-
299
- def update_progress(self):
300
- """Update the progress bar on each timer tick."""
301
- if self.iteration < self.progress_bar.maximum():
302
- timesleep(time_sleep_loop)
303
- self.title_label.setText(self.paths[self.iteration])
304
- self.func(self.iteration,self.step_options)
305
- self.progress_bar.setValue(self.iteration)
306
- self.iteration += 1
307
- if self.loop_running: self.timer.start(0)
308
- else:
309
- self.progress_bar.setValue(self.progress_bar.maximum())
310
- self.stop_progress()
311
- if not self.animation: self.done(0) # Closes the dialog when complete
312
-
313
- def cancel(self):
314
- if self.loop_running: self.stop_progress()
315
- if not self.animation: self.done(0) # Closes the dialog when complete
316
-
317
- def animate_window_resize(self):
318
- for w in self.widgets:
319
- w:QWidget
320
- w.setVisible(False)
321
- self.animation = QVariantAnimation(self)
322
- self.animation.valueChanged.connect(self.window_resize)
323
- self.animation.setDuration(300)
324
- self.animation.setStartValue(self.max_height)
325
- self.animation.setEndValue(self.min_height)
326
- self.animation.finished.connect(self.finishedAnimation)
327
- self.animation.start()
328
-
329
- def finishedAnimation(self):
330
- self.animation=None
331
- self.timer.start(0) # Start or resume the timer
332
-
333
- def window_resize(self, h):
334
- self.setFixedHeight(h)
335
-
336
- if __name__ == "__main__":
337
- import random
338
- import string
339
-
340
- # Function to generate a random string
341
- def generate_random_string(length):
342
- letters = string.ascii_letters # Includes uppercase and lowercase letters
343
- return ''.join(random.choice(letters) for i in range(length))
344
-
345
- # Number of strings and length of each string
346
- num_strings = 10 # Number of strings in the list
347
- string_length = 25 # Length of each string
348
-
349
- # Generate the list of random strings
350
- random_strings_list = [generate_random_string(string_length) for _ in range(num_strings)]
351
-
352
-
353
- app = QApplication(sys.argv)
354
- app.setStyle('Fusion')
355
-
356
- time_sleep_loop=0.01
357
-
358
- print(choose_directories())
359
-
360
- # Example lists
361
- pixmap_list = [icons_path + "cal_step.png",
362
- icons_path + "min_step.png",
363
- icons_path + "disp_step.png",
364
- icons_path + "piv_step.png"]
365
- flag_list = [False,True,True,True]
366
- name_list = ["Camera calibration",
367
- "Image pre-processing",
368
- "Disparity correction",
369
- "Stereoscopic PIV analysis"]
370
-
371
- dialog = FolderLoopDialog(pixmap_list, name_list, flag_list, paths=random_strings_list, process_name='Stereoscopic PIV process 1')
372
- 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())