source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11369:e9c95cc39be6

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

Added rst documentation for the canvas package.

Fixing docstrings in the process.

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