source: orange/Orange/OrangeCanvas/document/interactions.py @ 11164:58a1f1863e0d

Revision 11164:58a1f1863e0d, 28.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Filter suggested widgets by compatible channel types.

Line 
1"""
2User interaction handlers for CanvasScene.
3
4"""
5import logging
6
7from PyQt4.QtGui import (
8    QApplication, QGraphicsRectItem, QPen, QBrush, QColor
9)
10
11from PyQt4.QtCore import Qt, QSizeF, QRectF, QLineF
12
13from ..registry.qt import QtWidgetRegistry
14from .. import scheme
15from ..canvas import items
16from ..canvas.items import controlpoints
17from . import commands
18
19log = logging.getLogger(__name__)
20
21
22class UserInteraction(object):
23    def __init__(self, document):
24        self.document = document
25        self.scene = document.scene()
26        self.scheme = document.scheme()
27        self.finished = False
28        self.canceled = False
29
30    def start(self):
31        pass
32
33    def end(self):
34        self.finished = True
35        if self.scene.user_interaction_handler is self:
36            self.scene.set_user_interaction_handler(None)
37
38    def cancel(self):
39        self.canceled = True
40        self.end()
41
42    def mousePressEvent(self, event):
43        return False
44
45    def mouseMoveEvent(self, event):
46        return False
47
48    def mouseReleaseEvent(self, event):
49        return False
50
51    def mouseDoubleClickEvent(self, event):
52        return False
53
54    def keyPressEvent(self, event):
55        return False
56
57    def keyReleaseEvent(self, event):
58        return False
59
60
61class NoPossibleLinksError(ValueError):
62    pass
63
64
65def reversed_arguments(func):
66    """Return a function with reversed argument order.
67    """
68    def wrapped(*args):
69        return func(*reversed(args))
70    return wrapped
71
72
73class NewLinkAction(UserInteraction):
74    """User drags a new link from an existing node anchor item to create
75    a connection between two existing nodes or to a new node if the release
76    is over an empty area, in which case a quick menu for new node selection
77    is presented to the user.
78
79    """
80    # direction of the drag
81    FROM_SOURCE = 1
82    FROM_SINK = 2
83
84    def __init__(self, document):
85        UserInteraction.__init__(self, document)
86        self.source_item = None
87        self.sink_item = None
88        self.from_item = None
89        self.direction = None
90
91        self.current_target_item = None
92        self.tmp_link_item = None
93        self.tmp_anchor_point = None
94        self.cursor_anchor_point = None
95
96    def remove_tmp_anchor(self):
97        """Remove a temp anchor point from the current target item.
98        """
99        if self.direction == self.FROM_SOURCE:
100            self.current_target_item.removeInputAnchor(self.tmp_anchor_point)
101        else:
102            self.current_target_item.removeOutputAnchor(self.tmp_anchor_point)
103        self.tmp_anchor_point = None
104
105    def create_tmp_anchor(self, item):
106        """Create a new tmp anchor at the item (`NodeItem`).
107        """
108        assert(self.tmp_anchor_point is None)
109        if self.direction == self.FROM_SOURCE:
110            self.tmp_anchor_point = item.newInputAnchor()
111        else:
112            self.tmp_anchor_point = item.newOutputAnchor()
113
114    def can_connect(self, target_item):
115        """Is the connection between `self.from_item` (item where the drag
116        started) and `target_item`.
117
118        """
119        node1 = self.scene.node_for_item(self.from_item)
120        node2 = self.scene.node_for_item(target_item)
121
122        if self.direction == self.FROM_SOURCE:
123            return bool(self.scheme.propose_links(node1, node2))
124        else:
125            return bool(self.scheme.propose_links(node2, node1))
126
127    def set_link_target_anchor(self, anchor):
128        """Set the temp line target anchor
129        """
130        if self.direction == self.FROM_SOURCE:
131            self.tmp_link_item.setSinkItem(None, anchor)
132        else:
133            self.tmp_link_item.setSourceItem(None, anchor)
134
135    def target_node_item_at(self, pos):
136        """Return a suitable NodeItem on which a link can be dropped.
137        """
138        # Test for a suitable NodeAnchorItem or NodeItem at pos.
139        if self.direction == self.FROM_SOURCE:
140            anchor_type = items.SinkAnchorItem
141        else:
142            anchor_type = items.SourceAnchorItem
143
144        item = self.scene.item_at(pos, (anchor_type, items.NodeItem))
145
146        if isinstance(item, anchor_type):
147            item = item.parentNodeItem()
148
149        return item
150
151    def mousePressEvent(self, event):
152        anchor_item = self.scene.item_at(event.scenePos(),
153                                         items.NodeAnchorItem)
154        if anchor_item and event.button() == Qt.LeftButton:
155            # Start a new link starting at item
156            self.from_item = anchor_item.parentNodeItem()
157            if isinstance(anchor_item, items.SourceAnchorItem):
158                self.direction = NewLinkAction.FROM_SOURCE
159                self.source_item = self.from_item
160            else:
161                self.direction = NewLinkAction.FROM_SINK
162                self.sink_item = self.from_item
163
164            event.accept()
165            return True
166        else:
167            # Whoerver put us in charge did not know what he was doing.
168            self.cancel()
169            return False
170
171    def mouseMoveEvent(self, event):
172        if not self.tmp_link_item:
173            # On first mouse move event create the temp link item and
174            # initialize it to follow the `cursor_anchor_point`.
175            self.tmp_link_item = items.LinkItem()
176            # An anchor under the cursor for the duration of this action.
177            self.cursor_anchor_point = items.AnchorPoint()
178            self.cursor_anchor_point.setPos(event.scenePos())
179
180            # Set the `fixed` end of the temp link (where the drag started).
181            if self.direction == self.FROM_SOURCE:
182                self.tmp_link_item.setSourceItem(self.source_item)
183            else:
184                self.tmp_link_item.setSinkItem(self.sink_item)
185
186            self.set_link_target_anchor(self.cursor_anchor_point)
187            self.scene.addItem(self.tmp_link_item)
188
189        # `NodeItem` at the cursor position
190        item = self.target_node_item_at(event.scenePos())
191
192        if self.current_target_item is not None and \
193                (item is None or item is not self.current_target_item):
194            # `current_target_item` is no longer under the mouse cursor
195            # (was replaced by another item or the the cursor was moved over
196            # an empty scene spot.
197            log.info("%r is no longer the target.", self.current_target_item)
198            self.remove_tmp_anchor()
199            self.current_target_item = None
200
201        if item is not None and item is not self.from_item:
202            # The mouse is over an node item (different from the starting node)
203            if self.current_target_item is item:
204                # Avoid reseting the points
205                pass
206            elif self.can_connect(item):
207                # Grab a new anchor
208                log.info("%r is the new target.", item)
209                self.create_tmp_anchor(item)
210                self.set_link_target_anchor(self.tmp_anchor_point)
211                self.current_target_item = item
212            else:
213                log.info("%r does not have compatible channels", item)
214                self.set_link_target_anchor(self.cursor_anchor_point)
215                # TODO: How to indicate that the connection is not possible?
216                #       The node's anchor could be drawn with a 'disabled'
217                #       palette
218        else:
219            self.set_link_target_anchor(self.cursor_anchor_point)
220
221        self.cursor_anchor_point.setPos(event.scenePos())
222
223        return True
224
225    def mouseReleaseEvent(self, event):
226        if self.tmp_link_item:
227            item = self.target_node_item_at(event.scenePos())
228            node = None
229            stack = self.document.undoStack()
230            stack.beginMacro("Add link")
231
232            if item:
233                # If the release was over a widget item
234                # then connect them
235                node = self.scene.node_for_item(item)
236            else:
237                # Release on an empty canvas part
238                # Show a quick menu popup for a new widget creation.
239                try:
240                    node = self.create_new(event)
241                except Exception:
242                    log.error("Failed to create a new node, ending.",
243                              exc_info=True)
244                    node = None
245
246                if node is not None:
247                    self.document.addNode(node)
248
249            if node is not None:
250                self.connect_existing(node)
251            else:
252                self.end()
253
254            stack.endMacro()
255        else:
256            self.end()
257            return False
258
259    def create_new(self, event):
260        """Create and return a new node with a QuickWidgetMenu.
261        """
262        pos = event.screenPos()
263        menu = self.document.quickMenu()
264        node = self.scene.node_for_item(self.from_item)
265        from_desc = node.description
266
267        def is_compatible(source, sink):
268            return any(scheme.compatible_channels(output, input) \
269                       for output in source.outputs \
270                       for input in sink.inputs)
271
272        if self.direction == self.FROM_SINK:
273            # Reverse the argument order.
274            is_compatible = reversed_arguments(is_compatible)
275
276        def filter(index):
277            desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE)
278            if desc.isValid():
279                desc = desc.toPyObject()
280                return is_compatible(from_desc, desc)
281            else:
282                return False
283
284        menu.setFilterFunc(filter)
285        try:
286            action = menu.exec_(pos)
287        finally:
288            menu.setFilterFunc(None)
289
290        if action:
291            item = action.property("item").toPyObject()
292            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()
293            pos = event.scenePos()
294            node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
295            return node
296
297    def connect_existing(self, node):
298        """Connect anchor_item to `node`.
299        """
300        if self.direction == self.FROM_SOURCE:
301            source_item = self.source_item
302            source_node = self.scene.node_for_item(source_item)
303            sink_node = node
304        else:
305            source_node = node
306            sink_item = self.sink_item
307            sink_node = self.scene.node_for_item(sink_item)
308
309        try:
310            possible = self.scheme.propose_links(source_node, sink_node)
311
312            if not possible:
313                raise NoPossibleLinksError
314
315            links_to_add = []
316            links_to_remove = []
317
318            # Check for possible ties in the proposed link weights
319            if len(possible) >= 2:
320                source, sink, w1 = possible[0]
321                _, _, w2 = possible[1]
322                if w1 == w2:
323                    # If there are ties in the weights a detailed link
324                    # dialog is presented to the user.
325                    links_action = EditNodeLinksAction(
326                                    self.document, source_node, sink_node)
327                    try:
328                        links = links_action.edit_links()
329                    except Exception:
330                        log.error("'EditNodeLinksAction' failed",
331                                  exc_info=True)
332                        raise
333                else:
334                    links_to_add = [(source, sink)]
335            else:
336                source, sink, _ = possible[0]
337                links_to_add = [(source, sink)]
338
339            for source, sink in links_to_remove:
340                existing_link = self.scheme.find_links(
341                                    source_node=source_node,
342                                    source_channel=source,
343                                    sink_node=sink_node,
344                                    sink_channel=sink)
345
346                self.document.removeLink(existing_link)
347
348            for source, sink in links_to_add:
349                if sink.single:
350                    # Remove an existing link to the sink channel if present.
351                    existing_link = self.scheme.find_links(
352                        sink_node=sink_node, sink_channel=sink
353                    )
354
355                    if existing_link:
356                        self.document.removeLink(existing_link[0])
357
358                # Check if the new link is a duplicate of an existing link
359                duplicate = self.scheme.find_links(
360                    source_node, source, sink_node, sink
361                )
362
363                if duplicate:
364                    # Do nothing.
365                    continue
366
367                # Remove temp items before creating a new link
368                self.cleanup()
369
370                link = scheme.SchemeLink(source_node, source, sink_node, sink)
371                self.document.addLink(link)
372
373        except scheme.IncompatibleChannelTypeError:
374            log.info("Cannot connect: invalid channel types.")
375            self.cancel()
376        except scheme.SchemeTopologyError:
377            log.info("Cannot connect: connection creates a cycle.")
378            self.cancel()
379        except NoPossibleLinksError:
380            log.info("Cannot connect: no possible links.")
381            self.cancel()
382        except Exception:
383            log.error("An error occurred during the creation of a new link.",
384                      exc_info=True)
385            self.cancel()
386
387        self.end()
388
389    def end(self):
390        self.cleanup()
391        UserInteraction.end(self)
392
393    def cancel(self):
394        if not self.finished:
395            log.info("Canceling new link action, reverting scene state.")
396            self.cleanup()
397
398    def cleanup(self):
399        """Cleanup all temp items in the scene that are left.
400        """
401        if self.tmp_link_item:
402            self.tmp_link_item.setSinkItem(None)
403            self.tmp_link_item.setSourceItem(None)
404
405            if self.tmp_link_item.scene():
406                self.scene.removeItem(self.tmp_link_item)
407
408            self.tmp_link_item = None
409
410        if self.current_target_item:
411            self.remove_tmp_anchor()
412            self.current_target_item = None
413
414        if self.cursor_anchor_point and self.cursor_anchor_point.scene():
415            self.scene.removeItem(self.cursor_anchor_point)
416            self.cursor_anchor_point = None
417
418
419class NewNodeAction(UserInteraction):
420    """Present the user with a quick menu for node selection and
421    create the selected node.
422
423    """
424
425    def __init__(self, document):
426        UserInteraction.__init__(self, document)
427
428    def mousePressEvent(self, event):
429        if event.button() == Qt.RightButton:
430            self.create_new(event)
431            self.end()
432
433    def create_new(self, event):
434        """Create a new widget with a QuickWidgetMenu
435        """
436        pos = event.screenPos()
437        menu = self.document.quickMenu()
438        menu.setFilterFunc(None)
439
440        action = menu.exec_(pos)
441        if action:
442            item = action.property("item").toPyObject()
443            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()
444            pos = event.scenePos()
445            node = scheme.SchemeNode(desc, position=(pos.x(), pos.y()))
446            self.document.addNode(node)
447            return node
448
449
450class RectangleSelectionAction(UserInteraction):
451    """Select items in the scene using a Rectangle selection
452    """
453    def __init__(self, document):
454        UserInteraction.__init__(self, document)
455        self.initial_selection = None
456
457    def mousePressEvent(self, event):
458        pos = event.scenePos()
459        any_item = self.scene.item_at(pos)
460        if not any_item and event.button() & Qt.LeftButton:
461            self.selection_rect = QRectF(pos, QSizeF(0, 0))
462            self.rect_item = QGraphicsRectItem(
463                self.selection_rect.normalized()
464            )
465
466            self.rect_item.setPen(
467                QPen(QBrush(QColor(51, 153, 255, 192)),
468                     0.4, Qt.SolidLine, Qt.RoundCap)
469            )
470
471            self.rect_item.setBrush(
472                QBrush(QColor(168, 202, 236, 192))
473            )
474
475            self.rect_item.setZValue(-100)
476
477            # Clear the focus if necessary.
478            if not self.scene.stickyFocus():
479                self.scene.clearFocus()
480            event.accept()
481            return True
482        else:
483            self.cancel()
484            return False
485
486    def mouseMoveEvent(self, event):
487        if not self.rect_item.scene():
488            self.scene.addItem(self.rect_item)
489        self.update_selection(event)
490
491    def mouseReleaseEvent(self, event):
492        self.update_selection(event)
493        self.end()
494
495    def update_selection(self, event):
496        if self.initial_selection is None:
497            self.initial_selection = self.scene.selectedItems()
498
499        pos = event.scenePos()
500        self.selection_rect = QRectF(self.selection_rect.topLeft(), pos)
501        self.rect_item.setRect(self.selection_rect.normalized())
502
503        selected = self.scene.items(self.selection_rect.normalized(),
504                                    Qt.IntersectsItemShape,
505                                    Qt.AscendingOrder)
506
507        selected = [item for item in selected if \
508                    item.flags() & Qt.ItemIsSelectable]
509        if event.modifiers() & Qt.ControlModifier:
510            for item in selected:
511                item.setSelected(item not in self.initial_selection)
512        else:
513            for item in self.initial_selection:
514                item.setSelected(False)
515            for item in selected:
516                item.setSelected(True)
517
518    def end(self):
519        self.initial_selection = None
520        self.rect_item.hide()
521        if self.rect_item.scene() is not None:
522            self.scene.removeItem(self.rect_item)
523        UserInteraction.end(self)
524
525
526class EditNodeLinksAction(UserInteraction):
527    def __init__(self, document, source_node, sink_node):
528        UserInteraction.__init__(self, document)
529        self.source_node = source_node
530        self.sink_node = sink_node
531
532    def edit_links(self):
533        from ..canvas.editlinksdialog import EditLinksDialog
534
535        log.info("Constructing a Link Editor dialog.")
536
537        parent = self.scene.views()[0]
538        dlg = EditLinksDialog(parent)
539
540        links = self.scheme.find_links(source_node=self.source_node,
541                                       sink_node=self.sink_node)
542        existing_links = [(link.source_channel, link.sink_channel)
543                          for link in links]
544
545        dlg.setNodes(self.source_node, self.sink_node)
546        dlg.setLinks(existing_links)
547
548        log.info("Executing a Link Editor Dialog.")
549        rval = dlg.exec_()
550
551        if rval == EditLinksDialog.Accepted:
552            links = dlg.links()
553
554            links_to_add = set(links) - set(existing_links)
555            links_to_remove = set(existing_links) - set(links)
556
557            stack = self.document.undoStack()
558            stack.beginMacro("Edit Links")
559
560            for source_channel, sink_channel in links_to_remove:
561                links = self.scheme.find_links(source_node=self.source_node,
562                                               source_channel=source_channel,
563                                               sink_node=self.sink_node,
564                                               sink_channel=sink_channel)
565
566                self.document.removeLink(links[0])
567
568            for source_channel, sink_channel in links_to_add:
569                link = scheme.SchemeLink(self.source_node, source_channel,
570                                         self.sink_node, sink_channel)
571
572                self.document.addLink(link)
573            stack.endMacro()
574
575
576def point_to_tuple(point):
577    return point.x(), point.y()
578
579
580class NewArrowAnnotation(UserInteraction):
581    """Create a new arrow annotation.
582    """
583    def __init__(self, document):
584        UserInteraction.__init__(self, document)
585        self.down_pos = None
586        self.arrow_item = None
587        self.annotation = None
588
589    def start(self):
590        self.document.view().setCursor(Qt.CrossCursor)
591        UserInteraction.start(self)
592
593    def mousePressEvent(self, event):
594        if event.button() == Qt.LeftButton:
595            self.down_pos = event.scenePos()
596            event.accept()
597            return True
598
599    def mouseMoveEvent(self, event):
600        if event.buttons() & Qt.LeftButton:
601            if self.arrow_item is None and \
602                    (self.down_pos - event.scenePos()).manhattanLength() > \
603                    QApplication.instance().startDragDistance():
604
605                annot = scheme.SchemeArrowAnnotation(
606                    point_to_tuple(self.down_pos),
607                    point_to_tuple(event.scenePos())
608                )
609                item = self.scene.add_annotation(annot)
610                self.arrow_item = item
611                self.annotation = annot
612
613            if self.arrow_item is not None:
614                p1, p2 = map(self.arrow_item.mapFromScene,
615                             (self.down_pos, event.scenePos()))
616                self.arrow_item.setLine(QLineF(p1, p2))
617                self.arrow_item.adjustGeometry()
618
619            event.accept()
620            return True
621
622    def mouseReleaseEvent(self, event):
623        if event.button() == Qt.LeftButton:
624            if self.arrow_item is not None:
625                p1, p2 = self.down_pos, event.scenePos()
626
627                # Commit the annotation to the scheme
628                self.annotation.set_line(point_to_tuple(p1),
629                                         point_to_tuple(p2))
630
631                self.document.addAnnotation(self.annotation)
632
633                p1, p2 = map(self.arrow_item.mapFromScene, (p1, p2))
634                self.arrow_item.setLine(QLineF(p1, p2))
635                self.arrow_item.adjustGeometry()
636
637            self.end()
638            return True
639
640    def end(self):
641        self.down_pos = None
642        self.arrow_item = None
643        self.annotation = None
644        self.document.view().setCursor(Qt.ArrowCursor)
645        UserInteraction.end(self)
646
647
648def rect_to_tuple(rect):
649    return rect.x(), rect.y(), rect.width(), rect.height()
650
651
652class NewTextAnnotation(UserInteraction):
653    def __init__(self, document):
654        UserInteraction.__init__(self, document)
655        self.down_pos = None
656        self.annotation_item = None
657        self.annotation = None
658        self.control = None
659
660    def start(self):
661        self.document.view().setCursor(Qt.CrossCursor)
662        UserInteraction.start(self)
663
664    def mousePressEvent(self, event):
665        if event.button() == Qt.LeftButton:
666            self.down_pos = event.scenePos()
667            return True
668
669    def mouseMoveEvent(self, event):
670        if event.buttons() & Qt.LeftButton:
671            if self.annotation_item is None and \
672                    (self.down_pos - event.scenePos()).manhattanLength() > \
673                    QApplication.instance().startDragDistance():
674                rect = QRectF(self.down_pos, event.scenePos()).normalized()
675                annot = scheme.SchemeTextAnnotation(rect_to_tuple(rect))
676
677                item = self.scene.add_annotation(annot)
678                item.setTextInteractionFlags(Qt.TextEditorInteraction)
679                item.setFramePen(QPen(Qt.DashLine))
680
681                self.annotation_item = item
682                self.annotation = annot
683                self.control = controlpoints.ControlPointRect()
684                self.control.rectChanged.connect(
685                    self.annotation_item.setGeometry
686                )
687                self.scene.addItem(self.control)
688
689            if self.annotation_item is not None:
690                rect = QRectF(self.down_pos, event.scenePos()).normalized()
691                self.control.setRect(rect)
692
693            return True
694
695    def mouseReleaseEvent(self, event):
696        if event.button() == Qt.LeftButton:
697            if self.annotation_item is not None:
698                rect = QRectF(self.down_pos, event.scenePos()).normalized()
699
700                # Commit the annotation to the scheme.
701                self.annotation.rect = rect_to_tuple(rect)
702                self.document.addAnnotation(self.annotation)
703
704                self.annotation_item.setGeometry(rect)
705
706                self.control.rectChanged.disconnect(
707                    self.annotation_item.setGeometry
708                )
709                self.control.hide()
710
711                # Move the focus to the editor.
712                self.annotation_item.setFramePen(QPen(Qt.NoPen))
713                self.annotation_item.setFocus(Qt.OtherFocusReason)
714                self.annotation_item.startEdit()
715
716            self.end()
717
718    def end(self):
719        if self.control is not None:
720            self.scene.removeItem(self.control)
721        self.control = None
722        self.down_pos = None
723        self.annotation_item = None
724        self.annotation = None
725        self.document.view().setCursor(Qt.ArrowCursor)
726        UserInteraction.end(self)
727
728
729class ResizeTextAnnotation(UserInteraction):
730    def __init__(self, document, ):
731        UserInteraction.__init__(self, document)
732        self.item = None
733        self.annotation = None
734        self.control = None
735        self.savedFramePen = None
736
737    def mousePressEvent(self, event):
738        pos = event.scenePos()
739        if self.item is None:
740            item = self.scene.item_at(pos, items.TextAnnotation)
741            if item is not None and not item.hasFocus():
742                self.editItem(item)
743                return True
744
745        return UserInteraction.mousePressEvent(self, event)
746
747    def editItem(self, item):
748        annotation = self.scene.annotation_for_item(item)
749        rect = item.geometry()  # TODO: map to scene if item has a parent.
750        control = controlpoints.ControlPointRect(rect=rect)
751        self.scene.addItem(control)
752
753        self.savedFramePen = item.framePen()
754        self.initialRect = rect
755
756        control.setFocus()
757        control.editingFinished.connect(self.__on_editingFinished)
758        control.rectEdited.connect(item.setGeometry)
759
760        item.setFramePen(QPen(Qt.DashLine))
761
762        self.item = item
763
764        self.annotation = annotation
765        self.control = control
766
767    def __on_editingFinished(self):
768        rect = self.item.geometry()
769        command = commands.SetAttrCommand(
770            self.annotation, "rect",
771            (rect.x(), rect.y(), rect.width(), rect.height()),
772            name="Edit text geometry"
773        )
774        self.document.undoStack().push(command)
775        self.end()
776
777    def __on_rectEdited(self, rect):
778        self.item.setGeometry(rect)
779
780    def cancel(self):
781        if self.item is not None:
782            self.item.setGeometry(self.initialRect)
783
784        UserInteraction.cancel(self)
785
786    def end(self):
787        if self.control is not None:
788            self.control.clearFocus()
789            self.scene.removeItem(self.control)
790
791        if self.item is not None:
792            self.item.setFramePen(self.savedFramePen)
793
794        self.item = None
795        self.annotation = None
796        self.control = None
797
798        UserInteraction.end(self)
799
800
801class ResizeArrowAnnotation(UserInteraction):
802    def __init__(self, document):
803        UserInteraction.__init__(self, document)
804        self.item = None
805        self.annotation = None
806        self.control = None
807
808    def mousePressEvent(self, event):
809        pos = event.scenePos()
810        if self.item is None:
811            item = self.scene.item_at(pos, items.ArrowAnnotation)
812            if item is not None:
813                self.editItem(item)
814                return True
815
816        return UserInteraction.mousePressEvent(self, event)
817
818    def editItem(self, item):
819        annotation = self.scene.annotation_for_item(item)
820        control = controlpoints.ControlPointLine()
821        self.scene.addItem(control)
822
823        line = item.line()
824        self.initialLine = line
825
826        p1, p2 = map(item.mapToScene, (line.p1(), line.p2()))
827
828        control.setLine(QLineF(p1, p2))
829        control.setFocus()
830        control.editingFinished.connect(self.__on_editingFinished)
831        control.lineEdited.connect(self.__on_lineEdited)
832
833        self.item = item
834        self.annotation = annotation
835        self.control = control
836
837    def __on_editingFinished(self):
838        line = self.control.line()
839        p1, p2 = line.p1(), line.p2()
840
841        command = commands.SetAttrCommand(
842            self.annotation,
843            "geometry",
844            ((p1.x(), p1.y()), (p2.x(), p2.y())),
845            name="Edit arrow geometry",
846        )
847        self.document.undoStack().push(command)
848        self.control.hide()
849
850        self.end()
851
852    def __on_lineEdited(self, line):
853        p1, p2 = map(self.item.mapFromScene, (line.p1(), line.p2()))
854        self.item.setLine(QLineF(p1, p2))
855        self.item.adjustGeometry()
856
857    def cancel(self):
858        if self.item is not None:
859            self.item.setLine(self.initialLine)
860
861        UserInteraction.cancel(self)
862
863    def end(self):
864        if self.control is not None:
865            self.scene.removeItem(self.control)
866
867        self.control = None
868        self.item = None
869        self.annotation = None
870
871        UserInteraction.end(self)
Note: See TracBrowser for help on using the repository browser.