source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11343:84f579a58442

Revision 11343:84f579a58442, 26.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Fixed font size in the canvas scene.

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