PaIRS-UniNa 0.2.7__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.
@@ -1,6 +1,7 @@
1
1
  from .ui_Calibration_Tab import*
2
2
  from .TabTools import*
3
3
  from .listLib import*
4
+ from .SPIVCalHelp import showSPIVCalHelp
4
5
 
5
6
  spin_tips={
6
7
  'ncam' : 'Number of cameras',
@@ -21,6 +22,7 @@ button_tips={
21
22
  combo_tips={}
22
23
 
23
24
  class CALpar(TABpar):
25
+ FlagSPIVCal = True
24
26
  def __init__(self,Process=ProcessTypes.null,Step=StepTypes.null):
25
27
  self.setup(Process,Step)
26
28
  super().__init__('CALpar','Calibration')
@@ -61,6 +63,8 @@ class Calibration_Tab(gPaIRS_Tab):
61
63
  self.app=app
62
64
  setAppGuiPalette(self)
63
65
 
66
+ self.ui.button_info.setStyleSheet("border: none;")
67
+
64
68
  #------------------------------------- Declaration of parameters
65
69
  self.CALpar_base=CALpar()
66
70
  self.CALpar:CALpar=self.TABpar
@@ -133,6 +137,7 @@ class Calibration_Tab(gPaIRS_Tab):
133
137
  self.ui.button_paste_below.setEnabled(FlagNCal and FlagCuttedItems)
134
138
  self.ui.button_paste_above.setEnabled(FlagNCal and FlagCuttedItems)
135
139
  self.ui.button_clean.setEnabled(FlagFiles)
140
+ self.ui.button_info.setVisible(self.CALpar.Process==ProcessTypes.spiv)
136
141
 
137
142
  self.ui.button_CalVi.setVisible(self.CALpar.flagRun==0)
138
143
 
@@ -166,13 +171,23 @@ class Calibration_Tab(gPaIRS_Tab):
166
171
  #*************************************************** Buttons
167
172
  #******************** Actions
168
173
  def button_CalVi_action(self):
174
+ if self.CALpar.FlagSPIVCal and self.ui.button_info.isVisible() and self.ui.button_CalVi.isChecked():
175
+ showSPIVCalHelp(self,self.dontShowAgainSPIVCalHelp)
169
176
  self.CALpar.FlagCalVi=self.ui.button_CalVi.isChecked()
170
177
 
178
+ def dontShowAgainSPIVCalHelp(self):
179
+ self.CALpar.FlagSPIVCal = False
180
+
181
+ def button_info_action(self):
182
+ showSPIVCalHelp(self)
183
+
171
184
  def button_scan_list_action(self):
172
185
  self.ui.calTree.setLists()
173
186
  self.CALpar.calEx=deep_duplicate(self.ui.calTree.calEx)
174
187
 
175
188
  def button_import_action(self):
189
+ if self.CALpar.FlagSPIVCal and self.ui.button_info.isVisible():
190
+ showSPIVCalHelp(self,self.dontShowAgainSPIVCalHelp)
176
191
  filenames, _ = QFileDialog.getOpenFileNames(self,\
177
192
  "Select calibration files from the current directory", filter='*.cal',\
178
193
  options=optionNativeDialog)
PaIRS_UniNa/Changes.txt CHANGED
@@ -1,3 +1,30 @@
1
+ ********* Changes in version 0.2.9 (2025.12.12) **********
2
+ Bug fixes:
3
+ - fixed a bug in the batch folder-copy tool enabling automatic removal of incomplete image pairs and a full re-scan of destination folders to detect and resolve any image-set mismatches.
4
+
5
+ New features:
6
+ - a new help dialog has been introduced to assist users during the stereoscopic PIV calibration process. The dialog provides a detailed explanation of the required coordinate convention (plate defining the x–y plane, z-axis normal to the plate and x-axis aligned with the stereoscopic baseline) and displays example images illustrating correct and incorrect configurations.
7
+
8
+
9
+
10
+ ********* Changes in version 0.2.8 (2025.11.14) **********
11
+ Bug fixes:
12
+ - corrected a bug in the z-vorticity computation: velocity gradients were not properly converted to physical units, causing an error in the magnitude scales though not in the qualitative distributions;
13
+ - corrected the display of maps of output variables and vector fields in Vis to ensure proper alignment across option changes;
14
+ - fixed issues related to path handling in the batch-folder copy and other modules: paths are no longer relativized, avoiding inconsistent behavior across different operating systems or different.
15
+
16
+ New features:
17
+ - added new options in the batch folder-copy tool to automatically remove image pairs where one or more files are missing in the destination folders and to fully re-scan the destination folders for cases in which mismatches in the image sets may occur;
18
+ - extended the existing functionality in Vis for loading and visualizing past results located under the specified output path and name root: when available, the corresponding saved log file is now automatically loaded and displayed in the Log tab.
19
+
20
+ User-interface enhancements:
21
+ - improved the naming logic for duplicated processes: copied processes are now assigned consistent incremental suffixes;
22
+ - improved the example-image tree and the behavior of the step spin box in the Image Import Tool (minimum step is now 1);
23
+ - refined the behavior of the import button: it is now always enabled, and when no changes in the image list are detected upon importing, the user simply receives a warning message;
24
+ - improved the behaviour of the list of path completers in the Input tabs;
25
+ - in Vis, resizing settings and automatic level-reset options are no longer global but apply individually per each step.
26
+
27
+
1
28
  ********* Changes in version 0.2.7 (2025.10.13) **********
2
29
  Bug fixes:
3
30
  - resolved an issue related to visulazion of streamlines when the image is shown in pixel units and a custom resolution is specified (in previous versions, streamlines were always represented in physical units);
PaIRS_UniNa/Explorer.py CHANGED
@@ -1452,8 +1452,8 @@ class ProcessTree(PaIRSTree):
1452
1452
  ITE.FlagQueue=self.TREpar.FlagQueue
1453
1453
  ITE.ind=[self.TREpar.project,int(self.FlagBin),ind,0,0]
1454
1454
 
1455
+ processNames=[item[0].name for item in self.itemList[0]]+[item[0].name for item in self.restoreTree.itemList[0]]
1455
1456
  if name is None:
1456
- processNames=[item[0].name for item in self.itemList[0]]+[item[0].name for item in self.restoreTree.itemList[0]]
1457
1457
  nameInd=1
1458
1458
  ITE.name=f'{ITE.basename} {nameInd}'
1459
1459
  while ITE.name in processNames:
@@ -1461,6 +1461,10 @@ class ProcessTree(PaIRSTree):
1461
1461
  ITE.name=f'{ITE.basename} {nameInd}'
1462
1462
  else:
1463
1463
  ITE.name=name
1464
+ nameInd=1
1465
+ while ITE.name in processNames:
1466
+ nameInd+=1
1467
+ ITE.name=f'{name} ({nameInd})'
1464
1468
  self.createParPrevs(ITE)
1465
1469
  self.itemList[0].insert(ind,[ITE])
1466
1470
  else:
@@ -1917,12 +1921,12 @@ class ProcessTree(PaIRSTree):
1917
1921
  pixmap_list.append(icons_path+ITE.icon)
1918
1922
  name_list.append(ITE.name)
1919
1923
  flag_list.append(ITE.Step!=StepTypes.cal)
1920
- func=lambda i, opts: self.process_loop(paths,processType,ind,ITEs[0].name,i,opts)
1924
+ func=lambda i, opts, cleanup_flag, rescan_flag: self.process_loop(paths,processType,ind,ITEs[0].name,i,opts,cleanup_flag,rescan_flag)
1921
1925
  dialog = FolderLoopDialog(pixmap_list, name_list, flag_list, parent=self, paths=paths, func=func, process_name=ITEs[0].name)
1922
1926
  dialog.exec()
1923
1927
  return
1924
1928
 
1925
- def process_loop(self,paths,processType,ind,name0,i,opts):
1929
+ def process_loop(self,paths,processType,ind,name0,i,opts,cleanup_flag,rescan_flag):
1926
1930
  nProcess=self.topLevelItemCount()
1927
1931
  path=paths[i]
1928
1932
  name=name0+f" (.../{os.path.basename(path)}/)"
@@ -1933,6 +1937,7 @@ class ProcessTree(PaIRSTree):
1933
1937
  ind_slave[2]=nProcess
1934
1938
  ind_slave[-1]=0
1935
1939
 
1940
+ FlagWarning=False
1936
1941
  for j in range(len(opts)):
1937
1942
  ind_master[3]=j
1938
1943
  ind_slave[3]=j
@@ -1944,10 +1949,17 @@ class ProcessTree(PaIRSTree):
1944
1949
  if opts[j]==2:
1945
1950
  INP: INPpar=self.gui.w_Input.TABpar_at(ind_new)
1946
1951
  INP.path=myStandardPath(path)
1947
- self.gui.w_Input.scanImList(ind_new)
1952
+ if rescan_flag:
1953
+ flagWarning=self.rescanInputPath(ind_master,ind_new,cleanup_flag)
1954
+ else:
1955
+ flagWarning=self.gui.w_Input.scanImList(ind_new)
1956
+ if cleanup_flag: self.gui.w_Input.purgeImList(ind_new)
1957
+ INP.nimg=0
1958
+ if len(INP.imList[0]):
1959
+ if len(INP.imList[0][0]):
1960
+ INP.nimg=len(INP.imList[0][0])
1948
1961
  self.gui.w_Input.checkINPpar(ind_new)
1949
1962
  self.gui.w_Input.setINPwarn(ind_new)
1950
- self.Explorer.setITElayout(ITE)
1951
1963
 
1952
1964
  TABname=self.gui.w_Input.TABname
1953
1965
  self.gui.bridge(TABname,ind_new)
@@ -1966,12 +1978,54 @@ class ProcessTree(PaIRSTree):
1966
1978
  w.TABpar.copyfrom(currpar)
1967
1979
  self.gui.bridge(w.TABname,ind_new)
1968
1980
  TABpar.FlagSettingPar=FlagSettingPar
1981
+ self.Explorer.setITElayout(ITE)
1982
+ FlagWarning=FlagWarning or flagWarning or ITE.OptionDone==0
1983
+
1969
1984
  #item_child=item.child(j)
1970
1985
  #item_child.setSelected(True)
1971
1986
  #self.setCurrentItem(item_child)
1972
1987
  else:
1973
1988
  self.gui.link_pars(ind_slave,ind_master,FlagSet=False)
1974
- return
1989
+ return FlagWarning
1990
+
1991
+ def rescanInputPath(self, ind_master, ind_new, FlagNoWarning):
1992
+ """Rescan input on slave using master patterns, then rebuild imList/imEx."""
1993
+ INP: INPpar = self.gui.w_Input.TABpar_at(ind_new) # slave
1994
+ INPm: INPpar = self.gui.w_Input.TABpar_at(ind_master) # master
1995
+
1996
+ # Build A (for frame_1) and B (for frame_2) from master's pattern at master's frames
1997
+ patm = getattr(INPm.imSet, "pattern", [])
1998
+ ncam = INP.inp_ncam
1999
+ def _pat_list(frames,ind0=0):
2000
+ out=[];
2001
+ for k in range(ncam):
2002
+ f = frames[k]-ind0 if k < len(frames) else -1
2003
+ out.append(patm[f] if isinstance(f,int) and 0 <= f < len(patm) else None)
2004
+ return out
2005
+ A = _pat_list(INPm.frame_1)
2006
+ B = _pat_list(INPm.frame_2,1)
2007
+
2008
+ # Rescan slave input path with patterns=A,B (maps patterns -> frame indices on slave)
2009
+ self.gui.w_Input.scanInputPath(ind_new, patterns=[A, B], FlagNoWarning=FlagNoWarning)
2010
+
2011
+ # Rebuild imList/imEx from frames computed on slave
2012
+ INP.imList, INP.imEx = INP.imSet.genListsFromFrame(
2013
+ INP.frame_1, INP.frame_2, INP.ind_in, INP.npairs, INP.step, INP.FlagTR_Import
2014
+ )
2015
+ # Compare slave vs master lists
2016
+ def _lists_differ(a, b):
2017
+ if len(a) != len(b): return True
2018
+ for x, y in zip(a, b):
2019
+ if x != y: return True
2020
+ return False
2021
+
2022
+ FlagWarning = (
2023
+ _lists_differ(INP.imList, INPm.imList) or
2024
+ _lists_differ(INP.imEx, INPm.imEx)
2025
+ )
2026
+
2027
+ return FlagWarning
2028
+
1975
2029
 
1976
2030
  def button_delete_action(self):
1977
2031
  self.blockSignals(True)
@@ -2957,9 +3011,16 @@ class StartingPage(QFrame):
2957
3011
  for n, process in processes.items():
2958
3012
  # Layout orizzontale per ogni processo
2959
3013
  widget=QWidget()
3014
+ widget.setObjectName("process_item")
3015
+ widget.setStyleSheet("""
3016
+ QWidget#process_item:hover {
3017
+ background-color: rgba(64, 64, 255, 33);
3018
+ border-radius: 10px;
3019
+ }
3020
+ """)
2960
3021
  process_layout = QHBoxLayout(widget)
2961
3022
  process_layout.setSpacing(self.LAYOUT_SPACING)
2962
- process_layout.setContentsMargins(0, 0, 0, 0)
3023
+ process_layout.setContentsMargins(10, 10, 10, 0)
2963
3024
 
2964
3025
  # Pulsante con icona
2965
3026
  button_layout = QHBoxLayout()
@@ -3007,13 +3068,15 @@ class StartingPage(QFrame):
3007
3068
  caption_text_edit.setFont(caption_font)
3008
3069
  caption_text_edit.setAlignment(Qt.AlignmentFlag.AlignJustify)
3009
3070
  caption_text_edit.setReadOnly(True)
3071
+ caption_text_edit.setTextInteractionFlags(Qt.NoTextInteraction)
3072
+ caption_text_edit.viewport().setCursor(Qt.PointingHandCursor)
3010
3073
  caption_text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
3011
3074
  caption_text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
3012
3075
  caption_text_edit.setFrameStyle(QFrame.NoFrame)
3013
3076
  #caption_text_edit.setFixedWidth(self.CAPTION_WIDTH)
3014
3077
  caption_text_edit.setStyleSheet("background: transparent;")
3015
3078
  caption_text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
3016
-
3079
+
3017
3080
  # Adjust height to content
3018
3081
  caption_text_edit.document().setTextWidth(caption_text_edit.viewport().width())
3019
3082
  caption_text_edit.setFixedHeight(CAPTION_HEIGHT)#caption_text_edit.document().size().height())
@@ -3029,6 +3092,25 @@ class StartingPage(QFrame):
3029
3092
  # Aggiungi il layout orizzontale al layout principale
3030
3093
  self.main_layout.addWidget(widget)
3031
3094
 
3095
+ # --- Make the whole row clickable (icon + labels + background) ---
3096
+ if buttonBar:
3097
+
3098
+ def make_clickable(w, callback):
3099
+ w.setCursor(Qt.PointingHandCursor)
3100
+
3101
+ def mouseReleaseEvent(event, cb=callback, ww=w):
3102
+ if event.button() == Qt.LeftButton:
3103
+ cb()
3104
+ # call base implementation to keep default behaviour
3105
+ QWidget.mouseReleaseEvent(ww, event)
3106
+
3107
+ w.mouseReleaseEvent = mouseReleaseEvent
3108
+
3109
+ # entire row + title + caption all trigger the same action
3110
+ make_clickable(widget, action(n))
3111
+ #make_clickable(name_label, click_callback)
3112
+ #make_clickable(caption_text_edit, click_callback)
3113
+
3032
3114
  self.items[n]=widget
3033
3115
 
3034
3116
  self.main_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding))
