source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11241:72cdee438307

Revision 11241:72cdee438307, 25.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Fixed scene hit testing for mouse press events.

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