1"""
2Basic Qt testing framework
3==========================
4"""
5import unittest
6import gc
7from typing import Callable, Any
8
9from AnyQt.QtWidgets import QApplication, QWidget
10from AnyQt.QtCore import (
11    QCoreApplication, QTimer, QStandardPaths, QPoint, Qt, QMimeData
12)
13from AnyQt.QtGui import (
14    QMouseEvent, QDragEnterEvent, QDropEvent, QDragMoveEvent, QDragLeaveEvent,
15    QContextMenuEvent
16)
17from AnyQt.QtTest import QTest
18from AnyQt.QtCore import PYQT_VERSION
19
20DEFAULT_TIMEOUT = 50
21
22
23class QCoreAppTestCase(unittest.TestCase):
24    _AppClass = QCoreApplication
25    app = None  # type: QCoreApplication
26
27    __appdomain = ""
28    __appname = ""
29
30    @classmethod
31    def setUpClass(cls):
32        super(QCoreAppTestCase, cls).setUpClass()
33        QStandardPaths.setTestModeEnabled(True)
34        app = cls._AppClass.instance()
35        if app is None:
36            app = cls._AppClass([])
37        cls.app = app
38        cls.__appname = cls.app.applicationName()
39        cls.__appdomain = cls.app.organizationDomain()
40        cls.app.setApplicationName("orangecanvas.testing")
41        cls.app.setOrganizationDomain("biolab.si")
42
43    def setUp(self):
44        super(QCoreAppTestCase, self).setUp()
45
46    def tearDown(self):
47        super(QCoreAppTestCase, self).tearDown()
48
49    @classmethod
50    def tearDownClass(cls):
51        gc.collect()
52        cls.app.setApplicationName(cls.__appname)
53        cls.app.setOrganizationDomain(cls.__appdomain)
54        cls.app.sendPostedEvents(None, 0)
55        # Keep app instance alive between tests with PyQt5 5.14.0 and later
56        if PYQT_VERSION <= 0x050e00:
57            cls.app = None
58        super(QCoreAppTestCase, cls).tearDownClass()
59        QStandardPaths.setTestModeEnabled(False)
60
61    @classmethod
62    def qWait(cls, timeout=DEFAULT_TIMEOUT):
63        QTest.qWait(timeout)
64
65    @classmethod
66    def singleShot(cls, timeout: int, slot: 'Callable[[], Any]'):
67        QTimer.singleShot(timeout, slot)
68
69
70class QAppTestCase(QCoreAppTestCase):
71    _AppClass = QApplication
72    app = None  # type: QApplication
73
74
75def mouseMove(widget, buttons, modifier=Qt.NoModifier, pos=QPoint(), delay=-1):
76    # type: (QWidget, Qt.MouseButtons, Qt.KeyboardModifier, QPoint, int) -> None
77    """
78    Like QTest.mouseMove, but with `buttons` and `modifier` parameters.
79
80    Parameters
81    ----------
82    widget : QWidget
83    buttons: Qt.MouseButtons
84    modifier : Qt.KeyboardModifiers
85    pos : QPoint
86    delay : int
87    """
88    if pos.isNull():
89        pos = widget.rect().center()
90    me = QMouseEvent(
91        QMouseEvent.MouseMove, pos, widget.mapToGlobal(pos), Qt.NoButton,
92        buttons, modifier
93    )
94    if delay > 0:
95        QTest.qWait(delay)
96
97    QCoreApplication.sendEvent(widget, me)
98
99
100def contextMenu(widget: QWidget, pos: QPoint, delay=-1) -> None:
101    """
102    Simulates a contextMenuEvent on the widget.
103    """
104    ev = QContextMenuEvent(
105        QContextMenuEvent.Mouse, pos, widget.mapToGlobal(pos)
106    )
107    if delay > 0:
108        QTest.qWait(delay)
109    QCoreApplication.sendEvent(widget, ev)
110
111
112def dragDrop(
113        widget: QWidget, mime: QMimeData, pos: QPoint = QPoint(),
114        action=Qt.CopyAction, buttons=Qt.LeftButton, modifiers=Qt.NoModifier
115) -> bool:
116    """
117    Simulate a drag/drop interaction on the `widget`.
118
119    A `QDragEnterEvent`, `QDragMoveEvent` and `QDropEvent` are created and
120    dispatched to the `widget`. However if any of the `QDragEnterEvent` or
121    `QDragMoveEvent` are not accepted, a `QDragLeaveEvent` is dispatched
122    to 'reset' the widget state before this function returns `False`
123
124    Parameters
125    ----------
126    widget: QWidget
127        The target widget.
128    mime: QMimeData
129        The mime data associated with the drag/drop.
130    pos: QPoint
131        Position of the drop
132    action: Qt.DropActions
133        Type of acceptable drop actions
134    buttons: Qt.MouseButtons:
135        Pressed mouse buttons.
136    modifiers: Qt.KeyboardModifiers
137        Pressed keyboard modifiers.
138
139    Returns
140    -------
141    state: bool
142        Were the events accepted.
143
144    See Also
145    --------
146    QDragEnterEvent, QDropEvent
147    """
148    if pos.isNull():
149        pos = widget.rect().center()
150
151    ev = QDragEnterEvent(pos, action, mime, buttons, modifiers)
152    ev.setAccepted(False)
153    QApplication.sendEvent(widget, ev)
154
155    ev = QDragMoveEvent(pos, action, mime, buttons, modifiers)
156    ev.setAccepted(False)
157    QApplication.sendEvent(widget, ev)
158
159    if not ev.isAccepted():
160        QApplication.sendEvent(widget, QDragLeaveEvent())
161        return False
162
163    ev = QDropEvent(pos, action, mime, buttons, modifiers)
164    ev.setAccepted(False)
165    QApplication.sendEvent(widget, ev)
166    return ev.isAccepted()
167
168
169def dragEnterLeave(
170        widget: QWidget, mime: QMimeData, pos=QPoint(),
171        action=Qt.CopyAction, buttons=Qt.LeftButton, modifiers=Qt.NoModifier
172) -> None:
173    """
174    Simulate a drag/move/leave interaction on the `widget`.
175
176    A QDragEnterEvent, QDragMoveEvent and a QDragLeaveEvent are created
177    and dispatched to the widget.
178    """
179    if pos.isNull():
180        pos = widget.rect().center()
181
182    ev = QDragEnterEvent(pos, action, mime, buttons, modifiers)
183    ev.setAccepted(False)
184    QApplication.sendEvent(widget, ev)
185
186    ev = QDragMoveEvent(
187        pos, action, mime, buttons, modifiers, QDragMoveEvent.DragMove
188    )
189    ev.setAccepted(False)
190    QApplication.sendEvent(widget, ev)
191
192    ev = QDragLeaveEvent()
193    ev.setAccepted(False)
194    QApplication.sendEvent(widget, ev)
195    return
196