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.

544 lines
19KB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Scalable Dial, a custom Qt widget
  4. # Copyright (C) 2011-2020 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, QPixmap
  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.fImage = QSvgWidget(":/scalable/dial_03.svg")
  63. self.fImageNum = "01"
  64. if self.fImage.sizeHint().width() > self.fImage.sizeHint().height():
  65. self.fImageOrientation = self.HORIZONTAL
  66. else:
  67. self.fImageOrientation = 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.fImageBaseSize
  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. if isinstance(self.fImage, QPixmap):
  112. self.fImageWidth = self.fImage.width()
  113. self.fImageHeight = self.fImage.height()
  114. else:
  115. self.fImageWidth = self.fImage.sizeHint().width()
  116. self.fImageHeight = self.fImage.sizeHint().height()
  117. if self.fImageWidth < 1:
  118. self.fImageWidth = 1
  119. if self.fImageHeight < 1:
  120. self.fImageHeight = 1
  121. if self.fImageOrientation == self.HORIZONTAL:
  122. self.fImageBaseSize = self.fImageHeight
  123. self.fImageLayersCount = self.fImageWidth / self.fImageHeight
  124. else:
  125. self.fImageBaseSize = self.fImageWidth
  126. self.fImageLayersCount = self.fImageHeight / self.fImageWidth
  127. self.setMinimumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5)
  128. self.setMaximumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5)
  129. if not self.fLabel:
  130. self.fLabelHeight = 0
  131. self.fLabelWidth = 0
  132. return
  133. self.fLabelWidth = QFontMetrics(self.fLabelFont).width(self.fLabel)
  134. self.fLabelHeight = QFontMetrics(self.fLabelFont).height()
  135. self.fLabelPos.setX(float(self.fImageBaseSize)/2.0 - float(self.fLabelWidth)/2.0)
  136. if self.fImageNum in ("01", "02", "07", "08", "09", "10"):
  137. self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight)
  138. elif self.fImageNum in ("11",):
  139. self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight*2/3)
  140. else:
  141. self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight/2)
  142. self.fLabelGradient.setStart(0, float(self.fImageBaseSize)/2.0)
  143. self.fLabelGradient.setFinalStop(0, self.fImageBaseSize + self.fLabelHeight + 5)
  144. self.fLabelGradientRect = QRectF(float(self.fImageBaseSize)/8.0, float(self.fImageBaseSize)/2.0,
  145. float(self.fImageBaseSize*3)/4.0, self.fImageBaseSize+self.fLabelHeight+5)
  146. def setCustomPaintMode(self, paintMode):
  147. if self.fCustomPaintMode == paintMode:
  148. return
  149. self.fCustomPaintMode = paintMode
  150. self.update()
  151. def setCustomPaintColor(self, color):
  152. if self.fCustomPaintColor == color:
  153. return
  154. self.fCustomPaintColor = color
  155. self.update()
  156. def setLabel(self, label):
  157. if self.fLabel == label:
  158. return
  159. self.fLabel = label
  160. self.updateSizes()
  161. self.update()
  162. def setIndex(self, index):
  163. self.fIndex = index
  164. def setImage(self, imageId):
  165. self.fImageNum = "%02i" % imageId
  166. if imageId in (6,7,8,9,10,11,12,13):
  167. img = ":/bitmaps/dial_%s%s.png" % (self.fImageNum, "" if self.isEnabled() else "d")
  168. else:
  169. img = ":/scalable/dial_%s%s.svg" % (self.fImageNum, "" if self.isEnabled() else "d")
  170. if img.endswith(".png"):
  171. if not isinstance(self.fImage, QPixmap):
  172. self.fImage = QPixmap()
  173. else:
  174. if not isinstance(self.fImage, QSvgWidget):
  175. self.fImage = QSvgWidget()
  176. self.fImage.load(img)
  177. if self.fImage.width() > self.fImage.height():
  178. self.fImageOrientation = self.HORIZONTAL
  179. else:
  180. self.fImageOrientation = self.VERTICAL
  181. # special svgs
  182. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
  183. # reserved for carla-wet, carla-vol, carla-pan and color
  184. if self.fImageNum == "03":
  185. self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_COLOR
  186. # reserved for carla-L and carla-R
  187. elif self.fImageNum == "04":
  188. self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_CARLA_L
  189. # reserved for zita
  190. elif self.fImageNum == "06":
  191. self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_ZITA
  192. self.updateSizes()
  193. self.update()
  194. def setPrecision(self, value, isInteger):
  195. self.fPrecision = value
  196. self.fIsInteger = isInteger
  197. QDial.setMaximum(self, value)
  198. def setMinimum(self, value):
  199. self.fMinimum = value
  200. def setMaximum(self, value):
  201. self.fMaximum = value
  202. def setValue(self, value, emitSignal=False):
  203. if self.fRealValue == value or isnan(value):
  204. return
  205. if value <= self.fMinimum:
  206. qtValue = 0
  207. self.fRealValue = self.fMinimum
  208. elif value >= self.fMaximum:
  209. qtValue = self.fPrecision
  210. self.fRealValue = self.fMaximum
  211. else:
  212. qtValue = round(float(value - self.fMinimum) / float(self.fMaximum - self.fMinimum) * self.fPrecision)
  213. self.fRealValue = value
  214. # Block change signal, we'll handle it ourselves
  215. self.blockSignals(True)
  216. QDial.setValue(self, qtValue)
  217. self.blockSignals(False)
  218. if emitSignal:
  219. self.realValueChanged.emit(self.fRealValue)
  220. @pyqtSlot(int)
  221. def slot_valueChanged(self, value):
  222. self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum
  223. self.realValueChanged.emit(self.fRealValue)
  224. @pyqtSlot()
  225. def slot_updateImage(self):
  226. self.setImage(int(self.fImageNum))
  227. def minimumSizeHint(self):
  228. return QSize(self.fImageBaseSize, self.fImageBaseSize)
  229. def sizeHint(self):
  230. return QSize(self.fImageBaseSize, self.fImageBaseSize)
  231. def changeEvent(self, event):
  232. QDial.changeEvent(self, event)
  233. # Force svg update if enabled state changes
  234. if event.type() == QEvent.EnabledChange:
  235. self.slot_updateImage()
  236. def enterEvent(self, event):
  237. self.fIsHovered = True
  238. if self.fHoverStep == self.HOVER_MIN:
  239. self.fHoverStep = self.HOVER_MIN + 1
  240. QDial.enterEvent(self, event)
  241. def leaveEvent(self, event):
  242. self.fIsHovered = False
  243. if self.fHoverStep == self.HOVER_MAX:
  244. self.fHoverStep = self.HOVER_MAX - 1
  245. QDial.leaveEvent(self, event)
  246. def mousePressEvent(self, event):
  247. if self.fDialMode == self.MODE_DEFAULT:
  248. return QDial.mousePressEvent(self, event)
  249. if event.button() == Qt.LeftButton:
  250. self.fIsPressed = True
  251. self.fLastDragPos = event.pos()
  252. self.fLastDragValue = self.fRealValue
  253. self.dragStateChanged.emit(True)
  254. def mouseMoveEvent(self, event):
  255. if self.fDialMode == self.MODE_DEFAULT:
  256. return QDial.mouseMoveEvent(self, event)
  257. if not self.fIsPressed:
  258. return
  259. range = (self.fMaximum - self.fMinimum) / 4.0
  260. pos = event.pos()
  261. dx = range * float(pos.x() - self.fLastDragPos.x()) / self.width()
  262. dy = range * float(pos.y() - self.fLastDragPos.y()) / self.height()
  263. value = self.fLastDragValue + dx - dy
  264. if value < self.fMinimum:
  265. value = self.fMinimum
  266. elif value > self.fMaximum:
  267. value = self.fMaximum
  268. elif self.fIsInteger:
  269. value = float(round(value))
  270. self.setValue(value, True)
  271. def mouseReleaseEvent(self, event):
  272. if self.fDialMode == self.MODE_DEFAULT:
  273. return QDial.mouseReleaseEvent(self, event)
  274. if self.fIsPressed:
  275. self.fIsPressed = False
  276. self.dragStateChanged.emit(False)
  277. def paintEvent(self, event):
  278. painter = QPainter(self)
  279. event.accept()
  280. painter.save()
  281. painter.setRenderHint(QPainter.Antialiasing, True)
  282. if self.fLabel:
  283. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
  284. painter.setPen(self.fLabelGradientColor2)
  285. painter.setBrush(self.fLabelGradient)
  286. painter.drawRect(self.fLabelGradientRect)
  287. painter.setFont(self.fLabelFont)
  288. painter.setPen(self.fLabelGradientColorT[0 if self.isEnabled() else 1])
  289. painter.drawText(self.fLabelPos, self.fLabel)
  290. if self.isEnabled():
  291. normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum)
  292. curLayer = int((self.fImageLayersCount - 1) * normValue)
  293. if self.fImageOrientation == self.HORIZONTAL:
  294. xpos = self.fImageBaseSize * curLayer
  295. ypos = 0.0
  296. else:
  297. xpos = 0.0
  298. ypos = self.fImageBaseSize * curLayer
  299. source = QRectF(xpos, ypos, self.fImageBaseSize, self.fImageBaseSize)
  300. if isinstance(self.fImage, QPixmap):
  301. target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize)
  302. painter.drawPixmap(target, self.fImage, source)
  303. else:
  304. self.fImage.renderer().render(painter, source)
  305. # Custom knobs (Dry/Wet and Volume)
  306. if self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_WET, self.CUSTOM_PAINT_MODE_CARLA_VOL):
  307. # knob color
  308. colorGreen = QColor(0x5D, 0xE7, 0x3D).lighter(100 + self.fHoverStep*6)
  309. colorBlue = QColor(0x3E, 0xB8, 0xBE).lighter(100 + self.fHoverStep*6)
  310. # draw small circle
  311. ballRect = QRectF(8.0, 8.0, 15.0, 15.0)
  312. ballPath = QPainterPath()
  313. ballPath.addEllipse(ballRect)
  314. #painter.drawRect(ballRect)
  315. tmpValue = (0.375 + 0.75*normValue)
  316. ballValue = tmpValue - floor(tmpValue)
  317. ballPoint = ballPath.pointAtPercent(ballValue)
  318. # draw arc
  319. startAngle = 218*16
  320. spanAngle = -255*16*normValue
  321. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_WET:
  322. painter.setBrush(colorBlue)
  323. painter.setPen(QPen(colorBlue, 0))
  324. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  325. gradient = QConicalGradient(15.5, 15.5, -45)
  326. gradient.setColorAt(0.0, colorBlue)
  327. gradient.setColorAt(0.125, colorBlue)
  328. gradient.setColorAt(0.625, colorGreen)
  329. gradient.setColorAt(0.75, colorGreen)
  330. gradient.setColorAt(0.76, colorGreen)
  331. gradient.setColorAt(1.0, colorGreen)
  332. painter.setBrush(gradient)
  333. painter.setPen(QPen(gradient, 3))
  334. else:
  335. painter.setBrush(colorBlue)
  336. painter.setPen(QPen(colorBlue, 0))
  337. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  338. painter.setBrush(colorBlue)
  339. painter.setPen(QPen(colorBlue, 3))
  340. painter.drawArc(4.0, 4.0, 26.0, 26.0, startAngle, spanAngle)
  341. # Custom knobs (L and R)
  342. elif self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_L, self.CUSTOM_PAINT_MODE_CARLA_R):
  343. # knob color
  344. color = QColor(0xAD, 0xD5, 0x48).lighter(100 + self.fHoverStep*6)
  345. # draw small circle
  346. ballRect = QRectF(7.0, 8.0, 11.0, 12.0)
  347. ballPath = QPainterPath()
  348. ballPath.addEllipse(ballRect)
  349. #painter.drawRect(ballRect)
  350. tmpValue = (0.375 + 0.75*normValue)
  351. ballValue = tmpValue - floor(tmpValue)
  352. ballPoint = ballPath.pointAtPercent(ballValue)
  353. painter.setBrush(color)
  354. painter.setPen(QPen(color, 0))
  355. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.0, 2.0))
  356. # draw arc
  357. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L:
  358. startAngle = 218*16
  359. spanAngle = -255*16*normValue
  360. else:
  361. startAngle = 322.0*16
  362. spanAngle = 255.0*16*(1.0-normValue)
  363. painter.setPen(QPen(color, 2.5))
  364. painter.drawArc(3.5, 3.5, 22.0, 22.0, startAngle, spanAngle)
  365. # Custom knobs (Color)
  366. elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_COLOR:
  367. # knob color
  368. color = self.fCustomPaintColor.lighter(100 + self.fHoverStep*6)
  369. # draw small circle
  370. ballRect = QRectF(8.0, 8.0, 15.0, 15.0)
  371. ballPath = QPainterPath()
  372. ballPath.addEllipse(ballRect)
  373. tmpValue = (0.375 + 0.75*normValue)
  374. ballValue = tmpValue - floor(tmpValue)
  375. ballPoint = ballPath.pointAtPercent(ballValue)
  376. # draw arc
  377. startAngle = 218*16
  378. spanAngle = -255*16*normValue
  379. painter.setBrush(color)
  380. painter.setPen(QPen(color, 0))
  381. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  382. painter.setBrush(color)
  383. painter.setPen(QPen(color, 3))
  384. painter.drawArc(4.0, 4.8, 26.0, 26.0, startAngle, spanAngle)
  385. # Custom knobs (Zita)
  386. elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_ZITA:
  387. a = normValue * pi * 1.5 - 2.35
  388. r = 10.0
  389. x = 10.5
  390. y = 10.5
  391. x += r * sin(a)
  392. y -= r * cos(a)
  393. painter.setBrush(Qt.black)
  394. painter.setPen(QPen(Qt.black, 2))
  395. painter.drawLine(QPointF(11.0, 11.0), QPointF(x, y))
  396. # Custom knobs
  397. else:
  398. painter.restore()
  399. return
  400. if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX:
  401. self.fHoverStep += 1 if self.fIsHovered else -1
  402. QTimer.singleShot(20, self.update)
  403. else: # isEnabled()
  404. target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize)
  405. if isinstance(self.fImage, QPixmap):
  406. painter.drawPixmap(target, self.fImage, target)
  407. else:
  408. self.fImage.renderer().render(painter, target)
  409. painter.restore()
  410. def resizeEvent(self, event):
  411. QDial.resizeEvent(self, event)
  412. self.updateSizes()
  413. # ------------------------------------------------------------------------------------------------------------
  414. # Main Testing
  415. if __name__ == '__main__':
  416. import sys
  417. from PyQt5.QtWidgets import QApplication
  418. import resources_rc
  419. app = QApplication(sys.argv)
  420. gui = ScalableDial(None)
  421. #gui.setEnabled(True)
  422. #gui.setEnabled(False)
  423. gui.setSvg(3)
  424. gui.setLabel("hahaha")
  425. gui.show()
  426. sys.exit(app.exec_())