|
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 |