source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11773:41d75ac844c9

Revision 11773:41d75ac844c9, 29.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Initialize the SchemeNode state messages on widget initialization.

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