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.

908 lines
39KB

  1. #!/usr/bin/env python3
  2. # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
  3. # SPDX-License-Identifier: GPL-2.0-or-later
  4. # ---------------------------------------------------------------------------------------------------------------------
  5. # Imports (Global)
  6. from math import cos, floor, pi, sin
  7. import ast
  8. from qt_compat import qt_config
  9. if qt_config == 5:
  10. from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QPointF, QRectF, QTimer, QSize
  11. from PyQt5.QtGui import QColor, QLinearGradient, QRadialGradient, QConicalGradient, QFontMetrics, QPen, QPolygonF
  12. from PyQt5.QtWidgets import QToolTip
  13. elif qt_config == 6:
  14. from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QPoint, QPointF, QRectF, QTimer, QSize
  15. from PyQt6.QtGui import QColor, QLinearGradient, QRadialGradient, QConicalGradient, QFontMetrics, QPen, QPolygonF
  16. from PyQt6.QtWidgets import QToolTip
  17. from .commondial import CommonDial
  18. from carla_shared import fontMetricsHorizontalAdvance, RACK_KNOB_GAP
  19. from carla_backend import (
  20. PARAMETER_NULL,
  21. PARAMETER_DRYWET,
  22. PARAMETER_VOLUME,
  23. PARAMETER_BALANCE_LEFT,
  24. PARAMETER_BALANCE_RIGHT,
  25. PARAMETER_PANNING,
  26. PARAMETER_MAX )
  27. # ---------------------------------------------------------------------------------------------------------------------
  28. # Widget Class
  29. class ScalableDial(CommonDial):
  30. def __init__(self, parent, index,
  31. precision,
  32. default,
  33. minimum,
  34. maximum,
  35. label,
  36. paintMode,
  37. colorHint = -1, # Hue & Sat, -1 = NotColorable
  38. unit = "%", # Measurement Unit
  39. skinStyle = "default", # Full name (from full list)
  40. whiteLabels = 1, # Is light/white theme?
  41. tweaks = {},
  42. isInteger = 0, # Input is Integer
  43. isButton = 0, # Integer i/o is Button or LED
  44. isOutput = 0,
  45. isVuOutput = 0, # Output is analog VU meter
  46. isVisible = 1 ):
  47. # self.fWidth = self.fHeight = 32 # aka fImageBaseSize, not includes label.
  48. CommonDial.__init__(self, parent, index, precision, default, minimum, maximum, label, paintMode, colorHint, unit, skinStyle, whiteLabels, tweaks, isInteger, isButton, isOutput, isVuOutput, isVisible)
  49. # FIXME not every repaint need to re-calculate geometry?
  50. def updateSizes(self):
  51. knownModes = [
  52. # default
  53. self.CUSTOM_PAINT_MODE_NULL , # 0
  54. self.CUSTOM_PAINT_MODE_CARLA_WET , # 1
  55. self.CUSTOM_PAINT_MODE_CARLA_VOL , # 2
  56. self.CUSTOM_PAINT_MODE_CARLA_L , # 3
  57. self.CUSTOM_PAINT_MODE_CARLA_R , # 4
  58. self.CUSTOM_PAINT_MODE_CARLA_PAN , # 5
  59. self.CUSTOM_PAINT_MODE_CARLA_FORTH , # 6
  60. self.CUSTOM_PAINT_MODE_CARLA_WET_MINI, # 9
  61. self.CUSTOM_PAINT_MODE_CARLA_VOL_MINI, # 10
  62. # calf
  63. 16,
  64. # openav
  65. 32, 33, 34, 37, 38,
  66. # zynfx
  67. 48, 49, 50, 53, 54,
  68. # tube
  69. 64, 65, 66, 69, 70,
  70. ]
  71. index = -1
  72. for i in range(len(knownModes)):
  73. if knownModes[i] == self.fCustomPaintMode:
  74. index = i
  75. break
  76. if (index == -1):
  77. print("Unknown paint mode "+ str(self.fCustomPaintMode))
  78. return
  79. self.skin = int(self.fCustomPaintMode / 16)
  80. self.subSkin = int(self.fCustomPaintMode % 16)
  81. width, hueA, hueB, travel, radius, size, point, labelLift = [
  82. # default Aqua
  83. [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ],
  84. [ 32, 0.3 , 0.50, 260, 10, 10, 3 , 1/2, ], # WET
  85. [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ], # VOL
  86. [ 26, 0.21, 0.21, 260, 8, 10, 2.5, 1/2, ], # L
  87. [ 26, 0.21, 0.21, 260, 8, 10, 2.5, 1/2, ], # R
  88. [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ], # PAN
  89. [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ], # FORTH
  90. [ 28, 0.3 , 0.50, 260, 9, 10, 2.5, 1/2, ], # WET_MINI
  91. [ 28, 0.50, 0.50, 260, 9, 10, 2.5, 1/2, ], # VOL_MINI
  92. # calf Blue
  93. [ 40, 0.53, 0.53, 290, 12, 12, 4 , 1 , ], # calf absent any wet/vol knobs
  94. # openav Orange
  95. [ 32, 0.05, 0.05, 270, 12, 12, 2.5, 2/3, ],
  96. [ 32, 0.30, 0.5, 270, 12, 12, 2.5, 2/3, ], # WET
  97. [ 32, 0.5, 0.5, 270, 12, 12, 2.5, 2/3, ], # VOL
  98. [ 32, 0.5, 0.5, 270, 12, 12, 2.5, 2/3, ],
  99. [ 32, 0.5, 0.5, 270, 12, 12, 2.5, 2/3, ],
  100. # zynfx Teal
  101. [ 38, 0.55, 0.55, 264, 12, 12, 4 , 1/4, ],
  102. [ 38, 0.30, 0.5, 264, 12, 12, 4 , 1/4, ], # WET
  103. [ 38, 0.5, 0.5, 264, 12, 12, 4 , 1/4, ], # VOL
  104. [ 38, 0.5, 0.5, 264, 12, 12, 4 , 1/4, ],
  105. [ 38, 0.5, 0.5, 264, 12, 12, 4 , 1/4, ],
  106. # tube VFD
  107. [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ],
  108. [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ], # WET
  109. [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ], # VOL
  110. [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ],
  111. [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ],
  112. ] [index]
  113. # Geometry & Color of controls & displays, some are tweakable:
  114. # 1. Try to get value from per-skin tweak;
  115. # 2. Then try to get value from common tweak;
  116. # 3. Then use default value from array.
  117. self.fWidth = self.fHeight = width
  118. # Angle span (travel)
  119. # calf must be 360/36*29=290
  120. # tube must be 360/14*10=257.14 or 360/12*10=300
  121. self.fTravel = int(self.getTweak('KnobTravel', travel))
  122. # Radius of some notable element of Knob (not exactly the largest)
  123. self.fRadius = int(self.getTweak('KnobRadius', radius))
  124. # Size of Button (half of it, similar to "raduis")
  125. self.fSize = int(self.getTweak('ButtonSize', size))
  126. # Point, line or other accent on knob
  127. self.fPointSize = point
  128. # Colouring, either only one or both values can be used for skin.
  129. if (self.subSkin > 0) or (self.skin in (1, 3, 4,)) :
  130. self.fHueA = hueA
  131. self.fHueB = hueB
  132. # default and openav can be re-colored
  133. elif self.colorFollow:
  134. self.fHueA = self.fHueB = int(self.fColorHint) / 100.0 # we use hue only yet
  135. else:
  136. # NOTE: here all incoming color data, except hue, is lost.
  137. self.fHueA = self.fHueB = self.fCustomPaintColor.hueF()
  138. metrics = QFontMetrics(self.fLabelFont)
  139. if not self.fLabel:
  140. self.fLabelWidth = 0
  141. else:
  142. self.fLabelWidth = fontMetricsHorizontalAdvance(metrics, self.fLabel)
  143. extraWidthAuto = max((self.fLabelWidth - self.fWidth), 0)
  144. self.fLabelHeight = metrics.height()
  145. if (self.fCustomPaintMode % 16) == 0: # exclude: DryWet, Volume, etc.
  146. extraWidth = int(self.getTweak('GapMin', 0))
  147. extraWidthLimit = int(self.getTweak('GapMax', 0))
  148. if self.getTweak('GapAuto', 0):
  149. extraWidth = max(extraWidth, extraWidthAuto)
  150. extraWidth = min(extraWidth, extraWidthLimit)
  151. self.fWidth = self.fWidth + extraWidth
  152. self.setMinimumSize(self.fWidth, self.fHeight + self.fLabelHeight + RACK_KNOB_GAP)
  153. self.setMaximumSize(self.fWidth, self.fHeight + self.fLabelHeight + RACK_KNOB_GAP)
  154. if not self.fLabel:
  155. self.fLabelHeight = 0
  156. # self.fLabelWidth = 0
  157. return
  158. self.fLabelPos.setX(float(self.fWidth)/2.0 - float(self.fLabelWidth)/2.0)
  159. # labelLift = (1/2, 1, 2/3, 1/4, 1/2, 1, 1, 1)[skin % 8]
  160. self.fLabelPos.setY(self.fHeight + self.fLabelHeight * labelLift)
  161. # jpka: TODO Can't see how gradients work, looks like it's never triggered.
  162. self.fLabelGradient.setStart(0, float(self.fHeight)/2.0)
  163. self.fLabelGradient.setFinalStop(0, self.fHeight + self.fLabelHeight + 5)
  164. self.fLabelGradientRect = QRectF(float(self.fHeight)/8.0, float(self.fHeight)/2.0,
  165. float(self.fHeight*3)/4.0, self.fHeight+self.fLabelHeight+5)
  166. def setImage(self, imageId):
  167. print("Loopback for self.setupZynFxParams(), FIXME!")
  168. return
  169. def minimumSizeHint(self):
  170. return QSize(self.fWidth, self.fHeight)
  171. def sizeHint(self):
  172. return QSize(self.fWidth, self.fHeight)
  173. # def changeEvent(self, event):
  174. # CommonDial.changeEvent(self, event)
  175. #
  176. # # Force svg update if enabled state changes
  177. # if event.type() == QEvent.EnabledChange:
  178. # self.slot_updateImage()
  179. def drawMark(self, painter, X, Y, r1, r2, angle, width, color):
  180. A = angle * pi/180
  181. x = X + r1 * cos(A)
  182. y = Y - r1 * sin(A)
  183. painter.setPen(QPen(color, width, cap=Qt.RoundCap))
  184. if not (r1 == r2): # line
  185. x1 = X + r2 * cos(A)
  186. y1 = Y - r2 * sin(A)
  187. painter.drawLine(QPointF(x, y), QPointF(x1, y1))
  188. else: # ball
  189. painter.drawEllipse(QRectF(x-width/2, y-width/2, width, width))
  190. gradMachined = {5.9, 10.7, 15.7, 20.8, 25.8, 30.6, 40.6, 45.9,
  191. 55.9, 60.7, 65.7, 70.8, 75.8, 80.6, 90.6, 95.9}
  192. def grayGrad(self, painter, X, Y, a, b, gradPairs, alpha = 1.0):
  193. if b == -1:
  194. grad = QConicalGradient(X, Y, a)
  195. elif b == -2:
  196. grad = QRadialGradient (X, Y, a)
  197. else:
  198. grad = QLinearGradient (X, Y, a, b)
  199. for i in gradPairs:
  200. grad.setColorAt(int(i)/100.0, QColor.fromHslF(0, 0, (i % 1.0), alpha))
  201. return grad
  202. # Pen is always full opacity (alpha = 1)
  203. def grayGradPen(self, painter, X, Y, a, b, gradPairs = {0.10, 50.30, 100.10}, width = 1.0):
  204. painter.setPen(QPen(self.grayGrad(painter, X, Y, a, b, gradPairs, 1), width, Qt.SolidLine, Qt.FlatCap))
  205. def grayGradBrush(self, painter, X, Y, a, b, gradPairs, alpha = 1.0):
  206. painter.setBrush(self.grayGrad(painter, X, Y, a, b, gradPairs, alpha))
  207. # Replace Qt draw over substrate bitmap or svg to
  208. # all-in-one widget generated from stratch using Qt only,
  209. # make it highly tuneable, and uniformly look like
  210. # using HSL color model to make same brightness of colored things.
  211. # We can also easily have color tinted (themed) knobs.
  212. # Some things were simplified a little, to gain more speed.
  213. # R: knob nib (cap) radius
  214. def paintDial(self, painter, X, Y, H, S, L, E, normValue, enabled):
  215. R = self.fRadius
  216. barWidth = self.fPointSize
  217. angleSpan = self.fTravel
  218. hueA = self.fHueA
  219. hueB = self.fHueB
  220. color0 = QColor.fromHslF(hueA, S, L, 1)
  221. color0a = QColor.fromHslF(hueA, S, L/2-0.25, 1)
  222. color1 = QColor.fromHslF(hueB, S, L, 1)
  223. skin = self.skin
  224. def ang(value):
  225. return angleSpan * (0.5 - value) + 90
  226. def drawArcV(rect, valFrom, valTo, ticks = 0):
  227. # discretize scale: for 10 points, first will lit at 5%,
  228. # then 15%, and last at 95% of normalized value,
  229. # i.e. treshold is: center of point exactly matches knob mark angle
  230. if ticks:
  231. valTo = int(valTo * (ticks * angleSpan / 360) + 0.5) / (ticks * angleSpan / 360)
  232. painter.drawArc(rect, int(ang(valFrom) * 16), int((ang(valTo) - ang(valFrom)) * 16))
  233. def squareBorder(w):
  234. return QRectF(X-R-w, Y-R-w, (R+w)*2, (R+w)*2)
  235. def gray(luma):
  236. return QColor.fromHslF(0, 0, luma, 1)
  237. # Knob light arc "base" (starting) value/angle.
  238. if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L:
  239. refValue = 0
  240. elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_R:
  241. refValue = 1
  242. elif (self.fMinimum == -self.fMaximum) and (skin == 0):
  243. refValue = 0.5
  244. else:
  245. refValue = 0
  246. knobMuted = (self.knobPusheable and (normValue == refValue))
  247. haveLed = self.getTweak('WetVolPushLed', 1) and self.fCustomPaintMode in (1, 2, 5, 6, 9, 10,)
  248. if self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING): # -3, -4, -7
  249. if knobMuted:
  250. if self.fIndex == PARAMETER_DRYWET:
  251. self.pushLabel("Thru")
  252. elif self.fIndex == PARAMETER_VOLUME:
  253. self.pushLabel("Mute")
  254. elif self.fIndex == PARAMETER_PANNING:
  255. self.pushLabel("Center")
  256. else:
  257. self.pushLabel("Midway")
  258. else:
  259. self.popLabel()
  260. if skin == 0: # mimic svg dial
  261. # if not knobMuted:
  262. if not (knobMuted and haveLed):
  263. # light arc substrate: near black, 0.5 px exposed
  264. painter.setPen(QPen(gray(0.10), barWidth+1, cap=Qt.FlatCap))
  265. drawArcV(squareBorder(barWidth), 0, 1)
  266. # light arc: gray bar
  267. # should be combined with light (value) arc to be a bit faster ?
  268. self.grayGradPen(painter, X, Y, 270, -1, {0.20, 100.15}, barWidth)
  269. drawArcV(squareBorder(barWidth), 0, 1)
  270. # cap
  271. self.grayGradBrush(painter, X-R, Y-R, R*2, -2, {0.45+E, 100.15+E})
  272. painter.setPen(QPen(gray(0.10), 0.5))
  273. painter.drawEllipse(squareBorder(1))
  274. elif skin == 1: # calf
  275. # outer chamfer & leds substrate
  276. self.grayGradPen(painter, X, Y, 135, -1, {0.15, 50.50, 100.15}, 1.5)
  277. painter.setBrush(color0a)
  278. painter.drawEllipse(squareBorder(barWidth*2-1))
  279. # machined shiny cap with chamfer
  280. self.grayGradPen(painter, X, Y, -45, -1, {0.15, 50.50, 100.15})
  281. self.grayGradBrush(painter, X, Y, 0, -1, self.gradMachined)
  282. painter.drawEllipse(squareBorder(1))
  283. elif skin == 2: # openav
  284. # light arc substrate
  285. painter.setPen(QPen(gray(0.20+E), barWidth))
  286. drawArcV(squareBorder(barWidth), 0, 1)
  287. elif skin == 3: # zynfx
  288. # light arc substrate
  289. painter.setPen(QPen(QColor.fromHslF(0.57, 0.8, 0.25, 1), barWidth+2, cap=Qt.FlatCap))
  290. drawArcV(squareBorder(barWidth), 0, 1)
  291. # cap
  292. painter.setPen(QPen(gray(0.0), 1))
  293. painter.setBrush(gray(0.3 + E))
  294. painter.drawEllipse(squareBorder(-2))
  295. # These knobs are different for integers and for floats.
  296. elif skin == 4: # tube / bakelite
  297. chamfer = 1.5 # It is best when 1.5 at normal zoom, and 1.0 for >2x HiDpi
  298. # base
  299. self.grayGradPen(painter, X, Y, -45, -1, width=chamfer)
  300. self.grayGradBrush(painter, X-5-ang(normValue)/36, -20, 83, -2, {0.2, 50.2, 51.00, 100.00})
  301. if self.fIsInteger: # chickenhead knob: small base
  302. painter.drawEllipse(squareBorder(1))
  303. else: # round knob: larger base
  304. painter.drawEllipse(squareBorder(R*0.7))
  305. polygon = QPolygonF()
  306. # "chickenhead" pointer
  307. if self.fIsInteger:
  308. for i in range(17):
  309. A = ((0.01, 0.02, 0.03, 0.06, 0.2, 0.3, 0.44, 0.455, -0.455, -0.44, -0.3, -0.2, -0.06, -0.03, -0.02, -0.01, 0.01)[i] * 360 - ang(normValue)) * pi/180
  310. r = (1, 0.97, 0.91, 0.7, 0.38, 0.39, 0.87, 0.9, 0.9, 0.87, 0.39, 0.38, 0.7, 0.91, 0.97, 1, 1)[i] * R
  311. polygon.append(QPointF(X + r * 1.75 * cos(A), Y + r * 1.75 * sin(A)))
  312. # 8-teeth round knob outline
  313. else:
  314. for i in range(64):
  315. A = (i / 64 * 360 - ang(normValue)) * pi/180
  316. r = R * (1, 0.95, 0.91, 0.89, 0.88, 0.89, 0.91, 0.95)[i % 8]
  317. polygon.append(QPointF(X + r * 1.5 * cos(A), Y + r * 1.5 * sin(A)))
  318. self.grayGradPen(painter, X, Y, -45, -1, {0.10, 50.50, 100.10}, chamfer)
  319. self.grayGradBrush(painter, X-5-ang(normValue)/36, -20, 75, -2, {0.2, 50.2, 51.00, 100.00})
  320. painter.drawPolygon(polygon)
  321. # machined shiny penny with chamfer
  322. self.grayGradPen(painter, X, Y, 135, -1, {0.15, 50.50, 100.15})
  323. self.grayGradBrush(painter, X, Y, -ang(normValue)/36, -1, self.gradMachined, 0.75)
  324. if self.fIsInteger: # chickenhead knob: small circle
  325. painter.drawEllipse(squareBorder(-R*0.65))
  326. else: # round knob: large one
  327. painter.drawEllipse(squareBorder(-1))
  328. # Outer scale marks
  329. for i in range(0, 11):
  330. angle = ((0.5-i/10) * angleSpan + 90)
  331. self.drawMark(painter, X, Y, R*2, R*2, angle, barWidth/12 * (4 + 1 * int((i % 10) == 0)), gray(0.5 + E))
  332. # if knobMuted:
  333. if (knobMuted and haveLed):
  334. # if self.getTweak('WetVolPushLed', 1):
  335. self.drawMark(painter, X, Y, 0, 0, 0, barWidth, color0)
  336. return
  337. # draw arc: forward, or reverse (for 'R' ch knob)
  338. if (not (normValue == refValue)) and (not (skin == 4)):
  339. gradient = QConicalGradient(X, Y, 270)
  340. cap=Qt.FlatCap
  341. if not (skin == 1): # any, except calf
  342. ticks = 0
  343. gradient.setColorAt(0.75, color0)
  344. gradient.setColorAt(0.25, color1)
  345. if skin == 3: # zynfx
  346. # light arc partial (angled) black substrate
  347. painter.setPen(QPen(gray(0.0), barWidth+2, cap=Qt.FlatCap))
  348. drawArcV(squareBorder(barWidth), refValue-0.013, normValue+0.013)
  349. elif skin == 2: # openav
  350. cap=Qt.RoundCap
  351. else: # calf
  352. ticks = 36
  353. for i in range(2, ticks-2, 1):
  354. gradient.setColorAt((i+0.5-0.35)/ticks, color0)
  355. gradient.setColorAt((i+0.5) /ticks, Qt.black)
  356. gradient.setColorAt((i+0.5+0.35)/ticks, color0)
  357. painter.setPen(QPen(gradient, barWidth, Qt.SolidLine, cap))
  358. drawArcV(QRectF(squareBorder(barWidth)), refValue, normValue, ticks)
  359. # do not draw marks on disabled items
  360. if not enabled:
  361. return
  362. A = ang(normValue)
  363. match skin:
  364. case 0: # ball
  365. self.drawMark(painter, X, Y, R*0.8, R*0.8, A, barWidth/2+0.5, color0)
  366. case 1: # line for calf
  367. self.drawMark(painter, X, Y, R*0.6, R*0.9, A, barWidth/2, Qt.black)
  368. case 2: # line for openav
  369. self.drawMark(painter, X, Y, 0, R+barWidth, A, barWidth, color0)
  370. case 3: # line for zynfx
  371. self.drawMark(painter, X, Y, 2, R-3, A, barWidth/2+0.5, Qt.white)
  372. case 4: # ball
  373. r = R * (int(self.fIsInteger) * 0.25 + 1.2)
  374. self.drawMark(painter, X, Y, r, r, A, barWidth/2+0.5, Qt.white)
  375. def paintButton(self, painter, X, Y, H, S, L, E, normValue, enabled):
  376. # W: button cap half-size ; w: bar width
  377. W = self.fRadius
  378. w = self.fPointSize
  379. hue = self.fHueA
  380. skin = int(self.fCustomPaintMode / 16)
  381. def squareBorder(w, dw=0):
  382. return QRectF(X-W-w-dw, Y-W-w, (W+w+dw)*2, (W+w)*2)
  383. def gray(luma):
  384. return QColor.fromHslF(0, 0, luma, 1)
  385. color = QColor.fromHslF(hue, S, L, 1)
  386. centerLed = self.getTweak('ButtonHaveLed', 0) # LED itself & size increase
  387. coloredNeon = self.getTweak('ColoredNeon', 1) # But worse when HighContrast.
  388. if skin == 0: # internal
  389. if not centerLed:
  390. # light bar substrate: near black, 0.5 px exposed
  391. painter.setPen(QPen(gray(0.10), w+1))
  392. painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w))
  393. # light bar: gray bar
  394. painter.setPen(QPen(gray(0.20), w))
  395. painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w))
  396. # cap
  397. self.grayGradBrush(painter, X-W/2, Y-W/2, W*2, -2, {0.13+E, 50.18+E, 100.35+E})
  398. painter.setPen(QPen(gray(0.05), 1))
  399. # A bit larger buttons when no top LED, but centered one.
  400. painter.drawRoundedRect(squareBorder(-1+centerLed), 3, 3)
  401. elif skin == 1: # calf
  402. # outer chamfer & leds substrate
  403. self.grayGradPen(painter, X, Y, 135, -1, {24.25, 26.50, 76.50, 78.25}, 1.5)
  404. painter.setBrush(QColor.fromHslF(hue, S, 0.05+E/2, 1))
  405. painter.drawRoundedRect(QRectF(X-W-1, Y-W-w-0-1, W*2+2, W*2+w+0+2), 4, 4)
  406. # machined shiny cap with chamfer
  407. self.grayGradPen(painter, X, Y, -45, -1, {24.25, 26.50, 74.50, 76.25})
  408. self.grayGradBrush(painter, X, Y, -30, -1, self.gradMachined)
  409. painter.drawRoundedRect(squareBorder(-1), 3, 3)
  410. elif skin == 2: # openav
  411. # light substrate
  412. pen = QPen(gray(0.20+E), w)
  413. painter.setPen(pen)
  414. painter.drawRoundedRect(squareBorder(0), 3, 3)
  415. elif skin == 3: # zynfx
  416. if not centerLed:
  417. # light bar substrate: teal, 1 px exposed
  418. painter.setPen(QPen(QColor.fromHslF(hue, 0.8, 0.25, 1), w+2))
  419. painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w))
  420. # button
  421. painter.setPen(QPen(gray(0.0), 1))
  422. painter.setBrush(gray(0.3 + E))
  423. painter.drawRoundedRect(squareBorder(-2, 4), 3, 3)
  424. elif skin == 4: # tube
  425. # bakelite cap
  426. self.grayGradPen(painter, X, Y, -45, -1)
  427. self.grayGradBrush(painter, X-10, -40, 120, -2, {0.2, 50.2, 51.00, 100.00})
  428. painter.drawRoundedRect(squareBorder(W*0.2), 3, 3)
  429. # neon lamp
  430. if (normValue > 0):
  431. grad = QRadialGradient(X, Y, 10)
  432. for i in ({0.6, 20.6, 70.4, 100.0}):
  433. if coloredNeon:
  434. grad.setColorAt(int(i)/100.0, QColor.fromHslF((0.05 - normValue) % 1.0, S, (i % 1.0), 1))
  435. else:
  436. grad.setColorAt(int(i)/100.0, QColor.fromHslF(0.05, S, (i % 1.0) * normValue, 1))
  437. painter.setPen(QPen(Qt.NoPen))
  438. painter.setBrush(grad)
  439. painter.drawRoundedRect(squareBorder(-W*0.4), 1.5, 1.5)
  440. # glass over neon lamp
  441. self.grayGradPen(painter, X, Y, 135, -1)
  442. self.grayGradBrush(painter, X-10, -40, 124, -2, {0.9, 50.9, 51.4, 100.4}, 0.25)
  443. painter.drawRoundedRect(squareBorder(-W*0.4), 1.5, 1.5)
  444. # draw active lights
  445. if skin == 0: # internal
  446. if not centerLed:
  447. if (normValue > 0):
  448. painter.setPen(QPen(color, w))
  449. if (normValue < 1):
  450. painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X-w/2, Y-W-w))
  451. else:
  452. painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w))
  453. else:
  454. painter.setPen(QPen(gray(0.05), 0.5))
  455. painter.setBrush(color.darker(90 + int(300*(1-normValue))))
  456. painter.drawRoundedRect(squareBorder(w-W), 1, 1)
  457. elif skin == 1: # calf
  458. if (normValue > 0):
  459. grad = QLinearGradient(X-W, Y, X+W, Y)
  460. for i in ({20.0, 45.6, 55.6, 80.0} if (normValue < 1)
  461. else {0.0, 30.6, 40.5, 45.7, 55.7, 60.5, 70.6, 100.0}):
  462. grad.setColorAt(int(i)/100.0, QColor.fromHslF(hue, S, (i % 1)+E, 1))
  463. painter.setPen(QPen(grad, w-0.5, cap=Qt.FlatCap))
  464. painter.drawLine(QPointF(X-W, Y-W-w/2), QPointF(X+W, Y-W-w/2))
  465. elif skin == 2: # openav
  466. painter.setPen(QPen(color, w, cap=Qt.RoundCap))
  467. if (normValue > 0):
  468. painter.drawRoundedRect(squareBorder(-W * (1 - normValue)), 3, 3)
  469. else:
  470. painter.drawLine(QPointF(X-0.1, Y), QPointF(X+0.1, Y))
  471. elif skin == 3: # zynfx
  472. if not centerLed:
  473. if (normValue > 0):
  474. dx = (W - w) if (normValue < 1) else 0
  475. painter.setPen(QPen(gray(0), w+2))
  476. painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2-dx, Y-W-w))
  477. painter.setPen(QPen(color, w))
  478. painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2-dx, Y-W-w))
  479. else:
  480. painter.setPen(QPen(gray(0), 1))
  481. painter.setBrush(color.darker(90 + int(300*(1-normValue))))
  482. painter.drawEllipse(squareBorder(w-W+1))
  483. # do not draw marks on disabled items
  484. if not enabled:
  485. return
  486. match skin:
  487. case 0: # internal: ball at center
  488. if not centerLed:
  489. self.drawMark(painter, X, Y, 0, 0, 0, w/2+0.5, color)
  490. # case 3: # openav
  491. # painter.setPen(QPen(color, w, cap=Qt.RoundCap))
  492. # painter.drawLine(QPointF(X-0.1, Y), QPointF(X+0.1, Y))
  493. case 3: # zynfx: ball at center
  494. if not centerLed:
  495. self.drawMark(painter, X, Y, 0, 0, 0, w/2, gray(1))
  496. # Just a text label not so good for fast updated display, see issue #1934.
  497. # NOTE Work in progress.
  498. def paintDisplay(self, painter, X, Y, H, S, L, E, normValue, enabled):
  499. # X, Y: Center of label.
  500. def plotStr(self, painter, X, Y, st, fontSize, aspectRatio, skew):
  501. # Due to CPU/speed gain, we use simplest possible 7-segmented digits.
  502. # Shape to Speed balance: Speed
  503. h = ["KYNKNY ROZUZ RVKVY ROJUJ", # 0 NJ RJ VJ
  504. "KYVJVQ RVSVZ", # 1 NR RR VR
  505. "KYNJVJVRNRNZVZ", # 2 NZ RZ VZ
  506. "KYNJVJVZNZ RVRNR", # 3 P]
  507. "KYNJNRVR RVJVZ", # 4
  508. "KYVJNJNRVRVZNZ", # 5
  509. "KYVJNJNZVZVRNR", # 6
  510. "KYNJVJVZ", # 7
  511. "KYNJNZVZVJNJ RNRVR", # 8
  512. "KYNZVZVJNJNRVR", # 9
  513. "KYNRVR", # -
  514. "OTRZP]", # .
  515. "KYNNNX RVPNTVX", # k
  516. "KYNXNLRRVLVX" ] # M
  517. def plotHersheyChar(painter, X, Y, c, fontSize, aspectRatio, skew, justGetWidth):
  518. lm = (ord(h[c][0]) - ord('R')) * fontSize * aspectRatio
  519. rm = (ord(h[c][1]) - ord('R')) * fontSize * aspectRatio
  520. if justGetWidth:
  521. return X + rm - lm
  522. points = []
  523. X = X - lm
  524. # The speed and CPU load is critical here.
  525. # I try to make it as efficient as possible, but can it be even faster?
  526. for i in range(1, int(len(h[c])/2)):
  527. a = (h[c][i*2])
  528. b = (h[c][i*2+1])
  529. if (a == ' ') and (b == 'R'):
  530. painter.drawPolyline(points)
  531. points = []
  532. else:
  533. y = (ord(b) - ord('R')) * fontSize
  534. x = (ord(a) - ord('R')) * fontSize * aspectRatio + skew * y
  535. points.append(QPointF(X+x, Y+y))
  536. painter.drawPolyline(points)
  537. X = X + rm
  538. return X
  539. def plotDecodedChar(painter, X, Y, st, fontSize, aspectRatio, skew, justGetWidth):
  540. for i in range(len(st)):
  541. digit = "0123456789-.kM".find(st[i])
  542. if digit < 0:
  543. print("ERROR: Illegal char at " + str(i) + " in " + st)
  544. else:
  545. X = plotHersheyChar(painter, X, Y, digit, fontSize, aspectRatio, skew, justGetWidth)
  546. return X
  547. widthPx = plotDecodedChar(painter, 0, Y, st, fontSize, aspectRatio, skew, 1)
  548. plotDecodedChar(painter, X-widthPx/2, Y, st, fontSize, aspectRatio, skew, 0)
  549. return
  550. def strLimDigits(x):
  551. s = str(x)
  552. ret = lambda x: float(x) if '.' in s else int(x)
  553. return str(ret(s[:max(s.find('.'), 4+1)].strip('.')))
  554. # return str(ret(s[:max(s.find('.'), num+1 + ('-' in s))].strip('.')))
  555. def plotNixie(n):
  556. painter.setPen(QPen(QColor.fromHslF(0.05, S, L, 1), 2.5, cap=Qt.RoundCap))
  557. # We use true arcs instead of polyline/Bezier.
  558. # Arcs are perfectly matched with original tube.
  559. # x = 0..20, y = 0..32
  560. digits = [[[ 2,00,18,16, 480,2400],[ 00,-8,48,40,2400,3360],
  561. [ 2,16,18,32,3360,5280],[ -28,-8,20,40,5280,6240]], # 0
  562. [[10,00,10,32, 0, 0]], # 1
  563. [[ 1,-0.5,19,17.5,4608,8640],[1,17,30,46,1680,2880],
  564. [1,31.5,19,31.5, 0, 0]], # 2
  565. [[-2,10,20,32,3500,7300],[ 2,00,19,00, 0, 0],
  566. [19,00, 8,10, 0, 0]], # 3
  567. [[ 1,22,17,00, 0, 0],[ 17,00,17,32, 0, 0],
  568. [1,22,17,22, 0, 0]], # 4
  569. [[-1,12,19,32,3500,7920],[ 4,00,18,00, 0, 0],
  570. [4,00, 2,14, 0, 0]], # 5
  571. [[00,12,20,32, 0,5760],[ 0,-10,64,54,2150,2880]], # 6
  572. [[ 1,00,19,00, 0, 0],[ 19,00, 8,32, 0, 0]], # 7
  573. [[ 1,14,19,32, 0,5760],[ 3,00,17,14, 0,5760]], # 8
  574. [[00,00,20,20, 0,5760],[ 20,42,-44,-22,5030,5760]]] # 9
  575. for x0, y0, x1, y1, a0, a1 in digits[n]:
  576. if a0 == a1 == 0:
  577. painter.drawLine(QPointF(x0+X-10, y0+Y-16), QPointF(x1+X-10, y1+Y-16))
  578. else:
  579. rect = QRectF(x0+X-10, y0+Y-16, x1-x0, y1-y0)
  580. painter.drawArc(rect, a0, a1-a0)
  581. def squareBorder(w, dw=0):
  582. return QRectF(X-W-w-dw, Y-W-w, (W+w+dw)*2, (W+w)*2)
  583. W = Y-1 # "radius"
  584. value = self.fRealValue
  585. hue = self.fHueA
  586. # if self.fIsButton: # TODO make it separate paintLED
  587. if (self.fIsInteger and (self.fMinimum == 0) and (self.fMaximum == 1)): # TODO
  588. # Neon lamp
  589. if (self.fCustomPaintMode == 64): # tube
  590. # bakelite lamp holder
  591. self.grayGradPen(painter, X, Y, -45, -1)
  592. self.grayGradBrush(painter, X-10, -40, 120, -2, {0.2, 50.2, 51.00, 100.00})
  593. painter.drawRoundedRect(squareBorder(-W*0.45), 3, 3)
  594. # neon lamp
  595. if (normValue > 0):
  596. grad = QRadialGradient(X, Y, 13)
  597. for i in ({0.6, 20.6, 70.4, 100.0}):
  598. grad.setColorAt(int(i)/100.0, QColor.fromHslF(0.05, 1.0, (i % 1.0) * normValue, 1))
  599. painter.setPen(QPen(Qt.NoPen))
  600. painter.setBrush(grad)
  601. painter.drawRoundedRect(squareBorder(-W*0.6), 1.5, 1.5)
  602. # glass over neon lamp
  603. self.grayGradPen(painter, X, Y, 135, -1)
  604. self.grayGradBrush(painter, X-10, -40, 124, -2, {0.9, 50.9, 51.4, 100.4}, 0.25)
  605. painter.drawRoundedRect(squareBorder(-W*0.6), 1.5, 1.5)
  606. return
  607. painter.setPen(QPen(QColor.fromHslF(0, 0, 0.3-0.15*normValue+E, 1), 1.5))
  608. painter.setBrush(QColor(QColor.fromHslF(hue, S, L*(normValue*0.8+0.1), 1)))
  609. painter.drawRoundedRect(squareBorder(-W/2-2), 1.5, 1.5)
  610. return
  611. if (self.fCustomPaintMode == 64) and \
  612. (self.fIsInteger and (self.fMinimum >= 0) and (self.fMaximum < 20)):
  613. #Nixie tube for 0..9, or 1 1/2 tubes for 11..19
  614. Y = Y - 2
  615. self.grayGradPen(painter, X, Y, 135, -1)
  616. self.grayGradBrush(painter, X-10, -40, 120, -2, {0.2, 50.2, 51.00, 100.00})
  617. painter.drawRoundedRect(squareBorder(-2, -W*0.2), 3, 3)
  618. if (value < 10):
  619. plotNixie(int(value % 10))
  620. else:
  621. if (value == 11):
  622. X = X + 8
  623. plotNixie(1)
  624. else:
  625. X = X + 4
  626. plotNixie(int(value % 10))
  627. X = X - 16
  628. plotNixie(1)
  629. return
  630. # Will it be analog display, or digital 7-segment scale?
  631. if not self.fIsVuOutput:
  632. unit = ""
  633. if abs(value) >= 10000.0:
  634. value = value / 1000.0
  635. unit = "k"
  636. if abs(value) >= 10000.0:
  637. value = value / 1000.0
  638. unit = "M"
  639. # Remove trailing decimal zero and decimal point also.
  640. if (value % 1.0) == 0:
  641. value = int(value)
  642. valueStr = strLimDigits(value) + unit
  643. valueLen = len(valueStr)
  644. if valueLen == 0:
  645. print("Zero length string from " + str(self.fRealValue) + " value.")
  646. return
  647. autoFontsize = int(self.getTweak('Auto7segSize', 0)) # Full auto
  648. autoFontwidth = int(self.getTweak('Auto7segWidth', 1)) # Width only
  649. skew = -0.2
  650. substrate = 1
  651. dY = 3 # Work in progress here. NOTE
  652. fntSize = 0.9
  653. fntAspect = 0.5
  654. bgLuma = 0.05
  655. R = 10 # Replace to W
  656. width = 4
  657. lineWidth = 2.0
  658. if self.fCustomPaintMode == 0: # default / internal
  659. fntSize = 0.75
  660. Y = Y - 2
  661. elif self.fCustomPaintMode == 16: # calf
  662. autoFontsize = 1
  663. skew = 0
  664. dY = 4
  665. bgLuma = 0.1
  666. R = 12
  667. elif self.fCustomPaintMode == 32: # openav
  668. autoFontsize = 1
  669. substrate = 0
  670. R = 13
  671. lineWidth = 3.0
  672. elif self.fCustomPaintMode == 48: # zynfx
  673. fntSize = 0.99
  674. Y = Y - 2
  675. dY = 4
  676. bgLuma = 0.12
  677. R = 12
  678. elif self.fCustomPaintMode == 64: # tube
  679. autoFontsize = 1
  680. R = 13
  681. lineWidth = 3.0
  682. else:
  683. print("Unknown paint mode "+ str(self.fCustomPaintMode) + " display.")
  684. return
  685. if not self.fIsVuOutput:
  686. if autoFontwidth and (valueLen < 4):
  687. if autoFontsize:
  688. fntSize = fntSize * 4/3
  689. fntAspect = 1.0 - (valueLen-1) * 0.2
  690. lineWidth = fntSize + 0.5 # Work in progress here. NOTE
  691. # substrate
  692. if substrate:
  693. substratePen = QPen(QColor.fromHslF(0, 0, 0.4+E, 1), 0.5)
  694. if (self.fCustomPaintMode == 64): # tube
  695. if not self.fIsVuOutput:
  696. self.grayGradPen(painter, X, Y, 135, -1)
  697. self.grayGradBrush(painter, X-10, -80, 200, -2, {0.2, 50.2, 51.00, 100.00})
  698. painter.drawRoundedRect(squareBorder(-W*0.4, W*0.3), 3, 3)
  699. else:
  700. if self.fCustomPaintMode == 16: # calf
  701. if self.fIsVuOutput:
  702. dY = 9
  703. painter.setPen(substratePen)
  704. painter.setBrush(QColor(QColor.fromHslF(0, 0, bgLuma, 1)))
  705. painter.drawRoundedRect(squareBorder(-dY, dY), 3, 3)
  706. color = QColor.fromHslF(hue, S, L, 1)
  707. painter.setPen(QPen(color, lineWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
  708. if not self.fIsVuOutput:
  709. plotStr(self, painter, X, Y, valueStr, fntSize, fntAspect, skew);
  710. else:
  711. if self.fCustomPaintMode == 16: # calf # Work in progress here. NOTE
  712. for i in range(0, 10+1):
  713. if normValue > ((i+0.5)/11):
  714. painter.drawLine(QPointF(X-15+i*3, Y-R/2), QPointF(X-15+i*3, Y+R/2))
  715. elif self.fCustomPaintMode == 64: # tube # Work in progress here. NOTE
  716. # Draw Cat eye.
  717. chamfer = 1.5 # It is best when 1.5 at normal zoom, and 1.0 for >2x HiDpi
  718. # base
  719. self.grayGradPen(painter, X, Y, -45, -1, width=chamfer)
  720. self.grayGradBrush(painter, X-10, -20, 83, -2, {0.2, 50.2, 51.00, 100.00})
  721. painter.drawEllipse(squareBorder(-W*0.2))
  722. # green sectors
  723. rays = 4 # There are 4- or 8-rays (2 or 4 notches) tubes
  724. gradient = QConicalGradient(X, Y, 0)
  725. for i in range(rays):
  726. sign = 1 - (i % 2) * 2
  727. v = min(normValue, 1) * 1.2 + 0.05 # For output, it can be > max.
  728. a = (i % 2) + v * sign
  729. b = a + 0.02 * sign
  730. if v > 0.99 :
  731. # did you notice overlapping sectors?
  732. gradient.setColorAt((i+a)/rays, color)
  733. gradient.setColorAt((i+b)/rays, color.darker(130))
  734. else:
  735. b = max(-1, min(1, b))
  736. gradient.setColorAt((i+a)/rays, color.darker(130))
  737. gradient.setColorAt((i+b)/rays, color.darker(300))
  738. self.grayGradPen(painter, X, Y, 0, -1, width=chamfer)
  739. painter.setBrush(gradient)
  740. painter.drawEllipse(squareBorder(-W*0.4))
  741. # cap is black itself, but looks dark green on working tube.
  742. self.drawMark(painter, X, Y, 0, 0, 0, W/4, color.darker(800))
  743. else:
  744. # VU scale points
  745. for i in range(0, 5+1):
  746. angle = ((0.5-i/5) * 110 + 90)
  747. self.drawMark(painter, X, Y + R*0.6, R*1.3, R*1.5, angle, lineWidth-0.5, color.darker(150))
  748. # VU pointer
  749. angle = ((0.5-normValue) * 110 + 90)
  750. self.drawMark(painter, X, Y + R*0.6, 0, R*1.3, angle, lineWidth, color)
  751. # Draw "settling screw" of VU meter # Work in progress here. NOTE
  752. if substrate:
  753. painter.setPen(substratePen)
  754. painter.drawEllipse(QRectF(X-width, Y+R*0.6-width, width*2, width*2))
  755. # ---------------------------------------------------------------------------------------------------------------------