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.

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