source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11163:c2e366a1e9df

Revision 11163:c2e366a1e9df, 22.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Moved QuickMenu from 'canvas' to 'document' package.

Line 
1"""
2Scheme Edit widget.
3
4"""
5import logging
6from operator import attrgetter
7
8from PyQt4.QtGui import (
9    QWidget, QVBoxLayout, QInputDialog, QMenu, QAction, QUndoStack,
10    QGraphicsItem, QGraphicsObject, QPainter
11)
12
13from PyQt4.QtCore import Qt, QObject, QEvent, QSignalMapper, QRectF
14from PyQt4.QtCore import pyqtProperty as Property, pyqtSignal as Signal
15
16from ..scheme import scheme
17from ..canvas.scene import CanvasScene
18from ..canvas.view import CanvasView
19from ..canvas import items
20from . import interactions
21from . import commands
22from . import quickmenu
23
24
25log = logging.getLogger(__name__)
26
27
28# TODO: Should this be moved to CanvasScene?
29class GraphicsSceneFocusEventListener(QGraphicsObject):
30
31    itemFocusedIn = Signal(QGraphicsItem)
32    itemFocusedOut = Signal(QGraphicsItem)
33
34    def __init__(self, parent=None):
35        QGraphicsObject.__init__(self, parent)
36        self.setFlag(QGraphicsItem.ItemHasNoContents)
37
38    def sceneEventFilter(self, obj, event):
39        if event.type() == QEvent.FocusIn and \
40                obj.flags() & QGraphicsItem.ItemIsFocusable:
41            obj.focusInEvent(event)
42            if obj.hasFocus():
43                self.itemFocusedIn.emit(obj)
44            return True
45        elif event.type() == QEvent.FocusOut:
46            obj.focusOutEvent(event)
47            if not obj.hasFocus():
48                self.itemFocusedOut.emit(obj)
49            return True
50
51        return QGraphicsObject.sceneEventFilter(self, obj, event)
52
53    def boundingRect(self):
54        return QRectF()
55
56
57class SchemeEditWidget(QWidget):
58    undoAvailable = Signal(bool)
59    redoAvailable = Signal(bool)
60    modificationChanged = Signal(bool)
61    undoCommandAdded = Signal()
62    selectionChanged = Signal()
63
64    titleChanged = Signal(unicode)
65
66    def __init__(self, parent=None, ):
67        QWidget.__init__(self, parent)
68
69        self.__modified = False
70        self.__registry = None
71        self.__scheme = None
72        self.__undoStack = QUndoStack(self)
73        self.__undoStack.cleanChanged[bool].connect(self.__onCleanChanged)
74        self.__possibleMouseItemsMove = False
75        self.__itemsMoving = {}
76        self.__contextMenuTarget = None
77        self.__quickMenu = None
78
79        self.__editFinishedMapper = QSignalMapper(self)
80        self.__editFinishedMapper.mapped[QObject].connect(
81            self.__onEditingFinished
82        )
83
84        self.__annotationGeomChanged = QSignalMapper(self)
85
86        self.__setupUi()
87
88        self.__linkEnableAction = \
89            QAction(self.tr("Enabled"), self,
90                    objectName="link-enable-action",
91                    triggered=self.__toogleLinkEnabled,
92                    checkable=True,
93                    )
94
95        self.__linkRemoveAction = \
96            QAction(self.tr("Remove"), self,
97                    objectName="link-remove-action",
98                    triggered=self.__linkRemove,
99                    toolTip=self.tr("Remove link."),
100                    )
101
102        self.__linkResetAction = \
103            QAction(self.tr("Reset Signals"), self,
104                    objectName="link-reset-action",
105                    triggered=self.__linkReset,
106                    )
107
108        self.__linkMenu = QMenu(self)
109        self.__linkMenu.addAction(self.__linkEnableAction)
110        self.__linkMenu.addSeparator()
111        self.__linkMenu.addAction(self.__linkRemoveAction)
112        self.__linkMenu.addAction(self.__linkResetAction)
113
114    def __setupUi(self):
115        layout = QVBoxLayout()
116        layout.setContentsMargins(0, 0, 0, 0)
117        layout.setSpacing(0)
118
119        scene = CanvasScene()
120        view = CanvasView(scene)
121        view.setFrameStyle(CanvasView.NoFrame)
122        view.setRenderHint(QPainter.Antialiasing)
123        view.setContextMenuPolicy(Qt.CustomContextMenu)
124        view.customContextMenuRequested.connect(
125            self.__onCustomContextMenuRequested
126        )
127
128        self.__view = view
129        self.__scene = scene
130
131        self.__focusListener = GraphicsSceneFocusEventListener()
132        self.__focusListener.itemFocusedIn.connect(self.__onItemFocusedIn)
133        self.__focusListener.itemFocusedOut.connect(self.__onItemFocusedOut)
134        self.__scene.addItem(self.__focusListener)
135
136        self.__scene.selectionChanged.connect(
137            self.__onSelectionChanged
138        )
139
140        layout.addWidget(view)
141        self.setLayout(layout)
142
143    def isModified(self):
144        return not self.__undoStack.isClean()
145
146    def setModified(self, modified):
147        if modified and not self.isModified():
148            raise NotImplementedError
149        else:
150            self.__undoStack.setClean()
151
152    modified = Property(bool, fget=isModified, fset=setModified)
153
154    def undoStack(self):
155        """Return the undo stack.
156        """
157        return self.__undoStack
158
159    def setScheme(self, scheme):
160        if self.__scheme is not scheme:
161            if self.__scheme:
162                self.__scheme.title_changed.disconnect(self.titleChanged)
163
164            self.__scheme = scheme
165
166            if self.__scheme:
167                self.__scheme.title_changed.connect(self.titleChanged)
168                self.titleChanged.emit(scheme.title)
169
170            self.__annotationGeomChanged.deleteLater()
171            self.__annotationGeomChanged = QSignalMapper(self)
172
173            self.__undoStack.clear()
174
175            self.__focusListener.itemFocusedIn.disconnect(
176                self.__onItemFocusedIn
177            )
178            self.__focusListener.itemFocusedOut.disconnect(
179                self.__onItemFocusedOut
180            )
181
182            self.__scene.selectionChanged.disconnect(
183                self.__onSelectionChanged
184            )
185
186            self.__scene.clear()
187            self.__scene.removeEventFilter(self)
188            self.__scene.deleteLater()
189
190            self.__scene = CanvasScene()
191            self.__view.setScene(self.__scene)
192            self.__scene.installEventFilter(self)
193
194            self.__scene.set_registry(self.__registry)
195            self.__scene.set_scheme(scheme)
196
197            self.__scene.selectionChanged.connect(
198                self.__onSelectionChanged
199            )
200
201            self.__scene.node_item_activated.connect(
202                self.__onNodeActivate
203            )
204
205            self.__scene.annotation_added.connect(
206                self.__onAnnotationAdded
207            )
208
209            self.__scene.annotation_removed.connect(
210                self.__onAnnotationRemoved
211            )
212
213            self.__focusListener = GraphicsSceneFocusEventListener()
214            self.__focusListener.itemFocusedIn.connect(
215                self.__onItemFocusedIn
216            )
217            self.__focusListener.itemFocusedOut.connect(
218                self.__onItemFocusedOut
219            )
220            self.__scene.addItem(self.__focusListener)
221
222    def scheme(self):
223        return self.__scheme
224
225    def scene(self):
226        return self.__scene
227
228    def view(self):
229        return self.__view
230
231    def setRegistry(self, registry):
232        # Is this method necessary
233        self.__registry = registry
234        if self.__scene:
235            self.__scene.set_registry(registry)
236            self.__quickMenu = None
237
238    def quickMenu(self):
239        """Return a quick menu instance for quick new node creation.
240        """
241        if self.__quickMenu is None:
242            menu = quickmenu.QuickMenu(self)
243            if self.__registry is not None:
244                menu.setModel(self.__registry.model())
245            self.__quickMenu = menu
246        return self.__quickMenu
247
248    def addNode(self, node):
249        """Add a new node to the scheme.
250        """
251        command = commands.AddNodeCommand(self.__scheme, node)
252        self.__undoStack.push(command)
253
254    def createNewNode(self, description):
255        """Create a new SchemeNode add at it to the document at left of the
256        last added node.
257
258        """
259        node = scheme.SchemeNode(description)
260
261        if self.scheme().nodes:
262            x, y = self.scheme().nodes[-1].position
263            node.position = (x + 150, y)
264        else:
265            node.position = (150, 150)
266
267        self.addNode(node)
268
269    def removeNode(self, node):
270        command = commands.RemoveNodeCommand(self.__scheme, node)
271        self.__undoStack.push(command)
272
273    def renameNode(self, node, title):
274        command = commands.RenameNodeCommand(self.__scheme, node, title)
275        self.__undoStack.push(command)
276
277    def addLink(self, link):
278        command = commands.AddLinkCommand(self.__scheme, link)
279        self.__undoStack.push(command)
280
281    def removeLink(self, link):
282        command = commands.RemoveLinkCommand(self.__scheme, link)
283        self.__undoStack.push(command)
284
285    def addAnnotation(self, annotation):
286        command = commands.AddAnnotationCommand(self.__scheme, annotation)
287        self.__undoStack.push(command)
288
289    def removeAnnotation(self, annotation):
290        command = commands.RemoveAnnotationCommand(self.__scheme, annotation)
291        self.__undoStack.push(command)
292
293    def removeSelected(self):
294        selected = self.scene().selectedItems()
295        if not selected:
296            return
297
298        self.__undoStack.beginMacro(self.tr("Remove"))
299        for item in selected:
300            print item
301            if isinstance(item, items.NodeItem):
302                node = self.scene().node_for_item(item)
303                self.__undoStack.push(
304                    commands.RemoveNodeCommand(self.__scheme, node)
305                )
306            elif isinstance(item, items.annotationitem.Annotation):
307                annot = self.scene().annotation_for_item(item)
308                self.__undoStack.push(
309                    commands.RemoveAnnotationCommand(self.__scheme, annot)
310                )
311        self.__undoStack.endMacro()
312
313    def selectAll(self):
314        for item in self.__scene.items():
315            if item.flags() & QGraphicsItem.ItemIsSelectable:
316                item.setSelected(True)
317
318    def newArrowAnnotation(self):
319        handler = interactions.NewArrowAnnotation(self)
320        self.__scene.set_user_interaction_handler(handler)
321
322    def newTextAnnotation(self):
323        handler = interactions.NewTextAnnotation(self)
324        self.__scene.set_user_interaction_handler(handler)
325
326    def alignToGrid(self):
327        """Align nodes to a grid.
328        """
329        tile_size = 150
330        tiles = {}
331
332        nodes = sorted(self.scheme().nodes, key=attrgetter("position"))
333
334        if nodes:
335            self.__undoStack.beginMacro(self.tr("Align To Grid"))
336
337            for node in nodes:
338                x, y = node.position
339                x = int(round(float(x) / tile_size) * tile_size)
340                y = int(round(float(y) / tile_size) * tile_size)
341                while (x, y) in tiles:
342                    x += tile_size
343
344                self.__undoStack.push(
345                    commands.MoveNodeCommand(self.scheme(), node,
346                                             node.position, (x, y))
347                )
348
349                tiles[x, y] = node
350                self.__scene.item_for_node(node).setPos(x, y)
351
352            self.__undoStack.endMacro()
353
354    def selectedNodes(self):
355        return map(self.scene().node_for_item,
356                   self.scene().selected_node_items())
357
358    def openSelected(self):
359        selected = self.scene().selected_node_items()
360        for item in selected:
361            self.__onNodeActivate(item)
362
363    def editNodeTitle(self, node):
364        name, ok = QInputDialog.getText(
365                    self, self.tr("Rename"),
366                    unicode(self.tr("Enter a new name for the %r widget")) \
367                    % node.title,
368                    text=node.title
369                    )
370
371        if ok:
372            self.__undoStack.push(
373                commands.RenameNodeCommand(self.__scheme, node, node.title,
374                                           unicode(name))
375            )
376
377    def __onCleanChanged(self, clean):
378        if self.isWindowModified() != (not clean):
379            self.setWindowModified(not clean)
380            self.modificationChanged.emit(not clean)
381
382    def eventFilter(self, obj, event):
383        # Filter the scene's drag/drop events.
384        if obj is self.scene():
385            etype = event.type()
386            if  etype == QEvent.GraphicsSceneDragEnter or \
387                    etype == QEvent.GraphicsSceneDragMove:
388                mime_data = event.mimeData()
389                if mime_data.hasFormat(
390                        "application/vnv.orange-canvas.registry.qualified-name"
391                        ):
392                    event.acceptProposedAction()
393                return True
394            elif etype == QEvent.GraphicsSceneDrop:
395                data = event.mimeData()
396                qname = data.data(
397                    "application/vnv.orange-canvas.registry.qualified-name"
398                )
399                desc = self.__registry.widget(unicode(qname))
400                pos = event.scenePos()
401                node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
402                self.addNode(node)
403                return True
404
405            elif etype == QEvent.GraphicsSceneMousePress:
406                return self.sceneMousePressEvent(event)
407            elif etype == QEvent.GraphicsSceneMouseMove:
408                return self.sceneMouseMoveEvent(event)
409            elif etype == QEvent.GraphicsSceneMouseRelease:
410                return self.sceneMouseReleaseEvent(event)
411            elif etype == QEvent.GraphicsSceneMouseDoubleClick:
412                return self.sceneMouseDoubleClickEvent(event)
413            elif etype == QEvent.KeyRelease:
414                return self.sceneKeyPressEvent(event)
415            elif etype == QEvent.KeyRelease:
416                return self.sceneKeyReleaseEvent(event)
417            elif etype == QEvent.GraphicsSceneContextMenu:
418                return self.sceneContextMenuEvent(event)
419
420        return QWidget.eventFilter(self, obj, event)
421
422    def sceneMousePressEvent(self, event):
423        scene = self.__scene
424        if scene.user_interaction_handler:
425            return False
426
427        pos = event.scenePos()
428
429        anchor_item = scene.item_at(pos, items.NodeAnchorItem)
430        if anchor_item and event.button() == Qt.LeftButton:
431            # Start a new link starting at item
432            handler = interactions.NewLinkAction(self)
433            scene.set_user_interaction_handler(handler)
434
435            return handler.mousePressEvent(event)
436
437        annotation_item = scene.item_at(pos, (items.TextAnnotation,
438                                              items.ArrowAnnotation))
439
440        if annotation_item and event.button() == Qt.LeftButton and \
441                not event.modifiers() & Qt.ControlModifier:
442            if isinstance(annotation_item, items.TextAnnotation):
443                handler = interactions.ResizeTextAnnotation(self)
444            elif isinstance(annotation_item, items.ArrowAnnotation):
445                handler = interactions.ResizeArrowAnnotation(self)
446            else:
447                log.error("Unknown annotation item (%r).", annotation_item)
448                return False
449
450            scene.clearSelection()
451
452            scene.set_user_interaction_handler(handler)
453            return handler.mousePressEvent(event)
454
455        any_item = scene.item_at(pos)
456        if not any_item and event.button() == Qt.LeftButton:
457            # Start rect selection
458            handler = interactions.RectangleSelectionAction(self)
459            scene.set_user_interaction_handler(handler)
460            return handler.mousePressEvent(event)
461
462        if any_item and event.button() == Qt.LeftButton:
463            self.__possibleMouseItemsMove = True
464            self.__itemsMoving.clear()
465            self.__scene.node_item_position_changed.connect(
466                self.__onNodePositionChanged
467            )
468            self.__annotationGeomChanged.mapped[QObject].connect(
469                self.__onAnnotationGeometryChanged
470            )
471
472        return False
473
474    def sceneMouseMoveEvent(self, event):
475        scene = self.__scene
476        if scene.user_interaction_handler:
477            return False
478
479        return False
480
481    def sceneMouseReleaseEvent(self, event):
482        if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
483            self.__possibleMouseItemsMove = False
484            self.__scene.node_item_position_changed.disconnect(
485                self.__onNodePositionChanged
486            )
487            self.__annotationGeomChanged.mapped[QObject].disconnect(
488                self.__onAnnotationGeometryChanged
489            )
490
491            if self.__itemsMoving:
492                self.__scene.mouseReleaseEvent(event)
493                stack = self.undoStack()
494                stack.beginMacro(self.tr("Move"))
495                for scheme_item, (old, new) in self.__itemsMoving.items():
496                    if isinstance(scheme_item, scheme.SchemeNode):
497                        command = commands.MoveNodeCommand(
498                            self.scheme(), scheme_item, old, new
499                        )
500                    elif isinstance(scheme_item, scheme.BaseSchemeAnnotation):
501                        command = commands.AnnotationGeometryChange(
502                            self.scheme(), scheme_item, old, new
503                        )
504                    else:
505                        continue
506
507                    stack.push(command)
508                stack.endMacro()
509
510                self.__itemsMoving.clear()
511                return True
512        return False
513
514    def sceneMouseDoubleClickEvent(self, event):
515        scene = self.__scene
516        if scene.user_interaction_handler:
517            return False
518
519        item = scene.item_at(event.scenePos())
520        if not item:
521            # Double click on an empty spot
522            # Create a new node quick
523            action = interactions.NewNodeAction(self)
524            action.create_new(event)
525            event.accept()
526            return True
527
528        item = scene.item_at(event.scenePos(), items.LinkItem)
529        if item is not None:
530            link = self.scene().link_for_item(item)
531            action = interactions.EditNodeLinksAction(self, link.source_node,
532                                                      link.sink_node)
533            action.edit_links()
534            event.accept()
535            return True
536
537        return False
538
539    def sceneKeyPressEvent(self, event):
540        return False
541
542    def sceneKeyReleaseEvent(self, event):
543        return False
544
545    def sceneContextMenuEvent(self, event):
546        return False
547
548    def __onSelectionChanged(self):
549        pass
550
551    def __onNodeActivate(self, item):
552        node = self.__scene.node_for_item(item)
553        widget = self.scheme().widget_for_node[node]
554        widget.show()
555        widget.raise_()
556
557    def __onNodePositionChanged(self, item, pos):
558        node = self.__scene.node_for_item(item)
559        new = (pos.x(), pos.y())
560        if node not in self.__itemsMoving:
561            self.__itemsMoving[node] = (node.position, new)
562        else:
563            old, _ = self.__itemsMoving[node]
564            self.__itemsMoving[node] = (old, new)
565
566    def __onAnnotationGeometryChanged(self, item):
567        annot = self.scene().annotation_for_item(item)
568        if annot not in self.__itemsMoving:
569            self.__itemsMoving[annot] = (annot.geometry,
570                                         geometry_from_annotation_item(item))
571        else:
572            old, _ = self.__itemsMoving[annot]
573            self.__itemsMoving[annot] = (old,
574                                         geometry_from_annotation_item(item))
575
576    def __onAnnotationAdded(self, item):
577        item.setFlag(QGraphicsItem.ItemIsSelectable)
578        if isinstance(item, items.ArrowAnnotation):
579            pass
580        elif isinstance(item, items.TextAnnotation):
581            self.__editFinishedMapper.setMapping(item, item)
582            item.editingFinished.connect(
583                self.__editFinishedMapper.map
584            )
585        self.__annotationGeomChanged.setMapping(item, item)
586        item.geometryChanged.connect(
587            self.__annotationGeomChanged.map
588        )
589
590    def __onAnnotationRemoved(self, item):
591        if isinstance(item, items.ArrowAnnotation):
592            pass
593        elif isinstance(item, items.TextAnnotation):
594            item.editingFinished.disconnect(
595                self.__editFinishedMapper.map
596            )
597        self.__annotationGeomChanged.removeMappings(item)
598        item.geometryChanged.disconnect(
599            self.__annotationGeomChanged.map
600        )
601
602    def __onItemFocusedIn(self, item):
603        pass
604
605    def __onItemFocusedOut(self, item):
606        pass
607
608    def __onEditingFinished(self, item):
609        annot = self.__scene.annotation_for_item(item)
610        text = unicode(item.toPlainText())
611        if annot.text != text:
612            self.__undoStack.push(
613                commands.TextChangeCommand(self.scheme(), annot,
614                                           annot.text, text)
615            )
616
617    def __onCustomContextMenuRequested(self, pos):
618        scenePos = self.view().mapToScene(pos)
619        globalPos = self.view().mapToGlobal(pos)
620
621        item = self.scene().item_at(scenePos, items.NodeItem)
622        if item is not None:
623            self.window().widget_menu.popup(globalPos)
624            return
625
626        item = self.scene().item_at(scenePos, items.LinkItem)
627        if item is not None:
628            link = self.scene().link_for_item(item)
629            self.__linkEnableAction.setChecked(link.enabled)
630            self.__contextMenuTarget = link
631            self.__linkMenu.popup(globalPos)
632            return
633
634    def __toogleLinkEnabled(self, enabled):
635        if self.__contextMenuTarget:
636            link = self.__contextMenuTarget
637            command = commands.SetAttrCommand(
638                link, "enabled", enabled, name=self.tr("Set enabled"),
639            )
640            self.__undoStack.push(command)
641
642    def __linkRemove(self):
643        if self.__contextMenuTarget:
644            self.removeLink(self.__contextMenuTarget)
645
646    def __linkReset(self):
647        if self.__contextMenuTarget:
648            link = self.__contextMenuTarget
649            action = interactions.EditNodeLinksAction(
650                self, link.source_node, link.sink_node
651            )
652            action.edit_links()
653
654
655def geometry_from_annotation_item(item):
656    if isinstance(item, items.ArrowAnnotation):
657        line = item.line()
658        p1 = item.mapToScene(line.p1())
659        p2 = item.mapToScene(line.p2())
660        return ((p1.x(), p1.y()), (p2.x(), p2.y()))
661    elif isinstance(item, items.TextAnnotation):
662        geom = item.geometry()
663        return (geom.x(), geom.y(), geom.width(), geom.height())
Note: See TracBrowser for help on using the repository browser.