Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace GBulb with PyGObject's native asyncio handling #2550

Merged
merged 14 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
41 changes: 35 additions & 6 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,27 +47,47 @@ async def do_async(self, widget, **kwargs):
self.async_label.text = "Ready."
widget.enabled = True

async def do_background_task(self, widget, **kwargs):
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, **kwargs):
"""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
self.add_background_task(self.do_background_task)
asyncio.create_task(self.do_background_task())
rmartin16 marked this conversation as resolved.
Show resolved Hide resolved

# Buttons
btn_style = Pack(flex=1)
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",
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved
"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 = asyncio.get_event_loop()
rmartin16 marked this conversation as resolved.
Show resolved Hide resolved

# 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()
rmartin16 marked this conversation as resolved.
Show resolved Hide resolved

# 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