source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11221:13909de3464e

Revision 11221:13909de3464e, 40.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Fixed removal of selected annotations.

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 the :class:`Scheme` edited by the widget.
454        """
455        return self.__scheme
456
457    def scene(self):
458        """Return the QGraphicsScene instance used to display the scheme.
459        """
460        return self.__scene
461
462    def view(self):
463        """Return the QGraphicsView instance used to display the scene.
464        """
465        return self.__view
466
467    def setRegistry(self, registry):
468        # Is this method necessary
469        self.__registry = registry
470        if self.__scene:
471            self.__scene.set_registry(registry)
472            self.__quickMenu = None
473
474    def quickMenu(self):
475        """Return a quick menu instance for quick new node creation.
476        """
477        if self.__quickMenu is None:
478            menu = quickmenu.QuickMenu(self)
479            if self.__registry is not None:
480                menu.setModel(self.__registry.model())
481            self.__quickMenu = menu
482        return self.__quickMenu
483
484    def addNode(self, node):
485        """Add a new node to the scheme.
486        """
487        command = commands.AddNodeCommand(self.__scheme, node)
488        self.__undoStack.push(command)
489
490    def createNewNode(self, description):
491        """Create a new `SchemeNode` and add it to the document at left of the
492        last added node.
493
494        """
495        node = scheme.SchemeNode(description)
496
497        if self.scheme().nodes:
498            x, y = self.scheme().nodes[-1].position
499            node.position = (x + 150, y)
500        else:
501            node.position = (150, 150)
502
503        self.addNode(node)
504
505    def removeNode(self, node):
506        """Remove a `node` (:class:`SchemeNode`) from the scheme
507        """
508        command = commands.RemoveNodeCommand(self.__scheme, node)
509        self.__undoStack.push(command)
510
511    def renameNode(self, node, title):
512        """Rename a `node` (:class:`SchemeNode`) to `title`.
513        """
514        command = commands.RenameNodeCommand(self.__scheme, node, title)
515        self.__undoStack.push(command)
516
517    def addLink(self, link):
518        """Add a `link` (:class:`SchemeLink`) to the scheme.
519        """
520        command = commands.AddLinkCommand(self.__scheme, link)
521        self.__undoStack.push(command)
522
523    def removeLink(self, link):
524        """Remove a link (:class:`SchemeLink`) from the scheme.
525        """
526        command = commands.RemoveLinkCommand(self.__scheme, link)
527        self.__undoStack.push(command)
528
529    def addAnnotation(self, annotation):
530        """Add `annotation` (:class:`BaseSchemeAnnotation`) to the scheme
531        """
532        command = commands.AddAnnotationCommand(self.__scheme, annotation)
533        self.__undoStack.push(command)
534
535    def removeAnnotation(self, annotation):
536        """Remove `annotation` (:class:`BaseSchemeAnnotation`) from the scheme.
537        """
538        command = commands.RemoveAnnotationCommand(self.__scheme, annotation)
539        self.__undoStack.push(command)
540
541    def removeSelected(self):
542        """Remove all selected items in the scheme.
543        """
544        selected = self.scene().selectedItems()
545        if not selected:
546            return
547
548        self.__undoStack.beginMacro(self.tr("Remove"))
549        for item in selected:
550            if isinstance(item, items.NodeItem):
551                node = self.scene().node_for_item(item)
552                self.__undoStack.push(
553                    commands.RemoveNodeCommand(self.__scheme, node)
554                )
555            elif isinstance(item, items.annotationitem.Annotation):
556                annot = self.scene().annotation_for_item(item)
557                self.__undoStack.push(
558                    commands.RemoveAnnotationCommand(self.__scheme, annot)
559                )
560        self.__undoStack.endMacro()
561
562    def selectAll(self):
563        """Select all selectable items in the scheme.
564        """
565        for item in self.__scene.items():
566            if item.flags() & QGraphicsItem.ItemIsSelectable:
567                item.setSelected(True)
568
569    def toogleZoom(self, zoom):
570        view = self.view()
571        if zoom:
572            view.scale(1.5, 1.5)
573        else:
574            view.resetTransform()
575
576    def alignToGrid(self):
577        """Align nodes to a grid.
578        """
579        tile_size = 150
580        tiles = {}
581
582        nodes = sorted(self.scheme().nodes, key=attrgetter("position"))
583
584        if nodes:
585            self.__undoStack.beginMacro(self.tr("Align To Grid"))
586
587            for node in nodes:
588                x, y = node.position
589                x = int(round(float(x) / tile_size) * tile_size)
590                y = int(round(float(y) / tile_size) * tile_size)
591                while (x, y) in tiles:
592                    x += tile_size
593
594                self.__undoStack.push(
595                    commands.MoveNodeCommand(self.scheme(), node,
596                                             node.position, (x, y))
597                )
598
599                tiles[x, y] = node
600                self.__scene.item_for_node(node).setPos(x, y)
601
602            self.__undoStack.endMacro()
603
604    def selectedNodes(self):
605        """Return all selected `SchemeNode` items.
606        """
607        return map(self.scene().node_for_item,
608                   self.scene().selected_node_items())
609
610    def selectedAnnotations(self):
611        """Return all selected `SchemeAnnotation` items.
612        """
613        return map(self.scene().annotation_for_item,
614                   self.scene().selected_annotation_items())
615
616    def openSelected(self):
617        """Open (show and raise) all widgets for selected nodes.
618        """
619        selected = self.scene().selected_node_items()
620        for item in selected:
621            self.__onNodeActivate(item)
622
623    def editNodeTitle(self, node):
624        """Edit the `node`'s title.
625        """
626        name, ok = QInputDialog.getText(
627                    self, self.tr("Rename"),
628                    unicode(self.tr("Enter a new name for the %r widget")) \
629                    % node.title,
630                    text=node.title
631                    )
632
633        if ok:
634            self.__undoStack.push(
635                commands.RenameNodeCommand(self.__scheme, node, node.title,
636                                           unicode(name))
637            )
638
639    def __onCleanChanged(self, clean):
640        if self.isWindowModified() != (not clean):
641            self.setWindowModified(not clean)
642            self.modificationChanged.emit(not clean)
643
644    def eventFilter(self, obj, event):
645        # Filter the scene's drag/drop events.
646        if obj is self.scene():
647            etype = event.type()
648            if  etype == QEvent.GraphicsSceneDragEnter or \
649                    etype == QEvent.GraphicsSceneDragMove:
650                mime_data = event.mimeData()
651                if mime_data.hasFormat(
652                        "application/vnv.orange-canvas.registry.qualified-name"
653                        ):
654                    event.acceptProposedAction()
655                return True
656            elif etype == QEvent.GraphicsSceneDrop:
657                data = event.mimeData()
658                qname = data.data(
659                    "application/vnv.orange-canvas.registry.qualified-name"
660                )
661                desc = self.__registry.widget(unicode(qname))
662                pos = event.scenePos()
663                node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
664                self.addNode(node)
665                return True
666
667            elif etype == QEvent.GraphicsSceneMousePress:
668                return self.sceneMousePressEvent(event)
669            elif etype == QEvent.GraphicsSceneMouseMove:
670                return self.sceneMouseMoveEvent(event)
671            elif etype == QEvent.GraphicsSceneMouseRelease:
672                return self.sceneMouseReleaseEvent(event)
673            elif etype == QEvent.GraphicsSceneMouseDoubleClick:
674                return self.sceneMouseDoubleClickEvent(event)
675            elif etype == QEvent.KeyRelease:
676                return self.sceneKeyPressEvent(event)
677            elif etype == QEvent.KeyRelease:
678                return self.sceneKeyReleaseEvent(event)
679            elif etype == QEvent.GraphicsSceneContextMenu:
680                return self.sceneContextMenuEvent(event)
681
682        return QWidget.eventFilter(self, obj, event)
683
684    def sceneMousePressEvent(self, event):
685        scene = self.__scene
686        if scene.user_interaction_handler:
687            return False
688
689        pos = event.scenePos()
690
691        anchor_item = scene.item_at(pos, items.NodeAnchorItem)
692        if anchor_item and event.button() == Qt.LeftButton:
693            # Start a new link starting at item
694            scene.clearSelection()
695            handler = interactions.NewLinkAction(self)
696            scene.set_user_interaction_handler(handler)
697            return handler.mousePressEvent(event)
698
699        any_item = scene.item_at(pos)
700        if not any_item and event.button() == Qt.LeftButton:
701            self.__emptyClickButtons |= Qt.LeftButton
702            # Create a RectangleSelectionAction but do not set in on the scene
703            # just yet (instead wait for the mouse move event).
704            handler = interactions.RectangleSelectionAction(self)
705            rval = handler.mousePressEvent(event)
706            if rval == True:
707                self.__possibleSelectionHandler = handler
708            return False
709
710        if any_item and event.button() == Qt.LeftButton:
711            self.__possibleMouseItemsMove = True
712            self.__itemsMoving.clear()
713            self.__scene.node_item_position_changed.connect(
714                self.__onNodePositionChanged
715            )
716            self.__annotationGeomChanged.mapped[QObject].connect(
717                self.__onAnnotationGeometryChanged
718            )
719
720        return False
721
722    def sceneMouseMoveEvent(self, event):
723        scene = self.__scene
724        if scene.user_interaction_handler:
725            return False
726
727        if self.__emptyClickButtons & Qt.LeftButton and \
728                event.buttons() & Qt.LeftButton and \
729                self.__possibleSelectionHandler:
730            # Set the RectangleSelection (initialized in mousePressEvent)
731            # on the scene
732            handler = self.__possibleSelectionHandler
733            scene.set_user_interaction_handler(handler)
734            return handler.mouseMoveEvent(event)
735
736        return False
737
738    def sceneMouseReleaseEvent(self, event):
739        if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
740            self.__possibleMouseItemsMove = False
741            self.__scene.node_item_position_changed.disconnect(
742                self.__onNodePositionChanged
743            )
744            self.__annotationGeomChanged.mapped[QObject].disconnect(
745                self.__onAnnotationGeometryChanged
746            )
747
748            if self.__itemsMoving:
749                self.__scene.mouseReleaseEvent(event)
750                stack = self.undoStack()
751                stack.beginMacro(self.tr("Move"))
752                for scheme_item, (old, new) in self.__itemsMoving.items():
753                    if isinstance(scheme_item, scheme.SchemeNode):
754                        command = commands.MoveNodeCommand(
755                            self.scheme(), scheme_item, old, new
756                        )
757                    elif isinstance(scheme_item, scheme.BaseSchemeAnnotation):
758                        command = commands.AnnotationGeometryChange(
759                            self.scheme(), scheme_item, old, new
760                        )
761                    else:
762                        continue
763
764                    stack.push(command)
765                stack.endMacro()
766
767                self.__itemsMoving.clear()
768                return True
769
770        if self.__emptyClickButtons & Qt.LeftButton and \
771                event.button() & Qt.LeftButton:
772            self.__emptyClickButtons &= ~Qt.LeftButton
773
774            if self.__quickMenuTriggers & SchemeEditWidget.Clicked and \
775                    mouse_drag_distance(event, Qt.LeftButton) < 1:
776                action = interactions.NewNodeAction(self)
777                action.create_new(event.screenPos())
778                event.accept()
779                return True
780
781        return False
782
783    def sceneMouseDoubleClickEvent(self, event):
784        scene = self.__scene
785        if scene.user_interaction_handler:
786            return False
787
788        item = scene.item_at(event.scenePos())
789        if not item and self.__quickMenuTriggers & \
790                SchemeEditWidget.DoubleClicked:
791            # Double click on an empty spot
792            # Create a new node quick
793            action = interactions.NewNodeAction(self)
794            action.create_new(event.screenPos())
795            event.accept()
796            return True
797
798        item = scene.item_at(event.scenePos(), items.LinkItem)
799        if item is not None:
800            link = self.scene().link_for_item(item)
801            action = interactions.EditNodeLinksAction(self, link.source_node,
802                                                      link.sink_node)
803            action.edit_links()
804            event.accept()
805            return True
806
807        return False
808
809    def sceneKeyPressEvent(self, event):
810        scene = self.__scene
811        if scene.user_interaction_handler:
812            return False
813
814        # If a QGraphicsItem is in text editing mode, don't interrupt it
815        focusItem = scene.focusItem()
816        if focusItem and isinstance(focusItem, QGraphicsTextItem) and \
817                focusItem.textInteractionFlags() & Qt.TextEditable:
818            return False
819
820        # If the mouse is not over out view
821        if not self.view().underMouse():
822            return False
823
824        handler = None
825        if (event.key() == Qt.Key_Space and \
826                self.__quickMenuTriggers & SchemeEditWidget.SpaceKey):
827            handler = interactions.NewNodeAction(self)
828
829        elif len(event.text()) and \
830                self.__quickMenuTriggers & SchemeEditWidget.AnyKey:
831            handler = interactions.NewNodeAction(self)
832            # TODO: set the search text to event.text() and set focus on the
833            # search line
834
835        if handler is not None:
836            # Control + Backspace (remove widget action) conflicts with the
837            # 'Clear text action in the search widget, so we disable the
838            # remove widget action so the text editing follows standard
839            # 'look and feel'
840            with disabled(self.__removeSelectedAction):
841                handler.create_new(QCursor.pos())
842
843            event.accept()
844            return True
845
846        return False
847
848    def sceneKeyReleaseEvent(self, event):
849        return False
850
851    def sceneContextMenuEvent(self, event):
852        return False
853
854    def __onSelectionChanged(self):
855        nodes = self.selectedNodes()
856        annotations = self.selectedAnnotations()
857
858        self.__openSelectedAction.setEnabled(bool(nodes))
859        self.__removeSelectedAction.setEnabled(
860            bool(nodes) or bool(annotations)
861        )
862
863        self.__helpAction.setEnabled(len(nodes) == 1)
864        self.__renameAction.setEnabled(len(nodes) == 1)
865
866        if len(nodes) > 1:
867            self.__openSelectedAction.setText(self.tr("Open All"))
868        else:
869            self.__openSelectedAction.setText(self.tr("Open"))
870
871        if len(nodes) + len(annotations) > 1:
872            self.__removeSelectedAction.setText(self.tr("Remove All"))
873        else:
874            self.__removeSelectedAction.setText(self.tr("Remove"))
875
876        if len(nodes) == 0:
877            self.__openSelectedAction.setText(self.tr("Open"))
878            self.__removeSelectedAction.setText(self.tr("Remove"))
879
880    def __onNodeAdded(self, node):
881        widget = self.__scheme.widget_for_node[node]
882        widget.widgetStateChanged.connect(self.__onWidgetStateChanged)
883
884    def __onNodeRemoved(self, node):
885        widget = self.__scheme.widget_for_node[node]
886        widget.widgetStateChanged.disconnect(self.__onWidgetStateChanged)
887
888    def __onWidgetStateChanged(self, *args):
889        widget = self.sender()
890        self.scheme()
891        widget_to_node = dict(reversed(item) for item in \
892                              self.__scheme.widget_for_node.items())
893        node = widget_to_node[widget]
894        item = self.__scene.item_for_node(node)
895
896        info = widget.widgetStateToHtml(True, False, False)
897        warning = widget.widgetStateToHtml(False, True, False)
898        error = widget.widgetStateToHtml(False, False, True)
899
900        item.setInfoMessage(info or None)
901        item.setWarningMessage(warning or None)
902        item.setErrorMessage(error or None)
903
904    def __onNodeActivate(self, item):
905        node = self.__scene.node_for_item(item)
906        widget = self.scheme().widget_for_node[node]
907        widget.show()
908        widget.raise_()
909
910    def __onNodePositionChanged(self, item, pos):
911        node = self.__scene.node_for_item(item)
912        new = (pos.x(), pos.y())
913        if node not in self.__itemsMoving:
914            self.__itemsMoving[node] = (node.position, new)
915        else:
916            old, _ = self.__itemsMoving[node]
917            self.__itemsMoving[node] = (old, new)
918
919    def __onAnnotationGeometryChanged(self, item):
920        annot = self.scene().annotation_for_item(item)
921        if annot not in self.__itemsMoving:
922            self.__itemsMoving[annot] = (annot.geometry,
923                                         geometry_from_annotation_item(item))
924        else:
925            old, _ = self.__itemsMoving[annot]
926            self.__itemsMoving[annot] = (old,
927                                         geometry_from_annotation_item(item))
928
929    def __onAnnotationAdded(self, item):
930        log.debug("Annotation added (%r)", item)
931        item.setFlag(QGraphicsItem.ItemIsSelectable)
932        item.setFlag(QGraphicsItem.ItemIsMovable)
933        item.setFlag(QGraphicsItem.ItemIsFocusable)
934
935        item.installSceneEventFilter(self.__focusListener)
936
937        if isinstance(item, items.ArrowAnnotation):
938            pass
939        elif isinstance(item, items.TextAnnotation):
940            # Make the annotation editable.
941            item.setTextInteractionFlags(Qt.TextEditorInteraction)
942
943            self.__editFinishedMapper.setMapping(item, item)
944            item.editingFinished.connect(
945                self.__editFinishedMapper.map
946            )
947
948        self.__annotationGeomChanged.setMapping(item, item)
949        item.geometryChanged.connect(
950            self.__annotationGeomChanged.map
951        )
952
953    def __onAnnotationRemoved(self, item):
954        log.debug("Annotation removed (%r)", item)
955        if isinstance(item, items.ArrowAnnotation):
956            pass
957        elif isinstance(item, items.TextAnnotation):
958            item.editingFinished.disconnect(
959                self.__editFinishedMapper.map
960            )
961
962        item.removeSceneEventFilter(self.__focusListener)
963
964        self.__annotationGeomChanged.removeMappings(item)
965        item.geometryChanged.disconnect(
966            self.__annotationGeomChanged.map
967        )
968
969    def __onItemFocusedIn(self, item):
970        """Annotation item has gained focus.
971        """
972        if not self.__scene.user_interaction_handler:
973            self.__startControlPointEdit(item)
974
975    def __onItemFocusedOut(self, item):
976        """Annotation item lost focus.
977        """
978        self.__endControlPointEdit()
979
980    def __onEditingFinished(self, item):
981        """Text annotation editing has finished.
982        """
983        annot = self.__scene.annotation_for_item(item)
984        text = unicode(item.toPlainText())
985        if annot.text != text:
986            self.__undoStack.push(
987                commands.TextChangeCommand(self.scheme(), annot,
988                                           annot.text, text)
989            )
990
991    def __toggleNewArrowAnnotation(self, checked):
992        if self.__newTextAnnotationAction.isChecked():
993            self.__newTextAnnotationAction.setChecked(not checked)
994
995        action = self.__newArrowAnnotationAction
996
997        if not checked:
998            handler = self.__scene.user_interaction_handler
999            if isinstance(handler, interactions.NewArrowAnnotation):
1000                # Cancel the interaction and restore the state
1001                handler.ended.disconnect(action.toggle)
1002                handler.cancel(interactions.UserInteraction.UserCancelReason)
1003                log.info("Canceled new arrow annotation")
1004
1005        else:
1006            handler = interactions.NewArrowAnnotation(self)
1007            checked = self.__arrowColorActionGroup.checkedAction()
1008            handler.setColor(checked.data().toPyObject())
1009
1010            handler.ended.connect(action.toggle)
1011
1012            self.__scene.set_user_interaction_handler(handler)
1013
1014    def __onFontSizeTriggered(self, action):
1015        if not self.__newTextAnnotationAction.isChecked():
1016            # Trigger the action
1017            self.__newTextAnnotationAction.trigger()
1018        else:
1019            # just update the preferred font on the interaction handler
1020            handler = self.__scene.user_interaction_handler
1021            if isinstance(handler, interactions.NewTextAnnotation):
1022                handler.setFont(action.font())
1023
1024    def __toggleNewTextAnnotation(self, checked):
1025        if self.__newArrowAnnotationAction.isChecked():
1026            self.__newArrowAnnotationAction.setChecked(not checked)
1027
1028        action = self.__newTextAnnotationAction
1029
1030        if not checked:
1031            handler = self.__scene.user_interaction_handler
1032            if isinstance(handler, interactions.NewTextAnnotation):
1033                # cancel the interaction and restore the state
1034                handler.ended.disconnect(action.toggle)
1035                handler.cancel(interactions.UserInteraction.UserCancelReason)
1036                log.info("Canceled new text annotation")
1037
1038        else:
1039            handler = interactions.NewTextAnnotation(self)
1040            checked = self.__fontActionGroup.checkedAction()
1041            handler.setFont(checked.font())
1042
1043            handler.ended.connect(action.toggle)
1044
1045            self.__scene.set_user_interaction_handler(handler)
1046
1047    def __onArrowColorTriggered(self, action):
1048        if not self.__newArrowAnnotationAction.isChecked():
1049            # Trigger the action
1050            self.__newArrowAnnotationAction.trigger()
1051        else:
1052            # just update the preferred color on the interaction handler
1053            handler = self.__scene.user_interaction_handler
1054            if isinstance(handler, interactions.NewArrowAnnotation):
1055                handler.setColor(action.data().toPyObject())
1056
1057    def __onCustomContextMenuRequested(self, pos):
1058        scenePos = self.view().mapToScene(pos)
1059        globalPos = self.view().mapToGlobal(pos)
1060
1061        item = self.scene().item_at(scenePos, items.NodeItem)
1062        if item is not None:
1063            self.__widgetMenu.popup(globalPos)
1064            return
1065
1066        item = self.scene().item_at(scenePos, items.LinkItem)
1067        if item is not None:
1068            link = self.scene().link_for_item(item)
1069            self.__linkEnableAction.setChecked(link.enabled)
1070            self.__contextMenuTarget = link
1071            self.__linkMenu.popup(globalPos)
1072            return
1073
1074    def __onRenameAction(self):
1075        selected = self.selectedNodes()
1076        if len(selected) == 1:
1077            self.editNodeTitle(selected[0])
1078
1079    def __onHelpAction(self):
1080        nodes = self.selectedNodes()
1081        help_url = None
1082        if len(nodes) == 1:
1083            node = nodes[0]
1084            desc = node.description
1085            if desc.help:
1086                help_url = desc.help
1087
1088        if help_url is not None:
1089            QDesktopServices.openUrl(QUrl(help_url))
1090        else:
1091            message_information(
1092                self.tr("Sorry there is documentation available for "
1093                        "this widget."),
1094                parent=self)
1095
1096    def __toggleLinkEnabled(self, enabled):
1097        """Link enabled state was toggled in the context menu.
1098        """
1099        if self.__contextMenuTarget:
1100            link = self.__contextMenuTarget
1101            command = commands.SetAttrCommand(
1102                link, "enabled", enabled, name=self.tr("Set enabled"),
1103            )
1104            self.__undoStack.push(command)
1105
1106    def __linkRemove(self):
1107        """Remove link was requested from the context menu.
1108        """
1109        if self.__contextMenuTarget:
1110            self.removeLink(self.__contextMenuTarget)
1111
1112    def __linkReset(self):
1113        """Link reset from the context menu was requested.
1114        """
1115        if self.__contextMenuTarget:
1116            link = self.__contextMenuTarget
1117            action = interactions.EditNodeLinksAction(
1118                self, link.source_node, link.sink_node
1119            )
1120            action.edit_links()
1121
1122    def __startControlPointEdit(self, item):
1123        """Start a control point edit interaction for item.
1124        """
1125        if isinstance(item, items.ArrowAnnotation):
1126            handler = interactions.ResizeArrowAnnotation(self)
1127        elif isinstance(item, items.TextAnnotation):
1128            handler = interactions.ResizeTextAnnotation(self)
1129        else:
1130            log.warning("Unknown annotation item type %r" % item)
1131            return
1132
1133        handler.editItem(item)
1134        self.__scene.set_user_interaction_handler(handler)
1135
1136        log.info("Control point editing started (%r)." % item)
1137
1138    def __endControlPointEdit(self):
1139        """End the current control point edit interaction.
1140        """
1141        handler = self.__scene.user_interaction_handler
1142        if isinstance(handler, (interactions.ResizeArrowAnnotation,
1143                                interactions.ResizeTextAnnotation)) and \
1144                not handler.isFinished() and not handler.isCanceled():
1145            handler.commit()
1146            handler.end()
1147
1148            log.info("Control point editing finished.")
1149
1150
1151def geometry_from_annotation_item(item):
1152    if isinstance(item, items.ArrowAnnotation):
1153        line = item.line()
1154        p1 = item.mapToScene(line.p1())
1155        p2 = item.mapToScene(line.p2())
1156        return ((p1.x(), p1.y()), (p2.x(), p2.y()))
1157    elif isinstance(item, items.TextAnnotation):
1158        geom = item.geometry()
1159        return (geom.x(), geom.y(), geom.width(), geom.height())
1160
1161
1162def mouse_drag_distance(event, button=Qt.LeftButton):
1163    """Return the (manhattan) distance between the (screen position)
1164    when the `button` was pressed and the current mouse position.
1165
1166    """
1167    diff = (event.buttonDownScreenPos(button) - event.screenPos())
1168    return diff.manhattanLength()
Note: See TracBrowser for help on using the repository browser.