PaIRS-UniNa 0.2.5__cp313-cp313-win_amd64.whl → 0.2.9__cp313-cp313-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 (42) hide show
  1. PaIRS_UniNa/Calibration_Tab.py +15 -0
  2. PaIRS_UniNa/Changes.txt +54 -2
  3. PaIRS_UniNa/Explorer.py +118 -19
  4. PaIRS_UniNa/FolderLoop.py +196 -6
  5. PaIRS_UniNa/Input_Tab.py +167 -55
  6. PaIRS_UniNa/Input_Tab_CalVi.py +15 -17
  7. PaIRS_UniNa/Input_Tab_tools.py +9 -10
  8. PaIRS_UniNa/Output_Tab.py +2 -4
  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/SPIVCalHelp.py +155 -0
  13. PaIRS_UniNa/Saving_tools.py +7 -7
  14. PaIRS_UniNa/TabTools.py +7 -4
  15. PaIRS_UniNa/Vis_Tab.py +129 -60
  16. PaIRS_UniNa/Whatsnew.py +15 -3
  17. PaIRS_UniNa/_PaIRS_PIV.pyd +0 -0
  18. PaIRS_UniNa/__init__.py +4 -4
  19. PaIRS_UniNa/addwidgets_ps.py +28 -20
  20. PaIRS_UniNa/calibView.py +7 -0
  21. PaIRS_UniNa/gPaIRS.py +179 -34
  22. PaIRS_UniNa/icons/flaticon_PaIRS_download_warning.png +0 -0
  23. PaIRS_UniNa/icons/folder_loop_cleanup.png +0 -0
  24. PaIRS_UniNa/icons/folder_loop_cleanup_off.png +0 -0
  25. PaIRS_UniNa/icons/information.png +0 -0
  26. PaIRS_UniNa/icons/information2.png +0 -0
  27. PaIRS_UniNa/icons/pencil_bw.png +0 -0
  28. PaIRS_UniNa/icons/scan_path_loop.png +0 -0
  29. PaIRS_UniNa/icons/scan_path_loop_off.png +0 -0
  30. PaIRS_UniNa/icons/spiv_setup_no.png +0 -0
  31. PaIRS_UniNa/icons/spiv_setup_ok.png +0 -0
  32. PaIRS_UniNa/pivParFor.py +1 -1
  33. PaIRS_UniNa/procTools.py +51 -3
  34. PaIRS_UniNa/rqrdpckgs.txt +6 -5
  35. PaIRS_UniNa/stereoPivParFor.py +1 -1
  36. PaIRS_UniNa/ui_Calibration_Tab.py +90 -57
  37. PaIRS_UniNa/ui_gPairs.py +9 -3
  38. PaIRS_UniNa/whatsnew.txt +4 -4
  39. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/METADATA +32 -17
  40. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/RECORD +42 -32
  41. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/WHEEL +0 -0
  42. {pairs_unina-0.2.5.dist-info → pairs_unina-0.2.9.dist-info}/top_level.txt +0 -0
@@ -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,4 +1,56 @@
1
- ********* Changes in version 0.2.5 (2025.07.18) **********
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
+
28
+ ********* Changes in version 0.2.7 (2025.10.13) **********
29
+ Bug fixes:
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);
31
+ - corrected the update of reprojection errors in CalVi when the control point grid is expanded after a calibration. Each new control point now has its error recalculated and plotted, providing a correct view of the extrapolation error;
32
+ - fixed the automatic resize/reshape behavior specified in the Output tab: when an image transformation is assigned, it is now preserved even if the automatic reshape button is active.
33
+
34
+ User-interface enhancements:
35
+ - changed the behavior of the Vis button for restricting view on the interrogation window area: clicking it now opens a popup menu that allows users to select the desired interrogation-window size to focus on among all iterations of the process;
36
+ - names of processes duplicated in the process tree are now automatically updated to avoid conflicts: if a name already exists, the new one is appended with an incremental suffix;
37
+ - improved interaction with editable input fields: when hovering the highlighted style is displayed correctly, the text cursor now appears exactly where clicked and the background color is properly restored when leaving the field.
38
+
39
+
40
+
41
+ ********* Changes in version 0.2.6 (2025.09.06) **********
42
+ Bug fixes:
43
+ - fixed bugs related to process tree management.
44
+
45
+ User-interface enhancements:
46
+ - enhanced release check using SSL for reliable retrieval of the latest version.
47
+
48
+ Distribution:
49
+ - ready-to-use executables of PaIRS are now available!
50
+
51
+
52
+
53
+ ********* Changes in version 0.2.5 (2025.07.28) **********
2
54
  Bug fixes:
3
55
  - fixed bugs in launching PIV and stereoscopic PIV processes in special cases;
4
56
  - fixed tooltip crash introduced by PySide 6.9;
@@ -17,7 +69,7 @@ User-interface enhancements:
17
69
 
18
70
  Distribution:
19
71
  - Support for Python 3.9 has been discontinued;
20
- - Python 3.13 builds have been successfully created and tested.
72
+ - Python 3.13 builds have been successfully created and tested in Windows.
21
73
 
