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.

424 lines
16KB

  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 floor, ceil
  7. from qt_compat import qt_config
  8. if qt_config == 5:
  9. from PyQt5.QtCore import pyqtSignal, QT_VERSION, Qt, QRectF, QTimer, QEvent, QPoint
  10. from PyQt5.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QPixmap
  11. from PyQt5.QtWidgets import QFrame, QGraphicsScene
  12. elif qt_config == 6:
  13. from PyQt6.QtCore import pyqtSignal, QT_VERSION, Qt, QRectF, QTimer, QEvent, QPoint
  14. from PyQt6.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QPixmap
  15. from PyQt6.QtWidgets import QFrame, QGraphicsScene
  16. # ---------------------------------------------------------------------------------------------------------------------
  17. # Antialiasing settings
  18. from patchcanvas import options, ANTIALIASING_FULL
  19. # ---------------------------------------------------------------------------------------------------------------------
  20. # Widget Class
  21. class CanvasPreviewFrame(QFrame):
  22. miniCanvasMoved = pyqtSignal(float, float)
  23. # x = 2
  24. # y = 2
  25. # w = width - 4
  26. # h = height - 3
  27. # bounds -1 px
  28. _kRectX = 0
  29. _kRectY = 1
  30. _kRectWidth = 2
  31. _kRectHeight = 3
  32. _kCursorName = 0
  33. _kCursorZoom = 1
  34. _kCursorZoomIn = 2
  35. _kCursorZoomOut = 3
  36. _kCursorZoomCount = 4
  37. _MOUSE_MODE_NONE = 0
  38. _MOUSE_MODE_MOVE = 1
  39. _MOUSE_MODE_SCALE = 2
  40. _RUBBERBAND_BLENDING_DEFAULT = 0
  41. _RUBBERBAND_BLENDING_PLUS = 1
  42. _RUBBERBAND_BLENDING_DIFF = 2
  43. # -----------------------------------------------------------------------------------------------------------------
  44. def __init__(self, parent):
  45. QFrame.__init__(self, parent)
  46. #self.setMinimumWidth(210)
  47. #self.setMinimumHeight(162)
  48. #self.setMaximumHeight(162)
  49. self.fRealParent = None
  50. self.fInternalWidth = 210-6 # -4 for width + -1px*2 bounds
  51. self.fInternalHeight = 162-5 # -3 for height + -1px*2 bounds
  52. self.fInternalRatio = self.fInternalWidth / self.fInternalHeight
  53. self.fScene = None
  54. self.fRenderSource = QRectF(0.0, 0.0, 0.0, 0.0)
  55. self.fRenderTarget = QRectF(0.0, 0.0, 0.0, 0.0)
  56. self.fUseCustomPaint = False
  57. self.fFrameWidth = 0.0
  58. self.fInitialX = 0.0
  59. self.fInitialY = 0.0
  60. self.fScale = 1.0
  61. self.fViewBg = QColor(0, 0, 0)
  62. self.fViewBrush = QBrush(QColor(75, 75, 255, 30))
  63. self.fViewPen = QPen(Qt.blue, 1)
  64. self.fViewRect = [3.0, 3.0, 10.0, 10.0]
  65. self.fMouseMode = self._MOUSE_MODE_NONE
  66. self.fMouseLeftDown = False
  67. self.fMouseRightDown = False
  68. self.fMouseInitialZoomPos = None
  69. self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT
  70. self.fZoomCursors = [None for _ in range(self._kCursorZoomCount)]
  71. self.fZoomCursors[self._kCursorName] = "black"
  72. self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(":/cursors/zoom_black.png"), 8, 7)
  73. self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(":/cursors/zoom-in_black.png"), 8, 7)
  74. self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(":/cursors/zoom-out_black.png"), 8, 7)
  75. def init(self, scene: QGraphicsScene, realWidth: float, realHeight: float, useCustomPaint: bool = False):
  76. self.fScene = scene
  77. self.fRenderSource = QRectF(0.0, 0.0, realWidth, realHeight)
  78. self.fInternalRatio = realWidth / realHeight
  79. self._updateStyle()
  80. if self.fUseCustomPaint != useCustomPaint:
  81. self.fUseCustomPaint = useCustomPaint
  82. self.repaint()
  83. def setRealParent(self, parent):
  84. self.fRealParent = parent
  85. # -----------------------------------------------------------------------------------------------------------------
  86. def setViewPosX(self, xp: float):
  87. self.fViewRect[self._kRectX] = xp * (self.fInternalWidth - self.fViewRect[self._kRectWidth]/self.fScale)
  88. self.update()
  89. def setViewPosY(self, yp: float):
  90. self.fViewRect[self._kRectY] = yp * (self.fInternalHeight - self.fViewRect[self._kRectHeight]/self.fScale)
  91. self.update()
  92. def setViewScale(self, scale: float):
  93. self.fScale = scale
  94. if self.fRealParent is not None:
  95. QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)
  96. def setViewSize(self, width: float, height: float):
  97. self.fViewRect[self._kRectWidth] = width * self.fInternalWidth
  98. self.fViewRect[self._kRectHeight] = height * self.fInternalHeight
  99. self.update()
  100. def setViewTheme(self, bgColor, brushColor, penColor):
  101. bg_black = bgColor.blackF()
  102. brush_black = brushColor.blackF()
  103. r0, g0, b0, _ = bgColor.getRgb()
  104. r1, g1, b1, _ = brushColor.getRgb()
  105. if brush_black < bg_black:
  106. self.fRubberBandBlending = self._RUBBERBAND_BLENDING_PLUS
  107. brushColor = QColor(r1-r0, g1-g0, b1-b0, 40)
  108. elif bg_black < brush_black:
  109. self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DIFF
  110. brushColor = QColor(r0-r1, g0-g1, b0-b1, 40)
  111. else:
  112. bgColor.setAlpha(40)
  113. self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT
  114. penColor.setAlpha(100)
  115. self.fViewBg = bgColor
  116. self.fViewBrush = QBrush(brushColor)
  117. self.fViewPen = QPen(penColor, 1)
  118. cursorName = "black" if bg_black < 0.5 else "white"
  119. if self.fZoomCursors[self._kCursorName] != cursorName:
  120. prefix = ":/cursors/zoom"
  121. self.fZoomCursors[self._kCursorName] = cursorName
  122. self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(f"{prefix}_{cursorName}.png"), 8, 7)
  123. self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(f"{prefix}-in_{cursorName}.png"), 8, 7)
  124. self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(f"{prefix}-out_{cursorName}.png"), 8, 7)
  125. # -----------------------------------------------------------------------------------------------------------------
  126. def changeEvent(self, event):
  127. if event.type() in (QEvent.StyleChange, QEvent.PaletteChange):
  128. self._updateStyle()
  129. QFrame.changeEvent(self, event)
  130. def mousePressEvent(self, event):
  131. if event.button() == Qt.LeftButton:
  132. event.accept()
  133. self.fMouseLeftDown = True
  134. self._updateMouseMode(event)
  135. return
  136. if event.button() == Qt.RightButton:
  137. event.accept()
  138. self.fMouseRightDown = True
  139. self._updateMouseMode(event)
  140. return
  141. if event.button() == Qt.MiddleButton:
  142. event.accept()
  143. self.fMouseLeftDown = True
  144. self.fMouseRightDown = True
  145. self._updateMouseMode(event)
  146. return
  147. QFrame.mouseMoveEvent(self, event)
  148. def mouseMoveEvent(self, event):
  149. if self.fMouseMode == self._MOUSE_MODE_MOVE:
  150. event.accept()
  151. if QT_VERSION >= 0x60000:
  152. pos = event.position()
  153. x = pos.x()
  154. y = pos.y()
  155. else:
  156. x = event.x()
  157. y = event.y()
  158. self._moveViewRect(x, y)
  159. return
  160. if self.fMouseMode == self._MOUSE_MODE_SCALE:
  161. event.accept()
  162. if qt_config == 5:
  163. y = event.globalY()
  164. else:
  165. y = event.globalPosition().y()
  166. self._scaleViewRect(y)
  167. return
  168. QFrame.mouseMoveEvent(self, event)
  169. def mouseReleaseEvent(self, event):
  170. if event.button() == Qt.LeftButton:
  171. event.accept()
  172. self.fMouseLeftDown = False
  173. self._updateMouseMode(event)
  174. return
  175. if event.button() == Qt.RightButton:
  176. event.accept()
  177. self.fMouseRightDown = False
  178. self._updateMouseMode(event)
  179. return
  180. if event.button() == Qt.MiddleButton:
  181. event.accept()
  182. self.fMouseLeftDown = event.buttons() & Qt.LeftButton
  183. self.fMouseRightDown = event.buttons() & Qt.RightButton
  184. self._updateMouseMode(event)
  185. return
  186. QFrame.mouseReleaseEvent(self, event)
  187. def wheelEvent(self, event):
  188. if self.fScene is None:
  189. QFrame.wheelEvent(self, event)
  190. return
  191. event.accept()
  192. self.fScene.zoom_wheel(event.angleDelta().y())
  193. def paintEvent(self, event):
  194. if self.fScene is None:
  195. QFrame.paintEvent(self, event)
  196. return
  197. painter = QPainter(self)
  198. painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL))
  199. # Brightness-aware out-of-canvas shading
  200. bg_color = self.fViewBg
  201. bg_black = bg_color.black()
  202. bg_shade = -12 if bg_black < 127 else 12
  203. r,g,b,_ = bg_color.getRgb()
  204. bg_color = QColor(r+bg_shade, g+bg_shade, b+bg_shade)
  205. frameWidth = self.fFrameWidth
  206. if self.fUseCustomPaint:
  207. # Inner shadow
  208. color = QColor.fromHsv(40, 0, 255-max(210, bg_color.black(), bg_black))
  209. painter.setBrush(Qt.transparent)
  210. painter.setPen(color)
  211. painter.drawRect(QRectF(0.5, 0.5, self.width()-1, self.height()-1))
  212. # Background
  213. painter.setBrush(bg_color)
  214. painter.setPen(bg_color)
  215. painter.drawRect(QRectF(1.5, 1.5, self.width()-3, self.height()-3))
  216. else:
  217. use_rounding = int(frameWidth > 1)
  218. rounding = 0.5 * use_rounding
  219. painter.setBrush(bg_color)
  220. painter.setPen(bg_color)
  221. painter.drawRoundedRect(QRectF(0.5+frameWidth,
  222. 0.5+frameWidth,
  223. self.width()-1-frameWidth*2,
  224. self.height()-1-frameWidth*2), rounding, rounding)
  225. clipPath = QPainterPath()
  226. rounding = 1.0 * use_rounding
  227. clipPath.addRoundedRect(QRectF(frameWidth,
  228. frameWidth,
  229. self.width()-frameWidth*2,
  230. self.height()-frameWidth*2), rounding, rounding)
  231. painter.setClipPath(clipPath)
  232. self.fScene.render(painter, self.fRenderTarget, self.fRenderSource, Qt.KeepAspectRatio)
  233. # Allow cursor frame to look joined with minicanvas frame
  234. painter.setClipping(False)
  235. width = self.fViewRect[self._kRectWidth]/self.fScale
  236. height = self.fViewRect[self._kRectHeight]/self.fScale
  237. # cursor
  238. lineHinting = self.fViewPen.widthF() / 2
  239. x = self.fViewRect[self._kRectX]+self.fInitialX
  240. y = self.fViewRect[self._kRectY]+self.fInitialY
  241. scr_x = floor(x)
  242. scr_y = floor(y)
  243. rect = QRectF(
  244. scr_x+lineHinting,
  245. scr_y+lineHinting,
  246. ceil(width+x-scr_x)-lineHinting*2,
  247. ceil(height+y-scr_y)-lineHinting*2 )
  248. if self.fRubberBandBlending == self._RUBBERBAND_BLENDING_PLUS:
  249. painter.setCompositionMode(QPainter.CompositionMode_Plus)
  250. elif self.fRubberBandBlending == self._RUBBERBAND_BLENDING_DIFF:
  251. painter.setCompositionMode(QPainter.CompositionMode_Difference)
  252. painter.setBrush(self.fViewBrush)
  253. painter.setPen(Qt.NoPen)
  254. painter.drawRect(rect)
  255. painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
  256. painter.setBrush(Qt.NoBrush)
  257. painter.setPen(self.fViewPen)
  258. painter.drawRect(rect)
  259. if self.fUseCustomPaint:
  260. event.accept()
  261. else:
  262. QFrame.paintEvent(self, event)
  263. def resizeEvent(self, event):
  264. size = event.size()
  265. width = size.width()
  266. height = size.height()
  267. extRatio = (width - self.fFrameWidth * 2) / (height - self.fFrameWidth * 2)
  268. if extRatio >= self.fInternalRatio:
  269. self.fInternalHeight = floor(height - self.fFrameWidth * 2)
  270. self.fInternalWidth = floor(self.fInternalHeight * self.fInternalRatio)
  271. self.fInitialX = floor(float(width - self.fInternalWidth) / 2.0)
  272. self.fInitialY = self.fFrameWidth
  273. else:
  274. self.fInternalWidth = floor(width - self.fFrameWidth * 2)
  275. self.fInternalHeight = floor(self.fInternalWidth / self.fInternalRatio)
  276. self.fInitialX = self.fFrameWidth
  277. self.fInitialY = floor(float(height - self.fInternalHeight) / 2.0)
  278. self.fRenderTarget = QRectF(self.fInitialX,
  279. self.fInitialY,
  280. float(self.fInternalWidth),
  281. float(self.fInternalHeight))
  282. if self.fRealParent is not None:
  283. QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)
  284. QFrame.resizeEvent(self, event)
  285. # -----------------------------------------------------------------------------------------------------------------
  286. def _moveViewRect(self, x: float, y: float):
  287. if self.fScene is None:
  288. return
  289. x -= self.fInitialX
  290. y -= self.fInitialY
  291. fixPos = False
  292. rCentX = self.fViewRect[self._kRectWidth] / self.fScale / 2
  293. rCentY = self.fViewRect[self._kRectHeight] / self.fScale / 2
  294. if x < rCentX:
  295. x = rCentX
  296. fixPos = True
  297. elif x > self.fInternalWidth - rCentX:
  298. x = self.fInternalWidth - rCentX
  299. fixPos = True
  300. if y < rCentY:
  301. y = rCentY
  302. fixPos = True
  303. elif y > self.fInternalHeight - rCentY:
  304. y = self.fInternalHeight - rCentY
  305. fixPos = True
  306. if fixPos:
  307. globalPos = self.mapToGlobal(QPoint(int(self.fInitialX + x), int(self.fInitialY + y)))
  308. self.cursor().setPos(globalPos)
  309. x = self.fRenderSource.width() * x / self.fInternalWidth
  310. y = self.fRenderSource.height() * y / self.fInternalHeight
  311. self.fScene.m_view.centerOn(x, y)
  312. def _scaleViewRect(self, globalY: int):
  313. if self.fScene is None:
  314. return
  315. dy = self.fMouseInitialZoomPos.y() - globalY
  316. if dy != 0:
  317. self.setCursor(self.fZoomCursors[self._kCursorZoomIn if dy > 0 else self._kCursorZoomOut])
  318. self.fScene.zoom_wheel(dy)
  319. # FIXME do not rely on this
  320. self.cursor().setPos(self.fMouseInitialZoomPos)
  321. def _updateMouseMode(self, event):
  322. if self.fMouseLeftDown and self.fMouseRightDown:
  323. if qt_config == 5:
  324. self.fMouseInitialZoomPos = event.globalPos()
  325. else:
  326. self.fMouseInitialZoomPos = event.globalPosition().toPoint()
  327. self.setCursor(self.fZoomCursors[self._kCursorZoom])
  328. self.fMouseMode = self._MOUSE_MODE_SCALE
  329. elif self.fMouseLeftDown:
  330. self.setCursor(QCursor(Qt.SizeAllCursor))
  331. if self.fMouseMode == self._MOUSE_MODE_NONE:
  332. if qt_config == 5:
  333. x = event.x()
  334. y = event.y()
  335. else:
  336. pos = event.position()
  337. x = pos.x()
  338. y = pos.y()
  339. self._moveViewRect(x, y)
  340. self.fMouseMode = self._MOUSE_MODE_MOVE
  341. else:
  342. self.unsetCursor()
  343. self.fMouseMode = self._MOUSE_MODE_NONE
  344. def _updateStyle(self):
  345. self.fFrameWidth = 1 if self.fUseCustomPaint else self.frameWidth()
  346. # ---------------------------------------------------------------------------------------------------------------------