eric7/EricGraphics/EricGraphicsView.py

branch
eric7
changeset 8348
f4775ae8f441
parent 8318
962bce857696
child 8356
68ec9c3d4de5
equal deleted inserted replaced
8347:ca9ef7600df7 8348:f4775ae8f441
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a canvas view class.
8 """
9 # TODO: rename this module to EricGraphicsView
10
11 import sys
12
13 from PyQt6.QtCore import pyqtSignal, QRectF, QSize, QSizeF, Qt
14 from PyQt6.QtGui import QBrush, QPainter, QPixmap, QFont, QColor
15 from PyQt6.QtWidgets import QGraphicsView
16
17 from E5Gui.E5Application import e5App
18
19 import Preferences
20
21
22 class EricGraphicsView(QGraphicsView):
23 """
24 Class implementing a graphics view.
25
26 @signal zoomValueChanged(int) emitted to signal a change of the zoom value
27 """
28 zoomValueChanged = pyqtSignal(int)
29
30 ZoomLevels = [
31 1, 3, 5, 7, 9,
32 10, 20, 30, 50, 67, 80, 90,
33 100,
34 110, 120, 133, 150, 170, 200, 240, 300, 400,
35 500, 600, 700, 800, 900, 1000,
36 ]
37 ZoomLevelDefault = 100
38
39 def __init__(self, scene, parent=None):
40 """
41 Constructor
42
43 @param scene reference to the scene object (QGraphicsScene)
44 @param parent parent widget (QWidget)
45 """
46 super().__init__(scene, parent)
47 self.setObjectName("EricGraphicsView")
48
49 self.__initialSceneSize = self.scene().sceneRect().size()
50 self.setBackgroundBrush(QBrush(self.getBackgroundColor()))
51 self.setRenderHint(QPainter.RenderHint.Antialiasing, True)
52 self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
53 self.setAlignment(
54 Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
55 self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
56 self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
57 self.setViewportUpdateMode(
58 QGraphicsView.ViewportUpdateMode.SmartViewportUpdate)
59
60 self.setWhatsThis(self.tr(
61 "<b>Graphics View</b>\n"
62 "<p>This graphics view is used to show a diagram. \n"
63 "There are various actions available to manipulate the \n"
64 "shown items.</p>\n"
65 "<ul>\n"
66 "<li>Clicking on an item selects it.</li>\n"
67 "<li>Ctrl-clicking adds an item to the selection.</li>\n"
68 "<li>Ctrl-clicking a selected item deselects it.</li>\n"
69 "<li>Clicking on an empty spot of the canvas resets the selection."
70 "</li>\n"
71 "<li>Dragging the mouse over the canvas spans a rubberband to \n"
72 "select multiple items.</li>\n"
73 "<li>Dragging the mouse over a selected item moves the \n"
74 "whole selection.</li>\n"
75 "</ul>\n"
76 ))
77
78 def getDrawingColors(self):
79 """
80 Public method to get the configured drawing colors.
81
82 @return tuple containing the foreground and background colors
83 @rtype tuple of (QColor, QColor)
84 """
85 drawingMode = Preferences.getGraphics("DrawingMode")
86 if drawingMode == "automatic":
87 if e5App().usesDarkPalette():
88 drawingMode = "white_black"
89 else:
90 drawingMode = "black_white"
91
92 if drawingMode == "white_black":
93 return (QColor("#ffffff"), QColor("#262626"))
94 else:
95 return (QColor("#000000"), QColor("#ffffff"))
96
97 def getForegroundColor(self):
98 """
99 Public method to get the configured foreground color.
100
101 @return foreground color
102 @rtype QColor
103 """
104 return self.getDrawingColors()[0]
105
106 def getBackgroundColor(self):
107 """
108 Public method to get the configured background color.
109
110 @return background color
111 @rtype QColor
112 """
113 return self.getDrawingColors()[1]
114
115 def __levelForZoom(self, zoom):
116 """
117 Private method determining the zoom level index given a zoom factor.
118
119 @param zoom zoom factor (integer)
120 @return index of zoom factor (integer)
121 """
122 try:
123 index = EricGraphicsView.ZoomLevels.index(zoom)
124 except ValueError:
125 for index in range(len(EricGraphicsView.ZoomLevels)):
126 if zoom <= EricGraphicsView.ZoomLevels[index]:
127 break
128 return index
129
130 def zoomIn(self):
131 """
132 Public method to zoom in.
133 """
134 index = self.__levelForZoom(self.zoom())
135 if index < len(EricGraphicsView.ZoomLevels) - 1:
136 self.setZoom(EricGraphicsView.ZoomLevels[index + 1])
137
138 def zoomOut(self):
139 """
140 Public method to zoom out.
141 """
142 index = self.__levelForZoom(self.zoom())
143 if index > 0:
144 self.setZoom(EricGraphicsView.ZoomLevels[index - 1])
145
146 def zoomReset(self):
147 """
148 Public method to handle the reset the zoom value.
149 """
150 self.setZoom(
151 EricGraphicsView.ZoomLevels[EricGraphicsView.ZoomLevelDefault])
152
153 def setZoom(self, value):
154 """
155 Public method to set the zoom value in percent.
156
157 @param value zoom value in percent (integer)
158 """
159 if value != self.zoom():
160 self.resetTransform()
161 factor = value / 100.0
162 self.scale(factor, factor)
163 self.zoomValueChanged.emit(value)
164
165 def zoom(self):
166 """
167 Public method to get the current zoom factor in percent.
168
169 @return current zoom factor in percent (integer)
170 """
171 return int(self.transform().m11() * 100.0)
172
173 def resizeScene(self, amount, isWidth=True):
174 """
175 Public method to resize the scene.
176
177 @param amount size increment (integer)
178 @param isWidth flag indicating width is to be resized (boolean)
179 """
180 sceneRect = self.scene().sceneRect()
181 width = sceneRect.width()
182 height = sceneRect.height()
183 if isWidth:
184 width += amount
185 else:
186 height += amount
187 rect = self._getDiagramRect(10)
188 if width < rect.width():
189 width = rect.width()
190 if height < rect.height():
191 height = rect.height()
192
193 self.setSceneSize(width, height)
194
195 def setSceneSize(self, width, height):
196 """
197 Public method to set the scene size.
198
199 @param width width for the scene (real)
200 @param height height for the scene (real)
201 """
202 rect = self.scene().sceneRect()
203 rect.setHeight(height)
204 rect.setWidth(width)
205 self.scene().setSceneRect(rect)
206
207 def autoAdjustSceneSize(self, limit=False):
208 """
209 Public method to adjust the scene size to the diagram size.
210
211 @param limit flag indicating to limit the scene to the
212 initial size (boolean)
213 """
214 size = self._getDiagramSize(10)
215 if limit:
216 newWidth = max(size.width(), self.__initialSceneSize.width())
217 newHeight = max(size.height(), self.__initialSceneSize.height())
218 else:
219 newWidth = size.width()
220 newHeight = size.height()
221 self.setSceneSize(newWidth, newHeight)
222
223 def _getDiagramRect(self, border=0):
224 """
225 Protected method to calculate the minimum rectangle fitting the
226 diagram.
227
228 @param border border width to include in the calculation (integer)
229 @return the minimum rectangle (QRectF)
230 """
231 startx = sys.maxsize
232 starty = sys.maxsize
233 endx = 0
234 endy = 0
235 items = self.filteredItems(list(self.scene().items()))
236 for itm in items:
237 rect = itm.sceneBoundingRect()
238 itmEndX = rect.x() + rect.width()
239 itmEndY = rect.y() + rect.height()
240 itmStartX = rect.x()
241 itmStartY = rect.y()
242 if startx >= itmStartX:
243 startx = itmStartX
244 if starty >= itmStartY:
245 starty = itmStartY
246 if endx <= itmEndX:
247 endx = itmEndX
248 if endy <= itmEndY:
249 endy = itmEndY
250 if border:
251 startx -= border
252 starty -= border
253 endx += border
254 endy += border
255
256 return QRectF(startx, starty, endx - startx + 1, endy - starty + 1)
257
258 def _getDiagramSize(self, border=0):
259 """
260 Protected method to calculate the minimum size fitting the diagram.
261
262 @param border border width to include in the calculation (integer)
263 @return the minimum size (QSizeF)
264 """
265 endx = 0
266 endy = 0
267 items = self.filteredItems(list(self.scene().items()))
268 for itm in items:
269 rect = itm.sceneBoundingRect()
270 itmEndX = rect.x() + rect.width()
271 itmEndY = rect.y() + rect.height()
272 if endx <= itmEndX:
273 endx = itmEndX
274 if endy <= itmEndY:
275 endy = itmEndY
276 if border:
277 endx += border
278 endy += border
279
280 return QSizeF(endx + 1, endy + 1)
281
282 def __getDiagram(self, rect, imageFormat="PNG", filename=None):
283 """
284 Private method to retrieve the diagram from the scene fitting it
285 in the minimum rectangle.
286
287 @param rect minimum rectangle fitting the diagram
288 @type QRectF
289 @param imageFormat format for the image file
290 @type str
291 @param filename name of the file for non pixmaps
292 str
293 @return paint device containing the diagram
294 @rtype QPixmap or QSvgGenerator
295 """
296 selectedItems = self.scene().selectedItems()
297
298 # step 1: deselect all widgets
299 if selectedItems:
300 for item in selectedItems:
301 item.setSelected(False)
302
303 # step 2: grab the diagram
304 if imageFormat == "PNG":
305 paintDevice = QPixmap(int(rect.width()), int(rect.height()))
306 paintDevice.fill(self.backgroundBrush().color())
307 else:
308 from PyQt6.QtSvg import QSvgGenerator
309 paintDevice = QSvgGenerator()
310 paintDevice.setResolution(100) # 100 dpi
311 paintDevice.setSize(QSize(int(rect.width()), int(rect.height())))
312 paintDevice.setViewBox(rect)
313 paintDevice.setFileName(filename)
314 painter = QPainter(paintDevice)
315 painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
316 self.scene().render(painter, QRectF(), rect)
317
318 # step 3: reselect the widgets
319 if selectedItems:
320 for item in selectedItems:
321 item.setSelected(True)
322
323 return paintDevice
324
325 def saveImage(self, filename, imageFormat="PNG"):
326 """
327 Public method to save the scene to a file.
328
329 @param filename name of the file to write the image to (string)
330 @param imageFormat format for the image file (string)
331 @return flag indicating success (boolean)
332 """
333 rect = self._getDiagramRect(self.border)
334 if imageFormat == "SVG":
335 self.__getDiagram(rect, imageFormat=imageFormat, filename=filename)
336 return True
337 else:
338 pixmap = self.__getDiagram(rect)
339 return pixmap.save(filename, imageFormat)
340
341 def printDiagram(self, printer, diagramName=""):
342 """
343 Public method to print the diagram.
344
345 @param printer reference to a ready configured printer object
346 (QPrinter)
347 @param diagramName name of the diagram (string)
348 """
349 painter = QPainter(printer)
350
351 font = QFont(["times"], 10)
352 painter.setFont(font)
353 fm = painter.fontMetrics()
354 fontHeight = fm.lineSpacing()
355 marginX = (
356 printer.pageLayout().paintRectPixels(printer.resolution()).x() -
357 printer.pageLayout().fullRectPixels(printer.resolution()).x()
358 )
359 marginX = (
360 Preferences.getPrinter("LeftMargin") *
361 int(printer.resolution() / 2.54) - marginX
362 )
363 marginY = (
364 printer.pageLayout().paintRectPixels(printer.resolution()).y() -
365 printer.pageLayout().fullRectPixels(printer.resolution()).y()
366 )
367 marginY = (
368 Preferences.getPrinter("TopMargin") *
369 int(printer.resolution() / 2.54) - marginY
370 )
371
372 width = (
373 printer.width() - marginX -
374 Preferences.getPrinter("RightMargin") *
375 int(printer.resolution() / 2.54)
376 )
377 height = (
378 printer.height() - fontHeight - 4 - marginY -
379 Preferences.getPrinter("BottomMargin") *
380 int(printer.resolution() / 2.54)
381 )
382
383 self.scene().render(painter,
384 target=QRectF(marginX, marginY, width, height))
385
386 # write a foot note
387 tc = QColor(50, 50, 50)
388 painter.setPen(tc)
389 painter.drawRect(marginX, marginY, width, height)
390 painter.drawLine(marginX, marginY + height + 2,
391 marginX + width, marginY + height + 2)
392 painter.setFont(font)
393 painter.drawText(marginX, marginY + height + 4, width,
394 fontHeight, Qt.AlignmentFlag.AlignRight, diagramName)
395
396 painter.end()
397
398 ###########################################################################
399 ## The methods below should be overridden by subclasses to get special
400 ## behavior.
401 ###########################################################################
402
403 def filteredItems(self, items):
404 """
405 Public method to filter a list of items.
406
407 @param items list of items as returned by the scene object
408 (QGraphicsItem)
409 @return list of interesting collision items (QGraphicsItem)
410 """
411 # just return the list unchanged
412 return items

eric ide

mercurial