|
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
-
- # Scalable Dial, a custom Qt widget
- # Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com>
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License as
- # published by the Free Software Foundation; either version 2 of
- # the License, or any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # For a full copy of the GNU General Public License see the doc/GPL.txt file.
-
- # ------------------------------------------------------------------------------------------------------------
- # Imports (Global)
-
- from math import cos, floor, pi, sin, isnan
-
- from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QPointF, QRectF, QTimer, QSize
- from PyQt5.QtGui import QColor, QConicalGradient, QFont, QFontMetrics
- from PyQt5.QtGui import QLinearGradient, QPainter, QPainterPath, QPen, QPixmap
- from PyQt5.QtSvg import QSvgWidget
- from PyQt5.QtWidgets import QDial
-
- # ------------------------------------------------------------------------------------------------------------
- # Widget Class
-
- class ScalableDial(QDial):
- # enum CustomPaintMode
- CUSTOM_PAINT_MODE_NULL = 0 # default (NOTE: only this mode has label gradient)
- CUSTOM_PAINT_MODE_CARLA_WET = 1 # color blue-green gradient (reserved #3)
- CUSTOM_PAINT_MODE_CARLA_VOL = 2 # color blue (reserved #3)
- CUSTOM_PAINT_MODE_CARLA_L = 3 # color yellow (reserved #4)
- CUSTOM_PAINT_MODE_CARLA_R = 4 # color yellow (reserved #4)
- CUSTOM_PAINT_MODE_CARLA_PAN = 5 # color yellow (reserved #3)
- CUSTOM_PAINT_MODE_COLOR = 6 # color, selectable (reserved #3)
- CUSTOM_PAINT_MODE_ZITA = 7 # custom zita knob (reserved #6)
- CUSTOM_PAINT_MODE_NO_GRADIENT = 8 # skip label gradient
-
- # enum Orientation
- HORIZONTAL = 0
- VERTICAL = 1
-
- HOVER_MIN = 0
- HOVER_MAX = 9
-
- MODE_DEFAULT = 0
- MODE_LINEAR = 1
-
- # signals
- dragStateChanged = pyqtSignal(bool)
- realValueChanged = pyqtSignal(float)
-
- def __init__(self, parent, index=0):
- QDial.__init__(self, parent)
-
- self.fDialMode = self.MODE_LINEAR
-
- self.fMinimum = 0.0
- self.fMaximum = 1.0
- self.fRealValue = 0.0
- self.fPrecision = 10000
- self.fIsInteger = False
-
- self.fIsHovered = False
- self.fIsPressed = False
- self.fHoverStep = self.HOVER_MIN
-
- self.fLastDragPos = None
- self.fLastDragValue = 0.0
-
- self.fIndex = 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.fLabel = ""
- self.fLabelPos = QPointF(0.0, 0.0)
- self.fLabelFont = QFont(self.font())
- self.fLabelFont.setPixelSize(8)
- self.fLabelWidth = 0
- self.fLabelHeight = 0
-
- if self.palette().window().color().lightness() > 100:
- # Light background
- c = self.palette().dark().color()
- self.fLabelGradientColor1 = c
- self.fLabelGradientColor2 = QColor(c.red(), c.green(), c.blue(), 0)
- self.fLabelGradientColorT = [self.palette().buttonText().color(), self.palette().mid().color()]
- else:
- # Dark background
- self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
- self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
- self.fLabelGradientColorT = [Qt.white, Qt.darkGray]
-
- self.fLabelGradient = QLinearGradient(0, 0, 0, 1)
- self.fLabelGradient.setColorAt(0.0, self.fLabelGradientColor1)
- self.fLabelGradient.setColorAt(0.6, self.fLabelGradientColor1)
- self.fLabelGradient.setColorAt(1.0, self.fLabelGradientColor2)
-
- self.fLabelGradientRect = QRectF(0.0, 0.0, 0.0, 0.0)
-
- self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_NULL
- self.fCustomPaintColor = QColor(0xff, 0xff, 0xff)
-
- self.updateSizes()
-
- # Fake internal value, custom precision
- QDial.setMinimum(self, 0)
- QDial.setMaximum(self, self.fPrecision)
- QDial.setValue(self, 0)
-
- self.valueChanged.connect(self.slot_valueChanged)
-
- def getIndex(self):
- return self.fIndex
-
- def getBaseSize(self):
- return self.fImageBaseSize
-
- def forceWhiteLabelGradientText(self):
- self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
- self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
- self.fLabelGradientColorT = [Qt.white, Qt.darkGray]
-
- def setLabelColor(self, enabled, disabled):
- self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
- self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
- self.fLabelGradientColorT = [enabled, disabled]
-
- 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
- return
-
- self.fLabelWidth = QFontMetrics(self.fLabelFont).width(self.fLabel)
- self.fLabelHeight = QFontMetrics(self.fLabelFont).height()
-
- self.fLabelPos.setX(float(self.fImageBaseSize)/2.0 - float(self.fLabelWidth)/2.0)
-
- if self.fImageNum in ("01", "02", "07", "08", "09", "10"):
- self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight)
- elif self.fImageNum in ("11",):
- self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight*2/3)
- else:
- self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight/2)
-
- self.fLabelGradient.setStart(0, float(self.fImageBaseSize)/2.0)
- self.fLabelGradient.setFinalStop(0, self.fImageBaseSize + self.fLabelHeight + 5)
-
- self.fLabelGradientRect = QRectF(float(self.fImageBaseSize)/8.0, float(self.fImageBaseSize)/2.0,
- float(self.fImageBaseSize*3)/4.0, self.fImageBaseSize+self.fLabelHeight+5)
-
- def setCustomPaintMode(self, paintMode):
- if self.fCustomPaintMode == paintMode:
- return
-
- self.fCustomPaintMode = paintMode
- self.update()
-
- def setCustomPaintColor(self, color):
- if self.fCustomPaintColor == color:
- return
-
- self.fCustomPaintColor = color
- self.update()
-
- def setLabel(self, label):
- if self.fLabel == label:
- return
-
- self.fLabel = label
- self.updateSizes()
- self.update()
-
- def setIndex(self, index):
- self.fIndex = index
-
- def setImage(self, imageId):
- self.fImageNum = "%02i" % imageId
- if imageId in (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)
-
- if self.fImage.width() > self.fImage.height():
- self.fImageOrientation = self.HORIZONTAL
- else:
- self.fImageOrientation = self.VERTICAL
-
- # special svgs
- if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
- # reserved for carla-wet, carla-vol, carla-pan and color
- if self.fImageNum == "03":
- self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_COLOR
-
- # reserved for carla-L and carla-R
- elif self.fImageNum == "04":
- self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_CARLA_L
-
- # reserved for zita
- elif self.fImageNum == "06":
- self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_ZITA
-
- self.updateSizes()
- self.update()
-
- def setPrecision(self, value, isInteger):
- self.fPrecision = value
- self.fIsInteger = isInteger
- QDial.setMaximum(self, value)
-
- def setMinimum(self, value):
- self.fMinimum = value
-
- def setMaximum(self, value):
- self.fMaximum = value
-
- def setValue(self, value, emitSignal=False):
- if self.fRealValue == value or isnan(value):
- return
-
- if value <= self.fMinimum:
- qtValue = 0
- self.fRealValue = self.fMinimum
-
- elif value >= self.fMaximum:
- qtValue = self.fPrecision
- self.fRealValue = self.fMaximum
-
- else:
- qtValue = round(float(value - self.fMinimum) / float(self.fMaximum - self.fMinimum) * self.fPrecision)
- self.fRealValue = value
-
- # Block change signal, we'll handle it ourselves
- self.blockSignals(True)
- QDial.setValue(self, qtValue)
- self.blockSignals(False)
-
- if emitSignal:
- self.realValueChanged.emit(self.fRealValue)
-
- @pyqtSlot(int)
- def slot_valueChanged(self, value):
- self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum
- self.realValueChanged.emit(self.fRealValue)
-
- @pyqtSlot()
- def slot_updateImage(self):
- self.setImage(int(self.fImageNum))
-
- def minimumSizeHint(self):
- return QSize(self.fImageBaseSize, self.fImageBaseSize)
-
- def sizeHint(self):
- return QSize(self.fImageBaseSize, self.fImageBaseSize)
-
- def changeEvent(self, event):
- QDial.changeEvent(self, event)
-
- # Force svg update if enabled state changes
- if event.type() == QEvent.EnabledChange:
- self.slot_updateImage()
-
- def enterEvent(self, event):
- self.fIsHovered = True
- if self.fHoverStep == self.HOVER_MIN:
- self.fHoverStep = self.HOVER_MIN + 1
- QDial.enterEvent(self, event)
-
- def leaveEvent(self, event):
- self.fIsHovered = False
- if self.fHoverStep == self.HOVER_MAX:
- self.fHoverStep = self.HOVER_MAX - 1
- QDial.leaveEvent(self, event)
-
- def mousePressEvent(self, event):
- if self.fDialMode == self.MODE_DEFAULT:
- return QDial.mousePressEvent(self, event)
-
- if event.button() == Qt.LeftButton:
- self.fIsPressed = True
- self.fLastDragPos = event.pos()
- self.fLastDragValue = self.fRealValue
- self.dragStateChanged.emit(True)
-
- def mouseMoveEvent(self, event):
- if self.fDialMode == self.MODE_DEFAULT:
- return QDial.mouseMoveEvent(self, event)
-
- if not self.fIsPressed:
- return
-
- range = (self.fMaximum - self.fMinimum) / 4.0
- pos = event.pos()
- dx = range * float(pos.x() - self.fLastDragPos.x()) / self.width()
- dy = range * float(pos.y() - self.fLastDragPos.y()) / self.height()
- value = self.fLastDragValue + dx - dy
-
- if value < self.fMinimum:
- value = self.fMinimum
- elif value > self.fMaximum:
- value = self.fMaximum
- elif self.fIsInteger:
- value = float(round(value))
-
- self.setValue(value, True)
-
- def mouseReleaseEvent(self, event):
- if self.fDialMode == self.MODE_DEFAULT:
- return QDial.mouseReleaseEvent(self, event)
-
- if self.fIsPressed:
- self.fIsPressed = False
- self.dragStateChanged.emit(False)
-
- def paintEvent(self, event):
- painter = QPainter(self)
- event.accept()
-
- painter.save()
- painter.setRenderHint(QPainter.Antialiasing, True)
-
- if self.fLabel:
- if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
- painter.setPen(self.fLabelGradientColor2)
- painter.setBrush(self.fLabelGradient)
- painter.drawRect(self.fLabelGradientRect)
-
- painter.setFont(self.fLabelFont)
- painter.setPen(self.fLabelGradientColorT[0 if self.isEnabled() else 1])
- painter.drawText(self.fLabelPos, self.fLabel)
-
- 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:
- xpos = 0.0
- ypos = self.fImageBaseSize * curLayer
-
- source = QRectF(xpos, ypos, self.fImageBaseSize, self.fImageBaseSize)
-
- 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(4.0, 4.0, 26.0, 26.0, startAngle, 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(3.5, 3.5, 22.0, 22.0, startAngle, 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(4.0, 4.8, 26.0, 26.0, startAngle, 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:
- painter.restore()
- 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)
-
- painter.restore()
-
- def resizeEvent(self, event):
- QDial.resizeEvent(self, event)
- self.updateSizes()
-
- # ------------------------------------------------------------------------------------------------------------
- # Main Testing
-
- if __name__ == '__main__':
- import sys
- from PyQt5.QtWidgets import QApplication
- import resources_rc
-
- app = QApplication(sys.argv)
- gui = ScalableDial(None)
- #gui.setEnabled(True)
- #gui.setEnabled(False)
- gui.setSvg(3)
- gui.setLabel("hahaha")
- gui.show()
-
- sys.exit(app.exec_())
|