Cleanup
[carla.git] / source / frontend / widgets / paramspinbox.py
blob8d6c2dfe01a45acf60c2672e80001706ca908340
1 #!/usr/bin/env python3
2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
6 # Imports (Global)
8 from math import isnan, modf
9 from random import random
11 from qt_compat import qt_config
13 if qt_config == 5:
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
17 elif qt_config == 6:
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 # ------------------------------------------------------------------------------------------------------------
23 # Imports (Custom)
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):
33 if isnan(value):
34 print("Parameter '%s' is NaN! - %f" % (name, value))
35 return minimum
36 if value < minimum:
37 print("Parameter '%s' too low! - %f/%f" % (name, value, minimum))
38 return minimum
39 if value > maximum:
40 print("Parameter '%s' too high! - %f/%f" % (name, value, maximum))
41 return maximum
42 return value
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()
51 self.ui.setupUi(self)
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)
62 if MACOS:
63 self.setWindowModality(Qt.WindowModal)
65 if not scalePoints:
66 self.ui.groupBox.setVisible(False)
67 self.resize(200, 0)
68 else:
69 text = "<table>"
70 for scalePoint in scalePoints:
71 valuestr = ("%i" if decimals == 0 else "%f") % scalePoint['value']
72 text += "<tr>"
73 text += f"<td align='right'>{valuestr}</td><td align='left'> - {scalePoint['label']}</td>"
74 text += "</tr>"
75 text += "</table>"
76 self.ui.textBrowser.setText(text)
77 self.resize(200, 300)
79 self.fRetValue = current
80 self.adjustSize()
81 self.accepted.connect(self.slot_setReturnValue)
83 def returnValue(self):
84 return self.fRetValue
86 @pyqtSlot()
87 def slot_setReturnValue(self):
88 self.fRetValue = self.ui.doubleSpinBox.value()
90 def done(self, r):
91 QDialog.done(self, r)
92 self.close()
94 # ------------------------------------------------------------------------------------------------------------
95 # ProgressBar used for ParamSpinBox
97 class ParamProgressBar(QProgressBar):
98 # signals
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
109 self.fMinimum = 0.0
110 self.fMaximum = 1.0
111 self.fInitiated = False
112 self.fRealValue = 0.0
114 self.fLastPaintedValue = None
115 self.fCurrentPaintedText = ""
117 self.fName = ""
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:
138 return False
140 self.fInitiated = True
141 self.fRealValue = value
142 div = float(self.fMaximum - self.fMinimum)
144 if div == 0.0:
145 print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self.fName,
146 value,
147 self.fMaximum,
148 self.fMinimum))
149 vper = 1.0
150 elif isnan(value):
151 print("Parameter '%s' is NaN (value:%f, min:%f, max:%f)" % (self.fName,
152 value,
153 self.fMaximum,
154 self.fMinimum))
155 vper = 1.0
156 else:
157 vper = float(value - self.fMinimum) / div
159 if vper < 0.0:
160 vper = 0.0
161 elif vper > 1.0:
162 vper = 1.0
164 if self.fValueCall is not None:
165 self.fValueCall(value)
167 QProgressBar.setValue(self, int(vper * 10000))
168 return True
170 def setSuffixes(self, prefix, suffix):
171 self.fLabelPrefix = prefix
172 self.fLabelSuffix = suffix
174 # force refresh of text value
175 self.fLastPaintedValue = None
177 self.update()
179 def setName(self, name):
180 self.fName = 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):
192 if self.fIsReadOnly:
193 return
195 xper = float(pos.x()) / float(self.width())
196 value = xper * (self.fMaximum - self.fMinimum) + self.fMinimum
198 if self.fIsInteger:
199 value = round(value)
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):
210 if self.fIsReadOnly:
211 return
213 if event.button() == Qt.LeftButton:
214 self.handleMouseEventPos(event.pos())
215 self.fLeftClickDown = True
216 self.dragStateChanged.emit(True)
217 else:
218 self.fLeftClickDown = False
220 QProgressBar.mousePressEvent(self, event)
222 def mouseMoveEvent(self, event):
223 if self.fIsReadOnly:
224 return
226 if self.fLeftClickDown:
227 self.handleMouseEventPos(event.pos())
229 QProgressBar.mouseMoveEvent(self, event)
231 def mouseReleaseEvent(self, event):
232 if self.fIsReadOnly:
233 return
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))
249 else:
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):
258 # signals
259 valueChanged = pyqtSignal(float)
261 def __init__(self, parent):
262 QAbstractSpinBox.__init__(self, parent)
264 self.fName = ""
265 self.fLabelPrefix = ""
266 self.fLabelSuffix = ""
268 self.fMinimum = 0.0
269 self.fMaximum = 1.0
270 self.fDefault = 0.0
271 self.fValue = None
273 self.fStep = 0.01
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)
283 #self.fBar.show()
285 barPalette = self.fBar.palette()
286 barPalette.setColor(QPalette.Window, Qt.transparent)
287 self.fBar.setPalette(barPalette)
289 self.fBox = None
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:
317 value = round(value)
319 if self.fValue == value:
320 return False
322 self.fValue = value
323 self.fBar.setValue(value)
325 if self.fUseScalePoints:
326 self._setScalePointValue(value)
328 self.valueChanged.emit(value)
329 self.update()
331 return True
333 def setStep(self, value):
334 if value == 0.0:
335 self.fStep = 0.001
336 else:
337 self.fStep = 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):
345 if value == 0.0:
346 self.fStepSmall = 0.0001
347 elif value > self.fStep:
348 self.fStepSmall = self.fStep
349 else:
350 self.fStepSmall = value
352 self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
354 def setStepLarge(self, value):
355 if value == 0.0:
356 self.fStepLarge = 0.1
357 elif value < self.fStep:
358 self.fStepLarge = self.fStep
359 else:
360 self.fStepLarge = value
362 def setLabel(self, label):
363 prefix = ""
364 suffix = label.strip()
366 if suffix == "(coef)":
367 prefix = "* "
368 suffix = ""
369 else:
370 suffix = " " + suffix
372 self.fLabelPrefix = prefix
373 self.fLabelSuffix = suffix
374 self.fBar.setSuffixes(prefix, suffix)
376 def setName(self, name):
377 self.fName = 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
401 return
403 self.fScalePoints = scalePoints
404 self.fUseScalePoints = useScalePoints
406 if not useScalePoints:
407 return
409 # Hide ProgressBar and create a ComboBox
410 self.fBar.close()
411 self.fBox = QComboBox(self)
412 self.fBox.setContextMenuPolicy(Qt.NoContextMenu)
413 #self.fBox.show()
414 self.slot_updateProgressBarGeometry()
416 # Add items, sorted
417 boxItemValues = []
419 for scalePoint in scalePoints:
420 value = scalePoint['value']
422 if self.fStep == 1.0:
423 label = "%i - %s" % (int(value), scalePoint['label'])
424 else:
425 label = "%f - %s" % (value, scalePoint['label'])
427 if len(boxItemValues) == 0:
428 self.fBox.addItem(label)
429 boxItemValues.append(value)
431 else:
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)
438 else:
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)
443 break
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)
450 else:
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:
459 return
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
468 self.setValue(value)
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)
479 def updateAll(self):
480 self.update()
481 self.fBar.update()
482 if self.fBox is not None:
483 self.fBox.update()
485 def resizeEvent(self, event):
486 QAbstractSpinBox.resizeEvent(self, event)
487 self.slot_updateProgressBarGeometry()
489 @pyqtSlot(str)
490 def slot_comboBoxIndexChanged(self, boxText):
491 if self.fIsReadOnly:
492 return
494 value = float(boxText.split(" - ", 1)[0])
495 lastScaleValue = self.fScalePoints[-1]['value']
497 if value == lastScaleValue:
498 value = self.fMaximum
500 self.setValue(value)
502 @pyqtSlot(float)
503 def slot_progressBarValueChanged(self, value):
504 if self.fIsReadOnly:
505 return
507 if value <= self.fMinimum:
508 realValue = self.fMinimum
509 elif value >= self.fMaximum:
510 realValue = self.fMaximum
511 else:
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)
522 @pyqtSlot()
523 def slot_showCustomMenu(self):
524 clipboard = QApplication.instance().clipboard()
525 pasteText = clipboard.text()
526 pasteValue = None
528 if pasteText:
529 try:
530 pasteValue = float(pasteText)
531 except:
532 pass
534 menu = QMenu(self)
535 actReset = menu.addAction(self.tr("Reset (%f)" % self.fDefault))
536 actRandom = menu.addAction(self.tr("Random"))
537 menu.addSeparator()
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)
543 else:
544 actPaste = menu.addAction(self.tr("Paste (%f)" % pasteValue))
546 menu.addSeparator()
548 actSet = menu.addAction(self.tr("Set value..."))
550 if self.fIsReadOnly:
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
563 self.setValue(value)
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)
575 if dialog.exec_():
576 value = dialog.returnValue()
577 self.setValue(value)
579 @pyqtSlot()
580 def slot_updateProgressBarGeometry(self):
581 geometry = self.lineEdit().geometry()
582 dx = geometry.x()-1
583 dy = geometry.y()-1
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):
590 finalValue = 0.0
592 for i in range(len(self.fScalePoints)):
593 scaleValue = self.fScalePoints[i]["value"]
594 if i == 0:
595 finalValue = scaleValue
596 else:
597 srange1 = abs(realValue - scaleValue)
598 srange2 = abs(realValue - finalValue)
600 if srange2 > srange1:
601 finalValue = scaleValue
603 return finalValue
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)
611 break