source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11163:c2e366a1e9df

Revision 11163:c2e366a1e9df, 22.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Moved QuickMenu from 'canvas' to 'document' package.

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