PaIRS_UniNa/FolderLoop.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  from PySide6.QtWidgets import (
3
- QApplication, QDialog, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QSpacerItem,
3
+ QApplication, QDialog, QVBoxLayout, QLabel, QHBoxLayout, QWidget, QSpacerItem, QTreeWidget, QTreeWidgetItem,
4
4
  QPushButton, QProgressBar, QSizePolicy,
5
5
  QFileDialog, QListView, QAbstractItemView, QTreeView, QFileSystemModel
6
6
  )
@@ -83,14 +83,18 @@ class FolderLoopDialog(QDialog):
83
83
  self.tooltips = {
84
84
  "copy": "Copy the item",
85
85
  "link": "Link the item",
86
- "change_folder": "Change the folder"
86
+ "change_folder": "Change the folder",
87
+ "auto_purge": "Auto-remove missing-image pairs",
88
+ "rescan": "Re-scan destination paths"
87
89
  }
88
90
 
89
91
  # Icons for the buttons
90
92
  self.icons = {
91
93
  "copy": [QIcon(icons_path + "copy_process.png"), QIcon(icons_path + "copy_process_off.png")],
92
94
  "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")]
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")],
94
98
  }
95
99
  self.options_list=list(self.icons)
96
100
 
@@ -109,7 +113,8 @@ class FolderLoopDialog(QDialog):
109
113
  self.header_icon.setFixedSize(self.icon_size)
