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 Qt4 widget
  4. # Copyright (C) 2011-2014 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 (Config)
  19. from carla_config import *
  20. # ------------------------------------------------------------------------------------------------------------
  21. # Imports (Global)
  22. from math import isnan, modf
  23. from random import random
  24. if config_UseQt5:
  25. from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer
  26. from PyQt5.QtGui import QCursor, QPalette
  27. from PyQt5.QtWidgets import QAbstractSpinBox, QApplication, QComboBox, QDialog, QMenu, QProgressBar
  28. else:
  29. from PyQt4.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer
  30. from PyQt4.QtGui import QAbstractSpinBox, QApplication, QComboBox, QCursor, QDialog, QMenu, QProgressBar, QPalette
  31. # ------------------------------------------------------------------------------------------------------------
  32. # Imports (Custom)
  33. import ui_inputdialog_value
  34. # ------------------------------------------------------------------------------------------------------------
  35. # Get a fixed value within min/max bounds
  36. def geFixedValue(name, value, minimum, maximum):
  37. if isnan(value):
  38. print("Parameter '%s' is NaN! - %f" % (name, value))
  39. return minimum
  40. if value < minimum:
  41. print("Parameter '%s' too low! - %f/%f" % (name, value, minimum))
  42. return minimum
  43. if value > maximum:
  44. print("Parameter '%s' too high! - %f/%f" % (name, value, maximum))
  45. return maximum
  46. return value
  47. # ------------------------------------------------------------------------------------------------------------
  48. # Custom InputDialog with Scale Points support
  49. class CustomInputDialog(QDialog):
  50. def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints):
  51. QDialog.__init__(self, parent)
  52. self.ui = ui_inputdialog_value.Ui_Dialog()
  53. self.ui.setupUi(self)
  54. # calculate num decimals from stepSmall
  55. if stepSmall >= 1.0:
  56. decimals = 0
  57. elif step >= 1.0:
  58. decimals = 2
  59. else:
  60. decfrac, decwhole = modf(stepSmall)
  61. if "000" in str(decfrac):
  62. decfrac = round(decfrac, str(decfrac).find("000"))
  63. else:
  64. decfrac = round(decfrac, 12)
  65. decimals = abs(len(str(decfrac))-len(str(decwhole))-1)
  66. self.ui.label.setText(label)
  67. self.ui.doubleSpinBox.setDecimals(decimals)
  68. self.ui.doubleSpinBox.setRange(minimum, maximum)
  69. self.ui.doubleSpinBox.setSingleStep(step)
  70. self.ui.doubleSpinBox.setValue(current)
  71. if not scalePoints:
  72. self.ui.groupBox.setVisible(False)
  73. self.resize(200, 0)
  74. else:
  75. text = "<table>"
  76. for scalePoint in scalePoints:
  77. text += "<tr><td align='right'>%f</td><td align='left'> - %s</td></tr>" % (scalePoint['value'], scalePoint['label'])
  78. text += "</table>"
  79. self.ui.textBrowser.setText(text)
  80. self.resize(200, 300)
  81. self.fRetValue = current
  82. self.accepted.connect(self.slot_setReturnValue)
  83. def returnValue(self):
  84. return self.fRetValue
  85. @pyqtSlot()
  86. def slot_setReturnValue(self):
  87. self.fRetValue = self.ui.doubleSpinBox.value()
  88. def done(self, r):
  89. QDialog.done(self, r)
  90. self.close()
  91. # ------------------------------------------------------------------------------------------------------------
  92. # ProgressBar used for ParamSpinBox
  93. class ParamProgressBar(QProgressBar):
  94. # signals
  95. valueChanged = pyqtSignal(float)
  96. def __init__(self, parent):
  97. QProgressBar.__init__(self, parent)
  98. self.fLeftClickDown = False
  99. self.fIsInteger = False
  100. self.fIsReadOnly = False
  101. self.fMinimum = 0.0
  102. self.fMaximum = 1.0
  103. self.fInitiated = False
  104. self.fRealValue = 0.0
  105. self.fLastPaintedValue = None
  106. self.fCurrentPaintedText = ""
  107. self.fLabel = ""
  108. self.fName = ""
  109. self.fPreLabel = " "
  110. self.fTextCall = None
  111. self.fValueCall = None
  112. self.setFormat("(none)")
  113. # Fake internal value, 10'000 precision
  114. QProgressBar.setMinimum(self, 0)
  115. QProgressBar.setMaximum(self, 10000)
  116. QProgressBar.setValue(self, 0)
  117. def setMinimum(self, value):
  118. self.fMinimum = value
  119. def setMaximum(self, value):
  120. self.fMaximum = value
  121. def setValue(self, value):
  122. if self.fRealValue == value and self.fInitiated:
  123. return False
  124. self.fInitiated = True
  125. self.fRealValue = value
  126. div = float(self.fMaximum - self.fMinimum)
  127. if div == 0.0:
  128. print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self.fName, value, self.fMaximum, self.fMinimum))
  129. vper = 1.0
  130. else:
  131. vper = float(value - self.fMinimum) / div
  132. if vper < 0.0:
  133. vper = 0.0
  134. elif vper > 1.0:
  135. vper = 1.0
  136. if self.fValueCall is not None:
  137. self.fValueCall(value)
  138. QProgressBar.setValue(self, int(vper * 10000))
  139. return True
  140. def setLabel(self, label):
  141. self.fLabel = label.strip()
  142. if self.fLabel == "(coef)":
  143. self.fLabel = ""
  144. self.fPreLabel = "*"
  145. # force refresh of text value
  146. self.fLastPaintedValue = None
  147. self.update()
  148. def setName(self, name):
  149. self.fName = name
  150. def setReadOnly(self, yesNo):
  151. self.fIsReadOnly = yesNo
  152. def setTextCall(self, textCall):
  153. self.fTextCall = textCall
  154. def setValueCall(self, valueCall):
  155. self.fValueCall = valueCall
  156. def handleMouseEventPos(self, pos):
  157. if self.fIsReadOnly:
  158. return
  159. xper = float(pos.x()) / float(self.width())
  160. value = xper * (self.fMaximum - self.fMinimum) + self.fMinimum
  161. if self.fIsInteger:
  162. value = round(value)
  163. if value < self.fMinimum:
  164. value = self.fMinimum
  165. elif value > self.fMaximum:
  166. value = self.fMaximum
  167. if self.setValue(value):
  168. self.valueChanged.emit(value)
  169. def mousePressEvent(self, event):
  170. if self.fIsReadOnly:
  171. return
  172. if event.button() == Qt.LeftButton:
  173. self.handleMouseEventPos(event.pos())
  174. self.fLeftClickDown = 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. QProgressBar.mouseReleaseEvent(self, event)
  189. def paintEvent(self, event):
  190. if self.fTextCall is not None:
  191. if self.fLastPaintedValue != self.fRealValue:
  192. self.fLastPaintedValue = self.fRealValue
  193. self.fCurrentPaintedText = self.fTextCall()
  194. self.setFormat("%s %s %s" % (self.fPreLabel, self.fCurrentPaintedText, self.fLabel))
  195. elif self.fIsInteger:
  196. self.setFormat("%s %i %s" % (self.fPreLabel, int(self.fRealValue), self.fLabel))
  197. else:
  198. self.setFormat("%s %f %s" % (self.fPreLabel, self.fRealValue, self.fLabel))
  199. QProgressBar.paintEvent(self, event)
  200. # ------------------------------------------------------------------------------------------------------------
  201. # Special SpinBox used for parameters
  202. class ParamSpinBox(QAbstractSpinBox):
  203. # signals
  204. valueChanged = pyqtSignal(float)
  205. def __init__(self, parent):
  206. QAbstractSpinBox.__init__(self, parent)
  207. self.fName = ""
  208. self.fMinimum = 0.0
  209. self.fMaximum = 1.0
  210. self.fDefault = 0.0
  211. self.fValue = None
  212. self.fStep = 0.01
  213. self.fStepSmall = 0.0001
  214. self.fStepLarge = 0.1
  215. self.fIsReadOnly = False
  216. self.fScalePoints = None
  217. self.fUseScalePoints = False
  218. self.fBar = ParamProgressBar(self)
  219. self.fBar.setContextMenuPolicy(Qt.NoContextMenu)
  220. #self.fBar.show()
  221. barPalette = self.fBar.palette()
  222. barPalette.setColor(QPalette.Window, Qt.transparent)
  223. self.fBar.setPalette(barPalette)
  224. self.fBox = None
  225. self.lineEdit().hide()
  226. self.customContextMenuRequested.connect(self.slot_showCustomMenu)
  227. self.fBar.valueChanged.connect(self.slot_progressBarValueChanged)
  228. QTimer.singleShot(0, self.slot_updateProgressBarGeometry)
  229. def setDefault(self, value):
  230. value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
  231. self.fDefault = value
  232. def setMinimum(self, value):
  233. self.fMinimum = value
  234. self.fBar.setMinimum(value)
  235. def setMaximum(self, value):
  236. self.fMaximum = value
  237. self.fBar.setMaximum(value)
  238. def setValue(self, value):
  239. if not self.fIsReadOnly:
  240. value = geFixedValue(self.fName, value, self.fMinimum, self.fMaximum)
  241. if self.fBar.fIsInteger:
  242. value = round(value)
  243. if self.fValue == value:
  244. return False
  245. self.fValue = value
  246. self.fBar.setValue(value)
  247. if self.fUseScalePoints:
  248. self._setScalePointValue(value)
  249. self.valueChanged.emit(value)
  250. self.update()
  251. return True
  252. def setStep(self, value):
  253. if value == 0.0:
  254. self.fStep = 0.001
  255. else:
  256. self.fStep = value
  257. if self.fStepSmall > value:
  258. self.fStepSmall = value
  259. if self.fStepLarge < value:
  260. self.fStepLarge = value
  261. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  262. def setStepSmall(self, value):
  263. if value == 0.0:
  264. self.fStepSmall = 0.0001
  265. elif value > self.fStep:
  266. self.fStepSmall = self.fStep
  267. else:
  268. self.fStepSmall = value
  269. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  270. def setStepLarge(self, value):
  271. if value == 0.0:
  272. self.fStepLarge = 0.1
  273. elif value < self.fStep:
  274. self.fStepLarge = self.fStep
  275. else:
  276. self.fStepLarge = value
  277. def setLabel(self, label):
  278. self.fBar.setLabel(label)
  279. def setName(self, name):
  280. self.fName = name
  281. self.fBar.setName(name)
  282. def setTextCallback(self, textCall):
  283. self.fBar.setTextCall(textCall)
  284. def setValueCallback(self, valueCall):
  285. self.fBar.setValueCall(valueCall)
  286. def setReadOnly(self, yesNo):
  287. self.fIsReadOnly = yesNo
  288. self.fBar.setReadOnly(yesNo)
  289. self.setButtonSymbols(QAbstractSpinBox.UpDownArrows if yesNo else QAbstractSpinBox.NoButtons)
  290. QAbstractSpinBox.setReadOnly(self, yesNo)
  291. # FIXME use change event
  292. def setEnabled(self, yesNo):
  293. self.fBar.setEnabled(yesNo)
  294. QAbstractSpinBox.setEnabled(self, yesNo)
  295. def setScalePoints(self, scalePoints, useScalePoints):
  296. if len(scalePoints) == 0:
  297. self.fScalePoints = None
  298. self.fUseScalePoints = False
  299. return
  300. self.fScalePoints = scalePoints
  301. self.fUseScalePoints = useScalePoints
  302. if not useScalePoints:
  303. return
  304. # Hide ProgressBar and create a ComboBox
  305. self.fBar.close()
  306. self.fBox = QComboBox(self)
  307. self.fBox.setContextMenuPolicy(Qt.NoContextMenu)
  308. #self.fBox.show()
  309. self.slot_updateProgressBarGeometry()
  310. # Add items, sorted
  311. boxItemValues = []
  312. for scalePoint in scalePoints:
  313. value = scalePoint['value']
  314. if self.fStep == 1.0:
  315. label = "%i - %s" % (int(value), scalePoint['label'])
  316. else:
  317. label = "%f - %s" % (value, scalePoint['label'])
  318. if len(boxItemValues) == 0:
  319. self.fBox.addItem(label)
  320. boxItemValues.append(value)
  321. else:
  322. if value < boxItemValues[0]:
  323. self.fBox.insertItem(0, label)
  324. boxItemValues.insert(0, value)
  325. elif value > boxItemValues[-1]:
  326. self.fBox.addItem(label)
  327. boxItemValues.append(value)
  328. else:
  329. for index in range(len(boxItemValues)):
  330. if value >= boxItemValues[index]:
  331. self.fBox.insertItem(index+1, label)
  332. boxItemValues.insert(index+1, value)
  333. break
  334. if self.fValue is not None:
  335. self._setScalePointValue(self.fValue)
  336. self.fBox.currentIndexChanged['QString'].connect(self.slot_comboBoxIndexChanged)
  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. if dialog.exec_():
  428. value = dialog.returnValue()
  429. self.setValue(value)
  430. @pyqtSlot()
  431. def slot_updateProgressBarGeometry(self):
  432. geometry = self.lineEdit().geometry()
  433. dx = geometry.x()-1
  434. dy = geometry.y()-1
  435. geometry.adjust(-dx, -dy, dx, dy)
  436. self.fBar.setGeometry(geometry)
  437. if self.fUseScalePoints:
  438. self.fBox.setGeometry(geometry)
  439. def _getNearestScalePoint(self, realValue):
  440. finalValue = 0.0
  441. for i in range(len(self.fScalePoints)):
  442. scaleValue = self.fScalePoints[i]["value"]
  443. if i == 0:
  444. finalValue = scaleValue
  445. else:
  446. srange1 = abs(realValue - scaleValue)
  447. srange2 = abs(realValue - finalValue)
  448. if srange2 > srange1:
  449. finalValue = scaleValue
  450. return finalValue
  451. def _setScalePointValue(self, value):
  452. value = self._getNearestScalePoint(value)
  453. for i in range(self.fBox.count()):
  454. if float(self.fBox.itemText(i).split(" - ", 1)[0]) == value:
  455. self.fBox.setCurrentIndex(i)
  456. break