Skip to content

Commit

Permalink
Merge pull request #2550 from freakboy3742/gio-async
Browse files Browse the repository at this point in the history
Replace GBulb with PyGObject's native asyncio handling
  • Loading branch information
freakboy3742 authored Sep 13, 2024
2 parents fed0c7c + 04a47a3 commit c1b4709
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 50 deletions.
3 changes: 3 additions & 0 deletions android/tests_backend/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ def select_tab(self, index):
item.setChecked(True)
self.impl.onItemSelectedListener(item)

async def wait_for_tab(self, message):
await self.redraw(message)

def tab_enabled(self, index):
return self.native_navigationview.getMenu().getItem(index).isEnabled()

Expand Down
1 change: 1 addition & 0 deletions changes/2550.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The GTK backend was modified to use PyGObject's native asyncio handling, instead of GBulb.
3 changes: 3 additions & 0 deletions cocoa/tests_backend/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def height(self):
def select_tab(self, index):
self.native.selectTabViewItemAtIndex(index)

async def wait_for_tab(self, message):
await self.redraw(message)

def tab_enabled(self, index):
# _isTabEnabled() is a hidden method, so the naming messes with Rubicon's
# property lookup mechanism. Invoke it by passing the message directly.
Expand Down
37 changes: 33 additions & 4 deletions examples/handlers/handlers/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import asyncio
import random

import httpx

import toga
from toga.constants import COLUMN
from toga.style import Pack
Expand All @@ -10,7 +12,8 @@ class HandlerApp(toga.App):
# Button callback functions
def do_clear(self, widget, **kwargs):
self.counter = 0
self.label.text = "Ready."
self.on_running_label.text = "Ready."
self.background_label.text = "Ready."
self.function_label.text = "Ready."
self.generator_label.text = "Ready."
self.async_label.text = "Ready."
Expand Down Expand Up @@ -44,23 +47,43 @@ async def do_async(self, widget, **kwargs):
self.async_label.text = "Ready."
widget.enabled = True

async def on_running(self, **kwargs):
"""A task started when the app is running."""
# This task runs in the background, without blocking the main event loop
while True:
self.counter += 1
self.on_running_label.text = f"On Running: Iteration {self.counter}"
await asyncio.sleep(1)

async def do_background_task(self):
"""A background task."""
# This task runs in the background, without blocking the main event loop
while True:
self.counter += 1
self.label.text = f"Background: Iteration {self.counter}"
self.background_label.text = f"Background: Iteration {self.counter}"
await asyncio.sleep(1)

async def do_web_get(self, widget, **kwargs):
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://jsonplaceholder.typicode.com/posts/{random.randint(0, 100)}"
)

payload = response.json()

self.web_label.text = payload["title"]

def startup(self):
# Set up main window
self.main_window = toga.MainWindow()

# Labels to show responses.
self.label = toga.Label("Ready.", style=Pack(padding=10))
self.on_running_label = toga.Label("Ready.", style=Pack(padding=10))
self.background_label = toga.Label("Ready.", style=Pack(padding=10))
self.function_label = toga.Label("Ready.", style=Pack(padding=10))
self.generator_label = toga.Label("Ready.", style=Pack(padding=10))
self.async_label = toga.Label("Ready.", style=Pack(padding=10))
self.web_label = toga.Label("Ready.", style=Pack(padding=10))

# Add a background task.
self.counter = 0
Expand All @@ -78,17 +101,23 @@ def startup(self):
"Async callback", on_press=self.do_async, style=btn_style
)
btn_clear = toga.Button("Clear", on_press=self.do_clear, style=btn_style)
btn_web = toga.Button(
"Get web content", on_press=self.do_web_get, style=btn_style
)

# Outermost box
box = toga.Box(
children=[
self.label,
self.on_running_label,
self.background_label,
btn_function,
self.function_label,
btn_generator,
self.generator_label,
btn_async,
self.async_label,
btn_web,
self.web_label,
btn_clear,
],
style=Pack(flex=1, direction=COLUMN, padding=10),
Expand Down
1 change: 1 addition & 0 deletions examples/handlers/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ description = "A testing app"
sources = ["handlers"]
requires = [
"../../core",
"httpx",
]


Expand Down
6 changes: 1 addition & 5 deletions gtk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,8 @@ root = ".."

[tool.setuptools_dynamic_dependencies]
dependencies = [
"gbulb >= 0.5.3",
"pycairo >= 1.17.0",
# New asyncio handling introduced in 3.50.0; that code is incompatible
# with gbulb, See #2550 for the code that replaces GBulb with the new
# asyncio code.
"pygobject < 3.50.0",
"pygobject >= 3.50.0",
"toga-core == {version}",
]

Expand Down
20 changes: 14 additions & 6 deletions gtk/src/toga_gtk/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import asyncio
import signal

import gbulb

from toga.app import App as toga_App
from toga.command import Separator

from .keys import gtk_accel
from .libs import IS_WAYLAND, TOGA_DEFAULT_STYLES, Gdk, Gio, GLib, Gtk
from .libs import (
IS_WAYLAND,
TOGA_DEFAULT_STYLES,
Gdk,
Gio,
GLib,
GLibEventLoopPolicy,
Gtk,
)
from .screens import Screen as ScreenImpl


Expand All @@ -21,8 +27,9 @@ def __init__(self, interface):
self.interface = interface
self.interface._impl = self

gbulb.install(gtk=True)
self.loop = asyncio.new_event_loop()
self.policy = GLibEventLoopPolicy()
asyncio.set_event_loop_policy(self.policy)
self.loop = self.policy.get_event_loop()

# Stimulate the build of the app
self.native = Gtk.Application(
Expand Down Expand Up @@ -146,7 +153,8 @@ def main_loop(self):
# Retain a reference to the app so that no-window apps can exist
self.native.hold()

self.loop.run_forever(application=self.native)
# Start the app event loop
self.native.run()

# Release the reference to the app. This can't be invoked by the testbed,
# because it's after the `run_forever()` that runs the testbed.
Expand Down
1 change: 1 addition & 0 deletions gtk/src/toga_gtk/libs/gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")

from gi.events import GLibEventLoopPolicy # noqa: E402, F401
from gi.repository import ( # noqa: E402, F401
Gdk,
GdkPixbuf,
Expand Down
3 changes: 3 additions & 0 deletions gtk/tests_backend/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ def select_tab(self, index):
if self.tab_enabled(index):
self.native.set_current_page(index)

async def wait_for_tab(self, message):
await self.redraw(message, delay=0.1)

def tab_enabled(self, index):
return self.impl.sub_containers[index].get_visible()

Expand Down
3 changes: 3 additions & 0 deletions iOS/tests_backend/widgets/optioncontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def select_more(self):
more = self.impl.native_controller.moreNavigationController
self.impl.native_controller.selectedViewController = more

async def wait_for_tab(self, message):
await self.redraw(message)

def reset_more(self):
more = self.impl.native_controller.moreNavigationController
more.popToRootViewControllerAnimated(False)
Expand Down
Loading

0 comments on commit c1b4709

Please sign in to comment.