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
19KB

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