source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11162:cc402e04763c

Revision 11162:cc402e04763c, 21.7 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added annotation geometry edit interactions in SchemeEditWidget.

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