source: orange/Orange/OrangeCanvas/canvas/scene.py @ 11172:b9add0151621

Revision 11172:b9add0151621, 23.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Compatibility fixes for PyQt4 < 4.9

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