Cleanup
[carla.git] / source / frontend / widgets / canvaspreviewframe.py
blob064009a23de09253ab3d26e7410e8eb5e56826a4
1 #!/usr/bin/env python3
2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ---------------------------------------------------------------------------------------------------------------------
6 # Imports (Global)
8 from math import floor, ceil
10 from qt_compat import qt_config
12 if qt_config == 5:
13 from PyQt5.QtCore import pyqtSignal, QT_VERSION, Qt, QRectF, QTimer, QEvent, QPoint
14 from PyQt5.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QPixmap
15 from PyQt5.QtWidgets import QFrame, QGraphicsScene
16 elif qt_config == 6:
17 from PyQt6.QtCore import pyqtSignal, QT_VERSION, Qt, QRectF, QTimer, QEvent, QPoint
18 from PyQt6.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QPixmap
19 from PyQt6.QtWidgets import QFrame, QGraphicsScene
21 # ---------------------------------------------------------------------------------------------------------------------
22 # Antialiasing settings
24 from patchcanvas import options, ANTIALIASING_FULL
26 # ---------------------------------------------------------------------------------------------------------------------
27 # Widget Class
29 class CanvasPreviewFrame(QFrame):
30 miniCanvasMoved = pyqtSignal(float, float)
32 # x = 2
33 # y = 2
34 # w = width - 4
35 # h = height - 3
36 # bounds -1 px
38 _kRectX = 0
39 _kRectY = 1
40 _kRectWidth = 2
41 _kRectHeight = 3
43 _kCursorName = 0
44 _kCursorZoom = 1
45 _kCursorZoomIn = 2
46 _kCursorZoomOut = 3
47 _kCursorZoomCount = 4
49 _MOUSE_MODE_NONE = 0
50 _MOUSE_MODE_MOVE = 1
51 _MOUSE_MODE_SCALE = 2
53 _RUBBERBAND_BLENDING_DEFAULT = 0
54 _RUBBERBAND_BLENDING_PLUS = 1
55 _RUBBERBAND_BLENDING_DIFF = 2
57 # -----------------------------------------------------------------------------------------------------------------
59 def __init__(self, parent):
60 QFrame.__init__(self, parent)
62 #self.setMinimumWidth(210)
63 #self.setMinimumHeight(162)
64 #self.setMaximumHeight(162)
66 self.fRealParent = None
67 self.fInternalWidth = 210-6 # -4 for width + -1px*2 bounds
68 self.fInternalHeight = 162-5 # -3 for height + -1px*2 bounds
69 self.fInternalRatio = self.fInternalWidth / self.fInternalHeight
71 self.fScene = None
72 self.fRenderSource = QRectF(0.0, 0.0, 0.0, 0.0)
73 self.fRenderTarget = QRectF(0.0, 0.0, 0.0, 0.0)
74 self.fUseCustomPaint = False
75 self.fFrameWidth = 0.0
77 self.fInitialX = 0.0
78 self.fInitialY = 0.0
79 self.fScale = 1.0
81 self.fViewBg = QColor(0, 0, 0)
82 self.fViewBrush = QBrush(QColor(75, 75, 255, 30))
83 self.fViewPen = QPen(Qt.blue, 1)
84 self.fViewRect = [3.0, 3.0, 10.0, 10.0]
86 self.fMouseMode = self._MOUSE_MODE_NONE
87 self.fMouseLeftDown = False
88 self.fMouseRightDown = False
89 self.fMouseInitialZoomPos = None
91 self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT
93 self.fZoomCursors = [None for _ in range(self._kCursorZoomCount)]
94 self.fZoomCursors[self._kCursorName] = "black"
95 self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(":/cursors/zoom_black.png"), 8, 7)
96 self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(":/cursors/zoom-in_black.png"), 8, 7)
97 self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(":/cursors/zoom-out_black.png"), 8, 7)
99 def init(self, scene: QGraphicsScene, realWidth: float, realHeight: float, useCustomPaint: bool = False):
100 self.fScene = scene
101 self.fRenderSource = QRectF(0.0, 0.0, realWidth, realHeight)
102 self.fInternalRatio = realWidth / realHeight
103 self._updateStyle()
105 if self.fUseCustomPaint != useCustomPaint:
106 self.fUseCustomPaint = useCustomPaint
107 self.repaint()
109 def setRealParent(self, parent):
110 self.fRealParent = parent
112 # -----------------------------------------------------------------------------------------------------------------
114 def setViewPosX(self, xp: float):
115 self.fViewRect[self._kRectX] = xp * (self.fInternalWidth - self.fViewRect[self._kRectWidth]/self.fScale)
116 self.update()
118 def setViewPosY(self, yp: float):
119 self.fViewRect[self._kRectY] = yp * (self.fInternalHeight - self.fViewRect[self._kRectHeight]/self.fScale)
120 self.update()
122 def setViewScale(self, scale: float):
123 self.fScale = scale
125 if self.fRealParent is not None:
126 QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)
128 def setViewSize(self, width: float, height: float):
129 self.fViewRect[self._kRectWidth] = width * self.fInternalWidth
130 self.fViewRect[self._kRectHeight] = height * self.fInternalHeight
131 self.update()
133 def setViewTheme(self, bgColor, brushColor, penColor):
134 bg_black = bgColor.blackF()
135 brush_black = brushColor.blackF()
136 r0, g0, b0, _ = bgColor.getRgb()
137 r1, g1, b1, _ = brushColor.getRgb()
139 if brush_black < bg_black:
140 self.fRubberBandBlending = self._RUBBERBAND_BLENDING_PLUS
141 brushColor = QColor(r1-r0, g1-g0, b1-b0, 40)
142 elif bg_black < brush_black:
143 self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DIFF
144 brushColor = QColor(r0-r1, g0-g1, b0-b1, 40)
145 else:
146 bgColor.setAlpha(40)
147 self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT
149 penColor.setAlpha(100)
150 self.fViewBg = bgColor
151 self.fViewBrush = QBrush(brushColor)
152 self.fViewPen = QPen(penColor, 1)
154 cursorName = "black" if bg_black < 0.5 else "white"
155 if self.fZoomCursors[self._kCursorName] != cursorName:
156 prefix = ":/cursors/zoom"
157 self.fZoomCursors[self._kCursorName] = cursorName
158 self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(f"{prefix}_{cursorName}.png"), 8, 7)
159 self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(f"{prefix}-in_{cursorName}.png"), 8, 7)
160 self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(f"{prefix}-out_{cursorName}.png"), 8, 7)
162 # -----------------------------------------------------------------------------------------------------------------
164 def changeEvent(self, event):
165 if event.type() in (QEvent.StyleChange, QEvent.PaletteChange):
166 self._updateStyle()
167 QFrame.changeEvent(self, event)
169 def mousePressEvent(self, event):
170 if event.button() == Qt.LeftButton:
171 event.accept()
172 self.fMouseLeftDown = True
173 self._updateMouseMode(event)
174 return
175 if event.button() == Qt.RightButton:
176 event.accept()
177 self.fMouseRightDown = True
178 self._updateMouseMode(event)
179 return
180 if event.button() == Qt.MiddleButton:
181 event.accept()
182 self.fMouseLeftDown = True
183 self.fMouseRightDown = True
184 self._updateMouseMode(event)
185 return
186 QFrame.mouseMoveEvent(self, event)
188 def mouseMoveEvent(self, event):
189 if self.fMouseMode == self._MOUSE_MODE_MOVE:
190 event.accept()
191 if QT_VERSION >= 0x60000:
192 pos = event.position()
193 x = pos.x()
194 y = pos.y()
195 else:
196 x = event.x()
197 y = event.y()
198 self._moveViewRect(x, y)
199 return
200 if self.fMouseMode == self._MOUSE_MODE_SCALE:
201 event.accept()
202 self._scaleViewRect(event.globalY())
203 return
204 QFrame.mouseMoveEvent(self, event)
206 def mouseReleaseEvent(self, event):
207 if event.button() == Qt.LeftButton:
208 event.accept()
209 self.fMouseLeftDown = False
210 self._updateMouseMode(event)
211 return
212 if event.button() == Qt.RightButton:
213 event.accept()
214 self.fMouseRightDown = False
215 self._updateMouseMode(event)
216 return
217 if event.button() == Qt.MiddleButton:
218 event.accept()
219 self.fMouseLeftDown = event.buttons() & Qt.LeftButton
220 self.fMouseRightDown = event.buttons() & Qt.RightButton
221 self._updateMouseMode(event)
222 return
223 QFrame.mouseReleaseEvent(self, event)
225 def wheelEvent(self, event):
226 if self.fScene is None:
227 QFrame.wheelEvent(self, event)
228 return
230 event.accept()
231 self.fScene.zoom_wheel(event.angleDelta().y())
233 def paintEvent(self, event):
234 if self.fScene is None:
235 QFrame.paintEvent(self, event)
236 return
238 painter = QPainter(self)
239 painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL))
241 # Brightness-aware out-of-canvas shading
242 bg_color = self.fViewBg
243 bg_black = bg_color.black()
244 bg_shade = -12 if bg_black < 127 else 12
245 r,g,b,_ = bg_color.getRgb()
246 bg_color = QColor(r+bg_shade, g+bg_shade, b+bg_shade)
248 frameWidth = self.fFrameWidth
249 if self.fUseCustomPaint:
250 # Inner shadow
251 color = QColor.fromHsv(40, 0, 255-max(210, bg_color.black(), bg_black))
252 painter.setBrush(Qt.transparent)
253 painter.setPen(color)
254 painter.drawRect(QRectF(0.5, 0.5, self.width()-1, self.height()-1))
256 # Background
257 painter.setBrush(bg_color)
258 painter.setPen(bg_color)
259 painter.drawRect(QRectF(1.5, 1.5, self.width()-3, self.height()-3))
260 else:
261 use_rounding = int(frameWidth > 1)
263 rounding = 0.5 * use_rounding
264 painter.setBrush(bg_color)
265 painter.setPen(bg_color)
266 painter.drawRoundedRect(QRectF(0.5+frameWidth,
267 0.5+frameWidth,
268 self.width()-1-frameWidth*2,
269 self.height()-1-frameWidth*2), rounding, rounding)
271 clipPath = QPainterPath()
272 rounding = 1.0 * use_rounding
273 clipPath.addRoundedRect(QRectF(frameWidth,
274 frameWidth,
275 self.width()-frameWidth*2,
276 self.height()-frameWidth*2), rounding, rounding)
277 painter.setClipPath(clipPath)
279 self.fScene.render(painter, self.fRenderTarget, self.fRenderSource, Qt.KeepAspectRatio)
281 # Allow cursor frame to look joined with minicanvas frame
282 painter.setClipping(False)
284 width = self.fViewRect[self._kRectWidth]/self.fScale
285 height = self.fViewRect[self._kRectHeight]/self.fScale
287 # cursor
288 lineHinting = self.fViewPen.widthF() / 2
289 x = self.fViewRect[self._kRectX]+self.fInitialX
290 y = self.fViewRect[self._kRectY]+self.fInitialY
291 scr_x = floor(x)
292 scr_y = floor(y)
293 rect = QRectF(
294 scr_x+lineHinting,
295 scr_y+lineHinting,
296 ceil(width+x-scr_x)-lineHinting*2,
297 ceil(height+y-scr_y)-lineHinting*2 )
299 if self.fRubberBandBlending == self._RUBBERBAND_BLENDING_PLUS:
300 painter.setCompositionMode(QPainter.CompositionMode_Plus)
301 elif self.fRubberBandBlending == self._RUBBERBAND_BLENDING_DIFF:
302 painter.setCompositionMode(QPainter.CompositionMode_Difference)
304 painter.setBrush(self.fViewBrush)
305 painter.setPen(Qt.NoPen)
306 painter.drawRect(rect)
308 painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
309 painter.setBrush(Qt.NoBrush)
310 painter.setPen(self.fViewPen)
311 painter.drawRect(rect)
313 if self.fUseCustomPaint:
314 event.accept()
315 else:
316 QFrame.paintEvent(self, event)
318 def resizeEvent(self, event):
319 size = event.size()
320 width = size.width()
321 height = size.height()
322 extRatio = (width - self.fFrameWidth * 2) / (height - self.fFrameWidth * 2)
323 if extRatio >= self.fInternalRatio:
324 self.fInternalHeight = floor(height - self.fFrameWidth * 2)
325 self.fInternalWidth = floor(self.fInternalHeight * self.fInternalRatio)
326 self.fInitialX = floor(float(width - self.fInternalWidth) / 2.0)
327 self.fInitialY = self.fFrameWidth
328 else:
329 self.fInternalWidth = floor(width - self.fFrameWidth * 2)
330 self.fInternalHeight = floor(self.fInternalWidth / self.fInternalRatio)
331 self.fInitialX = self.fFrameWidth
332 self.fInitialY = floor(float(height - self.fInternalHeight) / 2.0)
334 self.fRenderTarget = QRectF(self.fInitialX,
335 self.fInitialY,
336 float(self.fInternalWidth),
337 float(self.fInternalHeight))
339 if self.fRealParent is not None:
340 QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)
342 QFrame.resizeEvent(self, event)
344 # -----------------------------------------------------------------------------------------------------------------
346 def _moveViewRect(self, x: float, y: float):
347 if self.fScene is None:
348 return
350 x -= self.fInitialX
351 y -= self.fInitialY
353 fixPos = False
354 rCentX = self.fViewRect[self._kRectWidth] / self.fScale / 2
355 rCentY = self.fViewRect[self._kRectHeight] / self.fScale / 2
356 if x < rCentX:
357 x = rCentX
358 fixPos = True
359 elif x > self.fInternalWidth - rCentX:
360 x = self.fInternalWidth - rCentX
361 fixPos = True
363 if y < rCentY:
364 y = rCentY
365 fixPos = True
366 elif y > self.fInternalHeight - rCentY:
367 y = self.fInternalHeight - rCentY
368 fixPos = True
370 if fixPos:
371 globalPos = self.mapToGlobal(QPoint(int(self.fInitialX + x), int(self.fInitialY + y)))
372 self.cursor().setPos(globalPos)
374 x = self.fRenderSource.width() * x / self.fInternalWidth
375 y = self.fRenderSource.height() * y / self.fInternalHeight
376 self.fScene.m_view.centerOn(x, y)
378 def _scaleViewRect(self, globalY: int):
379 if self.fScene is None:
380 return
382 dy = self.fMouseInitialZoomPos.y() - globalY
383 if dy != 0:
384 self.setCursor(self.fZoomCursors[self._kCursorZoomIn if dy > 0 else self._kCursorZoomOut])
385 self.fScene.zoom_wheel(dy)
387 self.cursor().setPos(self.fMouseInitialZoomPos)
389 def _updateMouseMode(self, event):
390 if self.fMouseLeftDown and self.fMouseRightDown:
391 self.fMouseInitialZoomPos = event.globalPos()
392 self.setCursor(self.fZoomCursors[self._kCursorZoom])
393 self.fMouseMode = self._MOUSE_MODE_SCALE
395 elif self.fMouseLeftDown:
396 self.setCursor(QCursor(Qt.SizeAllCursor))
397 if self.fMouseMode == self._MOUSE_MODE_NONE:
398 if QT_VERSION >= 0x60000:
399 pos = event.position()
400 x = pos.x()
401 y = pos.y()
402 else:
403 x = event.x()
404 y = event.y()
405 self._moveViewRect(x, y)
406 self.fMouseMode = self._MOUSE_MODE_MOVE
408 else:
409 self.unsetCursor()
410 self.fMouseMode = self._MOUSE_MODE_NONE
412 def _updateStyle(self):
413 self.fFrameWidth = 1 if self.fUseCustomPaint else self.frameWidth()
415 # ---------------------------------------------------------------------------------------------------------------------