mirror of
https://github.com/ComradCollective/Comrad
synced 2024-11-09 13:10:44 +00:00
176 lines
5.5 KiB
Python
176 lines
5.5 KiB
Python
"""
|
|
Kivy asyncio example app.
|
|
|
|
Kivy needs to run on the main thread and its graphical instructions have to be
|
|
called from there. But it's still possible to run an asyncio EventLoop, it
|
|
just has to happen on its own, separate thread.
|
|
|
|
Requires Python 3.5+.
|
|
"""
|
|
|
|
import kivy
|
|
|
|
kivy.require('1.10.0')
|
|
|
|
import asyncio
|
|
import threading
|
|
|
|
from kivy.app import App
|
|
from kivy.clock import mainthread
|
|
from kivy.event import EventDispatcher
|
|
from kivy.lang import Builder
|
|
from kivy.uix.boxlayout import BoxLayout
|
|
|
|
|
|
KV = '''\
|
|
<RootLayout>:
|
|
orientation: 'vertical'
|
|
Button:
|
|
id: btn
|
|
text: 'Start EventLoop thread.'
|
|
on_press: app.start_event_loop_thread()
|
|
TextInput:
|
|
multiline: False
|
|
size_hint_y: 0.25
|
|
on_text: app.submit_pulse_text(args[1])
|
|
BoxLayout:
|
|
Label:
|
|
id: pulse_listener
|
|
Label:
|
|
id: status
|
|
'''
|
|
|
|
|
|
class RootLayout(BoxLayout):
|
|
pass
|
|
|
|
|
|
class EventLoopWorker(EventDispatcher):
|
|
|
|
__events__ = ('on_pulse',) # defines this EventDispatcher's sole event
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._thread = threading.Thread(target=self._run_loop) # note the Thread target here
|
|
self._thread.daemon = True
|
|
self.loop = None
|
|
# the following are for the pulse() coroutine, see below
|
|
self._default_pulse = ['tick!', 'tock!']
|
|
self._pulse = None
|
|
self._pulse_task = None
|
|
|
|
def _run_loop(self):
|
|
self.loop = asyncio.get_event_loop_policy().new_event_loop()
|
|
asyncio.set_event_loop(self.loop)
|
|
self._restart_pulse()
|
|
# this example doesn't include any cleanup code, see the docs on how
|
|
# to properly set up and tear down an asyncio event loop
|
|
self.loop.run_forever()
|
|
|
|
def start(self):
|
|
self._thread.start()
|
|
|
|
async def pulse(self):
|
|
"""Core coroutine of this asyncio event loop.
|
|
|
|
Repeats a pulse message in a short interval on three channels:
|
|
|
|
- using `print()`
|
|
- by dispatching a Kivy event `on_pulse` with the help of `@mainthread`
|
|
- on the Kivy thread through `kivy_update_status()` with the help of
|
|
`@mainthread`
|
|
|
|
The decorator `@mainthread` is a convenience wrapper around
|
|
`Clock.schedule_once()` which ensures the callables run on the Kivy
|
|
thread.
|
|
"""
|
|
for msg in self._pulse_messages():
|
|
# show it through the console:
|
|
print(msg)
|
|
|
|
# `EventLoopWorker` is an `EventDispatcher` to which others can
|
|
# subscribe. See `display_on_pulse()` in `start_event_loop_thread()`
|
|
# on how it is bound to the `on_pulse` event. The indirection
|
|
# through the `notify()` function is necessary to apply the
|
|
# `@mainthread` decorator (left label):
|
|
@mainthread
|
|
def notify(text):
|
|
self.dispatch('on_pulse', text)
|
|
|
|
notify(msg) # dispatch the on_pulse event
|
|
|
|
# Same, but with a direct call instead of an event (right label):
|
|
@mainthread
|
|
def kivy_update_status(text):
|
|
status_label = App.get_running_app().root.ids.status
|
|
status_label.text = text
|
|
|
|
kivy_update_status(msg) # control a Label directly
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
def set_pulse_text(self, text):
|
|
self._pulse = text
|
|
# it's not really necessary to restart this task; just included for the
|
|
# sake of this example. Comment this line out and see what happens.
|
|
self._restart_pulse()
|
|
|
|
def _restart_pulse(self):
|
|
"""Helper to start/reset the pulse task when the pulse changes."""
|
|
if self._pulse_task is not None:
|
|
self._pulse_task.cancel()
|
|
self._pulse_task = self.loop.create_task(self.pulse())
|
|
|
|
def on_pulse(self, *_):
|
|
"""An EventDispatcher event must have a corresponding method."""
|
|
pass
|
|
|
|
def _pulse_messages(self):
|
|
"""A generator providing an inexhaustible supply of pulse messages."""
|
|
while True:
|
|
if isinstance(self._pulse, str) and self._pulse != '':
|
|
pulse = self._pulse.split()
|
|
yield from pulse
|
|
else:
|
|
yield from self._default_pulse
|
|
|
|
|
|
class AsyncioExampleApp(App):
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.event_loop_worker = None
|
|
|
|
def build(self):
|
|
return RootLayout()
|
|
|
|
def start_event_loop_thread(self):
|
|
"""Start the asyncio event loop thread. Bound to the top button."""
|
|
if self.event_loop_worker is not None:
|
|
return
|
|
self.root.ids.btn.text = ("Running the asyncio EventLoop now...\n\n\n\n"
|
|
"Now enter a few words below.")
|
|
self.event_loop_worker = worker = EventLoopWorker()
|
|
pulse_listener_label = self.root.ids.pulse_listener
|
|
|
|
def display_on_pulse(instance, text):
|
|
pulse_listener_label.text = text
|
|
|
|
# make the label react to the worker's `on_pulse` event:
|
|
worker.bind(on_pulse=display_on_pulse)
|
|
worker.start()
|
|
|
|
def submit_pulse_text(self, text):
|
|
"""Send the TextInput string over to the asyncio event loop worker."""
|
|
worker = self.event_loop_worker
|
|
if worker is not None:
|
|
loop = self.event_loop_worker.loop
|
|
# use the thread safe variant to run it on the asyncio event loop:
|
|
loop.call_soon_threadsafe(worker.set_pulse_text, text)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
Builder.load_string(KV)
|
|
AsyncioExampleApp().run()
|