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.

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