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