src/eric7/EricGraphics/EricGraphicsView.py

Sat, 23 Dec 2023 15:48:12 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 23 Dec 2023 15:48:12 +0100
branch
eric7
changeset 10439
21c28b0f9e41
parent 10423
299802979277
child 10917
4f40180b98dc
permissions
-rw-r--r--

Updated copyright for 2024.

# -*- coding: utf-8 -*-

# Copyright (c) 2007 - 2024 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a canvas view class.
"""

import sys

from PyQt6.QtCore import QRectF, QSize, QSizeF, Qt, pyqtSignal
from PyQt6.QtGui import QBrush, QColor, QFont, QPainter, QPixmap
from PyQt6.QtSvg import QSvgGenerator
from PyQt6.QtWidgets import QGraphicsView

from eric7 import Preferences
from eric7.EricWidgets.EricApplication import ericApp


class EricGraphicsView(QGraphicsView):
    """
    Class implementing a graphics view.

    @signal zoomValueChanged(int) emitted to signal a change of the zoom value
    """

    zoomValueChanged = pyqtSignal(int)

    ZoomLevels = [
        1,
        3,
        5,
        7,
        9,
        10,
        20,
        30,
        50,
        67,
        80,
        90,
        100,
        110,
        120,
        133,
        150,
        170,
        200,
        240,
        300,
        400,
        500,
        600,
        700,
        800,
        900,
        1000,
    ]
    ZoomLevelDefault = 100

    def __init__(self, scene, parent=None):
        """
        Constructor

        @param scene reference to the scene object
        @type QGraphicsScene
        @param parent parent widget
        @type QWidget
        """
        super().__init__(scene, parent)
        self.setObjectName("EricGraphicsView")

        self.__initialSceneSize = self.scene().sceneRect().size()
        self.setBackgroundBrush(QBrush(self.getBackgroundColor()))
        self.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
        self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.SmartViewportUpdate)

        self.setWhatsThis(
            self.tr(
                "<b>Graphics View</b>\n"
                "<p>This graphics view is used to show a diagram. \n"
                "There are various actions available to manipulate the \n"
                "shown items.</p>\n"
                "<ul>\n"
                "<li>Clicking on an item selects it.</li>\n"
                "<li>Ctrl-clicking adds an item to the selection.</li>\n"
                "<li>Ctrl-clicking a selected item deselects it.</li>\n"
                "<li>Clicking on an empty spot of the canvas resets the selection."
                "</li>\n"
                "<li>Dragging the mouse over the canvas spans a rubberband to \n"
                "select multiple items.</li>\n"
                "<li>Dragging the mouse over a selected item moves the \n"
                "whole selection.</li>\n"
                "</ul>\n"
            )
        )

    def getDrawingColors(self):
        """
        Public method to get the configured drawing colors.

        @return tuple containing the foreground and background colors
        @rtype tuple of (QColor, QColor)
        """
        drawingMode = Preferences.getGraphics("DrawingMode")
        if drawingMode == "automatic":
            if ericApp().usesDarkPalette():
                drawingMode = "white_black"
            else:
                drawingMode = "black_white"

        if drawingMode == "white_black":
            return (QColor("#ffffff"), QColor("#262626"))
        else:
            return (QColor("#000000"), QColor("#ffffff"))

    def getForegroundColor(self):
        """
        Public method to get the configured foreground color.

        @return foreground color
        @rtype QColor
        """
        return self.getDrawingColors()[0]

    def getBackgroundColor(self):
        """
        Public method to get the configured background color.

        @return background color
        @rtype QColor
        """
        return self.getDrawingColors()[1]

    def __levelForZoom(self, zoom):
        """
        Private method determining the zoom level index given a zoom factor.

        @param zoom zoom factor
        @type int
        @return index of zoom factor
        @rtype int
        """
        try:
            index = EricGraphicsView.ZoomLevels.index(zoom)
        except ValueError:
            for index in range(len(EricGraphicsView.ZoomLevels)):
                if zoom <= EricGraphicsView.ZoomLevels[index]:
                    break
        return index

    def zoomIn(self):
        """
        Public method to zoom in.
        """
        index = self.__levelForZoom(self.zoom())
        if index < len(EricGraphicsView.ZoomLevels) - 1:
            self.setZoom(EricGraphicsView.ZoomLevels[index + 1])

    def zoomOut(self):
        """
        Public method to zoom out.
        """
        index = self.__levelForZoom(self.zoom())
        if index > 0:
            self.setZoom(EricGraphicsView.ZoomLevels[index - 1])

    def zoomReset(self):
        """
        Public method to handle the reset the zoom value.
        """
        self.setZoom(EricGraphicsView.ZoomLevels[EricGraphicsView.ZoomLevelDefault])

    def setZoom(self, value):
        """
        Public method to set the zoom value in percent.

        @param value zoom value in percent
        @type int
        """
        if value != self.zoom():
            self.resetTransform()
            factor = value / 100.0
            self.scale(factor, factor)
            self.zoomValueChanged.emit(value)

    def zoom(self):
        """
        Public method to get the current zoom factor in percent.

        @return current zoom factor in percent
        @rtype int
        """
        return int(self.transform().m11() * 100.0)

    def resizeScene(self, amount, isWidth=True):
        """
        Public method to resize the scene.

        @param amount size increment
        @type int
        @param isWidth flag indicating width is to be resized
        @type bool
        """
        sceneRect = self.scene().sceneRect()
        width = sceneRect.width()
        height = sceneRect.height()
        if isWidth:
            width += amount
        else:
            height += amount
        rect = self._getDiagramRect(10)
        if width < rect.width():
            width = rect.width()
        if height < rect.height():
            height = rect.height()

        self.setSceneSize(width, height)

    def setSceneSize(self, width, height):
        """
        Public method to set the scene size.

        @param width width for the scene
        @type float
        @param height height for the scene
        @type float
        """
        rect = self.scene().sceneRect()
        rect.setHeight(height)
        rect.setWidth(width)
        self.scene().setSceneRect(rect)

    def autoAdjustSceneSize(self, limit=False):
        """
        Public method to adjust the scene size to the diagram size.

        @param limit flag indicating to limit the scene to the
            initial size
        @type bool
        """
        size = self._getDiagramSize(10)
        if limit:
            newWidth = max(size.width(), self.__initialSceneSize.width())
            newHeight = max(size.height(), self.__initialSceneSize.height())
        else:
            newWidth = size.width()
            newHeight = size.height()
        self.setSceneSize(newWidth, newHeight)

    def _getDiagramRect(self, border=0):
        """
        Protected method to calculate the minimum rectangle fitting the
        diagram.

        @param border border width to include in the calculation
        @type int
        @return the minimum rectangle
        @rtype QRectF
        """
        startx = sys.maxsize
        starty = sys.maxsize
        endx = 0
        endy = 0
        for itm in self.filteredItems(self.scene().items()):
            rect = itm.sceneBoundingRect()
            itmEndX = rect.x() + rect.width()
            itmEndY = rect.y() + rect.height()
            itmStartX = rect.x()
            itmStartY = rect.y()
            if startx >= itmStartX:
                startx = itmStartX
            if starty >= itmStartY:
                starty = itmStartY
            if endx <= itmEndX:
                endx = itmEndX
            if endy <= itmEndY:
                endy = itmEndY
        if border:
            startx -= border
            starty -= border
            endx += border
            endy += border

        return QRectF(startx, starty, endx - startx + 1, endy - starty + 1)

    def _getDiagramSize(self, border=0):
        """
        Protected method to calculate the minimum size fitting the diagram.

        @param border border width to include in the calculation
        @type int
        @return the minimum size
        @rtype QSizeF
        """
        endx = 0
        endy = 0
        for itm in self.filteredItems(self.scene().items()):
            rect = itm.sceneBoundingRect()
            itmEndX = rect.x() + rect.width()
            itmEndY = rect.y() + rect.height()
            if endx <= itmEndX:
                endx = itmEndX
            if endy <= itmEndY:
                endy = itmEndY
        if border:
            endx += border
            endy += border

        return QSizeF(endx + 1, endy + 1)

    def __getDiagram(self, rect, imageFormat="PNG", filename=None):
        """
        Private method to retrieve the diagram from the scene fitting it
        in the minimum rectangle.

        @param rect minimum rectangle fitting the diagram
        @type QRectF
        @param imageFormat format for the image file
        @type str
        @param filename name of the file for non pixmaps
        @type str
        @return paint device containing the diagram
        @rtype QPixmap or QSvgGenerator
        """
        selectedItems = self.scene().selectedItems()

        # step 1: deselect all widgets
        if selectedItems:
            for item in selectedItems:
                item.setSelected(False)

        # step 2: grab the diagram
        if imageFormat == "PNG":
            paintDevice = QPixmap(int(rect.width()), int(rect.height()))
            paintDevice.fill(self.backgroundBrush().color())
        else:
            paintDevice = QSvgGenerator()
            paintDevice.setResolution(100)  # 100 dpi
            paintDevice.setSize(QSize(int(rect.width()), int(rect.height())))
            paintDevice.setViewBox(rect)
            paintDevice.setFileName(filename)
        painter = QPainter(paintDevice)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        self.scene().render(painter, QRectF(), rect)

        # step 3: reselect the widgets
        if selectedItems:
            for item in selectedItems:
                item.setSelected(True)

        return paintDevice

    def saveImage(self, filename, imageFormat="PNG"):
        """
        Public method to save the scene to a file.

        @param filename name of the file to write the image to
        @type float
        @param imageFormat format for the image file
        @type float
        @return flag indicating success
        @rtype bool
        """
        rect = self._getDiagramRect(self.border)
        if imageFormat == "SVG":
            self.__getDiagram(rect, imageFormat=imageFormat, filename=filename)
            return True
        else:
            pixmap = self.__getDiagram(rect)
            return pixmap.save(filename, imageFormat)

    def printDiagram(self, printer, diagramName=""):
        """
        Public method to print the diagram.

        @param printer reference to a ready configured printer object
        @type QPrinter
        @param diagramName name of the diagram
        @type float
        """
        painter = QPainter(printer)

        font = QFont(["times"], 10)
        painter.setFont(font)
        fm = painter.fontMetrics()
        fontHeight = fm.lineSpacing()
        marginX = (
            printer.pageLayout().paintRectPixels(printer.resolution()).x()
            - printer.pageLayout().fullRectPixels(printer.resolution()).x()
        )
        marginX = (
            Preferences.getPrinter("LeftMargin") * int(printer.resolution() / 2.54)
            - marginX
        )
        marginY = (
            printer.pageLayout().paintRectPixels(printer.resolution()).y()
            - printer.pageLayout().fullRectPixels(printer.resolution()).y()
        )
        marginY = (
            Preferences.getPrinter("TopMargin") * int(printer.resolution() / 2.54)
            - marginY
        )

        width = (
            printer.width()
            - marginX
            - Preferences.getPrinter("RightMargin") * int(printer.resolution() / 2.54)
        )
        height = (
            printer.height()
            - fontHeight
            - 4
            - marginY
            - Preferences.getPrinter("BottomMargin") * int(printer.resolution() / 2.54)
        )

        self.scene().render(painter, target=QRectF(marginX, marginY, width, height))

        # write a foot note
        tc = QColor(50, 50, 50)
        painter.setPen(tc)
        painter.drawRect(marginX, marginY, width, height)
        painter.drawLine(
            marginX, marginY + height + 2, marginX + width, marginY + height + 2
        )
        painter.setFont(font)
        painter.drawText(
            marginX,
            marginY + height + 4,
            width,
            fontHeight,
            Qt.AlignmentFlag.AlignRight,
            diagramName,
        )

        painter.end()

    ###########################################################################
    ## The methods below should be overridden by subclasses to get special
    ## behavior.
    ###########################################################################

    def filteredItems(self, items):
        """
        Public method to filter a list of items.

        @param items list of items as returned by the scene object
        @type QGraphicsItem
        @return list of interesting collision items
        @rtype QGraphicsItem
        """
        # just return the list unchanged
        return list(items)

eric ide

mercurial