110
114
  header_layout.addWidget(self.header_icon)
111
115
 
112
- self.header_label = QLabel(process_name)
116
+ header_text=process_name[:48]+f"{'...' if len(process_name)>50 else ''}"
117
+ self.header_label = QLabel(header_text)
113
118
  self.header_label.setFont(self.title_font)
114
119
  self.header_label.setAlignment(Qt.AlignmentFlag.AlignLeft|Qt.AlignmentFlag.AlignVCenter)
115
120
  self.header_label.setFixedHeight(self.title_height)
@@ -191,6 +196,82 @@ class FolderLoopDialog(QDialog):
191
196
 
192
197
  layout.addWidget(widget)
193
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
+
194
275
  # Progress bar and final buttons (Cancel, Proceed)
195
276
  progress_widget = QWidget()
196
277
  progress_layout = QHBoxLayout()
@@ -228,8 +309,13 @@ class FolderLoopDialog(QDialog):
228
309
  self.setLayout(layout)
229
310
 
230
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)
231
315
  self.setFixedHeight(self.max_height)
232
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)
233
319
 
234
320
  # Timer for progress
235
321
  self.iteration = 0
@@ -271,6 +357,51 @@ class FolderLoopDialog(QDialog):
271
357
  button.setIconSize(self.icon_size)
