source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11180:c0e3d8cdbd08

Revision 11180:c0e3d8cdbd08, 23.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Added link AnchorLayout class.

Line 
1"""
2Canvas Graphics Scene
3
4"""
5
6import logging
7from operator import attrgetter
8
9from PyQt4.QtGui import QGraphicsScene, QPainter, QBrush, \
10                        QGraphicsItem
11
12from PyQt4.QtCore import Qt, QPointF, QRectF, QSizeF, QLineF, QBuffer
13
14from PyQt4.QtCore import pyqtSignal as Signal
15from PyQt4.QtCore import PYQT_VERSION_STR
16
17
18from .. import scheme
19
20from . import items
21from .layout import AnchorLayout
22from .items.utils import toGraphicsObjectIfPossible, typed_signal_mapper
23
24log = logging.getLogger(__name__)
25
26
27NodeItemSignalMapper = typed_signal_mapper(items.NodeItem)
28
29
30class CanvasScene(QGraphicsScene):
31    """A Graphics Scene for displaying and editing an Orange Scheme.
32    """
33
34    node_item_added = Signal(items.NodeItem)
35    """An node item has been added to the scene"""
36
37    node_item_removed = Signal(items.LinkItem)
38    """An node item has been removed from the scene"""
39
40    node_item_position_changed = Signal(items.NodeItem, QPointF)
41    """The position of a node has changed"""
42
43    node_item_double_clicked = Signal(items.NodeItem)
44    """An node item has been double clicked"""
45
46    node_item_activated = Signal(items.NodeItem)
47    """An node item has been activated (clicked)"""
48
49    node_item_hovered = Signal(items.NodeItem)
50    """An node item has been hovered"""
51
52    link_item_added = Signal(items.LinkItem)
53    """A new link item has been added to the scene"""
54
55    link_item_removed = Signal(items.LinkItem)
56    """Link item has been removed"""
57
58    link_item_hovered = Signal(items.LinkItem)
59    """Link item has been hovered"""
60
61    annotation_added = Signal(items.annotationitem.Annotation)
62    """Annotation item has been added"""
63
64    annotation_removed = Signal(items.annotationitem.Annotation)
65    """Annotation item has been removed"""
66
67    def __init__(self, *args, **kwargs):
68        QGraphicsScene.__init__(self, *args, **kwargs)
69
70        self.scheme = None
71        self.registry = None
72
73        # All node items
74        self.__node_items = []
75        # Mapping from SchemeNodes to canvas items
76        self.__item_for_node = {}
77        # All link items
78        self.__link_items = []
79        # Mapping from SchemeLinks to canvas items.
80        self.__item_for_link = {}
81
82        # All annotation items
83        self.__annotation_items = []
84        # Mapping from SchemeAnnotations to canvas items.
85        self.__item_for_annotation = {}
86
87        # Is the scene editable
88        self.editable = True
89
90        # Anchor Layout
91        self.__anchor_layout = AnchorLayout()
92        self.addItem(self.__anchor_layout)
93
94        self.user_interaction_handler = None
95
96        self.activated_mapper = NodeItemSignalMapper(self)
97        self.activated_mapper.pyMapped.connect(
98            self.node_item_activated
99        )
100
101        self.hovered_mapper = NodeItemSignalMapper(self)
102        self.hovered_mapper.pyMapped.connect(
103            self.node_item_hovered
104        )
105
106        self.position_change_mapper = NodeItemSignalMapper(self)
107        self.position_change_mapper.pyMapped.connect(
108            self._on_position_change
109        )
110
111        log.info("'%s' intitialized." % self)
112
113    def clear_scene(self):
114        self.scheme = None
115        self.__node_items = []
116        self.__item_for_node = {}
117        self.__link_items = []
118        self.__item_for_link = {}
119        self.__annotation_items = []
120        self.__item_for_annotation = {}
121
122        self.__anchor_layout.deleteLater()
123
124        self.user_interaction_handler = None
125
126        self.clear()
127        log.info("'%s' cleared." % self)
128
129    def set_scheme(self, scheme):
130        """Set the scheme to display and edit. Populates the scene
131        with nodes and links already in the scheme.
132
133        """
134        if self.scheme is not None:
135            # Clear the old scheme
136            self.scheme.node_added.disconnect(self.add_node)
137            self.scheme.node_removed.disconnect(self.remove_node)
138
139            self.scheme.link_added.disconnect(self.add_link)
140            self.scheme.link_removed.disconnect(self.remove_link)
141
142            self.scheme.annotation_added.disconnect(self.add_annotation)
143            self.scheme.annotation_removed.disconnect(self.remove_annotation)
144
145            self.scheme.node_state_changed.disconnect(
146                self.on_widget_state_change
147            )
148            self.scheme.channel_state_changed.disconnect(
149                self.on_link_state_change
150            )
151
152            self.clear_scene()
153
154        log.info("Setting scheme '%s' on '%s'" % (scheme, self))
155
156        self.scheme = scheme
157        if self.scheme is not None:
158            self.scheme.node_added.connect(self.add_node)
159            self.scheme.node_removed.connect(self.remove_node)
160
161            self.scheme.link_added.connect(self.add_link)
162            self.scheme.link_removed.connect(self.remove_link)
163
164            self.scheme.annotation_added.connect(self.add_annotation)
165            self.scheme.annotation_removed.connect(self.remove_annotation)
166
167            self.scheme.node_state_changed.connect(
168                self.on_widget_state_change
169            )
170            self.scheme.channel_state_changed.connect(
171                self.on_link_state_change
172            )
173
174            self.scheme.topology_changed.connect(self.on_scheme_change)
175
176        for node in scheme.nodes:
177            self.add_node(node)
178
179        for link in scheme.links:
180            self.add_link(link)
181
182        for annot in scheme.annotations:
183            self.add_annotation(annot)
184
185    def set_registry(self, registry):
186        """Set the widget registry.
187        """
188        log.info("Setting registry '%s on '%s'." % (registry, self))
189        self.registry = registry
190
191    def set_anchor_layout(self, layout):
192        if self.__anchor_layout != layout:
193            if self.__anchor_layout:
194                self.__anchor_layout.deleteLater()
195                self.__anchor_layout = None
196
197            self.__anchor_layout = layout
198
199    def anchor_layout(self):
200        return self.__anchor_layout
201
202    def add_node_item(self, item):
203        """Add a :class:`NodeItem` instance to the scene.
204        """
205        if item in self.__node_items:
206            raise ValueError("%r is already in the scene." % item)
207
208        if item.pos().isNull():
209            if self.__node_items:
210                pos = self.__node_items[-1].pos() + QPointF(150, 0)
211            else:
212                pos = QPointF(150, 150)
213
214            item.setPos(pos)
215
216        # Set signal mappings
217        self.activated_mapper.setPyMapping(item, item)
218        item.activated.connect(self.activated_mapper.pyMap)
219
220        self.hovered_mapper.setPyMapping(item, item)
221        item.hovered.connect(self.hovered_mapper.pyMap)
222
223        self.position_change_mapper.setPyMapping(item, item)
224        item.positionChanged.connect(self.position_change_mapper.pyMap)
225
226        self.addItem(item)
227
228        self.__node_items.append(item)
229
230        self.node_item_added.emit(item)
231
232        log.info("Added item '%s' to '%s'" % (item, self))
233        return item
234
235    def add_node(self, node):
236        """Add and return a default constructed `NodeItem` for a
237        `SchemeNode` instance. If the node is already in the scene
238        do nothing and just return its item.
239
240        """
241        if node in self.__item_for_node:
242            # Already added
243            return self.__item_for_node[node]
244
245        item = self.new_node_item(node.description)
246
247        if node.position:
248            pos = QPointF(*node.position)
249            item.setPos(pos)
250
251        self.__item_for_node[node] = item
252
253        node.position_changed.connect(self.__on_node_pos_changed)
254        node.title_changed.connect(item.setTitle)
255        node.progress_changed.connect(item.setProgress)
256        node.processing_state_changed.connect(item.setProcessingState)
257        return self.add_node_item(item)
258
259    def new_node_item(self, widget_desc, category_desc=None):
260        """Construct an new `NodeItem` from a `WidgetDescription`.
261        Optionally also set `CategoryDescription`.
262
263        """
264        item = items.NodeItem()
265        item.setWidgetDescription(widget_desc)
266
267        if category_desc is None and self.registry and widget_desc.category:
268            category_desc = self.registry.category(widget_desc.category)
269
270        if category_desc is None and self.registry is not None:
271            try:
272                category_desc = self.registry.category(widget_desc.category)
273            except KeyError:
274                pass
275
276        if category_desc is not None:
277            item.setWidgetCategory(category_desc)
278
279        return item
280
281    def remove_node_item(self, item):
282        """Remove `item` (:class:`NodeItem`) from the scene.
283        """
284        self.activated_mapper.removePyMappings(item)
285        self.hovered_mapper.removePyMappings(item)
286
287        item.hide()
288        self.removeItem(item)
289        self.__node_items.remove(item)
290
291        self.node_item_removed.emit(item)
292
293        log.info("Removed item '%s' from '%s'" % (item, self))
294
295    def remove_node(self, node):
296        """Remove the `NodeItem` instance that was previously constructed for
297        a `SchemeNode` node using the `add_node` method.
298
299        """
300        item = self.__item_for_node.pop(node)
301
302        node.position_changed.disconnect(self.__on_node_pos_changed)
303        node.title_changed.disconnect(item.setTitle)
304        node.progress_changed.disconnect(item.setProgress)
305        node.processing_state_changed.disconnect(item.setProcessingState)
306
307        self.remove_node_item(item)
308
309    def node_items(self):
310        """Return all :class:`NodeItem` instances in the scene.
311        """
312        return list(self.__node_items)
313
314    def add_link_item(self, item):
315        """Add a link (:class:`LinkItem`)to the scene.
316        """
317        if item.scene() is not self:
318            self.addItem(item)
319
320        self.__link_items.append(item)
321
322        self.link_item_added.emit(item)
323
324        log.info("Added link %r -> %r to '%s'" % \
325                 (item.sourceItem.title, item.sinkItem.title, self))
326
327        self.__anchor_layout.invalidateLink(item)
328
329        return item
330
331    def add_link(self, scheme_link):
332        """Create and add a `LinkItem` instance for a `SchemeLink`
333        instance. If the link is already in the scene do nothing
334        and just return its `LinkItem`.
335
336        """
337        if scheme_link in self.__item_for_link:
338            return self.__item_for_link[scheme_link]
339
340        source = self.__item_for_node[scheme_link.source_node]
341        sink = self.__item_for_node[scheme_link.sink_node]
342
343        item = self.new_link_item(source, scheme_link.source_channel,
344                                  sink, scheme_link.sink_channel)
345
346        item.setEnabled(scheme_link.enabled)
347
348        scheme_link.enabled_changed.connect(item.setEnabled)
349
350        self.add_link_item(item)
351        self.__item_for_link[scheme_link] = item
352        return item
353
354    def new_link_item(self, source_item, source_channel,
355                      sink_item, sink_channel):
356        """Construct and return a new `LinkItem`
357        """
358        item = items.LinkItem()
359        item.setSourceItem(source_item)
360        item.setSinkItem(sink_item)
361        return item
362
363    def remove_link_item(self, item):
364        """Remove a link (:class:`LinkItem`) from the scene.
365        """
366        self.__link_items.remove(item)
367
368        # Remove the anchor points.
369        item.removeLink()
370        self.removeItem(item)
371        self.link_item_removed.emit(item)
372
373        log.info("Removed link '%s' from '%s'" % (item, self))
374
375        return item
376
377    def remove_link(self, scheme_link):
378        """ Remove a `LinkItem` instance that was previously constructed for
379        a `SchemeLink` node using the `add_link` method.
380
381        """
382        item = self.__item_for_link.pop(scheme_link)
383        scheme_link.enabled_changed.disconnect(item.setEnabled)
384        self.remove_link_item(item)
385
386    def link_items(self):
387        """Return all :class:`LinkItems` in the scene.
388
389        """
390        return list(self.__link_items)
391
392    def add_annotation_item(self, annotation):
393        """Add an `Annotation` item to the scene.
394
395        """
396        self.__annotation_items.append(annotation)
397        self.addItem(annotation)
398        self.annotation_added.emit(annotation)
399        return annotation
400
401    def add_annotation(self, scheme_annot):
402        """Create a new item for :class:`SchemeAnnotation` and add it
403        to the scene. If the `scheme_annot` is already in the scene do
404        nothing and just return its item.
405
406        """
407        if scheme_annot in self.__item_for_annotation:
408            # Already added
409            return self.__item_for_annotation[scheme_annot]
410
411        if isinstance(scheme_annot, scheme.SchemeTextAnnotation):
412            item = items.TextAnnotation()
413            item.setPlainText(scheme_annot.text)
414            x, y, w, h = scheme_annot.rect
415            item.setPos(x, y)
416            item.resize(w, h)
417            item.setTextInteractionFlags(Qt.TextEditorInteraction)
418            scheme_annot.text_changed.connect(item.setPlainText)
419
420        elif isinstance(scheme_annot, scheme.SchemeArrowAnnotation):
421            item = items.ArrowAnnotation()
422            start, end = scheme_annot.start_pos, scheme_annot.end_pos
423            item.setLine(QLineF(QPointF(*start), QPointF(*end)))
424
425        scheme_annot.geometry_changed.connect(
426            self.__on_scheme_annot_geometry_change
427        )
428
429        self.add_annotation_item(item)
430        self.__item_for_annotation[scheme_annot] = item
431
432        return item
433
434    def remove_annotation_item(self, annotation):
435        """Remove an `Annotation` item from the scene.
436
437        """
438        self.__annotation_items.remove(annotation)
439        self.removeItem(annotation)
440        self.annotation_removed.emit(annotation)
441
442    def remove_annotation(self, scheme_annotation):
443        item = self.__item_for_annotation.pop(scheme_annotation)
444
445        scheme_annotation.geometry_changed.disconnect(
446            self.__on_scheme_annot_geometry_change
447        )
448
449        if isinstance(scheme_annotation, scheme.SchemeTextAnnotation):
450            scheme_annotation.text_changed.disconnect(
451                item.setPlainText
452            )
453
454        self.remove_annotation_item(item)
455
456    def annotation_items(self):
457        """Return all `Annotation` items in the scene.
458
459        """
460        return self.__annotation_items
461
462    def item_for_annotation(self, scheme_annotation):
463        return self.__item_for_annotation[scheme_annotation]
464
465    def annotation_for_item(self, item):
466        rev = dict(reversed(item) \
467                   for item in self.__item_for_annotation.items())
468        return rev[item]
469
470    def commit_scheme_node(self, node):
471        """Commit the `node` into the scheme.
472        """
473        if not self.editable:
474            raise Exception("Scheme not editable.")
475
476        if node not in self.__item_for_node:
477            raise ValueError("No 'NodeItem' for node.")
478
479        item = self.__item_for_node[node]
480
481        try:
482            self.scheme.add_node(node)
483        except Exception:
484            log.error("An unexpected error occurred while commiting node '%s'",
485                      node, exc_info=True)
486            # Cleanup (remove the node item)
487            self.remove_node_item(item)
488            raise
489
490        log.info("Commited node '%s' from '%s' to '%s'" % \
491                 (node, self, self.scheme))
492
493    def commit_scheme_link(self, link):
494        """Commit a scheme link.
495        """
496        if not self.editable:
497            raise Exception("Scheme not editable")
498
499        if link not in self.__item_for_link:
500            raise ValueError("No 'LinkItem' for link.")
501
502        self.scheme.add_link(link)
503        log.info("Commited link '%s' from '%s' to '%s'" % \
504                 (link, self, self.scheme))
505
506    def node_for_item(self, item):
507        """Return the `SchemeNode` for the `item`.
508        """
509        rev = dict([(v, k) for k, v in self.__item_for_node.items()])
510        return rev[item]
511
512    def item_for_node(self, node):
513        """Return the :class:`NodeItem` instance for a :class:`SchemeNode`.
514        """
515        return self.__item_for_node[node]
516
517    def link_for_item(self, item):
518        """Return the `SchemeLink for `item` (:class:`LinkItem`).
519        """
520        rev = dict([(v, k) for k, v in self.__item_for_link.items()])
521        return rev[item]
522
523    def item_for_link(self, link):
524        """Return the :class:`LinkItem` for a :class:`SchemeLink`
525        """
526        return self.__item_for_link[link]
527
528    def selected_node_items(self):
529        """Return the selected :class:`NodeItem`'s.
530        """
531        return [item for item in self.__node_items if item.isSelected()]
532
533    def selected_annotation_items(self):
534        """Return the selected :class:`Annotation`'s
535        """
536        return [item for item in self.__annotation_items if item.isSelected()]
537
538    def node_links(self, node_item):
539        """Return all links from the `node_item` (:class:`NodeItem`).
540        """
541        return self.node_output_links(node_item) + \
542               self.node_input_links(node_item)
543
544    def node_output_links(self, node_item):
545        """Return a list of all output links from `node_item`.
546        """
547        return [link for link in self.__link_items
548                if link.sourceItem == node_item]
549
550    def node_input_links(self, node_item):
551        """Return a list of all input links for `node_item`.
552        """
553        return [link for link in self.__link_items
554                if link.sinkItem == node_item]
555
556    def neighbor_nodes(self, node_item):
557        """Return a list of `node_item`'s (class:`NodeItem`) neighbor nodes.
558        """
559        neighbors = map(attrgetter("sourceItem"),
560                        self.node_input_links(node_item))
561
562        neighbors.extend(map(attrgetter("sinkItem"),
563                             self.node_output_links(node_item)))
564        return neighbors
565
566    def on_widget_state_change(self, widget, state):
567        pass
568
569    def on_link_state_change(self, link, state):
570        pass
571
572    def on_scheme_change(self, ):
573        pass
574
575    def _on_position_change(self, item):
576        # Invalidate the anchor point layout and schedule a layout.
577        self.__anchor_layout.invalidateNode(item)
578
579        self.node_item_position_changed.emit(item, item.pos())
580
581    def __on_node_pos_changed(self, pos):
582        node = self.sender()
583        item = self.__item_for_node[node]
584        item.setPos(*pos)
585
586    def __on_scheme_annot_geometry_change(self):
587        annot = self.sender()
588        item = self.__item_for_annotation[annot]
589        if isinstance(annot, scheme.SchemeTextAnnotation):
590            item.setGeometry(QRectF(*annot.rect))
591        elif isinstance(annot, scheme.SchemeArrowAnnotation):
592            p1 = item.mapFromScene(QPointF(*annot.start_pos))
593            p2 = item.mapFromScene(QPointF(*annot.end_pos))
594            item.setLine(QLineF(p1, p2))
595        else:
596            pass
597
598    def item_at(self, pos, type_or_tuple=None):
599        rect = QRectF(pos - QPointF(1.5, 1.5), QSizeF(3, 3))
600        items = self.items(rect)
601        if type_or_tuple:
602            items = [i for i in items if isinstance(i, type_or_tuple)]
603
604        return items[0] if items else None
605
606    if PYQT_VERSION_STR < "4.9":
607        # For QGraphicsObject subclasses items, itemAt ... return a
608        # QGraphicsItem wrapper instance and not the actual class instance.
609        def itemAt(self, *args, **kwargs):
610            item = QGraphicsScene.itemAt(self, *args, **kwargs)
611            return toGraphicsObjectIfPossible(item)
612
613        def items(self, *args, **kwargs):
614            items = QGraphicsScene.items(self, *args, **kwargs)
615            return map(toGraphicsObjectIfPossible, items)
616
617        def selectedItems(self, *args, **kwargs):
618            return map(toGraphicsObjectIfPossible,
619                       QGraphicsScene.selectedItems(self, *args, **kwargs))
620
621        def collidingItems(self, *args, **kwargs):
622            return map(toGraphicsObjectIfPossible,
623                       QGraphicsScene.collidingItems(self, *args, **kwargs))
624
625        def focusItem(self, *args, **kwargs):
626            item = QGraphicsScene.focusItem(self, *args, **kwargs)
627            return toGraphicsObjectIfPossible(item)
628
629        def mouseGrabberItem(self, *args, **kwargs):
630            item = QGraphicsScene.mouseGrabberItem(self, *args, **kwargs)
631            return toGraphicsObjectIfPossible(item)
632
633    def mousePressEvent(self, event):
634        if self.user_interaction_handler and \
635                self.user_interaction_handler.mousePressEvent(event):
636            return
637
638        # Right (context) click on the node item. If the widget is not
639        # in the current selection then select the widget (only the widget).
640        # Else simply return and let customContextMenuReqested signal
641        # handle it
642        shape_item = self.item_at(event.scenePos(), items.NodeItem)
643        if shape_item and event.button() == Qt.RightButton and \
644                shape_item.flags() & QGraphicsItem.ItemIsSelectable:
645            if not shape_item.isSelected():
646                self.clearSelection()
647                shape_item.setSelected(True)
648
649        return QGraphicsScene.mousePressEvent(self, event)
650
651    def mouseMoveEvent(self, event):
652        if self.user_interaction_handler and \
653                self.user_interaction_handler.mouseMoveEvent(event):
654            return
655
656        return QGraphicsScene.mouseMoveEvent(self, event)
657
658    def mouseReleaseEvent(self, event):
659        if self.user_interaction_handler and \
660                self.user_interaction_handler.mouseReleaseEvent(event):
661            return
662        return QGraphicsScene.mouseReleaseEvent(self, event)
663
664    def mouseDoubleClickEvent(self, event):
665        if self.user_interaction_handler and \
666                self.user_interaction_handler.mouseDoubleClickEvent(event):
667            return
668
669        return QGraphicsScene.mouseDoubleClickEvent(self, event)
670
671    def keyPressEvent(self, event):
672        if self.user_interaction_handler and \
673                self.user_interaction_handler.keyPressEvent(event):
674            return
675        return QGraphicsScene.keyPressEvent(self, event)
676
677    def keyReleaseEvent(self, event):
678        if self.user_interaction_handler and \
679                self.user_interaction_handler.keyReleaseEvent(event):
680            return
681        return QGraphicsScene.keyReleaseEvent(self, event)
682
683    def set_user_interaction_handler(self, handler):
684        if self.user_interaction_handler and \
685                not self.user_interaction_handler.finished:
686            self.user_interaction_handler.cancel()
687
688        log.info("Setting interaction '%s' to '%s'" % (handler, self))
689
690        self.user_interaction_handler = handler
691        if handler:
692            handler.start()
693
694    def __str__(self):
695        return "%s(objectName=%r, ...)" % \
696                (type(self).__name__, str(self.objectName()))
697
698
699def grab_svg(scene):
700    """Return a SVG rendering of the scene contents.
701    """
702    from PyQt4.QtSvg import QSvgGenerator
703    svg_buffer = QBuffer()
704    gen = QSvgGenerator()
705    gen.setOutputDevice(svg_buffer)
706
707    items_rect = scene.itemsBoundingRect().adjusted(-10, -10, 10, 10)
708
709    if items_rect.isNull():
710        items_rect = QRectF(0, 0, 10, 10)
711
712    width, height = items_rect.width(), items_rect.height()
713    rect_ratio = float(width) / height
714
715    # Keep a fixed aspect ratio.
716    aspect_ratio = 1.618
717    if rect_ratio > aspect_ratio:
718        height = int(height * rect_ratio / aspect_ratio)
719    else:
720        width = int(width * aspect_ratio / rect_ratio)
721
722    target_rect = QRectF(0, 0, width, height)
723    source_rect = QRectF(0, 0, width, height)
724    source_rect.moveCenter(items_rect.center())
725
726    gen.setSize(target_rect.size().toSize())
727    gen.setViewBox(target_rect)
728
729    painter = QPainter(gen)
730
731    # Draw background.
732    painter.setBrush(QBrush(Qt.white))
733    painter.drawRect(target_rect)
734
735    # Render the scene
736    scene.render(painter, target_rect, source_rect)
737    painter.end()
738
739    return unicode(svg_buffer.buffer())
Note: See TracBrowser for help on using the repository browser.