source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11169:c0f13424584c

Revision 11169:c0f13424584c, 22.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Fixed connections in TypedSignalMapper and simplified PyQt 4.8 compatibility.

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