source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11411:f1d5470c8031

Revision 11411:f1d5470c8031, 26.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Added ping and shadow animations for node items.

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