PaIRS-UniNa 0.2.7__cp312-cp312-macosx_11_0_universal2.whl → 0.2.10__cp312-cp312-macosx_11_0_universal2.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 (38) hide show
  1. PaIRS_UniNa/Calibration_Tab.py +16 -0
  2. PaIRS_UniNa/Changes.txt +39 -0
  3. PaIRS_UniNa/Explorer.py +311 -75
  4. PaIRS_UniNa/FolderLoop.py +196 -6
  5. PaIRS_UniNa/Input_Tab.py +160 -53
  6. PaIRS_UniNa/Input_Tab_CalVi.py +11 -12
  7. PaIRS_UniNa/Input_Tab_tools.py +17 -15
  8. PaIRS_UniNa/Output_Tab.py +1 -3
  9. PaIRS_UniNa/PaIRS_pypacks.py +63 -65
  10. PaIRS_UniNa/Process_Tab.py +19 -15
  11. PaIRS_UniNa/Process_Tab_Disp.py +8 -1
  12. PaIRS_UniNa/SPIVCalHelp.py +155 -0
  13. PaIRS_UniNa/Saving_tools.py +2 -0
  14. PaIRS_UniNa/TabTools.py +165 -6
  15. PaIRS_UniNa/Vis_Tab.py +50 -22
  16. PaIRS_UniNa/Vis_Tab_CalVi.py +1 -2
  17. PaIRS_UniNa/Whatsnew.py +4 -3
  18. PaIRS_UniNa/_PaIRS_PIV.so +0 -0
  19. PaIRS_UniNa/__init__.py +3 -3
  20. PaIRS_UniNa/addwidgets_ps.py +570 -70
  21. PaIRS_UniNa/gPaIRS.py +118 -17
  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/information.png +0 -0
  25. PaIRS_UniNa/icons/information2.png +0 -0
  26. PaIRS_UniNa/icons/scan_path_loop.png +0 -0
  27. PaIRS_UniNa/icons/scan_path_loop_off.png +0 -0
  28. PaIRS_UniNa/icons/spiv_setup_no.png +0 -0
  29. PaIRS_UniNa/icons/spiv_setup_ok.png +0 -0
  30. PaIRS_UniNa/procTools.py +46 -1
  31. PaIRS_UniNa/rqrdpckgs.txt +7 -7
  32. PaIRS_UniNa/ui_Calibration_Tab.py +92 -59
  33. PaIRS_UniNa/ui_gPairs.py +8 -8
  34. PaIRS_UniNa/whatsnew.txt +2 -3
  35. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.10.dist-info}/METADATA +7 -8
  36. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.10.dist-info}/RECORD +38 -30
  37. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.10.dist-info}/WHEEL +0 -0
  38. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.10.dist-info}/top_level.txt +0 -0
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
PaIRS_UniNa/Input_Tab.py CHANGED
@@ -71,13 +71,14 @@ class INPpar(TABpar):
71
71
  self.inp_cam = 1
72
72
  self.ind_in = 0
73
73
  self.npairs = 0
74
- self.step = 0
74
+ self.step = 1
75
75
  self.FlagTR_Import = False
76
76
  self.FlagImport = True
77
77
 
78
78
  FlagAutoList = True
79
79
  FlagAutoFrame = True
80
80
  FlagExample = True
81
+ pathCompleter = basefold_DEBUGOptions
81
82
 
82
83
  def __init__(self,Process=ProcessTypes.null,Step=StepTypes.null):
83
84
  self.setup(Process,Step)
@@ -104,7 +105,7 @@ class INPpar(TABpar):
104
105
  self.inp_cam = 1
105
106
  self.ind_in = 0
106
107
  self.npairs = 0
107
- self.step = 0
108
+ self.step = 1
108
109
  self.FlagTR_Import = False
109
110
 
110
111
  self.nExImTree = 3
@@ -132,11 +133,10 @@ class INPpar(TABpar):
132
133
 
133
134
  #self.FlagDISP = Step==StepTypes.disp
134
135
  #self.dispFile = ''
