source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11550:34029dacd329

Revision 11550:34029dacd329, 52.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Ignore/reject drops on the canvas with unsupported data.

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