source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11219:d24b63d1c4db

Revision 11219:d24b63d1c4db, 39.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Removed 'path' property from 'Scheme' class and added it to SchemeEditWidget.

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