source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11279:e788ced9f4b2

Revision 11279:e788ced9f4b2, 47.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Enumerating new node titles as they are created.

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