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> \u2192 <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