22
74
 
23
75
 
PaIRS_UniNa/Explorer.py CHANGED
@@ -1318,7 +1318,7 @@ class ProcessTree(PaIRSTree):
1318
1318
  par.ind[1]=int(self.FlagBin)
1319
1319
  par.ind[2]=i
1320
1320
  except Exception as inst:
1321
- pri.Error.red(inst)
1321
+ pri.Error.red(f"{inst}\n{traceback.format_exc()}")
1322
1322
  pass
1323
1323
  if hasattr(self.gui,'setLinks'):
1324
1324
  inds_new_list=list(inds_new)
@@ -1383,7 +1383,7 @@ class ProcessTree(PaIRSTree):
1383
1383
  if self.FlagExternalDrag:
1384
1384
  self.FlagExternalDrag=False
1385
1385
  self.dragged_items=None
1386
- self.setStyleSheet("")
1386
+ self.setStyleSheet(self.initialStyleSheet)
1387
1387
  if self.parent(): self.setPalette(self.parent().palette())
1388
1388
  self.hovered_item=None
1389
1389
 
@@ -1424,7 +1424,7 @@ class ProcessTree(PaIRSTree):
1424
1424
  if not self.widgets: return
1425
1425
  for w in self.widgets:
1426
1426
  w:gPaIRS_Tab
1427
- w.gen_TABpar(ITE.ind,FlagEmptyPrev=True,FlagInsert=3,Process=ITE.Process,Step=ITE.Step)
1427
+ w.gen_TABpar(ITE.ind,FlagEmptyPrev=True,FlagInsert=2,Process=ITE.Process,Step=ITE.Step)
1428
1428
  pass
1429
1429
  return
1430
1430
 
@@ -1434,11 +1434,11 @@ class ProcessTree(PaIRSTree):
1434
1434
  if t in self.widgetNames:
1435
1435
  k=self.widgetNames.index(t)
1436
1436
  w:gPaIRS_Tab=self.widgets[k]
1437
- w.gen_TABpar(ITE.ind,FlagInsert=4,Process=ITE.Process,Step=ITE.Step)
1437
+ w.gen_TABpar(ITE.ind,FlagInsert=3,Process=ITE.Process,Step=ITE.Step)
1438
1438
  pass
1439
1439
  for w, wn in zip(self.widgets,self.widgetNames):
1440
1440
  if wn not in ITE.tabs+['TabArea']:
1441
- w.gen_TABpar(ITE.ind,FlagNone=True,FlagInsert=4,Process=ITE.Process,Step=ITE.Step)
1441
+ w.gen_TABpar(ITE.ind,FlagNone=True,FlagInsert=3,Process=ITE.Process,Step=ITE.Step)
1442
1442
  pass
1443
1443
  return
1444
1444
 
@@ -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:
@@ -1697,10 +1701,14 @@ class ProcessTree(PaIRSTree):
1697
1701
  if errorString:
1698
1702
  if '_data' in filename:
1699
1703
  try:
1704
+ errorString=''
1700
1705
  basename,ext=os.path.splitext(filename)
1701
1706
  filename2=basename[:-5]+ext
1702
1707
  data, errorMessage=loadList(filename2)
1703
- errorString=errorString+errorMessage if errorMessage else ''
1708
+ #errorString=errorString+errorMessage if errorMessage else ''
1709
+ if errorMessage:
1710
+ WarningMessage="It was not possible to determine which process the selected data file belongs to.\nPlease, try again by loading the process output file directly."
1711
+ warningDialog(self,WarningMessage)
1704
1712
  except Exception as inst2:
1705
1713
  errorString+=str(inst2)
1706
1714
  FlagError=True
@@ -1855,11 +1863,20 @@ class ProcessTree(PaIRSTree):
1855
1863
  if not self.FlagCutted and not FlagPasteDeleted:
1856
1864
  self.cutItems(new_items)
1857
1865
  self.pasteLists(row,FlagPasteDeleted)
1858
- for k in indexes:
1859
- item=self.topLevelItem(k)
1860
- ITEs=self.itemList[0][k]
1861
- self.setProcessItemWidget(item,ITEs[0]) # Set the widget container as the
1862
- self.setStepItemWidgets(item,ITEs[0].ind[2])
1866
+ name_list=[i[0].name for i in self.itemList[0]]
1867
+ for pos,k in enumerate(indexes):
1868
+ item=self.topLevelItem(k)
1869
+ ITE=self.itemList[0][k][0]
1870
+ tail=set(indexes[pos:]);
1871
+ forbidden={name_list[j] for j in range(len(name_list)) if j not in tail}
1872
+ if ITE.name in forbidden:
1873
+ base=ITE.name
1874
+ n=1
1875
+ while f"{base} ({n})" in forbidden: n+=1
1876
+ ITE.name=f"{base} ({n})"
1877
+ name_list[k]=ITE.name
1878
+ self.setProcessItemWidget(item,ITE)
1879
+ self.setStepItemWidgets(item,ITE.ind[2])
1863
1880
 
1864
1881
  self.scrollToItem(firstItemToScroll)