135
-
136
- self.pathCompleter=basefold_DEBUGOptions
137
-
136
+
138
137
  class Input_Tab(gPaIRS_Tab):
139
138
  class Import_Tab_Signals(gPaIRS_Tab.Tab_Signals):
139
+ tooltipRequested = Signal(str)
140
140
  pass
141
141
 
142
142
  def __init__(self,parent: QWidget =None, flagInit= __name__ == "__main__"):
@@ -148,11 +148,33 @@ class Input_Tab(gPaIRS_Tab):
148
148
  #------------------------------------- Graphical interface: widgets
149
149
  self.TABname='Input'
150
150
  self.ui: Ui_InputTab
151
- self.exImTree=self.ui.exImTree=GlobalImageTree(self,FlagNum=True)
152
- self.ui.g_ImSet_layout.insertWidget(2,self.exImTree)
151
+ self.exImTree=self.ui.exImTree=GlobalImageTree(self,FlagNum=True)
153
152
  self.exImTree.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
153
+ self.exImTree.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
154
154
  self.exImTree.setSelectionBehavior(QTreeWidget.SelectionBehavior.SelectRows)
155
- self.exImTree.setHeaderHidden(True)
155
+ #self.exImTree.setHeaderHidden(True)
156
+ self.exImTree.header().setVisible(True)
157
+ self.hbarExImTree = QScrollBar(Qt.Horizontal)
158
+ self.hbarExImTree.setStyleSheet("""
159
+ QScrollBar::handle:horizontal {
160
+ min-width: 40px;
161
+ }
162
+ """)
163
+ self.exImTree.setVisible(True)
164
+ self.hbarExImTree.setVisible(True)
165
+ # Sync ranges and values
166
+ origHbar = self.exImTree.horizontalScrollBar()
167
+ origHbar.rangeChanged.connect(self.hbarExImTree.setRange)
168
+ origHbar.valueChanged.connect(self.hbarExImTree.setValue)
169
+ self.hbarExImTree.valueChanged.connect(origHbar.setValue)
170
+ # Layout
171
+ self.containerExImTree = QWidget()
172
+ layoutExImTree = QVBoxLayout(self.containerExImTree)
173
+ layoutExImTree.setContentsMargins(0,0,0,0)
174
+ layoutExImTree.setSpacing(0)
175
+ layoutExImTree.addWidget(self.exImTree)
176
+ layoutExImTree.addWidget(self.hbarExImTree)
177
+ self.ui.g_ImSet_layout.insertWidget(2,self.containerExImTree)
156
178
 
157
179
  #necessary to change the name and the order of the items
158
180
  for g in list(globals()):
@@ -170,6 +192,8 @@ class Input_Tab(gPaIRS_Tab):
170
192
  self.pixmap_wait = QPixmap(''+ icons_path +'sandglass.png')
171
193
  self.pixmap_warn = QPixmap(u""+ icons_path +"warning.png")
172
194
 
195
+ self.signals.tooltipRequested.connect(self.show_import_tooltip)
196
+
173
197
  #------------------------------------- Declaration of parameters
174
198
  self.INPpar_base=INPpar()
175
199
  self.INPpar:INPpar=self.TABpar
@@ -291,11 +315,39 @@ class Input_Tab(gPaIRS_Tab):
291
315
  def scanImList(self,ind=None):
292
316
  if ind: INP:INPpar=self.TABpar_at(ind)
293
317
  else: INP:INPpar=self.INPpar
318
+ FlagWarning=False
294
319
  for c in range(len(INP.imList)): #INP.ncam
295
320
  for f in range(2):
296
321
  for k in range(len(INP.imList[0][0])): #INP.nimg
297
322
  #ex=INP.imEx[c][f][k]
298
323
  INP.imEx[c][f][k]=os.path.exists(INP.path+INP.imList[c][f][k]) if INP.imList[c][f][k] else False
