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.

scalabledial.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  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 (2,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, int(value))
  198. def setMinimum(self, value):
  199. self.fMinimum = value
  200. def setMaximum(self, value):
  201. self.fMaximum = value
  202. def rvalue(self):
  203. return self.fRealValue
  204. def setValue(self, value, emitSignal=False):
  205. if self.fRealValue == value or isnan(value):
  206. return
  207. if value <= self.fMinimum:
  208. qtValue = 0
  209. self.fRealValue = self.fMinimum
  210. elif value >= self.fMaximum:
  211. qtValue = int(self.fPrecision)
  212. self.fRealValue = self.fMaximum
  213. else:
  214. qtValue = round(float(value - self.fMinimum) / float(self.fMaximum - self.fMinimum) * self.fPrecision)
  215. self.fRealValue = value
  216. # Block change signal, we'll handle it ourselves
  217. self.blockSignals(True)
  218. QDial.setValue(self, qtValue)
  219. self.blockSignals(False)
  220. if emitSignal:
  221. self.realValueChanged.emit(self.fRealValue)
  222. @pyqtSlot(int)
  223. def slot_valueChanged(self, value):
  224. self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum
  225. self.realValueChanged.emit(self.fRealValue)
  226. @pyqtSlot()
  227. def slot_updateImage(self):
  228. self.setImage(int(self.fImageNum))
  229. def minimumSizeHint(self):
  230. return QSize(self.fImageBaseSize, self.fImageBaseSize)
  231. def sizeHint(self):
  232. return QSize(self.fImageBaseSize, self.fImageBaseSize)
  233. def changeEvent(self, event):
  234. QDial.changeEvent(self, event)
  235. # Force svg update if enabled state changes
  236. if event.type() == QEvent.EnabledChange:
  237. self.slot_updateImage()
  238. def enterEvent(self, event):
  239. self.fIsHovered = True
  240. if self.fHoverStep == self.HOVER_MIN:
  241. self.fHoverStep = self.HOVER_MIN + 1
  242. QDial.enterEvent(self, event)
  243. def leaveEvent(self, event):
  244. self.fIsHovered = False
  245. if self.fHoverStep == self.HOVER_MAX:
  246. self.fHoverStep = self.HOVER_MAX - 1
  247. QDial.leaveEvent(self, event)
  248. def mousePressEvent(self, event):
  249. if self.fDialMode == self.MODE_DEFAULT:
  250. return QDial.mousePressEvent(self, event)
  251. if event.button() == Qt.LeftButton:
  252. self.fIsPressed = True
  253. self.fLastDragPos = event.pos()
  254. self.fLastDragValue = self.fRealValue
  255. self.dragStateChanged.emit(True)
  256. def mouseMoveEvent(self, event):
  257. if self.fDialMode == self.MODE_DEFAULT:
  258. return QDial.mouseMoveEvent(self, event)
  259. if not self.fIsPressed:
  260. return
  261. range = (self.fMaximum - self.fMinimum) / 4.0
  262. pos = event.pos()
  263. dx = range * float(pos.x() - self.fLastDragPos.x()) / self.width()
  264. dy = range * float(pos.y() - self.fLastDragPos.y()) / self.height()
  265. value = self.fLastDragValue + dx - dy
  266. if value < self.fMinimum:
  267. value = self.fMinimum
  268. elif value > self.fMaximum:
  269. value = self.fMaximum
  270. elif self.fIsInteger:
  271. value = float(round(value))
  272. self.setValue(value, True)
  273. def mouseReleaseEvent(self, event):
  274. if self.fDialMode == self.MODE_DEFAULT:
  275. return QDial.mouseReleaseEvent(self, event)
  276. if self.fIsPressed:
  277. self.fIsPressed = False
  278. self.dragStateChanged.emit(False)
  279. def paintEvent(self, event):
  280. painter = QPainter(self)
  281. event.accept()
  282. painter.save()
  283. painter.setRenderHint(QPainter.Antialiasing, True)
  284. if self.fLabel:
  285. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
  286. painter.setPen(self.fLabelGradientColor2)
  287. painter.setBrush(self.fLabelGradient)
  288. painter.drawRect(self.fLabelGradientRect)
  289. painter.setFont(self.fLabelFont)
  290. painter.setPen(self.fLabelGradientColorT[0 if self.isEnabled() else 1])
  291. painter.drawText(self.fLabelPos, self.fLabel)
  292. if self.isEnabled():
  293. normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum)
  294. curLayer = int((self.fImageLayersCount - 1) * normValue)
  295. if self.fImageOrientation == self.HORIZONTAL:
  296. xpos = self.fImageBaseSize * curLayer
  297. ypos = 0.0
  298. else:
  299. xpos = 0.0
  300. ypos = self.fImageBaseSize * curLayer
  301. source = QRectF(xpos, ypos, self.fImageBaseSize, self.fImageBaseSize)
  302. if isinstance(self.fImage, QPixmap):
  303. target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize)
  304. painter.drawPixmap(target, self.fImage, source)
  305. else:
  306. self.fImage.renderer().render(painter, source)
  307. # Custom knobs (Dry/Wet and Volume)
  308. if self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_WET, self.CUSTOM_PAINT_MODE_CARLA_VOL):
  309. # knob color
  310. colorGreen = QColor(0x5D, 0xE7, 0x3D).lighter(100 + self.fHoverStep*6)
  311. colorBlue = QColor(0x3E, 0xB8, 0xBE).lighter(100 + self.fHoverStep*6)
  312. # draw small circle
  313. ballRect = QRectF(8.0, 8.0, 15.0, 15.0)
  314. ballPath = QPainterPath()
  315. ballPath.addEllipse(ballRect)
  316. #painter.drawRect(ballRect)
  317. tmpValue = (0.375 + 0.75*normValue)
  318. ballValue = tmpValue - floor(tmpValue)
  319. ballPoint = ballPath.pointAtPercent(ballValue)
  320. # draw arc
  321. startAngle = 218*16
  322. spanAngle = -255*16*normValue
  323. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_WET:
  324. painter.setBrush(colorBlue)
  325. painter.setPen(QPen(colorBlue, 0))
  326. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  327. gradient = QConicalGradient(15.5, 15.5, -45)
  328. gradient.setColorAt(0.0, colorBlue)
  329. gradient.setColorAt(0.125, colorBlue)
  330. gradient.setColorAt(0.625, colorGreen)
  331. gradient.setColorAt(0.75, colorGreen)
  332. gradient.setColorAt(0.76, colorGreen)
  333. gradient.setColorAt(1.0, colorGreen)
  334. painter.setBrush(gradient)
  335. painter.setPen(QPen(gradient, 3))
  336. else:
  337. painter.setBrush(colorBlue)
  338. painter.setPen(QPen(colorBlue, 0))
  339. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  340. painter.setBrush(colorBlue)
  341. painter.setPen(QPen(colorBlue, 3))
  342. painter.drawArc(QRectF(4.0, 4.0, 26.0, 26.0), int(startAngle), int(spanAngle))
  343. # Custom knobs (L and R)
  344. elif self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_L, self.CUSTOM_PAINT_MODE_CARLA_R):
  345. # knob color
  346. color = QColor(0xAD, 0xD5, 0x48).lighter(100 + self.fHoverStep*6)
  347. # draw small circle
  348. ballRect = QRectF(7.0, 8.0, 11.0, 12.0)
  349. ballPath = QPainterPath()
  350. ballPath.addEllipse(ballRect)
  351. #painter.drawRect(ballRect)
  352. tmpValue = (0.375 + 0.75*normValue)
  353. ballValue = tmpValue - floor(tmpValue)
  354. ballPoint = ballPath.pointAtPercent(ballValue)
  355. painter.setBrush(color)
  356. painter.setPen(QPen(color, 0))
  357. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.0, 2.0))
  358. # draw arc
  359. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L:
  360. startAngle = 218*16
  361. spanAngle = -255*16*normValue
  362. else:
  363. startAngle = 322.0*16
  364. spanAngle = 255.0*16*(1.0-normValue)
  365. painter.setPen(QPen(color, 2.5))
  366. painter.drawArc(QRectF(3.5, 3.5, 22.0, 22.0), int(startAngle), int(spanAngle))
  367. # Custom knobs (Color)
  368. elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_COLOR:
  369. # knob color
  370. color = self.fCustomPaintColor.lighter(100 + self.fHoverStep*6)
  371. # draw small circle
  372. ballRect = QRectF(8.0, 8.0, 15.0, 15.0)
  373. ballPath = QPainterPath()
  374. ballPath.addEllipse(ballRect)
  375. tmpValue = (0.375 + 0.75*normValue)
  376. ballValue = tmpValue - floor(tmpValue)
  377. ballPoint = ballPath.pointAtPercent(ballValue)
  378. # draw arc
  379. startAngle = 218*16
  380. spanAngle = -255*16*normValue
  381. painter.setBrush(color)
  382. painter.setPen(QPen(color, 0))
  383. painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2))
  384. painter.setBrush(color)
  385. painter.setPen(QPen(color, 3))
  386. painter.drawArc(QRectF(4.0, 4.8, 26.0, 26.0), int(startAngle), int(spanAngle))
  387. # Custom knobs (Zita)
  388. elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_ZITA:
  389. a = normValue * pi * 1.5 - 2.35
  390. r = 10.0
  391. x = 10.5
  392. y = 10.5
  393. x += r * sin(a)
  394. y -= r * cos(a)
  395. painter.setBrush(Qt.black)
  396. painter.setPen(QPen(Qt.black, 2))
  397. painter.drawLine(QPointF(11.0, 11.0), QPointF(x, y))
  398. # Custom knobs
  399. else:
  400. painter.restore()
  401. return
  402. if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX:
  403. self.fHoverStep += 1 if self.fIsHovered else -1
  404. QTimer.singleShot(20, self.update)
  405. else: # isEnabled()
  406. target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize)
  407. if isinstance(self.fImage, QPixmap):
  408. painter.drawPixmap(target, self.fImage, target)
  409. else:
  410. self.fImage.renderer().render(painter, target)
  411. painter.restore()
  412. def resizeEvent(self, event):
  413. QDial.resizeEvent(self, event)
  414. self.updateSizes()
  415. # ------------------------------------------------------------------------------------------------------------
  416. # Main Testing
  417. if __name__ == '__main__':
  418. import sys
  419. from PyQt5.QtWidgets import QApplication
  420. import resources_rc
  421. app = QApplication(sys.argv)
  422. gui = ScalableDial(None)
  423. #gui.setEnabled(True)
  424. #gui.setEnabled(False)
  425. gui.setSvg(3)
  426. gui.setLabel("hahaha")
  427. gui.show()
  428. sys.exit(app.exec_())