source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11165:ab077db57cf9

Revision 11165:ab077db57cf9, 23.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added widget error/warning/info propagation to the canvas scene.

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                self.__scheme.node_added.disconnect(self.__onNodeAdded)
164                self.__scheme.node_removed.disconnect(self.__onNodeRemoved)
165
166            self.__scheme = scheme
167
168            if self.__scheme:
169                self.__scheme.title_changed.connect(self.titleChanged)
170                self.__scheme.node_added.connect(self.__onNodeAdded)
171                self.__scheme.node_removed.connect(self.__onNodeRemoved)
172                self.titleChanged.emit(scheme.title)
173
174            self.__annotationGeomChanged.deleteLater()
175            self.__annotationGeomChanged = QSignalMapper(self)
176
177            self.__undoStack.clear()
178
179            self.__focusListener.itemFocusedIn.disconnect(
180                self.__onItemFocusedIn
181            )
182            self.__focusListener.itemFocusedOut.disconnect(
183                self.__onItemFocusedOut
184            )
185
186            self.__scene.selectionChanged.disconnect(
187                self.__onSelectionChanged
188            )
189
190            self.__scene.clear()
191            self.__scene.removeEventFilter(self)
192            self.__scene.deleteLater()
193
194            self.__scene = CanvasScene()
195            self.__view.setScene(self.__scene)
196            self.__scene.installEventFilter(self)
197
198            self.__scene.set_registry(self.__registry)
199            self.__scene.set_scheme(scheme)
200
201            self.__scene.selectionChanged.connect(
202                self.__onSelectionChanged
203            )
204
205            self.__scene.node_item_activated.connect(
206                self.__onNodeActivate
207            )
208
209            self.__scene.annotation_added.connect(
210                self.__onAnnotationAdded
211            )
212
213            self.__scene.annotation_removed.connect(
214                self.__onAnnotationRemoved
215            )
216
217            self.__focusListener = GraphicsSceneFocusEventListener()
218            self.__focusListener.itemFocusedIn.connect(
219                self.__onItemFocusedIn
220            )
221            self.__focusListener.itemFocusedOut.connect(
222                self.__onItemFocusedOut
223            )
224            self.__scene.addItem(self.__focusListener)
225
226    def scheme(self):
227        return self.__scheme
228
229    def scene(self):
230        return self.__scene
231
232    def view(self):
233        return self.__view
234
235    def setRegistry(self, registry):
236        # Is this method necessary
237        self.__registry = registry
238        if self.__scene:
239            self.__scene.set_registry(registry)
240            self.__quickMenu = None
241
242    def quickMenu(self):
243        """Return a quick menu instance for quick new node creation.
244        """
245        if self.__quickMenu is None:
246            menu = quickmenu.QuickMenu(self)
247            if self.__registry is not None:
248                menu.setModel(self.__registry.model())
249            self.__quickMenu = menu
250        return self.__quickMenu
251
252    def addNode(self, node):
253        """Add a new node to the scheme.
254        """
255        command = commands.AddNodeCommand(self.__scheme, node)
256        self.__undoStack.push(command)
257
258    def createNewNode(self, description):
259        """Create a new SchemeNode add at it to the document at left of the
260        last added node.
261
262        """
263        node = scheme.SchemeNode(description)
264
265        if self.scheme().nodes:
266            x, y = self.scheme().nodes[-1].position
267            node.position = (x + 150, y)
268        else:
269            node.position = (150, 150)
270
271        self.addNode(node)
272
273    def removeNode(self, node):
274        command = commands.RemoveNodeCommand(self.__scheme, node)
275        self.__undoStack.push(command)
276
277    def renameNode(self, node, title):
278        command = commands.RenameNodeCommand(self.__scheme, node, title)
279        self.__undoStack.push(command)
280
281    def addLink(self, link):
282        command = commands.AddLinkCommand(self.__scheme, link)
283        self.__undoStack.push(command)
284
285    def removeLink(self, link):
286        command = commands.RemoveLinkCommand(self.__scheme, link)
287        self.__undoStack.push(command)
288
289    def addAnnotation(self, annotation):
290        command = commands.AddAnnotationCommand(self.__scheme, annotation)
291        self.__undoStack.push(command)
292
293    def removeAnnotation(self, annotation):
294        command = commands.RemoveAnnotationCommand(self.__scheme, annotation)
295        self.__undoStack.push(command)
296
297    def removeSelected(self):
298        selected = self.scene().selectedItems()
299        if not selected:
300            return
301
302        self.__undoStack.beginMacro(self.tr("Remove"))
303        for item in selected:
304            print item
305            if isinstance(item, items.NodeItem):
306                node = self.scene().node_for_item(item)
307                self.__undoStack.push(
308                    commands.RemoveNodeCommand(self.__scheme, node)
309                )
310            elif isinstance(item, items.annotationitem.Annotation):
311                annot = self.scene().annotation_for_item(item)
312                self.__undoStack.push(
313                    commands.RemoveAnnotationCommand(self.__scheme, annot)
314                )
315        self.__undoStack.endMacro()
316
317    def selectAll(self):
318        for item in self.__scene.items():
319            if item.flags() & QGraphicsItem.ItemIsSelectable:
320                item.setSelected(True)
321
322    def newArrowAnnotation(self):
323        handler = interactions.NewArrowAnnotation(self)
324        self.__scene.set_user_interaction_handler(handler)
325
326    def newTextAnnotation(self):
327        handler = interactions.NewTextAnnotation(self)
328        self.__scene.set_user_interaction_handler(handler)
329
330    def alignToGrid(self):
331        """Align nodes to a grid.
332        """
333        tile_size = 150
334        tiles = {}
335
336        nodes = sorted(self.scheme().nodes, key=attrgetter("position"))
337
338        if nodes:
339            self.__undoStack.beginMacro(self.tr("Align To Grid"))
340
341            for node in nodes:
342                x, y = node.position
343                x = int(round(float(x) / tile_size) * tile_size)
344                y = int(round(float(y) / tile_size) * tile_size)
345                while (x, y) in tiles:
346                    x += tile_size
347
348                self.__undoStack.push(
349                    commands.MoveNodeCommand(self.scheme(), node,
350                                             node.position, (x, y))
351                )
352
353                tiles[x, y] = node
354                self.__scene.item_for_node(node).setPos(x, y)
355
356            self.__undoStack.endMacro()
357
358    def selectedNodes(self):
359        return map(self.scene().node_for_item,
360                   self.scene().selected_node_items())
361
362    def openSelected(self):
363        selected = self.scene().selected_node_items()
364        for item in selected:
365            self.__onNodeActivate(item)
366
367    def editNodeTitle(self, node):
368        name, ok = QInputDialog.getText(
369                    self, self.tr("Rename"),
370                    unicode(self.tr("Enter a new name for the %r widget")) \
371                    % node.title,
372                    text=node.title
373                    )
374
375        if ok:
376            self.__undoStack.push(
377                commands.RenameNodeCommand(self.__scheme, node, node.title,
378                                           unicode(name))
379            )
380
381    def __onCleanChanged(self, clean):
382        if self.isWindowModified() != (not clean):
383            self.setWindowModified(not clean)
384            self.modificationChanged.emit(not clean)
385
386    def eventFilter(self, obj, event):
387        # Filter the scene's drag/drop events.
388        if obj is self.scene():
389            etype = event.type()
390            if  etype == QEvent.GraphicsSceneDragEnter or \
391                    etype == QEvent.GraphicsSceneDragMove:
392                mime_data = event.mimeData()
393                if mime_data.hasFormat(
394                        "application/vnv.orange-canvas.registry.qualified-name"
395                        ):
396                    event.acceptProposedAction()
397                return True
398            elif etype == QEvent.GraphicsSceneDrop:
399                data = event.mimeData()
400                qname = data.data(
401                    "application/vnv.orange-canvas.registry.qualified-name"
402                )
403                desc = self.__registry.widget(unicode(qname))
404                pos = event.scenePos()
405                node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
406                self.addNode(node)
407                return True
408
409            elif etype == QEvent.GraphicsSceneMousePress:
410                return self.sceneMousePressEvent(event)
411            elif etype == QEvent.GraphicsSceneMouseMove:
412                return self.sceneMouseMoveEvent(event)
413            elif etype == QEvent.GraphicsSceneMouseRelease:
414                return self.sceneMouseReleaseEvent(event)
415            elif etype == QEvent.GraphicsSceneMouseDoubleClick:
416                return self.sceneMouseDoubleClickEvent(event)
417            elif etype == QEvent.KeyRelease:
418                return self.sceneKeyPressEvent(event)
419            elif etype == QEvent.KeyRelease:
420                return self.sceneKeyReleaseEvent(event)
421            elif etype == QEvent.GraphicsSceneContextMenu:
422                return self.sceneContextMenuEvent(event)
423
424        return QWidget.eventFilter(self, obj, event)
425
426    def sceneMousePressEvent(self, event):
427        scene = self.__scene
428        if scene.user_interaction_handler:
429            return False
430
431        pos = event.scenePos()
432
433        anchor_item = scene.item_at(pos, items.NodeAnchorItem)
434        if anchor_item and event.button() == Qt.LeftButton:
435            # Start a new link starting at item
436            handler = interactions.NewLinkAction(self)
437            scene.set_user_interaction_handler(handler)
438
439            return handler.mousePressEvent(event)
440
441        annotation_item = scene.item_at(pos, (items.TextAnnotation,
442                                              items.ArrowAnnotation))
443
444        if annotation_item and event.button() == Qt.LeftButton and \
445                not event.modifiers() & Qt.ControlModifier:
446            if isinstance(annotation_item, items.TextAnnotation):
447                handler = interactions.ResizeTextAnnotation(self)
448            elif isinstance(annotation_item, items.ArrowAnnotation):
449                handler = interactions.ResizeArrowAnnotation(self)
450            else:
451                log.error("Unknown annotation item (%r).", annotation_item)
452                return False
453
454            scene.clearSelection()
455
456            scene.set_user_interaction_handler(handler)
457            return handler.mousePressEvent(event)
458
459        any_item = scene.item_at(pos)
460        if not any_item and event.button() == Qt.LeftButton:
461            # Start rect selection
462            handler = interactions.RectangleSelectionAction(self)
463            scene.set_user_interaction_handler(handler)
464            return handler.mousePressEvent(event)
465
466        if any_item and event.button() == Qt.LeftButton:
467            self.__possibleMouseItemsMove = True
468            self.__itemsMoving.clear()
469            self.__scene.node_item_position_changed.connect(
470                self.__onNodePositionChanged
471            )
472            self.__annotationGeomChanged.mapped[QObject].connect(
473                self.__onAnnotationGeometryChanged
474            )
475
476        return False
477
478    def sceneMouseMoveEvent(self, event):
479        scene = self.__scene
480        if scene.user_interaction_handler:
481            return False
482
483        return False
484
485    def sceneMouseReleaseEvent(self, event):
486        if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
487            self.__possibleMouseItemsMove = False
488            self.__scene.node_item_position_changed.disconnect(
489                self.__onNodePositionChanged
490            )
491            self.__annotationGeomChanged.mapped[QObject].disconnect(
492                self.__onAnnotationGeometryChanged
493            )
494
495            if self.__itemsMoving:
496                self.__scene.mouseReleaseEvent(event)
497                stack = self.undoStack()
498                stack.beginMacro(self.tr("Move"))
499                for scheme_item, (old, new) in self.__itemsMoving.items():
500                    if isinstance(scheme_item, scheme.SchemeNode):
501                        command = commands.MoveNodeCommand(
502                            self.scheme(), scheme_item, old, new
503                        )
504                    elif isinstance(scheme_item, scheme.BaseSchemeAnnotation):
505                        command = commands.AnnotationGeometryChange(
506                            self.scheme(), scheme_item, old, new
507                        )
508                    else:
509                        continue
510
511                    stack.push(command)
512                stack.endMacro()
513
514                self.__itemsMoving.clear()
515                return True
516        return False
517
518    def sceneMouseDoubleClickEvent(self, event):
519        scene = self.__scene
520        if scene.user_interaction_handler:
521            return False
522
523        item = scene.item_at(event.scenePos())
524        if not item:
525            # Double click on an empty spot
526            # Create a new node quick
527            action = interactions.NewNodeAction(self)
528            action.create_new(event)
529            event.accept()
530            return True
531
532        item = scene.item_at(event.scenePos(), items.LinkItem)
533        if item is not None:
534            link = self.scene().link_for_item(item)
535            action = interactions.EditNodeLinksAction(self, link.source_node,
536                                                      link.sink_node)
537            action.edit_links()
538            event.accept()
539            return True
540
541        return False
542
543    def sceneKeyPressEvent(self, event):
544        return False
545
546    def sceneKeyReleaseEvent(self, event):
547        return False
548
549    def sceneContextMenuEvent(self, event):
550        return False
551
552    def __onSelectionChanged(self):
553        pass
554
555    def __onNodeAdded(self, node):
556        widget = self.__scheme.widget_for_node[node]
557        widget.widgetStateChanged.connect(self.__onWidgetStateChanged)
558
559    def __onNodeRemoved(self, node):
560        widget = self.__scheme.widget_for_node[node]
561        widget.widgetStateChanged.disconnect(self.__onWidgetStateChanged)
562
563    def __onWidgetStateChanged(self, *args):
564        widget = self.sender()
565        self.scheme()
566        widget_to_node = dict(reversed(item) for item in \
567                              self.__scheme.widget_for_node.items())
568        node = widget_to_node[widget]
569        item = self.__scene.item_for_node(node)
570
571        info = widget.widgetStateToHtml(True, False, False)
572        warning = widget.widgetStateToHtml(False, True, False)
573        error = widget.widgetStateToHtml(False, False, True)
574
575        item.setInfoMessage(info or None)
576        item.setWarningMessage(warning or None)
577        item.setErrorMessage(error or None)
578
579    def __onNodeActivate(self, item):
580        node = self.__scene.node_for_item(item)
581        widget = self.scheme().widget_for_node[node]
582        widget.show()
583        widget.raise_()
584
585    def __onNodePositionChanged(self, item, pos):
586        node = self.__scene.node_for_item(item)
587        new = (pos.x(), pos.y())
588        if node not in self.__itemsMoving:
589            self.__itemsMoving[node] = (node.position, new)
590        else:
591            old, _ = self.__itemsMoving[node]
592            self.__itemsMoving[node] = (old, new)
593
594    def __onAnnotationGeometryChanged(self, item):
595        annot = self.scene().annotation_for_item(item)
596        if annot not in self.__itemsMoving:
597            self.__itemsMoving[annot] = (annot.geometry,
598                                         geometry_from_annotation_item(item))
599        else:
600            old, _ = self.__itemsMoving[annot]
601            self.__itemsMoving[annot] = (old,
602                                         geometry_from_annotation_item(item))
603
604    def __onAnnotationAdded(self, item):
605        item.setFlag(QGraphicsItem.ItemIsSelectable)
606        if isinstance(item, items.ArrowAnnotation):
607            pass
608        elif isinstance(item, items.TextAnnotation):
609            self.__editFinishedMapper.setMapping(item, item)
610            item.editingFinished.connect(
611                self.__editFinishedMapper.map
612            )
613        self.__annotationGeomChanged.setMapping(item, item)
614        item.geometryChanged.connect(
615            self.__annotationGeomChanged.map
616        )
617
618    def __onAnnotationRemoved(self, item):
619        if isinstance(item, items.ArrowAnnotation):
620            pass
621        elif isinstance(item, items.TextAnnotation):
622            item.editingFinished.disconnect(
623                self.__editFinishedMapper.map
624            )
625        self.__annotationGeomChanged.removeMappings(item)
626        item.geometryChanged.disconnect(
627            self.__annotationGeomChanged.map
628        )
629
630    def __onItemFocusedIn(self, item):
631        pass
632
633    def __onItemFocusedOut(self, item):
634        pass
635
636    def __onEditingFinished(self, item):
637        annot = self.__scene.annotation_for_item(item)
638        text = unicode(item.toPlainText())
639        if annot.text != text:
640            self.__undoStack.push(
641                commands.TextChangeCommand(self.scheme(), annot,
642                                           annot.text, text)
643            )
644
645    def __onCustomContextMenuRequested(self, pos):
646        scenePos = self.view().mapToScene(pos)
647        globalPos = self.view().mapToGlobal(pos)
648
649        item = self.scene().item_at(scenePos, items.NodeItem)
650        if item is not None:
651            self.window().widget_menu.popup(globalPos)
652            return
653
654        item = self.scene().item_at(scenePos, items.LinkItem)
655        if item is not None:
656            link = self.scene().link_for_item(item)
657            self.__linkEnableAction.setChecked(link.enabled)
658            self.__contextMenuTarget = link
659            self.__linkMenu.popup(globalPos)
660            return
661
662    def __toogleLinkEnabled(self, enabled):
663        if self.__contextMenuTarget:
664            link = self.__contextMenuTarget
665            command = commands.SetAttrCommand(
666                link, "enabled", enabled, name=self.tr("Set enabled"),
667            )
668            self.__undoStack.push(command)
669
670    def __linkRemove(self):
671        if self.__contextMenuTarget:
672            self.removeLink(self.__contextMenuTarget)
673
674    def __linkReset(self):
675        if self.__contextMenuTarget:
676            link = self.__contextMenuTarget
677            action = interactions.EditNodeLinksAction(
678                self, link.source_node, link.sink_node
679            )
680            action.edit_links()
681
682
683def geometry_from_annotation_item(item):
684    if isinstance(item, items.ArrowAnnotation):
685        line = item.line()
686        p1 = item.mapToScene(line.p1())
687        p2 = item.mapToScene(line.p2())
688        return ((p1.x(), p1.y()), (p2.x(), p2.y()))
689    elif isinstance(item, items.TextAnnotation):
690        geom = item.geometry()
691        return (geom.x(), geom.y(), geom.width(), geom.height())
Note: See TracBrowser for help on using the repository browser.