|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2010 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a canvas view class. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 |
|
12 from PyQt4.QtCore import * |
|
13 from PyQt4.QtGui import * |
|
14 |
|
15 import Preferences |
|
16 |
|
17 class E5GraphicsView(QGraphicsView): |
|
18 """ |
|
19 Class implementing a graphics view. |
|
20 """ |
|
21 def __init__(self, scene, parent = None): |
|
22 """ |
|
23 Constructor |
|
24 |
|
25 @param scene reference to the scene object (QGraphicsScene) |
|
26 @param parent parent widget (QWidget) |
|
27 """ |
|
28 QGraphicsView.__init__(self, scene, parent) |
|
29 self.setObjectName("E5GraphicsView") |
|
30 |
|
31 self.setBackgroundBrush(QBrush(Qt.white)) |
|
32 self.setRenderHint(QPainter.Antialiasing, True) |
|
33 self.setDragMode(QGraphicsView.RubberBandDrag) |
|
34 self.setAlignment(Qt.Alignment(Qt.AlignLeft | Qt.AlignTop)) |
|
35 self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) |
|
36 self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) |
|
37 |
|
38 # available as of Qt 4.3 |
|
39 try: |
|
40 self.setViewportUpdateMode(QGraphicsView.SmartViewportUpdate) |
|
41 except AttributeError: |
|
42 pass |
|
43 |
|
44 self.setWhatsThis(self.trUtf8("<b>Graphics View</b>\n" |
|
45 "<p>This graphics view is used to show a diagram. \n" |
|
46 "There are various actions available to manipulate the \n" |
|
47 "shown items.</p>\n" |
|
48 "<ul>\n" |
|
49 "<li>Clicking on an item selects it.</li>\n" |
|
50 "<li>Ctrl-clicking adds an item to the selection.</li>\n" |
|
51 "<li>Ctrl-clicking a selected item deselects it.</li>\n" |
|
52 "<li>Clicking on an empty spot of the canvas resets the selection.</li>\n" |
|
53 "<li>Dragging the mouse over the canvas spans a rubberband to \n" |
|
54 "select multiple items.</li>\n" |
|
55 "<li>Dragging the mouse over a selected item moves the \n" |
|
56 "whole selection.</li>\n" |
|
57 "</ul>\n" |
|
58 )) |
|
59 |
|
60 def zoomIn(self): |
|
61 """ |
|
62 Public method to zoom in. |
|
63 """ |
|
64 self.scale(1.25, 1.25) |
|
65 |
|
66 def zoomOut(self): |
|
67 """ |
|
68 Public method to zoom out. |
|
69 """ |
|
70 self.scale(0.8, 0.8) |
|
71 |
|
72 def zoomReset(self): |
|
73 """ |
|
74 Public method to handle the reset zoom context menu entry. |
|
75 """ |
|
76 self.resetMatrix() |
|
77 |
|
78 def setZoom(self, zoomFactor): |
|
79 """ |
|
80 Public method to set the zoom factor. |
|
81 |
|
82 @param zoomFactor new zoom factor (float) |
|
83 """ |
|
84 self.zoomReset() |
|
85 self.scale(zoomFactor, zoomFactor) |
|
86 |
|
87 def zoom(self): |
|
88 """ |
|
89 Public method to get the current zoom factor. |
|
90 |
|
91 @return current zoom factor (float) |
|
92 """ |
|
93 return self.matrix().m11() |
|
94 |
|
95 def resizeScene(self, amount, isWidth = True): |
|
96 """ |
|
97 Public method to resize the scene. |
|
98 |
|
99 @param isWidth flag indicating width is to be resized (boolean) |
|
100 @param amount size increment (integer) |
|
101 """ |
|
102 sceneRect = self.scene().sceneRect() |
|
103 width = sceneRect.width() |
|
104 height = sceneRect.height() |
|
105 if isWidth: |
|
106 width += amount |
|
107 else: |
|
108 height += amount |
|
109 rect = self._getDiagramRect(10) |
|
110 if width < rect.width(): |
|
111 width = rect.width() |
|
112 if height < rect.height(): |
|
113 height = rect.height() |
|
114 |
|
115 self.setSceneSize(width, height) |
|
116 |
|
117 def setSceneSize(self, width, height): |
|
118 """ |
|
119 Public method to set the scene size. |
|
120 |
|
121 @param width width for the scene (integer) |
|
122 @param height height for the scene (integer) |
|
123 """ |
|
124 rect = self.scene().sceneRect() |
|
125 rect.setHeight(height) |
|
126 rect.setWidth(width) |
|
127 self.setSceneRect(rect) |
|
128 |
|
129 def _getDiagramRect(self, border = 0): |
|
130 """ |
|
131 Protected method to calculate the minimum rectangle fitting the diagram. |
|
132 |
|
133 @param border border width to include in the calculation (integer) |
|
134 @return the minimum rectangle (QRectF) |
|
135 """ |
|
136 startx = sys.maxsize |
|
137 starty = sys.maxsize |
|
138 endx = 0 |
|
139 endy = 0 |
|
140 items = self.filteredItems(list(self.scene().items())) |
|
141 for itm in items: |
|
142 rect = itm.sceneBoundingRect() |
|
143 itmEndX = rect.x() + rect.width() |
|
144 itmEndY = rect.y()+ rect.height() |
|
145 itmStartX = rect.x() |
|
146 itmStartY = rect.y() |
|
147 if startx >= itmStartX: |
|
148 startx = itmStartX |
|
149 if starty >= itmStartY: |
|
150 starty = itmStartY |
|
151 if endx <= itmEndX: |
|
152 endx = itmEndX |
|
153 if endy <= itmEndY: |
|
154 endy = itmEndY |
|
155 if border: |
|
156 startx -= border |
|
157 starty -= border |
|
158 endx += border |
|
159 endy += border |
|
160 |
|
161 return QRectF(startx, starty, endx - startx + 1, endy - starty + 1) |
|
162 |
|
163 def _getDiagramSize(self, border = 0): |
|
164 """ |
|
165 Protected method to calculate the minimum size fitting the diagram. |
|
166 |
|
167 @param border border width to include in the calculation (integer) |
|
168 @return the minimum size (QSizeF) |
|
169 """ |
|
170 endx = 0 |
|
171 endy = 0 |
|
172 items = self.filteredItems(list(self.scene().items())) |
|
173 for itm in items: |
|
174 rect = itm.sceneBoundingRect() |
|
175 itmEndX = rect.x() + rect.width() |
|
176 itmEndY = rect.y()+ rect.height() |
|
177 if endx <= itmEndX: |
|
178 endx = itmEndX |
|
179 if endy <= itmEndY: |
|
180 endy = itmEndY |
|
181 if border: |
|
182 endx += border |
|
183 endy += border |
|
184 |
|
185 return QSizeF(endx + 1, endy + 1) |
|
186 |
|
187 def __getDiagram(self, rect, format = "PNG", filename = None): |
|
188 """ |
|
189 Private method to retrieve the diagram from the scene fitting it |
|
190 in the minimum rectangle. |
|
191 |
|
192 @param rect minimum rectangle fitting the diagram (QRectF) |
|
193 @param format format for the image file (string) |
|
194 @param filename name of the file for non pixmaps (string) |
|
195 @return diagram pixmap to receive the diagram (QPixmap) |
|
196 """ |
|
197 selectedItems = self.scene().selectedItems() |
|
198 |
|
199 # step 1: deselect all widgets |
|
200 if selectedItems: |
|
201 for item in selectedItems: |
|
202 item.setSelected(False) |
|
203 |
|
204 # step 2: grab the diagram |
|
205 if format == "PNG": |
|
206 paintDevice = QPixmap(int(rect.width()), int(rect.height())) |
|
207 paintDevice.fill(self.backgroundBrush().color()) |
|
208 else: |
|
209 from PyQt4.QtSvg import QSvgGenerator |
|
210 paintDevice = QSvgGenerator() |
|
211 paintDevice.setResolution(100) # 100 dpi |
|
212 paintDevice.setSize(QSize(int(rect.width()), int(rect.height()))) |
|
213 paintDevice.setFileName(filename) |
|
214 painter = QPainter(paintDevice) |
|
215 painter.setRenderHint(QPainter.Antialiasing, True) |
|
216 self.scene().render(painter, QRectF(), rect) |
|
217 |
|
218 # step 3: reselect the widgets |
|
219 if selectedItems: |
|
220 for item in selectedItems: |
|
221 item.setSelected(True) |
|
222 |
|
223 return paintDevice |
|
224 |
|
225 def saveImage(self, filename, format = "PNG"): |
|
226 """ |
|
227 Public method to save the scene to a file. |
|
228 |
|
229 @param filename name of the file to write the image to (string) |
|
230 @param format format for the image file (string) |
|
231 @return flag indicating success (boolean) |
|
232 """ |
|
233 rect = self._getDiagramRect(self.border) |
|
234 if format == "SVG": |
|
235 svg = self.__getDiagram(rect, format = format, filename = filename) |
|
236 return True |
|
237 else: |
|
238 pixmap = self.__getDiagram(rect) |
|
239 return pixmap.save(filename, format) |
|
240 |
|
241 def printDiagram(self, printer, diagramName = ""): |
|
242 """ |
|
243 Public method to print the diagram. |
|
244 |
|
245 @param printer reference to a ready configured printer object (QPrinter) |
|
246 @param diagramName name of the diagram (string) |
|
247 """ |
|
248 painter = QPainter() |
|
249 painter.begin(printer) |
|
250 offsetX = 0 |
|
251 offsetY = 0 |
|
252 widthX = 0 |
|
253 heightY = 0 |
|
254 font = QFont("times", 10) |
|
255 painter.setFont(font) |
|
256 fm = painter.fontMetrics() |
|
257 fontHeight = fm.lineSpacing() |
|
258 marginX = printer.pageRect().x() - printer.paperRect().x() |
|
259 marginX = \ |
|
260 Preferences.getPrinter("LeftMargin") * int(printer.resolution() / 2.54) \ |
|
261 - marginX |
|
262 marginY = printer.pageRect().y() - printer.paperRect().y() |
|
263 marginY = \ |
|
264 Preferences.getPrinter("TopMargin") * int(printer.resolution() / 2.54) \ |
|
265 - marginY |
|
266 |
|
267 width = printer.width() - marginX \ |
|
268 - Preferences.getPrinter("RightMargin") * int(printer.resolution() / 2.54) |
|
269 height = printer.height() - fontHeight - 4 - marginY \ |
|
270 - Preferences.getPrinter("BottomMargin") * int(printer.resolution() / 2.54) |
|
271 |
|
272 border = self.border == 0 and 5 or self.border |
|
273 rect = self._getDiagramRect(border) |
|
274 diagram = self.__getDiagram(rect) |
|
275 |
|
276 finishX = False |
|
277 finishY = False |
|
278 page = 0 |
|
279 pageX = 0 |
|
280 pageY = 1 |
|
281 while not finishX or not finishY: |
|
282 if not finishX: |
|
283 offsetX = pageX * width |
|
284 pageX += 1 |
|
285 elif not finishY: |
|
286 offsetY = pageY * height |
|
287 offsetX = 0 |
|
288 pageY += 1 |
|
289 finishX = False |
|
290 pageX = 1 |
|
291 if (width + offsetX) > diagram.width(): |
|
292 finishX = True |
|
293 widthX = diagram.width() - offsetX |
|
294 else: |
|
295 widthX = width |
|
296 if diagram.width() < width: |
|
297 widthX = diagram.width() |
|
298 finishX = True |
|
299 offsetX = 0 |
|
300 if (height + offsetY) > diagram.height(): |
|
301 finishY = True |
|
302 heightY = diagram.height() - offsetY |
|
303 else: |
|
304 heightY = height |
|
305 if diagram.height() < height: |
|
306 finishY = True |
|
307 heightY = diagram.height() |
|
308 offsetY = 0 |
|
309 |
|
310 painter.drawPixmap(marginX, marginY, diagram, |
|
311 offsetX, offsetY, widthX, heightY) |
|
312 # write a foot note |
|
313 s = self.trUtf8("Diagram: {0}, Page {1}").format(diagramName, page + 1) |
|
314 tc = QColor(50, 50, 50) |
|
315 painter.setPen(tc) |
|
316 painter.drawRect(marginX, marginY, width, height) |
|
317 painter.drawLine(marginX, marginY + height + 2, |
|
318 marginX + width, marginY + height + 2) |
|
319 painter.setFont(font) |
|
320 painter.drawText(marginX, marginY + height + 4, width, |
|
321 fontHeight, Qt.AlignRight, s) |
|
322 if not finishX or not finishY: |
|
323 printer.newPage() |
|
324 page += 1 |
|
325 |
|
326 painter.end() |
|
327 |
|
328 ############################################################################ |
|
329 ## The methods below should be overridden by subclasses to get special |
|
330 ## behavior. |
|
331 ############################################################################ |
|
332 |
|
333 def filteredItems(self, items): |
|
334 """ |
|
335 Public method to filter a list of items. |
|
336 |
|
337 @param items list of items as returned by the scene object |
|
338 (QGraphicsItem) |
|
339 @return list of interesting collision items (QGraphicsItem) |
|
340 """ |
|
341 # just return the list unchanged |
|
342 return items |