source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11278:c70c26ac6a25

Revision 11278:c70c26ac6a25, 26.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Initialize created NodeItems.

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        item.setTitle(node.title)
266        item.setProgress(node.progress)
267        item.setProcessingState(node.processing_state)
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
276        return self.add_node_item(item)
277
278    def new_node_item(self, widget_desc, category_desc=None):
279        """Construct an new `NodeItem` from a `WidgetDescription`.
280        Optionally also set `CategoryDescription`.
281
282        """
283        item = items.NodeItem()
284        item.setWidgetDescription(widget_desc)
285
286        if category_desc is None and self.registry and widget_desc.category:
287            category_desc = self.registry.category(widget_desc.category)
288
289        if category_desc is None and self.registry is not None:
290            try:
291                category_desc = self.registry.category(widget_desc.category)
292            except KeyError:
293                pass
294
295        if category_desc is not None:
296            item.setWidgetCategory(category_desc)
297
298        return item
299
300    def remove_node_item(self, item):
301        """Remove `item` (:class:`NodeItem`) from the scene.
302        """
303        self.activated_mapper.removePyMappings(item)
304        self.hovered_mapper.removePyMappings(item)
305
306        item.hide()
307        self.removeItem(item)
308        self.__node_items.remove(item)
309
310        self.node_item_removed.emit(item)
311
312        log.info("Removed item '%s' from '%s'" % (item, self))
313
314    def remove_node(self, node):
315        """Remove the `NodeItem` instance that was previously constructed for
316        a `SchemeNode` node using the `add_node` method.
317
318        """
319        item = self.__item_for_node.pop(node)
320
321        node.position_changed.disconnect(self.__on_node_pos_changed)
322        node.title_changed.disconnect(item.setTitle)
323        node.progress_changed.disconnect(item.setProgress)
324        node.processing_state_changed.disconnect(item.setProcessingState)
325
326        self.remove_node_item(item)
327
328    def node_items(self):
329        """Return all :class:`NodeItem` instances in the scene.
330        """
331        return list(self.__node_items)
332
333    def add_link_item(self, item):
334        """Add a link (:class:`LinkItem`)to the scene.
335        """
336        if item.scene() is not self:
337            self.addItem(item)
338
339        self.__link_items.append(item)
340
341        self.link_item_added.emit(item)
342
343        log.info("Added link %r -> %r to '%s'" % \
344                 (item.sourceItem.title(), item.sinkItem.title(), self))
345
346        self.__anchor_layout.invalidateLink(item)
347
348        return item
349
350    def add_link(self, scheme_link):
351        """Create and add a `LinkItem` instance for a `SchemeLink`
352        instance. If the link is already in the scene do nothing
353        and just return its `LinkItem`.
354
355        """
356        if scheme_link in self.__item_for_link:
357            return self.__item_for_link[scheme_link]
358
359        source = self.__item_for_node[scheme_link.source_node]
360        sink = self.__item_for_node[scheme_link.sink_node]
361
362        item = self.new_link_item(source, scheme_link.source_channel,
363                                  sink, scheme_link.sink_channel)
364
365        item.setEnabled(scheme_link.enabled)
366        scheme_link.enabled_changed.connect(item.setEnabled)
367
368        if scheme_link.is_dynamic():
369            item.setDynamic(True)
370            item.setDynamicEnabled(scheme_link.dynamic_enabled)
371            scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled)
372
373        self.add_link_item(item)
374        self.__item_for_link[scheme_link] = item
375        return item
376
377    def new_link_item(self, source_item, source_channel,
378                      sink_item, sink_channel):
379        """Construct and return a new `LinkItem`
380        """
381        item = items.LinkItem()
382        item.setSourceItem(source_item)
383        item.setSinkItem(sink_item)
384
385        def channel_name(channel):
386            if isinstance(channel, basestring):
387                return channel
388            else:
389                return channel.name
390
391        source_name = channel_name(source_channel)
392        sink_name = channel_name(sink_channel)
393
394        fmt = u"<b>{0}</b>&nbsp; \u2192 &nbsp;<b>{1}</b>"
395        item.setToolTip(
396            fmt.format(escape(source_name),
397                       escape(sink_name))
398        )
399
400        item.setSourceName(source_name)
401        item.setSinkName(sink_name)
402        item.setChannelNamesVisible(self.__channel_names_visible)
403
404        return item
405
406    def remove_link_item(self, item):
407        """Remove a link (:class:`LinkItem`) from the scene.
408        """
409        # Invalidate the anchor layout.
410        self.__anchor_layout.invalidateAnchorItem(
411            item.sourceItem.outputAnchorItem
412        )
413        self.__anchor_layout.invalidateAnchorItem(
414            item.sinkItem.inputAnchorItem
415        )
416
417        self.__link_items.remove(item)
418
419        # Remove the anchor points.
420        item.removeLink()
421        self.removeItem(item)
422
423        self.link_item_removed.emit(item)
424
425        log.info("Removed link '%s' from '%s'" % (item, self))
426
427        return item
428
429    def remove_link(self, scheme_link):
430        """ Remove a `LinkItem` instance that was previously constructed for
431        a `SchemeLink` node using the `add_link` method.
432
433        """
434        item = self.__item_for_link.pop(scheme_link)
435        scheme_link.enabled_changed.disconnect(item.setEnabled)
436        self.remove_link_item(item)
437
438    def link_items(self):
439        """Return all :class:`LinkItems` in the scene.
440
441        """
442        return list(self.__link_items)
443
444    def add_annotation_item(self, annotation):
445        """Add an `Annotation` item to the scene.
446
447        """
448        self.__annotation_items.append(annotation)
449        self.addItem(annotation)
450        self.annotation_added.emit(annotation)
451        return annotation
452
453    def add_annotation(self, scheme_annot):
454        """Create a new item for :class:`SchemeAnnotation` and add it
455        to the scene. If the `scheme_annot` is already in the scene do
456        nothing and just return its item.
457
458        """
459        if scheme_annot in self.__item_for_annotation:
460            # Already added
461            return self.__item_for_annotation[scheme_annot]
462
463        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
464            item = items.TextAnnotation()
465            item.setPlainText(scheme_annot.text)
466            x, y, w, h = scheme_annot.rect
467            item.setPos(x, y)
468            item.resize(w, h)
469            item.setTextInteractionFlags(Qt.TextEditorInteraction)
470
471            font = font_from_dict(scheme_annot.font, item.font())
472            item.setFont(font)
473            scheme_annot.text_changed.connect(item.setPlainText)
474
475        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
476            item = items.ArrowAnnotation()
477            start, end = scheme_annot.start_pos, scheme_annot.end_pos
478            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
479            item.setColor(QColor(scheme_annot.color))
480
481        scheme_annot.geometry_changed.connect(
482            self.__on_scheme_annot_geometry_change
483        )
484
485        self.add_annotation_item(item)
486        self.__item_for_annotation[scheme_annot] = item
487
488        return item
489
490    def remove_annotation_item(self, annotation):
491        """Remove an `Annotation` item from the scene.
492
493        """
494        self.__annotation_items.remove(annotation)
495        self.removeItem(annotation)
496        self.annotation_removed.emit(annotation)
497
498    def remove_annotation(self, scheme_annotation):
499        item = self.__item_for_annotation.pop(scheme_annotation)
500
501        scheme_annotation.geometry_changed.disconnect(
502            self.__on_scheme_annot_geometry_change
503        )
504
505        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
506            scheme_annotation.text_changed.disconnect(
507                item.setPlainText
508            )
509
510        self.remove_annotation_item(item)
511
512    def annotation_items(self):
513        """Return all `Annotation` items in the scene.
514
515        """
516        return self.__annotation_items
517
518    def item_for_annotation(self, scheme_annotation):
519        return self.__item_for_annotation[scheme_annotation]
520
521    def annotation_for_item(self, item):
522        rev = dict(reversed(item) \
523                   for item in self.__item_for_annotation.items())
524        return rev[item]
525
526    def commit_scheme_node(self, node):
527        """Commit the `node` into the scheme.
528        """
529        if not self.editable:
530            raise Exception("Scheme not editable.")
531
532        if node not in self.__item_for_node:
533            raise ValueError("No 'NodeItem' for node.")
534
535        item = self.__item_for_node[node]
536
537        try:
538            self.scheme.add_node(node)
539        except Exception:
540            log.error("An unexpected error occurred while commiting node '%s'",
541                      node, exc_info=True)
542            # Cleanup (remove the node item)
543            self.remove_node_item(item)
544            raise
545
546        log.info("Commited node '%s' from '%s' to '%s'" % \
547                 (node, self, self.scheme))
548
549    def commit_scheme_link(self, link):
550        """Commit a scheme link.
551        """
552        if not self.editable:
553            raise Exception("Scheme not editable")
554
555        if link not in self.__item_for_link:
556            raise ValueError("No 'LinkItem' for link.")
557
558        self.scheme.add_link(link)
559        log.info("Commited link '%s' from '%s' to '%s'" % \
560                 (link, self, self.scheme))
561
562    def node_for_item(self, item):
563        """Return the `SchemeNode` for the `item`.
564        """
565        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
566        return rev[item]
567
568    def item_for_node(self, node):
569        """Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
570        """
571        return self.__item_for_node[node]
572
573    def link_for_item(self, item):
574        """Return the `SchemeLink for `item` (:class:`LinkItem`).
575        """
576        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
577        return rev[item]
578
579    def item_for_link(self, link):
580        """Return the :class:`LinkItem` for a :class:`SchemeLink`
581        """
582        return self.__item_for_link[link]
583
584    def selected_node_items(self):
585        """Return the selected :class:`NodeItem`'s.
586        """
587        return [item for item in self.__node_items if item.isSelected()]
588
589    def selected_annotation_items(self):
590        """Return the selected :class:`Annotation`'s
591        """
592        return [item for item in self.__annotation_items if item.isSelected()]
593
594    def node_links(self, node_item):
595        """Return all links from the `node_item` (:class:`NodeItem`).
596        """
597        return self.node_output_links(node_item) + \
598               self.node_input_links(node_item)
599
600    def node_output_links(self, node_item):
601        """Return a list of all output links from `node_item`.
602        """
603        return [link for link in self.__link_items
604                if link.sourceItem == node_item]
605
606    def node_input_links(self, node_item):
607        """Return a list of all input links for `node_item`.
608        """
609        return [link for link in self.__link_items
610                if link.sinkItem == node_item]
611
612    def neighbor_nodes(self, node_item):
613        """Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
614        """
615        neighbors = map(attrgetter("sourceItem"),
616                        self.node_input_links(node_item))
617
618        neighbors.extend(map(attrgetter("sinkItem"),
619                             self.node_output_links(node_item)))
620        return neighbors
621
622    def on_widget_state_change(self, widget, state):
623        pass
624
625    def on_link_state_change(self, link, state):
626        pass
627
628    def on_scheme_change(self, ):
629        pass
630
631    def _on_position_change(self, item):
632        # Invalidate the anchor point layout and schedule a layout.
633        self.__anchor_layout.invalidateNode(item)
634
635        self.node_item_position_changed.emit(item, item.pos())
636
637    def __on_node_pos_changed(self, pos):
638        node = self.sender()
639        item = self.__item_for_node[node]
640        item.setPos(*pos)
641
642    def __on_scheme_annot_geometry_change(self):
643        annot = self.sender()
644        item = self.__item_for_annotation[annot]
645        if isinstance(annot, scheme.SchemeTextAnnotation):
646            item.setGeometry(QRectF(*annot.rect))
647        elif isinstance(annot, scheme.SchemeArrowAnnotation):
648            p1 = item.mapFromScene(QPointF(*annot.start_pos))
649            p2 = item.mapFromScene(QPointF(*annot.end_pos))
650            item.setLine(QLineF(p1, p2))
651        else:
652            pass
653
654    def item_at(self, pos, type_or_tuple=None, buttons=0):
655        """Return the item at `pos` that is an instance of the specified
656        type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given
657        only return the item if it is the top level item that would
658        accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`).
659
660        """
661        rect = QRectF(pos, QSizeF(1, 1))
662        items = self.items(rect)
663
664        if buttons:
665            items = itertools.dropwhile(
666                lambda item: not item.acceptedMouseButtons() & buttons,
667                items
668            )
669            items = list(items)[:1]
670
671        if type_or_tuple:
672            items = [i for i in items if isinstance(i, type_or_tuple)]
673
674        return items[0] if items else None
675
676    if PYQT_VERSION_STR < "4.9":
677        # For QGraphicsObject subclasses items, itemAt ... return a
678        # QGraphicsItem wrapper instance and not the actual class instance.
679        def itemAt(self, *args, **kwargs):
680            item = QGraphicsScene.itemAt(self, *args, **kwargs)
681            return toGraphicsObjectIfPossible(item)
682
683        def items(self, *args, **kwargs):
684            items = QGraphicsScene.items(self, *args, **kwargs)
685            return map(toGraphicsObjectIfPossible, items)
686
687        def selectedItems(self, *args, **kwargs):
688            return map(toGraphicsObjectIfPossible,
689                       QGraphicsScene.selectedItems(self, *args, **kwargs))
690
691        def collidingItems(self, *args, **kwargs):
692            return map(toGraphicsObjectIfPossible,
693                       QGraphicsScene.collidingItems(self, *args, **kwargs))
694
695        def focusItem(self, *args, **kwargs):
696            item = QGraphicsScene.focusItem(self, *args, **kwargs)
697            return toGraphicsObjectIfPossible(item)
698
699        def mouseGrabberItem(self, *args, **kwargs):
700            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
701            return toGraphicsObjectIfPossible(item)
702
703    def mousePressEvent(self, event):
704        if self.user_interaction_handler and \
705                self.user_interaction_handler.mousePressEvent(event):
706            return
707
708        # Right (context) click on the node item. If the widget is not
709        # in the current selection then select the widget (only the widget).
710        # Else simply return and let customContextMenuReqested signal
711        # handle it
712        shape_item = self.item_at(event.scenePos(), items.NodeItem)
713        if shape_item and event.button() == Qt.RightButton and \
714                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
715            if not shape_item.isSelected():
716                self.clearSelection()
717                shape_item.setSelected(True)
718
719        return QGraphicsScene.mousePressEvent(self, event)
720
721    def mouseMoveEvent(self, event):
722        if self.user_interaction_handler and \
723                self.user_interaction_handler.mouseMoveEvent(event):
724            return
725
726        return QGraphicsScene.mouseMoveEvent(self, event)
727
728    def mouseReleaseEvent(self, event):
729        if self.user_interaction_handler and \
730                self.user_interaction_handler.mouseReleaseEvent(event):
731            return
732        return QGraphicsScene.mouseReleaseEvent(self, event)
733
734    def mouseDoubleClickEvent(self, event):
735        if self.user_interaction_handler and \
736                self.user_interaction_handler.mouseDoubleClickEvent(event):
737            return
738
739        return QGraphicsScene.mouseDoubleClickEvent(self, event)
740
741    def keyPressEvent(self, event):
742        if self.user_interaction_handler and \
743                self.user_interaction_handler.keyPressEvent(event):
744            return
745        return QGraphicsScene.keyPressEvent(self, event)
746
747    def keyReleaseEvent(self, event):
748        if self.user_interaction_handler and \
749                self.user_interaction_handler.keyReleaseEvent(event):
750            return
751        return QGraphicsScene.keyReleaseEvent(self, event)
752
753    def set_user_interaction_handler(self, handler):
754        if self.user_interaction_handler and \
755                not self.user_interaction_handler.isFinished():
756            self.user_interaction_handler.cancel()
757
758        log.info("Setting interaction '%s' to '%s'" % (handler, self))
759
760        self.user_interaction_handler = handler
761        if handler:
762            handler.start()
763
764    def __str__(self):
765        return "%s(objectName=%r, ...)" % \
766                (type(self).__name__, str(self.objectName()))
767
768
769def font_from_dict(font_dict, font=None):
770    if font is None:
771        font = QFont()
772    else:
773        font = QFont(font)
774
775    if "family" in font_dict:
776        font.setFamily(font_dict["family"])
777
778    if "size" in font_dict:
779        font.setPointSize(font_dict["size"])
780
781    return font
782
783
784def grab_svg(scene):
785    """Return a SVG rendering of the scene contents.
786    """
787    from PyQt4.QtSvg import QSvgGenerator
788    svg_buffer = QBuffer()
789    gen = QSvgGenerator()
790    gen.setOutputDevice(svg_buffer)
791
792    items_rect = scene.itemsBoundingRect().adjusted(-10, -10, 10, 10)
793
794    if items_rect.isNull():
795        items_rect = QRectF(0, 0, 10, 10)
796
797    width, height = items_rect.width(), items_rect.height()
798    rect_ratio = float(width) / height
799
800    # Keep a fixed aspect ratio.
801    aspect_ratio = 1.618
802    if rect_ratio > aspect_ratio:
803        height = int(height * rect_ratio / aspect_ratio)
804    else:
805        width = int(width * aspect_ratio / rect_ratio)
806
807    target_rect = QRectF(0, 0, width, height)
808    source_rect = QRectF(0, 0, width, height)
809    source_rect.moveCenter(items_rect.center())
810
811    gen.setSize(target_rect.size().toSize())
812    gen.setViewBox(target_rect)
813
814    painter = QPainter(gen)
815
816    # Draw background.
817    painter.setBrush(QBrush(Qt.white))
818    painter.drawRect(target_rect)
819
820    # Render the scene
821    scene.render(painter, target_rect, source_rect)
822    painter.end()
823
824    buffer_str = str(svg_buffer.buffer())
825    return unicode(buffer_str.decode("utf-8"))
Note: See TracBrowser for help on using the repository browser.