PaIRS-UniNa 0.2.7__cp311-cp311-win_amd64.whl → 0.2.11__cp311-cp311-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 (50) hide show
  1. PaIRS_UniNa/Calibration_Tab.py +40 -24
  2. PaIRS_UniNa/Changes.txt +50 -0
  3. PaIRS_UniNa/Explorer.py +257 -77
  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 +30 -28
  8. PaIRS_UniNa/Output_Tab.py +1 -3
  9. PaIRS_UniNa/PaIRS_pypacks.py +171 -67
  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 +3 -0
  14. PaIRS_UniNa/TabTools.py +201 -9
  15. PaIRS_UniNa/Vis_Tab.py +221 -65
  16. PaIRS_UniNa/Vis_Tab_CalVi.py +139 -12
  17. PaIRS_UniNa/Whatsnew.py +4 -3
  18. PaIRS_UniNa/_PaIRS_PIV.pyd +0 -0
  19. PaIRS_UniNa/__init__.py +3 -3
  20. PaIRS_UniNa/addwidgets_ps.py +773 -97
  21. PaIRS_UniNa/calibView.py +5 -2
  22. PaIRS_UniNa/gPaIRS.py +307 -48
  23. PaIRS_UniNa/icons/closeAllFloat.png +0 -0
  24. PaIRS_UniNa/icons/defaultWinSize.png +0 -0
  25. PaIRS_UniNa/icons/dockVis.png +0 -0
  26. PaIRS_UniNa/icons/dockVis_disable.png +0 -0
  27. PaIRS_UniNa/icons/floatingVisSize.png +0 -0
  28. PaIRS_UniNa/icons/folder_loop_cleanup.png +0 -0
  29. PaIRS_UniNa/icons/folder_loop_cleanup_off.png +0 -0
  30. PaIRS_UniNa/icons/fullWinsize.png +0 -0
  31. PaIRS_UniNa/icons/icon_PaIRS.ico +0 -0
  32. PaIRS_UniNa/icons/information.png +0 -0
  33. PaIRS_UniNa/icons/information2.png +0 -0
  34. PaIRS_UniNa/icons/scan_path_loop.png +0 -0
  35. PaIRS_UniNa/icons/scan_path_loop_off.png +0 -0
  36. PaIRS_UniNa/icons/smallWinSize.png +0 -0
  37. PaIRS_UniNa/icons/spiv_setup_no.png +0 -0
  38. PaIRS_UniNa/icons/spiv_setup_ok.png +0 -0
  39. PaIRS_UniNa/icons/undockVis.png +0 -0
  40. PaIRS_UniNa/procTools.py +46 -1
  41. PaIRS_UniNa/rqrdpckgs.txt +7 -7
  42. PaIRS_UniNa/tabSplitter.py +6 -1
  43. PaIRS_UniNa/ui_Calibration_Tab.py +92 -59
  44. PaIRS_UniNa/ui_gPairs.py +92 -50
  45. PaIRS_UniNa/ui_infoPaIRS.py +8 -8
  46. PaIRS_UniNa/whatsnew.txt +2 -3
  47. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.11.dist-info}/METADATA +6 -8
  48. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.11.dist-info}/RECORD +50 -33
  49. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.11.dist-info}/WHEEL +1 -1
  50. {pairs_unina-0.2.7.dist-info → pairs_unina-0.2.11.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+ import types
1
3
  from .PaIRS_pypacks import *
2
4
  #from ui_Tree_Tab import Ui_TreeTab
3
5
 
@@ -9,9 +11,10 @@ InitCheck=True #False=Collap closed, True=opened
9
11
  font_italic=True
10
12
  font_weight=QFont.DemiBold
11
13
  backgroundcolor_none=" background-color: none;"
12
- backgroundcolor_changing=" background-color: rgb(255,230,230);"
13
- color_changing="color: rgb(33,33,255); "+backgroundcolor_changing
14
- color_changing_black="color: rgb(0,0,0); "+backgroundcolor_changing
14
+ backgroundcolor_changing=f" background-color: {PaIRS_pink};"
15
+ backgroundcolor_hover=f" background-color: {PaIRS_ghostblue};"
16
+ border_hover = " " #"border: 1px solid gray; "
17
+ color_changing=f"color: {PaIRS_darkblue}; "+backgroundcolor_changing
15
18
 
16
19
  #********************************************* Operating Widgets
17
20
  def setSS(b,style):
@@ -53,6 +56,9 @@ class MyQLineEdit(QtWidgets.QLineEdit):
53
56
  def setup(self):
54
57
  if not self.initFlag:
55
58
  self.initFlag=True
59
+ self.FlagFocusIn=False
60
+ self.oldFont=None
61
+ self.oldStyle=None
56
62
  font_changing = QtGui.QFont(self.font())
57
63
  font_changing.setItalic(font_italic)
58
64
  font_changing.setWeight(font_weight)
@@ -63,21 +69,17 @@ class MyQLineEdit(QtWidgets.QLineEdit):
63
69
  if hasattr(b,'setStyleSheet'):
64
70
  if hasStyleFlag:
65
71
  if b.styleFlag: continue
66
- b.flagS=True
67
72
  b.initialStyle=b.styleSheet()+" "+backgroundcolor_none
68
73
  b.setEnabled(False)
69
74
  b.disabledStyle=b.styleSheet()
70
75
  b.setEnabled(True)
71
- b.setStyleSheet(setSS(b,b.initialStyle))
72
- else:
73
- b.flagS=False
76
+ b.setStyleSheet(setSS(b,b.initialStyle)+"\n"+gPaIRS_QMenu_style)
74
77
  if hasattr(b,'setFont'):
75
- b.flagF=True
76
78
  b.initialFont=b.font()
77
79
  b.font_changing=font_changing
78
- else:
79
- b.flagF=False
80
80
  if hasStyleFlag: b.styleFlag=True
81
+ self.CElabs=[w for w in self.bros if isinstance(w, ClickableEditLabel)]
82
+ self.CElabs_styles=[w.styleSheet() for w in self.CElabs]
81
83
 
82
84
  def setup2(self):
83
85
  if not self.initFlag2:
@@ -99,57 +101,106 @@ class MyQLineEdit(QtWidgets.QLineEdit):
99
101
  self.setCompleterList()
100
102
 
101
103
  def enterEvent(self, event):
102
- super().enterEvent(event)
103
- if not self.font()==self.font_changing and not self.hasFocus():
104
+ if not self.hasFocus() and self.isEnabled():
105
+ self.oldFont=self.font()
106
+ self.oldStyle=self.styleSheet()
104
107
  self.setFont(self.font_changing)
105
-
108
+ self.setStyleSheet(setSS(self,self.initialStyle+" "+backgroundcolor_hover)+"\n"+gPaIRS_QMenu_style)
109
+ for k,w in enumerate(self.CElabs):
110
+ self.CElabs_styles[k]=w.styleSheet()
111
+ w.setStyleSheet(w.styleSheet()+" "+f"""
112
+ ClickableEditLabel {{
113
+ {backgroundcolor_hover};
114
+ }}
115
+ """)
116
+ w.repaint()
117
+ super().enterEvent(event)
118
+
106
119
  def leaveEvent(self, event):
120
+ if not self.hasFocus() and self.oldFont is not None:
121
+ self.setFont(self.oldFont)
122
+ self.setStyleSheet(self.oldStyle)
123
+ for k,w in enumerate(self.CElabs):
124
+ w.setStyleSheet(self.CElabs_styles[k])
125
+ w.repaint()
126
+ self.oldFont=None
127
+ self.oldStyle=None
107
128
  super().leaveEvent(event)
108
- if self.font()==self.font_changing and not self.hasFocus():
109
- self.setFont(self.initialFont)
110
129
 
111
130
  def focusInEvent(self, event):
112
131
  super().focusInEvent(event)
113
132
  for f in self.addfuncin:
114
133
  self.addfuncin[f]()
115
134
  self.focusInFun()
116
-
135
+
117
136
  def setFocus(self):
118
137
  super().setFocus()
119
138
  self.focusInFun()
120
139
 
121
140
  def focusInFun(self):
122
- self.setStyleSheet(setSS(self,self.initialStyle+" "+color_changing))
123
- self.setFont(self.font_changing)
141
+ self.setStyleFont(color_changing,self.font_changing)
142
+
143
+ def setStyleFont(self,color_changing,font):
124
144
  for b in self.bros:
125
- if (not b==self) and b.flagS:
126
- b.setStyleSheet(b.initialStyle+" "+color_changing_black)
127
-
145
+ if hasattr(b,'setInitalStyle') and hasattr(b,'FlagFocusIn') and b.FlagFocusIn:
146
+ b.setInitalStyle()
147
+ break
148
+ if not self.FlagFocusIn:
149
+ self.FlagFocusIn=True
150
+ self.setFont(font)
151
+ for b in self.bros:
152
+ if hasattr(b,'initialStyle'):
153
+ b.setStyleSheet(setSS(b,b.initialStyle+" "+color_changing)+"\n"+gPaIRS_QMenu_style)
154
+
128
155
  def focusOutEvent(self, event):
129
156
  super().focusOutEvent(event) #to preserve classical behaviour before adding the below
130
157
  for f in self.addfuncout:
131
158
  self.addfuncout[f]()
132
- self.focusOutFun()
159
+ self.setInitalStyle()
133
160
 
134
161
  def clearFocus(self):
135
162
  super().clearFocus()
136
- self.focusOutFun()
163
+ self.setInitalStyle()
137
164
 
138
- def focusOutFun(self):
139
- for b in self.bros:
140
- if b.flagS:
165
+ def setInitalStyle(self):
166
+ if self.FlagFocusIn:
167
+ self.FlagFocusIn=False
168
+ for b in self.bros:
141
169
  if hasattr(b,'default_stylesheet'):
142
170
  b.setStyleSheet(b.default_stylesheet)
143
- else:
144
- b.setStyleSheet(setSS(b,b.initialStyle))
145
- if b.flagF:
146
- b.setFont(b.initialFont)
147
- #self.addlab.clear()
171
+ elif hasattr(b,'initialStyle'):
172
+ b.setStyleSheet(b.initialStyle)
173
+ if hasattr(b,'initialFont'):
174
+ b.setFont(b.initialFont)
175
+ for k,w in enumerate(self.CElabs):
176
+ self.CElabs_styles[k]=w.default_stylesheet
177
+ w.setStyleSheet(self.CElabs_styles[k])
178
+ w.repaint()
179
+ self.oldFont=None
180
+ self.oldStyle=None
181
+ #self.addlab.clear()
148
182
 
149
183
  def showCompleter(self):
150
- if self.completer():
151
- self.completer().complete()
152
-
184
+ comp=self.completer()
185
+ if comp:
186
+ view=comp.popup()
187
+ # ---- focus item matching line edit text ----
188
+ text = (self.text() or "").strip()
189
+ if text:
190
+ model = comp.completionModel() or comp.model()
191
+ col = comp.completionColumn()
192
+ for r in range(model.rowCount()):
193
+ idx = model.index(r, col)
194
+ if not idx.isValid():
195
+ continue
196
+ if (idx.data() or "") == text:
197
+ comp.setCurrentRow(r)
198
+ view.setCurrentIndex(idx)
199
+ view.scrollTo(idx, QAbstractItemView.PositionAtCenter)
200
+ break
201
+ view.setStyleSheet(gPaIRS_Completer_style)
202
+ comp.complete()
203
+
153
204
  class MyQLineEditNumber(MyQLineEdit):
154
205
  def __init__(self,parent):
155
206
  super().__init__(parent)
@@ -185,11 +236,14 @@ class MyQSpin(QtWidgets.QSpinBox):
185
236
  self.addfuncreturn={}
186
237
 
187
238
  self.setAccelerated(True)
188
- self.setGroupSeparatorShown(True)
239
+ self.setGroupSeparatorShown(True)
189
240
 
190
241
  def setup(self):
191
242
  if not self.initFlag:
192
243
  self.initFlag=True
244
+ self.FlagFocusIn=False
245
+ self.oldFont=None
246
+ self.oldStyle=None
193
247
  font_changing = QtGui.QFont(self.font())
194
248
  font_changing.setItalic(font_italic)
195
249
  font_changing.setWeight(font_weight)
@@ -200,15 +254,14 @@ class MyQSpin(QtWidgets.QSpinBox):
200
254
  b.initialFont=b.font()
201
255
  b.font_changing=font_changing
202
256
  b.styleFlag=True
203
- self.spinFontObj=[]
204
- for c in self.findChildren(QObject):
205
- if hasattr(c,'setFont'):
206
- self.spinFontObj+=[c]
207
-
257
+ #self.spinFontObj = self.findChildren(QtWidgets.QLineEdit)[0]
258
+ #self.spinFontObj.initialStyle=self.spinFontObj.styleSheet()+" "+backgroundcolor_none
259
+ #self.spinFontObj.font_changing=font_changing
260
+
208
261
  def setFocus(self):
209
262
  super().setFocus()
210
263
  self.focusInFun()
211
-
264
+
212
265
  def focusInEvent(self, event):
213
266
  super().focusInEvent(event) #to preserve classical behaviour before adding the below
214
267
  for f in self.addfuncin:
@@ -216,23 +269,51 @@ class MyQSpin(QtWidgets.QSpinBox):
216
269
  self.focusInFun()
217
270
 
218
271
  def focusInFun(self):
219
- if not self.font()==self.font_changing:
272
+ if not self.FlagFocusIn:
273
+ self.FlagFocusIn=True
274
+ #self.spinFontObj.setStyleSheet(self.spinFontObj.initialStyle+" "+color_changing)
220
275
  for b in self.bros:
221
- b.setStyleSheet(b.initialStyle+" "+color_changing)
222
- b.setFont(b.font_changing)
223
- for b in self.spinFontObj:
276
+ b.setStyleSheet(setSS(b,b.initialStyle+" "+color_changing)+"\n"+gPaIRS_QMenu_style)
224
277
  b.setFont(self.font_changing)
225
278
 
279
+ def clearFocus(self):
280
+ super().clearFocus()
281
+ self.setInitalStyle()
282
+
226
283
  def focusOutEvent(self, event):
227
284
  super().focusOutEvent(event) #to preserve classical behaviour before adding the below
228
285
  for f in self.addfuncout:
229
286
  self.addfuncout[f]()
230
- if self.font()==self.font_changing:
287
+ self.setInitalStyle()
288
+
289
+ def setInitalStyle(self):
290
+ if self.FlagFocusIn:
291
+ self.FlagFocusIn=False
292
+ #self.spinFontObj.setStyleSheet(self.spinFontObj.initialStyle)
231
293
  for b in self.bros:
232
- b.setStyleSheet(b.initialStyle)
233
- b.setFont(b.initialFont)
234
- for b in self.spinFontObj:
235
- b.setFont(self.initialFont)
294
+ b.setStyleSheet(setSS(b,b.initialStyle)+"\n"+gPaIRS_QMenu_style)
295
+ b.setFont(self.initialFont)
296
+ self.findChildren(QtWidgets.QLineEdit)[0].setFont(self.initialFont)
297
+ self.oldFont=None
298
+ self.oldStyle=None
299
+
300
+ def enterEvent(self, event):
301
+ super().enterEvent(event)
302
+ if not self.hasFocus() and self.isEnabled():
303
+ self.oldFont=self.font()
304
+ self.setFont(self.font_changing)
305
+ b=self #b=self.spinFontObj
306
+ self.oldStyle=b.styleSheet()
307
+ b.setStyleSheet(setSS(b,b.initialStyle+" "+backgroundcolor_hover+" "+border_hover)+"\n"+gPaIRS_QMenu_style)
308
+
309
+ def leaveEvent(self, event):
310
+ super().leaveEvent(event)
311
+ if not self.hasFocus() and self.oldFont is not None:
312
+ self.setFont(self.oldFont)
313
+ b=self #b=self.spinFontObj
314
+ b.setStyleSheet(self.oldStyle)
315
+ self.oldFont=None
316
+ self.oldStyle=None
236
317
 
237
318
  def keyPressEvent(self, event):
238
319
  super().keyPressEvent(event)
@@ -284,6 +365,9 @@ class MyQDoubleSpin(QtWidgets.QDoubleSpinBox):
284
365
  def setup(self):
285
366
  if not self.initFlag:
286
367
  self.initFlag=True
368
+ self.FlagFocusIn=False
369
+ self.oldFont=None
370
+ self.oldStyle=None
287
371
  font_changing = QtGui.QFont(self.font())
288
372
  font_changing.setItalic(font_italic)
289
373
  font_changing.setWeight(font_weight)
@@ -294,33 +378,67 @@ class MyQDoubleSpin(QtWidgets.QDoubleSpinBox):
294
378
  b.initialFont=b.font()
295
379
  b.font_changing=font_changing
296
380
  b.styleFlag=True
297
- self.spinFontObj=[]
298
- for c in self.findChildren(QObject):
299
- if hasattr(c,'setFont'):
300
- self.spinFontObj+=[c]
381
+ #self.spinFontObj = self.findChildren(QtWidgets.QLineEdit)[0]
382
+ #self.spinFontObj.initialStyle=self.spinFontObj.styleSheet()+" "+backgroundcolor_none
383
+ #self.spinFontObj.font_changing=font_changing
384
+
385
+ def setFocus(self):
386
+ super().setFocus()
387
+ self.focusInFun()
301
388
 
302
389
  def focusInEvent(self, event):
303
390
  super().focusInEvent(event) #to preserve classical behaviour before adding the below
304
391
  for f in self.addfuncin:
305
392
  self.addfuncin[f]()
306
- if not self.font()==self.font_changing:
393
+ self.focusInFun()
394
+
395
+ def focusInFun(self):
396
+ if not self.FlagFocusIn:
397
+ self.FlagFocusIn=True
398
+ #self.spinFontObj.setStyleSheet(self.spinFontObj.initialStyle+" "+color_changing)
307
399
  for b in self.bros:
308
- b.setStyleSheet(b.initialStyle+" "+color_changing)
309
- b.setFont(self.font_changing)
310
- for b in self.spinFontObj:
400
+ b.setStyleSheet(setSS(b,b.initialStyle+" "+color_changing)+"\n"+gPaIRS_QMenu_style)
311
401
  b.setFont(self.font_changing)
312
402
 
403
+ def clearFocus(self):
404
+ super().clearFocus()
405
+ self.setInitalStyle()
406
+
313
407
  def focusOutEvent(self, event):
314
408
  super().focusOutEvent(event) #to preserve classical behaviour before adding the below
315
409
  for f in self.addfuncout:
316
410
  self.addfuncout[f]()
317
- if self.font()==self.font_changing:
411
+ self.setInitalStyle()
412
+
413
+ def setInitalStyle(self):
414
+ if self.FlagFocusIn:
415
+ self.FlagFocusIn=False
416
+ #self.spinFontObj.setStyleSheet(self.spinFontObj.initialStyle)
318
417
  for b in self.bros:
319
- b.setStyleSheet(b.initialStyle)
320
- b.setFont(b.initialFont)
321
- for b in self.spinFontObj:
418
+ b.setStyleSheet(setSS(b,b.initialStyle)+"\n"+gPaIRS_QMenu_style)
322
419
  b.setFont(self.initialFont)
323
-
420
+ self.findChildren(QtWidgets.QLineEdit)[0].setFont(self.initialFont)
421
+ self.oldFont=None
422
+ self.oldStyle=None
423
+
424
+ def enterEvent(self, event):
425
+ super().enterEvent(event)
426
+ if not self.hasFocus() and self.isEnabled():
427
+ self.oldFont=self.font()
428
+ self.setFont(self.font_changing)
429
+ b=self #b=self.spinFontObj
430
+ self.oldStyle=b.styleSheet()
431
+ b.setStyleSheet(setSS(b,b.initialStyle+" "+backgroundcolor_hover+" "+border_hover)+"\n"+gPaIRS_QMenu_style)
432
+
433
+ def leaveEvent(self, event):
434
+ super().leaveEvent(event)
435
+ if not self.hasFocus() and self.oldFont is not None:
436
+ self.setFont(self.oldFont)
437
+ b=self #b=self.spinFontObj
438
+ b.setStyleSheet(self.oldStyle)
439
+ self.oldFont=None
440
+ self.oldStyle=None
441
+
324
442
  def keyPressEvent(self, event):
325
443
  super().keyPressEvent(event)
326
444
  if event.key() in (Qt.Key.Key_Return,Qt.Key.Key_Enter) and self.hasFocus():
@@ -350,6 +468,20 @@ class CollapsibleBox(QtWidgets.QWidget):
350
468
  self.toggle_button:QPushButton=None
351
469
  self.push_button:MyToolButton=None
352
470
 
471
+ def initialize_widgets(self):
472
+ if self.content_area is None:
473
+ self.content_area=self.findChild(QtWidgets.QGroupBox)
474
+ if self.toggle_button is None:
475
+ self.toggle_button=self.findChild(QtWidgets.QToolButton)
476
+ if self.toggle_button:
477
+ self.toggle_button.setObjectName("CollapsibleBox_toggle")
478
+ self.toggle_button.setChecked(InitCheck)
479
+ self.toggle_button.clicked.connect(self.on_click)
480
+ if hasattr(self.toggle_button,'_orig_paintEvent'):
481
+ self.toggle_button.paintEvent = self.toggle_button._orig_paintEvent
482
+ if self.push_button is None:
483
+ self.push_button=self.findChild(MyToolButton)
484
+
353
485
  def setup(self,*args):
354
486
  if not self.initFlag:
355
487
  if len(args):
@@ -360,29 +492,17 @@ class CollapsibleBox(QtWidgets.QWidget):
360
492
  self.stretch=0
361
493
  self.initFlag=True
362
494
 
363
- if self.content_area is None:
364
- self.content_area=self.findChild(QtWidgets.QGroupBox)
495
+ self.initialize_widgets()
365
496
  self.content_area.setStyleSheet("QGroupBox{border: 1px solid gray; border-radius: 6px;}")
366
-
367
- if self.toggle_button is None:
368
- self.toggle_button=self.findChild(QtWidgets.QToolButton)
369
- self.toggle_button.setChecked(InitCheck)
370
- self.toggle_button.clicked.connect(self.on_click)
371
497
  self.toggle_button.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
372
498
  self.toggle_button.setMinimumWidth(self.toolMinimumWidth)
373
-
374
- if self.push_button is None:
375
- self.push_button=self.findChild(MyToolButton)
376
-
377
499
  self.OpenStyle=\
378
500
  "QToolButton { border: none; }\n"+\
379
- "QToolButton::hover{color: rgba(0,0,255,200);}"+\
380
- "QToolButton::focus{color: rgba(0,0,255,200);}"
501
+ "QToolButton::hover{color: rgba(0,116,255,200);}"
381
502
  #"QToolButton::hover{border: none; border-radius: 6px; background-color: rgba(0, 0,128,32); }"
382
503
  self.ClosedStyle=\
383
504
  "QToolButton { border: 1px solid lightgray; border-radius: 6px }\n"+\
384
- "QToolButton::hover{ border: 1px solid rgba(0,0,255,200); border-radius: 6px; color: rgba(0,0,255,200);}"+\
385
- "QToolButton::focus{ border: 1px solid rgba(0,0,255,200); border-radius: 6px; color: rgba(0,0,255,200);}" #background-color: rgba(0, 0,128,32); }"
505
+ "QToolButton::hover{ border: 1px solid rgba(0,116,255,200); border-radius: 6px; color: rgba(0,116,255,200);}"
386
506
 
387
507
  self.heightToogle=self.toggle_button.minimumHeight()
388
508
  self.heightOpened=self.minimumHeight()
@@ -579,6 +699,7 @@ class RichTextPushButton(QPushButton):
579
699
 
580
700
  self.lyt=self.__lyt
581
701
  self.lbl=self.__lbl
702
+ self.icnWidget=self.__icon
582
703
  self.icn=None
583
704
  return
584
705
 
@@ -682,8 +803,8 @@ class ClickableLabel(QLabel):
682
803
  def __init__(self, *args):
683
804
  super().__init__(*args)
684
805
 
685
- self.default_stylesheet = self.styleSheet()
686
- self.highlight_stylesheet = "background-color: #dcdcdc; border-radius: 3px;"
806
+ self.default_stylesheet = self.styleSheet()
807
+ self.highlight_stylesheet = f"background-color: {PaIRS_ghostblue}; border-radius: 3px;"
687
808
 
688
809
  self.timer = QTimer(self)
689
810
  self.timer.timeout.connect(self.resetHighlight)
@@ -703,6 +824,7 @@ class ClickableLabel(QLabel):
703
824
  warningDialog(self.window(),Message=self.toolTip(),pixmap=pixmap,title='Info')
704
825
 
705
826
  def highlight(self):
827
+ self.default_stylesheet = self.styleSheet() # <-- capture current style
706
828
  self.setStyleSheet(self.highlight_stylesheet)
707
829
  self.repaint()
708
830
 
@@ -719,12 +841,17 @@ class ClickableLabel(QLabel):
719
841
 
720
842
  class ClickableEditLabel(ClickableLabel):
721
843
  def setup(self):
722
- line_edit=QLineEdit(self)
723
- line_edit.setPalette(self.palette())
724
- line_edit_bg_color_str = line_edit.palette().color(QPalette.ColorRole.Base).name()
725
- self.default_stylesheet=self.styleSheet()+f"ClickableEditLabel{{background-color: {line_edit_bg_color_str}}};"
844
+ le = QLineEdit()
845
+ bg = le.palette().color(QPalette.Base)
846
+ bg_rgba = f"rgba({bg.red()}, {bg.green()}, {bg.blue()}, {bg.alpha()})"
847
+
848
+ self.default_stylesheet = self.styleSheet() + f"""
849
+ ClickableEditLabel {{
850
+ background-color: {bg_rgba};
851
+ }}
852
+ """
726
853
  self.setStyleSheet(self.default_stylesheet)
727
- line_edit.setParent(None)
854
+ le.setParent(None)
728
855
 
729
856
  class CustomLineEdit(QLineEdit):
730
857
  cancelEditing = Signal()
@@ -837,6 +964,372 @@ class EditableLabel(QWidget):
837
964
  self.label.show()
838
965
  self.updateLabel()
839
966
 
967
+ class HoverZoomToolButton(QToolButton):
968
+ def __init__(self, parent=None, base_icon=24, zoom=1.25):
969
+ super().__init__(parent)
970
+
971
+ self.zoom_factor = zoom
972
+ self.base_size = QSize(base_icon, base_icon)
973
+ self.zoom_size = QSize(
974
+ int(base_icon * zoom),
975
+ int(base_icon * zoom)
976
+ )
977
+
978
+ self.setIconSize(self.base_size)
979
+
980
+ self.anim = QPropertyAnimation(self, b"iconSize", self)
981
+ self.anim.setDuration(120)
982
+ self.anim.setEasingCurve(QEasingCurve.OutCubic)
983
+
984
+ # --- OVERRIDE ---
985
+ def setIconSize(self, size: QSize):
986
+ """
987
+ Override setIconSize to automatically update base and zoom sizes.
988
+ """
989
+ super().setIconSize(size)
990
+ self.updateFromCurrentIconSize()
991
+
992
+ def updateFromCurrentIconSize(self, zoom: float | None = None):
993
+ """
994
+ Recompute base_size and zoom_size starting from the current iconSize().
995
+ Optionally updates the zoom factor.
996
+ """
997
+ if zoom is not None:
998
+ self.zoom_factor = zoom
999
+
1000
+ current = self.iconSize()
1001
+ self.base_size = QSize(current.width(), current.height())
1002
+ self.zoom_size = QSize(
1003
+ int(current.width() * self.zoom_factor),
1004
+ int(current.height() * self.zoom_factor)
1005
+ )
1006
+
1007
+ def enterEvent(self, event):
1008
+ if self.isEnabled():
1009
+ self.anim.stop()
1010
+ self.anim.setStartValue(self.iconSize())
1011
+ self.anim.setEndValue(self.zoom_size)
1012
+ self.anim.start()
1013
+ super().enterEvent(event)
1014
+
1015
+ def leaveEvent(self, event):
1016
+ if self.isEnabled():
1017
+ self.anim.stop()
1018
+ self.anim.setStartValue(self.iconSize())
1019
+ self.anim.setEndValue(self.base_size)
1020
+ self.anim.start()
1021
+ super().leaveEvent(event)
1022
+
1023
+ def apply_native_hover_border(w, *, borderRadius=3, borderWidth=1, borderColor=str2QColor(PaIRS_gray)):
1024
+ """
1025
+ Adds a hover border overlay while preserving native button rendering.
1026
+ No stylesheet is used. No subclassing (wraps paintEvent + installs eventFilter).
1027
+ """
1028
+
1029
+ # ensure hover events
1030
+ w.setAttribute(Qt.WA_Hover, True)
1031
+
1032
+ # keep original paintEvent once
1033
+ if not hasattr(w, "_orig_paintEvent"):
1034
+ w._orig_paintEvent = w.paintEvent
1035
+
1036
+ def paintEvent_with_border(self, event):
1037
+ # native painting first
1038
+ self._orig_paintEvent(event)
1039
+
1040
+ # overlay border only on hover
1041
+ if self.underMouse() and self.isEnabled():
1042
+ p = QPainter(self)
1043
+ p.setRenderHint(QPainter.Antialiasing, True)
1044
+
1045
+ pen = QPen(borderColor)
1046
+ pen.setWidth(borderWidth)
1047
+ p.setPen(pen)
1048
+ p.setBrush(Qt.NoBrush)
1049
+
1050
+ # keep border inside widget rect
1051
+ d = borderWidth #/ 2.0
1052
+ r = self.rect().adjusted(int(d), int(d), -int(d), -int(d))
1053
+ p.drawRoundedRect(r, borderRadius, borderRadius)
1054
+
1055
+ p.end()
1056
+
1057
+ w.paintEvent = types.MethodType(paintEvent_with_border, w)
1058
+
1059
+ # eventFilter to trigger updates on hover transitions
1060
+ class _HoverUpdateFilter(type(w)):
1061
+ pass
1062
+
1063
+ # simpler: store filter object on widget
1064
+ if not hasattr(w, "_hoverBorderFilter"):
1065
+ from PySide6.QtCore import QObject
1066
+ class HoverBorderFilter(QObject):
1067
+ def eventFilter(self, obj, ev):
1068
+ if ev.type() in (QEvent.Enter, QEvent.Leave, QEvent.HoverEnter, QEvent.HoverLeave, QEvent.HoverMove):
1069
+ obj.update()
1070
+ return False
1071
+
1072
+ w._hoverBorderFilter = HoverBorderFilter(w)
1073
+ w.installEventFilter(w._hoverBorderFilter)
1074
+
1075
+ def apply_native_hover_glow(
1076
+ w:QWidget, *,
1077
+ borderRadius=3,
1078
+ glowSize=4, # spessore della sfumatura
1079
+ baseColor=str2QColor(PaIRS_gray)
1080
+ ):
1081
+ """
1082
+ Adds a soft glow around the widget border on hover,
1083
+ preserving native rendering (no stylesheet).
1084
+ """
1085
+ if w.objectName()=='CollapsibleBox_toggle':
1086
+ pass
1087
+ w.setAttribute(Qt.WA_Hover, True)
1088
+
1089
+ if not hasattr(w, "_orig_paintEvent"):
1090
+ w._orig_paintEvent = w.paintEvent
1091
+
1092
+ def paintEvent_with_glow(self, event):
1093
+ # 1) Native painting
1094
+ self._orig_paintEvent(event)
1095
+
1096
+ if not (self.underMouse() and self.isEnabled()):
1097
+ return
1098
+
1099
+ p = QPainter(self)
1100
+ p.setRenderHint(QPainter.Antialiasing, True)
1101
+ p.setBrush(Qt.NoBrush)
1102
+
1103
+ # 2) Draw soft glow (outer → inner)
1104
+ for i in range(glowSize):
1105
+ alpha = int(120 * (1 - i / glowSize)) # fade out
1106
+ c = QColor(baseColor)
1107
+ c.setAlpha(alpha)
1108
+
1109
+ pen = QPen(c)
1110
+ pen.setWidth(1)
1111
+ p.setPen(pen)
1112
+
1113
+ r = self.rect().adjusted(
1114
+ i + 1, i + 1,
1115
+ -(i + 1), -(i + 1)
1116
+ )
1117
+ p.drawRoundedRect(r, borderRadius, borderRadius)
1118
+
1119
+ p.end()
1120
+
1121
+ w.paintEvent = types.MethodType(paintEvent_with_glow, w)
1122
+
1123
+ # Force repaint on hover transitions
1124
+ if not hasattr(w, "_hoverGlowFilter"):
1125
+ from PySide6.QtCore import QObject
1126
+ class HoverGlowFilter(QObject):
1127
+ def eventFilter(self, obj, ev):
1128
+ if ev.type() in (
1129
+ QEvent.Enter, QEvent.Leave,
1130
+ QEvent.HoverEnter, QEvent.HoverLeave, QEvent.HoverMove
1131
+ ):
1132
+ obj.update()
1133
+ return False
1134
+
1135
+ w._hoverGlowFilter = HoverGlowFilter(w)
1136
+ w.installEventFilter(w._hoverGlowFilter)
1137
+
1138
+ def apply_hover_glow_label(
1139
+ w: QLabel,
1140
+ *,
1141
+ color=str2QColor(PaIRS_blue), #"#0051FF",
1142
+ blur=18,
1143
+ max_alpha=170,
1144
+ duration_ms=160,
1145
+ ):
1146
+ if getattr(w, "_hoverGlowInstalled", False):
1147
+ w._hoverGlowColor = QColor(color)
1148
+ w._hoverGlowBlur = float(blur)
1149
+ w._hoverGlowMaxAlpha = int(max_alpha)
1150
+ w._hoverGlowDuration = int(duration_ms)
1151
+ w._hoverGlowEffect.setBlurRadius(w._hoverGlowBlur)
1152
+ return
1153
+
1154
+ w._hoverGlowInstalled = True
1155
+ w._hoverGlowColor = QColor(color)
1156
+ w._hoverGlowBlur = float(blur)
1157
+ w._hoverGlowMaxAlpha = int(max_alpha)
1158
+ w._hoverGlowDuration = int(duration_ms)
1159
+
1160
+ eff = QGraphicsDropShadowEffect(w)
1161
+ eff.setOffset(0, 0)
1162
+ eff.setBlurRadius(w._hoverGlowBlur)
1163
+
1164
+ c = QColor(w._hoverGlowColor)
1165
+ c.setAlpha(0)
1166
+ eff.setColor(c)
1167
+
1168
+ w.setGraphicsEffect(eff)
1169
+ w._hoverGlowEffect = eff
1170
+
1171
+ def _set_alpha(val):
1172
+ col = QColor(w._hoverGlowColor)
1173
+ col.setAlpha(int(val))
1174
+ w._hoverGlowEffect.setColor(col)
1175
+
1176
+ # ✅ Animate a plain value (no Qt property needed)
1177
+ w._hoverGlowAnimAlpha = QVariantAnimation(w)
1178
+ w._hoverGlowAnimAlpha.setEasingCurve(QEasingCurve.OutCubic)
1179
+ w._hoverGlowAnimAlpha.valueChanged.connect(_set_alpha)
1180
+
1181
+ class _GlowFilter(QObject):
1182
+ def eventFilter(self, obj, ev):
1183
+ if obj is not w:
1184
+ return False
1185
+
1186
+ t = ev.type()
1187
+ if t in (QEvent.Enter, QEvent.HoverEnter):
1188
+ w._hoverGlowAnimAlpha.stop()
1189
+ w._hoverGlowAnimAlpha.setDuration(w._hoverGlowDuration)
1190
+ w._hoverGlowAnimAlpha.setStartValue(w._hoverGlowEffect.color().alpha())
1191
+ w._hoverGlowAnimAlpha.setEndValue(w._hoverGlowMaxAlpha)
1192
+ w._hoverGlowAnimAlpha.start()
1193
+
1194
+ elif t in (QEvent.Leave, QEvent.HoverLeave):
1195
+ w._hoverGlowAnimAlpha.stop()
1196
+ w._hoverGlowAnimAlpha.setDuration(w._hoverGlowDuration)
1197
+ w._hoverGlowAnimAlpha.setStartValue(w._hoverGlowEffect.color().alpha())
1198
+ w._hoverGlowAnimAlpha.setEndValue(0)
1199
+ w._hoverGlowAnimAlpha.start()
1200
+
1201
+ return False
1202
+
1203
+ w._hoverGlowFilter = _GlowFilter(w)
1204
+ w.installEventFilter(w._hoverGlowFilter)
1205
+
1206
+ w.setAttribute(Qt.WA_Hover, True)
1207
+ w.setMouseTracking(True)
1208
+
1209
+ def remove_hover_glow_label(w: QLabel):
1210
+ """Removes the hover glow behavior and restores a clean state."""
1211
+ if not getattr(w, "_hoverGlowInstalled", False):
1212
+ return
1213
+
1214
+ # Stop animation cleanly
1215
+ anim = getattr(w, "_hoverGlowAnimAlpha", None)
1216
+ if anim is not None:
1217
+ anim.stop()
1218
+
1219
+ # Remove event filter
1220
+ filt = getattr(w, "_hoverGlowFilter", None)
1221
+ if filt is not None:
1222
+ try:
1223
+ w.removeEventFilter(filt)
1224
+ except RuntimeError:
1225
+ pass
1226
+
1227
+ # Remove graphics effect
1228
+ w.setGraphicsEffect(None)
1229
+
1230
+ # Restore hover-related flags (safe defaults)
1231
+ w.setAttribute(Qt.WA_Hover, False)
1232
+ w.setMouseTracking(False)
1233
+
1234
+ # Cleanup attributes
1235
+ for attr in (
1236
+ "_hoverGlowInstalled",
1237
+ "_hoverGlowColor",
1238
+ "_hoverGlowBlur",
1239
+ "_hoverGlowMaxAlpha",
1240
+ "_hoverGlowDuration",
1241
+ "_hoverGlowEffect",
1242
+ "_hoverGlowAnimAlpha",
1243
+ "_hoverGlowFilter",
1244
+ ):
1245
+ if hasattr(w, attr):
1246
+ delattr(w, attr)
1247
+
1248
+ class SliderHandleCursorFilter(QObject):
1249
+ def eventFilter(self, obj, event):
1250
+ if isinstance(obj, QSlider) and event.type() == QEvent.MouseMove:
1251
+
1252
+ opt = QStyleOptionSlider()
1253
+ obj.initStyleOption(opt)
1254
+
1255
+ handle_rect = obj.style().subControlRect(
1256
+ QStyle.CC_Slider,
1257
+ opt,
1258
+ QStyle.SC_SliderHandle,
1259
+ obj
1260
+ )
1261
+
1262
+ if handle_rect.contains(event.position().toPoint()):
1263
+ obj.setCursor(QCursor(Qt.OpenHandCursor))
1264
+ else:
1265
+ obj.unsetCursor()
1266
+
1267
+ return False
1268
+
1269
+ def changes(self,TabType,filename,title=" Changes"):
1270
+ FlagShow=False
1271
+ if self.logChanges:
1272
+ if self.logChanges.isVisible():
1273
+ FlagShow=True
1274
+ if FlagShow:
1275
+ self.logChanges.hide()
1276
+ self.logChanges.show()
1277
+ else:
1278
+ self.logChanges=TabType(self,True)
1279
+ self.logChanges.resize(720,720)
1280
+ self.logChanges.show()
1281
+ self.logChanges.ui.progress_Proc.hide()
1282
+ self.logChanges.ui.button_close_tab.hide()
1283
+ icon=QPixmap(''+ icons_path +'news.png')
1284
+ self.logChanges.ui.icon.setPixmap(icon)
1285
+ apply_hover_glow_label(self.logChanges.ui.icon)
1286
+ self.logChanges.setWindowIcon(self.windowIcon())
1287
+ self.logChanges.setWindowTitle(title)
1288
+ self.logChanges.ui.name_tab.setText(title)
1289
+
1290
+ self.logChanges.ui.log.setLineWrapColumnOrWidth(self.logChanges.ui.log.width()-20)
1291
+ base="""
1292
+ QTextEdit {
1293
+ border: 1px solid #2a2a2a;
1294
+ border-radius: 6px;
1295
+
1296
+ padding: 2px;
1297
+
1298
+ selection-background-color: #0051FF;
1299
+ selection-color: #FFFFFF;
1300
+ }
1301
+ """
1302
+ self.logChanges.ui.log.setStyleSheet(base+"\n"+gPaIRS_QMenu_style)
1303
+
1304
+ def setFontPixelSize(logChanges:type(self.logChanges),fPixSize):
1305
+ logfont=self.font()
1306
+ logfont.setFamily(fontName)
1307
+ logfont.setPixelSize(fPixSize+2)
1308
+ logChanges.ui.log.setFont(logfont)
1309
+ fPixSize_TabNames=min([fPixSize*2,30])
1310
+ lab=logChanges.ui.name_tab
1311
+ font=lab.font()
1312
+ font.setPixelSize(fPixSize_TabNames)
1313
+ lab.setFont(font)
1314
+ self.logChanges.setFontPixelSize=lambda fS: setFontPixelSize(self.logChanges,fS)
1315
+ self.logChanges.setFontPixelSize(self.TABpar.fontPixelSize)
1316
+ def logResizeEvent(logChanges:type(self.logChanges),e):
1317
+ super(type(logChanges),logChanges).resizeEvent(e)
1318
+ logChanges.ui.log.setLineWrapColumnOrWidth(logChanges.ui.log.width()-20)
1319
+ self.logChanges.ui.log.resizeEvent=lambda e: logResizeEvent(self.logChanges,e)
1320
+
1321
+ self.logChanges.ui.icon.addfuncclick['whatsnew']=self.whatsNew
1322
+ self.logChanges.ui.icon.setCustomCursor()
1323
+
1324
+ try:
1325
+ file = open(filename, "rb")
1326
+ content = file.read().decode("utf-8")
1327
+ self.logChanges.ui.log.setText(content)
1328
+ file.close()
1329
+ except Exception as inst:
1330
+ pri.Error.red(f'There was a problem while reading the file {filename}:\n{inst}')
1331
+ self.logChanges.ui.log.setText(f'No information about PaIRS-UniNa updates available!\n\nSorry for this, try to reinstall PaIRS-UniNa or alternatively contact the authors at {__mail__}.')
1332
+ return
840
1333
 
841
1334
  #********************************************* Matplotlib
842
1335
  import io
@@ -905,7 +1398,7 @@ class MplCanvas(FigureCanvasQTAgg):
905
1398
  lbl=QLabel(fig2)
906
1399
  lbl.setAlignment(QtCore.Qt.AlignCenter)
907
1400
  with io.BytesIO() as buffer:
908
- self.fig.savefig(buffer)
1401
+ self.fig.savefig(buffer, bbox_inches='tight', pad_inches=0)
909
1402
  pixmap = QPixmap(QImage.fromData(buffer.getvalue()))
910
1403
  lbl.setPixmap(pixmap)
911
1404
  lbl.setScaledContents(False)
@@ -917,8 +1410,6 @@ class MplCanvas(FigureCanvasQTAgg):
917
1410
  lay.addWidget(lbl)
918
1411
  lay.addWidget(lbl2)
919
1412
 
920
- self.fig2.append(fig2)
921
-
922
1413
  def closeFig2(event):
923
1414
  type(fig2).closeEvent(fig2,event)
924
1415
  self.fig2.pop(self.fig2.index(fig2))
@@ -936,7 +1427,7 @@ class MplCanvas(FigureCanvasQTAgg):
936
1427
  fig2.scaleFactor=fig2.scaleFactor*scale
937
1428
  fig2.scaleFactor=min([fig2.scaleFactor,1.5])
938
1429
  fig2.scaleFactor=max([fig2.scaleFactor,0.5])
939
- fig2.setFixedSize(s0*fig2.scaleFactor)
1430
+ fig2.setFixedSize(fig2.s0*fig2.scaleFactor)
940
1431
  lbl.setPixmap(pixmap.scaled(pixmap.size()*fig2.scaleFactor,mode=Qt.TransformationMode.SmoothTransformation))
941
1432
  return
942
1433
  fig2.resizeFig2=resizeFig2
@@ -988,6 +1479,7 @@ class MplCanvas(FigureCanvasQTAgg):
988
1479
  fig2.lbl:QLabel=lbl
989
1480
  def contextMenuEventFig2(event):
990
1481
  contextMenu = QMenu()
1482
+ contextMenu.setStyleSheet(gPaIRS_QMenu_style)
991
1483
  copy2clipboard = contextMenu.addAction("Copy to clipboard ("+QS_copy2clipboard.key().toString(QKeySequence.NativeText)+")")
992
1484
  contextMenu.addSeparator()
993
1485
  scaleDown = contextMenu.addAction("Scale down ("+QS_scaleDown.key().toString(QKeySequence.NativeText)+")")
@@ -1028,9 +1520,11 @@ class MplCanvas(FigureCanvasQTAgg):
1028
1520
 
1029
1521
  fig2.show()
1030
1522
  fig2.setFixedSize(fig2.width(), fig2.height())
1031
- s0=fig2.size()
1523
+ fig2.s0=fig2.size()
1032
1524
 
1525
+ self.fig2.append(fig2)
1033
1526
  self.posWindow(len(self.fig2)-1)
1527
+
1034
1528
  """
1035
1529
  fgeo = fig2.frameGeometry()
1036
1530
  centerPoint = QGuiApplication.primaryScreen().availableGeometry().center()
@@ -1039,7 +1533,7 @@ class MplCanvas(FigureCanvasQTAgg):
1039
1533
  """
1040
1534
 
1041
1535
  def showTip(self,obj,message):
1042
- showTip(obj,message)
1536
+ show_mouse_tooltip(obj,message)
1043
1537
 
1044
1538
  def posWindow(self,ind):
1045
1539
  w=h=0
@@ -1115,12 +1609,12 @@ def setAppGuiPalette(self:QWidget,palette:QPalette=None):
1115
1609
  c.setPalette(palette)
1116
1610
  if hasattr(c,'initialStyle') and hasattr(c, 'setStyleSheet'):
1117
1611
  c.setStyleSheet(c.initialStyle)
1118
- for c in f.findChildren(QObject):
1119
- c:MyQLineEdit
1120
- if isinstance(c, MyQLineEdit) and hasattr(c, 'setup'):
1121
- c.initFlag=False
1122
- c.styleFlag=False
1123
- c.setup()
1612
+ for c in f.findChildren(MyQLineEdit):
1613
+ c.initFlag=False
1614
+ c.styleFlag=False
1615
+ c.setup()
1616
+ for c in f.findChildren(ClickableEditLabel):
1617
+ c.setup()
1124
1618
  for c in f.findChildren(QObject):
1125
1619
  if hasattr(c,'setup2'):
1126
1620
  c.initFlag2=False
@@ -1131,3 +1625,185 @@ def setAppGuiPalette(self:QWidget,palette:QPalette=None):
1131
1625
  if hasattr(self,'w_Vis'): self.w_Vis.addPlotToolBar()
1132
1626
  except:
1133
1627
  pri.Error.red("***** Error while setting the application palette! *****")
1628
+
1629
+ class Toast(QFrame):
1630
+ """
1631
+ Tooltip-like custom toast that appears at the current mouse cursor position.
1632
+ Non-native, does not steal focus. Auto-hides after timeout_ms.
1633
+ """
1634
+
1635
+ def __init__(
1636
+ self,
1637
+ parent: QWidget,
1638
+ msg: str,
1639
+ *,
1640
+ timeout_ms: int = 2500,
1641
+ offset: QPoint = QPoint(10, 15), # offset from cursor
1642
+ min_width: int = 0,
1643
+ max_width: int = 460,
1644
+ fade_in_ms: int = 80,
1645
+ fade_out_ms: int = 130,
1646
+ ):
1647
+ super().__init__(parent)
1648
+
1649
+ self.setObjectName("PaIRSToast")
1650
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.ToolTip)
1651
+ #self.setAttribute(Qt.WA_TranslucentBackground, True)
1652
+ self.setAttribute(Qt.WA_ShowWithoutActivating, True)
1653
+
1654
+ self._label = QLabel(msg, self)
1655
+ self._label.setObjectName("PaIRSToastLabel")
1656
+ self._label.setWordWrap(True)
1657
+
1658
+ lay = QHBoxLayout(self)
1659
+ lay.setContentsMargins(4,3,4,3)
1660
+ lay.addWidget(self._label)
1661
+
1662
+ # Classic tooltip-like styling
1663
+ self.setStyleSheet("""
1664
+ QFrame#PaIRSToast {
1665
+ background-color: #ffffdc; /* classic tooltip-ish */
1666
+ color: #000000;
1667
+ border: 1px solid rgba(0, 0, 0, 0.45);
1668
+ border-radius: 4px;
1669
+ }
1670
+ QLabel#PaIRSToastLabel {
1671
+ color: #000000;
1672
+ }
1673
+ """)
1674
+
1675
+ # Width clamp + nice wrapping
1676
+ # Reset any previous constraints (important if a previous tooltip was wider)
1677
+ self.setMinimumSize(min_width, 0)
1678
+ self.setMaximumSize(max_width, 16777215)
1679
+
1680
+ self._label.setMinimumSize(min_width, 0)
1681
+ self._label.setMaximumSize(max_width, 16777215)
1682
+
1683
+ # Make sure the label doesn't "expand"
1684
+ self._label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1685
+
1686
+ font=self.window().font()
1687
+ self._label.setFont(font)
1688
+ self._apply_size(msg)
1689
+
1690
+ # Position at cursor (global)
1691
+ self._move_to_cursor(offset)
1692
+
1693
+ # Opacity animation
1694
+ self.setWindowOpacity(0.0)
1695
+ self._anim = QPropertyAnimation(self, b"windowOpacity", self)
1696
+ self._anim.setEasingCurve(QEasingCurve.OutCubic)
1697
+
1698
+ # Auto hide
1699
+ self._timer = QTimer(self)
1700
+ self._timer.setSingleShot(True)
1701
+ self._timer.timeout.connect(lambda: self.fade_out(duration_ms=fade_out_ms))
1702
+
1703
+ self.show()
1704
+ self.raise_()
1705
+ self.fade_in(duration_ms=fade_in_ms)
1706
+ self._timer.start(timeout_ms)
1707
+
1708
+ def _apply_size(self, msg: str):
1709
+ self._label.setText(msg)
1710
+
1711
+ # Let QLabel compute the correct wrapped size
1712
+ self._label.adjustSize()
1713
+ sh = self._label.sizeHint()
1714
+
1715
+ # Lock label to its real hint size
1716
+ self._label.setFixedSize(sh)
1717
+
1718
+ # Now shrink the frame to content + margins
1719
+ self.adjustSize()
1720
+
1721
+ # Lock the whole toaster too (prevents extra blank area)
1722
+ self.setFixedSize(self.sizeHint())
1723
+
1724
+ def caret_global_pos(self, edit, offset):
1725
+ """
1726
+ Returns a stable global position near the text caret.
1727
+ Works for QLineEdit, QTextEdit, QPlainTextEdit.
1728
+ """
1729
+ if hasattr(edit, "cursorRect"):
1730
+ r = edit.cursorRect() # widget coords
1731
+
1732
+ # Anchor to bottom-left of caret rect (more stable than bottomRight)
1733
+ p = edit.mapToGlobal(r.bottomLeft())
1734
+ p += offset
1735
+
1736
+ # Clamp to screen to avoid off-screen placement
1737
+ screen = QApplication.screenAt(QCursor.pos()) or edit.screen()
1738
+ if screen:
1739
+ geo = screen.availableGeometry()
1740
+ x = min(max(p.x(), geo.left()), geo.right())
1741
+ y = min(max(p.y(), geo.top()), geo.bottom())
1742
+ return QPoint(x, y)
1743
+
1744
+ return p
1745
+
1746
+ # Fallback: mouse cursor
1747
+ return QCursor.pos() + offset
1748
+
1749
+ def _move_to_cursor(self, offset: QPoint):
1750
+ parent:QLineEdit = self.parent()
1751
+ if parent and hasattr(parent, "cursorRect"):
1752
+ p=self.caret_global_pos(parent,QPoint(0,6))
1753
+ else:
1754
+ p=self.caret_global_pos(parent,offset)
1755
+
1756
+ # Keep inside the current screen geometry
1757
+ screen = self.screen()
1758
+ if screen is None:
1759
+ self.move(p)
1760
+ return
1761
+
1762
+ geo = screen.availableGeometry()
1763
+ self.adjustSize()
1764
+ w, h = self.width(), self.height()
1765
+
1766
+ x = p.x()
1767
+ y = p.y()
1768
+
1769
+ if x + w > geo.right():
1770
+ x = geo.right() - w
1771
+ if y + h > geo.bottom():
1772
+ y = geo.bottom() - h
1773
+ if x < geo.left():
1774
+ x = geo.left()
1775
+ if y < geo.top():
1776
+ y = geo.top()
1777
+
1778
+ self.move(QPoint(x, y))
1779
+
1780
+ def fade_in(self, *, duration_ms: int = 80):
1781
+ self._anim.stop()
1782
+ self._anim.setDuration(duration_ms)
1783
+ self._anim.setStartValue(self.windowOpacity())
1784
+ self._anim.setEndValue(1.0)
1785
+ self._anim.start()
1786
+
1787
+ def fade_out(self, *, duration_ms: int = 130):
1788
+ self._anim.stop()
1789
+ self._anim.setDuration(duration_ms)
1790
+ self._anim.setStartValue(self.windowOpacity())
1791
+ self._anim.setEndValue(0.0)
1792
+ self._anim.finished.connect(self.close)
1793
+ self._anim.start()
1794
+
1795
+ def show_mouse_tooltip(parent: QWidget, msg: str, *, timeout_ms: int = 2500):
1796
+ """
1797
+ Convenience function: show a tooltip-like toaster at the mouse cursor.
1798
+ Keeps a reference on parent to avoid garbage collection.
1799
+ """
1800
+ old = getattr(parent, "_pairs_mouse_toaster", None)
1801
+ if old is not None and old.isVisible():
1802
+ old.close()
1803
+
1804
+ if msg:
1805
+ t = Toast(parent, msg, timeout_ms=timeout_ms)
1806
+ else: t=None
1807
+ parent._pairs_mouse_toaster = t
1808
+ return t
1809
+