324
+ if not FlagWarning and not INP.imEx[c][f][k]: FlagWarning=True
325
+ return FlagWarning
326
+
327
+ def purgeImList(self, ind=None):
328
+ """Drop every k where any INP.imEx[c][f][k] is False; remove k from both imEx and imList across all c,f."""
329
+ INP = self.INPpar if ind is None else self.TABpar_at(ind)
330
+ if not hasattr(INP,"imEx") or not hasattr(INP,"imList"): return 0
331
+
332
+ # Collect all k to drop if False appears anywhere at that k
333
+ ks_to_drop=set()
334
+ for c in range(len(INP.imEx)):
335
+ for f in range(len(INP.imEx[c])):
336
+ row=INP.imEx[c][f]
337
+ for k, ex in enumerate(row):
338
+ if not ex: ks_to_drop.add(k)
339
+
340
+ if not ks_to_drop: return 0
341
+ ks_sorted=sorted(ks_to_drop, reverse=True)
342
+
343
+ # Delete k across all c,f for both imEx and imList (bounds-checked, ragged-safe)
344
+ for c in range(len(INP.imEx)):
345
+ for f in range(len(INP.imEx[c])):
346
+ for k in ks_sorted:
347
+ if k < len(INP.imEx[c][f]): del INP.imEx[c][f][k]
348
+ if c < len(INP.imList) and f < len(INP.imList[c]) and k < len(INP.imList[c][f]):
349
+ del INP.imList[c][f][k]
350
+ return len(ks_to_drop)
299
351
 
300
352
  #*************************************************** Layout
301
353
  def setINPlayout(self):
@@ -320,9 +372,9 @@ class Input_Tab(gPaIRS_Tab):
320
372
  self.ui.spin_inp_cam.setEnabled(self.INPpar.inp_ncam>1)
321
373
  self.ui.spin_inp_cam.setMaximum(self.INPpar.inp_ncam)
322
374
  self.setImageNumberSpinLimits()
323
- self.exImTree.setVisible(INPpar.FlagExample)
375
+ self.containerExImTree.setVisible(INPpar.FlagExample)
324
376
  self.layoutExampleImageList()
325
- self.ui.button_import.setEnabled(not self.INPpar.FlagImport)
377
+ #self.ui.button_import.setEnabled(not self.INPpar.FlagImport)
326
378
 
