source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11490:38efa0b7e690

Revision 11490:38efa0b7e690, 52.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Initialize the search text with the pressed key.

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