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.

612 lines
19KB

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