eric7/EricGraphics/EricGraphicsView.py

branch
eric7
changeset 8348
f4775ae8f441
parent 8318
962bce857696
child 8356
68ec9c3d4de5
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/EricGraphics/EricGraphicsView.py	Fri May 21 18:01:11 2021 +0200
@@ -0,0 +1,412 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a canvas view class.
+"""
+# TODO: rename this module to EricGraphicsView
+
+import sys
+
+from PyQt6.QtCore import pyqtSignal, QRectF, QSize, QSizeF, Qt
+from PyQt6.QtGui import QBrush, QPainter, QPixmap, QFont, QColor
+from PyQt6.QtWidgets import QGraphicsView
+
+from E5Gui.E5Application import e5App
+
+import Preferences
+
+
+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 (QGraphicsScene)
+        @param parent parent widget (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 e5App().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 (integer)
+        @return index of zoom factor (integer)
+        """
+        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 (integer)
+        """
+        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 (integer)
+        """
+        return int(self.transform().m11() * 100.0)
+       
+    def resizeScene(self, amount, isWidth=True):
+        """
+        Public method to resize the scene.
+        
+        @param amount size increment (integer)
+        @param isWidth flag indicating width is to be resized (boolean)
+        """
+        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 (real)
+        @param height height for the scene (real)
+        """
+        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 (boolean)
+        """
+        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 (integer)
+        @return the minimum rectangle (QRectF)
+        """
+        startx = sys.maxsize
+        starty = sys.maxsize
+        endx = 0
+        endy = 0
+        items = self.filteredItems(list(self.scene().items()))
+        for itm in 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 (integer)
+        @return the minimum size (QSizeF)
+        """
+        endx = 0
+        endy = 0
+        items = self.filteredItems(list(self.scene().items()))
+        for itm in 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
+        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:
+            from PyQt6.QtSvg import QSvgGenerator
+            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 (string)
+        @param imageFormat format for the image file (string)
+        @return flag indicating success (boolean)
+        """
+        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
+            (QPrinter)
+        @param diagramName name of the diagram (string)
+        """
+        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
+            (QGraphicsItem)
+        @return list of interesting collision items (QGraphicsItem)
+        """
+        # just return the list unchanged
+        return items

eric ide

mercurial