source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11854:975cc3daadf3

Revision 11854:975cc3daadf3, 29.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 months ago (diff)

Fixed NodeItem progress state initialization and updating.

Progress indicator should only be visible when the node is
in a "processing state".

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