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.

520 lines
18KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Svg Dial, 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 cos, floor, pi, sin, isnan
  20. from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QPointF, QRectF, QTimer, QSize
  21. from PyQt5.QtGui import QColor, QConicalGradient, QFont, QFontMetrics
  22. from PyQt5.QtGui import QLinearGradient, QPainter, QPainterPath, QPen
  23. from PyQt5.QtSvg import QSvgWidget
  24. from PyQt5.QtWidgets import QDial
  25. # ------------------------------------------------------------------------------------------------------------
  26. # Widget Class
  27. class ScalableDial(QDial):
  28. # enum CustomPaintMode
  29. CUSTOM_PAINT_MODE_NULL = 0 # default (NOTE: only this mode has label gradient)
  30. CUSTOM_PAINT_MODE_CARLA_WET = 1 # color blue-green gradient (reserved #3)
  31. CUSTOM_PAINT_MODE_CARLA_VOL = 2 # color blue (reserved #3)
  32. CUSTOM_PAINT_MODE_CARLA_L = 3 # color yellow (reserved #4)
  33. CUSTOM_PAINT_MODE_CARLA_R = 4 # color yellow (reserved #4)
  34. CUSTOM_PAINT_MODE_CARLA_PAN = 5 # color yellow (reserved #3)
  35. CUSTOM_PAINT_MODE_COLOR = 6 # color, selectable (reserved #3)
  36. CUSTOM_PAINT_MODE_ZITA = 7 # custom zita knob (reserved #6)
  37. CUSTOM_PAINT_MODE_NO_GRADIENT = 8 # skip label gradient
  38. # enum Orientation
  39. HORIZONTAL = 0
  40. VERTICAL = 1
  41. HOVER_MIN = 0
  42. HOVER_MAX = 9
  43. MODE_DEFAULT = 0
  44. MODE_LINEAR = 1
  45. # signals
  46. dragStateChanged = pyqtSignal(bool)
  47. realValueChanged = pyqtSignal(float)
  48. def __init__(self, parent, index=0):
  49. QDial.__init__(self, parent)
  50. self.fDialMode = self.MODE_LINEAR
  51. self.fMinimum = 0.0
  52. self.fMaximum = 1.0
  53. self.fRealValue = 0.0
  54. self.fPrecision = 10000
  55. self.fIsInteger = False
  56. self.fIsHovered = False
  57. self.fIsPressed = False
  58. self.fHoverStep = self.HOVER_MIN
  59. self.fLastDragPos = None
  60. self.fLastDragValue = 0.0
  61. self.fIndex = index
  62. self.fSvg = QSvgWidget(":/scalable/dial_03.svg")
  63. self.fSvgNum = "01"
  64. if self.fSvg.sizeHint().width() > self.fSvg.sizeHint().height():
  65. self.fSvgOrientation = self.HORIZONTAL
  66. else:
  67. self.fSvgOrientation = self.VERTICAL
  68. self.fLabel = ""
  69. self.fLabelPos = QPointF(0.0, 0.0)
  70. self.fLabelFont = QFont(self.font())
  71. self.fLabelFont.setPixelSize(8)
  72. self.fLabelWidth = 0
  73. self.fLabelHeight = 0
  74. if self.palette().window().color().lightness() > 100:
  75. # Light background
  76. c = self.palette().dark().color()
  77. self.fLabelGradientColor1 = c
  78. self.fLabelGradientColor2 = QColor(c.red(), c.green(), c.blue(), 0)
  79. self.fLabelGradientColorT = [self.palette().buttonText().color(), self.palette().mid().color()]
  80. else:
  81. # Dark background
  82. self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
  83. self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
  84. self.fLabelGradientColorT = [Qt.white, Qt.darkGray]
  85. self.fLabelGradient = QLinearGradient(0, 0, 0, 1)
  86. self.fLabelGradient.setColorAt(0.0, self.fLabelGradientColor1)
  87. self.fLabelGradient.setColorAt(0.6, self.fLabelGradientColor1)
  88. self.fLabelGradient.setColorAt(1.0, self.fLabelGradientColor2)
  89. self.fLabelGradientRect = QRectF(0.0, 0.0, 0.0, 0.0)
  90. self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_NULL
  91. self.fCustomPaintColor = QColor(0xff, 0xff, 0xff)
  92. self.updateSizes()
  93. # Fake internal value, custom precision
  94. QDial.setMinimum(self, 0)
  95. QDial.setMaximum(self, self.fPrecision)
  96. QDial.setValue(self, 0)
  97. self.valueChanged.connect(self.slot_valueChanged)
  98. def getIndex(self):
  99. return self.fIndex
  100. def getBaseSize(self):
  101. return self.fSvgBaseSize
  102. def forceWhiteLabelGradientText(self):
  103. self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
  104. self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
  105. self.fLabelGradientColorT = [Qt.white, Qt.darkGray]
  106. def setLabelColor(self, enabled, disabled):
  107. self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
  108. self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
  109. self.fLabelGradientColorT = [enabled, disabled]
  110. def updateSizes(self):
  111. self.fSvgWidth = self.fSvg.sizeHint().width()
  112. self.fSvgHeight = self.fSvg.sizeHint().height()
  113. if self.fSvgWidth < 1:
  114. self.fSvgWidth = 1
  115. if self.fSvgHeight < 1:
  116. self.fSvgHeight = 1
  117. if self.fSvgOrientation == self.HORIZONTAL:
  118. self.fSvgBaseSize = self.fSvgHeight
  119. self.fSvgLayersCount = self.fSvgWidth / self.fSvgHeight
  120. else:
  121. self.fSvgBaseSize = self.fSvgWidth
  122. self.fSvgLayersCount = self.fSvgHeight / self.fSvgWidth
  123. self.setMinimumSize(self.fSvgBaseSize, self.fSvgBaseSize + self.fLabelHeight + 5)
  124. self.setMaximumSize(self.fSvgBaseSize, self.fSvgBaseSize + self.fLabelHeight + 5)
  125. if not self.fLabel:
  126. self.fLabelHeight = 0
  127. self.fLabelWidth = 0
  128. return
  129. self.fLabelWidth = QFontMetrics(self.fLabelFont).width(self.fLabel)
  130. self.fLabelHeight = QFontMetrics(self.fLabelFont).height()
  131. self.fLabelPos.setX(float(self.fSvgBaseSize)/2.0 - float(self.fLabelWidth)/2.0)
  132. if self.fSvgNum in ("01", "02", "07", "08", "09", "10"):
  133. self.fLabelPos.setY(self.fSvgBaseSize + self.fLabelHeight)
  134. elif self.fSvgNum in ("11",):
  135. self.fLabelPos.setY(self.fSvgBaseSize + self.fLabelHeight*2/3)
  136. else:
  137. self.fLabelPos.setY(self.fSvgBaseSize + self.fLabelHeight/2)
  138. self.fLabelGradient.setStart(0, float(self.fSvgBaseSize)/2.0)
  139. self.fLabelGradient.setFinalStop(0, self.fSvgBaseSize + self.fLabelHeight + 5)
  140. self.fLabelGradientRect = QRectF(float(self.fSvgBaseSize)/8.0, float(self.fSvgBaseSize)/2.0,
  141. float(self.fSvgBaseSize*3)/4.0, self.fSvgBaseSize+self.fLabelHeight+5)
  142. def setCustomPaintMode(self, paintMode):
  143. if self.fCustomPaintMode == paintMode:
  144. return
  145. self.fCustomPaintMode = paintMode
  146. self.update()
  147. def setCustomPaintColor(self, color):
  148. if self.fCustomPaintColor == color:
  149. return
  150. self.fCustomPaintColor = color
  151. self.update()
  152. def setLabel(self, label):
  153. if self.fLabel == label:
  154. return
  155. self.fLabel = label
  156. self.updateSizes()
  157. self.update()
  158. def setIndex(self, index):
  159. self.fIndex = index
  160. def setSvg(self, SvgId):
  161. self.fSvgNum = "%02i" % SvgId
  162. self.fSvg.load(":/scalable/dial_%s%s.svg" % (self.fSvgNum, "" if self.isEnabled() else "d"))
  163. if self.fSvg.width() > self.fSvg.height():
  164. self.fSvgOrientation = self.HORIZONTAL
  165. else:
  166. self.fSvgOrientation = self.VERTICAL
  167. # special svg
  168. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
  169. # reserved for carla-wet, carla-vol, carla-pan and color
  170. if self.fSvgNum == "03":
  171. self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_COLOR
  172. # reserved for carla-L and carla-R
  173. elif self.fSvgNum == "04":
  174. self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_CARLA_L
  175. # reserved for zita
  176. elif self.fSvgNum == "06":
  177. self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_ZITA
  178. self.updateSizes()
  179. self.update()
  180. def setPrecision(self, value, isInteger):
  181. self.fPrecision = value
  182. self.fIsInteger = isInteger
  183. QDial.setMaximum(self, value)
  184. def setMinimum(self, value):
  185. self.fMinimum = value
  186. def setMaximum(self, value):
  187. self.fMaximum = value
  188. def setValue(self, value, emitSignal=False):
  189. if self.fRealValue == value or isnan(value):
  190. return
  191. if value <= self.fMinimum:
  192. qtValue = 0
  193. self.fRealValue = self.fMinimum
  194. elif value >= self.fMaximum:
  195. qtValue = self.fPrecision
  196. self.fRealValue = self.fMaximum
  197. else:
  198. qtValue = round(float(value - self.fMinimum) / float(self.fMaximum - self.fMinimum) * self.fPrecision)
  199. self.fRealValue = value
  200. # Block change signal, we'll handle it ourselves
  201. self.blockSignals(True)
  202. QDial.setValue(self, qtValue)
  203. self.blockSignals(False)
  204. if emitSignal:
  205. self.realValueChanged.emit(self.fRealValue)
  206. @pyqtSlot(int)
  207. def slot_valueChanged(self, value):
  208. self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum
  209. self.realValueChanged.emit(self.fRealValue)
  210. @pyqtSlot()
  211. def slot_updateSvg(self):
  212. self.seSvg(int(self.fSvgNum))
  213. def minimumSizeHint(self):
  214. return QSize(self.fSvgBaseSize, self.fSvgBaseSize)
  215. def sizeHint(self):
  216. return QSize(self.fSvgBaseSize, self.fSvgBaseSize)
  217. def changeEvent(self, event):
  218. QDial.changeEvent(self, event)
  219. # Force svg update if enabled state changes
  220. if event.type() == QEvent.EnabledChange:
  221. self.setSvg(int(self.fSvgNum))
  222. def enterEvent(self, event):
  223. self.fIsHovered = True
  224. if self.fHoverStep == self.HOVER_MIN:
  225. self.fHoverStep = self.HOVER_MIN + 1
  226. QDial.enterEvent(self, event)
  227. def leaveEvent(self, event):
  228. self.fIsHovered = False
  229. if self.fHoverStep == self.HOVER_MAX:
  230. self.fHoverStep = self.HOVER_MAX - 1
  231. QDial.leaveEvent(self, event)
  232. def mousePressEvent(self, event):
  233. if self.fDialMode == self.MODE_DEFAULT:
  234. return QDial.mousePressEvent(self, event)
  235. if event.button() == Qt.LeftButton:
  236. self.fIsPressed = True
  237. self.fLastDragPos = event.pos()
  238. self.fLastDragValue = self.fRealValue
  239. self.dragStateChanged.emit(True)
  240. def mouseMoveEvent(self, event):
  241. if self.fDialMode == self.MODE_DEFAULT:
  242. return QDial.mouseMoveEvent(self, event)
  243. if not self.fIsPressed:
  244. return
  245. range = (self.fMaximum - self.fMinimum) / 4.0
  246. pos = event.pos()
  247. dx = range * float(pos.x() - self.fLastDragPos.x()) / self.width()
  248. dy = range * float(pos.y() - self.fLastDragPos.y()) / self.height()
  249. value = self.fLastDragValue + dx - dy
  250. if value < self.fMinimum:
  251. value = self.fMinimum
  252. elif value > self.fMaximum:
  253. value = self.fMaximum
  254. elif self.fIsInteger:
  255. value = float(round(value))
  256. self.setValue(value, True)
  257. def mouseReleaseEvent(self, event):
  258. if self.fDialMode == self.MODE_DEFAULT:
  259. return QDial.mouseReleaseEvent(self, event)
  260. if self.fIsPressed:
  261. self.fIsPressed = False
  262. self.dragStateChanged.emit(False)
  263. def paintEvent(self, event):
  264. painter = QPainter(self)
  265. event.accept()
  266. painter.save()
  267. painter.setRenderHint(QPainter.Antialiasing, True)
  268. if self.fLabel:
  269. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
  270. painter.setPen(self.fLabelGradientColor2)
  271. painter.setBrush(self.fLabelGradient)
  272. painter.drawRect(self.fLabelGradientRect)
  273. painter.setFont(self.fLabelFont)
  274. painter.setPen(self.fLabelGradientColorT[0 if self.isEnabled() else 1])
  275. painter.drawText(self.fLabelPos, self.fLabel)
  276. if self.isEnabled():
  277. normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum)
  278. curLayer = int((self.fSvgLayersCount - 1) * normValue)
  279. if self.fSvgOrientation == self.HORIZONTAL:
  280. xpos = self.fSvgBaseSize * curLayer
  281. ypos = 0.0
  282. else:
  283. xpos = 0.0
  284. ypos = self.fSvgBaseSize * curLayer
  285. source = QRectF(xpos, ypos, self.fSvgBaseSize, self.fSvgBaseSize)
  286. self.fSvg.renderer().render(painter, source)
  287. # Custom knobs (Dry/Wet and Volume)
  288. if self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_WET, self.CUSTOM_PAINT_MODE_CARLA_VOL):
  289. # knob color
  290. colorGreen = QColor(0x5D, 0xE7, 0x3D).lighter(100 + self.fHoverStep*6)
  291. colorBlue = QColor(0x3E, 0xB8, 0xBE).lighter(100 + self.fHoverStep*6)
  292. # draw small circle
  293. ballRect = QRectF(8.0, 8.0, 15.0, 15.0)
  294. ballPath = QPainterPath()
  295. ballPath.addEllipse(ballRect)
  296. #painter.drawRect(ballRect)
  297. tmpValue = (0.375 + 0.75*normValue)
  298. ballValue = tmpValue - floor(tmpValue)
  299. ballPoint = ballPath.pointAtPercent(ballValue)
  300. # draw arc
  301. startAngle = 218*16
  302. spanAngle = -255*16*normValue
  303. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_WET:
  304. painter.setBrush(colorBlue)
  305. painter.setPen(QPen(colorBlue, 0))
  306. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  307. gradient = QConicalGradient(15.5, 15.5, -45)
  308. gradient.setColorAt(0.0, colorBlue)
  309. gradient.setColorAt(0.125, colorBlue)
  310. gradient.setColorAt(0.625, colorGreen)
  311. gradient.setColorAt(0.75, colorGreen)
  312. gradient.setColorAt(0.76, colorGreen)
  313. gradient.setColorAt(1.0, colorGreen)
  314. painter.setBrush(gradient)
  315. painter.setPen(QPen(gradient, 3))
  316. else:
  317. painter.setBrush(colorBlue)
  318. painter.setPen(QPen(colorBlue, 0))
  319. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  320. painter.setBrush(colorBlue)
  321. painter.setPen(QPen(colorBlue, 3))
  322. painter.drawArc(4.0, 4.0, 26.0, 26.0, startAngle, spanAngle)
  323. # Custom knobs (L and R)
  324. elif self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_L, self.CUSTOM_PAINT_MODE_CARLA_R):
  325. # knob color
  326. color = QColor(0xAD, 0xD5, 0x48).lighter(100 + self.fHoverStep*6)
  327. # draw small circle
  328. ballRect = QRectF(7.0, 8.0, 11.0, 12.0)
  329. ballPath = QPainterPath()
  330. ballPath.addEllipse(ballRect)
  331. #painter.drawRect(ballRect)
  332. tmpValue = (0.375 + 0.75*normValue)
  333. ballValue = tmpValue - floor(tmpValue)
  334. ballPoint = ballPath.pointAtPercent(ballValue)
  335. painter.setBrush(color)
  336. painter.setPen(QPen(color, 0))
  337. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.0, 2.0))
  338. # draw arc
  339. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L:
  340. startAngle = 218*16
  341. spanAngle = -255*16*normValue
  342. else:
  343. startAngle = 322.0*16
  344. spanAngle = 255.0*16*(1.0-normValue)
  345. painter.setPen(QPen(color, 2.5))
  346. painter.drawArc(3.5, 3.5, 22.0, 22.0, startAngle, spanAngle)
  347. # Custom knobs (Color)
  348. elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_COLOR:
  349. # knob color
  350. color = self.fCustomPaintColor.lighter(100 + self.fHoverStep*6)
  351. # draw small circle
  352. ballRect = QRectF(8.0, 8.0, 15.0, 15.0)
  353. ballPath = QPainterPath()
  354. ballPath.addEllipse(ballRect)
  355. tmpValue = (0.375 + 0.75*normValue)
  356. ballValue = tmpValue - floor(tmpValue)
  357. ballPoint = ballPath.pointAtPercent(ballValue)
  358. # draw arc
  359. startAngle = 218*16
  360. spanAngle = -255*16*normValue
  361. painter.setBrush(color)
  362. painter.setPen(QPen(color, 0))
  363. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  364. painter.setBrush(color)
  365. painter.setPen(QPen(color, 3))
  366. painter.drawArc(4.0, 4.8, 26.0, 26.0, startAngle, spanAngle)
  367. # Custom knobs (Zita)
  368. elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_ZITA:
  369. a = normValue * pi * 1.5 - 2.35
  370. r = 10.0
  371. x = 10.5
  372. y = 10.5
  373. x += r * sin(a)
  374. y -= r * cos(a)
  375. painter.setBrush(Qt.black)
  376. painter.setPen(QPen(Qt.black, 2))
  377. painter.drawLine(QPointF(11.0, 11.0), QPointF(x, y))
  378. # Custom knobs
  379. else:
  380. painter.restore()
  381. return
  382. if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX:
  383. self.fHoverStep += 1 if self.fIsHovered else -1
  384. QTimer.singleShot(20, self.update)
  385. else: # isEnabled()
  386. target = QRectF(0.0, 0.0, self.fSvgBaseSize, self.fSvgBaseSize)
  387. self.fSvg.renderer().render(painter, target)
  388. painter.restore()
  389. def resizeEvent(self, event):
  390. QDial.resizeEvent(self, event)
  391. self.updateSizes()
  392. # ------------------------------------------------------------------------------------------------------------
  393. # Main Testing
  394. if __name__ == '__main__':
  395. import sys
  396. from PyQt5.QtWidgets import QApplication
  397. import resources_rc
  398. app = QApplication(sys.argv)
  399. gui = ScalableDial(None)
  400. #gui.setEnabled(True)
  401. #gui.setEnabled(False)
  402. gui.setSvg(3)
  403. gui.setLabel("hahaha")
  404. gui.show()
  405. sys.exit(app.exec_())