source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11201:04262f6c3392

Revision 11201:04262f6c3392, 34.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added annotation action menus for font size and arrow color.

Line 
1"""
2Scheme Edit widget.
3
4"""
5import logging
6from operator import attrgetter
7
8from PyQt4.QtGui import (
9    QWidget, QVBoxLayout, QInputDialog, QMenu, QAction, QActionGroup,
10    QKeySequence, QUndoStack, QGraphicsItem, QGraphicsObject,
11    QGraphicsTextItem, QCursor, QFont, QPainter, QPixmap, QColor,
12    QIcon
13)
14
15from PyQt4.QtCore import Qt, QObject, QEvent, QSignalMapper, QRectF
16from PyQt4.QtCore import pyqtProperty as Property, pyqtSignal as Signal
17
18from ..scheme import scheme
19from ..canvas.scene import CanvasScene
20from ..canvas.view import CanvasView
21from ..canvas import items
22from . import interactions
23from . import commands
24from . import quickmenu
25
26
27log = logging.getLogger(__name__)
28
29
30# TODO: Should this be moved to CanvasScene?
31class GraphicsSceneFocusEventListener(QGraphicsObject):
32
33    itemFocusedIn = Signal(QGraphicsItem)
34    itemFocusedOut = Signal(QGraphicsItem)
35
36    def __init__(self, parent=None):
37        QGraphicsObject.__init__(self, parent)
38        self.setFlag(QGraphicsItem.ItemHasNoContents)
39
40    def sceneEventFilter(self, obj, event):
41        print obj, event, event.type()
42        if event.type() == QEvent.FocusIn and \
43                obj.flags() & QGraphicsItem.ItemIsFocusable:
44            obj.focusInEvent(event)
45            if obj.hasFocus():
46                self.itemFocusedIn.emit(obj)
47            return True
48        elif event.type() == QEvent.FocusOut:
49            obj.focusOutEvent(event)
50            if not obj.hasFocus():
51                self.itemFocusedOut.emit(obj)
52            return True
53
54        return QGraphicsObject.sceneEventFilter(self, obj, event)
55
56    def boundingRect(self):
57        return QRectF()
58
59
60class SchemeEditWidget(QWidget):
61    undoAvailable = Signal(bool)
62    redoAvailable = Signal(bool)
63    modificationChanged = Signal(bool)
64    undoCommandAdded = Signal()
65    selectionChanged = Signal()
66
67    titleChanged = Signal(unicode)
68
69    # Quick Menu triggers
70    (NoTriggers,
71     Clicked,
72     DoubleClicked,
73     SpaceKey,
74     AnyKey) = [0, 1, 2, 4, 8]
75
76    def __init__(self, parent=None, ):
77        QWidget.__init__(self, parent)
78
79        self.__modified = False
80        self.__registry = None
81        self.__scheme = None
82        self.__quickMenuTriggers = SchemeEditWidget.SpaceKey | \
83                                   SchemeEditWidget.DoubleClicked
84        self.__emptyClickButtons = 0
85        self.__possibleSelectionHandler = None
86        self.__possibleMouseItemsMove = False
87        self.__itemsMoving = {}
88        self.__contextMenuTarget = None
89        self.__quickMenu = None
90
91        self.__undoStack = QUndoStack(self)
92        self.__undoStack.cleanChanged[bool].connect(self.__onCleanChanged)
93
94        self.__editFinishedMapper = QSignalMapper(self)
95        self.__editFinishedMapper.mapped[QObject].connect(
96            self.__onEditingFinished
97        )
98
99        self.__annotationGeomChanged = QSignalMapper(self)
100
101        self.__setupActions()
102        self.__setupUi()
103
104        self.__linkMenu = QMenu(self)
105        self.__linkMenu.addAction(self.__linkEnableAction)
106        self.__linkMenu.addSeparator()
107        self.__linkMenu.addAction(self.__linkRemoveAction)
108        self.__linkMenu.addAction(self.__linkResetAction)
109
110    def __setupActions(self):
111
112        self.__zoomAction = \
113            QAction(self.tr("Zoom"), self,
114                    objectName="zoom",
115                    checkable=True,
116                    shortcut=QKeySequence.ZoomIn,
117                    toolTip=self.tr("Zoom in the scheme."),
118                    toggled=self.toogleZoom,
119                    )
120
121        self.__cleanUpAction = \
122            QAction(self.tr("Clean Up"), self,
123                    objectName="cleanup",
124                    toolTip=self.tr("Align widget to a grid."),
125                    triggered=self.alignToGrid,
126                    )
127
128        self.__newTextAnnotationAction = \
129            QAction(self.tr("Text"), self,
130                    objectName="new-text-annotation",
131                    toolTip=self.tr("Add a text annotation to the scheme."),
132                    checkable=True,
133                    toggled=self.__toggleNewTextAnnotation,
134                    )
135
136        # Create a font size menu for the new annotation action.
137        self.__fontMenu = QMenu("Font Size", self)
138        self.__fontActionGroup = group = \
139            QActionGroup(self, exclusive=True,
140                         triggered=self.__onFontSizeTriggered)
141
142        def font(size):
143            return QFont("Helvetica", size)
144
145        for size in [12, 14, 16, 18, 20, 22, 24]:
146            action = QAction("%ip" % size, group,
147                             checkable=True,
148                             font=font(size))
149
150            self.__fontMenu.addAction(action)
151
152        group.actions()[2].setChecked(True)
153
154        self.__newTextAnnotationAction.setMenu(self.__fontMenu)
155
156        self.__newArrowAnnotationAction = \
157            QAction(self.tr("Arrow"), self,
158                    objectName="new-arrow-annotation",
159                    toolTip=self.tr("Add a arrow annotation to the scheme."),
160                    checkable=True,
161                    toggled=self.__toggleNewArrowAnnotation,
162                    )
163
164        # Create a color menu for the arrow annotation action
165        self.__arrowColorMenu = QMenu("Arrow Color",)
166        self.__arrowColorActionGroup = group = \
167            QActionGroup(self, exclusive=True,
168                         triggered=self.__onArrowColorTriggered)
169
170        def color_icon(color):
171            icon = QIcon()
172            for size in [16, 24, 32]:
173                pixmap = QPixmap(size, size)
174                pixmap.fill(QColor(0, 0, 0, 0))
175                p = QPainter(pixmap)
176                p.setRenderHint(QPainter.Antialiasing)
177                p.setBrush(color)
178                p.setPen(Qt.NoPen)
179                p.drawEllipse(1, 1, size - 2, size - 2)
180                p.end()
181                icon.addPixmap(pixmap)
182            return icon
183
184        for color in ["#000", "#C1272D", "#662D91", "#1F9CDF", "#39B54A"]:
185            icon = color_icon(QColor(color))
186            action = QAction(group, icon=icon, checkable=True,
187                             iconVisibleInMenu=True)
188            action.setData(color)
189            self.__arrowColorMenu.addAction(action)
190
191        group.actions()[1].setChecked(True)
192
193        self.__newArrowAnnotationAction.setMenu(self.__arrowColorMenu)
194
195        self.__linkEnableAction = \
196            QAction(self.tr("Enabled"), self,
197                    objectName="link-enable-action",
198                    triggered=self.__toggleLinkEnabled,
199                    checkable=True,
200                    )
201
202        self.__linkRemoveAction = \
203            QAction(self.tr("Remove"), self,
204                    objectName="link-remove-action",
205                    triggered=self.__linkRemove,
206                    toolTip=self.tr("Remove link."),
207                    )
208
209        self.__linkResetAction = \
210            QAction(self.tr("Reset Signals"), self,
211                    objectName="link-reset-action",
212                    triggered=self.__linkReset,
213                    )
214
215        self.addActions([self.__newTextAnnotationAction,
216                         self.__newArrowAnnotationAction,
217                         self.__linkEnableAction,
218                         self.__linkRemoveAction,
219                         self.__linkResetAction])
220
221    def __setupUi(self):
222        layout = QVBoxLayout()
223        layout.setContentsMargins(0, 0, 0, 0)
224        layout.setSpacing(0)
225
226        scene = CanvasScene()
227        view = CanvasView(scene)
228        view.setFrameStyle(CanvasView.NoFrame)
229        view.setRenderHint(QPainter.Antialiasing)
230        view.setContextMenuPolicy(Qt.CustomContextMenu)
231        view.customContextMenuRequested.connect(
232            self.__onCustomContextMenuRequested
233        )
234
235        self.__view = view
236        self.__scene = scene
237
238        self.__focusListener = GraphicsSceneFocusEventListener()
239        self.__focusListener.itemFocusedIn.connect(self.__onItemFocusedIn)
240        self.__focusListener.itemFocusedOut.connect(self.__onItemFocusedOut)
241        self.__scene.addItem(self.__focusListener)
242
243        self.__scene.selectionChanged.connect(
244            self.__onSelectionChanged
245        )
246
247        layout.addWidget(view)
248        self.setLayout(layout)
249
250    def toolbarActions(self):
251        """Return a list of actions that can be inserted into a toolbar.
252        """
253        return [self.__zoomAction,
254                self.__cleanUpAction,
255                self.__newTextAnnotationAction,
256                self.__newArrowAnnotationAction]
257
258    def isModified(self):
259        return not self.__undoStack.isClean()
260
261    def setModified(self, modified):
262        if modified and not self.isModified():
263            raise NotImplementedError
264        else:
265            self.__undoStack.setClean()
266
267    modified = Property(bool, fget=isModified, fset=setModified)
268
269    def setQuickMenuTriggers(self, triggers):
270        """Set quick menu triggers.
271        """
272        if self.__quickMenuTriggers != triggers:
273            self.__quickMenuTriggers = triggers
274
275    def quickMenuTriggres(self):
276        return self.__quickMenuTriggers
277
278    def undoStack(self):
279        """Return the undo stack.
280        """
281        return self.__undoStack
282
283    def setScheme(self, scheme):
284        if self.__scheme is not scheme:
285            if self.__scheme:
286                self.__scheme.title_changed.disconnect(self.titleChanged)
287                self.__scheme.node_added.disconnect(self.__onNodeAdded)
288                self.__scheme.node_removed.disconnect(self.__onNodeRemoved)
289
290            self.__scheme = scheme
291
292            if self.__scheme:
293                self.__scheme.title_changed.connect(self.titleChanged)
294                self.__scheme.node_added.connect(self.__onNodeAdded)
295                self.__scheme.node_removed.connect(self.__onNodeRemoved)
296                self.titleChanged.emit(scheme.title)
297
298            self.__annotationGeomChanged.deleteLater()
299            self.__annotationGeomChanged = QSignalMapper(self)
300
301            self.__undoStack.clear()
302
303            self.__focusListener.itemFocusedIn.disconnect(
304                self.__onItemFocusedIn
305            )
306            self.__focusListener.itemFocusedOut.disconnect(
307                self.__onItemFocusedOut
308            )
309
310            self.__scene.selectionChanged.disconnect(
311                self.__onSelectionChanged
312            )
313
314            self.__scene.clear()
315            self.__scene.removeEventFilter(self)
316            self.__scene.deleteLater()
317
318            self.__scene = CanvasScene()
319            self.__view.setScene(self.__scene)
320            self.__scene.installEventFilter(self)
321
322            self.__scene.set_registry(self.__registry)
323
324            # Focus listener
325            self.__focusListener = GraphicsSceneFocusEventListener()
326            self.__focusListener.itemFocusedIn.connect(
327                self.__onItemFocusedIn
328            )
329            self.__focusListener.itemFocusedOut.connect(
330                self.__onItemFocusedOut
331            )
332            self.__scene.addItem(self.__focusListener)
333
334            self.__scene.selectionChanged.connect(
335                self.__onSelectionChanged
336            )
337
338            self.__scene.node_item_activated.connect(
339                self.__onNodeActivate
340            )
341
342            self.__scene.annotation_added.connect(
343                self.__onAnnotationAdded
344            )
345
346            self.__scene.annotation_removed.connect(
347                self.__onAnnotationRemoved
348            )
349
350            self.__scene.set_scheme(scheme)
351
352    def scheme(self):
353        return self.__scheme
354
355    def scene(self):
356        return self.__scene
357
358    def view(self):
359        return self.__view
360
361    def setRegistry(self, registry):
362        # Is this method necessary
363        self.__registry = registry
364        if self.__scene:
365            self.__scene.set_registry(registry)
366            self.__quickMenu = None
367
368    def quickMenu(self):
369        """Return a quick menu instance for quick new node creation.
370        """
371        if self.__quickMenu is None:
372            menu = quickmenu.QuickMenu(self)
373            if self.__registry is not None:
374                menu.setModel(self.__registry.model())
375            self.__quickMenu = menu
376        return self.__quickMenu
377
378    def addNode(self, node):
379        """Add a new node to the scheme.
380        """
381        command = commands.AddNodeCommand(self.__scheme, node)
382        self.__undoStack.push(command)
383
384    def createNewNode(self, description):
385        """Create a new SchemeNode add at it to the document at left of the
386        last added node.
387
388        """
389        node = scheme.SchemeNode(description)
390
391        if self.scheme().nodes:
392            x, y = self.scheme().nodes[-1].position
393            node.position = (x + 150, y)
394        else:
395            node.position = (150, 150)
396
397        self.addNode(node)
398
399    def removeNode(self, node):
400        command = commands.RemoveNodeCommand(self.__scheme, node)
401        self.__undoStack.push(command)
402
403    def renameNode(self, node, title):
404        command = commands.RenameNodeCommand(self.__scheme, node, title)
405        self.__undoStack.push(command)
406
407    def addLink(self, link):
408        command = commands.AddLinkCommand(self.__scheme, link)
409        self.__undoStack.push(command)
410
411    def removeLink(self, link):
412        command = commands.RemoveLinkCommand(self.__scheme, link)
413        self.__undoStack.push(command)
414
415    def addAnnotation(self, annotation):
416        command = commands.AddAnnotationCommand(self.__scheme, annotation)
417        self.__undoStack.push(command)
418
419    def removeAnnotation(self, annotation):
420        command = commands.RemoveAnnotationCommand(self.__scheme, annotation)
421        self.__undoStack.push(command)
422
423    def removeSelected(self):
424        selected = self.scene().selectedItems()
425        if not selected:
426            return
427
428        self.__undoStack.beginMacro(self.tr("Remove"))
429        for item in selected:
430            print item
431            if isinstance(item, items.NodeItem):
432                node = self.scene().node_for_item(item)
433                self.__undoStack.push(
434                    commands.RemoveNodeCommand(self.__scheme, node)
435                )
436            elif isinstance(item, items.annotationitem.Annotation):
437                annot = self.scene().annotation_for_item(item)
438                self.__undoStack.push(
439                    commands.RemoveAnnotationCommand(self.__scheme, annot)
440                )
441        self.__undoStack.endMacro()
442
443    def selectAll(self):
444        for item in self.__scene.items():
445            if item.flags() & QGraphicsItem.ItemIsSelectable:
446                item.setSelected(True)
447
448    def toogleZoom(self, zoom):
449        view = self.view()
450        if zoom:
451            view.scale(1.5, 1.5)
452        else:
453            view.resetTransform()
454
455    def alignToGrid(self):
456        """Align nodes to a grid.
457        """
458        tile_size = 150
459        tiles = {}
460
461        nodes = sorted(self.scheme().nodes, key=attrgetter("position"))
462
463        if nodes:
464            self.__undoStack.beginMacro(self.tr("Align To Grid"))
465
466            for node in nodes:
467                x, y = node.position
468                x = int(round(float(x) / tile_size) * tile_size)
469                y = int(round(float(y) / tile_size) * tile_size)
470                while (x, y) in tiles:
471                    x += tile_size
472
473                self.__undoStack.push(
474                    commands.MoveNodeCommand(self.scheme(), node,
475                                             node.position, (x, y))
476                )
477
478                tiles[x, y] = node
479                self.__scene.item_for_node(node).setPos(x, y)
480
481            self.__undoStack.endMacro()
482
483    def selectedNodes(self):
484        return map(self.scene().node_for_item,
485                   self.scene().selected_node_items())
486
487    def openSelected(self):
488        selected = self.scene().selected_node_items()
489        for item in selected:
490            self.__onNodeActivate(item)
491
492    def editNodeTitle(self, node):
493        name, ok = QInputDialog.getText(
494                    self, self.tr("Rename"),
495                    unicode(self.tr("Enter a new name for the %r widget")) \
496                    % node.title,
497                    text=node.title
498                    )
499
500        if ok:
501            self.__undoStack.push(
502                commands.RenameNodeCommand(self.__scheme, node, node.title,
503                                           unicode(name))
504            )
505
506    def __onCleanChanged(self, clean):
507        if self.isWindowModified() != (not clean):
508            self.setWindowModified(not clean)
509            self.modificationChanged.emit(not clean)
510
511    def eventFilter(self, obj, event):
512        # Filter the scene's drag/drop events.
513        if obj is self.scene():
514            etype = event.type()
515            if  etype == QEvent.GraphicsSceneDragEnter or \
516                    etype == QEvent.GraphicsSceneDragMove:
517                mime_data = event.mimeData()
518                if mime_data.hasFormat(
519                        "application/vnv.orange-canvas.registry.qualified-name"
520                        ):
521                    event.acceptProposedAction()
522                return True
523            elif etype == QEvent.GraphicsSceneDrop:
524                data = event.mimeData()
525                qname = data.data(
526                    "application/vnv.orange-canvas.registry.qualified-name"
527                )
528                desc = self.__registry.widget(unicode(qname))
529                pos = event.scenePos()
530                node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
531                self.addNode(node)
532                return True
533
534            elif etype == QEvent.GraphicsSceneMousePress:
535                return self.sceneMousePressEvent(event)
536            elif etype == QEvent.GraphicsSceneMouseMove:
537                return self.sceneMouseMoveEvent(event)
538            elif etype == QEvent.GraphicsSceneMouseRelease:
539                return self.sceneMouseReleaseEvent(event)
540            elif etype == QEvent.GraphicsSceneMouseDoubleClick:
541                return self.sceneMouseDoubleClickEvent(event)
542            elif etype == QEvent.KeyRelease:
543                return self.sceneKeyPressEvent(event)
544            elif etype == QEvent.KeyRelease:
545                return self.sceneKeyReleaseEvent(event)
546            elif etype == QEvent.GraphicsSceneContextMenu:
547                return self.sceneContextMenuEvent(event)
548
549        return QWidget.eventFilter(self, obj, event)
550
551    def sceneMousePressEvent(self, event):
552        scene = self.__scene
553        if scene.user_interaction_handler:
554            return False
555
556        pos = event.scenePos()
557
558        anchor_item = scene.item_at(pos, items.NodeAnchorItem)
559        if anchor_item and event.button() == Qt.LeftButton:
560            # Start a new link starting at item
561            handler = interactions.NewLinkAction(self)
562            scene.set_user_interaction_handler(handler)
563            return handler.mousePressEvent(event)
564
565        any_item = scene.item_at(pos)
566        if not any_item and event.button() == Qt.LeftButton:
567            self.__emptyClickButtons |= Qt.LeftButton
568            # Create a RectangleSelectionAction but do not set in on the scene
569            # just yet (instead wait for the mouse move event).
570            handler = interactions.RectangleSelectionAction(self)
571            rval = handler.mousePressEvent(event)
572            if rval == True:
573                self.__possibleSelectionHandler = handler
574            return False
575
576        if any_item and event.button() == Qt.LeftButton:
577            self.__possibleMouseItemsMove = True
578            self.__itemsMoving.clear()
579            self.__scene.node_item_position_changed.connect(
580                self.__onNodePositionChanged
581            )
582            self.__annotationGeomChanged.mapped[QObject].connect(
583                self.__onAnnotationGeometryChanged
584            )
585
586        return False
587
588    def sceneMouseMoveEvent(self, event):
589        scene = self.__scene
590        if scene.user_interaction_handler:
591            return False
592
593        if self.__emptyClickButtons & Qt.LeftButton and \
594                event.buttons() & Qt.LeftButton and \
595                self.__possibleSelectionHandler:
596            # Set the RectangleSelection (initialized in mousePressEvent)
597            # on the scene
598            handler = self.__possibleSelectionHandler
599            scene.set_user_interaction_handler(handler)
600            return handler.mouseMoveEvent(event)
601
602        return False
603
604    def sceneMouseReleaseEvent(self, event):
605        if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
606            self.__possibleMouseItemsMove = False
607            self.__scene.node_item_position_changed.disconnect(
608                self.__onNodePositionChanged
609            )
610            self.__annotationGeomChanged.mapped[QObject].disconnect(
611                self.__onAnnotationGeometryChanged
612            )
613
614            if self.__itemsMoving:
615                self.__scene.mouseReleaseEvent(event)
616                stack = self.undoStack()
617                stack.beginMacro(self.tr("Move"))
618                for scheme_item, (old, new) in self.__itemsMoving.items():
619                    if isinstance(scheme_item, scheme.SchemeNode):
620                        command = commands.MoveNodeCommand(
621                            self.scheme(), scheme_item, old, new
622                        )
623                    elif isinstance(scheme_item, scheme.BaseSchemeAnnotation):
624                        command = commands.AnnotationGeometryChange(
625                            self.scheme(), scheme_item, old, new
626                        )
627                    else:
628                        continue
629
630                    stack.push(command)
631                stack.endMacro()
632
633                self.__itemsMoving.clear()
634                return True
635
636        if self.__emptyClickButtons & Qt.LeftButton and \
637                event.button() & Qt.LeftButton:
638            self.__emptyClickButtons &= ~Qt.LeftButton
639
640            if self.__quickMenuTriggers & SchemeEditWidget.Clicked and \
641                    mouse_drag_distance(event, Qt.LeftButton) < 1:
642                action = interactions.NewNodeAction(self)
643                action.create_new(event.screenPos())
644                event.accept()
645                return True
646
647        return False
648
649    def sceneMouseDoubleClickEvent(self, event):
650        scene = self.__scene
651        if scene.user_interaction_handler:
652            return False
653
654        item = scene.item_at(event.scenePos())
655        if not item and self.__quickMenuTriggers & \
656                SchemeEditWidget.DoubleClicked:
657            # Double click on an empty spot
658            # Create a new node quick
659            action = interactions.NewNodeAction(self)
660            action.create_new(event.screenPos())
661            event.accept()
662            return True
663
664        item = scene.item_at(event.scenePos(), items.LinkItem)
665        if item is not None:
666            link = self.scene().link_for_item(item)
667            action = interactions.EditNodeLinksAction(self, link.source_node,
668                                                      link.sink_node)
669            action.edit_links()
670            event.accept()
671            return True
672
673        return False
674
675    def sceneKeyPressEvent(self, event):
676        scene = self.__scene
677        if scene.user_interaction_handler:
678            return False
679
680        # If a QGraphicsItem is in text editing mode, don't interrupt it
681        focusItem = scene.focusItem()
682        if focusItem and isinstance(focusItem, QGraphicsTextItem) and \
683                focusItem.textInteractionFlags() & Qt.TextEditable:
684            return False
685
686        # If the mouse is not over out view
687        if not self.view().underMouse():
688            return False
689
690        if (event.key() == Qt.Key_Space and \
691                self.__quickMenuTriggers & SchemeEditWidget.SpaceKey):
692            action = interactions.NewNodeAction(self)
693            action.create_new(QCursor.pos())
694            event.accept()
695            return True
696
697        if len(event.text()) and \
698                self.__quickMenuTriggers & SchemeEditWidget.AnyKey:
699            action = interactions.NewNodeAction(self)
700            # TODO: set the search text to event.text() and set focus on the
701            # search line
702            action.create_new(QCursor.pos())
703            event.accept()
704            return True
705
706        return False
707
708    def sceneKeyReleaseEvent(self, event):
709        return False
710
711    def sceneContextMenuEvent(self, event):
712        return False
713
714    def __onSelectionChanged(self):
715        pass
716
717    def __onNodeAdded(self, node):
718        widget = self.__scheme.widget_for_node[node]
719        widget.widgetStateChanged.connect(self.__onWidgetStateChanged)
720
721    def __onNodeRemoved(self, node):
722        widget = self.__scheme.widget_for_node[node]
723        widget.widgetStateChanged.disconnect(self.__onWidgetStateChanged)
724
725    def __onWidgetStateChanged(self, *args):
726        widget = self.sender()
727        self.scheme()
728        widget_to_node = dict(reversed(item) for item in \
729                              self.__scheme.widget_for_node.items())
730        node = widget_to_node[widget]
731        item = self.__scene.item_for_node(node)
732
733        info = widget.widgetStateToHtml(True, False, False)
734        warning = widget.widgetStateToHtml(False, True, False)
735        error = widget.widgetStateToHtml(False, False, True)
736
737        item.setInfoMessage(info or None)
738        item.setWarningMessage(warning or None)
739        item.setErrorMessage(error or None)
740
741    def __onNodeActivate(self, item):
742        node = self.__scene.node_for_item(item)
743        widget = self.scheme().widget_for_node[node]
744        widget.show()
745        widget.raise_()
746
747    def __onNodePositionChanged(self, item, pos):
748        node = self.__scene.node_for_item(item)
749        new = (pos.x(), pos.y())
750        if node not in self.__itemsMoving:
751            self.__itemsMoving[node] = (node.position, new)
752        else:
753            old, _ = self.__itemsMoving[node]
754            self.__itemsMoving[node] = (old, new)
755
756    def __onAnnotationGeometryChanged(self, item):
757        annot = self.scene().annotation_for_item(item)
758        if annot not in self.__itemsMoving:
759            self.__itemsMoving[annot] = (annot.geometry,
760                                         geometry_from_annotation_item(item))
761        else:
762            old, _ = self.__itemsMoving[annot]
763            self.__itemsMoving[annot] = (old,
764                                         geometry_from_annotation_item(item))
765
766    def __onAnnotationAdded(self, item):
767        log.debug("Annotation added (%r)", item)
768        item.setFlag(QGraphicsItem.ItemIsSelectable)
769        item.setFlag(QGraphicsItem.ItemIsMovable)
770        item.setFlag(QGraphicsItem.ItemIsFocusable)
771
772        item.installSceneEventFilter(self.__focusListener)
773
774        if isinstance(item, items.ArrowAnnotation):
775            pass
776        elif isinstance(item, items.TextAnnotation):
777            # Make the annotation editable.
778            item.setTextInteractionFlags(Qt.TextEditorInteraction)
779
780            self.__editFinishedMapper.setMapping(item, item)
781            item.editingFinished.connect(
782                self.__editFinishedMapper.map
783            )
784
785        self.__annotationGeomChanged.setMapping(item, item)
786        item.geometryChanged.connect(
787            self.__annotationGeomChanged.map
788        )
789
790    def __onAnnotationRemoved(self, item):
791        log.debug("Annotation removed (%r)", item)
792        if isinstance(item, items.ArrowAnnotation):
793            pass
794        elif isinstance(item, items.TextAnnotation):
795            item.editingFinished.disconnect(
796                self.__editFinishedMapper.map
797            )
798
799        item.removeSceneEventFilter(self.__focusListener)
800
801        self.__annotationGeomChanged.removeMappings(item)
802        item.geometryChanged.disconnect(
803            self.__annotationGeomChanged.map
804        )
805
806    def __onItemFocusedIn(self, item):
807        """Annotation item has gained focus.
808        """
809        if not self.__scene.user_interaction_handler:
810            self.__startControlPointEdit(item)
811
812    def __onItemFocusedOut(self, item):
813        """Annotation item lost focus.
814        """
815        self.__endControlPointEdit()
816
817    def __onEditingFinished(self, item):
818        """Text annotation editing has finished.
819        """
820        annot = self.__scene.annotation_for_item(item)
821        text = unicode(item.toPlainText())
822        if annot.text != text:
823            self.__undoStack.push(
824                commands.TextChangeCommand(self.scheme(), annot,
825                                           annot.text, text)
826            )
827
828    def __toggleNewArrowAnnotation(self, checked):
829        if self.__newTextAnnotationAction.isChecked():
830            self.__newTextAnnotationAction.setChecked(not checked)
831
832        action = self.__newArrowAnnotationAction
833
834        if not checked:
835            handler = self.__scene.user_interaction_handler
836            if isinstance(handler, interactions.NewArrowAnnotation):
837                # Cancel the interaction and restore the state
838                handler.ended.disconnect(action.toggle)
839                handler.cancel(interactions.UserInteraction.UserCancelReason)
840                log.info("Canceled new arrow annotation")
841
842        else:
843            handler = interactions.NewArrowAnnotation(self)
844            checked = self.__arrowColorActionGroup.checkedAction()
845            handler.setColor(checked.data().toPyObject())
846
847            handler.ended.connect(action.toggle)
848
849            self.__scene.set_user_interaction_handler(handler)
850
851    def __onFontSizeTriggered(self, action):
852        if not self.__newTextAnnotationAction.isChecked():
853            # Trigger the action
854            self.__newTextAnnotationAction.trigger()
855        else:
856            # just update the preferred font on the interaction handler
857            handler = self.__scene.user_interaction_handler
858            if isinstance(handler, interactions.NewTextAnnotation):
859                handler.setFont(action.font())
860
861    def __toggleNewTextAnnotation(self, checked):
862        if self.__newArrowAnnotationAction.isChecked():
863            self.__newArrowAnnotationAction.setChecked(not checked)
864
865        action = self.__newTextAnnotationAction
866
867        if not checked:
868            handler = self.__scene.user_interaction_handler
869            if isinstance(handler, interactions.NewTextAnnotation):
870                # cancel the interaction and restore the state
871                handler.ended.disconnect(action.toggle)
872                handler.cancel(interactions.UserInteraction.UserCancelReason)
873                log.info("Canceled new text annotation")
874
875        else:
876            handler = interactions.NewTextAnnotation(self)
877            checked = self.__fontActionGroup.checkedAction()
878            handler.setFont(checked.font())
879
880            handler.ended.connect(action.toggle)
881
882            self.__scene.set_user_interaction_handler(handler)
883
884    def __onArrowColorTriggered(self, action):
885        if not self.__newArrowAnnotationAction.isChecked():
886            # Trigger the action
887            self.__newArrowAnnotationAction.trigger()
888        else:
889            # just update the preferred color on the interaction handler
890            handler = self.__scene.user_interaction_handler
891            if isinstance(handler, interactions.NewArrowAnnotation):
892                handler.setColor(action.data().toPyObject())
893
894    def __onCustomContextMenuRequested(self, pos):
895        scenePos = self.view().mapToScene(pos)
896        globalPos = self.view().mapToGlobal(pos)
897
898        item = self.scene().item_at(scenePos, items.NodeItem)
899        if item is not None:
900            self.window().widget_menu.popup(globalPos)
901            return
902
903        item = self.scene().item_at(scenePos, items.LinkItem)
904        if item is not None:
905            link = self.scene().link_for_item(item)
906            self.__linkEnableAction.setChecked(link.enabled)
907            self.__contextMenuTarget = link
908            self.__linkMenu.popup(globalPos)
909            return
910
911    def __toggleLinkEnabled(self, enabled):
912        """Link enabled state was toggled in the context menu.
913        """
914        if self.__contextMenuTarget:
915            link = self.__contextMenuTarget
916            command = commands.SetAttrCommand(
917                link, "enabled", enabled, name=self.tr("Set enabled"),
918            )
919            self.__undoStack.push(command)
920
921    def __linkRemove(self):
922        """Remove link was requested from the context menu.
923        """
924        if self.__contextMenuTarget:
925            self.removeLink(self.__contextMenuTarget)
926
927    def __linkReset(self):
928        """Link reset from the context menu was requested.
929        """
930        if self.__contextMenuTarget:
931            link = self.__contextMenuTarget
932            action = interactions.EditNodeLinksAction(
933                self, link.source_node, link.sink_node
934            )
935            action.edit_links()
936
937    def __startControlPointEdit(self, item):
938        """Start a control point edit interaction for item.
939        """
940        if isinstance(item, items.ArrowAnnotation):
941            handler = interactions.ResizeArrowAnnotation(self)
942        elif isinstance(item, items.TextAnnotation):
943            handler = interactions.ResizeTextAnnotation(self)
944        else:
945            log.warning("Unknown annotation item type %r" % item)
946            return
947
948        handler.editItem(item)
949        self.__scene.set_user_interaction_handler(handler)
950
951        log.info("Control point editing started (%r)." % item)
952
953    def __endControlPointEdit(self):
954        """End the current control point edit interaction.
955        """
956        handler = self.__scene.user_interaction_handler
957        if isinstance(handler, (interactions.ResizeArrowAnnotation,
958                                interactions.ResizeTextAnnotation)) and \
959                not handler.isFinished() and not handler.isCanceled():
960            handler.commit()
961            handler.end()
962
963            log.info("Control point editing finished.")
964
965
966def geometry_from_annotation_item(item):
967    if isinstance(item, items.ArrowAnnotation):
968        line = item.line()
969        p1 = item.mapToScene(line.p1())
970        p2 = item.mapToScene(line.p2())
971        return ((p1.x(), p1.y()), (p2.x(), p2.y()))
972    elif isinstance(item, items.TextAnnotation):
973        geom = item.geometry()
974        return (geom.x(), geom.y(), geom.width(), geom.height())
975
976
977def mouse_drag_distance(event, button=Qt.LeftButton):
978    """Return the (manhattan) distance between the (screen position)
979    when the `button` was pressed and the current mouse position.
980
981    """
982    diff = (event.buttonDownScreenPos(button) - event.screenPos())
983    return diff.manhattanLength()
Note: See TracBrowser for help on using the repository browser.