327
379
  """
328
380
  if self.INPpar.ind[-1]<len(self.TABpar_prev_at(self.INPpar.ind))-1:
@@ -397,13 +449,13 @@ class Input_Tab(gPaIRS_Tab):
397
449
  def line_edit_path_preaction(self):
398
450
  currpath=myStandardPath(self.ui.line_edit_path.text())
399
451
  self.FlagScanPath=os.path.normpath(self.INPpar.path)!=currpath
400
- directory_path = myStandardPath(os.getcwd())
401
- if directory_path in currpath:
402
- currpath=currpath.replace(directory_path,'./')
403
- if os.path.exists(currpath):
404
- if currpath in self.INPpar.pathCompleter: self.INPpar.pathCompleter.remove(currpath)
405
- self.INPpar.pathCompleter.insert(0,currpath)
406
- if len(self.INPpar.pathCompleter)>10: self.INPpar.pathCompleter=self.INPpar.pathCompleter[:10]
452
+ currpath=relativizePath(currpath)
453
+ if os.path.exists(currpath) and currpath!='./':
454
+ pathCompleter=INPpar.pathCompleter #self.INPpar.pathCompleter
455
+ if currpath in pathCompleter: pathCompleter.remove(currpath)
456
+ pathCompleter.insert(0,currpath)
457
+ if len(pathCompleter)>pathCompleterLength:
458
+ INPpar.pathCompleter=pathCompleter[:pathCompleterLength]
407
459
  self.ui.line_edit_path.setText(currpath)
408
460
 
409
461
  def line_edit_path_action_future(self):
@@ -426,15 +478,7 @@ class Input_Tab(gPaIRS_Tab):
426
478
  self.line_edit_path_action_future()
427
479
 
428
480
  def button_scan_path_action(self):
429
- self.INPpar.imSet.scanPath(self.INPpar.path)
430
- if self.INPpar.imSet.count:
431
- self.INPpar.frame_1[0]=0
432
- self.INPpar.frame_1,self.INPpar.frame_2=self.automaticFrames()
433
- else:
434
- self.INPpar.frame_1=[-1]*self.INPpar.inp_ncam
435
- self.INPpar.frame_2=[-1]*self.INPpar.inp_ncam
436
- self.INPpar.ind_in, _, self.INPpar.npairs, _= self.getIndNpairs()
437
- self.INPpar.step=1 if self.INPpar.npairs else 0
481
+ self.scanInputPath()
438
482
  if INPpar.FlagAutoList:
439
483
  self.button_import_action()
440
484
  else:
@@ -442,20 +486,65 @@ class Input_Tab(gPaIRS_Tab):
442
486
  self.ui.imTreeWidget.imTree.scanLists()
443
487
  self.INPpar.imEx=self.ui.imTreeWidget.imTree.imEx
444
488
 
445
- def automaticFrames(self):
446
- cam=self.INPpar.inp_cam-1
447
- if not self.INPpar.imSet.nimg:
448
- return self.INPpar.frame_1,self.INPpar.frame_2
449
- f1=self.INPpar.frame_1[cam]
450
- link1=self.INPpar.imSet.link[f1]
489
+ def scanInputPath(self,ind=None,patterns=None,FlagNoWarning=True):
490
+ INP = self.INPpar if ind is None else self.TABpar_at(ind)
491
+ INP.imSet.scanPath(INP.path)
492
+ ncam = INP.inp_ncam
493
+
494
+ FlagAutomaticFrames=True
495
+ if patterns and len(patterns)==2 and INP.imSet.count:
496
+ A,B = patterns # lists of patterns (len <= ncam)
497
+ INP.frame_1 = [-1]*ncam
498
+ INP.frame_2 = [-1]*ncam
499
+
500
+ # Build first-occurrence lookup: pattern -> index in slave's INP.imSet.pattern
501
+ pat_list = getattr(INP.imSet, "pattern", [])
502
+ idx_map = {}
503
+ for idx,p in enumerate(pat_list):
504
+ if p not in idx_map: idx_map[p]=idx
505
+
506
+ # Assign frames by matching patterns; leave -1 if not found
507
+ def _assign(dst, src, ind0=0):
508
+ for k in range(ncam):
509
+ key = src[k] if k < len(src) else None
510
+ dst[k] = idx_map.get(key, -1)
511
+ if ind0 and key is not None: dst[k]+=ind0
512
+
513
+ _assign(INP.frame_1, A)
514
+ _assign(INP.frame_2, B, 1)
515
+
516
+ valid=any(f>-1 for f in INP.frame_1) or any(f>0 for f in INP.frame_2)
517
+ if not valid and FlagNoWarning:
518
+ FlagAutomaticFrames = True
519
+ else:
520
+ FlagAutomaticFrames = False
521
+
522
+ if FlagAutomaticFrames:
523
+ if INP.imSet.count:
524
+ INP.frame_1[0]=0
525
+ INP.frame_1,INP.frame_2=self.automaticFrames(ind)
526
+ else:
527
+ INP.frame_1=[-1]*INP.inp_ncam
528
+ INP.frame_2=[-1]*INP.inp_ncam
529
+ # Update derived indices and safety on step
530
+ INP.ind_in, _, INP.npairs, _= self.getIndNpairs(ind)
531
+ if INP.npairs<1: INP.step=1
532
+
533
+ def automaticFrames(self,ind=None):
534
+ INP = self.INPpar if ind is None else self.TABpar_at(ind)
535
+ cam=INP.inp_cam-1
536
+ if not INP.imSet.nimg:
537
+ return INP.frame_1,INP.frame_2
538
+ f1=INP.frame_1[cam]
539
+ link1=INP.imSet.link[f1]
451
540
  nlink1=len(link1)
452
- frame_1=[f1]*self.INPpar.inp_ncam
453
- frame_2=[f1+1]*self.INPpar.inp_ncam
541
+ frame_1=[f1]*INP.inp_ncam
542
+ frame_2=[f1+1]*INP.inp_ncam
454
543
  frame_2[0]=link1[0]+1 if link1[0]!=f1 else 0
455
- for c in range(1,self.INPpar.inp_ncam):
544
+ for c in range(1,INP.inp_ncam):
456
545
  flagValid=c<nlink1-1
457
546
  frame_1[c]=f1c=link1[c] if flagValid else -1
458
- frame_2[c]=self.INPpar.imSet.link[f1c][0]+1 if flagValid else -1
547
+ frame_2[c]=INP.imSet.link[f1c][0]+1 if flagValid else -1
459
548
  if cam:
460
549
  frame_1=frame_1[-cam:]+frame_1[:-cam]
461
550
  frame_2=frame_2[-cam:]+frame_2[:-cam]
@@ -511,7 +600,7 @@ class Input_Tab(gPaIRS_Tab):
511
600
  INPpar.FlagAutoFrame=self.ui.button_automatic_frame.isChecked()
512
601
  return True
513
602
 
514
- def combo_frame_a_action(self):
603
+ def combo_frame_a_action(self):
515
604
  self.INPpar.frame_1[self.INPpar.inp_cam-1]=self.ui.combo_frame_a.currentIndex()
516
605
  if INPpar.FlagAutoFrame:
517
606
  self.INPpar.frame_1,self.INPpar.frame_2=self.automaticFrames()
@@ -522,7 +611,8 @@ class Input_Tab(gPaIRS_Tab):
522
611
 
523
612
  def button_example_list_action(self):
524
613
  INPpar.FlagExample=self.ui.button_example_list.isChecked()
525
- self.exImTree.setVisible(INPpar.FlagExample)
614
+ self.layoutExampleImageList()
615
+ self.containerExImTree.setVisible(INPpar.FlagExample)
526
616
  return True
527
617
 
528
618
  def spin_inp_cam_action(self):
@@ -538,16 +628,24 @@ class Input_Tab(gPaIRS_Tab):
538
628
  item.setSelected(True)
539
629
 
540
630
  def button_import_action(self):
541
- INPpar.FlagExample=False
542
631
  self.INPpar.imList,self.INPpar.imEx=self.INPpar.imSet.genListsFromFrame(self.INPpar.frame_1,self.INPpar.frame_2,self.INPpar.ind_in,self.INPpar.npairs,self.INPpar.step,self.INPpar.FlagTR_Import)
543
- self.INPpar.selection=[1,1,1]
544
- self.InputAdjustSelection()
545
632
  if self.INPpar.isDifferentFrom(self.INPpar_old,fields=['imList','imEx']):
633
+ INPpar.FlagExample=False
634
+ self.INPpar.selection=[1,1,1]
635
+ self.InputAdjustSelection()
546
636
  self.INPpar.FlagImport=True
547
637
  self.INPpar.importPar.copyfrom(self.INPpar,exceptions=['name','surname'])
548
- self.ui.CollapBox_ImSet.toggle_button.click()
549
- self.ui.imTreeWidget.setFocus()
638
+ self.ui.CollapBox_ImSet.toggle_button.click()
639
+ self.ui.imTreeWidget.setFocus()
640
+ else:
641
+ # --- Tooltip warning ---
642
+ msg = "No changes to be applied to the image list!"
643
+ self.signals.tooltipRequested.emit(msg) # thread-safe
550
644
 
645
+ @Slot(str)
646
+ def show_import_tooltip(self, msg: str):
647
+ show_mouse_tooltip(self,msg)
648
+
551
649
  #******************** Settings
552
650
  """
