source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11492:30c137827890

Revision 11492:30c137827890, 52.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Do not show the menu on a key press if the key is a control character.

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