source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11192:d51939aa6f45

Revision 11192:d51939aa6f45, 24.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Changed annotation item selection and (control point) geometry editing.

Control point editing is now fixed to the items focus state.

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