553
651
  def button_box_set(self):
@@ -577,7 +675,7 @@ class Input_Tab(gPaIRS_Tab):
577
675
  ind_in, ind_fin, npairs, npairs_max= self.getIndNpairs()
578
676
  self.INPpar.ind_in=min([max([ind_in,self.INPpar.ind_in]),ind_fin])
579
677
  self.INPpar.npairs=npairs if self.INPpar.isDifferentFrom(self.INPpar_old,fields=['FlagTR_Import']) else min([self.INPpar.npairs,npairs])
580
- self.INPpar.step=min([self.INPpar.step,npairs])
678
+ self.INPpar.step=max([min([self.INPpar.step,npairs]),1])
581
679
 
582
680
  self.ui.spin_ind_in.setMinimum(ind_in)
583
681
  self.ui.spin_ind_in.setMaximum(ind_fin)
@@ -596,27 +694,28 @@ class Input_Tab(gPaIRS_Tab):
596
694
  #ind_in=self.INPpar.imSet.ind_in[self.INPpar.frame_a]
597
695
  pass
598
696
 
599
- def getIndNpairs(self):
600
- if self.INPpar.imSet.count:
601
- l=[self.INPpar.imSet.ind_in[f] for f in self.INPpar.frame_1 if f>-1]
697
+ def getIndNpairs(self,ind=None):
698
+ INP = self.INPpar if ind is None else self.TABpar_at(ind)
699
+ if INP.imSet.count:
700
+ l=[INP.imSet.ind_in[f] for f in INP.frame_1 if f>-1]
602
701
  ind_in_1=min(l) if l else None
