source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11266:09eccd24f94d

Revision 11266:09eccd24f94d, 46.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Make sure widgetStateChanged signals are connected when setting a new scheme, added focusNode method.

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