| @@ -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) | |||
| # --------------------------------------------------------------------------------------------------------------------- | |||