603
- l=[self.INPpar.imSet.ind_in[f-1] for f in self.INPpar.frame_2 if f>0]
702
+ l=[INP.imSet.ind_in[f-1] for f in INP.frame_2 if f>0]
604
703
  ind_in_2=min(l) if l else ind_in_1 if ind_in_1 is not None else None
605
704
  ind_in=min([ind_in_1,ind_in_2]) if ind_in_2 is not None else 0
606
705
 
607
- l=[self.INPpar.imSet.ind_fin[f] for f in self.INPpar.frame_1 if f>-1]
706
+ l=[INP.imSet.ind_fin[f] for f in INP.frame_1 if f>-1]
608
707
  ind_fin_1=max(l) if l else None
609
- l=[self.INPpar.imSet.ind_fin[f-1] for f in self.INPpar.frame_2 if f>0]
708
+ l=[INP.imSet.ind_fin[f-1] for f in INP.frame_2 if f>0]
610
709
  ind_fin_2=max(l) if l else ind_fin_1 if ind_fin_1 is not None else None
611
710
  ind_fin=min([ind_fin_1,ind_fin_2]) if ind_fin_2 is not None else -1
612
711
  else:
613
712
  ind_in=ind_in_2=0
614
713
  ind_fin=-1
615
714
  npairs=nimg=ind_fin-ind_in+1
616
- if not all(self.INPpar.frame_2):
715
+ if not all(INP.frame_2):
617
716
  npairs=int(npairs/2)
618
- if self.INPpar.FlagTR_Import:
619
- npairs=2*npairs-1+nimg%2 #(1+int(any(self.INPpar.frame_2)))*npairs-1
717
+ if INP.FlagTR_Import:
718
+ npairs=2*npairs-1+nimg%2 #(1+int(any(INP.frame_2)))*npairs-1
620
719
  if npairs==0: ind_in=ind_fin=0
621
720
  npairs_max=npairs
622
721
  return ind_in, ind_fin, npairs, npairs_max
@@ -655,6 +754,14 @@ class Input_Tab(gPaIRS_Tab):
655
754
  self.exImTree.setMaximumHeight(height)
656
755
  self.exImTree.verticalScrollBar().setMinimumHeight(height)
657
756
  self.exImTree.verticalScrollBar().setMaximumHeight(height)
757
+ if self.hbarExImTree.maximum()==self.hbarExImTree.minimum():
758
+ self.hbarExImTree.setVisible(False)
759
+ containerHeight=height
760
+ else:
761
+ self.hbarExImTree.setVisible(True)
762
+ containerHeight=height+self.hbarExImTree.height()
763
+ self.containerExImTree.setMinimumHeight(containerHeight)
764
+ self.containerExImTree.setMaximumHeight(containerHeight)
658
765
  self.ui.CollapBox_ImSet.heightArea=height+110 if INPpar.FlagExample else 110
659
766
  self.ui.CollapBox_ImSet.heightOpened=self.ui.CollapBox_ImSet.heightArea+20
660
767
  self.ui.CollapBox_ImSet.on_click()