source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11390:72c3678f0e44

Revision 11390:72c3678f0e44, 51.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Added sphinx documentation for SchemeEditWidget.

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