source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11198:62976c7440b4

Revision 11198:62976c7440b4, 31.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added quick menu trigger flags (default is double click and space key).

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