source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11245:98f95d6d6bff

Revision 11245:98f95d6d6bff, 45.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Fixed freeze/edit actions state update when setting a new scheme.

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