source: orange/Orange/OrangeCanvas/document/interactions.py @ 11162:cc402e04763c

Revision 11162:cc402e04763c, 27.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added annotation geometry edit interactions in SchemeEditWidget.

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