Sun, 03 Dec 2023 19:46:34 +0100
Corrected some uses of dict.keys(), dict.values() and dict.items().
# -*- coding: utf-8 -*- # Copyright (c) 2007 - 2023 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 (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 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 (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 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 (integer) @return the minimum size (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 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 (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 list(items)