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.

613 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
  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):
  39. QDialog.__init__(self, parent)
  40. self.ui = ui_inputdialog_value.Ui_Dialog()
  41. self.ui.setupUi(self)
  42. decimals = countDecimalPoints(step, stepSmall)
  43. self.ui.label.setText(label)
  44. self.ui.doubleSpinBox.setDecimals(decimals)
  45. self.ui.doubleSpinBox.setRange(minimum, maximum)
  46. self.ui.doubleSpinBox.setSingleStep(step)
  47. self.ui.doubleSpinBox.setValue(current)
  48. self.ui.doubleSpinBox.setPrefix(prefix)
  49. self.ui.doubleSpinBox.setSuffix(suffix)
  50. if CARLA_OS_MAC:
  51. self.setWindowModality(Qt.WindowModal)
  52. if not scalePoints:
  53. self.ui.groupBox.setVisible(False)
  54. self.resize(200, 0)
  55. else:
  56. text = "<table>"
  57. for scalePoint in scalePoints:
  58. valuestr = ("%i" if decimals == 0 else "%f") % scalePoint['value']
  59. text += "<tr>"
  60. text += f"<td align='right'>{valuestr}</td><td align='left'> - {scalePoint['label']}</td>"
  61. text += "</tr>"
  62. text += "</table>"
  63. self.ui.textBrowser.setText(text)
  64. self.resize(200, 300)
  65. self.fRetValue = current
  66. self.adjustSize()
  67. self.accepted.connect(self.slot_setReturnValue)
  68. def returnValue(self):
  69. return self.fRetValue
  70. @pyqtSlot()
  71. def slot_setReturnValue(self):
  72. self.fRetValue = self.ui.doubleSpinBox.value()
  73. def done(self, r):
  74. QDialog.done(self, r)
  75. self.close()
  76. # ------------------------------------------------------------------------------------------------------------
  77. # ProgressBar used for ParamSpinBox
  78. class ParamProgressBar(QProgressBar):
  79. # signals
  80. dragStateChanged = pyqtSignal(bool)
  81. valueChanged = pyqtSignal(float)
  82. def __init__(self, parent):
  83. QProgressBar.__init__(self, parent)
  84. self.fLeftClickDown = False
  85. self.fIsInteger = False
  86. self.fIsReadOnly = False
  87. self.fMinimum = 0.0
  88. self.fMaximum = 1.0
  89. self.fInitiated = False
  90. self.fRealValue = 0.0
  91. self.fLastPaintedValue = None
  92. self.fCurrentPaintedText = ""
  93. self.fName = ""
  94. self.fLabelPrefix = ""
  95. self.fLabelSuffix = ""
  96. self.fTextCall = None
  97. self.fValueCall = None
  98. self.setFormat("(none)")
  99. # Fake internal value, 10'000 precision
  100. QProgressBar.setMinimum(self, 0)
  101. QProgressBar.setMaximum(self, 10000)
  102. QProgressBar.setValue(self, 0)
  103. def setMinimum(self, value):
  104. self.fMinimum = value
  105. def setMaximum(self, value):
  106. self.fMaximum = value
  107. def setValue(self, value):
  108. if (self.fRealValue == value or isnan(value)) and self.fInitiated:
  109. return False
  110. self.fInitiated = True
  111. self.fRealValue = value
  112. div = float(self.fMaximum - self.fMinimum)
  113. if div == 0.0:
  114. print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self.fName,
  115. value,
  116. self.fMaximum,
  117. self.fMinimum))
  118. vper = 1.0
  119. elif isnan(value):
  120. print("Parameter '%s' is NaN (value:%f, min:%f, max:%f)" % (self.fName,
  121. value,
  122. self.fMaximum,
  123. self.fMinimum))
  124. vper = 1.0
  125. else:
  126. vper = float(value - self.fMinimum) / div
  127. if vper < 0.0:
  128. vper = 0.0
  129. elif vper > 1.0:
  130. vper = 1.0
  131. if self.fValueCall is not None:
  132. self.fValueCall(value)
  133. QProgressBar.setValue(self, int(vper * 10000))
  134. return True
  135. def setSuffixes(self, prefix, suffix):
  136. self.fLabelPrefix = prefix
  137. self.fLabelSuffix = suffix
  138. # force refresh of text value
  139. self.fLastPaintedValue = None
  140. self.update()
  141. def setName(self, name):
  142. self.fName = name
  143. def setReadOnly(self, yesNo):
  144. self.fIsReadOnly = yesNo
  145. def setTextCall(self, textCall):
  146. self.fTextCall = textCall
  147. def setValueCall(self, valueCall):
  148. self.fValueCall = valueCall
  149. def handleMouseEventPos(self, pos):
  150. if self.fIsReadOnly:
  151. return
  152. xper = float(pos.x()) / float(self.width())
  153. value = xper * (self.fMaximum - self.fMinimum) + self.fMinimum
  154. if self.fIsInteger:
  155. value = round(value)
  156. if value < self.fMinimum:
  157. value = self.fMinimum
  158. elif value > self.fMaximum:
  159. value = self.fMaximum
  160. if self.setValue(value):
  161. self.valueChanged.emit(value)
  162. def mousePressEvent(self, event):
  163. if self.fIsReadOnly:
  164. return
  165. if event.button() == Qt.LeftButton:
  166. self.handleMouseEventPos(event.pos())
  167. self.fLeftClickDown = True
  168. self.dragStateChanged.emit(True)
  169. else:
  170. self.fLeftClickDown = False
  171. QProgressBar.mousePressEvent(self, event)
  172. def mouseMoveEvent(self, event):
  173. if self.fIsReadOnly:
  174. return
  175. if self.fLeftClickDown:
  176. self.handleMouseEventPos(event.pos())
  177. QProgressBar.mouseMoveEvent(self, event)
  178. def mouseReleaseEvent(self, event):
  179. if self.fIsReadOnly:
  180. return
  181. self.fLeftClickDown = False
  182. self.dragStateChanged.emit(False)
  183. QProgressBar.mouseReleaseEvent(self, event)
  184. def paintEvent(self, event):
  185. if self.fTextCall is not None:
  186. if self.fLastPaintedValue != self.fRealValue:
  187. self.fLastPaintedValue = self.fRealValue
  188. self.fCurrentPaintedText = self.fTextCall()
  189. self.setFormat("%s%s%s" % (self.fLabelPrefix, self.fCurrentPaintedText, self.fLabelSuffix))
  190. elif self.fIsInteger:
  191. self.setFormat("%s%i%s" % (self.fLabelPrefix, int(self.fRealValue), self.fLabelSuffix))
  192. else:
  193. self.setFormat("%s%f%s" % (self.fLabelPrefix, self.fRealValue, self.fLabelSuffix))
  194. QProgressBar.paintEvent(self, event)
  195. # ------------------------------------------------------------------------------------------------------------
  196. # Special SpinBox used for parameters
  197. class ParamSpinBox(QAbstractSpinBox):
  198. # signals
  199. valueChanged = pyqtSignal(float)
  200. def __init__(self, parent):
  201. QAbstractSpinBox.__init__(self, parent)
  202. self.fName = ""
  203. self.fLabelPrefix = ""
  204. self.fLabelSuffix = ""
  205. self.fMinimum = 0.0
  206. self.fMaximum = 1.0
  207. self.fDefault = 0.0
  208. self.fValue = None
  209. self.fStep = 0.01
  210. self.fStepSmall = 0.0001
  211. self.fStepLarge = 0.1
  212. self.fIsReadOnly = False
  213. self.fScalePoints = None
  214. self.fUseScalePoints = False
  215. self.fBar = ParamProgressBar(self)
  216. self.fBar.setContextMenuPolicy(Qt.NoContextMenu)
  217. #self.fBar.show()
  218. barPalette = self.fBar.palette()
  219. barPalette.setColor(QPalette.Window, Qt.transparent)
  220. self.fBar.setPalette(barPalette)
  221. self.fBox = None
  222. self.lineEdit().hide()
  223. self.customContextMenuRequested.connect(self.slot_showCustomMenu)
  224. self.fBar.valueChanged.connect(self.slot_progressBarValueChanged)
  225. self.dragStateChanged = self.fBar.dragStateChanged
  226. QTimer.singleShot(0, self.slot_updateProgressBarGeometry)
  227. def setDefault(self, value):
  228. value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
  229. self.fDefault = value
  230. def setMinimum(self, value):
  231. self.fMinimum = value
  232. self.fBar.setMinimum(value)
  233. def setMaximum(self, value):
  234. self.fMaximum = value
  235. self.fBar.setMaximum(value)
  236. def setValue(self, value):
  237. if not self.fIsReadOnly:
  238. value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
  239. if self.fBar.fIsInteger:
  240. value = round(value)
  241. if self.fValue == value:
  242. return False
  243. self.fValue = value
  244. self.fBar.setValue(value)
  245. if self.fUseScalePoints:
  246. self._setScalePointValue(value)
  247. self.valueChanged.emit(value)
  248. self.update()
  249. return True
  250. def setStep(self, value):
  251. if value == 0.0:
  252. self.fStep = 0.001
  253. else:
  254. self.fStep = value
  255. self.fStepSmall = min(self.fStepSmall, value)
  256. self.fStepLarge = max(self.fStepLarge, value)
  257. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  258. def setStepSmall(self, value):
  259. if value == 0.0:
  260. self.fStepSmall = 0.0001
  261. elif value > self.fStep:
  262. self.fStepSmall = self.fStep
  263. else:
  264. self.fStepSmall = value
  265. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  266. def setStepLarge(self, value):
  267. if value == 0.0:
  268. self.fStepLarge = 0.1
  269. elif value < self.fStep:
  270. self.fStepLarge = self.fStep
  271. else:
  272. self.fStepLarge = value
  273. def setLabel(self, label):
  274. prefix = ""
  275. suffix = label.strip()
  276. if suffix == "(coef)":
  277. prefix = "* "
  278. suffix = ""
  279. else:
  280. suffix = " " + suffix
  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 (%f)" % self.fDefault))
  410. actRandom = menu.addAction(self.tr("Random"))
  411. menu.addSeparator()
  412. actCopy = menu.addAction(self.tr("Copy (%f)" % 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 (%f)" % 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