source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11658:5f2145410559

Revision 11658:5f2145410559, 52.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 8 months ago (diff)

Changed 'isModifiedStrict' to directly compare the node propetries.

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