1"""
2=========================
3User Interaction Handlers
4=========================
5
6User interaction handlers for a :class:`~.SchemeEditWidget`.
7
8User interactions encapsulate the logic of user interactions with the
9scheme document.
10
11All interactions are subclasses of :class:`UserInteraction`.
12
13
14"""
15import typing
16from typing import Optional, Any, Tuple, List, Set, Iterable, Sequence, Dict
17
18import abc
19import logging
20from functools import reduce
21
22from AnyQt.QtWidgets import (
23    QApplication, QGraphicsRectItem, QGraphicsSceneMouseEvent,
24    QGraphicsSceneContextMenuEvent, QWidget, QGraphicsItem,
25    QGraphicsSceneDragDropEvent, QMenu, QAction
26)
27from AnyQt.QtGui import QPen, QBrush, QColor, QFontMetrics, QKeyEvent, QFont
28from AnyQt.QtCore import (
29    Qt, QObject, QCoreApplication, QSizeF, QPointF, QRect, QRectF, QLineF,
30    QPoint, QMimeData,
31)
32from AnyQt.QtCore import pyqtSignal as Signal
33
34from orangecanvas.document.commands import UndoCommand
35from .usagestatistics import UsageStatistics
36from ..registry.description import WidgetDescription, OutputSignal, InputSignal
37from ..registry.qt import QtWidgetRegistry, tooltip_helper, whats_this_helper
38from .. import scheme
39from ..scheme import (
40    SchemeNode as Node, SchemeLink as Link, Scheme, WorkflowEvent,
41    compatible_channels
42)
43from ..canvas import items
44from ..canvas.items import controlpoints
45from ..gui.quickhelp import QuickHelpTipEvent
46from . import commands
47from .editlinksdialog import EditLinksDialog
48
49if typing.TYPE_CHECKING:
50    from .schemeedit import SchemeEditWidget
51
52    A = typing.TypeVar("A")
53    #: Output/Input pair of a link
54    OIPair = Tuple[OutputSignal, InputSignal]
55
56try:
57    from importlib.metadata import EntryPoint, entry_points
58except ImportError:
59    from importlib_metadata import EntryPoint, entry_points
60
61
62log = logging.getLogger(__name__)
63
64
65def assert_not_none(optional):
66    # type: (Optional[A]) -> A
67    assert optional is not None
68    return optional
69
70
71class UserInteraction(QObject):
72    """
73    Base class for user interaction handlers.
74
75    Parameters
76    ----------
77    document : :class:`~.SchemeEditWidget`
78        An scheme editor instance with which the user is interacting.
79    parent : :class:`QObject`, optional
80        A parent QObject
81    deleteOnEnd : bool, optional
82        Should the UserInteraction be deleted when it finishes (``True``
83        by default).
84
85    """
86    # Cancel reason flags
87
88    #: No specified reason
89    NoReason = 0
90    #: User canceled the operation (e.g. pressing ESC)
91    UserCancelReason = 1
92    #: Another interaction was set
93    InteractionOverrideReason = 3
94    #: An internal error occurred
95    ErrorReason = 4
96    #: Other (unspecified) reason
97    OtherReason = 5
98
99    #: Emitted when the interaction is set on the scene.
100    started = Signal()
101
102    #: Emitted when the interaction finishes successfully.
103    finished = Signal()
104
105    #: Emitted when the interaction ends (canceled or finished)
106    ended = Signal()
107
108    #: Emitted when the interaction is canceled.
109    canceled = Signal([], [int])
110
111    def __init__(self, document, parent=None, deleteOnEnd=True):
112        # type: ('SchemeEditWidget', Optional[QObject], bool) -> None
113        super().__init__(parent)
114        self.document = document
115        self.scene = document.scene()
116        scheme_ = document.scheme()
117        assert scheme_ is not None
118        self.scheme = scheme_  # type: scheme.Scheme
119        self.suggestions = document.suggestions()
120        self.deleteOnEnd = deleteOnEnd
121
122        self.cancelOnEsc = False
123
124        self.__finished = False
125        self.__canceled = False
126        self.__cancelReason = self.NoReason
127
128    def start(self):
129        # type: () -> None
130        """
131        Start the interaction. This is called by the :class:`CanvasScene` when
132        the interaction is installed.
133
134        .. note:: Must be called from subclass implementations.
135
136        """
137        self.started.emit()
138
139    def end(self):
140        # type: () -> None
141        """
142        Finish the interaction. Restore any leftover state in this method.
143
144        .. note:: This gets called from the default :func:`cancel`
145                  implementation.
146
147        """
148        self.__finished = True
149
150        if self.scene.user_interaction_handler is self:
151            self.scene.set_user_interaction_handler(None)
152
153        if self.__canceled:
154            self.canceled.emit()
155            self.canceled[int].emit(self.__cancelReason)
156        else:
157            self.finished.emit()
158
159        self.ended.emit()
160
161        if self.deleteOnEnd:
162            self.deleteLater()
163
164    def cancel(self, reason=OtherReason):
165        # type: (int) -> None
166        """
167        Cancel the interaction with `reason`.
168        """
169
170        self.__canceled = True
171        self.__cancelReason = reason
172
173        self.end()
174
175    def isFinished(self):
176        # type: () -> bool
177        """
178        Is the interaction finished.
179        """
180        return self.__finished
181
182    def isCanceled(self):
183        # type: () -> bool
184        """
185        Was the interaction canceled.
186        """
187        return self.__canceled
188
189    def cancelReason(self):
190        # type: () -> int
191        """
192        Return the reason the interaction was canceled.
193        """
194        return self.__cancelReason
195
196    def mousePressEvent(self, event):
197        # type: (QGraphicsSceneMouseEvent) -> bool
198        """
199        Handle a `QGraphicsScene.mousePressEvent`.
200        """
201        return False
202
203    def mouseMoveEvent(self, event):
204        # type: (QGraphicsSceneMouseEvent) -> bool
205        """
206        Handle a `GraphicsScene.mouseMoveEvent`.
207        """
208        return False
209
210    def mouseReleaseEvent(self, event):
211        # type: (QGraphicsSceneMouseEvent) -> bool
212        """
213        Handle a `QGraphicsScene.mouseReleaseEvent`.
214        """
215        return False
216
217    def mouseDoubleClickEvent(self, event):
218        # type: (QGraphicsSceneMouseEvent) -> bool
219        """
220        Handle a `QGraphicsScene.mouseDoubleClickEvent`.
221        """
222        return False
223
224    def keyPressEvent(self, event):
225        # type: (QKeyEvent) -> bool
226        """
227        Handle a `QGraphicsScene.keyPressEvent`
228        """
229        if self.cancelOnEsc and event.key() == Qt.Key_Escape:
230            self.cancel(self.UserCancelReason)
231        return False
232
233    def keyReleaseEvent(self, event):
234        # type: (QKeyEvent) -> bool
235        """
236        Handle a `QGraphicsScene.keyPressEvent`
237        """
238        return False
239
240    def contextMenuEvent(self, event):
241        # type: (QGraphicsSceneContextMenuEvent) -> bool
242        """
243        Handle a `QGraphicsScene.contextMenuEvent`
244        """
245        return False
246
247    def dragEnterEvent(self, event):
248        # type: (QGraphicsSceneDragDropEvent) -> bool
249        """
250        Handle a `QGraphicsScene.dragEnterEvent`
251
252        .. versionadded:: 0.1.20
253        """
254        return False
255
256    def dragMoveEvent(self, event):
257        # type: (QGraphicsSceneDragDropEvent) -> bool
258        """
259        Handle a `QGraphicsScene.dragMoveEvent`
260
261        .. versionadded:: 0.1.20
262        """
263        return False
264
265    def dragLeaveEvent(self, event):
266        # type: (QGraphicsSceneDragDropEvent) -> bool
267        """
268        Handle a `QGraphicsScene.dragLeaveEvent`
269
270        .. versionadded:: 0.1.20
271        """
272        return False
273
274    def dropEvent(self, event):
275        # type: (QGraphicsSceneDragDropEvent) -> bool
276        """
277        Handle a `QGraphicsScene.dropEvent`
278
279        .. versionadded:: 0.1.20
280        """
281        return False
282
283
284class NoPossibleLinksError(ValueError):
285    pass
286
287
288class UserCanceledError(ValueError):
289    pass
290
291
292def reversed_arguments(func):
293    """
294    Return a function with reversed argument order.
295    """
296    def wrapped(*args):
297        return func(*reversed(args))
298    return wrapped
299
300
301class NewLinkAction(UserInteraction):
302    """
303    User drags a new link from an existing `NodeAnchorItem` to create
304    a connection between two existing nodes or to a new node if the release
305    is over an empty area, in which case a quick menu for new node selection
306    is presented to the user.
307
308    """
309    # direction of the drag
310    FROM_SOURCE = 1
311    FROM_SINK = 2
312
313    def __init__(self, document, *args, **kwargs):
314        super().__init__(document, *args, **kwargs)
315        self.from_item = None    # type: Optional[items.NodeItem]
316        self.from_signal = None  # type: Optional[Union[InputSignal, OutputSignal]]
317        self.direction = 0       # type: int
318        self.showing_incompatible_widget = False  # type: bool
319
320        # An `NodeItem` currently under the mouse as a possible
321        # link drop target.
322        self.current_target_item = None  # type: Optional[items.NodeItem]
323        # A temporary `LinkItem` used while dragging.
324        self.tmp_link_item = None        # type: Optional[items.LinkItem]
325        # An temporary `AnchorPoint` inserted into `current_target_item`
326        self.tmp_anchor_point = None     # type: Optional[items.AnchorPoint]
327        # An `AnchorPoint` following the mouse cursor
328        self.cursor_anchor_point = None  # type: Optional[items.AnchorPoint]
329        # An UndoCommand
330        self.macro = None  # type: Optional[UndoCommand]
331
332        # Cache viable signals of currently hovered node
333        self.__target_compatible_signals = None
334
335        self.cancelOnEsc = True
336
337    def remove_tmp_anchor(self):
338        # type: () -> None
339        """
340        Remove a temporary anchor point from the current target item.
341        """
342        assert self.current_target_item is not None
343        assert self.tmp_anchor_point is not None
344        if self.direction == self.FROM_SOURCE:
345            self.current_target_item.removeInputAnchor(self.tmp_anchor_point)
346        else:
347            self.current_target_item.removeOutputAnchor(self.tmp_anchor_point)
348        self.tmp_anchor_point = None
349
350    def update_tmp_anchor(self, item, scenePos):
351        # type: (items.NodeItem, QPointF) -> None
352        """
353        If hovering over a new compatible channel, move it.
354        """
355        assert self.tmp_anchor_point is not None
356        if self.direction == self.FROM_SOURCE:
357            signal = item.inputAnchorItem.signalAtPos(scenePos,
358                                                      self.__target_compatible_signals)
359        else:
360            signal = item.outputAnchorItem.signalAtPos(scenePos,
361                                                       self.__target_compatible_signals)
362        self.tmp_anchor_point.setSignal(signal)
363
364    def create_tmp_anchor(self, item, scenePos, viableLinks=None):
365        # type: (items.NodeItem, QPointF) -> None
366        """
367        Create a new tmp anchor at the `item` (:class:`NodeItem`).
368        """
369        assert self.tmp_anchor_point is None
370        if self.direction == self.FROM_SOURCE:
371            anchor = item.inputAnchorItem
372            signal = anchor.signalAtPos(scenePos,
373                                        self.__target_compatible_signals)
374            self.tmp_anchor_point = item.newInputAnchor(signal)
375        else:
376            anchor = item.outputAnchorItem
377            signal = anchor.signalAtPos(scenePos,
378                                        self.__target_compatible_signals)
379            self.tmp_anchor_point = item.newOutputAnchor(signal)
380
381    def can_connect(self, target_item):
382        # type: (items.NodeItem) -> bool
383        """
384        Is the connection between `self.from_item` (item where the drag
385        started) and `target_item` possible.
386
387        If possible, initialize the variables regarding the node.
388        """
389        if self.from_item is None:
390            return False
391        node1 = self.scene.node_for_item(self.from_item)
392        node2 = self.scene.node_for_item(target_item)
393
394        if self.direction == self.FROM_SOURCE:
395            links = self.scheme.propose_links(node1, node2,
396                                              source_signal=self.from_signal)
397            self.__target_compatible_signals = [l[1] for l in links]
398        else:
399            links = self.scheme.propose_links(node2, node1,
400                                              sink_signal=self.from_signal)
401            self.__target_compatible_signals = [l[0] for l in links]
402
403        return bool(links)
404
405    def set_link_target_anchor(self, anchor):
406        # type: (items.AnchorPoint) -> None
407        """
408        Set the temp line target anchor.
409        """
410        assert self.tmp_link_item is not None
411        if self.direction == self.FROM_SOURCE:
412            self.tmp_link_item.setSinkItem(None, anchor=anchor)
413        else:
414            self.tmp_link_item.setSourceItem(None, anchor=anchor)
415
416    def target_node_item_at(self, pos):
417        # type: (QPointF) -> Optional[items.NodeItem]
418        """
419        Return a suitable :class:`NodeItem` at position `pos` on which
420        a link can be dropped.
421        """
422        # Test for a suitable `NodeAnchorItem` or `NodeItem` at pos.
423        if self.direction == self.FROM_SOURCE:
424            anchor_type = items.SinkAnchorItem
425        else:
426            anchor_type = items.SourceAnchorItem
427
428        item = self.scene.item_at(pos, (anchor_type, items.NodeItem))
429
430        if isinstance(item, anchor_type):
431            return item.parentNodeItem()
432        elif isinstance(item, items.NodeItem):
433            return item
434        else:
435            return None
436
437    def mousePressEvent(self, event):
438        # type: (QGraphicsSceneMouseEvent) -> bool
439        anchor_item = self.scene.item_at(
440            event.scenePos(), items.NodeAnchorItem
441        )
442        if anchor_item is not None and event.button() == Qt.LeftButton:
443            # Start a new link starting at item
444            self.from_item = anchor_item.parentNodeItem()
445            if isinstance(anchor_item, items.SourceAnchorItem):
446                self.direction = NewLinkAction.FROM_SOURCE
447            else:
448                self.direction = NewLinkAction.FROM_SINK
449
450            event.accept()
451
452            helpevent = QuickHelpTipEvent(
453                self.tr("Create a new link"),
454                self.tr('<h3>Create new link</h3>'
455                        '<p>Drag a link to an existing node or release on '
456                        'an empty spot to create a new node.</p>'
457                        '<p>Hold Shift when releasing the mouse button to '
458                        'edit connections.</p>'
459#                        '<a href="help://orange-canvas/create-new-links">'
460#                        'More ...</a>'
461                        )
462            )
463            QCoreApplication.postEvent(self.document, helpevent)
464            return True
465        else:
466            # Whoever put us in charge did not know what he was doing.
467            self.cancel(self.ErrorReason)
468            return False
469
470    def mouseMoveEvent(self, event):
471        # type: (QGraphicsSceneMouseEvent) -> bool
472        if self.tmp_link_item is None:
473            # On first mouse move event create the temp link item and
474            # initialize it to follow the `cursor_anchor_point`.
475            self.tmp_link_item = items.LinkItem()
476            # An anchor under the cursor for the duration of this action.
477            self.cursor_anchor_point = items.AnchorPoint()
478            self.cursor_anchor_point.setPos(event.scenePos())
479
480            # Set the `fixed` end of the temp link (where the drag started).
481            scenePos = event.scenePos()
482
483            if self.direction == self.FROM_SOURCE:
484                anchor = self.from_item.outputAnchorItem
485            else:
486                anchor = self.from_item.inputAnchorItem
487            anchor.setHovered(False)
488            anchor.setCompatibleSignals(None)
489
490            if anchor.anchorOpen():
491                signal = anchor.signalAtPos(scenePos)
492                anchor.setKeepAnchorOpen(signal)
493            else:
494                signal = None
495            self.from_signal = signal
496
497            if self.direction == self.FROM_SOURCE:
498                self.tmp_link_item.setSourceItem(self.from_item, signal)
499            else:
500                self.tmp_link_item.setSinkItem(self.from_item, signal)
501
502            self.set_link_target_anchor(self.cursor_anchor_point)
503            self.scene.addItem(self.tmp_link_item)
504
505        assert self.cursor_anchor_point is not None
506
507        # `NodeItem` at the cursor position
508        item = self.target_node_item_at(event.scenePos())
509
510        if self.current_target_item is not None and \
511                (item is None or item is not self.current_target_item):
512            # `current_target_item` is no longer under the mouse cursor
513            # (was replaced by another item or the the cursor was moved over
514            # an empty scene spot.
515            log.info("%r is no longer the target.", self.current_target_item)
516            if self.direction == self.FROM_SOURCE:
517                anchor = self.current_target_item.inputAnchorItem
518            else:
519                anchor = self.current_target_item.outputAnchorItem
520            if self.showing_incompatible_widget:
521                anchor.setIncompatible(False)
522                self.showing_incompatible_widget = False
523            else:
524                self.remove_tmp_anchor()
525            anchor.setHovered(False)
526            anchor.setCompatibleSignals(None)
527            self.current_target_item = None
528
529        if item is not None and item is not self.from_item:
530            # The mouse is over a node item (different from the starting node)
531            if self.current_target_item is item:
532                # Mouse is over the same item
533                scenePos = event.scenePos()
534                # Move to new potential anchor
535                if not self.showing_incompatible_widget:
536                    self.update_tmp_anchor(item, scenePos)
537                else:
538                    self.set_link_target_anchor(self.cursor_anchor_point)
539            elif self.can_connect(item):
540                # Mouse is over a new item
541                log.info("%r is the new target.", item)
542                if self.direction == self.FROM_SOURCE:
543                    item.inputAnchorItem.setCompatibleSignals(
544                        self.__target_compatible_signals)
545                    item.inputAnchorItem.setHovered(True)
546                else:
547                    item.outputAnchorItem.setCompatibleSignals(
548                        self.__target_compatible_signals)
549                    item.outputAnchorItem.setHovered(True)
550                scenePos = event.scenePos()
551                self.create_tmp_anchor(item, scenePos)
552                self.set_link_target_anchor(
553                    assert_not_none(self.tmp_anchor_point)
554                )
555                self.current_target_item = item
556                self.showing_incompatible_widget = False
557            else:
558                log.info("%r does not have compatible channels", item)
559                self.__target_compatible_signals = []
560                if self.direction == self.FROM_SOURCE:
561                    anchor = item.inputAnchorItem
562                else:
563                    anchor = item.outputAnchorItem
564                anchor.setCompatibleSignals(
565                    self.__target_compatible_signals)
566                anchor.setHovered(True)
567                anchor.setIncompatible(True)
568                self.showing_incompatible_widget = True
569                self.set_link_target_anchor(self.cursor_anchor_point)
570                self.current_target_item = item
571        else:
572            self.set_link_target_anchor(self.cursor_anchor_point)
573
574        self.cursor_anchor_point.setPos(event.scenePos())
575
576        return True
577
578    def mouseReleaseEvent(self, event):
579        # type: (QGraphicsSceneMouseEvent) -> bool
580        if self.tmp_link_item is not None:
581            item = self.target_node_item_at(event.scenePos())
582            node = None  # type: Optional[Node]
583            stack = self.document.undoStack()
584
585            self.macro = UndoCommand(self.tr("Add link"))
586
587            if item:
588                # If the release was over a node item then connect them
589                node = self.scene.node_for_item(item)
590            else:
591                # Release on an empty canvas part
592                # Show a quick menu popup for a new widget creation.
593                try:
594                    node = self.create_new(event)
595                except Exception:
596                    log.error("Failed to create a new node, ending.",
597                              exc_info=True)
598                    node = None
599
600                if node is not None:
601                    commands.AddNodeCommand(self.scheme, node,
602                                            parent=self.macro)
603
604            if node is not None and not self.showing_incompatible_widget:
605                if self.direction == self.FROM_SOURCE:
606                    source_node = self.scene.node_for_item(self.from_item)
607                    source_signal = self.from_signal
608                    sink_node = node
609                    if item is not None and item.inputAnchorItem.anchorOpen():
610                        sink_signal = item.inputAnchorItem.signalAtPos(
611                            event.scenePos(),
612                            self.__target_compatible_signals
613                        )
614                    else:
615                        sink_signal = None
616                else:
617                    source_node = node
618                    if item is not None and item.outputAnchorItem.anchorOpen():
619                        source_signal = item.outputAnchorItem.signalAtPos(
620                            event.scenePos(),
621                            self.__target_compatible_signals
622                        )
623                    else:
624                        source_signal = None
625                    sink_node = self.scene.node_for_item(self.from_item)
626                    sink_signal = self.from_signal
627                self.suggestions.set_direction(self.direction)
628                self.connect_nodes(source_node, sink_node,
629                                   source_signal, sink_signal)
630
631                if not self.isCanceled() or not self.isFinished() and \
632                        self.macro is not None:
633                    # Push (commit) the add link/node action on the stack.
634                    stack.push(self.macro)
635
636            self.end()
637            return True
638        else:
639            self.end()
640            return False
641
642    def create_new(self, event):
643        # type: (QGraphicsSceneMouseEvent) -> Optional[Node]
644        """
645        Create and return a new node with a `QuickMenu`.
646        """
647        pos = event.screenPos()
648        menu = self.document.quickMenu()
649        node = self.scene.node_for_item(self.from_item)
650        from_signal = self.from_signal
651        from_desc = node.description
652
653        def is_compatible(
654                source_signal: OutputSignal,
655                source: WidgetDescription,
656                sink: WidgetDescription,
657                sink_signal: InputSignal
658        ) -> bool:
659            return any(scheme.compatible_channels(output, input)
660                       for output
661                       in ([source_signal] if source_signal else source.outputs)
662                       for input
663                       in ([sink_signal] if sink_signal else sink.inputs))
664
665        from_sink = self.direction == self.FROM_SINK
666        if from_sink:
667            # Reverse the argument order.
668            is_compatible = reversed_arguments(is_compatible)
669            suggestion_sort = self.suggestions.get_source_suggestions(from_desc.name)
670        else:
671            suggestion_sort = self.suggestions.get_sink_suggestions(from_desc.name)
672
673        def sort(left, right):
674            # list stores frequencies, so sign is flipped
675            return suggestion_sort[left] > suggestion_sort[right]
676
677        menu.setSortingFunc(sort)
678
679        def filter(index):
680            desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE)
681            if isinstance(desc, WidgetDescription):
682                return is_compatible(from_signal, from_desc, desc, None)
683            else:
684                return False
685
686        menu.setFilterFunc(filter)
687        menu.triggerSearch()
688        try:
689            action = menu.exec_(pos)
690        finally:
691            menu.setFilterFunc(None)
692
693        if action:
694            item = action.property("item")
695            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE)
696            pos = event.scenePos()
697            # a new widget should be placed so that the connection
698            # stays as it was
699            offset = 31 * (-1 if self.direction == self.FROM_SINK else
700                           1 if self.direction == self.FROM_SOURCE else 0)
701            statistics = self.document.usageStatistics()
702            statistics.begin_extend_action(from_sink, node)
703            node = self.document.newNodeHelper(desc,
704                                               position=(pos.x() + offset,
705                                                         pos.y()))
706            return node
707        else:
708            return None
709
710    def connect_nodes(
711            self, source_node: Node, sink_node: Node,
712            source_signal: Optional[OutputSignal] = None,
713            sink_signal: Optional[InputSignal] = None
714    ) -> None:
715        """
716        Connect `source_node` to `sink_node`. If there are more then one
717        equally weighted and non conflicting links possible present a
718        detailed dialog for link editing.
719
720        """
721        UsageStatistics.set_sink_anchor_open(sink_signal is not None)
722        UsageStatistics.set_source_anchor_open(source_signal is not None)
723        try:
724            possible = self.scheme.propose_links(source_node, sink_node,
725                                                 source_signal, sink_signal)
726
727            log.debug("proposed (weighted) links: %r",
728                      [(s1.name, s2.name, w) for s1, s2, w in possible])
729
730            if not possible:
731                raise NoPossibleLinksError
732
733            source, sink, w = possible[0]
734
735            # just a list of signal tuples for now, will be converted
736            # to SchemeLinks later
737            links_to_add = []     # type: List[Link]
738            links_to_remove = []  # type: List[Link]
739            show_link_dialog = False
740
741            # Ambiguous new link request.
742            if len(possible) >= 2:
743                # Check for possible ties in the proposed link weights
744                _, _, w2 = possible[1]
745                if w == w2:
746                    show_link_dialog = True
747
748                # Check for destructive action (i.e. would the new link
749                # replace a previous link)
750                if sink.single and self.scheme.find_links(sink_node=sink_node,
751                                                          sink_channel=sink):
752                    show_link_dialog = True
753
754            if show_link_dialog:
755                existing = self.scheme.find_links(source_node=source_node,
756                                                  sink_node=sink_node)
757
758                if existing:
759                    # edit_links will populate the view with existing links
760                    initial_links = None
761                else:
762                    initial_links = [(source, sink)]
763
764                try:
765                    rstatus, links_to_add, links_to_remove = self.edit_links(
766                        source_node, sink_node, initial_links
767                    )
768                except Exception:
769                    log.error("Failed to edit the links",
770                              exc_info=True)
771                    raise
772                if rstatus == EditLinksDialog.Rejected:
773                    raise UserCanceledError
774            else:
775                # links_to_add now needs to be a list of actual SchemeLinks
776                links_to_add = [
777                    scheme.SchemeLink(source_node, source, sink_node, sink)
778                ]
779                links_to_add, links_to_remove = \
780                    add_links_plan(self.scheme, links_to_add)
781
782            # Remove temp items before creating any new links
783            self.cleanup()
784
785            for link in links_to_remove:
786                commands.RemoveLinkCommand(self.scheme, link,
787                                           parent=self.macro)
788
789            for link in links_to_add:
790                # Check if the new requested link is a duplicate of an
791                # existing link
792                duplicate = self.scheme.find_links(
793                    link.source_node, link.source_channel,
794                    link.sink_node, link.sink_channel
795                )
796
797                if not duplicate:
798                    commands.AddLinkCommand(self.scheme, link,
799                                            parent=self.macro)
800
801        except scheme.IncompatibleChannelTypeError:
802            log.info("Cannot connect: invalid channel types.")
803            self.cancel()
804        except scheme.SchemeTopologyError:
805            log.info("Cannot connect: connection creates a cycle.")
806            self.cancel()
807        except NoPossibleLinksError:
808            log.info("Cannot connect: no possible links.")
809            self.cancel()
810        except UserCanceledError:
811            log.info("User canceled a new link action.")
812            self.cancel(UserInteraction.UserCancelReason)
813        except Exception:
814            log.error("An error occurred during the creation of a new link.",
815                      exc_info=True)
816            self.cancel()
817
818    def edit_links(
819            self,
820            source_node: Node,
821            sink_node: Node,
822            initial_links: 'Optional[List[OIPair]]' = None
823    ) -> 'Tuple[int, List[Link], List[Link]]':
824        """
825        Show and execute the `EditLinksDialog`.
826        Optional `initial_links` list can provide a list of initial
827        `(source, sink)` channel tuples to show in the view, otherwise
828        the dialog is populated with existing links in the scheme (passing
829        an empty list will disable all initial links).
830
831        """
832        status, links_to_add_spec, links_to_remove_spec = \
833            edit_links(
834                self.scheme, source_node, sink_node, initial_links,
835                parent=self.document
836            )
837
838        if status == EditLinksDialog.Accepted:
839            links_to_add = [
840                scheme.SchemeLink(
841                    source_node, source_channel,
842                    sink_node, sink_channel
843                ) for source_channel, sink_channel in links_to_add_spec
844            ]
845            links_to_remove = list(reduce(
846                list.__iadd__, (
847                    self.scheme.find_links(
848                        source_node, source_channel,
849                        sink_node, sink_channel
850                    ) for source_channel, sink_channel in links_to_remove_spec
851                ),
852                []
853            ))  # type: List[Link]
854            conflicting = [conflicting_single_link(self.scheme, link)
855                           for link in links_to_add]
856            conflicting = [link for link in conflicting if link is not None]
857            for link in conflicting:
858                if link not in links_to_remove:
859                    links_to_remove.append(link)
860
861            return status, links_to_add, links_to_remove
862        else:
863            return status, [], []
864
865    def end(self):
866        # type: () -> None
867        self.cleanup()
868        self.reset_open_anchor()
869        # Remove the help tip set in mousePressEvent
870        self.macro = None
871        helpevent = QuickHelpTipEvent("", "")
872        QCoreApplication.postEvent(self.document, helpevent)
873        super().end()
874
875    def cancel(self, reason=UserInteraction.OtherReason):
876        # type: (int) -> None
877        self.cleanup()
878        self.reset_open_anchor()
879        super().cancel(reason)
880
881    def cleanup(self):
882        # type: () -> None
883        """
884        Cleanup all temporary items in the scene that are left.
885        """
886        if self.tmp_link_item:
887            self.tmp_link_item.setSinkItem(None)
888            self.tmp_link_item.setSourceItem(None)
889
890            if self.tmp_link_item.scene():
891                self.scene.removeItem(self.tmp_link_item)
892
893            self.tmp_link_item = None
894
895        if self.current_target_item:
896            if not self.showing_incompatible_widget:
897                self.remove_tmp_anchor()
898            else:
899                if self.direction == self.FROM_SOURCE:
900                    anchor = self.current_target_item.inputAnchorItem
901                else:
902                    anchor = self.current_target_item.outputAnchorItem
903                anchor.setIncompatible(False)
904
905            self.current_target_item = None
906
907        if self.cursor_anchor_point and self.cursor_anchor_point.scene():
908            self.scene.removeItem(self.cursor_anchor_point)
909            self.cursor_anchor_point = None
910
911    def reset_open_anchor(self):
912        """
913        This isn't part of cleanup, because it should retain its value
914        until the link is created.
915        """
916        if self.direction == self.FROM_SOURCE:
917            anchor = self.from_item.outputAnchorItem
918        else:
919            anchor = self.from_item.inputAnchorItem
920        anchor.setKeepAnchorOpen(None)
921
922
923def edit_links(
924        scheme: Scheme,
925        source_node: Node,
926        sink_node: Node,
927        initial_links: 'Optional[List[OIPair]]' = None,
928        parent: 'Optional[QWidget]' = None
929) -> 'Tuple[int, List[OIPair], List[OIPair]]':
930    """
931    Show and execute the `EditLinksDialog`.
932    Optional `initial_links` list can provide a list of initial
933    `(source, sink)` channel tuples to show in the view, otherwise
934    the dialog is populated with existing links in the scheme (passing
935    an empty list will disable all initial links).
936
937    """
938    log.info("Constructing a Link Editor dialog.")
939
940    dlg = EditLinksDialog(parent, windowTitle="Edit Links")
941
942    # all SchemeLinks between the two nodes.
943    links = scheme.find_links(source_node=source_node, sink_node=sink_node)
944    existing_links = [(link.source_channel, link.sink_channel)
945                      for link in links]
946
947    if initial_links is None:
948        initial_links = list(existing_links)
949
950    dlg.setNodes(source_node, sink_node)
951    dlg.setLinks(initial_links)
952
953    log.info("Executing a Link Editor Dialog.")
954    rval = dlg.exec_()
955
956    if rval == EditLinksDialog.Accepted:
957        edited_links = dlg.links()
958
959        # Differences
960        links_to_add = set(edited_links) - set(existing_links)
961        links_to_remove = set(existing_links) - set(edited_links)
962        return rval, list(links_to_add), list(links_to_remove)
963    else:
964        return rval, [], []
965
966
967def add_links_plan(scheme, links, force_replace=False):
968    # type: (Scheme, Iterable[Link], bool) -> Tuple[List[Link], List[Link]]
969    """
970    Return a plan for adding a list of links to the scheme.
971    """
972    links_to_add = list(links)
973    links_to_remove = [conflicting_single_link(scheme, link)
974                       for link in links]
975    links_to_remove = [link for link in links_to_remove if link is not None]
976
977    if not force_replace:
978        links_to_add, links_to_remove = remove_duplicates(links_to_add,
979                                                          links_to_remove)
980    return links_to_add, links_to_remove
981
982
983def conflicting_single_link(scheme, link):
984    # type: (Scheme, Link) -> Optional[Link]
985    """
986    Find and return an existing link in `scheme` connected to the same
987    input channel as `link` if the channel has the 'single' flag.
988    If no such channel exists (or sink channel is not 'single')
989    return `None`.
990    """
991    if link.sink_channel.single:
992        existing = scheme.find_links(
993            sink_node=link.sink_node,
994            sink_channel=link.sink_channel
995        )
996
997        if existing:
998            assert len(existing) == 1
999            return existing[0]
1000    return None
1001
1002
1003def remove_duplicates(links_to_add, links_to_remove):
1004    # type: (List[Link], List[Link]) -> Tuple[List[Link], List[Link]]
1005    def link_key(link):
1006        # type: (Link) -> Tuple[Node, OutputSignal, Node, InputSignal]
1007        return (link.source_node, link.source_channel,
1008                link.sink_node, link.sink_channel)
1009
1010    add_keys = list(map(link_key, links_to_add))
1011    remove_keys = list(map(link_key, links_to_remove))
1012    duplicate_keys = set(add_keys).intersection(remove_keys)
1013
1014    def not_duplicate(link):
1015        # type: (Link) -> bool
1016        return link_key(link) not in duplicate_keys
1017
1018    links_to_add = list(filter(not_duplicate, links_to_add))
1019    links_to_remove = list(filter(not_duplicate, links_to_remove))
1020    return links_to_add, links_to_remove
1021
1022
1023class NewNodeAction(UserInteraction):
1024    """
1025    Present the user with a quick menu for node selection and
1026    create the selected node.
1027    """
1028    def mousePressEvent(self, event):
1029        # type: (QGraphicsSceneMouseEvent) -> bool
1030        if event.button() == Qt.RightButton:
1031            self.create_new(event.screenPos())
1032            self.end()
1033        return True
1034
1035    def create_new(self, pos, search_text=""):
1036        # type: (QPoint, str) -> Optional[Node]
1037        """
1038        Create and add new node to the workflow using `QuickMenu` popup at
1039        `pos` (in screen coordinates).
1040        """
1041        menu = self.document.quickMenu()
1042        menu.setFilterFunc(None)
1043
1044        # compares probability of the user needing the widget as a source
1045        def defaultSort(left, right):
1046            default_suggestions = self.suggestions.get_default_suggestions()
1047            left_frequency = sum(default_suggestions[left].values())
1048            right_frequency = sum(default_suggestions[right].values())
1049            return left_frequency > right_frequency
1050
1051        menu.setSortingFunc(defaultSort)
1052
1053        action = menu.exec_(pos, search_text)
1054        if action:
1055            item = action.property("item")
1056            desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE)
1057            # Get the scene position
1058            view = self.document.view()
1059            pos = view.mapToScene(view.mapFromGlobal(pos))
1060
1061            statistics = self.document.usageStatistics()
1062            statistics.begin_action(UsageStatistics.QuickMenu)
1063            node = self.document.newNodeHelper(desc,
1064                                               position=(pos.x(), pos.y()))
1065            self.document.addNode(node)
1066            return node
1067        else:
1068            return None
1069
1070
1071class RectangleSelectionAction(UserInteraction):
1072    """
1073    Select items in the scene using a Rectangle selection
1074    """
1075    def __init__(self, document, *args, **kwargs):
1076        # type: (SchemeEditWidget, Any, Any) -> None
1077        super().__init__(document, *args, **kwargs)
1078        # The initial selection at drag start
1079        self.initial_selection = None  # type: Optional[Set[QGraphicsItem]]
1080        # Selection when last updated in a mouseMoveEvent
1081        self.last_selection = None     # type: Optional[Set[QGraphicsItem]]
1082        # A selection rect (`QRectF`)
1083        self.selection_rect = None     # type: Optional[QRectF]
1084        # Keyboard modifiers
1085        self.modifiers = Qt.NoModifier
1086        self.rect_item = None          # type: Optional[QGraphicsRectItem]
1087
1088    def mousePressEvent(self, event):
1089        # type: (QGraphicsSceneMouseEvent) -> bool
1090        pos = event.scenePos()
1091        any_item = self.scene.item_at(pos)
1092        if not any_item and event.button() & Qt.LeftButton:
1093            self.modifiers = event.modifiers()
1094            self.selection_rect = QRectF(pos, QSizeF(0, 0))
1095            self.rect_item = QGraphicsRectItem(
1096                self.selection_rect.normalized()
1097            )
1098
1099            self.rect_item.setPen(
1100                QPen(QBrush(QColor(51, 153, 255, 192)),
1101                     0.4, Qt.SolidLine, Qt.RoundCap)
1102            )
1103
1104            self.rect_item.setBrush(
1105                QBrush(QColor(168, 202, 236, 192))
1106            )
1107
1108            self.rect_item.setZValue(-100)
1109
1110            # Clear the focus if necessary.
1111            if not self.scene.stickyFocus():
1112                self.scene.clearFocus()
1113
1114            if not self.modifiers & Qt.ControlModifier:
1115                self.scene.clearSelection()
1116
1117            event.accept()
1118            return True
1119        else:
1120            self.cancel(self.ErrorReason)
1121            return False
1122
1123    def mouseMoveEvent(self, event):
1124        # type: (QGraphicsSceneMouseEvent) -> bool
1125        if self.rect_item is not None and not self.rect_item.scene():
1126            # Add the rect item to the scene when the mouse moves.
1127            self.scene.addItem(self.rect_item)
1128        self.update_selection(event)
1129        return True
1130
1131    def mouseReleaseEvent(self, event):
1132        # type: (QGraphicsSceneMouseEvent) -> bool
1133        if event.button() == Qt.LeftButton:
1134            if self.initial_selection is None:
1135                # A single click.
1136                self.scene.clearSelection()
1137            else:
1138                self.update_selection(event)
1139        self.end()
1140        return True
1141
1142    def update_selection(self, event):
1143        # type: (QGraphicsSceneMouseEvent) -> None
1144        """
1145        Update the selection rectangle from a QGraphicsSceneMouseEvent
1146        `event` instance.
1147        """
1148        if self.initial_selection is None:
1149            self.initial_selection = set(self.scene.selectedItems())
1150            self.last_selection = self.initial_selection
1151
1152        assert self.selection_rect is not None
1153        assert self.rect_item is not None
1154        assert self.initial_selection is not None
1155        assert self.last_selection is not None
1156
1157        pos = event.scenePos()
1158        self.selection_rect = QRectF(self.selection_rect.topLeft(), pos)
1159
1160        # Make sure the rect_item does not cause the scene rect to grow.
1161        rect = self._bound_selection_rect(self.selection_rect.normalized())
1162
1163        # Need that 0.5 constant otherwise the sceneRect will still
1164        # grow (anti-aliasing correction by QGraphicsScene?)
1165        pw = self.rect_item.pen().width() + 0.5
1166
1167        self.rect_item.setRect(rect.adjusted(pw, pw, -pw, -pw))
1168
1169        selected = self.scene.items(self.selection_rect.normalized(),
1170                                    Qt.IntersectsItemShape,
1171                                    Qt.AscendingOrder)
1172
1173        selected = set([item for item in selected if \
1174                        item.flags() & Qt.ItemIsSelectable])
1175
1176        if self.modifiers & Qt.ControlModifier:
1177            for item in selected | self.last_selection | \
1178                    self.initial_selection:
1179                item.setSelected(
1180                    (item in selected) ^ (item in self.initial_selection)
1181                )
1182        else:
1183            for item in selected.union(self.last_selection):
1184                item.setSelected(item in selected)
1185
1186        self.last_selection = set(self.scene.selectedItems())
1187
1188    def end(self):
1189        # type: () -> None
1190        self.initial_selection = None
1191        self.last_selection = None
1192        self.modifiers = Qt.NoModifier
1193        if self.rect_item is not None:
1194            self.rect_item.hide()
1195            if self.rect_item.scene() is not None:
1196                self.scene.removeItem(self.rect_item)
1197        super().end()
1198
1199    def viewport_rect(self):
1200        # type: () -> QRectF
1201        """
1202        Return the bounding rect of the document's viewport on the scene.
1203        """
1204        view = self.document.view()
1205        vsize = view.viewport().size()
1206        viewportrect = QRect(0, 0, vsize.width(), vsize.height())
1207        return view.mapToScene(viewportrect).boundingRect()
1208
1209    def _bound_selection_rect(self, rect):
1210        # type: (QRectF) -> QRectF
1211        """
1212        Bound the selection `rect` to a sensible size.
1213        """
1214        srect = self.scene.sceneRect()
1215        vrect = self.viewport_rect()
1216        maxrect = srect.united(vrect)
1217        return rect.intersected(maxrect)
1218
1219
1220class EditNodeLinksAction(UserInteraction):
1221    """
1222    Edit multiple links between two :class:`SchemeNode` instances using
1223    a :class:`EditLinksDialog`
1224
1225    Parameters
1226    ----------
1227    document : :class:`SchemeEditWidget`
1228        The editor widget.
1229    source_node : :class:`SchemeNode`
1230        The source (link start) node for the link editor.
1231    sink_node : :class:`SchemeNode`
1232        The sink (link end) node for the link editor.
1233
1234    """
1235    def __init__(self, document, source_node, sink_node, *args, **kwargs):
1236        # type: (SchemeEditWidget, Node, Node, Any, Any) -> None
1237        super().__init__(document, *args, **kwargs)
1238        self.source_node = source_node
1239        self.sink_node = sink_node
1240
1241    def edit_links(self, initial_links=None):
1242        # type: (Optional[List[OIPair]]) -> None
1243        """
1244        Show and execute the `EditLinksDialog`.
1245        Optional `initial_links` list can provide a list of initial
1246        `(source, sink)` channel tuples to show in the view, otherwise
1247        the dialog is populated with existing links in the scheme (passing
1248        an empty list will disable all initial links).
1249
1250        """
1251        log.info("Constructing a Link Editor dialog.")
1252
1253        dlg = EditLinksDialog(self.document, windowTitle="Edit Links")
1254
1255        links = self.scheme.find_links(source_node=self.source_node,
1256                                       sink_node=self.sink_node)
1257        existing_links = [(link.source_channel, link.sink_channel)
1258                          for link in links]
1259
1260        if initial_links is None:
1261            initial_links = list(existing_links)
1262
1263        dlg.setNodes(self.source_node, self.sink_node)
1264        dlg.setLinks(initial_links)
1265
1266        log.info("Executing a Link Editor Dialog.")
1267        rval = dlg.exec_()
1268
1269        if rval == EditLinksDialog.Accepted:
1270            links_spec = dlg.links()
1271
1272            links_to_add = set(links_spec) - set(existing_links)
1273            links_to_remove = set(existing_links) - set(links_spec)
1274
1275            stack = self.document.undoStack()
1276            stack.beginMacro("Edit Links")
1277
1278            # First remove links into a 'Single' sink channel,
1279            # but only the ones that do not have self.source_node as
1280            # a source (they will be removed later from links_to_remove)
1281            for _, sink_channel in links_to_add:
1282                if sink_channel.single:
1283                    existing = self.scheme.find_links(
1284                        sink_node=self.sink_node,
1285                        sink_channel=sink_channel
1286                    )
1287
1288                    existing = [link for link in existing
1289                                if link.source_node is not self.source_node]
1290
1291                    if existing:
1292                        assert len(existing) == 1
1293                        self.document.removeLink(existing[0])
1294
1295            for source_channel, sink_channel in links_to_remove:
1296                links = self.scheme.find_links(source_node=self.source_node,
1297                                               source_channel=source_channel,
1298                                               sink_node=self.sink_node,
1299                                               sink_channel=sink_channel)
1300                assert len(links) == 1
1301                self.document.removeLink(links[0])
1302
1303            for source_channel, sink_channel in links_to_add:
1304                link = scheme.SchemeLink(self.source_node, source_channel,
1305                                         self.sink_node, sink_channel)
1306
1307                self.document.addLink(link)
1308
1309            stack.endMacro()
1310
1311
1312def point_to_tuple(point):
1313    # type: (QPointF) -> Tuple[float, float]
1314    """
1315    Convert a QPointF into a (x, y) tuple.
1316    """
1317    return (point.x(), point.y())
1318
1319
1320class NewArrowAnnotation(UserInteraction):
1321    """
1322    Create a new arrow annotation handler.
1323    """
1324    def __init__(self, document, *args, **kwargs):
1325        # type: (SchemeEditWidget, Any, Any) -> None
1326        super().__init__(document, *args, **kwargs)
1327        self.down_pos = None  # type: Optional[QPointF]
1328        self.arrow_item = None  # type: Optional[items.ArrowAnnotation]
1329        self.annotation = None  # type: Optional[scheme.SchemeArrowAnnotation]
1330        self.color = "red"
1331        self.cancelOnEsc = True
1332
1333    def start(self):
1334        # type: () -> None
1335        self.document.view().setCursor(Qt.CrossCursor)
1336
1337        helpevent = QuickHelpTipEvent(
1338            self.tr("Click and drag to create a new arrow"),
1339            self.tr('<h3>New arrow annotation</h3>'
1340                    '<p>Click and drag to create a new arrow annotation</p>'
1341#                    '<a href="help://orange-canvas/arrow-annotations>'
1342#                    'More ...</a>'
1343                    )
1344        )
1345        QCoreApplication.postEvent(self.document, helpevent)
1346
1347        super().start()
1348
1349    def setColor(self, color):
1350        """
1351        Set the color for the new arrow.
1352        """
1353        self.color = color
1354
1355    def mousePressEvent(self, event):
1356        # type: (QGraphicsSceneMouseEvent) -> bool
1357        if event.button() == Qt.LeftButton:
1358            self.down_pos = event.scenePos()
1359            event.accept()
1360            return True
1361        else:
1362            return super().mousePressEvent(event)
1363
1364    def mouseMoveEvent(self, event):
1365        # type: (QGraphicsSceneMouseEvent) -> bool
1366        if event.buttons() & Qt.LeftButton:
1367            assert self.down_pos is not None
1368            if self.arrow_item is None and \
1369                    (self.down_pos - event.scenePos()).manhattanLength() > \
1370                    QApplication.instance().startDragDistance():
1371
1372                annot = scheme.SchemeArrowAnnotation(
1373                    point_to_tuple(self.down_pos),
1374                    point_to_tuple(event.scenePos())
1375                )
1376                annot.set_color(self.color)
1377                item = self.scene.add_annotation(annot)
1378
1379                self.arrow_item = item
1380                self.annotation = annot
1381
1382            if self.arrow_item is not None:
1383                p1, p2 = map(self.arrow_item.mapFromScene,
1384                             (self.down_pos, event.scenePos()))
1385                self.arrow_item.setLine(QLineF(p1, p2))
1386
1387            event.accept()
1388            return True
1389        else:
1390            return super().mouseMoveEvent(event)
1391
1392    def mouseReleaseEvent(self, event):
1393        # type: (QGraphicsSceneMouseEvent) -> bool
1394        if event.button() == Qt.LeftButton:
1395            if self.arrow_item is not None:
1396                assert self.down_pos is not None and self.annotation is not None
1397                p1, p2 = self.down_pos, event.scenePos()
1398                # Commit the annotation to the scheme
1399                self.annotation.set_line(point_to_tuple(p1),
1400                                         point_to_tuple(p2))
1401
1402                self.document.addAnnotation(self.annotation)
1403
1404                p1, p2 = map(self.arrow_item.mapFromScene, (p1, p2))
1405                self.arrow_item.setLine(QLineF(p1, p2))
1406
1407            self.end()
1408            return True
1409        else:
1410            return super().mouseReleaseEvent(event)
1411
1412    def cancel(self, reason=UserInteraction.OtherReason):  # type: (int) -> None
1413        if self.arrow_item is not None:
1414            self.scene.removeItem(self.arrow_item)
1415            self.arrow_item = None
1416        super().cancel(reason)
1417
1418    def end(self):
1419        # type: () -> None
1420        self.down_pos = None
1421        self.arrow_item = None
1422        self.annotation = None
1423        self.document.view().setCursor(Qt.ArrowCursor)
1424
1425        # Clear the help tip
1426        helpevent = QuickHelpTipEvent("", "")
1427        QCoreApplication.postEvent(self.document, helpevent)
1428
1429        super().end()
1430
1431
1432def rect_to_tuple(rect):
1433    # type: (QRectF) -> Tuple[float, float, float, float]
1434    """
1435    Convert a QRectF into a (x, y, width, height) tuple.
1436    """
1437    return rect.x(), rect.y(), rect.width(), rect.height()
1438
1439
1440class NewTextAnnotation(UserInteraction):
1441    """
1442    A New Text Annotation interaction handler
1443    """
1444    def __init__(self, document, *args, **kwargs):
1445        # type: (SchemeEditWidget, Any, Any) -> None
1446        super().__init__(document, *args, **kwargs)
1447        self.down_pos = None  # type: Optional[QPointF]
1448        self.annotation_item = None  # type: Optional[items.TextAnnotation]
1449        self.annotation = None  # type: Optional[scheme.SchemeTextAnnotation]
1450        self.control = None  # type: Optional[controlpoints.ControlPointRect]
1451        self.font = document.font()  # type: QFont
1452        self.cancelOnEsc = True
1453
1454    def setFont(self, font):
1455        # type: (QFont) -> None
1456        self.font = QFont(font)
1457
1458    def start(self):
1459        # type: () -> None
1460        self.document.view().setCursor(Qt.CrossCursor)
1461
1462        helpevent = QuickHelpTipEvent(
1463            self.tr("Click to create a new text annotation"),
1464            self.tr('<h3>New text annotation</h3>'
1465                    '<p>Click (and drag to resize) on the canvas to create '
1466                    'a new text annotation item.</p>'
1467#                    '<a href="help://orange-canvas/text-annotations">'
1468#                    'More ...</a>'
1469                    )
1470        )
1471        QCoreApplication.postEvent(self.document, helpevent)
1472
1473        super().start()
1474
1475    def createNewAnnotation(self, rect):
1476        # type: (QRectF) -> None
1477        """
1478        Create a new TextAnnotation at with `rect` as the geometry.
1479        """
1480        annot = scheme.SchemeTextAnnotation(rect_to_tuple(rect))
1481        font = {"family": self.font.family(),
1482                "size": self.font.pixelSize()}
1483        annot.set_font(font)
1484
1485        item = self.scene.add_annotation(annot)
1486        item.setTextInteractionFlags(Qt.TextEditorInteraction)
1487        item.setFramePen(QPen(Qt.DashLine))
1488
1489        self.annotation_item = item
1490        self.annotation = annot
1491        self.control = controlpoints.ControlPointRect()
1492        self.control.rectChanged.connect(item.setGeometry)
1493        self.scene.addItem(self.control)
1494
1495    def mousePressEvent(self, event):
1496        # type: (QGraphicsSceneMouseEvent) -> bool
1497        if event.button() == Qt.LeftButton:
1498            self.down_pos = event.scenePos()
1499            return True
1500        return super().mousePressEvent(event)
1501
1502    def mouseMoveEvent(self, event):
1503        # type: (QGraphicsSceneMouseEvent) -> bool
1504        if event.buttons() & Qt.LeftButton:
1505            assert self.down_pos is not None
1506            if self.annotation_item is None and \
1507                    (self.down_pos - event.scenePos()).manhattanLength() > \
1508                    QApplication.instance().startDragDistance():
1509                rect = QRectF(self.down_pos, event.scenePos()).normalized()
1510                self.createNewAnnotation(rect)
1511
1512            if self.annotation_item is not None:
1513                assert self.control is not None
1514                rect = QRectF(self.down_pos, event.scenePos()).normalized()
1515                self.control.setRect(rect)
1516            return True
1517        return super().mouseMoveEvent(event)
1518
1519    def mouseReleaseEvent(self, event):
1520        # type: (QGraphicsSceneMouseEvent) -> bool
1521        if event.button() == Qt.LeftButton:
1522            if self.annotation_item is None:
1523                self.createNewAnnotation(QRectF(event.scenePos(),
1524                                                event.scenePos()))
1525                rect = self.defaultTextGeometry(event.scenePos())
1526            else:
1527                assert self.down_pos is not None
1528                rect = QRectF(self.down_pos, event.scenePos()).normalized()
1529            assert self.annotation_item is not None
1530            assert self.control is not None
1531            assert self.annotation is not None
1532            # Commit the annotation to the scheme.
1533            self.annotation.rect = rect_to_tuple(rect)
1534
1535            self.document.addAnnotation(self.annotation)
1536
1537            self.annotation_item.setGeometry(rect)
1538
1539            self.control.rectChanged.disconnect(
1540                self.annotation_item.setGeometry
1541            )
1542            self.control.hide()
1543
1544            # Move the focus to the editor.
1545            self.annotation_item.setFramePen(QPen(Qt.NoPen))
1546            self.annotation_item.setFocus(Qt.OtherFocusReason)
1547            self.annotation_item.startEdit()
1548
1549            self.end()
1550            return True
1551        return super().mouseMoveEvent(event)
1552
1553    def defaultTextGeometry(self, point):
1554        # type: (QPointF) -> QRectF
1555        """
1556        Return the default text geometry. Used in case the user single
1557        clicked in the scene.
1558        """
1559        assert self.annotation_item is not None
1560        font = self.annotation_item.font()
1561        metrics = QFontMetrics(font)
1562        spacing = metrics.lineSpacing()
1563        margin = self.annotation_item.document().documentMargin()
1564
1565        rect = QRectF(QPointF(point.x(), point.y() - spacing - margin),
1566                      QSizeF(150, spacing + 2 * margin))
1567        return rect
1568
1569    def cancel(self, reason=UserInteraction.OtherReason):  # type: (int) -> None
1570        if self.annotation_item is not None:
1571            self.annotation_item.clearFocus()
1572            self.scene.removeItem(self.annotation_item)
1573            self.annotation_item = None
1574        super().cancel(reason)
1575
1576    def end(self):
1577        # type: () -> None
1578        if self.control is not None:
1579            self.scene.removeItem(self.control)
1580
1581        self.control = None
1582        self.down_pos = None
1583        self.annotation_item = None
1584        self.annotation = None
1585        self.document.view().setCursor(Qt.ArrowCursor)
1586
1587        # Clear the help tip
1588        helpevent = QuickHelpTipEvent("", "")
1589        QCoreApplication.postEvent(self.document, helpevent)
1590
1591        super().end()
1592
1593
1594class ResizeTextAnnotation(UserInteraction):
1595    """
1596    Resize a Text Annotation interaction handler.
1597    """
1598    def __init__(self, document, *args, **kwargs):
1599        # type: (SchemeEditWidget, Any, Any) -> None
1600        super().__init__(document, *args, **kwargs)
1601        self.item = None        # type: Optional[items.TextAnnotation]
1602        self.annotation = None  # type: Optional[scheme.SchemeTextAnnotation]
1603        self.control = None     # type: Optional[controlpoints.ControlPointRect]
1604        self.savedFramePen = None  # type: Optional[QPen]
1605        self.savedRect = None      # type: Optional[QRectF]
1606
1607    def mousePressEvent(self, event):
1608        # type: (QGraphicsSceneMouseEvent) -> bool
1609        pos = event.scenePos()
1610        if event.button() & Qt.LeftButton and self.item is None:
1611            item = self.scene.item_at(pos, items.TextAnnotation)
1612            if item is not None and not item.hasFocus():
1613                self.editItem(item)
1614                return False
1615        return super().mousePressEvent(event)
1616
1617    def editItem(self, item):
1618        # type: (items.TextAnnotation) -> None
1619        annotation = self.scene.annotation_for_item(item)
1620        rect = item.geometry()  # TODO: map to scene if item has a parent.
1621        control = controlpoints.ControlPointRect(rect=rect)
1622        self.scene.addItem(control)
1623
1624        self.savedFramePen = item.framePen()
1625        self.savedRect = rect
1626
1627        control.rectEdited.connect(item.setGeometry)
1628        control.setFocusProxy(item)
1629
1630        item.setFramePen(QPen(Qt.DashDotLine))
1631        item.geometryChanged.connect(self.__on_textGeometryChanged)
1632
1633        self.item = item
1634
1635        self.annotation = annotation
1636        self.control = control
1637
1638    def commit(self):
1639        # type: () -> None
1640        """
1641        Commit the current item geometry state to the document.
1642        """
1643        if self.item is None:
1644            return
1645        rect = self.item.geometry()
1646        if self.savedRect != rect:
1647            command = commands.SetAttrCommand(
1648                self.annotation, "rect",
1649                (rect.x(), rect.y(), rect.width(), rect.height()),
1650                name="Edit text geometry"
1651            )
1652            self.document.undoStack().push(command)
1653            self.savedRect = rect
1654
1655    def __on_editingFinished(self):
1656        # type: () -> None
1657        self.commit()
1658        self.end()
1659
1660    def __on_rectEdited(self, rect):
1661        # type: (QRectF) -> None
1662        assert self.item is not None
1663        self.item.setGeometry(rect)
1664
1665    def __on_textGeometryChanged(self):
1666        # type: () -> None
1667        assert self.control is not None and self.item is not None
1668        if not self.control.isControlActive():
1669            rect = self.item.geometry()
1670            self.control.setRect(rect)
1671
1672    def cancel(self, reason=UserInteraction.OtherReason):
1673        # type: (int) -> None
1674        log.debug("ResizeTextAnnotation.cancel(%s)", reason)
1675        if self.item is not None and self.savedRect is not None:
1676            self.item.setGeometry(self.savedRect)
1677        super().cancel(reason)
1678
1679    def end(self):
1680        # type: () -> None
1681        if self.control is not None:
1682            self.scene.removeItem(self.control)
1683
1684        if self.item is not None and self.savedFramePen is not None:
1685            self.item.setFramePen(self.savedFramePen)
1686
1687        self.item = None
1688        self.annotation = None
1689        self.control = None
1690
1691        super().end()
1692
1693
1694class ResizeArrowAnnotation(UserInteraction):
1695    """
1696    Resize an Arrow Annotation interaction handler.
1697    """
1698    def __init__(self, document, *args, **kwargs):
1699        # type: (SchemeEditWidget, Any, Any) -> None
1700        super().__init__(document, *args, **kwargs)
1701        self.item = None        # type: Optional[items.ArrowAnnotation]
1702        self.annotation = None  # type: Optional[scheme.SchemeArrowAnnotation]
1703        self.control = None     # type: Optional[controlpoints.ControlPointLine]
1704        self.savedLine = None   # type: Optional[QLineF]
1705
1706    def mousePressEvent(self, event):
1707        # type: (QGraphicsSceneMouseEvent) -> bool
1708        pos = event.scenePos()
1709        if self.item is None:
1710            item = self.scene.item_at(pos, items.ArrowAnnotation)
1711            if item is not None and not item.hasFocus():
1712                self.editItem(item)
1713                return False
1714
1715        return super().mousePressEvent(event)
1716
1717    def editItem(self, item):
1718        # type: (items.ArrowAnnotation) -> None
1719        annotation = self.scene.annotation_for_item(item)
1720        control = controlpoints.ControlPointLine()
1721        self.scene.addItem(control)
1722
1723        line = item.line()
1724        self.savedLine = line
1725
1726        p1, p2 = map(item.mapToScene, (line.p1(), line.p2()))
1727
1728        control.setLine(QLineF(p1, p2))
1729        control.setFocusProxy(item)
1730        control.lineEdited.connect(self.__on_lineEdited)
1731
1732        item.geometryChanged.connect(self.__on_lineGeometryChanged)
1733
1734        self.item = item
1735        self.annotation = annotation
1736        self.control = control
1737
1738    def commit(self):
1739        # type: () -> None
1740        """Commit the current geometry of the item to the document.
1741
1742        Does nothing if the actual geometry has not changed.
1743        """
1744        if self.control is None or self.item is None:
1745            return
1746        line = self.control.line()
1747        p1, p2 = line.p1(), line.p2()
1748
1749        if self.item.line() != self.savedLine:
1750            command = commands.SetAttrCommand(
1751                self.annotation,
1752                "geometry",
1753                ((p1.x(), p1.y()), (p2.x(), p2.y())),
1754                name="Edit arrow geometry",
1755            )
1756            self.document.undoStack().push(command)
1757            self.savedLine = self.item.line()
1758
1759    def __on_editingFinished(self):
1760        # type: () -> None
1761        self.commit()
1762        self.end()
1763
1764    def __on_lineEdited(self, line):
1765        # type: (QLineF) -> None
1766        if self.item is not None:
1767            p1, p2 = map(self.item.mapFromScene, (line.p1(), line.p2()))
1768            self.item.setLine(QLineF(p1, p2))
1769
1770    def __on_lineGeometryChanged(self):
1771        # type: () -> None
1772        # Possible geometry change from out of our control, for instance
1773        # item move as a part of a selection group.
1774        assert self.control is not None and self.item is not None
1775        if not self.control.isControlActive():
1776            assert self.item is not None
1777            line = self.item.line()
1778            p1, p2 = map(self.item.mapToScene, (line.p1(), line.p2()))
1779            self.control.setLine(QLineF(p1, p2))
1780
1781    def cancel(self, reason=UserInteraction.OtherReason):
1782        # type: (int) -> None
1783        log.debug("ResizeArrowAnnotation.cancel(%s)", reason)
1784        if self.item is not None and self.savedLine is not None:
1785            self.item.setLine(self.savedLine)
1786
1787        super().cancel(reason)
1788
1789    def end(self):
1790        # type: () -> None
1791        if self.control is not None:
1792            self.scene.removeItem(self.control)
1793
1794        if self.item is not None:
1795            self.item.geometryChanged.disconnect(self.__on_lineGeometryChanged)
1796
1797        self.control = None
1798        self.item = None
1799        self.annotation = None
1800
1801        super().end()
1802
1803
1804class DropHandler(abc.ABC):
1805    """
1806    An abstract drop handler.
1807
1808    .. versionadded:: 0.1.20
1809    """
1810    @abc.abstractmethod
1811    def accepts(self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent') -> bool:
1812        """
1813        Returns True if a `document` can accept a drop of the data from `event`.
1814        """
1815        return False
1816
1817    @abc.abstractmethod
1818    def doDrop(self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent') -> bool:
1819        """
1820        Complete the drop of data from `event` onto the `document`.
1821        """
1822        return False
1823
1824
1825class DropHandlerAction(abc.ABC):
1826    @abc.abstractmethod
1827    def actionFromDropEvent(
1828            self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent'
1829    ) -> QAction:
1830        """
1831        Create and return an QAction representing a drop action.
1832
1833        This action is used to disambiguate between possible drop actions.
1834
1835        The action can have sub menus, however all actions in submenus **must**
1836        have the `DropHandler` instance set as their `QAction.data()`.
1837
1838        The actions **must not** execute the actual drop from their triggered
1839        slot connections. The drop will be dispatched to the `action.data()`
1840        handler's `doDrop()` after that action is triggered and the menu is
1841        closed.
1842        """
1843        raise NotImplementedError
1844
1845
1846class NodeFromMimeDataDropHandler(DropHandler, DropHandlerAction):
1847    """
1848    Create a new node from dropped mime data.
1849
1850    Subclasses must override `canDropMimeData`, `parametersFromMimeData`,
1851    and `qualifiedName`.
1852
1853    .. versionadded:: 0.1.20
1854    """
1855    @abc.abstractmethod
1856    def qualifiedName(self) -> str:
1857        """
1858        The qualified name for the node created by this handler. The handler
1859        will not be invoked if this name does not appear in the registry
1860        associated with the workflow.
1861        """
1862        raise NotImplementedError
1863
1864    @abc.abstractmethod
1865    def canDropMimeData(self, document: 'SchemeEditWidget', data: 'QMimeData') -> bool:
1866        """
1867        Can the handler create a node from the drop mime data.
1868
1869        Reimplement this in a subclass to check if the `data` has appropriate
1870        format.
1871        """
1872        raise NotImplementedError
1873
1874    @abc.abstractmethod
1875    def parametersFromMimeData(self, document: 'SchemeEditWidget', data: 'QMimeData') -> 'Dict[str, Any]':
1876        """
1877        Return the node parameters based from the drop mime data.
1878        """
1879        raise NotImplementedError
1880
1881    def accepts(self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent') -> bool:
1882        """Reimplemented."""
1883        reg = document.registry()
1884        if not reg.has_widget(self.qualifiedName()):
1885            return False
1886        return self.canDropMimeData(document, event.mimeData())
1887
1888    def nodeFromMimeData(self, document: 'SchemeEditWidget', data: 'QMimeData') -> 'Node':
1889        reg = document.registry()
1890        wd = reg.widget(self.qualifiedName())
1891        node = document.newNodeHelper(wd)
1892        parameters = self.parametersFromMimeData(document, data)
1893        node.properties = parameters
1894        return node
1895
1896    def doDrop(self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent') -> bool:
1897        """Reimplemented."""
1898        reg = document.registry()
1899        if not reg.has_widget(self.qualifiedName()):
1900            return False
1901        node = self.nodeFromMimeData(document, event.mimeData())
1902        node.position = (event.scenePos().x(), event.scenePos().y())
1903        activate = self.shouldActivateNode()
1904        wd = document.widgetManager()
1905        if activate and wd is not None:
1906            def activate(node_, widget):
1907                if node_ is node:
1908                    try:
1909                        self.activateNode(document, node, widget)
1910                    finally:
1911                        # self-disconnect the slot
1912                        wd.widget_for_node_added.disconnect(activate)
1913            wd.widget_for_node_added.connect(activate)
1914        document.addNode(node)
1915        if activate:
1916            QApplication.postEvent(node, WorkflowEvent(WorkflowEvent.NodeActivateRequest))
1917        return True
1918
1919    def shouldActivateNode(self) -> bool:
1920        """
1921        Should the new dropped node activate (open GUI controller) immediately.
1922
1923        If this method returns `True` then the `activateNode` method will be
1924        called after the node has been added and the GUI controller created.
1925
1926        The default implementation returns False.
1927        """
1928        return False
1929
1930    def activateNode(self, document: 'SchemeEditWidget', node: 'Node', widget: 'QWidget') -> None:
1931        """
1932        Activate (open) the `node`'s GUI controller `widget` after a
1933        completed drop.
1934
1935        Reimplement this if the node requires further configuration via the
1936        GUI.
1937
1938        The default implementation delegates to the :class:`WidgetManager`
1939        associated with the document.
1940        """
1941        wd = document.widgetManager()
1942        if wd is not None:
1943            wd.activate_widget_for_node(node, widget)
1944        else:
1945            widget.show()
1946
1947    def actionFromDropEvent(
1948            self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent'
1949    ) -> QAction:
1950        """Reimplemented."""
1951        reg = document.registry()
1952        ac = QAction(None)
1953        ac.setData(self)
1954        if reg is not None:
1955            desc = reg.widget(self.qualifiedName())
1956            ac.setText(desc.name)
1957            ac.setToolTip(tooltip_helper(desc))
1958            ac.setWhatsThis(whats_this_helper(desc))
1959        else:
1960            ac.setText(f"{self.qualifiedName()}")
1961            ac.setEnabled(False)
1962            ac.setVisible(False)
1963        return ac
1964
1965
1966def load_entry_point(
1967        ep: EntryPoint, log: logging.Logger = None,
1968) -> Tuple['EntryPoint', Any]:
1969    if log is None:
1970        log = logging.getLogger(__name__)
1971    try:
1972        value = ep.load()
1973    except (ImportError, AttributeError):
1974        log.exception("Could not load %s", ep)
1975    except Exception:  # noqa
1976        log.exception("Unexpected Error; %s will be skipped", ep)
1977    else:
1978        return ep, value
1979
1980
1981def iter_load_entry_points(
1982        iter: Iterable[EntryPoint], log: logging.Logger = None,
1983):
1984    if log is None:
1985        log = logging.getLogger(__name__)
1986    for ep in iter:
1987        try:
1988            ep, value = load_entry_point(ep, log)
1989        except Exception:
1990            pass
1991        else:
1992            yield ep, value
1993
1994
1995class PluginDropHandler(DropHandler):
1996    """
1997    Delegate drop event processing to plugin drop handlers.
1998
1999    .. versionadded:: 0.1.20
2000    """
2001    #: The default entry point group
2002    ENTRY_POINT = "orangecanvas.document.interactions.DropHandler"
2003
2004    def __init__(self, group=ENTRY_POINT, **kwargs):
2005        super().__init__(**kwargs)
2006        self.__group = group
2007
2008    def iterEntryPoints(self) -> Iterable['EntryPoint']:
2009        """
2010        Return an iterator over all entry points.
2011        """
2012        return entry_points().get(self.__group, [])
2013
2014    __entryPoints = None
2015
2016    def entryPoints(self) -> Iterable[Tuple['EntryPoint', 'DropHandler']]:
2017        """
2018        Return an iterator over entry points and instantiated drop handlers.
2019        """
2020        eps = []
2021        if self.__entryPoints:
2022            ep_iter = self.__entryPoints
2023            store_eps = lambda ep, value: None
2024        else:
2025            ep_iter = self.iterEntryPoints()
2026            ep_iter = iter_load_entry_points(ep_iter, log)
2027            store_eps = lambda ep, value: eps.append((ep, value))
2028
2029        for ep, value in ep_iter:
2030            if not issubclass(value, DropHandler):
2031                log.error(
2032                    f"{ep} yielded {type(value)}, expected a "
2033                    f"{DropHandler} subtype"
2034                )
2035                continue
2036            try:
2037                handler = value()
2038            except Exception:  # noqa
2039                log.exception("Error in default constructor of %s", value)
2040            else:
2041                yield ep, handler
2042                store_eps(ep, value)
2043
2044        self.__entryPoints = tuple(eps)
2045
2046    __accepts: Sequence[Tuple[EntryPoint, DropHandler]] = ()
2047
2048    def accepts(self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent') -> bool:
2049        """
2050        Reimplemented.
2051
2052        Accept the event if any plugin handlers accept the event.
2053        """
2054        accepts = []
2055        self.__accepts = ()
2056        for ep, handler in self.entryPoints():
2057            if handler.accepts(document, event):
2058                accepts.append((ep, handler))
2059        self.__accepts = tuple(accepts)
2060        return bool(accepts)
2061
2062    def doDrop(
2063            self, document: 'SchemeEditWidget', event: 'QGraphicsSceneDragDropEvent'
2064    ) -> bool:
2065        """
2066        Reimplemented.
2067
2068        Dispatch the drop to the plugin handler that accepted the event.
2069        In case there are multiple handlers that accepted the event, a menu
2070        is used to select the handler.
2071        """
2072        handler: Optional[DropHandler] = None
2073        if len(self.__accepts) == 1:
2074            ep, handler = self.__accepts[0]
2075        elif len(self.__accepts) > 1:
2076            menu = QMenu(event.widget())
2077            for ep_, handler_ in self.__accepts:
2078                ac = action_for_handler(handler_, document, event)
2079                if ac is None:
2080                    ac = menu.addAction(ep_.name, )
2081                else:
2082                    menu.addAction(ac)
2083                    ac.setParent(menu)
2084                if not ac.toolTip():
2085                    ac.setToolTip(f"{ep_.name} ({ep_.module_name})")
2086                ac.setData(handler_)
2087            action = menu.exec(event.screenPos())
2088            if action is not None:
2089                handler = action.data()
2090        if handler is None:
2091            return False
2092        return handler.doDrop(document, event)
2093
2094
2095def action_for_handler(handler: DropHandler, document, event) -> Optional[QAction]:
2096    if isinstance(handler, DropHandlerAction):
2097        return handler.actionFromDropEvent(document, event)
2098    else:
2099        return None
2100
2101
2102class DropAction(UserInteraction):
2103    """
2104    A drop action on the workflow.
2105    """
2106    def __init__(
2107            self, document, *args, dropHandlers: Sequence[DropHandler] = (),
2108            **kwargs
2109    ) -> None:
2110        super().__init__(document, *args, **kwargs)
2111        self.__designatedAction: Optional[DropHandler] = None
2112        self.__dropHandlers = dropHandlers
2113
2114    def dropHandlers(self) -> Iterable[DropHandler]:
2115        """Return an iterable over drop handlers."""
2116        return iter(self.__dropHandlers)
2117
2118    def canHandleDrop(self, event: 'QGraphicsSceneDragDropEvent') -> bool:
2119        """
2120        Can this interactions handle the drop `event`.
2121
2122        The default implementation checks each `dropHandler` if it
2123        :func:`~DropHandler.accepts` the event. The first such handler that
2124        accepts is selected to be the designated handler and will receive
2125        the drop (:func:`~DropHandler.doDrop`).
2126        """
2127        for ep in self.dropHandlers():
2128            if ep.accepts(self.document, event):
2129                self.__designatedAction = ep
2130                return True
2131        else:
2132            return False
2133
2134    def dragEnterEvent(self, event):
2135        if self.canHandleDrop(event):
2136            event.acceptProposedAction()
2137            return True
2138        else:
2139            return False
2140
2141    def dragMoveEvent(self, event):
2142        if self.canHandleDrop(event):
2143            event.acceptProposedAction()
2144            return True
2145        else:
2146            return False
2147
2148    def dragLeaveEvent(self, even):
2149        self.__designatedAction = None
2150        self.end()
2151        return False
2152
2153    def dropEvent(self, event):
2154        if self.__designatedAction is not None:
2155            try:
2156                res = self.__designatedAction.doDrop(self.document, event)
2157            except Exception:  # noqa
2158                log.exception("")
2159                res = False
2160            if res:
2161                event.acceptProposedAction()
2162            self.end()
2163            return True
2164        else:
2165            self.end()
2166            return False
2167