source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11343:84f579a58442

Revision 11343:84f579a58442, 48.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Fixed font size in the canvas scene.

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