diff --git a/source/frontend/widgets/scalabledial.py b/source/frontend/widgets/scalabledial.py index 0d842c738..074994e98 100644 --- a/source/frontend/widgets/scalabledial.py +++ b/source/frontend/widgets/scalabledial.py @@ -5,18 +5,17 @@ # --------------------------------------------------------------------------------------------------------------------- # Imports (Global) -from math import cos, floor, pi, sin +from math import cos, floor, pi, sin, copysign +import numpy as np from qt_compat import qt_config if qt_config == 5: from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPointF, QRectF, QTimer, QSize - from PyQt5.QtGui import QColor, QConicalGradient, QFontMetrics, QPainterPath, QPen, QPixmap - from PyQt5.QtSvg import QSvgWidget + from PyQt5.QtGui import QColor, QLinearGradient, QRadialGradient, QConicalGradient, QFontMetrics, QPen elif qt_config == 6: from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QPointF, QRectF, QTimer, QSize - from PyQt6.QtGui import QColor, QConicalGradient, QFontMetrics, QPainterPath, QPen, QPixmap - from PyQt6.QtSvgWidgets import QSvgWidget + from PyQt6.QtGui import QColor, QLinearGradient, QRadialGradient, QConicalGradient, QFontMetrics, QPen from .commondial import CommonDial from carla_shared import fontMetricsHorizontalAdvance @@ -27,44 +26,14 @@ from carla_shared import fontMetricsHorizontalAdvance class ScalableDial(CommonDial): def __init__(self, parent, index=0): CommonDial.__init__(self, parent, index) - - self.fImage = QSvgWidget(":/scalable/dial_03.svg") - self.fImageNum = "01" - - if self.fImage.sizeHint().width() > self.fImage.sizeHint().height(): - self.fImageOrientation = self.HORIZONTAL - else: - self.fImageOrientation = self.VERTICAL - - self.updateSizes() + self.setImage(0) def getBaseSize(self): return self.fImageBaseSize def updateSizes(self): - if isinstance(self.fImage, QPixmap): - self.fImageWidth = self.fImage.width() - self.fImageHeight = self.fImage.height() - else: - self.fImageWidth = self.fImage.sizeHint().width() - self.fImageHeight = self.fImage.sizeHint().height() - - if self.fImageWidth < 1: - self.fImageWidth = 1 - - if self.fImageHeight < 1: - self.fImageHeight = 1 - - if self.fImageOrientation == self.HORIZONTAL: - self.fImageBaseSize = self.fImageHeight - self.fImageLayersCount = self.fImageWidth / self.fImageHeight - else: - self.fImageBaseSize = self.fImageWidth - self.fImageLayersCount = self.fImageHeight / self.fImageWidth - self.setMinimumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5) self.setMaximumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5) - if not self.fLabel: self.fLabelHeight = 0 self.fLabelWidth = 0 @@ -76,9 +45,9 @@ class ScalableDial(CommonDial): self.fLabelPos.setX(float(self.fImageBaseSize)/2.0 - float(self.fLabelWidth)/2.0) - if self.fImageNum in ("01", "02", "07", "08", "09", "10"): + if self.fImageId in (1, 2, 7, 8, 9, 10): self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight) - elif self.fImageNum in ("11",): + elif self.fImageId in (11,): self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight*2/3) else: self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight/2) @@ -90,46 +59,33 @@ class ScalableDial(CommonDial): float(self.fImageBaseSize*3)/4.0, self.fImageBaseSize+self.fLabelHeight+5) def setImage(self, imageId): - self.fImageNum = "%02i" % imageId - if imageId in (2,6,7,8,9,10,11,12,13): - img = ":/bitmaps/dial_%s%s.png" % (self.fImageNum, "" if self.isEnabled() else "d") - else: - img = ":/scalable/dial_%s%s.svg" % (self.fImageNum, "" if self.isEnabled() else "d") - - if img.endswith(".png"): - if not isinstance(self.fImage, QPixmap): - self.fImage = QPixmap() - else: - if not isinstance(self.fImage, QSvgWidget): - self.fImage = QSvgWidget() - - self.fImage.load(img) + self.fImageId = imageId + self.fImageBaseSize = 30 # internal + if (imageId == 0): + return - if self.fImage.width() > self.fImage.height(): - self.fImageOrientation = self.HORIZONTAL - else: - self.fImageOrientation = self.VERTICAL + if imageId in (7, 8, 9, 10): # calf + self.fImageBaseSize = 40 + elif imageId in (11, 12, 13): # openav + self.fImageBaseSize = 32 # special svgs if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL: # reserved for carla-wet, carla-vol, carla-pan and color - if self.fImageNum == "03": + if self.fImageId == 3: self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_COLOR # reserved for carla-L and carla-R - elif self.fImageNum == "04": + elif self.fImageId == 4: self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_CARLA_L - - # reserved for zita - elif self.fImageNum == "06": - self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_ZITA + self.fImageBaseSize = 26 self.updateSizes() self.update() @pyqtSlot() def slot_updateImage(self): - self.setImage(int(self.fImageNum)) + self.setImage(int(self.fImageId)) def minimumSizeHint(self): return QSize(self.fImageBaseSize, self.fImageBaseSize) @@ -145,148 +101,204 @@ class ScalableDial(CommonDial): self.slot_updateImage() def paintDial(self, painter): + + # Replace Qt draw over substrate bitmap or svg to + # all-in-one widget generated from stratch using Qt only, + # make it highly tuneable, and uniformly look like + # using HSL color model to make same brightness of colored things. + # We can also easily have color tinted (themed) knobs. + # Some things were simplified a little, to gain more speed. + # R: knob nib (cap) radius + def createDial(R, hue1, hue2, barWidth, angleSpan, ticks, dialType, value): + # X,Y: center + X = Y = self.fImageBaseSize / 2 + + ang0 = int((angleSpan/2+90)*16) + ang1 = -angleSpan*16 + + if (value < -1.1): + value = 0 + enabled = 0 + enlight = 0 + else: + enabled = 1 + enlight = self.fHoverStep / 40 + + def rectBorder(w): + return QRectF(X-R-w, Y-R-w, (R+w)*2, (R+w)*2) + + def gray(luma): + return QColor.fromHslF(0, 0, luma, 1) + + S = 0.9 # Saturation + color0 = Qt.black + color1 = QColor.fromHslF(hue1, S, 0.5+enlight, 1) + color2 = QColor.fromHslF(hue2, S, 0.5+enlight, 1) + + if dialType == 1: # mimic svg dial + # light arc substrate: near black, 0.5 px exposed + painter.setPen(QPen(gray(0.10), barWidth+1, cap=Qt.FlatCap)) + painter.drawArc(rectBorder(barWidth), ang0, ang1) + + # light arc: gray bar + # should be combined with light (value) arc to be a bit faster. TODO + g1 = QConicalGradient(X, Y, 270) + g1.setColorAt(0.0, gray(0.20)) # right part + g1.setColorAt(1.0, gray(0.15)) # left part + painter.setPen(QPen(g1, barWidth, cap=Qt.FlatCap)) + painter.drawArc(rectBorder(barWidth), ang0, ang1) + + # cap + g2 = QRadialGradient(X-R, Y-R, R*2) + g2.setColorAt(0.0, gray(0.45+enlight)) + g2.setColorAt(1.0, gray(0.15+enlight)) + painter.setBrush(g2) + painter.setPen(QPen(g2, 1)) + painter.drawEllipse(rectBorder(0.5)) + + elif dialType == 2: # calf + # outer chamfer + g1 = QConicalGradient(X, Y, 135) + g1.setColorAt(0.0, gray(0.15)) + g1.setColorAt(0.5, gray(0.50)) + g1.setColorAt(1.0, gray(0.15)) + painter.setPen(QPen(g1, 2, cap=Qt.FlatCap)) + painter.drawArc(rectBorder(barWidth*2-1), 0, 360*16) + + # knob chamfer + g2 = QConicalGradient(X, Y, -45) + g2.setColorAt(0.0, gray(0.15)) + g2.setColorAt(0.5, gray(0.50)) + g2.setColorAt(1.0, gray(0.15)) + painter.setPen(QPen(g2, 2, cap=Qt.FlatCap)) + painter.drawArc(rectBorder(1), 0, 360*16) + + # leds substrate + # should be combined with light (value) "arc" to be a bit faster. TODO + # painter.setPen(QPen(gray(0.05+enlight), barWidth + 1)) + painter.setPen(QPen(QColor.fromHslF(hue1, S, 0.05+enlight/2, 1), barWidth+1)) + painter.drawArc(rectBorder(barWidth), 0, 360*16) + + # machined shiny cap + g3 = QConicalGradient(X, Y, 0) + for i in (5.9, 10.7, 15.7, 20.8, 25.8, 30.6, 40.6, 45.9, + 55.9, 60.7, 65.7, 70.8, 75.8, 80.6, 90.6, 95.9): + g3.setColorAt(int(i)/100.0, gray(i % 1.0)) + painter.setBrush(g3) + painter.setPen(QPen(g3, 1)) + painter.drawEllipse(rectBorder(0)) + + elif dialType == 3: # openav + # light arc substrate + painter.setPen(QPen(gray(0.20+enlight), barWidth)) + painter.drawArc(rectBorder(barWidth), ang0, ang1) + + + # do not draw marks on disabled items + if enabled == 1: + # Forward or reverse (for 'R' ch knob) + a = ((0.5-abs(value)) * copysign(angleSpan, value) + 90) * pi/180 + + if dialType == 1: # ball + x = X + R * 0.8 * cos(a) + y = Y - R * 0.8 * sin(a) + w = barWidth - 0.5 + painter.setBrush(color1) + painter.setPen(QPen(color1, 0)) + painter.drawEllipse(QRectF(x-w/2, y-w/2, w, w)) + + elif dialType == 2: # line for calf + x = X + R * 0.9 * cos(a) + y = Y - R * 0.9 * sin(a) + x1 = X + R * 0.6 * cos(a) + y1 = Y - R * 0.6 * sin(a) + painter.setPen(QPen(Qt.black, 1.5)) + painter.drawLine(QPointF(x, y), QPointF(x1, y1)) + + elif dialType == 3: # line for openav + x = X + (R + barWidth) * cos(a) + y = Y - (R + barWidth) * sin(a) + painter.setPen(QPen(color1, barWidth, cap=Qt.RoundCap)) + painter.drawLine(QPointF(x, y), QPointF(X, Y)) + + # draw arc: forward, or reverse (for 'R' ch knob) + startAngle = int((copysign(angleSpan/2, value) + 90)*16) + + gradient = QConicalGradient(X, Y, 270) + if (ticks == 0): + spanAngle = int(-angleSpan*16 * value) + gradient.setColorAt(0.25, color1) + gradient.setColorAt(0.75, color2) + else: + spanAngle = int(-int(angleSpan*value/(360/ticks)+0.5)*(360/ticks)*16) + n = ticks*2 + for i in range(0, n, 2): + gradient.setColorAt((i-0.40)/n, color0) + gradient.setColorAt((i+0.85)/n, color1.lighter(100)) + + if dialType == 3: # openav + painter.setPen(QPen(gradient, barWidth, cap=Qt.RoundCap)) + else: + painter.setPen(QPen(gradient, barWidth, cap=Qt.FlatCap)) + painter.drawArc(QRectF(rectBorder(barWidth)), startAngle, spanAngle) + if self.isEnabled(): normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum) - curLayer = int((self.fImageLayersCount - 1) * normValue) - - if self.fImageOrientation == self.HORIZONTAL: - xpos = self.fImageBaseSize * curLayer - ypos = 0.0 + else: + normValue = -9.9 + + # Custom knobs (Dry/Wet and Volume) + if self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_WET, self.CUSTOM_PAINT_MODE_CARLA_VOL): + hue1 = 0.3 # Green + hue2 = 0.5 # Blue + if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_VOL: + hue1 = hue2 + + createDial(10, hue1, hue2, 3, 260, 0, 1, normValue) + + # Custom knobs (L and R) + elif self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_L, self.CUSTOM_PAINT_MODE_CARLA_R): + hue = 0.21 # Lime + if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_R: + # Shift to full negative range (incl. 0) for reversed knob + normValue = normValue - 1 - np.nextafter(0, 1) + + createDial(8, hue, hue, 2.5, 260, 0, 1, normValue) + + # Custom knobs (Color) + elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_COLOR: + # FIXME It's better to move it below, because calf is not custom colored. + # (need to change not in this file) + if self.fImageId in (1, 2, 7, 8, 9, 10): # calf + hue = 0.52 # Aqua + + createDial(12, hue, hue, 4, 296, 36, 2, normValue) + + else: # internal + # NOTE here all incoming color data, except hue, is lost. + hue = self.fCustomPaintColor.hueF() + + createDial(10, hue, hue, 3, 260, 0, 1, normValue) + + # Custom knobs + elif self.fImageId in (11, 12, 13): # openav + if self.fImageId == 12: + hue1 = hue2 = 0.5 # Blue + elif self.fImageId == 13: + hue1 = 0.3 # Green + hue2 = 0.5 # Blue else: - xpos = 0.0 - ypos = self.fImageBaseSize * curLayer + hue1 = hue2 = 0.05 # Orange - source = QRectF(xpos, ypos, self.fImageBaseSize, self.fImageBaseSize) + createDial(12, hue1, hue2, 2.5, 270, 0, 3, normValue) - if isinstance(self.fImage, QPixmap): - target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize) - painter.drawPixmap(target, self.fImage, source) - else: - self.fImage.renderer().render(painter, source) - - # Custom knobs (Dry/Wet and Volume) - if self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_WET, self.CUSTOM_PAINT_MODE_CARLA_VOL): - # knob color - colorGreen = QColor(0x5D, 0xE7, 0x3D).lighter(100 + self.fHoverStep*6) - colorBlue = QColor(0x3E, 0xB8, 0xBE).lighter(100 + self.fHoverStep*6) - - # draw small circle - ballRect = QRectF(8.0, 8.0, 15.0, 15.0) - ballPath = QPainterPath() - ballPath.addEllipse(ballRect) - #painter.drawRect(ballRect) - tmpValue = (0.375 + 0.75*normValue) - ballValue = tmpValue - floor(tmpValue) - ballPoint = ballPath.pointAtPercent(ballValue) - - # draw arc - startAngle = 218*16 - spanAngle = -255*16*normValue - - if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_WET: - painter.setBrush(colorBlue) - painter.setPen(QPen(colorBlue, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) - - gradient = QConicalGradient(15.5, 15.5, -45) - gradient.setColorAt(0.0, colorBlue) - gradient.setColorAt(0.125, colorBlue) - gradient.setColorAt(0.625, colorGreen) - gradient.setColorAt(0.75, colorGreen) - gradient.setColorAt(0.76, colorGreen) - gradient.setColorAt(1.0, colorGreen) - painter.setBrush(gradient) - painter.setPen(QPen(gradient, 3)) - - else: - painter.setBrush(colorBlue) - painter.setPen(QPen(colorBlue, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) - - painter.setBrush(colorBlue) - painter.setPen(QPen(colorBlue, 3)) - - painter.drawArc(QRectF(4.0, 4.0, 26.0, 26.0), int(startAngle), int(spanAngle)) - - # Custom knobs (L and R) - elif self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_L, self.CUSTOM_PAINT_MODE_CARLA_R): - # knob color - color = QColor(0xAD, 0xD5, 0x48).lighter(100 + self.fHoverStep*6) - - # draw small circle - ballRect = QRectF(7.0, 8.0, 11.0, 12.0) - ballPath = QPainterPath() - ballPath.addEllipse(ballRect) - #painter.drawRect(ballRect) - tmpValue = (0.375 + 0.75*normValue) - ballValue = tmpValue - floor(tmpValue) - ballPoint = ballPath.pointAtPercent(ballValue) - - painter.setBrush(color) - painter.setPen(QPen(color, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.0, 2.0)) - - # draw arc - if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L: - startAngle = 218*16 - spanAngle = -255*16*normValue - else: - startAngle = 322.0*16 - spanAngle = 255.0*16*(1.0-normValue) - - painter.setPen(QPen(color, 2.5)) - painter.drawArc(QRectF(3.5, 3.5, 22.0, 22.0), int(startAngle), int(spanAngle)) - - # Custom knobs (Color) - elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_COLOR: - # knob color - color = self.fCustomPaintColor.lighter(100 + self.fHoverStep*6) - - # draw small circle - ballRect = QRectF(8.0, 8.0, 15.0, 15.0) - ballPath = QPainterPath() - ballPath.addEllipse(ballRect) - tmpValue = (0.375 + 0.75*normValue) - ballValue = tmpValue - floor(tmpValue) - ballPoint = ballPath.pointAtPercent(ballValue) - - # draw arc - startAngle = 218*16 - spanAngle = -255*16*normValue - - painter.setBrush(color) - painter.setPen(QPen(color, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) - - painter.setBrush(color) - painter.setPen(QPen(color, 3)) - painter.drawArc(QRectF(4.0, 4.8, 26.0, 26.0), int(startAngle), int(spanAngle)) - - # Custom knobs (Zita) - elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_ZITA: - a = normValue * pi * 1.5 - 2.35 - r = 10.0 - x = 10.5 - y = 10.5 - x += r * sin(a) - y -= r * cos(a) - painter.setBrush(Qt.black) - painter.setPen(QPen(Qt.black, 2)) - painter.drawLine(QPointF(11.0, 11.0), QPointF(x, y)) - - # Custom knobs - else: - return + else: + return - if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX: - self.fHoverStep += 1 if self.fIsHovered else -1 - QTimer.singleShot(20, self.update) - else: # isEnabled() - target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize) - if isinstance(self.fImage, QPixmap): - painter.drawPixmap(target, self.fImage, target) - else: - self.fImage.renderer().render(painter, target) + # Maybe it's better to be disabled when not self.isEnabled() ? FIXME + if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX: + self.fHoverStep += 1 if self.fIsHovered else -1 + QTimer.singleShot(20, self.update) # ---------------------------------------------------------------------------------------------------------------------