eric6/E5Graphics/E5GraphicsView.py

Sun, 12 Apr 2020 18:40:37 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 12 Apr 2020 18:40:37 +0200
changeset 7529
2b8a0d8ba12a
parent 7525
6d1f954947bc
child 7780
41420f82c0ac
permissions
-rw-r--r--

UML Diagrams: added support for dark color scheme.

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

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

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


import sys

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

from E5Gui.E5Application import e5App

import Preferences


class E5GraphicsView(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(E5GraphicsView, self).__init__(scene, parent)
        self.setObjectName("E5GraphicsView")
        
        self.__initialSceneSize = self.scene().sceneRect().size()
        self.setBackgroundBrush(QBrush(self.getBackgroundColor()))
        self.setRenderHint(QPainter.Antialiasing, True)
        self.setDragMode(QGraphicsView.RubberBandDrag)
        self.setAlignment(Qt.Alignment(Qt.AlignLeft | Qt.AlignTop))
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.setViewportUpdateMode(QGraphicsView.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 = E5GraphicsView.ZoomLevels.index(zoom)
        except ValueError:
            for index in range(len(E5GraphicsView.ZoomLevels)):
                if zoom <= E5GraphicsView.ZoomLevels[index]:
                    break
        return index
    
    def zoomIn(self):
        """
        Public method to zoom in.
        """
        index = self.__levelForZoom(self.zoom())
        if index < len(E5GraphicsView.ZoomLevels) - 1:
            self.setZoom(E5GraphicsView.ZoomLevels[index + 1])
        
    def zoomOut(self):
        """
        Public method to zoom out.
        """
        index = self.__levelForZoom(self.zoom())
        if index > 0:
            self.setZoom(E5GraphicsView.ZoomLevels[index - 1])
    
    def zoomReset(self):
        """
        Public method to handle the reset the zoom value.
        """
        self.setZoom(
            E5GraphicsView.ZoomLevels[E5GraphicsView.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 (QRectF)
        @param imageFormat format for the image file (string)
        @param filename name of the file for non pixmaps (string)
        @return diagram pixmap to receive the diagram (QPixmap)
        """
        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 PyQt5.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.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()
        painter.begin(printer)
        offsetX = 0
        offsetY = 0
        widthX = 0
        heightY = 0
        font = QFont("times", 10)
        painter.setFont(font)
        fm = painter.fontMetrics()
        fontHeight = fm.lineSpacing()
        marginX = printer.pageRect().x() - printer.paperRect().x()
        marginX = (
            Preferences.getPrinter("LeftMargin") *
            int(printer.resolution() / 2.54) - marginX
        )
        marginY = printer.pageRect().y() - printer.paperRect().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)
        )
        
        border = self.border == 0 and 5 or self.border
        rect = self._getDiagramRect(border)
        diagram = self.__getDiagram(rect)
        
        finishX = False
        finishY = False
        page = 0
        pageX = 0
        pageY = 1
        while not finishX or not finishY:
            if not finishX:
                offsetX = pageX * width
                pageX += 1
            elif not finishY:
                offsetY = pageY * height
                offsetX = 0
                pageY += 1
                finishX = False
                pageX = 1
            if (width + offsetX) > diagram.width():
                finishX = True
                widthX = diagram.width() - offsetX
            else:
                widthX = width
            if diagram.width() < width:
                widthX = diagram.width()
                finishX = True
                offsetX = 0
            if (height + offsetY) > diagram.height():
                finishY = True
                heightY = diagram.height() - offsetY
            else:
                heightY = height
            if diagram.height() < height:
                finishY = True
                heightY = diagram.height()
                offsetY = 0
            
            painter.drawPixmap(marginX, marginY, diagram,
                               offsetX, offsetY, widthX, heightY)
            # write a foot note
            s = self.tr("{0}, Page {1}").format(diagramName, page + 1)
            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.AlignRight, s)
            if not finishX or not finishY:
                printer.newPage()
                page += 1
        
        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