File: event_loop_helper.py

package info (click to toggle)
python-pyface 8.0.0-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 13,944 kB
  • sloc: python: 54,107; makefile: 82
file content (217 lines) | stat: -rw-r--r-- 7,371 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!


import contextlib
import threading
import warnings

from pyface.i_gui import IGUI
from pyface.qt import QtCore, QtGui
from traits.api import HasStrictTraits, Instance


class ConditionTimeoutError(RuntimeError):
    pass


@contextlib.contextmanager
def dont_quit_when_last_window_closed(qt_app):
    """
    Suppress exit of the application when the last window is closed.

    """
    flag = qt_app.quitOnLastWindowClosed()
    qt_app.setQuitOnLastWindowClosed(False)
    try:
        yield
    finally:
        qt_app.setQuitOnLastWindowClosed(flag)


class EventLoopHelper(HasStrictTraits):

    qt_app = Instance(QtGui.QApplication)

    gui = Instance(IGUI)

    def event_loop_with_timeout(self, repeat=2, timeout=10.0):
        """Helper function to send all posted events to the event queue and
        wait for them to be processed.  This runs the real event loop and
        does not emulate it with QApplication.processEvents.

        Parameters
        ----------
        repeat : int
            Number of times to process events. Default is 2.
        timeout: float, optional, keyword only
            Number of seconds to run the event loop in the case that the trait
            change does not occur. Default value is 10.0.

        Notes
        -----
            `timeout` is rounded to the nearest millisecond.
        """

        def repeat_loop(condition, repeat):
            # We sendPostedEvents to ensure that enaml events are processed
            self.qt_app.sendPostedEvents()
            repeat = repeat - 1
            if repeat <= 0:
                self.gui.invoke_later(condition.set)
            else:
                self.gui.invoke_later(
                    repeat_loop, condition=condition, repeat=repeat
                )

        condition = threading.Event()
        self.gui.invoke_later(repeat_loop, repeat=repeat, condition=condition)
        self.event_loop_until_condition(
            condition=condition.is_set, timeout=timeout
        )

    def event_loop(self, repeat=1):
        """Emulates an event loop `repeat` times with
        QApplication.processEvents.

        Parameters
        ----------
        repeat : int
            Number of times to process events. Default is 1.
        """
        for i in range(repeat):
            self.qt_app.sendPostedEvents()
            self.qt_app.processEvents()

    def event_loop_until_condition(self, condition, timeout=10.0):
        """Runs the real Qt event loop until the provided condition evaluates
        to True.

        Notes
        -----

        This runs the real Qt event loop, polling the condition every 50 ms and
        returning as soon as the condition becomes true. If the condition does
        not become true within the given timeout, a ConditionTimeoutError is
        raised.

        Because the state of the condition is only polled every 50 ms, it
        may fail to detect transient states that appear and disappear within
        a 50 ms window.  Code should ensure that any state that is being
        tested by the condition cannot revert to a False value once it becomes
        True.

        Parameters
        ----------
        condition : Callable
            A callable to determine if the stop criteria have been met. This
            should accept no arguments.

        timeout : float
            Number of seconds to run the event loop in the case that the trait
            change does not occur.

            `timeout` is rounded to the nearest millisecond.

        Raises
        ------

        Raises ConditionTimeoutError if the timeout occurs before the condition
        is satisfied.  If the event loop exits before the condition evaluates
        to True or times out, a RuntimeWarning will be generated.

        In either of these cases, the message will indicate whether the
        condition was ever successfully evaluated (which may indicate an error
        in the condition's code) or whether it always evalutated to False.
        """

        condition_result = None
        timed_out = False

        def handler():
            nonlocal condition_result

            condition_result = bool(condition_result or condition())
            if condition_result:
                self.qt_app.exit()

        def do_timeout():
            nonlocal timed_out

            timed_out = True
            self.qt_app.exit()

        # Make sure we don't get a premature exit from the event loop.
        with dont_quit_when_last_window_closed(self.qt_app):
            condition_timer = QtCore.QTimer()
            condition_timer.setInterval(50)
            condition_timer.timeout.connect(handler)
            timeout_timer = QtCore.QTimer()
            timeout_timer.setSingleShot(True)
            timeout_timer.setInterval(round(timeout * 1000))
            timeout_timer.timeout.connect(do_timeout)
            timeout_timer.start()
            condition_timer.start()
            try:
                if hasattr(self.qt_app, 'exec'):
                    self.qt_app.exec()
                else:
                    self.qt_app.exec_()
                if not condition_result:
                    if condition_result is None:
                        status = "without evaluating condition"
                    else:
                        status = "without condition evaluating to True"
                    if timed_out:
                        raise ConditionTimeoutError(f"Timed out {status}.")
                    else:
                        warnings.warn(
                            RuntimeWarning(
                                f"Event loop exited early {status}."
                            )
                        )
            finally:
                timeout_timer.stop()
                condition_timer.stop()

    @contextlib.contextmanager
    def delete_widget(self, widget, timeout=1.0):
        """Runs the real Qt event loop until the widget provided has been
        deleted.  Raises ConditionTimeoutError on timeout.

        Parameters
        ----------
        widget : QObject
            The widget whose deletion will stop the event loop.

        timeout : float
            Number of seconds to run the event loop in the case that the
            widget is not deleted.

        Notes
        -----
            `timeout` is rounded to the nearest millisecond.
        """
        timer = QtCore.QTimer()
        timer.setSingleShot(True)
        timer.setInterval(round(timeout * 1000))
        timer.timeout.connect(self.qt_app.quit)
        widget.destroyed.connect(self.qt_app.quit)
        yield
        timer.start()
        if hasattr(self.qt_app, 'exec'):
            self.qt_app.exec()
        else:
            self.qt_app.exec_()
        if not timer.isActive():
            # We exited the event loop on timeout
            raise ConditionTimeoutError(
                "Could not destroy widget before timeout: {!r}".format(widget)
            )