| Total Complexity | 149 |
| Total Lines | 849 |
| Duplicated Lines | 0 % |
Complex classes like Orange.canvas.canvas.CanvasScene often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | """ |
||
| 36 | class CanvasScene(QGraphicsScene): |
||
| 37 | """ |
||
| 38 | A Graphics Scene for displaying an :class:`~.scheme.Scheme` instance. |
||
| 39 | """ |
||
| 40 | |||
| 41 | #: Signal emitted when a :class:`NodeItem` has been added to the scene. |
||
| 42 | node_item_added = Signal(items.NodeItem) |
||
| 43 | |||
| 44 | #: Signal emitted when a :class:`NodeItem` has been removed from the |
||
| 45 | #: scene. |
||
| 46 | node_item_removed = Signal(items.LinkItem) |
||
| 47 | |||
| 48 | #: Signal emitted when a new :class:`LinkItem` has been added to the |
||
| 49 | #: scene. |
||
| 50 | link_item_added = Signal(items.LinkItem) |
||
| 51 | |||
| 52 | #: Signal emitted when a :class:`LinkItem` has been removed. |
||
| 53 | link_item_removed = Signal(items.LinkItem) |
||
| 54 | |||
| 55 | #: Signal emitted when a :class:`Annotation` item has been added. |
||
| 56 | annotation_added = Signal(items.annotationitem.Annotation) |
||
| 57 | |||
| 58 | #: Signal emitted when a :class:`Annotation` item has been removed. |
||
| 59 | annotation_removed = Signal(items.annotationitem.Annotation) |
||
| 60 | |||
| 61 | #: Signal emitted when the position of a :class:`NodeItem` has changed. |
||
| 62 | node_item_position_changed = Signal(items.NodeItem, QPointF) |
||
| 63 | |||
| 64 | #: Signal emitted when an :class:`NodeItem` has been double clicked. |
||
| 65 | node_item_double_clicked = Signal(items.NodeItem) |
||
| 66 | |||
| 67 | #: An node item has been activated (clicked) |
||
| 68 | node_item_activated = Signal(items.NodeItem) |
||
| 69 | |||
| 70 | #: An node item has been hovered |
||
| 71 | node_item_hovered = Signal(items.NodeItem) |
||
| 72 | |||
| 73 | #: Link item has been hovered |
||
| 74 | link_item_hovered = Signal(items.LinkItem) |
||
| 75 | |||
| 76 | def __init__(self, *args, **kwargs): |
||
| 77 | QGraphicsScene.__init__(self, *args, **kwargs) |
||
| 78 | |||
| 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 | |||
| 99 | # Anchor Layout |
||
| 100 | self.__anchor_layout = AnchorLayout() |
||
| 101 | self.addItem(self.__anchor_layout) |
||
| 102 | |||
| 103 | self.__channel_names_visible = True |
||
| 104 | self.__node_animation_enabled = True |
||
| 105 | |||
| 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): |
||
| 126 | """ |
||
| 127 | Clear (reset) the scene. |
||
| 128 | """ |
||
| 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 | |||
| 161 | self.scheme = None |
||
| 162 | self.__node_items = [] |
||
| 163 | self.__item_for_node = {} |
||
| 164 | self.__link_items = [] |
||
| 165 | self.__item_for_link = {} |
||
| 166 | self.__annotation_items = [] |
||
| 167 | self.__item_for_annotation = {} |
||
| 168 | |||
| 169 | self.__anchor_layout.deleteLater() |
||
| 170 | |||
| 171 | self.user_interaction_handler = None |
||
| 172 | |||
| 173 | self.clear() |
||
| 174 | log.info("'%s' cleared." % self) |
||
| 175 | |||
| 176 | def set_scheme(self, scheme): |
||
| 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` |
||
| 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 | """ |
||
| 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. |
||
| 228 | log.info("Setting registry '%s on '%s'." % (registry, self)) |
||
| 229 | self.registry = registry |
||
| 230 | |||
| 231 | def set_anchor_layout(self, layout): |
||
| 232 | """ |
||
| 233 | Set an :class:`~.layout.AnchorLayout` |
||
| 234 | """ |
||
| 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): |
||
| 243 | """ |
||
| 244 | Return the anchor layout instance. |
||
| 245 | """ |
||
| 246 | return self.__anchor_layout |
||
| 247 | |||
| 248 | def set_channel_names_visible(self, visible): |
||
| 249 | """ |
||
| 250 | Set the channel names visibility. |
||
| 251 | """ |
||
| 252 | self.__channel_names_visible = visible |
||
| 253 | for link in self.__link_items: |
||
| 254 | link.setChannelNamesVisible(visible) |
||
| 255 | |||
| 256 | def channel_names_visible(self): |
||
| 257 | """ |
||
| 258 | Return the channel names visibility state. |
||
| 259 | """ |
||
| 260 | return self.__channel_names_visible |
||
| 261 | |||
| 262 | def set_node_animation_enabled(self, enabled): |
||
| 263 | """ |
||
| 264 | Set node animation enabled state. |
||
| 265 | """ |
||
| 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 | |||
| 272 | def add_node_item(self, item): |
||
| 273 | """ |
||
| 274 | Add a :class:`.NodeItem` instance to the scene. |
||
| 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 | |||
| 287 | item.setFont(self.font()) |
||
| 288 | |||
| 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): |
||
| 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. |
||
| 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 | |||
| 325 | item.setTitle(node.title) |
||
| 326 | item.setProcessingState(node.processing_state) |
||
| 327 | item.setProgress(node.progress) |
||
| 328 | |||
| 329 | for message in node.state_messages(): |
||
| 330 | item.setStateMessage(message) |
||
| 331 | |||
| 332 | item.setStatusMessage(node.status_message()) |
||
| 333 | |||
| 334 | self.__item_for_node[node] = item |
||
| 335 | |||
| 336 | node.position_changed.connect(self.__on_node_pos_changed) |
||
| 337 | node.title_changed.connect(item.setTitle) |
||
| 338 | node.progress_changed.connect(item.setProgress) |
||
| 339 | node.processing_state_changed.connect(item.setProcessingState) |
||
| 340 | node.state_message_changed.connect(item.setStateMessage) |
||
| 341 | node.status_message_changed.connect(item.setStatusMessage) |
||
| 342 | |||
| 343 | return self.add_node_item(item) |
||
| 344 | |||
| 345 | def new_node_item(self, widget_desc, category_desc=None): |
||
| 346 | """ |
||
| 347 | Construct an new :class:`.NodeItem` from a `WidgetDescription`. |
||
| 348 | Optionally also set `CategoryDescription`. |
||
| 349 | |||
| 350 | """ |
||
| 351 | item = items.NodeItem() |
||
| 352 | item.setWidgetDescription(widget_desc) |
||
| 353 | |||
| 354 | if category_desc is None and self.registry and widget_desc.category: |
||
| 355 | category_desc = self.registry.category(widget_desc.category) |
||
| 356 | |||
| 357 | if category_desc is None and self.registry is not None: |
||
| 358 | try: |
||
| 359 | category_desc = self.registry.category(widget_desc.category) |
||
| 360 | except KeyError: |
||
| 361 | pass |
||
| 362 | |||
| 363 | if category_desc is not None: |
||
| 364 | item.setWidgetCategory(category_desc) |
||
| 365 | |||
| 366 | item.setAnimationEnabled(self.__node_animation_enabled) |
||
| 367 | return item |
||
| 368 | |||
| 369 | def remove_node_item(self, item): |
||
| 370 | """ |
||
| 371 | Remove `item` (:class:`.NodeItem`) from the scene. |
||
| 372 | """ |
||
| 373 | self.activated_mapper.removePyMappings(item) |
||
| 374 | self.hovered_mapper.removePyMappings(item) |
||
| 375 | |||
| 376 | item.hide() |
||
| 377 | self.removeItem(item) |
||
| 378 | self.__node_items.remove(item) |
||
| 379 | |||
| 380 | self.node_item_removed.emit(item) |
||
| 381 | |||
| 382 | log.info("Removed item '%s' from '%s'" % (item, self)) |
||
| 383 | |||
| 384 | def remove_node(self, node): |
||
| 385 | """ |
||
| 386 | Remove the :class:`.NodeItem` instance that was previously |
||
| 387 | constructed for a :class:`SchemeNode` `node` using the `add_node` |
||
| 388 | method. |
||
| 389 | |||
| 390 | """ |
||
| 391 | item = self.__item_for_node.pop(node) |
||
| 392 | |||
| 393 | node.position_changed.disconnect(self.__on_node_pos_changed) |
||
| 394 | node.title_changed.disconnect(item.setTitle) |
||
| 395 | node.progress_changed.disconnect(item.setProgress) |
||
| 396 | node.processing_state_changed.disconnect(item.setProcessingState) |
||
| 397 | node.state_message_changed.disconnect(item.setStateMessage) |
||
| 398 | |||
| 399 | self.remove_node_item(item) |
||
| 400 | |||
| 401 | def node_items(self): |
||
| 402 | """ |
||
| 403 | Return all :class:`.NodeItem` instances in the scene. |
||
| 404 | """ |
||
| 405 | return list(self.__node_items) |
||
| 406 | |||
| 407 | def add_link_item(self, item): |
||
| 408 | """ |
||
| 409 | Add a link (:class:`.LinkItem`) to the scene. |
||
| 410 | """ |
||
| 411 | if item.scene() is not self: |
||
| 412 | self.addItem(item) |
||
| 413 | |||
| 414 | item.setFont(self.font()) |
||
| 415 | self.__link_items.append(item) |
||
| 416 | |||
| 417 | self.link_item_added.emit(item) |
||
| 418 | |||
| 419 | log.info("Added link %r -> %r to '%s'" % \ |
||
| 420 | (item.sourceItem.title(), item.sinkItem.title(), self)) |
||
| 421 | |||
| 422 | self.__anchor_layout.invalidateLink(item) |
||
| 423 | |||
| 424 | return item |
||
| 425 | |||
| 426 | def add_link(self, scheme_link): |
||
| 427 | """ |
||
| 428 | Create and add a :class:`.LinkItem` instance for a |
||
| 429 | :class:`SchemeLink` instance. If the link is already in the scene |
||
| 430 | do nothing and just return its :class:`.LinkItem`. |
||
| 431 | |||
| 432 | """ |
||
| 433 | if scheme_link in self.__item_for_link: |
||
| 434 | return self.__item_for_link[scheme_link] |
||
| 435 | |||
| 436 | source = self.__item_for_node[scheme_link.source_node] |
||
| 437 | sink = self.__item_for_node[scheme_link.sink_node] |
||
| 438 | |||
| 439 | item = self.new_link_item(source, scheme_link.source_channel, |
||
| 440 | sink, scheme_link.sink_channel) |
||
| 441 | |||
| 442 | item.setEnabled(scheme_link.enabled) |
||
| 443 | scheme_link.enabled_changed.connect(item.setEnabled) |
||
| 444 | |||
| 445 | if scheme_link.is_dynamic(): |
||
| 446 | item.setDynamic(True) |
||
| 447 | item.setDynamicEnabled(scheme_link.dynamic_enabled) |
||
| 448 | scheme_link.dynamic_enabled_changed.connect(item.setDynamicEnabled) |
||
| 449 | |||
| 450 | self.add_link_item(item) |
||
| 451 | self.__item_for_link[scheme_link] = item |
||
| 452 | return item |
||
| 453 | |||
| 454 | def new_link_item(self, source_item, source_channel, |
||
| 455 | sink_item, sink_channel): |
||
| 456 | """ |
||
| 457 | Construct and return a new :class:`.LinkItem` |
||
| 458 | """ |
||
| 459 | item = items.LinkItem() |
||
| 460 | item.setSourceItem(source_item) |
||
| 461 | item.setSinkItem(sink_item) |
||
| 462 | |||
| 463 | def channel_name(channel): |
||
| 464 | if isinstance(channel, str): |
||
| 465 | return channel |
||
| 466 | else: |
||
| 467 | return channel.name |
||
| 468 | |||
| 469 | source_name = channel_name(source_channel) |
||
| 470 | sink_name = channel_name(sink_channel) |
||
| 471 | |||
| 472 | fmt = "<b>{0}</b> \u2192 <b>{1}</b>" |
||
| 473 | item.setToolTip( |
||
| 474 | fmt.format(escape(source_name), |
||
| 475 | escape(sink_name)) |
||
| 476 | ) |
||
| 477 | |||
| 478 | item.setSourceName(source_name) |
||
| 479 | item.setSinkName(sink_name) |
||
| 480 | item.setChannelNamesVisible(self.__channel_names_visible) |
||
| 481 | |||
| 482 | return item |
||
| 483 | |||
| 484 | def remove_link_item(self, item): |
||
| 485 | """ |
||
| 486 | Remove a link (:class:`.LinkItem`) from the scene. |
||
| 487 | """ |
||
| 488 | # Invalidate the anchor layout. |
||
| 489 | self.__anchor_layout.invalidateAnchorItem( |
||
| 490 | item.sourceItem.outputAnchorItem |
||
| 491 | ) |
||
| 492 | self.__anchor_layout.invalidateAnchorItem( |
||
| 493 | item.sinkItem.inputAnchorItem |
||
| 494 | ) |
||
| 495 | |||
| 496 | self.__link_items.remove(item) |
||
| 497 | |||
| 498 | # Remove the anchor points. |
||
| 499 | item.removeLink() |
||
| 500 | self.removeItem(item) |
||
| 501 | |||
| 502 | self.link_item_removed.emit(item) |
||
| 503 | |||
| 504 | log.info("Removed link '%s' from '%s'" % (item, self)) |
||
| 505 | |||
| 506 | return item |
||
| 507 | |||
| 508 | def remove_link(self, scheme_link): |
||
| 509 | """ |
||
| 510 | Remove a :class:`.LinkItem` instance that was previously constructed |
||
| 511 | for a :class:`SchemeLink` instance `link` using the `add_link` method. |
||
| 512 | |||
| 513 | """ |
||
| 514 | item = self.__item_for_link.pop(scheme_link) |
||
| 515 | scheme_link.enabled_changed.disconnect(item.setEnabled) |
||
| 516 | |||
| 517 | if scheme_link.is_dynamic(): |
||
| 518 | scheme_link.dynamic_enabled_changed.disconnect( |
||
| 519 | item.setDynamicEnabled |
||
| 520 | ) |
||
| 521 | |||
| 522 | self.remove_link_item(item) |
||
| 523 | |||
| 524 | def link_items(self): |
||
| 525 | """ |
||
| 526 | Return all :class:`.LinkItem`\s in the scene. |
||
| 527 | """ |
||
| 528 | return list(self.__link_items) |
||
| 529 | |||
| 530 | def add_annotation_item(self, annotation): |
||
| 531 | """ |
||
| 532 | Add an :class:`.Annotation` item to the scene. |
||
| 533 | """ |
||
| 534 | self.__annotation_items.append(annotation) |
||
| 535 | self.addItem(annotation) |
||
| 536 | self.annotation_added.emit(annotation) |
||
| 537 | return annotation |
||
| 538 | |||
| 539 | def add_annotation(self, scheme_annot): |
||
| 540 | """ |
||
| 541 | Create a new item for :class:`SchemeAnnotation` and add it |
||
| 542 | to the scene. If the `scheme_annot` is already in the scene do |
||
| 543 | nothing and just return its item. |
||
| 544 | |||
| 545 | """ |
||
| 546 | if scheme_annot in self.__item_for_annotation: |
||
| 547 | # Already added |
||
| 548 | return self.__item_for_annotation[scheme_annot] |
||
| 549 | |||
| 550 | if isinstance(scheme_annot, scheme.SchemeTextAnnotation): |
||
| 551 | item = items.TextAnnotation() |
||
| 552 | item.setPlainText(scheme_annot.text) |
||
| 553 | x, y, w, h = scheme_annot.rect |
||
| 554 | item.setPos(x, y) |
||
| 555 | item.resize(w, h) |
||
| 556 | item.setTextInteractionFlags(Qt.TextEditorInteraction) |
||
| 557 | |||
| 558 | font = font_from_dict(scheme_annot.font, item.font()) |
||
| 559 | item.setFont(font) |
||
| 560 | scheme_annot.text_changed.connect(item.setPlainText) |
||
| 561 | |||
| 562 | elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation): |
||
| 563 | item = items.ArrowAnnotation() |
||
| 564 | start, end = scheme_annot.start_pos, scheme_annot.end_pos |
||
| 565 | item.setLine(QLineF(QPointF(*start), QPointF(*end))) |
||
| 566 | item.setColor(QColor(scheme_annot.color)) |
||
| 567 | |||
| 568 | scheme_annot.geometry_changed.connect( |
||
| 569 | self.__on_scheme_annot_geometry_change |
||
| 570 | ) |
||
| 571 | |||
| 572 | self.add_annotation_item(item) |
||
| 573 | self.__item_for_annotation[scheme_annot] = item |
||
| 574 | |||
| 575 | return item |
||
| 576 | |||
| 577 | def remove_annotation_item(self, annotation): |
||
| 578 | """ |
||
| 579 | Remove an :class:`.Annotation` instance from the scene. |
||
| 580 | |||
| 581 | """ |
||
| 582 | self.__annotation_items.remove(annotation) |
||
| 583 | self.removeItem(annotation) |
||
| 584 | self.annotation_removed.emit(annotation) |
||
| 585 | |||
| 586 | def remove_annotation(self, scheme_annotation): |
||
| 587 | """ |
||
| 588 | Remove an :class:`.Annotation` instance that was previously added |
||
| 589 | using :func:`add_anotation`. |
||
| 590 | |||
| 591 | """ |
||
| 592 | item = self.__item_for_annotation.pop(scheme_annotation) |
||
| 593 | |||
| 594 | scheme_annotation.geometry_changed.disconnect( |
||
| 595 | self.__on_scheme_annot_geometry_change |
||
| 596 | ) |
||
| 597 | |||
| 598 | if isinstance(scheme_annotation, scheme.SchemeTextAnnotation): |
||
| 599 | scheme_annotation.text_changed.disconnect( |
||
| 600 | item.setPlainText |
||
| 601 | ) |
||
| 602 | |||
| 603 | self.remove_annotation_item(item) |
||
| 604 | |||
| 605 | def annotation_items(self): |
||
| 606 | """ |
||
| 607 | Return all :class:`.Annotation` items in the scene. |
||
| 608 | """ |
||
| 609 | return self.__annotation_items |
||
| 610 | |||
| 611 | def item_for_annotation(self, scheme_annotation): |
||
| 612 | return self.__item_for_annotation[scheme_annotation] |
||
| 613 | |||
| 614 | def annotation_for_item(self, item): |
||
| 615 | rev = dict(reversed(item) \ |
||
| 616 | for item in self.__item_for_annotation.items()) |
||
| 617 | return rev[item] |
||
| 618 | |||
| 619 | def commit_scheme_node(self, node): |
||
| 620 | """ |
||
| 621 | Commit the `node` into the scheme. |
||
| 622 | """ |
||
| 623 | if not self.editable: |
||
| 624 | raise Exception("Scheme not editable.") |
||
| 625 | |||
| 626 | if node not in self.__item_for_node: |
||
| 627 | raise ValueError("No 'NodeItem' for node.") |
||
| 628 | |||
| 629 | item = self.__item_for_node[node] |
||
| 630 | |||
| 631 | try: |
||
| 632 | self.scheme.add_node(node) |
||
| 633 | except Exception: |
||
| 634 | log.error("An error occurred while committing node '%s'", |
||
| 635 | node, exc_info=True) |
||
| 636 | # Cleanup (remove the node item) |
||
| 637 | self.remove_node_item(item) |
||
| 638 | raise |
||
| 639 | |||
| 640 | log.info("Commited node '%s' from '%s' to '%s'" % \ |
||
| 641 | (node, self, self.scheme)) |
||
| 642 | |||
| 643 | def commit_scheme_link(self, link): |
||
| 644 | """ |
||
| 645 | Commit a scheme link. |
||
| 646 | """ |
||
| 647 | if not self.editable: |
||
| 648 | raise Exception("Scheme not editable") |
||
| 649 | |||
| 650 | if link not in self.__item_for_link: |
||
| 651 | raise ValueError("No 'LinkItem' for link.") |
||
| 652 | |||
| 653 | self.scheme.add_link(link) |
||
| 654 | log.info("Commited link '%s' from '%s' to '%s'" % \ |
||
| 655 | (link, self, self.scheme)) |
||
| 656 | |||
| 657 | def node_for_item(self, item): |
||
| 658 | """ |
||
| 659 | Return the `SchemeNode` for the `item`. |
||
| 660 | """ |
||
| 661 | rev = dict([(v, k) for k, v in self.__item_for_node.items()]) |
||
| 662 | return rev[item] |
||
| 663 | |||
| 664 | def item_for_node(self, node): |
||
| 665 | """ |
||
| 666 | Return the :class:`NodeItem` instance for a :class:`SchemeNode`. |
||
| 667 | """ |
||
| 668 | return self.__item_for_node[node] |
||
| 669 | |||
| 670 | def link_for_item(self, item): |
||
| 671 | """ |
||
| 672 | Return the `SchemeLink for `item` (:class:`LinkItem`). |
||
| 673 | """ |
||
| 674 | rev = dict([(v, k) for k, v in self.__item_for_link.items()]) |
||
| 675 | return rev[item] |
||
| 676 | |||
| 677 | def item_for_link(self, link): |
||
| 678 | """ |
||
| 679 | Return the :class:`LinkItem` for a :class:`SchemeLink` |
||
| 680 | """ |
||
| 681 | return self.__item_for_link[link] |
||
| 682 | |||
| 683 | def selected_node_items(self): |
||
| 684 | """ |
||
| 685 | Return the selected :class:`NodeItem`'s. |
||
| 686 | """ |
||
| 687 | return [item for item in self.__node_items if item.isSelected()] |
||
| 688 | |||
| 689 | def selected_annotation_items(self): |
||
| 690 | """ |
||
| 691 | Return the selected :class:`Annotation`'s |
||
| 692 | """ |
||
| 693 | return [item for item in self.__annotation_items if item.isSelected()] |
||
| 694 | |||
| 695 | def node_links(self, node_item): |
||
| 696 | """ |
||
| 697 | Return all links from the `node_item` (:class:`NodeItem`). |
||
| 698 | """ |
||
| 699 | return self.node_output_links(node_item) + \ |
||
| 700 | self.node_input_links(node_item) |
||
| 701 | |||
| 702 | def node_output_links(self, node_item): |
||
| 703 | """ |
||
| 704 | Return a list of all output links from `node_item`. |
||
| 705 | """ |
||
| 706 | return [link for link in self.__link_items |
||
| 707 | if link.sourceItem == node_item] |
||
| 708 | |||
| 709 | def node_input_links(self, node_item): |
||
| 710 | """ |
||
| 711 | Return a list of all input links for `node_item`. |
||
| 712 | """ |
||
| 713 | return [link for link in self.__link_items |
||
| 714 | if link.sinkItem == node_item] |
||
| 715 | |||
| 716 | def neighbor_nodes(self, node_item): |
||
| 717 | """ |
||
| 718 | Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes. |
||
| 719 | """ |
||
| 720 | neighbors = list(map(attrgetter("sourceItem"), |
||
| 721 | self.node_input_links(node_item))) |
||
| 722 | |||
| 723 | neighbors.extend(map(attrgetter("sinkItem"), |
||
| 724 | self.node_output_links(node_item))) |
||
| 725 | return neighbors |
||
| 726 | |||
| 727 | def on_widget_state_change(self, widget, state): |
||
| 728 | pass |
||
| 729 | |||
| 730 | def on_link_state_change(self, link, state): |
||
| 731 | pass |
||
| 732 | |||
| 733 | def on_scheme_change(self, ): |
||
| 734 | pass |
||
| 735 | |||
| 736 | def _on_position_change(self, item): |
||
| 737 | # Invalidate the anchor point layout and schedule a layout. |
||
| 738 | self.__anchor_layout.invalidateNode(item) |
||
| 739 | |||
| 740 | self.node_item_position_changed.emit(item, item.pos()) |
||
| 741 | |||
| 742 | def __on_node_pos_changed(self, pos): |
||
| 743 | node = self.sender() |
||
| 744 | item = self.__item_for_node[node] |
||
| 745 | item.setPos(*pos) |
||
| 746 | |||
| 747 | def __on_scheme_annot_geometry_change(self): |
||
| 748 | annot = self.sender() |
||
| 749 | item = self.__item_for_annotation[annot] |
||
| 750 | if isinstance(annot, scheme.SchemeTextAnnotation): |
||
| 751 | item.setGeometry(QRectF(*annot.rect)) |
||
| 752 | elif isinstance(annot, scheme.SchemeArrowAnnotation): |
||
| 753 | p1 = item.mapFromScene(QPointF(*annot.start_pos)) |
||
| 754 | p2 = item.mapFromScene(QPointF(*annot.end_pos)) |
||
| 755 | item.setLine(QLineF(p1, p2)) |
||
| 756 | else: |
||
| 757 | pass |
||
| 758 | |||
| 759 | def item_at(self, pos, type_or_tuple=None, buttons=0): |
||
| 760 | """Return the item at `pos` that is an instance of the specified |
||
| 761 | type (`type_or_tuple`). If `buttons` (`Qt.MouseButtons`) is given |
||
| 762 | only return the item if it is the top level item that would |
||
| 763 | accept any of the buttons (`QGraphicsItem.acceptedMouseButtons`). |
||
| 764 | |||
| 765 | """ |
||
| 766 | rect = QRectF(pos, QSizeF(1, 1)) |
||
| 767 | items = self.items(rect) |
||
| 768 | |||
| 769 | if buttons: |
||
| 770 | items = itertools.dropwhile( |
||
| 771 | lambda item: not item.acceptedMouseButtons() & buttons, |
||
| 772 | items |
||
| 773 | ) |
||
| 774 | items = list(items)[:1] |
||
| 775 | |||
| 776 | if type_or_tuple: |
||
| 777 | items = [i for i in items if isinstance(i, type_or_tuple)] |
||
| 778 | |||
| 779 | return items[0] if items else None |
||
| 780 | |||
| 781 | if list(map(int, PYQT_VERSION_STR.split('.'))) < [4, 9]: |
||
| 782 | # For QGraphicsObject subclasses items, itemAt ... return a |
||
| 783 | # QGraphicsItem wrapper instance and not the actual class instance. |
||
| 784 | def itemAt(self, *args, **kwargs): |
||
| 785 | item = QGraphicsScene.itemAt(self, *args, **kwargs) |
||
| 786 | return toGraphicsObjectIfPossible(item) |
||
| 787 | |||
| 788 | def items(self, *args, **kwargs): |
||
| 789 | items = QGraphicsScene.items(self, *args, **kwargs) |
||
| 790 | return list(map(toGraphicsObjectIfPossible, items)) |
||
| 791 | |||
| 792 | def selectedItems(self, *args, **kwargs): |
||
| 793 | return list(map(toGraphicsObjectIfPossible, |
||
| 794 | QGraphicsScene.selectedItems(self, *args, **kwargs))) |
||
| 795 | |||
| 796 | def collidingItems(self, *args, **kwargs): |
||
| 797 | return list(map(toGraphicsObjectIfPossible, |
||
| 798 | QGraphicsScene.collidingItems(self, *args, **kwargs))) |
||
| 799 | |||
| 800 | def focusItem(self, *args, **kwargs): |
||
| 801 | item = QGraphicsScene.focusItem(self, *args, **kwargs) |
||
| 802 | return toGraphicsObjectIfPossible(item) |
||
| 803 | |||
| 804 | def mouseGrabberItem(self, *args, **kwargs): |
||
| 805 | item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs) |
||
| 806 | return toGraphicsObjectIfPossible(item) |
||
| 807 | |||
| 808 | def mousePressEvent(self, event): |
||
| 809 | if self.user_interaction_handler and \ |
||
| 810 | self.user_interaction_handler.mousePressEvent(event): |
||
| 811 | return |
||
| 812 | |||
| 813 | # Right (context) click on the node item. If the widget is not |
||
| 814 | # in the current selection then select the widget (only the widget). |
||
| 815 | # Else simply return and let customContextMenuReqested signal |
||
| 816 | # handle it |
||
| 817 | shape_item = self.item_at(event.scenePos(), items.NodeItem) |
||
| 818 | if shape_item and event.button() == Qt.RightButton and \ |
||
| 819 | shape_item.flags() & QGraphicsItem.ItemIsSelectable: |
||
| 820 | if not shape_item.isSelected(): |
||
| 821 | self.clearSelection() |
||
| 822 | shape_item.setSelected(True) |
||
| 823 | |||
| 824 | return QGraphicsScene.mousePressEvent(self, event) |
||
| 825 | |||
| 826 | def mouseMoveEvent(self, event): |
||
| 827 | if self.user_interaction_handler and \ |
||
| 828 | self.user_interaction_handler.mouseMoveEvent(event): |
||
| 829 | return |
||
| 830 | |||
| 831 | return QGraphicsScene.mouseMoveEvent(self, event) |
||
| 832 | |||
| 833 | def mouseReleaseEvent(self, event): |
||
| 834 | if self.user_interaction_handler and \ |
||
| 835 | self.user_interaction_handler.mouseReleaseEvent(event): |
||
| 836 | return |
||
| 837 | return QGraphicsScene.mouseReleaseEvent(self, event) |
||
| 838 | |||
| 839 | def mouseDoubleClickEvent(self, event): |
||
| 840 | if self.user_interaction_handler and \ |
||
| 841 | self.user_interaction_handler.mouseDoubleClickEvent(event): |
||
| 842 | return |
||
| 843 | |||
| 844 | return QGraphicsScene.mouseDoubleClickEvent(self, event) |
||
| 845 | |||
| 846 | def keyPressEvent(self, event): |
||
| 847 | if self.user_interaction_handler and \ |
||
| 848 | self.user_interaction_handler.keyPressEvent(event): |
||
| 849 | return |
||
| 850 | return QGraphicsScene.keyPressEvent(self, event) |
||
| 851 | |||
| 852 | def keyReleaseEvent(self, event): |
||
| 853 | if self.user_interaction_handler and \ |
||
| 854 | self.user_interaction_handler.keyReleaseEvent(event): |
||
| 855 | return |
||
| 856 | return QGraphicsScene.keyReleaseEvent(self, event) |
||
| 857 | |||
| 858 | def set_user_interaction_handler(self, handler): |
||
| 859 | if self.user_interaction_handler and \ |
||
| 860 | not self.user_interaction_handler.isFinished(): |
||
| 861 | self.user_interaction_handler.cancel() |
||
| 862 | |||
| 863 | log.info("Setting interaction '%s' to '%s'" % (handler, self)) |
||
| 864 | |||
| 865 | self.user_interaction_handler = handler |
||
| 866 | if handler: |
||
| 867 | handler.start() |
||
| 868 | |||
| 869 | def event(self, event): |
||
| 870 | # TODO: change the base class of Node/LinkItem to QGraphicsWidget. |
||
| 871 | # It already handles font changes. |
||
| 872 | if event.type() == QEvent.FontChange: |
||
| 873 | self.__update_font() |
||
| 874 | |||
| 875 | return QGraphicsScene.event(self, event) |
||
| 876 | |||
| 877 | def __update_font(self): |
||
| 878 | font = self.font() |
||
| 879 | for item in self.__node_items + self.__link_items: |
||
| 880 | item.setFont(font) |
||
| 881 | |||
| 882 | def __str__(self): |
||
| 883 | return "%s(objectName=%r, ...)" % \ |
||
| 884 | (type(self).__name__, str(self.objectName())) |
||
| 885 | |||
| 950 |
This can be caused by one of the following:
1. Missing Dependencies
This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.
2. Missing __init__.py files
This error could also result from missing
__init__.pyfiles in your module folders. Make sure that you place one file in each sub-folder.