source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11259:93eddd7eed11

Revision 11259:93eddd7eed11, 45.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Set the channel names visiblity state from the settings.

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