source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11442:279d7a51ea1d

Revision 11442:279d7a51ea1d, 28.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 13 months ago (diff)

Fixes to canvas package documentation.

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