source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11451:c21c7305e28c

Revision 11451:c21c7305e28c, 52.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixed a typo in method name.

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