1#!/usr/bin/env python
2
3
4#############################################################################
5##
6## Copyright (C) 2013 Riverbank Computing Limited.
7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
8## All rights reserved.
9##
10## This file is part of the examples of PyQt.
11##
12## $QT_BEGIN_LICENSE:BSD$
13## You may use this file under the terms of the BSD license as follows:
14##
15## "Redistribution and use in source and binary forms, with or without
16## modification, are permitted provided that the following conditions are
17## met:
18##   * Redistributions of source code must retain the above copyright
19##     notice, this list of conditions and the following disclaimer.
20##   * Redistributions in binary form must reproduce the above copyright
21##     notice, this list of conditions and the following disclaimer in
22##     the documentation and/or other materials provided with the
23##     distribution.
24##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
25##     the names of its contributors may be used to endorse or promote
26##     products derived from this software without specific prior written
27##     permission.
28##
29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
40## $QT_END_LICENSE$
41##
42#############################################################################
43
44
45import math
46
47from PyQt5.QtCore import (pyqtSignal, QLineF, QPointF, QRect, QRectF, QSize,
48        QSizeF, Qt)
49from PyQt5.QtGui import (QBrush, QColor, QFont, QIcon, QIntValidator, QPainter,
50        QPainterPath, QPen, QPixmap, QPolygonF)
51from PyQt5.QtWidgets import (QAction, QApplication, QButtonGroup, QComboBox,
52        QFontComboBox, QGraphicsItem, QGraphicsLineItem, QGraphicsPolygonItem,
53        QGraphicsScene, QGraphicsTextItem, QGraphicsView, QGridLayout,
54        QHBoxLayout, QLabel, QMainWindow, QMenu, QMessageBox, QSizePolicy,
55        QToolBox, QToolButton, QWidget)
56
57import diagramscene_rc
58
59
60class Arrow(QGraphicsLineItem):
61    def __init__(self, startItem, endItem, parent=None, scene=None):
62        super(Arrow, self).__init__(parent, scene)
63
64        self.arrowHead = QPolygonF()
65
66        self.myStartItem = startItem
67        self.myEndItem = endItem
68        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
69        self.myColor = Qt.black
70        self.setPen(QPen(self.myColor, 2, Qt.SolidLine, Qt.RoundCap,
71                Qt.RoundJoin))
72
73    def setColor(self, color):
74        self.myColor = color
75
76    def startItem(self):
77        return self.myStartItem
78
79    def endItem(self):
80        return self.myEndItem
81
82    def boundingRect(self):
83        extra = (self.pen().width() + 20) / 2.0
84        p1 = self.line().p1()
85        p2 = self.line().p2()
86        return QRectF(p1, QSizeF(p2.x() - p1.x(), p2.y() - p1.y())).normalized().adjusted(-extra, -extra, extra, extra)
87
88    def shape(self):
89        path = super(Arrow, self).shape()
90        path.addPolygon(self.arrowHead)
91        return path
92
93    def updatePosition(self):
94        line = QLineF(self.mapFromItem(self.myStartItem, 0, 0), self.mapFromItem(self.myEndItem, 0, 0))
95        self.setLine(line)
96
97    def paint(self, painter, option, widget=None):
98        if (self.myStartItem.collidesWithItem(self.myEndItem)):
99            return
100
101        myStartItem = self.myStartItem
102        myEndItem = self.myEndItem
103        myColor = self.myColor
104        myPen = self.pen()
105        myPen.setColor(self.myColor)
106        arrowSize = 20.0
107        painter.setPen(myPen)
108        painter.setBrush(self.myColor)
109
110        centerLine = QLineF(myStartItem.pos(), myEndItem.pos())
111        endPolygon = myEndItem.polygon()
112        p1 = endPolygon.first() + myEndItem.pos()
113
114        intersectPoint = QPointF()
115        for i in endPolygon:
116            p2 = i + myEndItem.pos()
117            polyLine = QLineF(p1, p2)
118            intersectType = polyLine.intersect(centerLine, intersectPoint)
119            if intersectType == QLineF.BoundedIntersection:
120                break
121            p1 = p2
122
123        self.setLine(QLineF(intersectPoint, myStartItem.pos()))
124        line = self.line()
125
126        angle = math.acos(line.dx() / line.length())
127        if line.dy() >= 0:
128            angle = (math.pi * 2.0) - angle
129
130        arrowP1 = line.p1() + QPointF(math.sin(angle + math.pi / 3.0) * arrowSize,
131                                        math.cos(angle + math.pi / 3) * arrowSize)
132        arrowP2 = line.p1() + QPointF(math.sin(angle + math.pi - math.pi / 3.0) * arrowSize,
133                                        math.cos(angle + math.pi - math.pi / 3.0) * arrowSize)
134
135        self.arrowHead.clear()
136        for point in [line.p1(), arrowP1, arrowP2]:
137            self.arrowHead.append(point)
138
139        painter.drawLine(line)
140        painter.drawPolygon(self.arrowHead)
141        if self.isSelected():
142            painter.setPen(QPen(myColor, 1, Qt.DashLine))
143            myLine = QLineF(line)
144            myLine.translate(0, 4.0)
145            painter.drawLine(myLine)
146            myLine.translate(0,-8.0)
147            painter.drawLine(myLine)
148
149
150class DiagramTextItem(QGraphicsTextItem):
151    lostFocus = pyqtSignal(QGraphicsTextItem)
152
153    selectedChange = pyqtSignal(QGraphicsItem)
154
155    def __init__(self, parent=None, scene=None):
156        super(DiagramTextItem, self).__init__(parent, scene)
157
158        self.setFlag(QGraphicsItem.ItemIsMovable)
159        self.setFlag(QGraphicsItem.ItemIsSelectable)
160
161    def itemChange(self, change, value):
162        if change == QGraphicsItem.ItemSelectedChange:
163            self.selectedChange.emit(self)
164        return value
165
166    def focusOutEvent(self, event):
167        self.setTextInteractionFlags(Qt.NoTextInteraction)
168        self.lostFocus.emit(self)
169        super(DiagramTextItem, self).focusOutEvent(event)
170
171    def mouseDoubleClickEvent(self, event):
172        if self.textInteractionFlags() == Qt.NoTextInteraction:
173            self.setTextInteractionFlags(Qt.TextEditorInteraction)
174        super(DiagramTextItem, self).mouseDoubleClickEvent(event)
175
176
177class DiagramItem(QGraphicsPolygonItem):
178    Step, Conditional, StartEnd, Io = range(4)
179
180    def __init__(self, diagramType, contextMenu, parent=None):
181        super(DiagramItem, self).__init__(parent)
182
183        self.arrows = []
184
185        self.diagramType = diagramType
186        self.contextMenu = contextMenu
187
188        path = QPainterPath()
189        if self.diagramType == self.StartEnd:
190            path.moveTo(200, 50)
191            path.arcTo(150, 0, 50, 50, 0, 90)
192            path.arcTo(50, 0, 50, 50, 90, 90)
193            path.arcTo(50, 50, 50, 50, 180, 90)
194            path.arcTo(150, 50, 50, 50, 270, 90)
195            path.lineTo(200, 25)
196            self.myPolygon = path.toFillPolygon()
197        elif self.diagramType == self.Conditional:
198            self.myPolygon = QPolygonF([
199                    QPointF(-100, 0), QPointF(0, 100),
200                    QPointF(100, 0), QPointF(0, -100),
201                    QPointF(-100, 0)])
202        elif self.diagramType == self.Step:
203            self.myPolygon = QPolygonF([
204                    QPointF(-100, -100), QPointF(100, -100),
205                    QPointF(100, 100), QPointF(-100, 100),
206                    QPointF(-100, -100)])
207        else:
208            self.myPolygon = QPolygonF([
209                    QPointF(-120, -80), QPointF(-70, 80),
210                    QPointF(120, 80), QPointF(70, -80),
211                    QPointF(-120, -80)])
212
213        self.setPolygon(self.myPolygon)
214        self.setFlag(QGraphicsItem.ItemIsMovable, True)
215        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
216
217    def removeArrow(self, arrow):
218        try:
219            self.arrows.remove(arrow)
220        except ValueError:
221            pass
222
223    def removeArrows(self):
224        for arrow in self.arrows[:]:
225            arrow.startItem().removeArrow(arrow)
226            arrow.endItem().removeArrow(arrow)
227            self.scene().removeItem(arrow)
228
229    def addArrow(self, arrow):
230        self.arrows.append(arrow)
231
232    def image(self):
233        pixmap = QPixmap(250, 250)
234        pixmap.fill(Qt.transparent)
235        painter = QPainter(pixmap)
236        painter.setPen(QPen(Qt.black, 8))
237        painter.translate(125, 125)
238        painter.drawPolyline(self.myPolygon)
239        return pixmap
240
241    def contextMenuEvent(self, event):
242        self.scene().clearSelection()
243        self.setSelected(True)
244        self.myContextMenu.exec_(event.screenPos())
245
246    def itemChange(self, change, value):
247        if change == QGraphicsItem.ItemPositionChange:
248            for arrow in self.arrows:
249                arrow.updatePosition()
250
251        return value
252
253
254class DiagramScene(QGraphicsScene):
255    InsertItem, InsertLine, InsertText, MoveItem  = range(4)
256
257    itemInserted = pyqtSignal(DiagramItem)
258
259    textInserted = pyqtSignal(QGraphicsTextItem)
260
261    itemSelected = pyqtSignal(QGraphicsItem)
262
263    def __init__(self, itemMenu, parent=None):
264        super(DiagramScene, self).__init__(parent)
265
266        self.myItemMenu = itemMenu
267        self.myMode = self.MoveItem
268        self.myItemType = DiagramItem.Step
269        self.line = None
270        self.textItem = None
271        self.myItemColor = Qt.white
272        self.myTextColor = Qt.black
273        self.myLineColor = Qt.black
274        self.myFont = QFont()
275
276    def setLineColor(self, color):
277        self.myLineColor = color
278        if self.isItemChange(Arrow):
279            item = self.selectedItems()[0]
280            item.setColor(self.myLineColor)
281            self.update()
282
283    def setTextColor(self, color):
284        self.myTextColor = color
285        if self.isItemChange(DiagramTextItem):
286            item = self.selectedItems()[0]
287            item.setDefaultTextColor(self.myTextColor)
288
289    def setItemColor(self, color):
290        self.myItemColor = color
291        if self.isItemChange(DiagramItem):
292            item = self.selectedItems()[0]
293            item.setBrush(self.myItemColor)
294
295    def setFont(self, font):
296        self.myFont = font
297        if self.isItemChange(DiagramTextItem):
298            item = self.selectedItems()[0]
299            item.setFont(self.myFont)
300
301    def setMode(self, mode):
302        self.myMode = mode
303
304    def setItemType(self, type):
305        self.myItemType = type
306
307    def editorLostFocus(self, item):
308        cursor = item.textCursor()
309        cursor.clearSelection()
310        item.setTextCursor(cursor)
311
312        if item.toPlainText():
313            self.removeItem(item)
314            item.deleteLater()
315
316    def mousePressEvent(self, mouseEvent):
317        if (mouseEvent.button() != Qt.LeftButton):
318            return
319
320        if self.myMode == self.InsertItem:
321            item = DiagramItem(self.myItemType, self.myItemMenu)
322            item.setBrush(self.myItemColor)
323            self.addItem(item)
324            item.setPos(mouseEvent.scenePos())
325            self.itemInserted.emit(item)
326        elif self.myMode == self.InsertLine:
327            self.line = QGraphicsLineItem(QLineF(mouseEvent.scenePos(),
328                    mouseEvent.scenePos()))
329            self.line.setPen(QPen(self.myLineColor, 2))
330            self.addItem(self.line)
331        elif self.myMode == self.InsertText:
332            textItem = DiagramTextItem()
333            textItem.setFont(self.myFont)
334            textItem.setTextInteractionFlags(Qt.TextEditorInteraction)
335            textItem.setZValue(1000.0)
336            textItem.lostFocus.connect(self.editorLostFocus)
337            textItem.selectedChange.connect(self.itemSelected)
338            self.addItem(textItem)
339            textItem.setDefaultTextColor(self.myTextColor)
340            textItem.setPos(mouseEvent.scenePos())
341            self.textInserted.emit(textItem)
342
343        super(DiagramScene, self).mousePressEvent(mouseEvent)
344
345    def mouseMoveEvent(self, mouseEvent):
346        if self.myMode == self.InsertLine and self.line:
347            newLine = QLineF(self.line.line().p1(), mouseEvent.scenePos())
348            self.line.setLine(newLine)
349        elif self.myMode == self.MoveItem:
350            super(DiagramScene, self).mouseMoveEvent(mouseEvent)
351
352    def mouseReleaseEvent(self, mouseEvent):
353        if self.line and self.myMode == self.InsertLine:
354            startItems = self.items(self.line.line().p1())
355            if len(startItems) and startItems[0] == self.line:
356                startItems.pop(0)
357            endItems = self.items(self.line.line().p2())
358            if len(endItems) and endItems[0] == self.line:
359                endItems.pop(0)
360
361            self.removeItem(self.line)
362            self.line = None
363
364            if len(startItems) and len(endItems) and \
365                    isinstance(startItems[0], DiagramItem) and \
366                    isinstance(endItems[0], DiagramItem) and \
367                    startItems[0] != endItems[0]:
368                startItem = startItems[0]
369                endItem = endItems[0]
370                arrow = Arrow(startItem, endItem)
371                arrow.setColor(self.myLineColor)
372                startItem.addArrow(arrow)
373                endItem.addArrow(arrow)
374                arrow.setZValue(-1000.0)
375                self.addItem(arrow)
376                arrow.updatePosition()
377
378        self.line = None
379        super(DiagramScene, self).mouseReleaseEvent(mouseEvent)
380
381    def isItemChange(self, type):
382        for item in self.selectedItems():
383            if isinstance(item, type):
384                return True
385        return False
386
387
388class MainWindow(QMainWindow):
389    InsertTextButton = 10
390
391    def __init__(self):
392        super(MainWindow, self).__init__()
393
394        self.createActions()
395        self.createMenus()
396        self.createToolBox()
397
398        self.scene = DiagramScene(self.itemMenu)
399        self.scene.setSceneRect(QRectF(0, 0, 5000, 5000))
400        self.scene.itemInserted.connect(self.itemInserted)
401        self.scene.textInserted.connect(self.textInserted)
402        self.scene.itemSelected.connect(self.itemSelected)
403
404        self.createToolbars()
405
406        layout = QHBoxLayout()
407        layout.addWidget(self.toolBox)
408        self.view = QGraphicsView(self.scene)
409        layout.addWidget(self.view)
410
411        self.widget = QWidget()
412        self.widget.setLayout(layout)
413
414        self.setCentralWidget(self.widget)
415        self.setWindowTitle("Diagramscene")
416
417    def backgroundButtonGroupClicked(self, button):
418        buttons = self.backgroundButtonGroup.buttons()
419        for myButton in buttons:
420            if myButton != button:
421                button.setChecked(False)
422
423        text = button.text()
424        if text == "Blue Grid":
425            self.scene.setBackgroundBrush(QBrush(QPixmap(':/images/background1.png')))
426        elif text == "White Grid":
427            self.scene.setBackgroundBrush(QBrush(QPixmap(':/images/background2.png')))
428        elif text == "Gray Grid":
429            self.scene.setBackgroundBrush(QBrush(QPixmap(':/images/background3.png')))
430        else:
431            self.scene.setBackgroundBrush(QBrush(QPixmap(':/images/background4.png')))
432
433        self.scene.update()
434        self.view.update()
435
436    def buttonGroupClicked(self, id):
437        buttons = self.buttonGroup.buttons()
438        for button in buttons:
439            if self.buttonGroup.button(id) != button:
440                button.setChecked(False)
441
442        if id == self.InsertTextButton:
443            self.scene.setMode(DiagramScene.InsertText)
444        else:
445            self.scene.setItemType(id)
446            self.scene.setMode(DiagramScene.InsertItem)
447
448    def deleteItem(self):
449        for item in self.scene.selectedItems():
450            if isinstance(item, DiagramItem):
451                item.removeArrows()
452            self.scene.removeItem(item)
453
454    def pointerGroupClicked(self, i):
455        self.scene.setMode(self.pointerTypeGroup.checkedId())
456
457    def bringToFront(self):
458        if not self.scene.selectedItems():
459            return
460
461        selectedItem = self.scene.selectedItems()[0]
462        overlapItems = selectedItem.collidingItems()
463
464        zValue = 0
465        for item in overlapItems:
466            if (item.zValue() >= zValue and isinstance(item, DiagramItem)):
467                zValue = item.zValue() + 0.1
468        selectedItem.setZValue(zValue)
469
470    def sendToBack(self):
471        if not self.scene.selectedItems():
472            return
473
474        selectedItem = self.scene.selectedItems()[0]
475        overlapItems = selectedItem.collidingItems()
476
477        zValue = 0
478        for item in overlapItems:
479            if (item.zValue() <= zValue and isinstance(item, DiagramItem)):
480                zValue = item.zValue() - 0.1
481        selectedItem.setZValue(zValue)
482
483    def itemInserted(self, item):
484        self.pointerTypeGroup.button(DiagramScene.MoveItem).setChecked(True)
485        self.scene.setMode(self.pointerTypeGroup.checkedId())
486        self.buttonGroup.button(item.diagramType).setChecked(False)
487
488    def textInserted(self, item):
489        self.buttonGroup.button(self.InsertTextButton).setChecked(False)
490        self.scene.setMode(self.pointerTypeGroup.checkedId())
491
492    def currentFontChanged(self, font):
493        self.handleFontChange()
494
495    def fontSizeChanged(self, font):
496        self.handleFontChange()
497
498    def sceneScaleChanged(self, scale):
499        newScale = scale.left(scale.indexOf("%")).toDouble()[0] / 100.0
500        oldMatrix = self.view.matrix()
501        self.view.resetMatrix()
502        self.view.translate(oldMatrix.dx(), oldMatrix.dy())
503        self.view.scale(newScale, newScale)
504
505    def textColorChanged(self):
506        self.textAction = self.sender()
507        self.fontColorToolButton.setIcon(
508                self.createColorToolButtonIcon(':/images/textpointer.png',
509                        QColor(self.textAction.data())))
510        self.textButtonTriggered()
511
512    def itemColorChanged(self):
513        self.fillAction = self.sender()
514        self.fillColorToolButton.setIcon(
515                self.createColorToolButtonIcon( ':/images/floodfill.png',
516                        QColor(self.fillAction.data())))
517        self.fillButtonTriggered()
518
519    def lineColorChanged(self):
520        self.lineAction = self.sender()
521        self.lineColorToolButton.setIcon(
522                self.createColorToolButtonIcon(':/images/linecolor.png',
523                        QColor(self.lineAction.data())))
524        self.lineButtonTriggered()
525
526    def textButtonTriggered(self):
527        self.scene.setTextColor(QColor(self.textAction.data()))
528
529    def fillButtonTriggered(self):
530        self.scene.setItemColor(QColor(self.fillAction.data()))
531
532    def lineButtonTriggered(self):
533        self.scene.setLineColor(QColor(self.lineAction.data()))
534
535    def handleFontChange(self):
536        font = self.fontCombo.currentFont()
537        font.setPointSize(self.fontSizeCombo.currentText().toInt()[0])
538        if self.boldAction.isChecked():
539            font.setWeight(QFont.Bold)
540        else:
541            font.setWeight(QFont.Normal)
542        font.setItalic(self.italicAction.isChecked())
543        font.setUnderline(self.underlineAction.isChecked())
544
545        self.scene.setFont(font)
546
547    def itemSelected(self, item):
548        font = item.font()
549        color = item.defaultTextColor()
550        self.fontCombo.setCurrentFont(font)
551        self.fontSizeCombo.setEditText(str(font.pointSize()))
552        self.boldAction.setChecked(font.weight() == QFont.Bold)
553        self.italicAction.setChecked(font.italic())
554        self.underlineAction.setChecked(font.underline())
555
556    def about(self):
557        QMessageBox.about(self, "About Diagram Scene",
558                "The <b>Diagram Scene</b> example shows use of the graphics framework.")
559
560    def createToolBox(self):
561        self.buttonGroup = QButtonGroup()
562        self.buttonGroup.setExclusive(False)
563        self.buttonGroup.buttonClicked[int].connect(self.buttonGroupClicked)
564
565        layout = QGridLayout()
566        layout.addWidget(self.createCellWidget("Conditional", DiagramItem.Conditional),
567                0, 0)
568        layout.addWidget(self.createCellWidget("Process", DiagramItem.Step), 0,
569                1)
570        layout.addWidget(self.createCellWidget("Input/Output", DiagramItem.Io),
571                1, 0)
572
573        textButton = QToolButton()
574        textButton.setCheckable(True)
575        self.buttonGroup.addButton(textButton, self.InsertTextButton)
576        textButton.setIcon(QIcon(QPixmap(':/images/textpointer.png').scaled(30, 30)))
577        textButton.setIconSize(QSize(50, 50))
578
579        textLayout = QGridLayout()
580        textLayout.addWidget(textButton, 0, 0, Qt.AlignHCenter)
581        textLayout.addWidget(QLabel("Text"), 1, 0, Qt.AlignCenter)
582        textWidget = QWidget()
583        textWidget.setLayout(textLayout)
584        layout.addWidget(textWidget, 1, 1)
585
586        layout.setRowStretch(3, 10)
587        layout.setColumnStretch(2, 10)
588
589        itemWidget = QWidget()
590        itemWidget.setLayout(layout)
591
592        self.backgroundButtonGroup = QButtonGroup()
593        self.backgroundButtonGroup.buttonClicked.connect(self.backgroundButtonGroupClicked)
594
595        backgroundLayout = QGridLayout()
596        backgroundLayout.addWidget(self.createBackgroundCellWidget("Blue Grid",
597                ':/images/background1.png'), 0, 0)
598        backgroundLayout.addWidget(self.createBackgroundCellWidget("White Grid",
599                ':/images/background2.png'), 0, 1)
600        backgroundLayout.addWidget(self.createBackgroundCellWidget("Gray Grid",
601                ':/images/background3.png'), 1, 0)
602        backgroundLayout.addWidget(self.createBackgroundCellWidget("No Grid",
603                ':/images/background4.png'), 1, 1)
604
605        backgroundLayout.setRowStretch(2, 10)
606        backgroundLayout.setColumnStretch(2, 10)
607
608        backgroundWidget = QWidget()
609        backgroundWidget.setLayout(backgroundLayout)
610
611        self.toolBox = QToolBox()
612        self.toolBox.setSizePolicy(QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored))
613        self.toolBox.setMinimumWidth(itemWidget.sizeHint().width())
614        self.toolBox.addItem(itemWidget, "Basic Flowchart Shapes")
615        self.toolBox.addItem(backgroundWidget, "Backgrounds")
616
617    def createActions(self):
618        self.toFrontAction = QAction(
619                QIcon(':/images/bringtofront.png'), "Bring to &Front",
620                self, shortcut="Ctrl+F", statusTip="Bring item to front",
621                triggered=self.bringToFront)
622
623        self.sendBackAction = QAction(
624                QIcon(':/images/sendtoback.png'), "Send to &Back", self,
625                shortcut="Ctrl+B", statusTip="Send item to back",
626                triggered=self.sendToBack)
627
628        self.deleteAction = QAction(QIcon(':/images/delete.png'),
629                "&Delete", self, shortcut="Delete",
630                statusTip="Delete item from diagram",
631                triggered=self.deleteItem)
632
633        self.exitAction = QAction("E&xit", self, shortcut="Ctrl+X",
634                statusTip="Quit Scenediagram example", triggered=self.close)
635
636        self.boldAction = QAction(QIcon(':/images/bold.png'),
637                "Bold", self, checkable=True, shortcut="Ctrl+B",
638                triggered=self.handleFontChange)
639
640        self.italicAction = QAction(QIcon(':/images/italic.png'),
641                "Italic", self, checkable=True, shortcut="Ctrl+I",
642                triggered=self.handleFontChange)
643
644        self.underlineAction = QAction(
645                QIcon(':/images/underline.png'), "Underline", self,
646                checkable=True, shortcut="Ctrl+U",
647                triggered=self.handleFontChange)
648
649        self.aboutAction = QAction("A&bout", self, shortcut="Ctrl+B",
650                triggered=self.about)
651
652    def createMenus(self):
653        self.fileMenu = self.menuBar().addMenu("&File")
654        self.fileMenu.addAction(self.exitAction)
655
656        self.itemMenu = self.menuBar().addMenu("&Item")
657        self.itemMenu.addAction(self.deleteAction)
658        self.itemMenu.addSeparator()
659        self.itemMenu.addAction(self.toFrontAction)
660        self.itemMenu.addAction(self.sendBackAction)
661
662        self.aboutMenu = self.menuBar().addMenu("&Help")
663        self.aboutMenu.addAction(self.aboutAction)
664
665    def createToolbars(self):
666        self.editToolBar = self.addToolBar("Edit")
667        self.editToolBar.addAction(self.deleteAction)
668        self.editToolBar.addAction(self.toFrontAction)
669        self.editToolBar.addAction(self.sendBackAction)
670
671        self.fontCombo = QFontComboBox()
672        self.fontCombo.currentFontChanged.connect(self.currentFontChanged)
673
674        self.fontSizeCombo = QComboBox()
675        self.fontSizeCombo.setEditable(True)
676        for i in range(8, 30, 2):
677            self.fontSizeCombo.addItem(str(i))
678        validator = QIntValidator(2, 64, self)
679        self.fontSizeCombo.setValidator(validator)
680        self.fontSizeCombo.currentIndexChanged.connect(self.fontSizeChanged)
681
682        self.fontColorToolButton = QToolButton()
683        self.fontColorToolButton.setPopupMode(QToolButton.MenuButtonPopup)
684        self.fontColorToolButton.setMenu(
685                self.createColorMenu(self.textColorChanged, Qt.black))
686        self.textAction = self.fontColorToolButton.menu().defaultAction()
687        self.fontColorToolButton.setIcon(
688                self.createColorToolButtonIcon(':/images/textpointer.png',
689                        Qt.black))
690        self.fontColorToolButton.setAutoFillBackground(True)
691        self.fontColorToolButton.clicked.connect(self.textButtonTriggered)
692
693        self.fillColorToolButton = QToolButton()
694        self.fillColorToolButton.setPopupMode(QToolButton.MenuButtonPopup)
695        self.fillColorToolButton.setMenu(
696                self.createColorMenu(self.itemColorChanged, Qt.white))
697        self.fillAction = self.fillColorToolButton.menu().defaultAction()
698        self.fillColorToolButton.setIcon(
699                self.createColorToolButtonIcon(':/images/floodfill.png',
700                        Qt.white))
701        self.fillColorToolButton.clicked.connect(self.fillButtonTriggered)
702
703        self.lineColorToolButton = QToolButton()
704        self.lineColorToolButton.setPopupMode(QToolButton.MenuButtonPopup)
705        self.lineColorToolButton.setMenu(
706                self.createColorMenu(self.lineColorChanged, Qt.black))
707        self.lineAction = self.lineColorToolButton.menu().defaultAction()
708        self.lineColorToolButton.setIcon(
709                self.createColorToolButtonIcon(':/images/linecolor.png',
710                        Qt.black))
711        self.lineColorToolButton.clicked.connect(self.lineButtonTriggered)
712
713        self.textToolBar = self.addToolBar("Font")
714        self.textToolBar.addWidget(self.fontCombo)
715        self.textToolBar.addWidget(self.fontSizeCombo)
716        self.textToolBar.addAction(self.boldAction)
717        self.textToolBar.addAction(self.italicAction)
718        self.textToolBar.addAction(self.underlineAction)
719
720        self.colorToolBar = self.addToolBar("Color")
721        self.colorToolBar.addWidget(self.fontColorToolButton)
722        self.colorToolBar.addWidget(self.fillColorToolButton)
723        self.colorToolBar.addWidget(self.lineColorToolButton)
724
725        pointerButton = QToolButton()
726        pointerButton.setCheckable(True)
727        pointerButton.setChecked(True)
728        pointerButton.setIcon(QIcon(':/images/pointer.png'))
729        linePointerButton = QToolButton()
730        linePointerButton.setCheckable(True)
731        linePointerButton.setIcon(QIcon(':/images/linepointer.png'))
732
733        self.pointerTypeGroup = QButtonGroup()
734        self.pointerTypeGroup.addButton(pointerButton, DiagramScene.MoveItem)
735        self.pointerTypeGroup.addButton(linePointerButton,
736                DiagramScene.InsertLine)
737        self.pointerTypeGroup.buttonClicked[int].connect(self.pointerGroupClicked)
738
739        self.sceneScaleCombo = QComboBox()
740        self.sceneScaleCombo.addItems(["50%", "75%", "100%", "125%", "150%"])
741        self.sceneScaleCombo.setCurrentIndex(2)
742        self.sceneScaleCombo.currentIndexChanged[str].connect(self.sceneScaleChanged)
743
744        self.pointerToolbar = self.addToolBar("Pointer type")
745        self.pointerToolbar.addWidget(pointerButton)
746        self.pointerToolbar.addWidget(linePointerButton)
747        self.pointerToolbar.addWidget(self.sceneScaleCombo)
748
749    def createBackgroundCellWidget(self, text, image):
750        button = QToolButton()
751        button.setText(text)
752        button.setIcon(QIcon(image))
753        button.setIconSize(QSize(50, 50))
754        button.setCheckable(True)
755        self.backgroundButtonGroup.addButton(button)
756
757        layout = QGridLayout()
758        layout.addWidget(button, 0, 0, Qt.AlignHCenter)
759        layout.addWidget(QLabel(text), 1, 0, Qt.AlignCenter)
760
761        widget = QWidget()
762        widget.setLayout(layout)
763
764        return widget
765
766    def createCellWidget(self, text, diagramType):
767        item = DiagramItem(diagramType, self.itemMenu)
768        icon = QIcon(item.image())
769
770        button = QToolButton()
771        button.setIcon(icon)
772        button.setIconSize(QSize(50, 50))
773        button.setCheckable(True)
774        self.buttonGroup.addButton(button, diagramType)
775
776        layout = QGridLayout()
777        layout.addWidget(button, 0, 0, Qt.AlignHCenter)
778        layout.addWidget(QLabel(text), 1, 0, Qt.AlignCenter)
779
780        widget = QWidget()
781        widget.setLayout(layout)
782
783        return widget
784
785    def createColorMenu(self, slot, defaultColor):
786        colors = [Qt.black, Qt.white, Qt.red, Qt.blue, Qt.yellow]
787        names = ["black", "white", "red", "blue", "yellow"]
788
789        colorMenu = QMenu(self)
790        for color, name in zip(colors, names):
791            action = QAction(self.createColorIcon(color), name, self,
792                    triggered=slot)
793            action.setData(QColor(color))
794            colorMenu.addAction(action)
795            if color == defaultColor:
796                colorMenu.setDefaultAction(action)
797        return colorMenu
798
799    def createColorToolButtonIcon(self, imageFile, color):
800        pixmap = QPixmap(50, 80)
801        pixmap.fill(Qt.transparent)
802        painter = QPainter(pixmap)
803        image = QPixmap(imageFile)
804        target = QRect(0, 0, 50, 60)
805        source = QRect(0, 0, 42, 42)
806        painter.fillRect(QRect(0, 60, 50, 80), color)
807        painter.drawPixmap(target, image, source)
808        painter.end()
809
810        return QIcon(pixmap)
811
812    def createColorIcon(self, color):
813        pixmap = QPixmap(20, 20)
814        painter = QPainter(pixmap)
815        painter.setPen(Qt.NoPen)
816        painter.fillRect(QRect(0, 0, 20, 20), color)
817        painter.end()
818
819        return QIcon(pixmap)
820
821
822if __name__ == '__main__':
823
824    import sys
825
826    app = QApplication(sys.argv)
827
828    mainWindow = MainWindow()
829    mainWindow.setGeometry(100, 100, 800, 500)
830    mainWindow.show()
831
832    sys.exit(app.exec_())
833