Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

614 lines
19KB

  1. #!/usr/bin/env python3
  2. # SPDX-FileCopyrightText: 2011-2025 Filipe Coelho <falktx@falktx.com>
  3. # SPDX-License-Identifier: GPL-2.0-or-later
  4. # ------------------------------------------------------------------------------------------------------------
  5. # Imports (Global)
  6. from math import isnan, modf
  7. from random import random
  8. from qt_compat import qt_config
  9. if qt_config == 5:
  10. from PyQt5.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QTimer
  11. from PyQt5.QtGui import QCursor, QPalette
  12. from PyQt5.QtWidgets import QAbstractSpinBox, QApplication, QComboBox, QDialog, QMenu, QProgressBar
  13. elif qt_config == 6:
  14. from PyQt6.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QTimer
  15. from PyQt6.QtGui import QCursor, QPalette
  16. from PyQt6.QtWidgets import QAbstractSpinBox, QApplication, QComboBox, QDialog, QMenu, QProgressBar
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Custom)
  19. import ui_inputdialog_value
  20. from carla_backend import CARLA_OS_MAC
  21. from carla_shared import countDecimalPoints, getPrefixSuffix, strLim
  22. # ------------------------------------------------------------------------------------------------------------
  23. # Get a fixed value within min/max bounds
  24. def geFixedValue(name, value, minimum, maximum):
  25. if isnan(value):
  26. print("Parameter '%s' is NaN! - %f" % (name, value))
  27. return minimum
  28. if value < minimum:
  29. print("Parameter '%s' too low! - %f/%f" % (name, value, minimum))
  30. return minimum
  31. if value > maximum:
  32. print("Parameter '%s' too high! - %f/%f" % (name, value, maximum))
  33. return maximum
  34. return value
  35. # ------------------------------------------------------------------------------------------------------------
  36. # Custom InputDialog with Scale Points support
  37. class CustomInputDialog(QDialog):
  38. def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints, prefix, suffix, unit=""):
  39. QDialog.__init__(self, parent)
  40. self.ui = ui_inputdialog_value.Ui_Dialog()
  41. self.ui.setupUi(self)
  42. if not (unit == ""):
  43. prefix, suffix = getPrefixSuffix(unit)
  44. if unit == "%":
  45. decimals = 1
  46. else:
  47. decimals = countDecimalPoints(step, stepSmall)
  48. # self.ui.label.setText(label + " [" + strRound(self, minimum, decimals) + "..." + strRound(self, maximum, decimals) + "]")
  49. self.ui.label.setText(label + " [" + strLim(minimum) + "..." + strLim(maximum) + "]")
  50. self.ui.doubleSpinBox.setDecimals(decimals)
  51. self.ui.doubleSpinBox.setRange(minimum, maximum)
  52. self.ui.doubleSpinBox.setSingleStep(step)
  53. self.ui.doubleSpinBox.setValue(current)
  54. self.ui.doubleSpinBox.setPrefix(prefix)
  55. self.ui.doubleSpinBox.setSuffix(suffix)
  56. if CARLA_OS_MAC:
  57. self.setWindowModality(Qt.WindowModal)
  58. if not scalePoints:
  59. self.ui.groupBox.setVisible(False)
  60. self.resize(200, 0)
  61. else:
  62. text = "<table>"
  63. for scalePoint in scalePoints:
  64. valuestr = ("%i" if decimals == 0 else "%f") % scalePoint['value']
  65. text += "<tr>"
  66. text += f"<td align='right'>{valuestr}</td><td align='left'> - {scalePoint['label']}</td>"
  67. text += "</tr>"
  68. text += "</table>"
  69. self.ui.textBrowser.setText(text)
  70. self.resize(200, 300)
  71. self.fRetValue = current
  72. self.adjustSize()
  73. self.accepted.connect(self.slot_setReturnValue)
  74. def returnValue(self):
  75. return self.fRetValue
  76. @pyqtSlot()
  77. def slot_setReturnValue(self):
  78. self.fRetValue = self.ui.doubleSpinBox.value()
  79. def done(self, r):
  80. QDialog.done(self, r)
  81. self.close()
  82. # ------------------------------------------------------------------------------------------------------------
  83. # ProgressBar used for ParamSpinBox
  84. class ParamProgressBar(QProgressBar):
  85. # signals
  86. dragStateChanged = pyqtSignal(bool)
  87. valueChanged = pyqtSignal(float)
  88. def __init__(self, parent):
  89. QProgressBar.__init__(self, parent)
  90. self.fLeftClickDown = False
  91. self.fIsInteger = False
  92. self.fIsReadOnly = False
  93. self.fMinimum = 0.0
  94. self.fMaximum = 1.0
  95. self.fInitiated = False
  96. self.fRealValue = 0.0
  97. self.fLastPaintedValue = None
  98. self.fCurrentPaintedText = ""
  99. self.fName = ""
  100. self.fLabelPrefix = ""
  101. self.fLabelSuffix = ""
  102. self.fTextCall = None
  103. self.fValueCall = None
  104. self.setFormat("(none)")
  105. # Fake internal value, 10'000 precision
  106. QProgressBar.setMinimum(self, 0)
  107. QProgressBar.setMaximum(self, 10000)
  108. QProgressBar.setValue(self, 0)
  109. def setMinimum(self, value):
  110. self.fMinimum = value
  111. def setMaximum(self, value):
  112. self.fMaximum = value
  113. def setValue(self, value):
  114. if (self.fRealValue == value or isnan(value)) and self.fInitiated:
  115. return False
  116. self.fInitiated = True
  117. self.fRealValue = value
  118. div = float(self.fMaximum - self.fMinimum)
  119. if div == 0.0:
  120. print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self.fName,
  121. value,
  122. self.fMaximum,
  123. self.fMinimum))
  124. vper = 1.0
  125. elif isnan(value):
  126. print("Parameter '%s' is NaN (value:%f, min:%f, max:%f)" % (self.fName,
  127. value,
  128. self.fMaximum,
  129. self.fMinimum))
  130. vper = 1.0
  131. else:
  132. vper = float(value - self.fMinimum) / div
  133. if vper < 0.0:
  134. vper = 0.0
  135. elif vper > 1.0:
  136. vper = 1.0
  137. if self.fValueCall is not None:
  138. self.fValueCall(value)
  139. QProgressBar.setValue(self, int(vper * 10000))
  140. return True
  141. def setSuffixes(self, prefix, suffix):
  142. self.fLabelPrefix = prefix
  143. self.fLabelSuffix = suffix
  144. # force refresh of text value
  145. self.fLastPaintedValue = None
  146. self.update()
  147. def setName(self, name):
  148. self.fName = name
  149. def setReadOnly(self, yesNo):
  150. self.fIsReadOnly = yesNo
  151. def setTextCall(self, textCall):
  152. self.fTextCall = textCall
  153. def setValueCall(self, valueCall):
  154. self.fValueCall = valueCall
  155. def handleMouseEventPos(self, pos):
  156. if self.fIsReadOnly:
  157. return
  158. xper = float(pos.x()) / float(self.width())
  159. value = xper * (self.fMaximum - self.fMinimum) + self.fMinimum
  160. if self.fIsInteger:
  161. value = round(value)
  162. if value < self.fMinimum:
  163. value = self.fMinimum
  164. elif value > self.fMaximum:
  165. value = self.fMaximum
  166. if self.setValue(value):
  167. self.valueChanged.emit(value)
  168. def mousePressEvent(self, event):
  169. if self.fIsReadOnly:
  170. return
  171. if event.button() == Qt.LeftButton:
  172. self.handleMouseEventPos(event.pos())
  173. self.fLeftClickDown = True
  174. self.dragStateChanged.emit(True)
  175. else:
  176. self.fLeftClickDown = False
  177. QProgressBar.mousePressEvent(self, event)
  178. def mouseMoveEvent(self, event):
  179. if self.fIsReadOnly:
  180. return
  181. if self.fLeftClickDown:
  182. self.handleMouseEventPos(event.pos())
  183. QProgressBar.mouseMoveEvent(self, event)
  184. def mouseReleaseEvent(self, event):
  185. if self.fIsReadOnly:
  186. return
  187. self.fLeftClickDown = False
  188. self.dragStateChanged.emit(False)
  189. QProgressBar.mouseReleaseEvent(self, event)
  190. def paintEvent(self, event):
  191. if self.fTextCall is not None:
  192. if self.fLastPaintedValue != self.fRealValue:
  193. self.fLastPaintedValue = self.fRealValue
  194. self.fCurrentPaintedText = self.fTextCall()
  195. self.setFormat("%s%s%s" % (self.fLabelPrefix, self.fCurrentPaintedText, self.fLabelSuffix))
  196. elif self.fIsInteger:
  197. self.setFormat("%s%i%s" % (self.fLabelPrefix, int(self.fRealValue), self.fLabelSuffix))
  198. else:
  199. self.setFormat("%s%f%s" % (self.fLabelPrefix, self.fRealValue, self.fLabelSuffix))
  200. QProgressBar.paintEvent(self, event)
  201. # ------------------------------------------------------------------------------------------------------------
  202. # Special SpinBox used for parameters
  203. class ParamSpinBox(QAbstractSpinBox):
  204. # signals
  205. valueChanged = pyqtSignal(float)
  206. def __init__(self, parent):
  207. QAbstractSpinBox.__init__(self, parent)
  208. self.fName = ""
  209. self.fLabelPrefix = ""
  210. self.fLabelSuffix = ""
  211. self.fMinimum = 0.0
  212. self.fMaximum = 1.0
  213. self.fDefault = 0.0
  214. self.fValue = None
  215. self.fStep = 0.01
  216. self.fStepSmall = 0.0001
  217. self.fStepLarge = 0.1
  218. self.fIsReadOnly = False
  219. self.fScalePoints = None
  220. self.fUseScalePoints = False
  221. self.fBar = ParamProgressBar(self)
  222. self.fBar.setContextMenuPolicy(Qt.NoContextMenu)
  223. #self.fBar.show()
  224. barPalette = self.fBar.palette()
  225. barPalette.setColor(QPalette.Window, Qt.transparent)
  226. self.fBar.setPalette(barPalette)
  227. self.fBox = None
  228. self.lineEdit().hide()
  229. self.customContextMenuRequested.connect(self.slot_showCustomMenu)
  230. self.fBar.valueChanged.connect(self.slot_progressBarValueChanged)
  231. self.dragStateChanged = self.fBar.dragStateChanged
  232. QTimer.singleShot(0, self.slot_updateProgressBarGeometry)
  233. def setDefault(self, value):
  234. value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
  235. self.fDefault = value
  236. def setMinimum(self, value):
  237. self.fMinimum = value
  238. self.fBar.setMinimum(value)
  239. def setMaximum(self, value):
  240. self.fMaximum = value
  241. self.fBar.setMaximum(value)
  242. def setValue(self, value):
  243. if not self.fIsReadOnly:
  244. value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
  245. if self.fBar.fIsInteger:
  246. value = round(value)
  247. if self.fValue == value:
  248. return False
  249. self.fValue = value
  250. self.fBar.setValue(value)
  251. if self.fUseScalePoints:
  252. self._setScalePointValue(value)
  253. self.valueChanged.emit(value)
  254. self.update()
  255. return True
  256. def setStep(self, value):
  257. if value == 0.0:
  258. self.fStep = 0.001
  259. else:
  260. self.fStep = value
  261. self.fStepSmall = min(self.fStepSmall, value)
  262. self.fStepLarge = max(self.fStepLarge, value)
  263. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  264. def setStepSmall(self, value):
  265. if value == 0.0:
  266. self.fStepSmall = 0.0001
  267. elif value > self.fStep:
  268. self.fStepSmall = self.fStep
  269. else:
  270. self.fStepSmall = value
  271. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  272. def setStepLarge(self, value):
  273. if value == 0.0:
  274. self.fStepLarge = 0.1
  275. elif value < self.fStep:
  276. self.fStepLarge = self.fStep
  277. else:
  278. self.fStepLarge = value
  279. def setLabel(self, label):
  280. prefix, suffix = getPrefixSuffix(label)
  281. self.fLabelPrefix = prefix
  282. self.fLabelSuffix = suffix
  283. self.fBar.setSuffixes(prefix, suffix)
  284. def setName(self, name):
  285. self.fName = name
  286. self.fBar.setName(name)
  287. def setTextCallback(self, textCall):
  288. self.fBar.setTextCall(textCall)
  289. def setValueCallback(self, valueCall):
  290. self.fBar.setValueCall(valueCall)
  291. def setReadOnly(self, yesNo):
  292. self.fIsReadOnly = yesNo
  293. self.fBar.setReadOnly(yesNo)
  294. self.setButtonSymbols(QAbstractSpinBox.UpDownArrows if yesNo else QAbstractSpinBox.NoButtons)
  295. QAbstractSpinBox.setReadOnly(self, yesNo)
  296. # FIXME use change event
  297. def setEnabled(self, yesNo):
  298. self.fBar.setEnabled(yesNo)
  299. QAbstractSpinBox.setEnabled(self, yesNo)
  300. def setScalePoints(self, scalePoints, useScalePoints):
  301. if len(scalePoints) == 0:
  302. self.fScalePoints = None
  303. self.fUseScalePoints = False
  304. return
  305. self.fScalePoints = scalePoints
  306. self.fUseScalePoints = useScalePoints
  307. if not useScalePoints:
  308. return
  309. # Hide ProgressBar and create a ComboBox
  310. self.fBar.close()
  311. self.fBox = QComboBox(self)
  312. self.fBox.setContextMenuPolicy(Qt.NoContextMenu)
  313. #self.fBox.show()
  314. self.slot_updateProgressBarGeometry()
  315. # Add items, sorted
  316. boxItemValues = []
  317. for scalePoint in scalePoints:
  318. value = scalePoint['value']
  319. if self.fStep == 1.0:
  320. label = "%i - %s" % (int(value), scalePoint['label'])
  321. else:
  322. label = "%f - %s" % (value, scalePoint['label'])
  323. if len(boxItemValues) == 0:
  324. self.fBox.addItem(label)
  325. boxItemValues.append(value)
  326. else:
  327. if value < boxItemValues[0]:
  328. self.fBox.insertItem(0, label)
  329. boxItemValues.insert(0, value)
  330. elif value > boxItemValues[-1]:
  331. self.fBox.addItem(label)
  332. boxItemValues.append(value)
  333. else:
  334. for index in range(len(boxItemValues)):
  335. if value >= boxItemValues[index]:
  336. self.fBox.insertItem(index+1, label)
  337. boxItemValues.insert(index+1, value)
  338. break
  339. if self.fValue is not None:
  340. self._setScalePointValue(self.fValue)
  341. if QT_VERSION >= 0x60000:
  342. self.fBox.currentTextChanged.connect(self.slot_comboBoxIndexChanged)
  343. else:
  344. self.fBox.currentIndexChanged['QString'].connect(self.slot_comboBoxIndexChanged)
  345. def setToolTip(self, text):
  346. self.fBar.setToolTip(text)
  347. QAbstractSpinBox.setToolTip(self, text)
  348. def stepBy(self, steps):
  349. if steps == 0 or self.fValue is None:
  350. return
  351. value = self.fValue + (self.fStep * steps)
  352. if value < self.fMinimum:
  353. value = self.fMinimum
  354. elif value > self.fMaximum:
  355. value = self.fMaximum
  356. self.setValue(value)
  357. def stepEnabled(self):
  358. if self.fIsReadOnly or self.fValue is None:
  359. return QAbstractSpinBox.StepNone
  360. if self.fValue <= self.fMinimum:
  361. return QAbstractSpinBox.StepUpEnabled
  362. if self.fValue >= self.fMaximum:
  363. return QAbstractSpinBox.StepDownEnabled
  364. return (QAbstractSpinBox.StepUpEnabled | QAbstractSpinBox.StepDownEnabled)
  365. def updateAll(self):
  366. self.update()
  367. self.fBar.update()
  368. if self.fBox is not None:
  369. self.fBox.update()
  370. def resizeEvent(self, event):
  371. QAbstractSpinBox.resizeEvent(self, event)
  372. self.slot_updateProgressBarGeometry()
  373. @pyqtSlot(str)
  374. def slot_comboBoxIndexChanged(self, boxText):
  375. if self.fIsReadOnly:
  376. return
  377. value = float(boxText.split(" - ", 1)[0])
  378. lastScaleValue = self.fScalePoints[-1]['value']
  379. if value == lastScaleValue:
  380. value = self.fMaximum
  381. self.setValue(value)
  382. @pyqtSlot(float)
  383. def slot_progressBarValueChanged(self, value):
  384. if self.fIsReadOnly:
  385. return
  386. if value <= self.fMinimum:
  387. realValue = self.fMinimum
  388. elif value >= self.fMaximum:
  389. realValue = self.fMaximum
  390. else:
  391. curStep = int((value - self.fMinimum) / self.fStep + 0.5)
  392. realValue = self.fMinimum + (self.fStep * curStep)
  393. if realValue < self.fMinimum:
  394. realValue = self.fMinimum
  395. elif realValue > self.fMaximum:
  396. realValue = self.fMaximum
  397. self.setValue(realValue)
  398. @pyqtSlot()
  399. def slot_showCustomMenu(self):
  400. clipboard = QApplication.instance().clipboard()
  401. pasteText = clipboard.text()
  402. pasteValue = None
  403. if pasteText:
  404. try:
  405. pasteValue = float(pasteText)
  406. except:
  407. pass
  408. menu = QMenu(self)
  409. actReset = menu.addAction(self.tr("Reset (" + strLim(self.fDefault) + ")"))
  410. actRandom = menu.addAction(self.tr("Random"))
  411. menu.addSeparator()
  412. actCopy = menu.addAction(self.tr("Copy (" + strLim(self.fValue) + ")"))
  413. if pasteValue is None:
  414. actPaste = menu.addAction(self.tr("Paste"))
  415. actPaste.setEnabled(False)
  416. else:
  417. actPaste = menu.addAction(self.tr("Paste (" + strLim(pasteValue) + ")"))
  418. menu.addSeparator()
  419. actSet = menu.addAction(self.tr("Set value..."))
  420. if self.fIsReadOnly:
  421. actReset.setEnabled(False)
  422. actRandom.setEnabled(False)
  423. actPaste.setEnabled(False)
  424. actSet.setEnabled(False)
  425. actSel = menu.exec_(QCursor.pos())
  426. if actSel == actReset:
  427. self.setValue(self.fDefault)
  428. elif actSel == actRandom:
  429. value = random() * (self.fMaximum - self.fMinimum) + self.fMinimum
  430. self.setValue(value)
  431. elif actSel == actCopy:
  432. clipboard.setText("%f" % self.fValue)
  433. elif actSel == actPaste:
  434. self.setValue(pasteValue)
  435. elif actSel == actSet:
  436. dialog = CustomInputDialog(self, self.fName, self.fValue, self.fMinimum, self.fMaximum,
  437. self.fStep, self.fStepSmall, self.fScalePoints,
  438. self.fLabelPrefix, self.fLabelSuffix)
  439. if dialog.exec_():
  440. value = dialog.returnValue()
  441. self.setValue(value)
  442. @pyqtSlot()
  443. def slot_updateProgressBarGeometry(self):
  444. geometry = self.lineEdit().geometry()
  445. dx = geometry.x()-1
  446. dy = geometry.y()-1
  447. geometry.adjust(-dx, -dy, dx, dy)
  448. self.fBar.setGeometry(geometry)
  449. if self.fUseScalePoints:
  450. self.fBox.setGeometry(geometry)
  451. def _getNearestScalePoint(self, realValue):
  452. finalValue = 0.0
  453. for i in range(len(self.fScalePoints)):
  454. scaleValue = self.fScalePoints[i]["value"]
  455. if i == 0:
  456. finalValue = scaleValue
  457. else:
  458. srange1 = abs(realValue - scaleValue)
  459. srange2 = abs(realValue - finalValue)
  460. if srange2 > srange1:
  461. finalValue = scaleValue
  462. return finalValue
  463. def _setScalePointValue(self, value):
  464. value = self._getNearestScalePoint(value)
  465. for i in range(self.fBox.count()):
  466. if float(self.fBox.itemText(i).split(" - ", 1)[0]) == value:
  467. self.fBox.setCurrentIndex(i)
  468. break