source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11614:a4aa7f9b09fd

Revision 11614:a4aa7f9b09fd, 29.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 10 months ago (diff)

Refactored widget state messages handling.

Moved the resposibility for handling messages (OWBaseWidget.widgetStateChanged)
to WidgetsScheme.

Added support for messages in base SchemeNode class.

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