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.
- PaIRS_UniNa/Calibration_Tab.py +15 -0
- PaIRS_UniNa/Changes.txt +27 -0
- PaIRS_UniNa/Explorer.py +90 -8
- PaIRS_UniNa/FolderLoop.py +196 -6
- PaIRS_UniNa/Input_Tab.py +162 -53
- PaIRS_UniNa/Input_Tab_CalVi.py +10 -12
- PaIRS_UniNa/Input_Tab_tools.py +3 -5
- PaIRS_UniNa/Output_Tab.py +1 -3
- PaIRS_UniNa/PaIRS_pypacks.py +45 -0
- PaIRS_UniNa/SPIVCalHelp.py +155 -0
- PaIRS_UniNa/Vis_Tab.py +39 -18
- PaIRS_UniNa/Whatsnew.py +4 -3
- PaIRS_UniNa/_PaIRS_PIV.pyd +0 -0
- PaIRS_UniNa/__init__.py +2 -2
- PaIRS_UniNa/gPaIRS.py +60 -2
- PaIRS_UniNa/icons/folder_loop_cleanup.png +0 -0
- PaIRS_UniNa/icons/folder_loop_cleanup_off.png +0 -0
- PaIRS_UniNa/icons/information.png +0 -0
- PaIRS_UniNa/icons/information2.png +0 -0
- PaIRS_UniNa/icons/scan_path_loop.png +0 -0
- PaIRS_UniNa/icons/scan_path_loop_off.png +0 -0
- PaIRS_UniNa/icons/spiv_setup_no.png +0 -0
- PaIRS_UniNa/icons/spiv_setup_ok.png +0 -0
- PaIRS_UniNa/procTools.py +46 -1
- PaIRS_UniNa/ui_Calibration_Tab.py +90 -57
- PaIRS_UniNa/whatsnew.txt +3 -2
- {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.9.dist-info}/METADATA +14 -8
- {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.9.dist-info}/RECORD +30 -22
- {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.9.dist-info}/WHEEL +0 -0
- {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.9.dist-info}/top_level.txt +0 -0
PaIRS_UniNa/Calibration_Tab.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
304
|
-
self.
|
|
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
|