1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Scalable Dial, a custom Qt widget 5# Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com> 6# 7# This program is free software; you can redistribute it and/or 8# modify it under the terms of the GNU General Public License as 9# published by the Free Software Foundation; either version 2 of 10# the License, or any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# For a full copy of the GNU General Public License see the doc/GPL.txt file. 18 19# ------------------------------------------------------------------------------------------------------------ 20# Imports (Global) 21 22from math import cos, floor, pi, sin, isnan 23 24from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent, QPointF, QRectF, QTimer, QSize 25from PyQt5.QtGui import QColor, QConicalGradient, QFont, QFontMetrics 26from PyQt5.QtGui import QLinearGradient, QPainter, QPainterPath, QPen, QPixmap 27from PyQt5.QtSvg import QSvgWidget 28from PyQt5.QtWidgets import QDial 29 30# ------------------------------------------------------------------------------------------------------------ 31# Widget Class 32 33class ScalableDial(QDial): 34 # enum CustomPaintMode 35 CUSTOM_PAINT_MODE_NULL = 0 # default (NOTE: only this mode has label gradient) 36 CUSTOM_PAINT_MODE_CARLA_WET = 1 # color blue-green gradient (reserved #3) 37 CUSTOM_PAINT_MODE_CARLA_VOL = 2 # color blue (reserved #3) 38 CUSTOM_PAINT_MODE_CARLA_L = 3 # color yellow (reserved #4) 39 CUSTOM_PAINT_MODE_CARLA_R = 4 # color yellow (reserved #4) 40 CUSTOM_PAINT_MODE_CARLA_PAN = 5 # color yellow (reserved #3) 41 CUSTOM_PAINT_MODE_COLOR = 6 # color, selectable (reserved #3) 42 CUSTOM_PAINT_MODE_ZITA = 7 # custom zita knob (reserved #6) 43 CUSTOM_PAINT_MODE_NO_GRADIENT = 8 # skip label gradient 44 45 # enum Orientation 46 HORIZONTAL = 0 47 VERTICAL = 1 48 49 HOVER_MIN = 0 50 HOVER_MAX = 9 51 52 MODE_DEFAULT = 0 53 MODE_LINEAR = 1 54 55 # signals 56 dragStateChanged = pyqtSignal(bool) 57 realValueChanged = pyqtSignal(float) 58 59 def __init__(self, parent, index=0): 60 QDial.__init__(self, parent) 61 62 self.fDialMode = self.MODE_LINEAR 63 64 self.fMinimum = 0.0 65 self.fMaximum = 1.0 66 self.fRealValue = 0.0 67 self.fPrecision = 10000 68 self.fIsInteger = False 69 70 self.fIsHovered = False 71 self.fIsPressed = False 72 self.fHoverStep = self.HOVER_MIN 73 74 self.fLastDragPos = None 75 self.fLastDragValue = 0.0 76 77 self.fIndex = index 78 self.fImage = QSvgWidget(":/scalable/dial_03.svg") 79 self.fImageNum = "01" 80 81 if self.fImage.sizeHint().width() > self.fImage.sizeHint().height(): 82 self.fImageOrientation = self.HORIZONTAL 83 else: 84 self.fImageOrientation = self.VERTICAL 85 86 self.fLabel = "" 87 self.fLabelPos = QPointF(0.0, 0.0) 88 self.fLabelFont = QFont(self.font()) 89 self.fLabelFont.setPixelSize(8) 90 self.fLabelWidth = 0 91 self.fLabelHeight = 0 92 93 if self.palette().window().color().lightness() > 100: 94 # Light background 95 c = self.palette().dark().color() 96 self.fLabelGradientColor1 = c 97 self.fLabelGradientColor2 = QColor(c.red(), c.green(), c.blue(), 0) 98 self.fLabelGradientColorT = [self.palette().buttonText().color(), self.palette().mid().color()] 99 else: 100 # Dark background 101 self.fLabelGradientColor1 = QColor(0, 0, 0, 255) 102 self.fLabelGradientColor2 = QColor(0, 0, 0, 0) 103 self.fLabelGradientColorT = [Qt.white, Qt.darkGray] 104 105 self.fLabelGradient = QLinearGradient(0, 0, 0, 1) 106 self.fLabelGradient.setColorAt(0.0, self.fLabelGradientColor1) 107 self.fLabelGradient.setColorAt(0.6, self.fLabelGradientColor1) 108 self.fLabelGradient.setColorAt(1.0, self.fLabelGradientColor2) 109 110 self.fLabelGradientRect = QRectF(0.0, 0.0, 0.0, 0.0) 111 112 self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_NULL 113 self.fCustomPaintColor = QColor(0xff, 0xff, 0xff) 114 115 self.updateSizes() 116 117 # Fake internal value, custom precision 118 QDial.setMinimum(self, 0) 119 QDial.setMaximum(self, self.fPrecision) 120 QDial.setValue(self, 0) 121 122 self.valueChanged.connect(self.slot_valueChanged) 123 124 def getIndex(self): 125 return self.fIndex 126 127 def getBaseSize(self): 128 return self.fImageBaseSize 129 130 def forceWhiteLabelGradientText(self): 131 self.fLabelGradientColor1 = QColor(0, 0, 0, 255) 132 self.fLabelGradientColor2 = QColor(0, 0, 0, 0) 133 self.fLabelGradientColorT = [Qt.white, Qt.darkGray] 134 135 def setLabelColor(self, enabled, disabled): 136 self.fLabelGradientColor1 = QColor(0, 0, 0, 255) 137 self.fLabelGradientColor2 = QColor(0, 0, 0, 0) 138 self.fLabelGradientColorT = [enabled, disabled] 139 140 def updateSizes(self): 141 if isinstance(self.fImage, QPixmap): 142 self.fImageWidth = self.fImage.width() 143 self.fImageHeight = self.fImage.height() 144 else: 145 self.fImageWidth = self.fImage.sizeHint().width() 146 self.fImageHeight = self.fImage.sizeHint().height() 147 148 if self.fImageWidth < 1: 149 self.fImageWidth = 1 150 151 if self.fImageHeight < 1: 152 self.fImageHeight = 1 153 154 if self.fImageOrientation == self.HORIZONTAL: 155 self.fImageBaseSize = self.fImageHeight 156 self.fImageLayersCount = self.fImageWidth / self.fImageHeight 157 else: 158 self.fImageBaseSize = self.fImageWidth 159 self.fImageLayersCount = self.fImageHeight / self.fImageWidth 160 161 self.setMinimumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5) 162 self.setMaximumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5) 163 164 if not self.fLabel: 165 self.fLabelHeight = 0 166 self.fLabelWidth = 0 167 return 168 169 self.fLabelWidth = QFontMetrics(self.fLabelFont).width(self.fLabel) 170 self.fLabelHeight = QFontMetrics(self.fLabelFont).height() 171 172 self.fLabelPos.setX(float(self.fImageBaseSize)/2.0 - float(self.fLabelWidth)/2.0) 173 174 if self.fImageNum in ("01", "02", "07", "08", "09", "10"): 175 self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight) 176 elif self.fImageNum in ("11",): 177 self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight*2/3) 178 else: 179 self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight/2) 180 181 self.fLabelGradient.setStart(0, float(self.fImageBaseSize)/2.0) 182 self.fLabelGradient.setFinalStop(0, self.fImageBaseSize + self.fLabelHeight + 5) 183 184 self.fLabelGradientRect = QRectF(float(self.fImageBaseSize)/8.0, float(self.fImageBaseSize)/2.0, 185 float(self.fImageBaseSize*3)/4.0, self.fImageBaseSize+self.fLabelHeight+5) 186 187 def setCustomPaintMode(self, paintMode): 188 if self.fCustomPaintMode == paintMode: 189 return 190 191 self.fCustomPaintMode = paintMode 192 self.update() 193 194 def setCustomPaintColor(self, color): 195 if self.fCustomPaintColor == color: 196 return 197 198 self.fCustomPaintColor = color 199 self.update() 200 201 def setLabel(self, label): 202 if self.fLabel == label: 203 return 204 205 self.fLabel = label 206 self.updateSizes() 207 self.update() 208 209 def setIndex(self, index): 210 self.fIndex = index 211 212 def setImage(self, imageId): 213 self.fImageNum = "%02i" % imageId 214 if imageId in (2,6,7,8,9,10,11,12,13): 215 img = ":/bitmaps/dial_%s%s.png" % (self.fImageNum, "" if self.isEnabled() else "d") 216 else: 217 img = ":/scalable/dial_%s%s.svg" % (self.fImageNum, "" if self.isEnabled() else "d") 218 219 if img.endswith(".png"): 220 if not isinstance(self.fImage, QPixmap): 221 self.fImage = QPixmap() 222 else: 223 if not isinstance(self.fImage, QSvgWidget): 224 self.fImage = QSvgWidget() 225 226 self.fImage.load(img) 227 228 if self.fImage.width() > self.fImage.height(): 229 self.fImageOrientation = self.HORIZONTAL 230 else: 231 self.fImageOrientation = self.VERTICAL 232 233 # special svgs 234 if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL: 235 # reserved for carla-wet, carla-vol, carla-pan and color 236 if self.fImageNum == "03": 237 self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_COLOR 238 239 # reserved for carla-L and carla-R 240 elif self.fImageNum == "04": 241 self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_CARLA_L 242 243 # reserved for zita 244 elif self.fImageNum == "06": 245 self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_ZITA 246 247 self.updateSizes() 248 self.update() 249 250 def setPrecision(self, value, isInteger): 251 self.fPrecision = value 252 self.fIsInteger = isInteger 253 QDial.setMaximum(self, value) 254 255 def setMinimum(self, value): 256 self.fMinimum = value 257 258 def setMaximum(self, value): 259 self.fMaximum = value 260 261 def rvalue(self): 262 return self.fRealValue 263 264 def setValue(self, value, emitSignal=False): 265 if self.fRealValue == value or isnan(value): 266 return 267 268 if value <= self.fMinimum: 269 qtValue = 0 270 self.fRealValue = self.fMinimum 271 272 elif value >= self.fMaximum: 273 qtValue = self.fPrecision 274 self.fRealValue = self.fMaximum 275 276 else: 277 qtValue = round(float(value - self.fMinimum) / float(self.fMaximum - self.fMinimum) * self.fPrecision) 278 self.fRealValue = value 279 280 # Block change signal, we'll handle it ourselves 281 self.blockSignals(True) 282 QDial.setValue(self, qtValue) 283 self.blockSignals(False) 284 285 if emitSignal: 286 self.realValueChanged.emit(self.fRealValue) 287 288 @pyqtSlot(int) 289 def slot_valueChanged(self, value): 290 self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum 291 self.realValueChanged.emit(self.fRealValue) 292 293 @pyqtSlot() 294 def slot_updateImage(self): 295 self.setImage(int(self.fImageNum)) 296 297 def minimumSizeHint(self): 298 return QSize(self.fImageBaseSize, self.fImageBaseSize) 299 300 def sizeHint(self): 301 return QSize(self.fImageBaseSize, self.fImageBaseSize) 302 303 def changeEvent(self, event): 304 QDial.changeEvent(self, event) 305 306 # Force svg update if enabled state changes 307 if event.type() == QEvent.EnabledChange: 308 self.slot_updateImage() 309 310 def enterEvent(self, event): 311 self.fIsHovered = True 312 if self.fHoverStep == self.HOVER_MIN: 313 self.fHoverStep = self.HOVER_MIN + 1 314 QDial.enterEvent(self, event) 315 316 def leaveEvent(self, event): 317 self.fIsHovered = False 318 if self.fHoverStep == self.HOVER_MAX: 319 self.fHoverStep = self.HOVER_MAX - 1 320 QDial.leaveEvent(self, event) 321 322 def mousePressEvent(self, event): 323 if self.fDialMode == self.MODE_DEFAULT: 324 return QDial.mousePressEvent(self, event) 325 326 if event.button() == Qt.LeftButton: 327 self.fIsPressed = True 328 self.fLastDragPos = event.pos() 329 self.fLastDragValue = self.fRealValue 330 self.dragStateChanged.emit(True) 331 332 def mouseMoveEvent(self, event): 333 if self.fDialMode == self.MODE_DEFAULT: 334 return QDial.mouseMoveEvent(self, event) 335 336 if not self.fIsPressed: 337 return 338 339 range = (self.fMaximum - self.fMinimum) / 4.0 340 pos = event.pos() 341 dx = range * float(pos.x() - self.fLastDragPos.x()) / self.width() 342 dy = range * float(pos.y() - self.fLastDragPos.y()) / self.height() 343 value = self.fLastDragValue + dx - dy 344 345 if value < self.fMinimum: 346 value = self.fMinimum 347 elif value > self.fMaximum: 348 value = self.fMaximum 349 elif self.fIsInteger: 350 value = float(round(value)) 351 352 self.setValue(value, True) 353 354 def mouseReleaseEvent(self, event): 355 if self.fDialMode == self.MODE_DEFAULT: 356 return QDial.mouseReleaseEvent(self, event) 357 358 if self.fIsPressed: 359 self.fIsPressed = False 360 self.dragStateChanged.emit(False) 361 362 def paintEvent(self, event): 363 painter = QPainter(self) 364 event.accept() 365 366 painter.save() 367 painter.setRenderHint(QPainter.Antialiasing, True) 368 369 if self.fLabel: 370 if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL: 371 painter.setPen(self.fLabelGradientColor2) 372 painter.setBrush(self.fLabelGradient) 373 painter.drawRect(self.fLabelGradientRect) 374 375 painter.setFont(self.fLabelFont) 376 painter.setPen(self.fLabelGradientColorT[0 if self.isEnabled() else 1]) 377 painter.drawText(self.fLabelPos, self.fLabel) 378 379 if self.isEnabled(): 380 normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum) 381 curLayer = int((self.fImageLayersCount - 1) * normValue) 382 383 if self.fImageOrientation == self.HORIZONTAL: 384 xpos = self.fImageBaseSize * curLayer 385 ypos = 0.0 386 else: 387 xpos = 0.0 388 ypos = self.fImageBaseSize * curLayer 389 390 source = QRectF(xpos, ypos, self.fImageBaseSize, self.fImageBaseSize) 391 392 if isinstance(self.fImage, QPixmap): 393 target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize) 394 painter.drawPixmap(target, self.fImage, source) 395 else: 396 self.fImage.renderer().render(painter, source) 397 398 # Custom knobs (Dry/Wet and Volume) 399 if self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_WET, self.CUSTOM_PAINT_MODE_CARLA_VOL): 400 # knob color 401 colorGreen = QColor(0x5D, 0xE7, 0x3D).lighter(100 + self.fHoverStep*6) 402 colorBlue = QColor(0x3E, 0xB8, 0xBE).lighter(100 + self.fHoverStep*6) 403 404 # draw small circle 405 ballRect = QRectF(8.0, 8.0, 15.0, 15.0) 406 ballPath = QPainterPath() 407 ballPath.addEllipse(ballRect) 408 #painter.drawRect(ballRect) 409 tmpValue = (0.375 + 0.75*normValue) 410 ballValue = tmpValue - floor(tmpValue) 411 ballPoint = ballPath.pointAtPercent(ballValue) 412 413 # draw arc 414 startAngle = 218*16 415 spanAngle = -255*16*normValue 416 417 if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_WET: 418 painter.setBrush(colorBlue) 419 painter.setPen(QPen(colorBlue, 0)) 420 painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) 421 422 gradient = QConicalGradient(15.5, 15.5, -45) 423 gradient.setColorAt(0.0, colorBlue) 424 gradient.setColorAt(0.125, colorBlue) 425 gradient.setColorAt(0.625, colorGreen) 426 gradient.setColorAt(0.75, colorGreen) 427 gradient.setColorAt(0.76, colorGreen) 428 gradient.setColorAt(1.0, colorGreen) 429 painter.setBrush(gradient) 430 painter.setPen(QPen(gradient, 3)) 431 432 else: 433 painter.setBrush(colorBlue) 434 painter.setPen(QPen(colorBlue, 0)) 435 painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) 436 437 painter.setBrush(colorBlue) 438 painter.setPen(QPen(colorBlue, 3)) 439 440 painter.drawArc(4.0, 4.0, 26.0, 26.0, startAngle, spanAngle) 441 442 # Custom knobs (L and R) 443 elif self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_L, self.CUSTOM_PAINT_MODE_CARLA_R): 444 # knob color 445 color = QColor(0xAD, 0xD5, 0x48).lighter(100 + self.fHoverStep*6) 446 447 # draw small circle 448 ballRect = QRectF(7.0, 8.0, 11.0, 12.0) 449 ballPath = QPainterPath() 450 ballPath.addEllipse(ballRect) 451 #painter.drawRect(ballRect) 452 tmpValue = (0.375 + 0.75*normValue) 453 ballValue = tmpValue - floor(tmpValue) 454 ballPoint = ballPath.pointAtPercent(ballValue) 455 456 painter.setBrush(color) 457 painter.setPen(QPen(color, 0)) 458 painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.0, 2.0)) 459 460 # draw arc 461 if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L: 462 startAngle = 218*16 463 spanAngle = -255*16*normValue 464 else: 465 startAngle = 322.0*16 466 spanAngle = 255.0*16*(1.0-normValue) 467 468 painter.setPen(QPen(color, 2.5)) 469 painter.drawArc(3.5, 3.5, 22.0, 22.0, startAngle, spanAngle) 470 471 # Custom knobs (Color) 472 elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_COLOR: 473 # knob color 474 color = self.fCustomPaintColor.lighter(100 + self.fHoverStep*6) 475 476 # draw small circle 477 ballRect = QRectF(8.0, 8.0, 15.0, 15.0) 478 ballPath = QPainterPath() 479 ballPath.addEllipse(ballRect) 480 tmpValue = (0.375 + 0.75*normValue) 481 ballValue = tmpValue - floor(tmpValue) 482 ballPoint = ballPath.pointAtPercent(ballValue) 483 484 # draw arc 485 startAngle = 218*16 486 spanAngle = -255*16*normValue 487 488 painter.setBrush(color) 489 painter.setPen(QPen(color, 0)) 490 painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) 491 492 painter.setBrush(color) 493 painter.setPen(QPen(color, 3)) 494 painter.drawArc(4.0, 4.8, 26.0, 26.0, startAngle, spanAngle) 495 496 # Custom knobs (Zita) 497 elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_ZITA: 498 a = normValue * pi * 1.5 - 2.35 499 r = 10.0 500 x = 10.5 501 y = 10.5 502 x += r * sin(a) 503 y -= r * cos(a) 504 painter.setBrush(Qt.black) 505 painter.setPen(QPen(Qt.black, 2)) 506 painter.drawLine(QPointF(11.0, 11.0), QPointF(x, y)) 507 508 # Custom knobs 509 else: 510 painter.restore() 511 return 512 513 if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX: 514 self.fHoverStep += 1 if self.fIsHovered else -1 515 QTimer.singleShot(20, self.update) 516 517 else: # isEnabled() 518 target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize) 519 if isinstance(self.fImage, QPixmap): 520 painter.drawPixmap(target, self.fImage, target) 521 else: 522 self.fImage.renderer().render(painter, target) 523 524 painter.restore() 525 526 def resizeEvent(self, event): 527 QDial.resizeEvent(self, event) 528 self.updateSizes() 529 530# ------------------------------------------------------------------------------------------------------------ 531# Main Testing 532 533if __name__ == '__main__': 534 import sys 535 from PyQt5.QtWidgets import QApplication 536 import resources_rc 537 538 app = QApplication(sys.argv) 539 gui = ScalableDial(None) 540 #gui.setEnabled(True) 541 #gui.setEnabled(False) 542 gui.setSvg(3) 543 gui.setLabel("hahaha") 544 gui.show() 545 546 sys.exit(app.exec_()) 547