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.

517 lines
18KB

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