source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11210:bb3860029714

Revision 11210:bb3860029714, 25.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Fixed some errors caught by failing uniitests.

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