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.

602 lines
18KB

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