|  | #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Custom Mini Canvas Preview, a custom Qt widget
# Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# For a full copy of the GNU General Public License see the doc/GPL.txt file.
# ---------------------------------------------------------------------------------------------------------------------
# Imports (Global)
from math import floor, ceil
from PyQt5.QtCore import pyqtSignal, Qt, QRectF, QTimer, QEvent, QPoint
from PyQt5.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QPixmap
from PyQt5.QtWidgets import QFrame, QGraphicsScene
# ---------------------------------------------------------------------------------------------------------------------
# Antialiasing settings
from patchcanvas import options, ANTIALIASING_FULL
# ---------------------------------------------------------------------------------------------------------------------
# Widget Class
class CanvasPreviewFrame(QFrame):
    miniCanvasMoved = pyqtSignal(float, float)
    # x = 2
    # y = 2
    # w = width - 4
    # h = height - 3
    # bounds -1 px
    _kRectX = 0
    _kRectY = 1
    _kRectWidth = 2
    _kRectHeight = 3
    _kCursorName = 0
    _kCursorZoom = 1
    _kCursorZoomIn = 2
    _kCursorZoomOut = 3
    _kCursorZoomCount = 4
    _MOUSE_MODE_NONE = 0
    _MOUSE_MODE_MOVE = 1
    _MOUSE_MODE_SCALE = 2
    _RUBBERBAND_BLENDING_DEFAULT = 0
    _RUBBERBAND_BLENDING_PLUS = 1
    _RUBBERBAND_BLENDING_DIFF = 2
    # -----------------------------------------------------------------------------------------------------------------
    def __init__(self, parent):
        QFrame.__init__(self, parent)
        #self.setMinimumWidth(210)
        #self.setMinimumHeight(162)
        #self.setMaximumHeight(162)
        self.fRealParent = None
        self.fInternalWidth  = 210-6 # -4 for width + -1px*2 bounds
        self.fInternalHeight = 162-5 # -3 for height + -1px*2 bounds
        self.fInternalRatio  = self.fInternalWidth / self.fInternalHeight
        self.fScene = None
        self.fRenderSource   = QRectF(0.0, 0.0, 0.0, 0.0)
        self.fRenderTarget   = QRectF(0.0, 0.0, 0.0, 0.0)
        self.fUseCustomPaint = False
        self.fFrameWidth = 0.0
        self.fInitialX = 0.0
        self.fInitialY = 0.0
        self.fScale    = 1.0
        self.fViewBg    = QColor(0, 0, 0)
        self.fViewBrush = QBrush(QColor(75, 75, 255, 30))
        self.fViewPen   = QPen(Qt.blue, 1)
        self.fViewRect  = [3.0, 3.0, 10.0, 10.0]
        self.fMouseMode = self._MOUSE_MODE_NONE
        self.fMouseLeftDown = False
        self.fMouseRightDown = False
        self.fMouseInitialZoomPos = None
        self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT
        self.fZoomCursors = [None for _ in range(self._kCursorZoomCount)]
        self.fZoomCursors[self._kCursorName] = "black"
        self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(":/cursors/zoom_black.png"), 8, 7)
        self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(":/cursors/zoom-in_black.png"), 8, 7)
        self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(":/cursors/zoom-out_black.png"), 8, 7)
    def init(self, scene: QGraphicsScene, realWidth: float, realHeight: float, useCustomPaint: bool = False):
        self.fScene = scene
        self.fRenderSource = QRectF(0.0, 0.0, realWidth, realHeight)
        self.fInternalRatio = realWidth / realHeight
        self._updateStyle()
        if self.fUseCustomPaint != useCustomPaint:
            self.fUseCustomPaint = useCustomPaint
            self.repaint()
    def setRealParent(self, parent):
        self.fRealParent = parent
    # -----------------------------------------------------------------------------------------------------------------
    def setViewPosX(self, xp: float):
        self.fViewRect[self._kRectX] = xp * (self.fInternalWidth - self.fViewRect[self._kRectWidth]/self.fScale)
        self.update()
    def setViewPosY(self, yp: float):
        self.fViewRect[self._kRectY] = yp * (self.fInternalHeight - self.fViewRect[self._kRectHeight]/self.fScale)
        self.update()
    def setViewScale(self, scale: float):
        self.fScale = scale
        if self.fRealParent is not None:
            QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)
    def setViewSize(self, width: float, height: float):
        self.fViewRect[self._kRectWidth] = width * self.fInternalWidth
        self.fViewRect[self._kRectHeight] = height * self.fInternalHeight
        self.update()
    def setViewTheme(self, bgColor, brushColor, penColor):
        bg_black      = bgColor.blackF()
        brush_black   = brushColor.blackF()
        r0, g0, b0, _ = bgColor.getRgb()
        r1, g1, b1, _ = brushColor.getRgb()
        if brush_black < bg_black:
            self.fRubberBandBlending = self._RUBBERBAND_BLENDING_PLUS
            brushColor = QColor(r1-r0, g1-g0, b1-b0, 40)
        elif bg_black < brush_black:
            self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DIFF
            brushColor = QColor(r0-r1, g0-g1, b0-b1, 40)
        else:
            bgColor.setAlpha(40)
            self.fRubberBandBlending = self._RUBBERBAND_BLENDING_DEFAULT
        penColor.setAlpha(100)
        self.fViewBg    = bgColor
        self.fViewBrush = QBrush(brushColor)
        self.fViewPen   = QPen(penColor, 1)
        cursorName = "black" if bg_black < 0.5 else "white"
        if self.fZoomCursors[self._kCursorName] != cursorName:
            prefix = ":/cursors/zoom"
            self.fZoomCursors[self._kCursorName] = cursorName
            self.fZoomCursors[self._kCursorZoom] = QCursor(QPixmap(f"{prefix}_{cursorName}.png"), 8, 7)
            self.fZoomCursors[self._kCursorZoomIn] = QCursor(QPixmap(f"{prefix}-in_{cursorName}.png"), 8, 7)
            self.fZoomCursors[self._kCursorZoomOut] = QCursor(QPixmap(f"{prefix}-out_{cursorName}.png"), 8, 7)
    # -----------------------------------------------------------------------------------------------------------------
    def changeEvent(self, event):
        if event.type() in (QEvent.StyleChange, QEvent.PaletteChange):
            self._updateStyle()
        QFrame.changeEvent(self, event)
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            event.accept()
            self.fMouseLeftDown = True
            self._updateMouseMode(event)
            return
        if event.button() == Qt.RightButton:
            event.accept()
            self.fMouseRightDown = True
            self._updateMouseMode(event)
            return
        if event.button() == Qt.MidButton:
            event.accept()
            self.fMouseLeftDown = True
            self.fMouseRightDown = True
            self._updateMouseMode(event)
            return
        QFrame.mouseMoveEvent(self, event)
    def mouseMoveEvent(self, event):
        if self.fMouseMode == self._MOUSE_MODE_MOVE:
            event.accept()
            self._moveViewRect(event.x(), event.y())
            return
        if self.fMouseMode == self._MOUSE_MODE_SCALE:
            event.accept()
            self._scaleViewRect(event.globalY())
            return
        QFrame.mouseMoveEvent(self, event)
    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            event.accept()
            self.fMouseLeftDown = False
            self._updateMouseMode()
            return
        if event.button() == Qt.RightButton:
            event.accept()
            self.fMouseRightDown = False
            self._updateMouseMode(event)
            return
        if event.button() == Qt.MidButton:
            event.accept()
            self.fMouseLeftDown = event.buttons() & Qt.LeftButton
            self.fMouseRightDown = event.buttons() & Qt.RightButton
            self._updateMouseMode(event)
            return
        QFrame.mouseReleaseEvent(self, event)
    def wheelEvent(self, event):
        if self.fScene is None:
            QFrame.wheelEvent(self, event)
            return
        event.accept()
        self.fScene.zoom_wheel(event.angleDelta().y())
    def paintEvent(self, event):
        if self.fScene is None:
            QFrame.paintEvent(self, event)
            return
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing, bool(options.antialiasing == ANTIALIASING_FULL))
        # Brightness-aware out-of-canvas shading
        bg_color = self.fViewBg
        bg_black = bg_color.black()
        bg_shade = -12 if bg_black < 127 else 12
        r,g,b,_ = bg_color.getRgb()
        bg_color = QColor(r+bg_shade, g+bg_shade, b+bg_shade)
        frameWidth = self.fFrameWidth
        if self.fUseCustomPaint:
            # Inner shadow
            color = QColor.fromHsv(40, 0, 255-max(210, bg_color.black(), bg_black))
            painter.setBrush(Qt.transparent)
            painter.setPen(color)
            painter.drawRect(QRectF(0.5, 0.5, self.width()-1, self.height()-1))
            # Background
            painter.setBrush(bg_color)
            painter.setPen(bg_color)
            painter.drawRect(QRectF(1.5, 1.5, self.width()-3, self.height()-3))
        else:
            use_rounding = int(frameWidth > 1)
            rounding = 0.5 * use_rounding
            painter.setBrush(bg_color)
            painter.setPen(bg_color)
            painter.drawRoundedRect(QRectF(0.5+frameWidth,
                                           0.5+frameWidth,
                                           self.width()-1-frameWidth*2,
                                           self.height()-1-frameWidth*2), rounding, rounding)
            clipPath = QPainterPath()
            rounding = 1.0 * use_rounding
            clipPath.addRoundedRect(QRectF(frameWidth,
                                           frameWidth,
                                           self.width()-frameWidth*2,
                                           self.height()-frameWidth*2), rounding, rounding)
            painter.setClipPath(clipPath)
        self.fScene.render(painter, self.fRenderTarget, self.fRenderSource, Qt.KeepAspectRatio)
        # Allow cursor frame to look joined with minicanvas frame
        painter.setClipping(False)
        width  = self.fViewRect[self._kRectWidth]/self.fScale
        height = self.fViewRect[self._kRectHeight]/self.fScale
        # cursor
        lineHinting = self.fViewPen.widthF() / 2
        x = self.fViewRect[self._kRectX]+self.fInitialX
        y = self.fViewRect[self._kRectY]+self.fInitialY
        scr_x = floor(x)
        scr_y = floor(y)
        rect = QRectF(
            scr_x+lineHinting,
            scr_y+lineHinting,
            ceil(width+x-scr_x)-lineHinting*2,
            ceil(height+y-scr_y)-lineHinting*2 )
        if self.fRubberBandBlending == self._RUBBERBAND_BLENDING_PLUS:
            painter.setCompositionMode(QPainter.CompositionMode_Plus)
        elif self.fRubberBandBlending == self._RUBBERBAND_BLENDING_DIFF:
            painter.setCompositionMode(QPainter.CompositionMode_Difference)
        painter.setBrush(self.fViewBrush)
        painter.setPen(Qt.NoPen)
        painter.drawRect(rect)
        painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
        painter.setBrush(Qt.NoBrush)
        painter.setPen(self.fViewPen)
        painter.drawRect(rect)
        if self.fUseCustomPaint:
            event.accept()
        else:
            QFrame.paintEvent(self, event)
    def resizeEvent(self, event):
        size = event.size()
        width = size.width()
        height = size.height()
        extRatio = (width - self.fFrameWidth * 2) / (height - self.fFrameWidth * 2)
        if extRatio >= self.fInternalRatio:
            self.fInternalHeight = floor(height - self.fFrameWidth * 2)
            self.fInternalWidth  = floor(self.fInternalHeight * self.fInternalRatio)
            self.fInitialX       = floor(float(width - self.fInternalWidth) / 2.0)
            self.fInitialY       = self.fFrameWidth
        else:
            self.fInternalWidth  = floor(width - self.fFrameWidth * 2)
            self.fInternalHeight = floor(self.fInternalWidth / self.fInternalRatio)
            self.fInitialX       = self.fFrameWidth
            self.fInitialY       = floor(float(height - self.fInternalHeight) / 2.0)
        self.fRenderTarget = QRectF(self.fInitialX,
                                    self.fInitialY,
                                    float(self.fInternalWidth),
                                    float(self.fInternalHeight))
        if self.fRealParent is not None:
            QTimer.singleShot(0, self.fRealParent.slot_miniCanvasCheckAll)
        QFrame.resizeEvent(self, event)
    # -----------------------------------------------------------------------------------------------------------------
    def _moveViewRect(self, x: float, y: float):
        if self.fScene is None:
            return
        x -= self.fInitialX
        y -= self.fInitialY
        fixPos = False
        rCentX = self.fViewRect[self._kRectWidth] / self.fScale / 2
        rCentY = self.fViewRect[self._kRectHeight] / self.fScale / 2
        if x < rCentX:
            x = rCentX
            fixPos = True
        elif x > self.fInternalWidth - rCentX:
            x = self.fInternalWidth - rCentX
            fixPos = True
        if y < rCentY:
            y = rCentY
            fixPos = True
        elif y > self.fInternalHeight - rCentY:
            y = self.fInternalHeight - rCentY
            fixPos = True
        if fixPos:
            globalPos = self.mapToGlobal(QPoint(int(self.fInitialX + x), int(self.fInitialY + y)))
            self.cursor().setPos(globalPos)
        x = self.fRenderSource.width() * x / self.fInternalWidth
        y = self.fRenderSource.height() * y / self.fInternalHeight
        self.fScene.m_view.centerOn(x, y)
    def _scaleViewRect(self, globalY: int):
        if self.fScene is None:
            return
        dy = self.fMouseInitialZoomPos.y() - globalY
        if dy != 0:
            self.setCursor(self.fZoomCursors[self._kCursorZoomIn if dy > 0 else self._kCursorZoomOut])
            self.fScene.zoom_wheel(dy)
        self.cursor().setPos(self.fMouseInitialZoomPos)
    def _updateMouseMode(self, event = None):
        if self.fMouseLeftDown and self.fMouseRightDown:
            self.fMouseInitialZoomPos = event.globalPos()
            self.setCursor(self.fZoomCursors[self._kCursorZoom])
            self.fMouseMode = self._MOUSE_MODE_SCALE
        elif self.fMouseLeftDown:
            self.setCursor(QCursor(Qt.SizeAllCursor))
            if self.fMouseMode == self._MOUSE_MODE_NONE:
                self._moveViewRect(event.x(), event.y())
            self.fMouseMode = self._MOUSE_MODE_MOVE
        else:
            self.unsetCursor()
            self.fMouseMode = self._MOUSE_MODE_NONE
    def _updateStyle(self):
        self.fFrameWidth = 1 if self.fUseCustomPaint else self.frameWidth()
# ---------------------------------------------------------------------------------------------------------------------
 |