source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11296:207c96a59fe3

Revision 11296:207c96a59fe3, 48.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Capturing help requests ("F1" key press) from OWBaseWidget.

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, QWhatsThisClickedEvent
19)
20
21from PyQt4.QtCore import (
22    Qt, QObject, QEvent, QSignalMapper, QRectF, 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                self.__scheme.removeEventFilter(self)
451
452            self.__scheme = scheme
453
454            self.setPath("")
455
456            if self.__scheme:
457                self.__scheme.title_changed.connect(self.titleChanged)
458                self.__scheme.node_added.connect(self.__onNodeAdded)
459                self.__scheme.node_removed.connect(self.__onNodeRemoved)
460                self.titleChanged.emit(scheme.title)
461                self.__cleanSettings = scheme.widget_settings()
462            else:
463                self.__cleanSettings = []
464
465            # Clear the current item selection in the scene so edit action
466            # states are updated accordingly.
467            self.__scene.clearSelection()
468
469            self.__annotationGeomChanged.deleteLater()
470            self.__annotationGeomChanged = QSignalMapper(self)
471
472            self.__undoStack.clear()
473
474            self.__focusListener.itemFocusedIn.disconnect(
475                self.__onItemFocusedIn
476            )
477            self.__focusListener.itemFocusedOut.disconnect(
478                self.__onItemFocusedOut
479            )
480
481            self.__scene.selectionChanged.disconnect(
482                self.__onSelectionChanged
483            )
484
485            self.__scene.clear()
486            self.__scene.removeEventFilter(self)
487            self.__scene.deleteLater()
488
489            self.__scene = CanvasScene()
490            self.__view.setScene(self.__scene)
491            self.__scene.set_channel_names_visible(self.__channelNamesVisible)
492
493            self.__scene.installEventFilter(self)
494
495            self.__scene.set_registry(self.__registry)
496
497            # Focus listener
498            self.__focusListener = GraphicsSceneFocusEventListener()
499            self.__focusListener.itemFocusedIn.connect(
500                self.__onItemFocusedIn
501            )
502            self.__focusListener.itemFocusedOut.connect(
503                self.__onItemFocusedOut
504            )
505            self.__scene.addItem(self.__focusListener)
506
507            self.__scene.selectionChanged.connect(
508                self.__onSelectionChanged
509            )
510
511            self.__scene.node_item_activated.connect(
512                self.__onNodeActivate
513            )
514
515            self.__scene.annotation_added.connect(
516                self.__onAnnotationAdded
517            )
518
519            self.__scene.annotation_removed.connect(
520                self.__onAnnotationRemoved
521            )
522
523            self.__scene.set_scheme(scheme)
524
525            if self.__scheme:
526                for node in self.__scheme.nodes:
527                    self.__onNodeAdded(node)
528
529                self.__scheme.installEventFilter(self)
530
531    def scheme(self):
532        """Return the :class:`Scheme` edited by the widget.
533        """
534        return self.__scheme
535
536    def scene(self):
537        """Return the QGraphicsScene instance used to display the scheme.
538        """
539        return self.__scene
540
541    def view(self):
542        """Return the QGraphicsView instance used to display the scene.
543        """
544        return self.__view
545
546    def setRegistry(self, registry):
547        # Is this method necessary
548        self.__registry = registry
549        if self.__scene:
550            self.__scene.set_registry(registry)
551            self.__quickMenu = None
552
553    def quickMenu(self):
554        """Return a quick menu instance for quick new node creation.
555        """
556        if self.__quickMenu is None:
557            menu = quickmenu.QuickMenu(self)
558            if self.__registry is not None:
559                menu.setModel(self.__registry.model())
560            self.__quickMenu = menu
561        return self.__quickMenu
562
563    def setTitle(self, title):
564        """
565        Set the scheme title.
566        """
567        self.__undoStack.push(
568            commands.SetAttrCommand(self.__scheme, "title", title)
569        )
570
571    def setDescription(self, description):
572        """
573        Set the scheme description string.
574        """
575        self.__undoStack.push(
576            commands.SetAttrCommand(self.__scheme, "description", description)
577        )
578
579    def addNode(self, node):
580        """
581        Add a new node (:class:`SchemeNode`) to the document.
582        """
583        command = commands.AddNodeCommand(self.__scheme, node)
584        self.__undoStack.push(command)
585
586    def createNewNode(self, description, title=None, position=None):
587        """
588        Create a new `SchemeNode` and add it to the document. The new
589        node is constructed using `newNodeHelper` method.
590
591        """
592        node = self.newNodeHelper(description, title, position)
593        self.addNode(node)
594
595        return node
596
597    def newNodeHelper(self, description, title=None, position=None):
598        """
599        Return a new initialized `SchemeNode`. If title and position are
600        not supplied they are initialized to a sensible defaults.
601
602        """
603        if title is None:
604            title = self.enumerateTitle(description.name)
605
606        if position is None:
607            position = self.nextPosition()
608
609        return scheme.SchemeNode(description, title=title, position=position)
610
611    def enumerateTitle(self, title):
612        """
613        Enumerate a title string (i.e. add a number in parentheses) so it is
614        not equal to any node title in the current scheme.
615
616        """
617        curr_titles = set([node.title for node in self.scheme().nodes])
618        template = title + " ({0})"
619
620        enumerated = itertools.imap(template.format, itertools.count(1))
621        candidates = itertools.chain([title], enumerated)
622
623        seq = itertools.dropwhile(curr_titles.__contains__, candidates)
624        return next(seq)
625
626    def nextPosition(self):
627        """
628        Return the next default node position (x, y) tuple. This is
629        a position left of the last added node.
630
631        """
632        nodes = self.scheme().nodes
633        if nodes:
634            x, y = nodes[-1].position
635            position = (x + 150, y)
636        else:
637            position = (150, 150)
638        return position
639
640    def removeNode(self, node):
641        """Remove a `node` (:class:`SchemeNode`) from the scheme
642        """
643        command = commands.RemoveNodeCommand(self.__scheme, node)
644        self.__undoStack.push(command)
645
646    def renameNode(self, node, title):
647        """Rename a `node` (:class:`SchemeNode`) to `title`.
648        """
649        command = commands.RenameNodeCommand(self.__scheme, node, title)
650        self.__undoStack.push(command)
651
652    def addLink(self, link):
653        """Add a `link` (:class:`SchemeLink`) to the scheme.
654        """
655        command = commands.AddLinkCommand(self.__scheme, link)
656        self.__undoStack.push(command)
657
658    def removeLink(self, link):
659        """Remove a link (:class:`SchemeLink`) from the scheme.
660        """
661        command = commands.RemoveLinkCommand(self.__scheme, link)
662        self.__undoStack.push(command)
663
664    def addAnnotation(self, annotation):
665        """Add `annotation` (:class:`BaseSchemeAnnotation`) to the scheme
666        """
667        command = commands.AddAnnotationCommand(self.__scheme, annotation)
668        self.__undoStack.push(command)
669
670    def removeAnnotation(self, annotation):
671        """Remove `annotation` (:class:`BaseSchemeAnnotation`) from the scheme.
672        """
673        command = commands.RemoveAnnotationCommand(self.__scheme, annotation)
674        self.__undoStack.push(command)
675
676    def removeSelected(self):
677        """Remove all selected items in the scheme.
678        """
679        selected = self.scene().selectedItems()
680        if not selected:
681            return
682
683        self.__undoStack.beginMacro(self.tr("Remove"))
684        for item in selected:
685            if isinstance(item, items.NodeItem):
686                node = self.scene().node_for_item(item)
687                self.__undoStack.push(
688                    commands.RemoveNodeCommand(self.__scheme, node)
689                )
690            elif isinstance(item, items.annotationitem.Annotation):
691                annot = self.scene().annotation_for_item(item)
692                self.__undoStack.push(
693                    commands.RemoveAnnotationCommand(self.__scheme, annot)
694                )
695        self.__undoStack.endMacro()
696
697    def selectAll(self):
698        """Select all selectable items in the scheme.
699        """
700        for item in self.__scene.items():
701            if item.flags() & QGraphicsItem.ItemIsSelectable:
702                item.setSelected(True)
703
704    def toogleZoom(self, zoom):
705        view = self.view()
706        if zoom:
707            view.scale(1.5, 1.5)
708        else:
709            view.resetTransform()
710
711    def alignToGrid(self):
712        """Align nodes to a grid.
713        """
714        tile_size = 150
715        tiles = {}
716
717        nodes = sorted(self.scheme().nodes, key=attrgetter("position"))
718
719        if nodes:
720            self.__undoStack.beginMacro(self.tr("Align To Grid"))
721
722            for node in nodes:
723                x, y = node.position
724                x = int(round(float(x) / tile_size) * tile_size)
725                y = int(round(float(y) / tile_size) * tile_size)
726                while (x, y) in tiles:
727                    x += tile_size
728
729                self.__undoStack.push(
730                    commands.MoveNodeCommand(self.scheme(), node,
731                                             node.position, (x, y))
732                )
733
734                tiles[x, y] = node
735                self.__scene.item_for_node(node).setPos(x, y)
736
737            self.__undoStack.endMacro()
738
739    def focusNode(self):
740        """Return the current focused `SchemeNode` or None if no
741        node has focus.
742
743        """
744        focus = self.__scene.focusItem()
745        node = None
746        if isinstance(focus, items.NodeItem):
747            try:
748                node = self.__scene.node_for_item(focus)
749            except KeyError:
750                # in case the node has been removed but the scene was not
751                # yet fully updated.
752                node = None
753        return node
754
755    def selectedNodes(self):
756        """Return all selected `SchemeNode` items.
757        """
758        return map(self.scene().node_for_item,
759                   self.scene().selected_node_items())
760
761    def selectedAnnotations(self):
762        """Return all selected `SchemeAnnotation` items.
763        """
764        return map(self.scene().annotation_for_item,
765                   self.scene().selected_annotation_items())
766
767    def openSelected(self):
768        """Open (show and raise) all widgets for selected nodes.
769        """
770        selected = self.scene().selected_node_items()
771        for item in selected:
772            self.__onNodeActivate(item)
773
774    def editNodeTitle(self, node):
775        """Edit the `node`'s title.
776        """
777        name, ok = QInputDialog.getText(
778                    self, self.tr("Rename"),
779                    unicode(self.tr("Enter a new name for the %r widget")) \
780                    % node.title,
781                    text=node.title
782                    )
783
784        if ok:
785            self.__undoStack.push(
786                commands.RenameNodeCommand(self.__scheme, node, node.title,
787                                           unicode(name))
788            )
789
790    def __onCleanChanged(self, clean):
791        if self.isWindowModified() != (not clean):
792            self.setWindowModified(not clean)
793            self.modificationChanged.emit(not clean)
794
795    def eventFilter(self, obj, event):
796        # Filter the scene's drag/drop events.
797        if obj is self.scene():
798            etype = event.type()
799            if  etype == QEvent.GraphicsSceneDragEnter or \
800                    etype == QEvent.GraphicsSceneDragMove:
801                mime_data = event.mimeData()
802                if mime_data.hasFormat(
803                        "application/vnv.orange-canvas.registry.qualified-name"
804                        ):
805                    event.acceptProposedAction()
806                return True
807            elif etype == QEvent.GraphicsSceneDrop:
808                data = event.mimeData()
809                qname = data.data(
810                    "application/vnv.orange-canvas.registry.qualified-name"
811                )
812                desc = self.__registry.widget(unicode(qname))
813                pos = event.scenePos()
814                self.createNewNode(desc, position=(pos.x(), pos.y()))
815                return True
816
817            elif etype == QEvent.GraphicsSceneMousePress:
818                return self.sceneMousePressEvent(event)
819            elif etype == QEvent.GraphicsSceneMouseMove:
820                return self.sceneMouseMoveEvent(event)
821            elif etype == QEvent.GraphicsSceneMouseRelease:
822                return self.sceneMouseReleaseEvent(event)
823            elif etype == QEvent.GraphicsSceneMouseDoubleClick:
824                return self.sceneMouseDoubleClickEvent(event)
825            elif etype == QEvent.KeyRelease:
826                return self.sceneKeyPressEvent(event)
827            elif etype == QEvent.KeyRelease:
828                return self.sceneKeyReleaseEvent(event)
829            elif etype == QEvent.GraphicsSceneContextMenu:
830                return self.sceneContextMenuEvent(event)
831
832        elif obj is self.__scheme:
833            if event.type() == QEvent.WhatsThisClicked:
834                # Re post the event
835                self.__showHelpFor(event.href())
836
837        return QWidget.eventFilter(self, obj, event)
838
839    def sceneMousePressEvent(self, event):
840        scene = self.__scene
841        if scene.user_interaction_handler:
842            return False
843
844        pos = event.scenePos()
845
846        anchor_item = scene.item_at(pos, items.NodeAnchorItem,
847                                    buttons=Qt.LeftButton)
848        if anchor_item and event.button() == Qt.LeftButton:
849            # Start a new link starting at item
850            scene.clearSelection()
851            handler = interactions.NewLinkAction(self)
852            self._setUserInteractionHandler(handler)
853            return handler.mousePressEvent(event)
854
855        any_item = scene.item_at(pos)
856        if not any_item and event.button() == Qt.LeftButton:
857            self.__emptyClickButtons |= Qt.LeftButton
858            # Create a RectangleSelectionAction but do not set in on the scene
859            # just yet (instead wait for the mouse move event).
860            handler = interactions.RectangleSelectionAction(self)
861            rval = handler.mousePressEvent(event)
862            if rval == True:
863                self.__possibleSelectionHandler = handler
864            return rval
865
866        if any_item and event.button() == Qt.LeftButton:
867            self.__possibleMouseItemsMove = True
868            self.__itemsMoving.clear()
869            self.__scene.node_item_position_changed.connect(
870                self.__onNodePositionChanged
871            )
872            self.__annotationGeomChanged.mapped[QObject].connect(
873                self.__onAnnotationGeometryChanged
874            )
875
876            set_enabled_all(self.__disruptiveActions, False)
877
878        return False
879
880    def sceneMouseMoveEvent(self, event):
881        scene = self.__scene
882        if scene.user_interaction_handler:
883            return False
884
885        if self.__emptyClickButtons & Qt.LeftButton and \
886                event.buttons() & Qt.LeftButton and \
887                self.__possibleSelectionHandler:
888            # Set the RectangleSelection (initialized in mousePressEvent)
889            # on the scene
890            handler = self.__possibleSelectionHandler
891            self._setUserInteractionHandler(handler)
892            return handler.mouseMoveEvent(event)
893
894        return False
895
896    def sceneMouseReleaseEvent(self, event):
897        if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
898            self.__possibleMouseItemsMove = False
899            self.__scene.node_item_position_changed.disconnect(
900                self.__onNodePositionChanged
901            )
902            self.__annotationGeomChanged.mapped[QObject].disconnect(
903                self.__onAnnotationGeometryChanged
904            )
905
906            set_enabled_all(self.__disruptiveActions, True)
907
908            if self.__itemsMoving:
909                self.__scene.mouseReleaseEvent(event)
910                stack = self.undoStack()
911                stack.beginMacro(self.tr("Move"))
912                for scheme_item, (old, new) in self.__itemsMoving.items():
913                    if isinstance(scheme_item, scheme.SchemeNode):
914                        command = commands.MoveNodeCommand(
915                            self.scheme(), scheme_item, old, new
916                        )
917                    elif isinstance(scheme_item, scheme.BaseSchemeAnnotation):
918                        command = commands.AnnotationGeometryChange(
919                            self.scheme(), scheme_item, old, new
920                        )
921                    else:
922                        continue
923
924                    stack.push(command)
925                stack.endMacro()
926
927                self.__itemsMoving.clear()
928                return True
929
930        if self.__emptyClickButtons & Qt.LeftButton and \
931                event.button() & Qt.LeftButton:
932            self.__emptyClickButtons &= ~Qt.LeftButton
933
934            if self.__quickMenuTriggers & SchemeEditWidget.Clicked and \
935                    mouse_drag_distance(event, Qt.LeftButton) < 1:
936                action = interactions.NewNodeAction(self)
937
938                with nested(disabled(self.__undoAction),
939                            disabled(self.__redoAction)):
940                    action.create_new(event.screenPos())
941
942                event.accept()
943                return True
944
945        return False
946
947    def sceneMouseDoubleClickEvent(self, event):
948        scene = self.__scene
949        if scene.user_interaction_handler:
950            return False
951
952        item = scene.item_at(event.scenePos())
953        if not item and self.__quickMenuTriggers & \
954                SchemeEditWidget.DoubleClicked:
955            # Double click on an empty spot
956            # Create a new node using QuickMenu
957            action = interactions.NewNodeAction(self)
958
959            with nested(disabled(self.__undoAction),
960                        disabled(self.__redoAction)):
961                action.create_new(event.screenPos())
962
963            event.accept()
964            return True
965
966        item = scene.item_at(event.scenePos(), items.LinkItem,
967                             buttons=Qt.LeftButton)
968
969        if item is not None and event.button() == Qt.LeftButton:
970            link = self.scene().link_for_item(item)
971            action = interactions.EditNodeLinksAction(self, link.source_node,
972                                                      link.sink_node)
973            action.edit_links()
974            event.accept()
975            return True
976
977        return False
978
979    def sceneKeyPressEvent(self, event):
980        scene = self.__scene
981        if scene.user_interaction_handler:
982            return False
983
984        # If a QGraphicsItem is in text editing mode, don't interrupt it
985        focusItem = scene.focusItem()
986        if focusItem and isinstance(focusItem, QGraphicsTextItem) and \
987                focusItem.textInteractionFlags() & Qt.TextEditable:
988            return False
989
990        # If the mouse is not over out view
991        if not self.view().underMouse():
992            return False
993
994        handler = None
995        if (event.key() == Qt.Key_Space and \
996                self.__quickMenuTriggers & SchemeEditWidget.SpaceKey):
997            handler = interactions.NewNodeAction(self)
998
999        elif len(event.text()) and \
1000                self.__quickMenuTriggers & SchemeEditWidget.AnyKey:
1001            handler = interactions.NewNodeAction(self)
1002            # TODO: set the search text to event.text() and set focus on the
1003            # search line
1004
1005        if handler is not None:
1006            # Control + Backspace (remove widget action on Mac OSX) conflicts
1007            # with the 'Clear text' action in the search widget (there might
1008            # be selected items in the canvas), so we disable the
1009            # remove widget action so the text editing follows standard
1010            # 'look and feel'
1011            with nested(disabled(self.__removeSelectedAction),
1012                        disabled(self.__undoAction),
1013                        disabled(self.__redoAction)):
1014                handler.create_new(QCursor.pos())
1015
1016            event.accept()
1017            return True
1018
1019        return False
1020
1021    def sceneKeyReleaseEvent(self, event):
1022        return False
1023
1024    def sceneContextMenuEvent(self, event):
1025        return False
1026
1027    def _setUserInteractionHandler(self, handler):
1028        """Helper method for setting the user interaction handlers.
1029        """
1030        if self.__scene.user_interaction_handler:
1031            if isinstance(self.__scene.user_interaction_handler,
1032                          (interactions.ResizeArrowAnnotation,
1033                           interactions.ResizeTextAnnotation)):
1034                self.__scene.user_interaction_handler.commit()
1035
1036            self.__scene.user_interaction_handler.ended.disconnect(
1037                self.__onInteractionEnded
1038            )
1039
1040        if handler:
1041            handler.ended.connect(self.__onInteractionEnded)
1042            # Disable actions which could change the model
1043            set_enabled_all(self.__disruptiveActions, False)
1044
1045        self.__scene.set_user_interaction_handler(handler)
1046
1047    def __onInteractionEnded(self):
1048        self.sender().ended.disconnect(self.__onInteractionEnded)
1049        set_enabled_all(self.__disruptiveActions, True)
1050
1051    def __onSelectionChanged(self):
1052        nodes = self.selectedNodes()
1053        annotations = self.selectedAnnotations()
1054
1055        self.__openSelectedAction.setEnabled(bool(nodes))
1056        self.__removeSelectedAction.setEnabled(
1057            bool(nodes) or bool(annotations)
1058        )
1059
1060        self.__helpAction.setEnabled(len(nodes) == 1)
1061        self.__renameAction.setEnabled(len(nodes) == 1)
1062
1063        if len(nodes) > 1:
1064            self.__openSelectedAction.setText(self.tr("Open All"))
1065        else:
1066            self.__openSelectedAction.setText(self.tr("Open"))
1067
1068        if len(nodes) + len(annotations) > 1:
1069            self.__removeSelectedAction.setText(self.tr("Remove All"))
1070        else:
1071            self.__removeSelectedAction.setText(self.tr("Remove"))
1072
1073        if len(nodes) == 0:
1074            self.__openSelectedAction.setText(self.tr("Open"))
1075            self.__removeSelectedAction.setText(self.tr("Remove"))
1076
1077        focus = self.focusNode()
1078        if focus is not None:
1079            desc = focus.description
1080            tip = whats_this_helper(desc)
1081        else:
1082            tip = ""
1083
1084        if tip != self.__quickTip:
1085            self.__quickTip = tip
1086            ev = QuickHelpTipEvent("", self.__quickTip,
1087                                   priority=QuickHelpTipEvent.Permanent)
1088
1089            QCoreApplication.sendEvent(self, ev)
1090
1091    def __onNodeAdded(self, node):
1092        widget = self.__scheme.widget_for_node[node]
1093        widget.widgetStateChanged.connect(self.__onWidgetStateChanged)
1094
1095    def __onNodeRemoved(self, node):
1096        widget = self.__scheme.widget_for_node[node]
1097        widget.widgetStateChanged.disconnect(self.__onWidgetStateChanged)
1098
1099    def __onWidgetStateChanged(self, *args):
1100        widget = self.sender()
1101        self.scheme()
1102        widget_to_node = dict(reversed(item) for item in \
1103                              self.__scheme.widget_for_node.items())
1104        node = widget_to_node[widget]
1105        item = self.__scene.item_for_node(node)
1106
1107        info = widget.widgetStateToHtml(True, False, False)
1108        warning = widget.widgetStateToHtml(False, True, False)
1109        error = widget.widgetStateToHtml(False, False, True)
1110
1111        item.setInfoMessage(info or None)
1112        item.setWarningMessage(warning or None)
1113        item.setErrorMessage(error or None)
1114
1115    def __onNodeActivate(self, item):
1116        node = self.__scene.node_for_item(item)
1117        widget = self.scheme().widget_for_node[node]
1118        widget.show()
1119        widget.raise_()
1120
1121    def __onNodePositionChanged(self, item, pos):
1122        node = self.__scene.node_for_item(item)
1123        new = (pos.x(), pos.y())
1124        if node not in self.__itemsMoving:
1125            self.__itemsMoving[node] = (node.position, new)
1126        else:
1127            old, _ = self.__itemsMoving[node]
1128            self.__itemsMoving[node] = (old, new)
1129
1130    def __onAnnotationGeometryChanged(self, item):
1131        annot = self.scene().annotation_for_item(item)
1132        if annot not in self.__itemsMoving:
1133            self.__itemsMoving[annot] = (annot.geometry,
1134                                         geometry_from_annotation_item(item))
1135        else:
1136            old, _ = self.__itemsMoving[annot]
1137            self.__itemsMoving[annot] = (old,
1138                                         geometry_from_annotation_item(item))
1139
1140    def __onAnnotationAdded(self, item):
1141        log.debug("Annotation added (%r)", item)
1142        item.setFlag(QGraphicsItem.ItemIsSelectable)
1143        item.setFlag(QGraphicsItem.ItemIsMovable)
1144        item.setFlag(QGraphicsItem.ItemIsFocusable)
1145
1146        item.installSceneEventFilter(self.__focusListener)
1147
1148        if isinstance(item, items.ArrowAnnotation):
1149            pass
1150        elif isinstance(item, items.TextAnnotation):
1151            # Make the annotation editable.
1152            item.setTextInteractionFlags(Qt.TextEditorInteraction)
1153
1154            self.__editFinishedMapper.setMapping(item, item)
1155            item.editingFinished.connect(
1156                self.__editFinishedMapper.map
1157            )
1158
1159        self.__annotationGeomChanged.setMapping(item, item)
1160        item.geometryChanged.connect(
1161            self.__annotationGeomChanged.map
1162        )
1163
1164    def __onAnnotationRemoved(self, item):
1165        log.debug("Annotation removed (%r)", item)
1166        if isinstance(item, items.ArrowAnnotation):
1167            pass
1168        elif isinstance(item, items.TextAnnotation):
1169            item.editingFinished.disconnect(
1170                self.__editFinishedMapper.map
1171            )
1172
1173        item.removeSceneEventFilter(self.__focusListener)
1174
1175        self.__annotationGeomChanged.removeMappings(item)
1176        item.geometryChanged.disconnect(
1177            self.__annotationGeomChanged.map
1178        )
1179
1180    def __onItemFocusedIn(self, item):
1181        """Annotation item has gained focus.
1182        """
1183        if not self.__scene.user_interaction_handler:
1184            self.__startControlPointEdit(item)
1185
1186    def __onItemFocusedOut(self, item):
1187        """Annotation item lost focus.
1188        """
1189        self.__endControlPointEdit()
1190
1191    def __onEditingFinished(self, item):
1192        """Text annotation editing has finished.
1193        """
1194        annot = self.__scene.annotation_for_item(item)
1195        text = unicode(item.toPlainText())
1196        if annot.text != text:
1197            self.__undoStack.push(
1198                commands.TextChangeCommand(self.scheme(), annot,
1199                                           annot.text, text)
1200            )
1201
1202    def __toggleNewArrowAnnotation(self, checked):
1203        if self.__newTextAnnotationAction.isChecked():
1204            self.__newTextAnnotationAction.setChecked(not checked)
1205
1206        action = self.__newArrowAnnotationAction
1207
1208        if not checked:
1209            handler = self.__scene.user_interaction_handler
1210            if isinstance(handler, interactions.NewArrowAnnotation):
1211                # Cancel the interaction and restore the state
1212                handler.ended.disconnect(action.toggle)
1213                handler.cancel(interactions.UserInteraction.UserCancelReason)
1214                log.info("Canceled new arrow annotation")
1215
1216        else:
1217            handler = interactions.NewArrowAnnotation(self)
1218            checked = self.__arrowColorActionGroup.checkedAction()
1219            handler.setColor(checked.data().toPyObject())
1220
1221            handler.ended.connect(action.toggle)
1222
1223            self._setUserInteractionHandler(handler)
1224
1225    def __onFontSizeTriggered(self, action):
1226        if not self.__newTextAnnotationAction.isChecked():
1227            # Trigger the action
1228            self.__newTextAnnotationAction.trigger()
1229        else:
1230            # just update the preferred font on the interaction handler
1231            handler = self.__scene.user_interaction_handler
1232            if isinstance(handler, interactions.NewTextAnnotation):
1233                handler.setFont(action.font())
1234
1235    def __toggleNewTextAnnotation(self, checked):
1236        if self.__newArrowAnnotationAction.isChecked():
1237            self.__newArrowAnnotationAction.setChecked(not checked)
1238
1239        action = self.__newTextAnnotationAction
1240
1241        if not checked:
1242            handler = self.__scene.user_interaction_handler
1243            if isinstance(handler, interactions.NewTextAnnotation):
1244                # cancel the interaction and restore the state
1245                handler.ended.disconnect(action.toggle)
1246                handler.cancel(interactions.UserInteraction.UserCancelReason)
1247                log.info("Canceled new text annotation")
1248
1249        else:
1250            handler = interactions.NewTextAnnotation(self)
1251            checked = self.__fontActionGroup.checkedAction()
1252            handler.setFont(checked.font())
1253
1254            handler.ended.connect(action.toggle)
1255
1256            self._setUserInteractionHandler(handler)
1257
1258    def __onArrowColorTriggered(self, action):
1259        if not self.__newArrowAnnotationAction.isChecked():
1260            # Trigger the action
1261            self.__newArrowAnnotationAction.trigger()
1262        else:
1263            # just update the preferred color on the interaction handler
1264            handler = self.__scene.user_interaction_handler
1265            if isinstance(handler, interactions.NewArrowAnnotation):
1266                handler.setColor(action.data().toPyObject())
1267
1268    def __onCustomContextMenuRequested(self, pos):
1269        scenePos = self.view().mapToScene(pos)
1270        globalPos = self.view().mapToGlobal(pos)
1271
1272        item = self.scene().item_at(scenePos, items.NodeItem)
1273        if item is not None:
1274            self.__widgetMenu.popup(globalPos)
1275            return
1276
1277        item = self.scene().item_at(scenePos, items.LinkItem,
1278                                    buttons=Qt.RightButton)
1279        if item is not None:
1280            link = self.scene().link_for_item(item)
1281            self.__linkEnableAction.setChecked(link.enabled)
1282            self.__contextMenuTarget = link
1283            self.__linkMenu.popup(globalPos)
1284            return
1285
1286    def __onRenameAction(self):
1287        selected = self.selectedNodes()
1288        if len(selected) == 1:
1289            self.editNodeTitle(selected[0])
1290
1291    def __onHelpAction(self):
1292        """Help was requested for the selected widget.
1293        """
1294        nodes = self.selectedNodes()
1295        help_url = None
1296        if len(nodes) == 1:
1297            node = nodes[0]
1298            desc = node.description
1299
1300            help_url = "help://search?" + urlencode({"id": desc.id})
1301            self.__showHelpFor(help_url)
1302
1303    def __showHelpFor(self, help_url):
1304        """
1305        Show help for an "help" url.
1306        """
1307        # Notify the parent chain and let them respond
1308        ev = QWhatsThisClickedEvent(help_url)
1309        handled = QCoreApplication.sendEvent(self, ev)
1310
1311        if not handled:
1312            message_information(
1313                self.tr("Sorry there is no documentation available for "
1314                        "this widget."),
1315                parent=self)
1316
1317    def __toggleLinkEnabled(self, enabled):
1318        """Link enabled state was toggled in the context menu.
1319        """
1320        if self.__contextMenuTarget:
1321            link = self.__contextMenuTarget
1322            command = commands.SetAttrCommand(
1323                link, "enabled", enabled, name=self.tr("Set enabled"),
1324            )
1325            self.__undoStack.push(command)
1326
1327    def __linkRemove(self):
1328        """Remove link was requested from the context menu.
1329        """
1330        if self.__contextMenuTarget:
1331            self.removeLink(self.__contextMenuTarget)
1332
1333    def __linkReset(self):
1334        """Link reset from the context menu was requested.
1335        """
1336        if self.__contextMenuTarget:
1337            link = self.__contextMenuTarget
1338            action = interactions.EditNodeLinksAction(
1339                self, link.source_node, link.sink_node
1340            )
1341            action.edit_links()
1342
1343    def __startControlPointEdit(self, item):
1344        """Start a control point edit interaction for item.
1345        """
1346        if isinstance(item, items.ArrowAnnotation):
1347            handler = interactions.ResizeArrowAnnotation(self)
1348        elif isinstance(item, items.TextAnnotation):
1349            handler = interactions.ResizeTextAnnotation(self)
1350        else:
1351            log.warning("Unknown annotation item type %r" % item)
1352            return
1353
1354        handler.editItem(item)
1355        self._setUserInteractionHandler(handler)
1356
1357        log.info("Control point editing started (%r)." % item)
1358
1359    def __endControlPointEdit(self):
1360        """End the current control point edit interaction.
1361        """
1362        handler = self.__scene.user_interaction_handler
1363        if isinstance(handler, (interactions.ResizeArrowAnnotation,
1364                                interactions.ResizeTextAnnotation)) and \
1365                not handler.isFinished() and not handler.isCanceled():
1366            handler.commit()
1367            handler.end()
1368
1369            log.info("Control point editing finished.")
1370
1371
1372def geometry_from_annotation_item(item):
1373    if isinstance(item, items.ArrowAnnotation):
1374        line = item.line()
1375        p1 = item.mapToScene(line.p1())
1376        p2 = item.mapToScene(line.p2())
1377        return ((p1.x(), p1.y()), (p2.x(), p2.y()))
1378    elif isinstance(item, items.TextAnnotation):
1379        geom = item.geometry()
1380        return (geom.x(), geom.y(), geom.width(), geom.height())
1381
1382
1383def mouse_drag_distance(event, button=Qt.LeftButton):
1384    """Return the (manhattan) distance between the (screen position)
1385    when the `button` was pressed and the current mouse position.
1386
1387    """
1388    diff = (event.buttonDownScreenPos(button) - event.screenPos())
1389    return diff.manhattanLength()
1390
1391
1392def set_enabled_all(objects, enable):
1393    """Set enabled properties on all objects (QObjects with setEnabled).
1394    """
1395    for obj in objects:
1396        obj.setEnabled(enable)
Note: See TracBrowser for help on using the repository browser.