272
358
  button.setIcon(self.icons[self.options_list[k]][0]) # Checked state icon
273
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)
274
405
 
275
406
  def on_proceed(self):
276
407
  """Start or stop the progress loop depending on the button state."""
@@ -300,14 +431,17 @@ class FolderLoopDialog(QDialog):
300
431
  """Update the progress bar on each timer tick."""
301
432
  if self.iteration < self.progress_bar.maximum():
302
433
  timesleep(time_sleep_loop)
303
- self.title_label.setText(self.paths[self.iteration])
304
- self.func(self.iteration,self.step_options)
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)
305
437
  self.progress_bar.setValue(self.iteration)
306
438
  self.iteration += 1
307
439
  if self.loop_running: self.timer.start(0)
308
440
  else:
309
441
  self.progress_bar.setValue(self.progress_bar.maximum())
310
442
  self.stop_progress()
443
+ self.hide()
444
+ self.show_batch_issues_dialog()
311
445
  if not self.animation: self.done(0) # Closes the dialog when complete
312
446
 
313
447
  def cancel(self):
@@ -332,6 +466,62 @@ class FolderLoopDialog(QDialog):
332
466
 
333
467
  def window_resize(self, h):
334
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()
335
525
 
336
526
  if __name__ == "__main__":
337
527
  import random