Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

582 lines
21KB

  1. #!/usr/bin/env python3
  2. # SPDX-FileCopyrightText: 2011-2025 Filipe Coelho <falktx@falktx.com>
  3. # SPDX-License-Identifier: GPL-2.0-or-later
  4. # ---------------------------------------------------------------------------------------------------------------------
  5. # Imports (Global)
  6. from math import isnan, log10
  7. from qt_compat import qt_config
  8. if qt_config == 5:
  9. from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QRectF, QEvent, QTimer
  10. from PyQt5.QtGui import QColor, QFont, QLinearGradient, QPainter
  11. from PyQt5.QtWidgets import QWidget, QToolTip, QInputDialog
  12. elif qt_config == 6:
  13. from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QRectF, QEvent, QTimer
  14. from PyQt6.QtGui import QColor, QFont, QLinearGradient, QPainter
  15. from PyQt6.QtWidgets import QWidget, QToolTip, QInputDialog
  16. from carla_shared import strLim
  17. from widgets.paramspinbox import CustomInputDialog
  18. from carla_backend import PARAMETER_NULL, PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING
  19. # ---------------------------------------------------------------------------------------------------------------------
  20. # Widget Class
  21. # to be implemented by subclasses
  22. #def updateSizes(self):
  23. #def paintDial(self, painter):
  24. class CommonDial(QWidget):
  25. # enum CustomPaintMode
  26. CUSTOM_PAINT_MODE_NULL = 0 # default (NOTE: only this mode has label gradient)
  27. CUSTOM_PAINT_MODE_CARLA_WET = 1 # color blue-green gradient (reserved #3)
  28. CUSTOM_PAINT_MODE_CARLA_VOL = 2 # color blue (reserved #3)
  29. CUSTOM_PAINT_MODE_CARLA_L = 3 # color yellow (reserved #4)
  30. CUSTOM_PAINT_MODE_CARLA_R = 4 # color yellow (reserved #4)
  31. CUSTOM_PAINT_MODE_CARLA_PAN = 5 # color yellow (reserved #3)
  32. CUSTOM_PAINT_MODE_CARLA_FORTH = 6 # Experimental
  33. CUSTOM_PAINT_MODE_COLOR = 7 # May be deprecated (unless zynfx internal mode)
  34. CUSTOM_PAINT_MODE_NO_GRADIENT = 8 # skip label gradient
  35. CUSTOM_PAINT_MODE_CARLA_WET_MINI = 9 # for compacted slot
  36. CUSTOM_PAINT_MODE_CARLA_VOL_MINI = 10 # for compacted slot
  37. # enum Orientation
  38. HORIZONTAL = 0
  39. VERTICAL = 1
  40. HOVER_MIN = 0
  41. HOVER_MAX = 9
  42. MODE_DEFAULT = 0
  43. MODE_LINEAR = 1
  44. # signals
  45. dragStateChanged = pyqtSignal(bool)
  46. realValueChanged = pyqtSignal(float)
  47. def __init__(self, parent, index, precision, default, minimum, maximum, label, paintMode, colorHint, unit, skinStyle, whiteLabels, tweaks, isInteger, isButton, isOutput, isVuOutput, isVisible):
  48. QWidget.__init__(self, parent)
  49. self.fIndex = index
  50. self.fPrecision = precision
  51. self.fDefault = default
  52. self.fMinimum = minimum
  53. self.fMaximum = maximum
  54. self.fCustomPaintMode = paintMode
  55. self.fColorHint = colorHint
  56. self.fUnit = unit
  57. self.fSkinStyle = skinStyle
  58. self.fWhiteLabels = whiteLabels
  59. self.fTweaks = tweaks
  60. self.fIsInteger = isInteger
  61. self.fIsButton = isButton
  62. self.fIsOutput = isOutput
  63. self.fIsVuOutput = isVuOutput
  64. self.fIsVisible = isVisible
  65. self.fDialMode = self.MODE_LINEAR
  66. self.fLabel = label
  67. self.fLastLabel = ""
  68. self.fRealValue = 0.0
  69. self.fLastValue = self.fDefault
  70. self.fScalePoints = []
  71. self.fNumScalePoints = 0
  72. self.fScalePointsPrefix = ""
  73. self.fScalePointsSuffix = ""
  74. self.fIsHovered = False
  75. self.fIsPressed = False
  76. self.fHoverStep = self.HOVER_MIN
  77. self.fLastDragPos = None
  78. self.fLastDragValue = 0.0
  79. self.fLabelPos = QPointF(0.0, 0.0)
  80. self.fLabelFont = QFont(self.font())
  81. self.fLabelFont.setPixelSize(8)
  82. self.fLabelWidth = 0
  83. self.fLabelHeight = 0
  84. if self.palette().window().color().lightness() > 100:
  85. # Light background
  86. c = self.palette().dark().color()
  87. self.fLabelGradientColor1 = c
  88. self.fLabelGradientColor2 = QColor(c.red(), c.green(), c.blue(), 0)
  89. self.fLabelGradientColorT = [self.palette().buttonText().color(), self.palette().mid().color()]
  90. else:
  91. # Dark background
  92. self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
  93. self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
  94. self.fLabelGradientColorT = [Qt.white, Qt.darkGray]
  95. self.fLabelGradient = QLinearGradient(0, 0, 0, 1)
  96. self.fLabelGradient.setColorAt(0.0, self.fLabelGradientColor1)
  97. self.fLabelGradient.setColorAt(0.6, self.fLabelGradientColor1)
  98. self.fLabelGradient.setColorAt(1.0, self.fLabelGradientColor2)
  99. self.fLabelGradientRect = QRectF(0.0, 0.0, 0.0, 0.0)
  100. self.fCustomPaintColor = QColor(0xff, 0xff, 0xff)
  101. self.addContrast = int(bool(self.getTweak('HighContrast', 0)))
  102. self.colorFollow = bool(self.getTweak('ColorFollow', 0))
  103. self.knobPusheable = bool(self.getTweak('WetVolPush', 0))
  104. self.displayTooltip = bool(self.getTweak('Tooltips', 1))
  105. # We have two group of knobs, non-repaintable (like in Edit dialog) and normal.
  106. # For non-repaintable, we init sizes/color once here;
  107. # for normals, it should be (re)inited separately: we do not init it here
  108. # to save CPU, some parameters are not known yet, repaint need anyway.
  109. if self.fColorHint == -1:
  110. self.updateSizes()
  111. self.update()
  112. # self.valueChanged.connect(self.slot_valueChanged) # FIXME
  113. def forceWhiteLabelGradientText(self):
  114. self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
  115. self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
  116. self.fLabelGradientColorT = [Qt.white, Qt.darkGray]
  117. # def setLabelColor(self, enabled, disabled):
  118. # self.fLabelGradientColor1 = QColor(0, 0, 0, 255)
  119. # self.fLabelGradientColor2 = QColor(0, 0, 0, 0)
  120. # self.fLabelGradientColorT = [enabled, disabled]
  121. def getIndex(self):
  122. return self.fIndex
  123. def rvalue(self):
  124. return self.fRealValue
  125. def pushLabel(self, label):
  126. if self.fLastLabel == "":
  127. self.fLastLabel = self.fLabel
  128. self.fLabel = label
  129. self.updateSizes()
  130. self.update()
  131. def popLabel(self):
  132. if not (self.fLastLabel == ""):
  133. self.fLabel = self.fLastLabel
  134. self.fLastLabel = ""
  135. self.updateSizes()
  136. self.update()
  137. def setScalePPS(self, scalePoints, prefix, suffix):
  138. self.fScalePoints = scalePoints
  139. self.fNumScalePoints = len(self.fScalePoints)
  140. self.fScalePointsPrefix = prefix
  141. self.fScalePointsSuffix = suffix
  142. def setValue(self, value, emitSignal=False):
  143. if self.fRealValue == value or isnan(value):
  144. return
  145. if (not self.fIsOutput) and value <= self.fMinimum:
  146. self.fRealValue = self.fMinimum
  147. elif (not self.fIsOutput) and value >= self.fMaximum:
  148. self.fRealValue = self.fMaximum
  149. elif self.fIsInteger or (abs(value - int(value)) < 1e-8): # tiny "notch"
  150. self.fRealValue = round(value)
  151. else:
  152. self.fRealValue = value
  153. if emitSignal:
  154. self.realValueChanged.emit(self.fRealValue)
  155. self.update()
  156. def setCustomPaintColor(self, color):
  157. if self.fCustomPaintColor == color:
  158. return
  159. self.fCustomPaintColor = color
  160. self.updateSizes()
  161. self.update()
  162. def getTweak(self, tweakName, default):
  163. return self.fTweaks.get(self.fSkinStyle + tweakName, self.fTweaks.get(tweakName, default))
  164. def getIsVisible(self):
  165. # print (self.fIsVisible)
  166. return self.fIsVisible
  167. @pyqtSlot(int)
  168. def slot_valueChanged(self, value):
  169. self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum
  170. self.realValueChanged.emit(self.fRealValue)
  171. # jpka: TODO should be replaced by common dialog, but
  172. # PluginEdit.slot_knobCustomMenu(...) - not found, import not work.
  173. # So this is copy w/o access to 'step's.
  174. def knobCustomInputDialog(self):
  175. if self.fIndex < PARAMETER_NULL:
  176. percent = 100.0
  177. else:
  178. percent = 1
  179. if self.fIsInteger:
  180. step = max(1, int((self.fMaximum - self.fMinimum)/100))
  181. stepSmall = max(1, int(step/10))
  182. else:
  183. step = 10 ** (round(log10((self.fMaximum - self.fMinimum) * percent))-2)
  184. stepSmall = step / 100
  185. dialog = CustomInputDialog(self, self.fLabel, self.fRealValue * percent, self.fMinimum * percent, self.fMaximum * percent, step, stepSmall, self.fScalePoints, "", "", self.fUnit)
  186. if not dialog.exec_():
  187. return
  188. self.setValue(dialog.returnValue() / percent, True)
  189. def enterEvent(self, event):
  190. self.setFocus()
  191. self.fIsHovered = True
  192. if self.fHoverStep == self.HOVER_MIN:
  193. self.fHoverStep = self.HOVER_MIN + 1
  194. self.update()
  195. def leaveEvent(self, event):
  196. self.fIsHovered = False
  197. if self.fHoverStep == self.HOVER_MAX:
  198. self.fHoverStep = self.HOVER_MAX - 1
  199. self.update()
  200. def nextScalePoint(self):
  201. for i in range(self.fNumScalePoints):
  202. value = self.fScalePoints[i]['value']
  203. if value > self.fRealValue:
  204. self.setValue(value, True)
  205. return
  206. self.setValue(self.fScalePoints[0]['value'], True)
  207. def mousePressEvent(self, event):
  208. if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput:
  209. return
  210. if event.button() == Qt.LeftButton:
  211. # if self.fNumScalePoints:
  212. # self.nextScalePoint()
  213. #
  214. if self.fIsButton:
  215. value = int(self.fRealValue) + 1;
  216. if (value > self.fMaximum):
  217. value = 0
  218. self.setValue(value, True)
  219. else:
  220. self.fIsPressed = True
  221. self.fLastDragPos = event.pos()
  222. self.fLastDragValue = self.fRealValue
  223. self.dragStateChanged.emit(True)
  224. elif event.button() == Qt.MiddleButton:
  225. if self.fIsOutput:
  226. return
  227. self.setValue(self.fDefault, True)
  228. def mouseDoubleClickEvent(self, event):
  229. if self.knobPusheable and self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING): # -3, -4, -7
  230. return # Mutex with special Single Click
  231. if event.button() == Qt.LeftButton:
  232. if self.fIsButton:
  233. value = int(self.fRealValue) + 1;
  234. if (value > self.fMaximum):
  235. value = 0
  236. self.setValue(value, True)
  237. else:
  238. if self.fIsOutput:
  239. return
  240. self.knobCustomInputDialog()
  241. def mouseMoveEvent(self, event):
  242. if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput:
  243. return
  244. if not self.fIsPressed:
  245. return
  246. pos = event.pos()
  247. delta = (float(pos.x() - self.fLastDragPos.x()) - float(pos.y() - self.fLastDragPos.y())) / 10
  248. mod = event.modifiers()
  249. self.applyDelta(mod, delta, True)
  250. def mouseReleaseEvent(self, event):
  251. if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput:
  252. return
  253. if self.fIsPressed:
  254. self.fIsPressed = False
  255. self.dragStateChanged.emit(False)
  256. if event.button() == Qt.LeftButton:
  257. if event.pos() == self.fLastDragPos:
  258. if self.fNumScalePoints:
  259. self.nextScalePoint()
  260. else:
  261. self.knobPush()
  262. # NOTE: fLastLabel state managed @ scalabledial
  263. def knobPush(self):
  264. if self.knobPusheable and self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING): # -3, -4, -7
  265. if self.fLastLabel == "": # push value
  266. self.fLastValue = self.fRealValue
  267. self.setValue(0, True) # Thru or Mute
  268. else: # pop value
  269. self.setValue(self.fLastValue, True)
  270. def applyDelta(self, mod, delta, anchor = False):
  271. if self.fIsOutput:
  272. return
  273. if self.fIsButton:
  274. self.setValue(self.fRealValue + delta, True)
  275. return
  276. if self.fIsInteger: # 4 to 50 ticks per revolution
  277. if (mod & Qt.ShiftModifier):
  278. delta = delta * 5
  279. elif (mod & Qt.ControlModifier):
  280. delta = delta / min(int((self.fMaximum-self.fMinimum)/self.fPrecision), 5)
  281. else: # Floats are 250 to 500 ticks per revolution
  282. # jpka: 1. Should i use these steps?
  283. # 2. And what do i do when i TODO add MODE_LOG along with MODE_LINEAR?
  284. # 3. And they're too small for large ints like in TAP Reverb, and strange for scalepoints.
  285. # paramRanges = self.host.get_parameter_ranges(self.fPluginId, i)
  286. # paramRanges['step'], paramRanges['stepSmall'], paramRanges['stepLarge']
  287. if (mod & Qt.ControlModifier) and (mod & Qt.ShiftModifier):
  288. delta = delta * 2/5
  289. elif (mod & Qt.ControlModifier):
  290. delta = delta * 2
  291. elif (mod & Qt.ShiftModifier):
  292. delta = delta * 50
  293. else:
  294. delta = delta * 10
  295. difference = float(self.fMaximum-self.fMinimum) * float(delta) / float(self.fPrecision)
  296. if anchor:
  297. self.setValue((self.fLastDragValue + difference), True)
  298. else:
  299. self.setValue((self.fRealValue + difference), True)
  300. return
  301. def wheelEvent(self, event):
  302. if self.fIsOutput:
  303. return
  304. direction = event.angleDelta().y()
  305. if direction < 0:
  306. delta = -1.0
  307. elif direction > 0:
  308. delta = 1.0
  309. else:
  310. return
  311. mod = event.modifiers()
  312. self.applyDelta(mod, delta)
  313. return
  314. def keyPressEvent(self, event):
  315. if self.fIsOutput:
  316. return
  317. key = event.key()
  318. mod = event.modifiers()
  319. modsNone = not ((mod & Qt.ShiftModifier) | (mod & Qt.ControlModifier) | (mod & Qt.AltModifier))
  320. if modsNone:
  321. match key:
  322. case Qt.Key_Space | Qt.Key_Enter | Qt.Key_Return :
  323. if self.fIsButton:
  324. value = int(self.fRealValue) + 1
  325. if (value > self.fMaximum):
  326. value = 0
  327. self.setValue(value, True)
  328. elif not key == Qt.Key_Space:
  329. self.knobCustomInputDialog()
  330. else:
  331. if self.fNumScalePoints:
  332. self.nextScalePoint()
  333. else:
  334. self.knobPush()
  335. case Qt.Key_E:
  336. self.knobCustomInputDialog()
  337. case key if Qt.Key_0 <= key <= Qt.Key_9:
  338. if self.fIsInteger and (self.fMinimum == 0) and (self.fMaximum <= 10):
  339. self.setValue(key-Qt.Key_0, True)
  340. else:
  341. self.setValue(self.fMinimum + float(self.fMaximum-self.fMinimum)/10.0*(key-Qt.Key_0), True)
  342. case Qt.Key_Home: # NOTE: interferes with Canvas control hotkey
  343. self.setValue(self.fMinimum, True)
  344. case Qt.Key_End:
  345. self.setValue(self.fMaximum, True)
  346. case Qt.Key_D:
  347. self.setValue(self.fDefault, True)
  348. case Qt.Key_R:
  349. self.setValue(self.fDefault, True)
  350. match key:
  351. case Qt.Key_PageDown:
  352. self.applyDelta(mod, -1)
  353. case Qt.Key_PageUp:
  354. self.applyDelta(mod, 1)
  355. return
  356. def paintEvent(self, event):
  357. painter = QPainter(self)
  358. event.accept()
  359. painter.save()
  360. painter.setRenderHint(QPainter.Antialiasing, True)
  361. enabled = int(bool(self.isEnabled()))
  362. if enabled:
  363. self.setContextMenuPolicy(Qt.CustomContextMenu)
  364. else:
  365. self.setContextMenuPolicy(Qt.NoContextMenu)
  366. if self.fLabel:
  367. # if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL:
  368. # painter.setPen(self.fLabelGradientColor2)
  369. # painter.setBrush(self.fLabelGradient)
  370. # # painter.drawRect(self.fLabelGradientRect) FIXME restore gradients.
  371. luma = int(bool(self.fWhiteLabels)) - 0.5
  372. if enabled:
  373. L = (luma * (1.6 + self.addContrast * 0.4)) / 2 + 0.5
  374. else:
  375. L = (luma * (0.2 + self.addContrast * 0.2)) / 2 + 0.5
  376. painter.setFont(self.fLabelFont)
  377. # painter.setPen(self.fLabelGradientColorT[0 if self.fIsEnabled() else 1])
  378. painter.setPen(QColor.fromHslF(0, 0, L, 1))
  379. painter.drawText(self.fLabelPos, self.fLabel)
  380. X = self.fWidth / 2
  381. Y = self.fHeight / 2
  382. S = enabled * 0.9 # saturation
  383. E = enabled * self.fHoverStep / 40 # enlight
  384. L = 0.6 + E
  385. if self.addContrast:
  386. L = min(L + 0.3, 1) # luma
  387. normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum)
  388. # Work In Progress FIXME
  389. H=0
  390. if self.fIsOutput:
  391. if self.fIsButton:
  392. # self.paintLed (painter, X, Y, H, S, L, E, normValue)
  393. self.paintDisplay(painter, X, Y, H, S, L, E, normValue, enabled)
  394. else:
  395. self.paintDisplay(painter, X, Y, H, S, L, E, normValue, enabled)
  396. else:
  397. if self.fIsButton:
  398. self.paintButton (painter, X, Y, H, S, L, E, normValue, enabled)
  399. else:
  400. self.paintDial (painter, X, Y, H, S, L, E, normValue, enabled)
  401. # Display tooltip, above the knob (OS-independent, unlike of mouse tooltip).
  402. # Note, update/redraw Qt's tooltip eats much more CPU than expected,
  403. # so we have tweak for turn it off. See also #1934.
  404. if self.fHoverStep == self.HOVER_MAX and self.displayTooltip:
  405. # First, we need to find exact or nearest match (index from value).
  406. # It is also tests if we have scale points at all.
  407. num = -1
  408. for i in range(self.fNumScalePoints):
  409. scaleValue = self.fScalePoints[i]['value']
  410. if i == 0:
  411. finalValue = scaleValue
  412. num = 0
  413. else:
  414. srange2 = abs(self.fRealValue - finalValue)
  415. srange1 = abs(self.fRealValue - scaleValue)
  416. if srange2 > srange1:
  417. finalValue = scaleValue
  418. num = i
  419. if (srange1 == 0): # Exact match, save some CPU.
  420. break
  421. tip = ""
  422. if (num >= 0): # Scalepoints are used
  423. tip = str(self.fScalePoints[num]['label'])
  424. if not self.fIsButton:
  425. tip = self.fScalePointsPrefix + \
  426. strLim(self.fScalePoints[num]['value']) + \
  427. self.fScalePointsSuffix + ": " + tip
  428. # ? We most probably not need tooltip for button, if it is not scalepoint.
  429. # elif not self.fIsButton:
  430. else:
  431. if self.fRealValue == 0 and self.fIndex == PARAMETER_DRYWET: #-3,-4,-7,-9
  432. tip = "THRU"
  433. elif self.fRealValue == 0 and self.fIndex == PARAMETER_VOLUME:
  434. tip = "MUTE"
  435. elif self.fRealValue == 0 and self.fIndex == PARAMETER_PANNING:
  436. tip = "Center"
  437. else:
  438. if self.fIndex < PARAMETER_NULL:
  439. percent = 100.0
  440. else:
  441. percent = 1
  442. tip = (strLim(self.fRealValue * percent) + " " + self.fUnit).strip()
  443. if self.fIsOutput:
  444. tip = tip + " [" + strLim(self.fMinimum * percent) + "..." + \
  445. strLim(self.fMaximum * percent) + "]"
  446. # Wrong vert. position for Calf:
  447. # QToolTip.showText(self.mapToGlobal(QPoint(0, 0-self.geometry().height())), tip)
  448. # FIXME Still wrong vert. position for QT_SCALE_FACTOR=2.
  449. QToolTip.showText(self.mapToGlobal(QPoint(0, 0-45)), tip)
  450. else:
  451. QToolTip.hideText()
  452. if enabled:
  453. if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX:
  454. self.fHoverStep += 1 if self.fIsHovered else -1
  455. QTimer.singleShot(20, self.update)
  456. painter.restore()
  457. # def resizeEvent(self, event):
  458. # QWidget.resizeEvent(self, event)
  459. # self.updateSizes()
  460. # ---------------------------------------------------------------------------------------------------------------------