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. # ------------------------------------------------------------------------------------------------------------
  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. dragStateChanged = pyqtSignal(bool)
  89. valueChanged = pyqtSignal(float)
  90. def __init__(self, parent):
  91. QProgressBar.__init__(self, parent)
  92. self.fLeftClickDown = False
  93. self.fIsInteger = False
  94. self.fIsReadOnly = False
  95. self.fMinimum = 0.0
  96. self.fMaximum = 1.0
  97. self.fInitiated = False
  98. self.fRealValue = 0.0
  99. self.fLastPaintedValue = None
  100. self.fCurrentPaintedText = ""
  101. self.fLabel = ""
  102. self.fName = ""
  103. self.fPreLabel = " "
  104. self.fTextCall = None
  105. self.fValueCall = None
  106. self.setFormat("(none)")
  107. # Fake internal value, 10'000 precision
  108. QProgressBar.setMinimum(self, 0)
  109. QProgressBar.setMaximum(self, 10000)
  110. QProgressBar.setValue(self, 0)
  111. def setMinimum(self, value):
  112. self.fMinimum = value
  113. def setMaximum(self, value):
  114. self.fMaximum = value
  115. def setValue(self, value):
  116. if self.fRealValue == value and self.fInitiated:
  117. return False
  118. self.fInitiated = True
  119. self.fRealValue = value
  120. div = float(self.fMaximum - self.fMinimum)
  121. if div == 0.0:
  122. print("Parameter '%s' division by 0 prevented (value:%f, min:%f, max:%f)" % (self.fName, value, self.fMaximum, 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 setLabel(self, label):
  135. self.fLabel = label.strip()
  136. if self.fLabel == "(coef)":
  137. self.fLabel = ""
  138. self.fPreLabel = "*"
  139. # force refresh of text value
  140. self.fLastPaintedValue = None
  141. self.update()
  142. def setName(self, name):
  143. self.fName = name
  144. def setReadOnly(self, yesNo):
  145. self.fIsReadOnly = yesNo
  146. def setTextCall(self, textCall):
  147. self.fTextCall = textCall
  148. def setValueCall(self, valueCall):
  149. self.fValueCall = valueCall
  150. def handleMouseEventPos(self, pos):
  151. if self.fIsReadOnly:
  152. return
  153. xper = float(pos.x()) / float(self.width())
  154. value = xper * (self.fMaximum - self.fMinimum) + self.fMinimum
  155. if self.fIsInteger:
  156. value = round(value)
  157. if value < self.fMinimum:
  158. value = self.fMinimum
  159. elif value > self.fMaximum:
  160. value = self.fMaximum
  161. if self.setValue(value):
  162. self.valueChanged.emit(value)
  163. def mousePressEvent(self, event):
  164. if self.fIsReadOnly:
  165. return
  166. if event.button() == Qt.LeftButton:
  167. self.handleMouseEventPos(event.pos())
  168. self.fLeftClickDown = True
  169. self.dragStateChanged.emit(True)
  170. else:
  171. self.fLeftClickDown = False
  172. QProgressBar.mousePressEvent(self, event)
  173. def mouseMoveEvent(self, event):
  174. if self.fIsReadOnly:
  175. return
  176. if self.fLeftClickDown:
  177. self.handleMouseEventPos(event.pos())
  178. QProgressBar.mouseMoveEvent(self, event)
  179. def mouseReleaseEvent(self, event):
  180. if self.fIsReadOnly:
  181. return
  182. self.fLeftClickDown = False
  183. self.dragStateChanged.emit(False)
  184. QProgressBar.mouseReleaseEvent(self, event)
  185. def paintEvent(self, event):
  186. if self.fTextCall is not None:
  187. if self.fLastPaintedValue != self.fRealValue:
  188. self.fLastPaintedValue = self.fRealValue
  189. self.fCurrentPaintedText = self.fTextCall()
  190. self.setFormat("%s %s %s" % (self.fPreLabel, self.fCurrentPaintedText, self.fLabel))
  191. elif self.fIsInteger:
  192. self.setFormat("%s %i %s" % (self.fPreLabel, int(self.fRealValue), self.fLabel))
  193. else:
  194. self.setFormat("%s %f %s" % (self.fPreLabel, self.fRealValue, self.fLabel))
  195. QProgressBar.paintEvent(self, event)
  196. # ------------------------------------------------------------------------------------------------------------
  197. # Special SpinBox used for parameters
  198. class ParamSpinBox(QAbstractSpinBox):
  199. # signals
  200. valueChanged = pyqtSignal(float)
  201. def __init__(self, parent):
  202. QAbstractSpinBox.__init__(self, parent)
  203. self.fName = ""
  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. if self.fStepSmall > value:
  255. self.fStepSmall = value
  256. if self.fStepLarge < value:
  257. self.fStepLarge = value
  258. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  259. def setStepSmall(self, value):
  260. if value == 0.0:
  261. self.fStepSmall = 0.0001
  262. elif value > self.fStep:
  263. self.fStepSmall = self.fStep
  264. else:
  265. self.fStepSmall = value
  266. self.fBar.fIsInteger = bool(self.fStepSmall == 1.0)
  267. def setStepLarge(self, value):
  268. if value == 0.0:
  269. self.fStepLarge = 0.1
  270. elif value < self.fStep:
  271. self.fStepLarge = self.fStep
  272. else:
  273. self.fStepLarge = value
  274. def setLabel(self, label):
  275. self.fBar.setLabel(label)
  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. 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