source: orange/Orange/OrangeCanvas/document/interactions.py @ 11241:72cdee438307

Revision 11241:72cdee438307, 34.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Fixed scene hit testing for mouse press events.

Line 
1"""
2User interaction handlers for CanvasScene.
3
4"""
5import logging
6
7from PyQt4.QtGui import (
8    QApplication, QGraphicsRectItem, QPen, QBrush, QColor, QFontMetrics
9)
10
11from PyQt4.QtCore import Qt, QObject, QSizeF, QPointF, QRect, QRectF, QLineF
12from PyQt4.QtCore import pyqtSignal as Signal
13
14from ..registry.qt import QtWidgetRegistry
15from .. import scheme
16from ..canvas import items
17from ..canvas.items import controlpoints
18from . import commands
19
20log = logging.getLogger(__name__)
21
22
23class UserInteraction(QObject):
24    # cancel reason flags
25    NoReason = 0  # No specified reason
26    UserCancelReason = 1  # User canceled the operation (e.g. pressing ESC)
27    InteractionOverrideReason = 3  # Another interaction was set
28    ErrorReason = 4  # An internal error occurred
29    OtherReason = 5
30
31    # Emitted when the interaction is set on the scene.
32    started = Signal()
33
34    # Emitted when the interaction finishes successfully.
35    finished = Signal()
36
37    # Emitted when the interaction ends (canceled or finished)
38    ended = Signal()
39
40    # Emitted when the interaction is canceled.
41    canceled = Signal([], [int])
42
43    def __init__(self, document, parent=None, deleteOnEnd=True):
44        QObject.__init__(self, parent)
45        self.document = document
46        self.scene = document.scene()
47        self.scheme = document.scheme()
48        self.deleteOnEnd = deleteOnEnd
49
50        self.cancelOnEsc = False
51
52        self.__finished = False
53        self.__canceled = False
54        self.__cancelReason = self.NoReason
55
56    def start(self):
57        """Start the interaction. This is called by the scene when
58        the interaction is installed.
59
60        Must be called from subclass implementations.
61
62        """
63        self.started.emit()
64
65    def end(self):
66        """Finish the interaction. Restore any leftover state in
67        this method.
68
69        .. note:: This gets called from the default `cancel` implementation.
70
71        """
72        self.__finished = True
73
74        if self.scene.user_interaction_handler is self:
75            self.scene.set_user_interaction_handler(None)
76
77        if self.__canceled:
78            self.canceled.emit()
79            self.canceled[int].emit(self.__cancelReason)
80        else:
81            self.finished.emit()
82
83        self.ended.emit()
84
85        if self.deleteOnEnd:
86            self.deleteLater()
87
88    def cancel(self, reason=OtherReason):
89        """Cancel the interaction for `reason`.
90        """
91
92        self.__canceled = True
93        self.__cancelReason = reason
94
95        self.end()
96
97    def isFinished(self):
98        """Has the interaction finished.
99        """
100        return self.__finished
101
102    def isCanceled(self):
103        """Was the interaction canceled.
104        """
105        return self.__canceled
106
107    def cancelReason(self):
108        """Return the reason the interaction was canceled.
109        """
110        return self.__cancelReason
111
112    def mousePressEvent(self, event):
113        return False
114
115    def mouseMoveEvent(self, event):
116        return False
117
118    def mouseReleaseEvent(self, event):
119        return False
120
121    def mouseDoubleClickEvent(self, event):
122        return False
123
124    def keyPressEvent(self, event):
125        if self.cancelOnEsc and event.key() == Qt.Key_Escape:
126            self.cancel(self.UserCancelReason)
127        return False
128
129    def keyReleaseEvent(self, event):
130        return False
131
132
133class NoPossibleLinksError(ValueError):
134    pass
135
136
137def reversed_arguments(func):
138    """Return a function with reversed argument order.
139    """
140    def wrapped(*args):
141        return func(*reversed(args))
142    return wrapped
143
144
145class NewLinkAction(UserInteraction):
146    """User drags a new link from an existing node anchor item to create
147    a connection between two existing nodes or to a new node if the release
148    is over an empty area, in which case a quick menu for new node selection
149    is presented to the user.
150
151    """
152    # direction of the drag
153    FROM_SOURCE = 1
154    FROM_SINK = 2
155
156    def __init__(self, document, *args, **kwargs):
157        UserInteraction.__init__(self, document, *args, **kwargs)
158        self.source_item = None
159        self.sink_item = None
160        self.from_item = None
161        self.direction = None
162
163        self.current_target_item = None
164        self.tmp_link_item = None
165        self.tmp_anchor_point = None
166        self.cursor_anchor_point = None
167
168    def remove_tmp_anchor(self):
169        """Remove a temp anchor point from the current target item.
170        """
171        if self.direction == self.FROM_SOURCE:
172            self.current_target_item.removeInputAnchor(self.tmp_anchor_point)
173        else:
174            self.current_target_item.removeOutputAnchor(self.tmp_anchor_point)
175        self.tmp_anchor_point = None
176
177    def create_tmp_anchor(self, item):
178        """Create a new tmp anchor at the item (`NodeItem`).
179        """
180        assert(self.tmp_anchor_point is None)
181        if self.direction == self.FROM_SOURCE:
182            self.tmp_anchor_point = item.newInputAnchor()
183        else:
184            self.tmp_anchor_point = item.newOutputAnchor()
185
186    def can_connect(self, target_item):
187        """Is the connection between `self.from_item` (item where the drag
188        started) and `target_item`.
189
190        """
191        node1 = self.scene.node_for_item(self.from_item)
192        node2 = self.scene.node_for_item(target_item)
193
194        if self.direction == self.FROM_SOURCE:
195            return bool(self.scheme.propose_links(node1, node2))
196        else:
197            return bool(self.scheme.propose_links(node2, node1))
198
199    def set_link_target_anchor(self, anchor):
200        """Set the temp line target anchor
201        """
202        if self.direction == self.FROM_SOURCE:
203            self.tmp_link_item.setSinkItem(None, anchor)
204        else:
205            self.tmp_link_item.setSourceItem(None, anchor)
206
207    def target_node_item_at(self, pos):
208        """Return a suitable NodeItem on which a link can be dropped.
209        """
210        # Test for a suitable NodeAnchorItem or NodeItem at pos.
211        if self.direction == self.FROM_SOURCE:
212            anchor_type = items.SinkAnchorItem
213        else:
214            anchor_type = items.SourceAnchorItem
215
216        item = self.scene.item_at(pos, (anchor_type, items.NodeItem))
217
218        if isinstance(item, anchor_type):
219            item = item.parentNodeItem()
220
221        return item
222
223    def mousePressEvent(self, event):
224        anchor_item = self.scene.item_at(event.scenePos(),
225                                         items.NodeAnchorItem,
226                                         buttons=Qt.LeftButton)
227        if anchor_item and event.button() == Qt.LeftButton:
228            # Start a new link starting at item
229            self.from_item = anchor_item.parentNodeItem()
230            if isinstance(anchor_item, items.SourceAnchorItem):
231                self.direction = NewLinkAction.FROM_SOURCE
232                self.source_item = self.from_item
233            else:
234                self.direction = NewLinkAction.FROM_SINK
235                self.sink_item = self.from_item
236
237            event.accept()
238            return True
239        else:
240            # Whoever put us in charge did not know what he was doing.
241            self.cancel(self.ErrorReason)
242            return False
243
244    def mouseMoveEvent(self, event):
245        if not self.tmp_link_item:
246            # On first mouse move event create the temp link item and
247            # initialize it to follow the `cursor_anchor_point`.
248            self.tmp_link_item = items.LinkItem()
249            # An anchor under the cursor for the duration of this action.
250            self.cursor_anchor_point = items.AnchorPoint()
251            self.cursor_anchor_point.setPos(event.scenePos())
252
253            # Set the `fixed` end of the temp link (where the drag started).
254            if self.direction == self.FROM_SOURCE:
255                self.tmp_link_item.setSourceItem(self.source_item)
256            else:
257                self.tmp_link_item.setSinkItem(self.sink_item)
258
259            self.set_link_target_anchor(self.cursor_anchor_point)
260            self.scene.addItem(self.tmp_link_item)
261
262        # `NodeItem` at the cursor position
263        item = self.target_node_item_at(event.scenePos())
264
265        if self.current_target_item is not None and \
266                (item is None or item is not self.current_target_item):
267            # `current_target_item` is no longer under the mouse cursor
268            # (was replaced by another item or the the cursor was moved over
269            # an empty scene spot.
270            log.info("%r is no longer the target.", self.current_target_item)
271            self.remove_tmp_anchor()
272            self.current_target_item = None
273
274        if item is not None and item is not self.from_item:
275            # The mouse is over an node item (different from the starting node)
276            if self.current_target_item is item:
277                # Avoid reseting the points
278                pass
279            elif self.can_connect(item):
280                # Grab a new anchor
281                log.info("%r is the new target.", item)
282                self.create_tmp_anchor(item)
283                self.set_link_target_anchor(self.tmp_anchor_point)
284                self.current_target_item = item
285            else:
286                log.info("%r does not have compatible channels", item)
287                self.set_link_target_anchor(self.cursor_anchor_point)
288                # TODO: How to indicate that the connection is not possible?
289                #       The node's anchor could be drawn with a 'disabled'
290                #       palette
291        else:
292            self.set_link_target_anchor(self.cursor_anchor_point)
293
294        self.cursor_anchor_point.setPos(event.scenePos())
295
296        return True
297
298    def mouseReleaseEvent(self, event):
299        if self.tmp_link_item:
300            item = self.target_node_item_at(event.scenePos())
301            node = None
302            stack = self.document.undoStack()
303            stack.beginMacro("Add link")
304
305            if item:
306                # If the release was over a widget item
307                # then connect them
308                node = self.scene.node_for_item(item)
309            else:
310                # Release on an empty canvas part
311                # Show a quick menu popup for a new widget creation.
312                try:
313                    node = self.create_new(event)
314                except Exception:
315                    log.error("Failed to create a new node, ending.",
316                              exc_info=True)
317                    node = None
318
319                if node is not None:
320                    self.document.addNode(node)
321
322            if node is not None:
323                self.connect_existing(node)
324            else:
325                self.end()
326
327            stack.endMacro()
328        else:
329            self.end()
330            return False
331
332    def create_new(self, event):
333        """Create and return a new node with a QuickWidgetMenu.
334        """
335        pos = event.screenPos()
336        menu = self.document.quickMenu()
337        node = self.scene.node_for_item(self.from_item)
338        from_desc = node.description
339
340        def is_compatible(source, sink):
341            return any(scheme.compatible_channels(output, input) \
342                       for output in source.outputs \
343                       for input in sink.inputs)
344
345        if self.direction == self.FROM_SINK:
346            # Reverse the argument order.
347            is_compatible = reversed_arguments(is_compatible)
348
349        def filter(index):
350            desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE)
351            if desc.isValid():
352                desc = desc.toPyObject()
353                return is_compatible(from_desc, desc)
354            else:
355                return False
356
357        menu.setFilterFunc(filter)
358        try:
359            action = menu.exec_(pos)
360        finally:
361            menu.setFilterFunc(None)
362
363        if action:
364            item = action.property("item").toPyObject()
365            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()
366            pos = event.scenePos()
367            node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
368            return node
369
370    def connect_existing(self, node):
371        """Connect anchor_item to `node`.
372        """
373        if self.direction == self.FROM_SOURCE:
374            source_item = self.source_item
375            source_node = self.scene.node_for_item(source_item)
376            sink_node = node
377        else:
378            source_node = node
379            sink_item = self.sink_item
380            sink_node = self.scene.node_for_item(sink_item)
381
382        try:
383            possible = self.scheme.propose_links(source_node, sink_node)
384
385            log.debug("proposed (weighted) links: %r",
386                      [(s1.name, s2.name, w) for s1, s2, w in possible])
387
388            if not possible:
389                raise NoPossibleLinksError
390
391            source, sink, w = possible[0]
392            links_to_add = [(source, sink)]
393
394            show_link_dialog = False
395
396            # Ambiguous new link request.
397            if len(possible) >= 2:
398                # Check for possible ties in the proposed link weights
399                _, _, w2 = possible[1]
400                if w == w2:
401                    show_link_dialog = True
402
403                # Check for destructive action (i.e. would the new link
404                # replace a previous link)
405                if sink.single and self.scheme.find_links(sink_node=sink_node,
406                                                          sink_channel=sink):
407                    show_link_dialog = True
408
409                if show_link_dialog:
410                    links_action = EditNodeLinksAction(
411                                    self.document, source_node, sink_node)
412                    try:
413                        links_action.edit_links()
414                    except Exception:
415                        log.error("'EditNodeLinksAction' failed",
416                                  exc_info=True)
417                        raise
418                    # EditNodeLinksAction already commits the links on accepted
419                    links_to_add = []
420
421            for source, sink in links_to_add:
422                if sink.single:
423                    # Remove an existing link to the sink channel if present.
424                    existing_link = self.scheme.find_links(
425                        sink_node=sink_node, sink_channel=sink
426                    )
427
428                    if existing_link:
429                        self.document.removeLink(existing_link[0])
430
431                # Check if the new link is a duplicate of an existing link
432                duplicate = self.scheme.find_links(
433                    source_node, source, sink_node, sink
434                )
435
436                if duplicate:
437                    # Do nothing.
438                    continue
439
440                # Remove temp items before creating a new link
441                self.cleanup()
442
443                link = scheme.SchemeLink(source_node, source, sink_node, sink)
444                self.document.addLink(link)
445
446        except scheme.IncompatibleChannelTypeError:
447            log.info("Cannot connect: invalid channel types.")
448            self.cancel()
449        except scheme.SchemeTopologyError:
450            log.info("Cannot connect: connection creates a cycle.")
451            self.cancel()
452        except NoPossibleLinksError:
453            log.info("Cannot connect: no possible links.")
454            self.cancel()
455        except Exception:
456            log.error("An error occurred during the creation of a new link.",
457                      exc_info=True)
458            self.cancel()
459
460        if not self.isFinished():
461            self.end()
462
463    def end(self):
464        self.cleanup()
465        UserInteraction.end(self)
466
467    def cancel(self, reason=UserInteraction.OtherReason):
468        self.cleanup()
469        UserInteraction.cancel(self, reason)
470
471    def cleanup(self):
472        """Cleanup all temp items in the scene that are left.
473        """
474        if self.tmp_link_item:
475            self.tmp_link_item.setSinkItem(None)
476            self.tmp_link_item.setSourceItem(None)
477
478            if self.tmp_link_item.scene():
479                self.scene.removeItem(self.tmp_link_item)
480
481            self.tmp_link_item = None
482
483        if self.current_target_item:
484            self.remove_tmp_anchor()
485            self.current_target_item = None
486
487        if self.cursor_anchor_point and self.cursor_anchor_point.scene():
488            self.scene.removeItem(self.cursor_anchor_point)
489            self.cursor_anchor_point = None
490
491
492class NewNodeAction(UserInteraction):
493    """Present the user with a quick menu for node selection and
494    create the selected node.
495
496    """
497
498    def mousePressEvent(self, event):
499        if event.button() == Qt.RightButton:
500            self.create_new(event.screenPos())
501            self.end()
502
503    def create_new(self, pos):
504        """Create a new widget with a QuickWidgetMenu at `pos`
505        (in screen coordinates).
506
507        """
508        menu = self.document.quickMenu()
509        menu.setFilterFunc(None)
510
511        action = menu.exec_(pos)
512        if action:
513            item = action.property("item").toPyObject()
514            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()
515            # Get the scene position
516            view = self.document.view()
517            pos = view.mapToScene(view.mapFromGlobal(pos))
518            node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
519            self.document.addNode(node)
520            return node
521
522
523class RectangleSelectionAction(UserInteraction):
524    """Select items in the scene using a Rectangle selection
525    """
526    def __init__(self, document, *args, **kwargs):
527        UserInteraction.__init__(self, document, *args, **kwargs)
528        self.initial_selection = None
529        self.last_selection = None
530        self.selection_rect = None
531        self.modifiers = 0
532
533    def mousePressEvent(self, event):
534        pos = event.scenePos()
535        any_item = self.scene.item_at(pos)
536        if not any_item and event.button() & Qt.LeftButton:
537            self.modifiers = event.modifiers()
538            self.selection_rect = QRectF(pos, QSizeF(0, 0))
539            self.rect_item = QGraphicsRectItem(
540                self.selection_rect.normalized()
541            )
542
543            self.rect_item.setPen(
544                QPen(QBrush(QColor(51, 153, 255, 192)),
545                     0.4, Qt.SolidLine, Qt.RoundCap)
546            )
547
548            self.rect_item.setBrush(
549                QBrush(QColor(168, 202, 236, 192))
550            )
551
552            self.rect_item.setZValue(-100)
553
554            # Clear the focus if necessary.
555            if not self.scene.stickyFocus():
556                self.scene.clearFocus()
557
558            if not self.modifiers & Qt.ControlModifier:
559                self.scene.clearSelection()
560
561            event.accept()
562            return True
563        else:
564            self.cancel(self.ErrorReason)
565            return False
566
567    def mouseMoveEvent(self, event):
568        if not self.rect_item.scene():
569            self.scene.addItem(self.rect_item)
570        self.update_selection(event)
571        return True
572
573    def mouseReleaseEvent(self, event):
574        if event.button() == Qt.LeftButton:
575            if self.initial_selection is None:
576                # A single click.
577                self.scene.clearSelection()
578            else:
579                self.update_selection(event)
580        self.end()
581        return True
582
583    def update_selection(self, event):
584        if self.initial_selection is None:
585            self.initial_selection = set(self.scene.selectedItems())
586            self.last_selection = self.initial_selection
587
588        pos = event.scenePos()
589        self.selection_rect = QRectF(self.selection_rect.topLeft(), pos)
590
591        rect = self._bound_selection_rect(self.selection_rect.normalized())
592
593        # Need that constant otherwise the sceneRect will still grow
594        pw = self.rect_item.pen().width() + 0.5
595
596        self.rect_item.setRect(rect.adjusted(pw, pw, -pw, -pw))
597
598        selected = self.scene.items(self.selection_rect.normalized(),
599                                    Qt.IntersectsItemShape,
600                                    Qt.AscendingOrder)
601
602        selected = set([item for item in selected if \
603                        item.flags() & Qt.ItemIsSelectable])
604
605        if self.modifiers & Qt.ControlModifier:
606            for item in selected | self.last_selection | \
607                    self.initial_selection:
608                item.setSelected(
609                    (item in selected) ^ (item in self.initial_selection)
610                )
611        else:
612            for item in selected.union(self.last_selection):
613                item.setSelected(item in selected)
614
615        self.last_selection = set(self.scene.selectedItems())
616
617    def end(self):
618        self.initial_selection = None
619        self.last_selection = None
620        self.modifiers = 0
621
622        self.rect_item.hide()
623        if self.rect_item.scene() is not None:
624            self.scene.removeItem(self.rect_item)
625        UserInteraction.end(self)
626
627    def viewport_rect(self):
628        """Return the bounding rect of the document's viewport on the
629        scene.
630
631        """
632        view = self.document.view()
633        vsize = view.viewport().size()
634        viewportrect = QRect(0, 0, vsize.width(), vsize.height())
635        return view.mapToScene(viewportrect).boundingRect()
636
637    def _bound_selection_rect(self, rect):
638        """Bound the selection `rect` to a sensible size.
639        """
640        srect = self.scene.sceneRect()
641        vrect = self.viewport_rect()
642        maxrect = srect.united(vrect)
643        return rect.intersected(maxrect)
644
645
646class EditNodeLinksAction(UserInteraction):
647    def __init__(self, document, source_node, sink_node, *args, **kwargs):
648        UserInteraction.__init__(self, document, *args, **kwargs)
649        self.source_node = source_node
650        self.sink_node = sink_node
651
652    def edit_links(self):
653        from ..canvas.editlinksdialog import EditLinksDialog
654
655        log.info("Constructing a Link Editor dialog.")
656
657        parent = self.scene.views()[0]
658        dlg = EditLinksDialog(parent)
659
660        links = self.scheme.find_links(source_node=self.source_node,
661                                       sink_node=self.sink_node)
662        existing_links = [(link.source_channel, link.sink_channel)
663                          for link in links]
664
665        dlg.setNodes(self.source_node, self.sink_node)
666        dlg.setLinks(existing_links)
667
668        log.info("Executing a Link Editor Dialog.")
669        rval = dlg.exec_()
670
671        if rval == EditLinksDialog.Accepted:
672            links = dlg.links()
673
674            links_to_add = set(links) - set(existing_links)
675            links_to_remove = set(existing_links) - set(links)
676
677            stack = self.document.undoStack()
678            stack.beginMacro("Edit Links")
679
680            for source_channel, sink_channel in links_to_remove:
681                links = self.scheme.find_links(source_node=self.source_node,
682                                               source_channel=source_channel,
683                                               sink_node=self.sink_node,
684                                               sink_channel=sink_channel)
685
686                self.document.removeLink(links[0])
687
688            for source_channel, sink_channel in links_to_add:
689                link = scheme.SchemeLink(self.source_node, source_channel,
690                                         self.sink_node, sink_channel)
691
692                self.document.addLink(link)
693            stack.endMacro()
694
695
696def point_to_tuple(point):
697    return point.x(), point.y()
698
699
700class NewArrowAnnotation(UserInteraction):
701    """Create a new arrow annotation.
702    """
703    def __init__(self, document, *args, **kwargs):
704        UserInteraction.__init__(self, document, *args, **kwargs)
705        self.down_pos = None
706        self.arrow_item = None
707        self.annotation = None
708        self.color = "red"
709
710    def start(self):
711        self.document.view().setCursor(Qt.CrossCursor)
712        UserInteraction.start(self)
713
714    def setColor(self, color):
715        self.color = color
716
717    def mousePressEvent(self, event):
718        if event.button() == Qt.LeftButton:
719            self.down_pos = event.scenePos()
720            event.accept()
721            return True
722
723    def mouseMoveEvent(self, event):
724        if event.buttons() & Qt.LeftButton:
725            if self.arrow_item is None and \
726                    (self.down_pos - event.scenePos()).manhattanLength() > \
727                    QApplication.instance().startDragDistance():
728
729                annot = scheme.SchemeArrowAnnotation(
730                    point_to_tuple(self.down_pos),
731                    point_to_tuple(event.scenePos())
732                )
733                annot.set_color(self.color)
734                item = self.scene.add_annotation(annot)
735
736                self.arrow_item = item
737                self.annotation = annot
738
739            if self.arrow_item is not None:
740                p1, p2 = map(self.arrow_item.mapFromScene,
741                             (self.down_pos, event.scenePos()))
742                self.arrow_item.setLine(QLineF(p1, p2))
743                self.arrow_item.adjustGeometry()
744
745            event.accept()
746            return True
747
748    def mouseReleaseEvent(self, event):
749        if event.button() == Qt.LeftButton:
750            if self.arrow_item is not None:
751                p1, p2 = self.down_pos, event.scenePos()
752
753                # Commit the annotation to the scheme
754                self.annotation.set_line(point_to_tuple(p1),
755                                         point_to_tuple(p2))
756
757                self.document.addAnnotation(self.annotation)
758
759                p1, p2 = map(self.arrow_item.mapFromScene, (p1, p2))
760                self.arrow_item.setLine(QLineF(p1, p2))
761                self.arrow_item.adjustGeometry()
762
763            self.end()
764            return True
765
766    def end(self):
767        self.down_pos = None
768        self.arrow_item = None
769        self.annotation = None
770        self.document.view().setCursor(Qt.ArrowCursor)
771        UserInteraction.end(self)
772
773
774def rect_to_tuple(rect):
775    return rect.x(), rect.y(), rect.width(), rect.height()
776
777
778class NewTextAnnotation(UserInteraction):
779    def __init__(self, document, *args, **kwargs):
780        UserInteraction.__init__(self, document, *args, **kwargs)
781        self.down_pos = None
782        self.annotation_item = None
783        self.annotation = None
784        self.control = None
785        self.font = document.font()
786
787    def setFont(self, font):
788        self.font = font
789
790    def start(self):
791        self.document.view().setCursor(Qt.CrossCursor)
792        UserInteraction.start(self)
793
794    def createNewAnnotation(self, rect):
795        """Create a new TextAnnotation at with `rect` as the geometry.
796        """
797        annot = scheme.SchemeTextAnnotation(rect_to_tuple(rect))
798        font = {"family": unicode(self.font.family()),
799                "size": self.font.pointSize()}
800        annot.set_font(font)
801
802        item = self.scene.add_annotation(annot)
803        item.setTextInteractionFlags(Qt.TextEditorInteraction)
804        item.setFramePen(QPen(Qt.DashLine))
805
806        self.annotation_item = item
807        self.annotation = annot
808        self.control = controlpoints.ControlPointRect()
809        self.control.rectChanged.connect(
810            self.annotation_item.setGeometry
811        )
812        self.scene.addItem(self.control)
813
814    def mousePressEvent(self, event):
815        if event.button() == Qt.LeftButton:
816            self.down_pos = event.scenePos()
817            return True
818
819    def mouseMoveEvent(self, event):
820        if event.buttons() & Qt.LeftButton:
821            if self.annotation_item is None and \
822                    (self.down_pos - event.scenePos()).manhattanLength() > \
823                    QApplication.instance().startDragDistance():
824                rect = QRectF(self.down_pos, event.scenePos()).normalized()
825                self.createNewAnnotation(rect)
826
827            if self.annotation_item is not None:
828                rect = QRectF(self.down_pos, event.scenePos()).normalized()
829                self.control.setRect(rect)
830
831            return True
832
833    def mouseReleaseEvent(self, event):
834        if event.button() == Qt.LeftButton:
835            if self.annotation_item is None:
836                self.createNewAnnotation(QRectF(event.scenePos(),
837                                                event.scenePos()))
838                rect = self.defaultTextGeometry(event.scenePos())
839
840            else:
841                rect = QRectF(self.down_pos, event.scenePos()).normalized()
842
843            # Commit the annotation to the scheme.
844            self.annotation.rect = rect_to_tuple(rect)
845
846            self.document.addAnnotation(self.annotation)
847
848            self.annotation_item.setGeometry(rect)
849
850            self.control.rectChanged.disconnect(
851                self.annotation_item.setGeometry
852            )
853            self.control.hide()
854
855            # Move the focus to the editor.
856            self.annotation_item.setFramePen(QPen(Qt.NoPen))
857            self.annotation_item.setFocus(Qt.OtherFocusReason)
858            self.annotation_item.startEdit()
859
860            self.end()
861
862    def defaultTextGeometry(self, point):
863        """Return the default text geometry. Used in case the user
864        single clicked in the scene.
865
866        """
867        font = self.annotation_item.font()
868        metrics = QFontMetrics(font)
869        spacing = metrics.lineSpacing()
870        margin = self.annotation_item.document().documentMargin()
871
872        rect = QRectF(QPointF(point.x(), point.y() - spacing - margin),
873                      QSizeF(150, spacing + 2 * margin))
874        return rect
875
876    def end(self):
877        if self.control is not None:
878            self.scene.removeItem(self.control)
879
880        self.control = None
881        self.down_pos = None
882        self.annotation_item = None
883        self.annotation = None
884        self.document.view().setCursor(Qt.ArrowCursor)
885        UserInteraction.end(self)
886
887
888class ResizeTextAnnotation(UserInteraction):
889    def __init__(self, document, *args, **kwargs):
890        UserInteraction.__init__(self, document, *args, **kwargs)
891        self.item = None
892        self.annotation = None
893        self.control = None
894        self.savedFramePen = None
895        self.savedRect = None
896
897    def mousePressEvent(self, event):
898        pos = event.scenePos()
899        if self.item is None:
900            item = self.scene.item_at(pos, items.TextAnnotation)
901            if item is not None and not item.hasFocus():
902                self.editItem(item)
903                return False
904
905        return UserInteraction.mousePressEvent(self, event)
906
907    def editItem(self, item):
908        annotation = self.scene.annotation_for_item(item)
909        rect = item.geometry()  # TODO: map to scene if item has a parent.
910        control = controlpoints.ControlPointRect(rect=rect)
911        self.scene.addItem(control)
912
913        self.savedFramePen = item.framePen()
914        self.savedRect = rect
915
916        control.rectEdited.connect(item.setGeometry)
917        control.setFocusProxy(item)
918
919        item.setFramePen(QPen(Qt.DashDotLine))
920        item.geometryChanged.connect(self.__on_textGeometryChanged)
921
922        self.item = item
923
924        self.annotation = annotation
925        self.control = control
926
927    def commit(self):
928        """Commit the current item geometry state to the document.
929        """
930        rect = self.item.geometry()
931        if self.savedRect != rect:
932            command = commands.SetAttrCommand(
933                self.annotation, "rect",
934                (rect.x(), rect.y(), rect.width(), rect.height()),
935                name="Edit text geometry"
936            )
937            self.document.undoStack().push(command)
938            self.savedRect = rect
939
940    def __on_editingFinished(self):
941        self.commit()
942        self.end()
943
944    def __on_rectEdited(self, rect):
945        self.item.setGeometry(rect)
946
947    def __on_textGeometryChanged(self):
948        if not self.control.isControlActive():
949            rect = self.item.geometry()
950            self.control.setRect(rect)
951
952    def cancel(self, reason=UserInteraction.OtherReason):
953        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
954        if self.item is not None and self.savedRect is not None:
955            self.item.setGeometry(self.savedRect)
956
957        UserInteraction.cancel(self, reason)
958
959    def end(self):
960        if self.control is not None:
961            self.scene.removeItem(self.control)
962
963        if self.item is not None:
964            self.item.setFramePen(self.savedFramePen)
965
966        self.item = None
967        self.annotation = None
968        self.control = None
969
970        UserInteraction.end(self)
971
972
973class ResizeArrowAnnotation(UserInteraction):
974    def __init__(self, document, *args, **kwargs):
975        UserInteraction.__init__(self, document, *args, **kwargs)
976        self.item = None
977        self.annotation = None
978        self.control = None
979        self.savedLine = None
980
981    def mousePressEvent(self, event):
982        pos = event.scenePos()
983        if self.item is None:
984            item = self.scene.item_at(pos, items.ArrowAnnotation)
985            if item is not None and not item.hasFocus():
986                self.editItem(item)
987                return False
988
989        return UserInteraction.mousePressEvent(self, event)
990
991    def editItem(self, item):
992        annotation = self.scene.annotation_for_item(item)
993        control = controlpoints.ControlPointLine()
994        self.scene.addItem(control)
995
996        line = item.line()
997        self.savedLine = line
998
999        p1, p2 = map(item.mapToScene, (line.p1(), line.p2()))
1000
1001        control.setLine(QLineF(p1, p2))
1002        control.setFocusProxy(item)
1003        control.lineEdited.connect(self.__on_lineEdited)
1004
1005        item.geometryChanged.connect(self.__on_lineGeometryChanged)
1006
1007        self.item = item
1008        self.annotation = annotation
1009        self.control = control
1010
1011    def commit(self):
1012        """Commit the current geometry of the item to the document.
1013
1014        .. note:: Does nothing if the actual geometry is not changed.
1015
1016        """
1017        line = self.control.line()
1018        p1, p2 = line.p1(), line.p2()
1019
1020        if self.item.line() != self.savedLine:
1021            command = commands.SetAttrCommand(
1022                self.annotation,
1023                "geometry",
1024                ((p1.x(), p1.y()), (p2.x(), p2.y())),
1025                name="Edit arrow geometry",
1026            )
1027            self.document.undoStack().push(command)
1028            self.savedLine = self.item.line()
1029
1030    def __on_editingFinished(self):
1031        self.commit()
1032        self.end()
1033
1034    def __on_lineEdited(self, line):
1035        p1, p2 = map(self.item.mapFromScene, (line.p1(), line.p2()))
1036        self.item.setLine(QLineF(p1, p2))
1037        self.item.adjustGeometry()
1038
1039    def __on_lineGeometryChanged(self):
1040        # Possible geometry change from out of our control, for instance
1041        # item move as a part of a selection group.
1042        if not self.control.isControlActive():
1043            line = self.item.line()
1044            p1, p2 = map(self.item.mapToScene, (line.p1(), line.p2()))
1045            self.control.setLine(QLineF(p1, p2))
1046
1047    def cancel(self, reason=UserInteraction.OtherReason):
1048        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
1049        if self.item is not None and self.savedLine is not None:
1050            self.item.setLine(self.savedLine)
1051
1052        UserInteraction.cancel(self, reason)
1053
1054    def end(self):
1055        if self.control is not None:
1056            self.scene.removeItem(self.control)
1057
1058        if self.item is not None:
1059            self.item.geometryChanged.disconnect(self.__on_lineGeometryChanged)
1060
1061        self.control = None
1062        self.item = None
1063        self.annotation = None
1064
1065        UserInteraction.end(self)
Note: See TracBrowser for help on using the repository browser.