source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11615:11a0d2030073

Revision 11615:11a0d2030073, 51.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Removed uses of WidgetsScheme.widget_settings in SchemeEditWidget.

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