2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
8 from math
import isnan
, modf
9 from random
import random
11 from qt_compat
import qt_config
14 from PyQt5
.QtCore
import pyqtSignal
, pyqtSlot
, QT_VERSION
, Qt
, QTimer
15 from PyQt5
.QtGui
import QCursor
, QPalette
16 from PyQt5
.QtWidgets
import QAbstractSpinBox
, QApplication
, QComboBox
, QDialog
, QMenu
, QProgressBar
18 from PyQt6
.QtCore
import pyqtSignal
, pyqtSlot
, QT_VERSION
, Qt
, QTimer
19 from PyQt6
.QtGui
import QCursor
, QPalette
20 from PyQt6
.QtWidgets
import QAbstractSpinBox
, QApplication
, QComboBox
, QDialog
, QMenu
, QProgressBar
22 # ------------------------------------------------------------------------------------------------------------
25 import ui_inputdialog_value
27 from carla_shared
import countDecimalPoints
, MACOS
29 # ------------------------------------------------------------------------------------------------------------
30 # Get a fixed value within min/max bounds
32 def geFixedValue(name
, value
, minimum
, maximum
):
34 print("Parameter '%s' is NaN! - %f" % (name
, value
))
37 print("Parameter '%s' too low! - %f/%f" % (name
, value
, minimum
))
40 print("Parameter '%s' too high! - %f/%f" % (name
, value
, maximum
))
44 # ------------------------------------------------------------------------------------------------------------
45 # Custom InputDialog with Scale Points support
47 class CustomInputDialog(QDialog
):
48 def __init__(self
, parent
, label
, current
, minimum
, maximum
, step
, stepSmall
, scalePoints
, prefix
, suffix
):
49 QDialog
.__init
__(self
, parent
)
50 self
.ui
= ui_inputdialog_value
.Ui_Dialog()
53 decimals
= countDecimalPoints(step
, stepSmall
)
54 self
.ui
.label
.setText(label
)
55 self
.ui
.doubleSpinBox
.setDecimals(decimals
)
56 self
.ui
.doubleSpinBox
.setRange(minimum
, maximum
)
57 self
.ui
.doubleSpinBox
.setSingleStep(step
)
58 self
.ui
.doubleSpinBox
.setValue(current
)
59 self
.ui
.doubleSpinBox
.setPrefix(prefix
)
60 self
.ui
.doubleSpinBox
.setSuffix(suffix
)
63 self
.setWindowModality(Qt
.WindowModal
)
66 self
.ui
.groupBox
.setVisible(False)
70 for scalePoint
in scalePoints
:
71 valuestr
= ("%i" if decimals
== 0 else "%f") % scalePoint
['value']
73 text
+= f
"<td align='right'>{valuestr}</td><td align='left'> - {scalePoint['label']}</td>"
76 self
.ui
.textBrowser
.setText(text
)
79 self
.fRetValue
= current
81 self
.accepted
.connect(self
.slot_setReturnValue
)
83 def returnValue(self
):
87 def slot_setReturnValue(self
):
88 self
.fRetValue
= self
.ui
.doubleSpinBox
.value()
94 # ------------------------------------------------------------------------------------------------------------
95 # ProgressBar used for ParamSpinBox
97 class ParamProgressBar(QProgressBar
):
99 dragStateChanged
= pyqtSignal(bool)
100 valueChanged
= pyqtSignal(float)
102 def __init__(self
, parent
):
103 QProgressBar
.__init
__(self
, parent
)
105 self
.fLeftClickDown
= False
106 self
.fIsInteger
= False
107 self
.fIsReadOnly
= False
111 self
.fInitiated
= False
112 self
.fRealValue
= 0.0
114 self
.fLastPaintedValue
= None
115 self
.fCurrentPaintedText
= ""
118 self
.fLabelPrefix
= ""
119 self
.fLabelSuffix
= ""
120 self
.fTextCall
= None
121 self
.fValueCall
= None
123 self
.setFormat("(none)")
125 # Fake internal value, 10'000 precision
126 QProgressBar
.setMinimum(self
, 0)
127 QProgressBar
.setMaximum(self
, 10000)
128 QProgressBar
.setValue(self
, 0)
130 def setMinimum(self
, value
):
131 self
.fMinimum
= value
133 def setMaximum(self
, value
):
134 self
.fMaximum
= value
136 def setValue(self
, value
):
137 if (self
.fRealValue
== value
or isnan(value
)) and self
.fInitiated
:
140 self
.fInitiated
= True
141 self
.fRealValue
= value
142 div
= float(self
.fMaximum
- self
.fMinimum
)
145 print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self
.fName
,
151 print("Parameter '%s' is NaN (value:%f, min:%f, max:%f)" % (self
.fName
,
157 vper
= float(value
- self
.fMinimum
) / div
164 if self
.fValueCall
is not None:
165 self
.fValueCall(value
)
167 QProgressBar
.setValue(self
, int(vper
* 10000))
170 def setSuffixes(self
, prefix
, suffix
):
171 self
.fLabelPrefix
= prefix
172 self
.fLabelSuffix
= suffix
174 # force refresh of text value
175 self
.fLastPaintedValue
= None
179 def setName(self
, name
):
182 def setReadOnly(self
, yesNo
):
183 self
.fIsReadOnly
= yesNo
185 def setTextCall(self
, textCall
):
186 self
.fTextCall
= textCall
188 def setValueCall(self
, valueCall
):
189 self
.fValueCall
= valueCall
191 def handleMouseEventPos(self
, pos
):
195 xper
= float(pos
.x()) / float(self
.width())
196 value
= xper
* (self
.fMaximum
- self
.fMinimum
) + self
.fMinimum
201 if value
< self
.fMinimum
:
202 value
= self
.fMinimum
203 elif value
> self
.fMaximum
:
204 value
= self
.fMaximum
206 if self
.setValue(value
):
207 self
.valueChanged
.emit(value
)
209 def mousePressEvent(self
, event
):
213 if event
.button() == Qt
.LeftButton
:
214 self
.handleMouseEventPos(event
.pos())
215 self
.fLeftClickDown
= True
216 self
.dragStateChanged
.emit(True)
218 self
.fLeftClickDown
= False
220 QProgressBar
.mousePressEvent(self
, event
)
222 def mouseMoveEvent(self
, event
):
226 if self
.fLeftClickDown
:
227 self
.handleMouseEventPos(event
.pos())
229 QProgressBar
.mouseMoveEvent(self
, event
)
231 def mouseReleaseEvent(self
, event
):
235 self
.fLeftClickDown
= False
236 self
.dragStateChanged
.emit(False)
237 QProgressBar
.mouseReleaseEvent(self
, event
)
239 def paintEvent(self
, event
):
240 if self
.fTextCall
is not None:
241 if self
.fLastPaintedValue
!= self
.fRealValue
:
242 self
.fLastPaintedValue
= self
.fRealValue
243 self
.fCurrentPaintedText
= self
.fTextCall()
244 self
.setFormat("%s%s%s" % (self
.fLabelPrefix
, self
.fCurrentPaintedText
, self
.fLabelSuffix
))
246 elif self
.fIsInteger
:
247 self
.setFormat("%s%i%s" % (self
.fLabelPrefix
, int(self
.fRealValue
), self
.fLabelSuffix
))
250 self
.setFormat("%s%f%s" % (self
.fLabelPrefix
, self
.fRealValue
, self
.fLabelSuffix
))
252 QProgressBar
.paintEvent(self
, event
)
254 # ------------------------------------------------------------------------------------------------------------
255 # Special SpinBox used for parameters
257 class ParamSpinBox(QAbstractSpinBox
):
259 valueChanged
= pyqtSignal(float)
261 def __init__(self
, parent
):
262 QAbstractSpinBox
.__init
__(self
, parent
)
265 self
.fLabelPrefix
= ""
266 self
.fLabelSuffix
= ""
274 self
.fStepSmall
= 0.0001
275 self
.fStepLarge
= 0.1
277 self
.fIsReadOnly
= False
278 self
.fScalePoints
= None
279 self
.fUseScalePoints
= False
281 self
.fBar
= ParamProgressBar(self
)
282 self
.fBar
.setContextMenuPolicy(Qt
.NoContextMenu
)
285 barPalette
= self
.fBar
.palette()
286 barPalette
.setColor(QPalette
.Window
, Qt
.transparent
)
287 self
.fBar
.setPalette(barPalette
)
291 self
.lineEdit().hide()
293 self
.customContextMenuRequested
.connect(self
.slot_showCustomMenu
)
294 self
.fBar
.valueChanged
.connect(self
.slot_progressBarValueChanged
)
296 self
.dragStateChanged
= self
.fBar
.dragStateChanged
298 QTimer
.singleShot(0, self
.slot_updateProgressBarGeometry
)
300 def setDefault(self
, value
):
301 value
= geFixedValue(self
.fName
, value
, self
.fMinimum
, self
.fMaximum
)
302 self
.fDefault
= value
304 def setMinimum(self
, value
):
305 self
.fMinimum
= value
306 self
.fBar
.setMinimum(value
)
308 def setMaximum(self
, value
):
309 self
.fMaximum
= value
310 self
.fBar
.setMaximum(value
)
312 def setValue(self
, value
):
313 if not self
.fIsReadOnly
:
314 value
= geFixedValue(self
.fName
, value
, self
.fMinimum
, self
.fMaximum
)
316 if self
.fBar
.fIsInteger
:
319 if self
.fValue
== value
:
323 self
.fBar
.setValue(value
)
325 if self
.fUseScalePoints
:
326 self
._setScalePointValue
(value
)
328 self
.valueChanged
.emit(value
)
333 def setStep(self
, value
):
339 self
.fStepSmall
= min(self
.fStepSmall
, value
)
340 self
.fStepLarge
= max(self
.fStepLarge
, value
)
342 self
.fBar
.fIsInteger
= bool(self
.fStepSmall
== 1.0)
344 def setStepSmall(self
, value
):
346 self
.fStepSmall
= 0.0001
347 elif value
> self
.fStep
:
348 self
.fStepSmall
= self
.fStep
350 self
.fStepSmall
= value
352 self
.fBar
.fIsInteger
= bool(self
.fStepSmall
== 1.0)
354 def setStepLarge(self
, value
):
356 self
.fStepLarge
= 0.1
357 elif value
< self
.fStep
:
358 self
.fStepLarge
= self
.fStep
360 self
.fStepLarge
= value
362 def setLabel(self
, label
):
364 suffix
= label
.strip()
366 if suffix
== "(coef)":
370 suffix
= " " + suffix
372 self
.fLabelPrefix
= prefix
373 self
.fLabelSuffix
= suffix
374 self
.fBar
.setSuffixes(prefix
, suffix
)
376 def setName(self
, name
):
378 self
.fBar
.setName(name
)
380 def setTextCallback(self
, textCall
):
381 self
.fBar
.setTextCall(textCall
)
383 def setValueCallback(self
, valueCall
):
384 self
.fBar
.setValueCall(valueCall
)
386 def setReadOnly(self
, yesNo
):
387 self
.fIsReadOnly
= yesNo
388 self
.fBar
.setReadOnly(yesNo
)
389 self
.setButtonSymbols(QAbstractSpinBox
.UpDownArrows
if yesNo
else QAbstractSpinBox
.NoButtons
)
390 QAbstractSpinBox
.setReadOnly(self
, yesNo
)
392 # FIXME use change event
393 def setEnabled(self
, yesNo
):
394 self
.fBar
.setEnabled(yesNo
)
395 QAbstractSpinBox
.setEnabled(self
, yesNo
)
397 def setScalePoints(self
, scalePoints
, useScalePoints
):
398 if len(scalePoints
) == 0:
399 self
.fScalePoints
= None
400 self
.fUseScalePoints
= False
403 self
.fScalePoints
= scalePoints
404 self
.fUseScalePoints
= useScalePoints
406 if not useScalePoints
:
409 # Hide ProgressBar and create a ComboBox
411 self
.fBox
= QComboBox(self
)
412 self
.fBox
.setContextMenuPolicy(Qt
.NoContextMenu
)
414 self
.slot_updateProgressBarGeometry()
419 for scalePoint
in scalePoints
:
420 value
= scalePoint
['value']
422 if self
.fStep
== 1.0:
423 label
= "%i - %s" % (int(value
), scalePoint
['label'])
425 label
= "%f - %s" % (value
, scalePoint
['label'])
427 if len(boxItemValues
) == 0:
428 self
.fBox
.addItem(label
)
429 boxItemValues
.append(value
)
432 if value
< boxItemValues
[0]:
433 self
.fBox
.insertItem(0, label
)
434 boxItemValues
.insert(0, value
)
435 elif value
> boxItemValues
[-1]:
436 self
.fBox
.addItem(label
)
437 boxItemValues
.append(value
)
439 for index
in range(len(boxItemValues
)):
440 if value
>= boxItemValues
[index
]:
441 self
.fBox
.insertItem(index
+1, label
)
442 boxItemValues
.insert(index
+1, value
)
445 if self
.fValue
is not None:
446 self
._setScalePointValue
(self
.fValue
)
448 if QT_VERSION
>= 0x60000:
449 self
.fBox
.currentTextChanged
.connect(self
.slot_comboBoxIndexChanged
)
451 self
.fBox
.currentIndexChanged
['QString'].connect(self
.slot_comboBoxIndexChanged
)
453 def setToolTip(self
, text
):
454 self
.fBar
.setToolTip(text
)
455 QAbstractSpinBox
.setToolTip(self
, text
)
457 def stepBy(self
, steps
):
458 if steps
== 0 or self
.fValue
is None:
461 value
= self
.fValue
+ (self
.fStep
* steps
)
463 if value
< self
.fMinimum
:
464 value
= self
.fMinimum
465 elif value
> self
.fMaximum
:
466 value
= self
.fMaximum
470 def stepEnabled(self
):
471 if self
.fIsReadOnly
or self
.fValue
is None:
472 return QAbstractSpinBox
.StepNone
473 if self
.fValue
<= self
.fMinimum
:
474 return QAbstractSpinBox
.StepUpEnabled
475 if self
.fValue
>= self
.fMaximum
:
476 return QAbstractSpinBox
.StepDownEnabled
477 return (QAbstractSpinBox
.StepUpEnabled | QAbstractSpinBox
.StepDownEnabled
)
482 if self
.fBox
is not None:
485 def resizeEvent(self
, event
):
486 QAbstractSpinBox
.resizeEvent(self
, event
)
487 self
.slot_updateProgressBarGeometry()
490 def slot_comboBoxIndexChanged(self
, boxText
):
494 value
= float(boxText
.split(" - ", 1)[0])
495 lastScaleValue
= self
.fScalePoints
[-1]['value']
497 if value
== lastScaleValue
:
498 value
= self
.fMaximum
503 def slot_progressBarValueChanged(self
, value
):
507 if value
<= self
.fMinimum
:
508 realValue
= self
.fMinimum
509 elif value
>= self
.fMaximum
:
510 realValue
= self
.fMaximum
512 curStep
= int((value
- self
.fMinimum
) / self
.fStep
+ 0.5)
513 realValue
= self
.fMinimum
+ (self
.fStep
* curStep
)
515 if realValue
< self
.fMinimum
:
516 realValue
= self
.fMinimum
517 elif realValue
> self
.fMaximum
:
518 realValue
= self
.fMaximum
520 self
.setValue(realValue
)
523 def slot_showCustomMenu(self
):
524 clipboard
= QApplication
.instance().clipboard()
525 pasteText
= clipboard
.text()
530 pasteValue
= float(pasteText
)
535 actReset
= menu
.addAction(self
.tr("Reset (%f)" % self
.fDefault
))
536 actRandom
= menu
.addAction(self
.tr("Random"))
538 actCopy
= menu
.addAction(self
.tr("Copy (%f)" % self
.fValue
))
540 if pasteValue
is None:
541 actPaste
= menu
.addAction(self
.tr("Paste"))
542 actPaste
.setEnabled(False)
544 actPaste
= menu
.addAction(self
.tr("Paste (%f)" % pasteValue
))
548 actSet
= menu
.addAction(self
.tr("Set value..."))
551 actReset
.setEnabled(False)
552 actRandom
.setEnabled(False)
553 actPaste
.setEnabled(False)
554 actSet
.setEnabled(False)
556 actSel
= menu
.exec_(QCursor
.pos())
558 if actSel
== actReset
:
559 self
.setValue(self
.fDefault
)
561 elif actSel
== actRandom
:
562 value
= random() * (self
.fMaximum
- self
.fMinimum
) + self
.fMinimum
565 elif actSel
== actCopy
:
566 clipboard
.setText("%f" % self
.fValue
)
568 elif actSel
== actPaste
:
569 self
.setValue(pasteValue
)
571 elif actSel
== actSet
:
572 dialog
= CustomInputDialog(self
, self
.fName
, self
.fValue
, self
.fMinimum
, self
.fMaximum
,
573 self
.fStep
, self
.fStepSmall
, self
.fScalePoints
,
574 self
.fLabelPrefix
, self
.fLabelSuffix
)
576 value
= dialog
.returnValue()
580 def slot_updateProgressBarGeometry(self
):
581 geometry
= self
.lineEdit().geometry()
584 geometry
.adjust(-dx
, -dy
, dx
, dy
)
585 self
.fBar
.setGeometry(geometry
)
586 if self
.fUseScalePoints
:
587 self
.fBox
.setGeometry(geometry
)
589 def _getNearestScalePoint(self
, realValue
):
592 for i
in range(len(self
.fScalePoints
)):
593 scaleValue
= self
.fScalePoints
[i
]["value"]
595 finalValue
= scaleValue
597 srange1
= abs(realValue
- scaleValue
)
598 srange2
= abs(realValue
- finalValue
)
600 if srange2
> srange1
:
601 finalValue
= scaleValue
605 def _setScalePointValue(self
, value
):
606 value
= self
._getNearestScalePoint
(value
)
608 for i
in range(self
.fBox
.count()):
609 if float(self
.fBox
.itemText(i
).split(" - ", 1)[0]) == value
:
610 self
.fBox
.setCurrentIndex(i
)