source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11159:07b6db67859c

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

Fixed selection when left clicking on a node.

Line 
1"""
2Canvas Graphics Scene
3
4"""
5
6import logging
7
8from PyQt4.QtGui import QGraphicsScene, QPainter, QBrush, \
9                        QGraphicsItem
10
11from PyQt4.QtCore import Qt, QPointF, QRectF, QSizeF, QLineF, \
12                         QBuffer, QSignalMapper
13
14from PyQt4.QtCore import pyqtSignal as Signal
15from PyQt4.QtCore import PYQT_VERSION_STR
16
17import sip
18
19from .. import scheme
20
21from . import items
22from . import quickmenu
23
24log = logging.getLogger(__name__)
25
26
27def typed_signal_mapper(pyType):
28    """Create a TypedSignalMapper class supporting signal
29    mapping for `pyType` (the default QSigalMapper only supports
30    int, string, QObject and QWidget (but not for instance QGraphicsItem).
31
32    """
33    class TypedSignalMapper(QSignalMapper):
34        pyMapped = Signal(pyType)
35
36        def __init__(self, parent=None):
37            QSignalMapper.__init__(self, parent)
38            self.__mapping = {}
39
40        def setPyMapping(self, sender, mapped):
41            self.__mapping[sender] = mapped
42            sender.destroyed.connect(self.removeMappings)
43
44        def removePyMappings(self, sender):
45            del self.__mapping[sender]
46            sender.destroyed.disconnect(self.removeMappings)
47
48        def pyMap(self, sender=None):
49            if sender is None:
50                sender = self.sender()
51            mapped = self.__mapping[sender]
52            self.pyMapped.emit(mapped)
53
54    return TypedSignalMapper
55
56
57NodeItemSignalMapper = typed_signal_mapper(items.NodeItem)
58
59
60class CanvasScene(QGraphicsScene):
61    """A Graphics Scene for displaying and editing an Orange Scheme.
62    """
63
64    node_item_added = Signal(items.NodeItem)
65    """An node item has been added to the scene"""
66
67    node_item_removed = Signal(items.LinkItem)
68    """An node item has been removed from the scene"""
69
70    node_item_position_changed = Signal(items.NodeItem, QPointF)
71    """The position of a node has changed"""
72
73    node_item_double_clicked = Signal(items.NodeItem)
74    """An node item has been double clicked"""
75
76    node_item_activated = Signal(items.NodeItem)
77    """An node item has been activated (clicked)"""
78
79    node_item_hovered = Signal(items.NodeItem)
80    """An node item has been hovered"""
81
82    link_item_added = Signal(items.LinkItem)
83    """A new link item has been added to the scene"""
84
85    link_item_removed = Signal(items.LinkItem)
86    """Link item has been removed"""
87
88    link_item_hovered = Signal(items.LinkItem)
89    """Link item has been hovered"""
90
91    annotation_added = Signal(items.annotationitem.Annotation)
92    """Annotation item has been added"""
93
94    annotation_removed = Signal(items.annotationitem.Annotation)
95    """Annotation item has been removed"""
96
97    def __init__(self, *args, **kwargs):
98        QGraphicsScene.__init__(self, *args, **kwargs)
99
100        self.scheme = None
101        self.registry = None
102
103        # All node items
104        self.__node_items = []
105        # Mapping from SchemeNodes to canvas items
106        self.__item_for_node = {}
107        # All link items
108        self.__link_items = []
109        # Mapping from SchemeLinks to canvas items.
110        self.__item_for_link = {}
111
112        # All annotation items
113        self.__annotation_items = []
114        # Mapping from SchemeAnnotations to canvas items.
115        self.__item_for_annotation = {}
116
117        # Is the scene editable
118        self.editable = True
119
120        self.user_interaction_handler = None
121        self._quick_menu = None
122
123        self.activated_mapper = NodeItemSignalMapper(self)
124        self.activated_mapper.pyMapped.connect(
125            self.node_item_activated
126        )
127
128        self.hovered_mapper = NodeItemSignalMapper(self)
129        self.hovered_mapper.pyMapped.connect(
130            self.node_item_hovered
131        )
132
133        self.position_change_mapper = NodeItemSignalMapper(self)
134        self.position_change_mapper.pyMapped.connect(
135            self._on_position_change
136        )
137
138        log.info("'%s' intitialized." % self)
139
140    def clear_scene(self):
141        self.scheme = None
142        self.__node_items = []
143        self.__item_for_node = {}
144        self.__link_items = []
145        self.__item_for_link = {}
146
147        self.user_interaction_handler = None
148        self._quick_menu = None
149
150        self.clear()
151        log.info("'%s' cleared." % self)
152
153    def set_scheme(self, scheme):
154        """Set the scheme to display and edit. Populates the scene
155        with nodes and links already in the scheme.
156
157        """
158        if self.scheme is not None:
159            # Clear the old scheme
160            self.scheme.node_added.disconnect(self.add_node)
161            self.scheme.node_removed.disconnect(self.remove_node)
162
163            self.scheme.link_added.disconnect(self.add_link)
164            self.scheme.link_removed.disconnect(self.remove_link)
165
166            self.scheme.annotation_added.disconnect(self.add_annotation)
167            self.scheme.annotation_removed.disconnect(self.remove_annotation)
168
169            self.scheme.node_state_changed.disconnect(
170                self.on_widget_state_change
171            )
172            self.scheme.channel_state_changed.disconnect(
173                self.on_link_state_change
174            )
175
176            self.clear_scene()
177
178        log.info("Setting scheme '%s' on '%s'" % (scheme, self))
179
180        self.scheme = scheme
181        if self.scheme is not None:
182            self.scheme.node_added.connect(self.add_node)
183            self.scheme.node_removed.connect(self.remove_node)
184
185            self.scheme.link_added.connect(self.add_link)
186            self.scheme.link_removed.connect(self.remove_link)
187
188            self.scheme.annotation_added.connect(self.add_annotation)
189            self.scheme.annotation_removed.connect(self.remove_annotation)
190
191            self.scheme.node_state_changed.connect(
192                self.on_widget_state_change
193            )
194            self.scheme.channel_state_changed.connect(
195                self.on_link_state_change
196            )
197
198            self.scheme.topology_changed.connect(self.on_scheme_change)
199
200        for node in scheme.nodes:
201            self.add_node(node)
202
203        for link in scheme.links:
204            self.add_link(link)
205
206        for annot in scheme.annotations:
207            self.add_annotation(annot)
208
209    def set_registry(self, registry):
210        """Set the widget registry.
211        """
212        log.info("Setting registry '%s on '%s'." % (registry, self))
213        self.registry = registry
214        self._quick_menu = None
215
216    def quick_menu(self):
217        if self._quick_menu is None:
218            menu = quickmenu.QuickMenu()
219            if self.registry:
220                menu.setModel(self.registry.model())
221            self._quick_menu = menu
222        return self._quick_menu
223
224    def add_node_item(self, item):
225        """Add a :class:`NodeItem` instance to the scene.
226        """
227        if item in self.__node_items:
228            raise ValueError("%r is already in the scene." % item)
229
230        if item.pos().isNull():
231            if self.__node_items:
232                pos = self.__node_items[-1].pos() + QPointF(150, 0)
233            else:
234                pos = QPointF(150, 150)
235
236            item.setPos(pos)
237
238        # Set signal mappings
239        self.activated_mapper.setPyMapping(item, item)
240        item.activated.connect(self.activated_mapper.pyMap)
241
242        self.hovered_mapper.setPyMapping(item, item)
243        item.hovered.connect(self.hovered_mapper.pyMap)
244
245        self.position_change_mapper.setPyMapping(item, item)
246        item.positionChanged.connect(self.position_change_mapper.pyMap)
247
248        self.addItem(item)
249
250        self.__node_items.append(item)
251
252        self.node_item_added.emit(item)
253
254        log.info("Added item '%s' to '%s'" % (item, self))
255        return item
256
257    def add_node(self, node):
258        """Add and return a default constructed `NodeItem` for a
259        `SchemeNode` instance. If the node is already in the scene
260        do nothing and just return its item.
261
262        """
263        if node in self.__item_for_node:
264            # Already added
265            return self.__item_for_node[node]
266
267        item = self.new_node_item(node.description)
268
269        if node.position:
270            pos = QPointF(*node.position)
271            item.setPos(pos)
272
273        self.__item_for_node[node] = item
274
275        node.position_changed.connect(self.__on_node_pos_changed)
276        node.title_changed.connect(item.setTitle)
277        node.progress_changed.connect(item.setProgress)
278        node.processing_state_changed.connect(item.setProcessingState)
279        return self.add_node_item(item)
280
281    def new_node_item(self, widget_desc, category_desc=None):
282        """Construct an new `NodeItem` from a `WidgetDescription`.
283        Optionally also set `CategoryDescription`.
284
285        """
286        item = items.NodeItem()
287        item.setWidgetDescription(widget_desc)
288
289        if category_desc is None and self.registry and widget_desc.category:
290            category_desc = self.registry.category(widget_desc.category)
291
292        if category_desc is None and self.registry is not None:
293            try:
294                category_desc = self.registry.category(widget_desc.category)
295            except KeyError:
296                pass
297
298        if category_desc is not None:
299            item.setWidgetCategory(category_desc)
300
301        return item
302
303    def remove_node_item(self, item):
304        """Remove `item` (:class:`NodeItem`) from the scene.
305        """
306        self.activated_mapper.removePyMappings(item)
307        self.hovered_mapper.removePyMappings(item)
308
309        item.hide()
310        self.removeItem(item)
311        self.__node_items.remove(item)
312
313        self.node_item_removed.emit(item)
314
315        log.info("Removed item '%s' from '%s'" % (item, self))
316
317    def remove_node(self, node):
318        """Remove the `NodeItem` instance that was previously constructed for
319        a `SchemeNode` node using the `add_node` method.
320
321        """
322        item = self.__item_for_node.pop(node)
323
324        node.position_changed.disconnect(self.__on_node_pos_changed)
325        node.title_changed.disconnect(item.setTitle)
326        node.progress_changed.disconnect(item.setProgress)
327        node.processing_state_changed.disconnect(item.setProcessingState)
328
329        self.remove_node_item(item)
330
331    def node_items(self):
332        """Return all :class:`NodeItem` instances in the scene.
333        """
334        return list(self.__node_items)
335
336    def add_link_item(self, item):
337        """Add a link (:class:`LinkItem`)to the scene.
338        """
339        if item.scene() is not self:
340            self.addItem(item)
341
342        self.__link_items.append(item)
343
344        self.link_item_added.emit(item)
345
346        log.info("Added link %r -> %r to '%s'" % \
347                 (item.sourceItem.title, item.sinkItem.title, self))
348
349        return item
350
351    def add_link(self, scheme_link):
352        """Create and add a `LinkItem` instance for a `SchemeLink`
353        instance. If the link is already in the scene do nothing
354        and just return its `LinkItem`.
355
356        """
357        if scheme_link in self.__item_for_link:
358            return self.__item_for_link[scheme_link]
359
360        source = self.__item_for_node[scheme_link.source_node]
361        sink = self.__item_for_node[scheme_link.sink_node]
362
363        item = self.new_link_item(source, scheme_link.source_channel,
364                                  sink, scheme_link.sink_channel)
365
366        item.setEnabled(scheme_link.enabled)
367
368        scheme_link.enabled_changed.connect(item.setEnabled)
369
370        self.add_link_item(item)
371        self.__item_for_link[scheme_link] = item
372        return item
373
374    def new_link_item(self, source_item, source_channel,
375                      sink_item, sink_channel):
376        """Construct and return a new `LinkItem`
377        """
378        item = items.LinkItem()
379        item.setSourceItem(source_item)
380        item.setSinkItem(sink_item)
381        return item
382
383    def remove_link_item(self, item):
384        """Remove a link (:class:`LinkItem`) from the scene.
385        """
386        self.__link_items.remove(item)
387
388        # Remove the anchor points.
389        item.removeLink()
390        self.removeItem(item)
391        self.link_item_removed.emit(item)
392
393        log.info("Removed link '%s' from '%s'" % (item, self))
394
395        return item
396
397    def remove_link(self, scheme_link):
398        """ Remove a `LinkItem` instance that was previously constructed for
399        a `SchemeLink` node using the `add_link` method.
400
401        """
402        item = self.__item_for_link.pop(scheme_link)
403        scheme_link.enabled_changed.disconnect(item.setEnabled)
404        self.remove_link_item(item)
405
406    def link_items(self):
407        """Return all :class:`LinkItems` in the scene.
408
409        """
410        return list(self.__link_items)
411
412    def add_annotation_item(self, annotation):
413        """Add an `Annotation` item to the scene.
414
415        """
416        self.__annotation_items.append(annotation)
417        self.addItem(annotation)
418        self.annotation_added.emit(annotation)
419        return annotation
420
421    def add_annotation(self, scheme_annot):
422        """Create a new item for :class:`SchemeAnnotation` and add it
423        to the scene. If the `scheme_annot` is already in the scene do
424        nothing and just return its item.
425
426        """
427        if scheme_annot in self.__item_for_annotation:
428            # Already added
429            return self.__item_for_annotation[scheme_annot]
430
431        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
432            item = items.TextAnnotation()
433            item.setPlainText(scheme_annot.text)
434            x, y, w, h = scheme_annot.rect
435            item.setPos(x, y)
436            item.resize(w, h)
437            item.setTextInteractionFlags(Qt.TextEditorInteraction)
438            scheme_annot.text_changed.connect(item.setPlainText)
439
440        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
441            item = items.ArrowAnnotation()
442            start, end = scheme_annot.start_pos, scheme_annot.end_pos
443            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
444
445        scheme_annot.geometry_changed.connect(
446            self.__on_scheme_annot_geometry_change
447        )
448
449        self.add_annotation_item(item)
450        self.__item_for_annotation[scheme_annot] = item
451
452        return item
453
454    def remove_annotation_item(self, annotation):
455        """Remove an `Annotation` item from the scene.
456
457        """
458        self.__annotation_items.remove(annotation)
459        self.removeItem(annotation)
460        self.annotation_removed.emit(annotation)
461
462    def remove_annotation(self, scheme_annotation):
463        item = self.__item_for_annotation.pop(scheme_annotation)
464
465        scheme_annotation.geometry_changed.disconnect(
466            self.__on_scheme_annot_geometry_change
467        )
468
469        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
470            scheme_annotation.text_changed.disconnect(
471                item.setPlainText
472            )
473
474        self.remove_annotation_item(item)
475
476    def annotation_items(self):
477        """Return all `Annotation` items in the scene.
478
479        """
480        return self.__annotation_items
481
482    def item_for_annotation(self, scheme_annotation):
483        return self.__item_for_annotation[scheme_annotation]
484
485    def annotation_for_item(self, item):
486        rev = dict(reversed(item) \
487                   for item in self.__item_for_annotation.items())
488        return rev[item]
489
490    def commit_scheme_node(self, node):
491        """Commit the `node` into the scheme.
492        """
493        if not self.editable:
494            raise Exception("Scheme not editable.")
495
496        if node not in self.__item_for_node:
497            raise ValueError("No 'NodeItem' for node.")
498
499        item = self.__item_for_node[node]
500
501        try:
502            self.scheme.add_node(node)
503        except Exception:
504            log.error("An unexpected error occurred while commiting node '%s'",
505                      node, exc_info=True)
506            # Cleanup (remove the node item)
507            self.remove_node_item(item)
508            raise
509
510        log.info("Commited node '%s' from '%s' to '%s'" % \
511                 (node, self, self.scheme))
512
513    def commit_scheme_link(self, link):
514        """Commit a scheme link.
515        """
516        if not self.editable:
517            raise Exception("Scheme not editable")
518
519        if link not in self.__item_for_link:
520            raise ValueError("No 'LinkItem' for link.")
521
522        self.scheme.add_link(link)
523        log.info("Commited link '%s' from '%s' to '%s'" % \
524                 (link, self, self.scheme))
525
526    def node_for_item(self, item):
527        """Return the `SchemeNode` for the `item`.
528        """
529        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
530        return rev[item]
531
532    def item_for_node(self, node):
533        """Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
534        """
535        return self.__item_for_node[node]
536
537    def link_for_item(self, item):
538        """Return the `SchemeLink for `item` (:class:`LinkItem`).
539        """
540        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
541        return rev[item]
542
543    def item_for_link(self, link):
544        """Return the :class:`LinkItem` for a :class:`SchemeLink`
545        """
546        return self.__item_for_link[link]
547
548    def selected_node_items(self):
549        """Return the selected :class:`NodeItem`'s.
550        """
551        return [item for item in self.__node_items if item.isSelected()]
552
553    def selected_annotation_items(self):
554        """Return the selected :class:`Annotation`'s
555        """
556        return [item for item in self.__annotation_items if item.isSelected()]
557
558    def on_widget_state_change(self, widget, state):
559        pass
560
561    def on_link_state_change(self, link, state):
562        pass
563
564    def on_scheme_change(self, ):
565        pass
566
567    def _on_position_change(self, item):
568        self.node_item_position_changed.emit(item, item.pos())
569
570    def __on_node_pos_changed(self, pos):
571        node = self.sender()
572        item = self.__item_for_node[node]
573        item.setPos(*pos)
574
575    def __on_scheme_annot_geometry_change(self):
576        annot = self.sender()
577        item = self.__item_for_annotation[annot]
578        if isinstance(annot, scheme.SchemeTextAnnotation):
579            item.setGeometry(*annot.rect)
580        elif isinstance(annot, scheme.SchemeArrowAnnotation):
581            p1 = item.mapFromScene(QPointF(*annot.start_pos))
582            p2 = item.mapFromScene(QPointF(*annot.end_pos))
583            item.setLine(QLineF(p1, p2))
584        else:
585            pass
586
587    def item_at(self, pos, type_or_tuple=None):
588        rect = QRectF(pos - QPointF(1.5, 1.5), QSizeF(3, 3))
589        items = self.items(rect)
590        if type_or_tuple:
591            items = [i for i in items if isinstance(i, type_or_tuple)]
592
593        return items[0] if items else None
594
595    if PYQT_VERSION_STR < "4.9":
596        # For QGraphicsObject subclasses items/itemAt return a QGraphicsItem
597        # wrapper instance and not the actual class instance.
598        def itemAt(self, *args, **kwargs):
599            item = QGraphicsScene.itemAt(self, *args, **kwargs)
600            if type(item) is QGraphicsItem:
601                for obj in self.__node_items:
602                    if sip.unwrapinstance(sip.cast(obj, QGraphicsItem)) == \
603                            sip.unwrapinstance(item):
604                        return obj
605            return item
606
607        def items(self, *args, **kwargs):
608            items = QGraphicsScene.items(self, *args, **kwargs)
609            instance_map = \
610                dict((sip.unwrapinstance(sip.cast(obj, QGraphicsItem)), obj)
611                     for obj in self.__node_items)
612            for i, item in enumerate(items):
613                if type(item) is QGraphicsItem and \
614                        sip.unwrapinstance(item) in instance_map:
615                        items[i] = instance_map[sip.unwrapinstance(item)]
616            return items
617
618        def _unwrap_node(self, node):
619            unwrapped = sip.unwrapinstance(node)
620            for item in self.__node_items:
621                unwrapped_item = \
622                    sip.unwrapinstance(sip.cast(item, QGraphicsItem))
623                if unwrapped_item == unwrapped:
624                    return item
625            else:
626                raise ValueError
627
628    def mousePressEvent(self, event):
629        if self.user_interaction_handler and \
630                self.user_interaction_handler.mousePressEvent(event):
631            return
632
633        # Right (context) click on the node item. If the widget is not
634        # in the current selection then select the widget (only the widget).
635        # Else simply return and let customContextMenuReqested signal
636        # handle it
637        shape_item = self.item_at(event.scenePos(), items.NodeItem)
638        if shape_item and event.button() == Qt.RightButton and \
639                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
640            if not shape_item.isSelected():
641                self.clearSelection()
642                shape_item.setSelected(True)
643
644        return QGraphicsScene.mousePressEvent(self, event)
645
646    def mouseMoveEvent(self, event):
647        if self.user_interaction_handler and \
648                self.user_interaction_handler.mouseMoveEvent(event):
649            return
650
651        return QGraphicsScene.mouseMoveEvent(self, event)
652
653    def mouseReleaseEvent(self, event):
654        if self.user_interaction_handler and \
655                self.user_interaction_handler.mouseReleaseEvent(event):
656            return
657        return QGraphicsScene.mouseReleaseEvent(self, event)
658
659    def mouseDoubleClickEvent(self, event):
660        if self.user_interaction_handler and \
661                self.user_interaction_handler.mouseDoubleClickEvent(event):
662            return
663
664        return QGraphicsScene.mouseDoubleClickEvent(self, event)
665
666    def keyPressEvent(self, event):
667        if self.user_interaction_handler and \
668                self.user_interaction_handler.keyPressEvent(event):
669            return
670        return QGraphicsScene.keyPressEvent(self, event)
671
672    def keyReleaseEvent(self, event):
673        if self.user_interaction_handler and \
674                self.user_interaction_handler.keyReleaseEvent(event):
675            return
676        return QGraphicsScene.keyReleaseEvent(self, event)
677
678    def set_user_interaction_handler(self, handler):
679        if self.user_interaction_handler and \
680                not self.user_interaction_handler.finished:
681            self.user_interaction_handler.cancel()
682
683        log.info("Setting interaction '%s' to '%s'" % (handler, self))
684
685        self.user_interaction_handler = handler
686        if handler:
687            handler.start()
688
689    def __str__(self):
690        return "%s(objectName=%r, ...)" % \
691                (type(self).__name__, str(self.objectName()))
692
693
694def grab_svg(scene):
695    """Return a SVG rendering of the scene contents.
696    """
697    from PyQt4.QtSvg import QSvgGenerator
698    svg_buffer = QBuffer()
699    gen = QSvgGenerator()
700    gen.setOutputDevice(svg_buffer)
701
702    items_rect = scene.itemsBoundingRect().adjusted(-10, -10, 10, 10)
703
704    if items_rect.isNull():
705        items_rect = QRectF(0, 0, 10, 10)
706
707    width, height = items_rect.width(), items_rect.height()
708    rect_ratio = float(width) / height
709
710    # Keep a fixed aspect ratio.
711    aspect_ratio = 1.618
712    if rect_ratio > aspect_ratio:
713        height = int(height * rect_ratio / aspect_ratio)
714    else:
715        width = int(width * aspect_ratio / rect_ratio)
716
717    target_rect = QRectF(0, 0, width, height)
718    source_rect = QRectF(0, 0, width, height)
719    source_rect.moveCenter(items_rect.center())
720
721    gen.setSize(target_rect.size().toSize())
722    gen.setViewBox(target_rect)
723
724    painter = QPainter(gen)
725
726    # Draw background.
727    painter.setBrush(QBrush(Qt.white))
728    painter.drawRect(target_rect)
729
730    # Render the scene
731    scene.render(painter, target_rect, source_rect)
732    painter.end()
733
734    return unicode(svg_buffer.buffer())
Note: See TracBrowser for help on using the repository browser.