source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11208:b2091aba2a49

Revision 11208:b2091aba2a49, 39.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Moved widget actions definitions into SchemeEditWidget.

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