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.

616 lines
19KB

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