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