1865
1882
  self.scrollToItem(lastItemToScroll)
@@ -1904,12 +1921,12 @@ class ProcessTree(PaIRSTree):
1904
1921
  pixmap_list.append(icons_path+ITE.icon)
1905
1922
  name_list.append(ITE.name)
1906
1923
  flag_list.append(ITE.Step!=StepTypes.cal)
1907
- 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)
1908
1925
  dialog = FolderLoopDialog(pixmap_list, name_list, flag_list, parent=self, paths=paths, func=func, process_name=ITEs[0].name)
1909
1926
  dialog.exec()
1910
1927
  return
1911
1928
 
1912
- 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):
1913
1930
  nProcess=self.topLevelItemCount()
1914
1931
  path=paths[i]
1915
1932
  name=name0+f" (.../{os.path.basename(path)}/)"
@@ -1920,6 +1937,7 @@ class ProcessTree(PaIRSTree):
1920
1937
  ind_slave[2]=nProcess
1921
1938
  ind_slave[-1]=0
1922
1939
 
1940
+ FlagWarning=False
1923
1941
  for j in range(len(opts)):
1924
1942
  ind_master[3]=j
1925
1943
  ind_slave[3]=j
@@ -1931,13 +1949,23 @@ class ProcessTree(PaIRSTree):
1931
1949
  if opts[j]==2:
1932
1950
  INP: INPpar=self.gui.w_Input.TABpar_at(ind_new)
1933
1951
  INP.path=myStandardPath(path)
1934
- 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])
1935
1961
  self.gui.w_Input.checkINPpar(ind_new)
1936
1962
  self.gui.w_Input.setINPwarn(ind_new)
1937
- self.Explorer.setITElayout(ITE)
1938
1963
 
1939
1964
  TABname=self.gui.w_Input.TABname
1940
1965
  self.gui.bridge(TABname,ind_new)
1966
+
1967
+ FlagSettingPar=TABpar.FlagSettingPar
1968
+ TABpar.FlagSettingPar=True
1941
1969
  for w in self.gui.tabWidgets:
1942
1970
  w:gPaIRS_Tab
1943
1971
  if w!=self.gui.w_Input:
@@ -1949,12 +1977,55 @@ class ProcessTree(PaIRSTree):
1949
1977
  w.adjustTABparInd()
1950
1978
  w.TABpar.copyfrom(currpar)
1951
1979
  self.gui.bridge(w.TABname,ind_new)
1980
+ TABpar.FlagSettingPar=FlagSettingPar
1981
+ self.Explorer.setITElayout(ITE)
1982
+ FlagWarning=FlagWarning or flagWarning or ITE.OptionDone==0
1983
+
1952
1984
  #item_child=item.child(j)
1953
1985
  #item_child.setSelected(True)
1954
1986
  #self.setCurrentItem(item_child)
1955
1987
  else:
1956
1988
  self.gui.link_pars(ind_slave,ind_master,FlagSet=False)
1957
- 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
+
1958
2029
 
1959
2030
  def button_delete_action(self):
1960
2031
  self.blockSignals(True)
@@ -2940,9 +3011,16 @@ class StartingPage(QFrame):
2940
3011
  for n, process in processes.items():
2941
3012
  # Layout orizzontale per ogni processo
2942
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
+ """)
2943
3021
  process_layout = QHBoxLayout(widget)
2944
3022
  process_layout.setSpacing(self.LAYOUT_SPACING)
2945
- process_layout.setContentsMargins(0, 0, 0, 0)
3023
+ process_layout.setContentsMargins(10, 10, 10, 0)
2946
3024
 
2947
3025
  # Pulsante con icona
2948
3026
  button_layout = QHBoxLayout()
@@ -2990,13 +3068,15 @@ class StartingPage(QFrame):
2990
3068
  caption_text_edit.setFont(caption_font)
2991
3069
  caption_text_edit.setAlignment(Qt.AlignmentFlag.AlignJustify)
2992
3070
  caption_text_edit.setReadOnly(True)
3071
+ caption_text_edit.setTextInteractionFlags(Qt.NoTextInteraction)
3072
+ caption_text_edit.viewport().setCursor(Qt.PointingHandCursor)
2993
3073
  caption_text_edit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
2994
3074
  caption_text_edit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
2995
3075
  caption_text_edit.setFrameStyle(QFrame.NoFrame)
2996
3076
  #caption_text_edit.setFixedWidth(self.CAPTION_WIDTH)
2997
3077
  caption_text_edit.setStyleSheet("background: transparent;")
2998
3078
  caption_text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
2999
-
3079
+
3000
3080
  # Adjust height to content
3001
3081
  caption_text_edit.document().setTextWidth(caption_text_edit.viewport().width())
3002
3082
  caption_text_edit.setFixedHeight(CAPTION_HEIGHT)#caption_text_edit.document().size().height())
@@ -3012,6 +3092,25 @@ class StartingPage(QFrame):
3012
3092
  # Aggiungi il layout orizzontale al layout principale
3013
3093
  self.main_layout.addWidget(widget)
3014
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
+
3015
3114
  self.items[n]=widget
3016
3115
 
3017
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