diff -r ca9ef7600df7 -r f4775ae8f441 eric7/EricGraphics/EricGraphicsView.py --- /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