source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11389:f1c916d51171

Revision 11389:f1c916d51171, 51.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Fixed wrong event type check.

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