source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11471:20cbd9d2b23b

Revision 11471:20cbd9d2b23b, 52.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Fix an TypeError when clearing the CanvasScene instance.

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