source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11616:2e29f6b9b531

Revision 11616:2e29f6b9b531, 52.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Change canvas background when the signal propagation is paused.

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