source: orange/Orange/OrangeCanvas/document/schemeedit.py @ 11159:07b6db67859c

Revision 11159:07b6db67859c, 21.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Fixed selection when left clicking on a node.

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        if annotation_item and event.button() == Qt.LeftButton and \
426                not event.modifiers() & Qt.ControlModifier:
427            # TODO: Start a text rect edit.
428            pass
429
430        any_item = scene.item_at(pos)
431        if not any_item and event.button() == Qt.LeftButton:
432            # Start rect selection
433            handler = interactions.RectangleSelectionAction(self)
434            scene.set_user_interaction_handler(handler)
435            return handler.mousePressEvent(event)
436
437        if any_item and event.button() == Qt.LeftButton:
438            self.__possibleMouseItemsMove = True
439            self.__itemsMoving.clear()
440            self.__scene.node_item_position_changed.connect(
441                self.__onNodePositionChanged
442            )
443            self.__annotationGeomChanged.mapped[QObject].connect(
444                self.__onAnnotationGeometryChanged
445            )
446
447        return False
448
449    def sceneMouseMoveEvent(self, event):
450        scene = self.__scene
451        if scene.user_interaction_handler:
452            return False
453
454        return False
455
456    def sceneMouseReleaseEvent(self, event):
457        if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
458            self.__possibleMouseItemsMove = False
459            self.__scene.node_item_position_changed.disconnect(
460                self.__onNodePositionChanged
461            )
462            self.__annotationGeomChanged.mapped[QObject].disconnect(
463                self.__onAnnotationGeometryChanged
464            )
465
466            if self.__itemsMoving:
467                self.__scene.mouseReleaseEvent(event)
468                stack = self.undoStack()
469                stack.beginMacro(self.tr("Move"))
470                for scheme_item, (old, new) in self.__itemsMoving.items():
471                    if isinstance(scheme_item, scheme.SchemeNode):
472                        command = commands.MoveNodeCommand(
473                            self.scheme(), scheme_item, old, new
474                        )
475                    elif isinstance(scheme_item, scheme.BaseSchemeAnnotation):
476                        command = commands.AnnotationGeometryChange(
477                            self.scheme(), scheme_item, old, new
478                        )
479                    else:
480                        continue
481
482                    stack.push(command)
483                stack.endMacro()
484
485                self.__itemsMoving.clear()
486                return True
487        return False
488
489    def sceneMouseDoubleClickEvent(self, event):
490        scene = self.__scene
491        if scene.user_interaction_handler:
492            return False
493
494        item = scene.item_at(event.scenePos())
495        if not item:
496            # Double click on an empty spot
497            # Create a new node quick
498            action = interactions.NewNodeAction(self)
499            action.create_new(event)
500            event.accept()
501            return True
502
503        item = scene.item_at(event.scenePos(), items.LinkItem)
504        if item is not None:
505            link = self.scene().link_for_item(item)
506            action = interactions.EditNodeLinksAction(self, link.source_node,
507                                                      link.sink_node)
508            action.edit_links()
509            event.accept()
510            return True
511
512        return False
513
514    def sceneKeyPressEvent(self, event):
515        return False
516
517    def sceneKeyReleaseEvent(self, event):
518        return False
519
520    def sceneContextMenuEvent(self, event):
521        return False
522
523    def __onSelectionChanged(self):
524        pass
525
526    def __onNodeActivate(self, item):
527        node = self.__scene.node_for_item(item)
528        widget = self.scheme().widget_for_node[node]
529        widget.show()
530        widget.raise_()
531
532    def __onNodePositionChanged(self, item, pos):
533        node = self.__scene.node_for_item(item)
534        new = (pos.x(), pos.y())
535        if node not in self.__itemsMoving:
536            self.__itemsMoving[node] = (node.position, new)
537        else:
538            old, _ = self.__itemsMoving[node]
539            self.__itemsMoving[node] = (old, new)
540
541    def __onAnnotationGeometryChanged(self, item):
542        annot = self.scene().annotation_for_item(item)
543        if annot not in self.__itemsMoving:
544            self.__itemsMoving[annot] = (annot.geometry,
545                                         geometry_from_annotation_item(item))
546        else:
547            old, _ = self.__itemsMoving[annot]
548            self.__itemsMoving[annot] = (old,
549                                         geometry_from_annotation_item(item))
550
551    def __onAnnotationAdded(self, item):
552        item.setFlag(QGraphicsItem.ItemIsSelectable)
553        if isinstance(item, items.ArrowAnnotation):
554            pass
555        elif isinstance(item, items.TextAnnotation):
556            self.__editFinishedMapper.setMapping(item, item)
557            item.editingFinished.connect(
558                self.__editFinishedMapper.map
559            )
560        self.__annotationGeomChanged.setMapping(item, item)
561        item.geometryChanged.connect(
562            self.__annotationGeomChanged.map
563        )
564
565    def __onAnnotationRemoved(self, item):
566        if isinstance(item, items.ArrowAnnotation):
567            pass
568        elif isinstance(item, items.TextAnnotation):
569            item.editingFinished.disconnect(
570                self.__editFinishedMapper.map
571            )
572        self.__annotationGeomChanged.removeMappings(item)
573        item.geometryChanged.disconnect(
574            self.__annotationGeomChanged.map
575        )
576
577    def __onItemFocusedIn(self, item):
578        pass
579
580    def __onItemFocusedOut(self, item):
581        pass
582
583    def __onEditingFinished(self, item):
584        annot = self.__scene.annotation_for_item(item)
585        text = unicode(item.toPlainText())
586        if annot.text != text:
587            self.__undoStack.push(
588                commands.TextChangeCommand(self.scheme(), annot,
589                                           annot.text, text)
590            )
591
592    def __onCustomContextMenuRequested(self, pos):
593        scenePos = self.view().mapToScene(pos)
594        globalPos = self.view().mapToGlobal(pos)
595
596        item = self.scene().item_at(scenePos, items.NodeItem)
597        if item is not None:
598            self.window().widget_menu.popup(globalPos)
599            return
600
601        item = self.scene().item_at(scenePos, items.LinkItem)
602        if item is not None:
603            link = self.scene().link_for_item(item)
604            self.__linkEnableAction.setChecked(link.enabled)
605            self.__contextMenuTarget = link
606            self.__linkMenu.popup(globalPos)
607            return
608
609    def __toogleLinkEnabled(self, enabled):
610        if self.__contextMenuTarget:
611            link = self.__contextMenuTarget
612            command = commands.SetAttrCommand(
613                link, "enabled", enabled, name=self.tr("Set enabled"),
614            )
615            self.__undoStack.push(command)
616
617    def __linkRemove(self):
618        if self.__contextMenuTarget:
619            self.removeLink(self.__contextMenuTarget)
620
621    def __linkReset(self):
622        if self.__contextMenuTarget:
623            link = self.__contextMenuTarget
624            action = interactions.EditNodeLinksAction(
625                self, link.source_node, link.sink_node
626            )
627            action.edit_links()
628
629
630def geometry_from_annotation_item(item):
631    if isinstance(item, items.ArrowAnnotation):
632        line = item.line()
633        p1 = item.mapToScene(line.p1())
634        p2 = item.mapToScene(line.p2())
635        return ((p1.x(), p1.y()), (p2.x(), p2.y()))
636    elif isinstance(item, items.TextAnnotation):
637        geom = item.geometry()
638        return (geom.x(), geom.y(), geom.width(), geom.height())
Note: See TracBrowser for help on using the repository browser.