source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11558:e050e7ade4e4

Revision 11558:e050e7ade4e4, 29.2 KB checked in by Jure Zbontar <jure.zbontar@…>, 11 months ago (diff)

Fix PyQt version comparison bug

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