FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(runtime): marshal native UI calls to the platform main thread · pythonnative/pythonnative@235007e · GitHub

Repository navigation

Commit 235007e

Browse files
committed
fix(runtime): marshal native UI calls to the platform main thread
1 parent d8e8933 commit 235007e

4 files changed

Lines changed: 410 additions & 109 deletions

File tree

‎src/pythonnative/native_views/android.py‎

Lines changed: 73 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,13 +1516,23 @@ def _present_alert(
15161516
) -> None:
15171517
"""Present an AlertDialog or BottomSheet (``style='action_sheet'``).
15181518
1519+
Safe to call from any thread — the AlertDialog work is automatically
1520+
marshalled to the main looper via
1521+
[`pythonnative.runtime.call_on_main_thread`][pythonnative.runtime.call_on_main_thread].
1522+
Returns immediately; the dialog appears on the next main-loop tick.
1523+
15191524
``buttons`` is a list of ``{"label": str, "style":
15201525
"default"|"cancel"|"destructive"}`` dicts (no ``on_press``).
15211526
Exactly one ``on_result(index)`` is invoked when the user picks a
1522-
button; a dismiss delivers ``-1``. ``on_result`` runs on the
1523-
Android main thread.
1527+
button; a dismiss delivers ``-1``. ``on_result`` always runs on
1528+
the Android main thread; if it needs to wake an asyncio.Future,
1529+
use
1530+
[`pythonnative.runtime.resolve_future`][pythonnative.runtime.resolve_future]
1531+
to hop back onto the loop thread.
15241532
"""
15251533
del style # AlertDialog has no distinct action-sheet style on Android.
1534+
from ..runtime import call_on_main_thread
1535+
15261536
delivered = [False]
15271537

15281538
def _deliver(index: int) -> None:
@@ -1534,69 +1544,71 @@ def _deliver(index: int) -> None:
15341544
except Exception:
15351545
pass
15361546

1537-
try:
1538-
AlertDialog = jclass("android.app.AlertDialog$Builder")
1539-
builder = AlertDialog(_ctx())
1540-
builder.setTitle(str(title or ""))
1541-
if message is not None:
1542-
builder.setMessage(str(message))
1543-
if not buttons:
1544-
buttons = [{"label": "OK", "style": "default"}]
1545-
1546-
# AlertDialog only has three slots (positive/negative/neutral).
1547-
# Assign the first matching style class to the conventional
1548-
# slot, then spill any leftovers into whichever slot is free.
1549-
slot_for: dict = {}
1550-
free_slots = ["positive", "negative", "neutral"]
1551-
for i, spec in enumerate(buttons):
1552-
kind = spec.get("style", "default")
1553-
preferred = {
1554-
"default": "positive",
1555-
"cancel": "negative",
1556-
"destructive": "neutral",
1557-
}.get(kind, "positive")
1558-
if preferred in free_slots:
1559-
slot_for[i] = preferred
1560-
free_slots.remove(preferred)
1561-
for i, spec in enumerate(buttons):
1562-
if i in slot_for:
1563-
continue
1564-
if not free_slots:
1565-
break
1566-
slot_for[i] = free_slots.pop(0)
1567-
1568-
OnClickListener = jclass("android.content.DialogInterface$OnClickListener")
1569-
1570-
def make_listener(button_index: int) -> Any:
1571-
class _Proxy(dynamic_proxy(OnClickListener)):
1572-
def onClick(self, dialog: Any, which: int) -> None:
1573-
_deliver(button_index)
1574-
1575-
return _Proxy()
1547+
def _present_on_main() -> None:
1548+
try:
1549+
AlertDialog = jclass("android.app.AlertDialog$Builder")
1550+
builder = AlertDialog(_ctx())
1551+
builder.setTitle(str(title or ""))
1552+
if message is not None:
1553+
builder.setMessage(str(message))
1554+
button_specs = buttons or [{"label": "OK", "style": "default"}]
1555+
1556+
# AlertDialog only has three slots (positive/negative/neutral).
1557+
# Assign the first matching style class to the conventional
1558+
# slot, then spill any leftovers into whichever slot is free.
1559+
slot_for: dict = {}
1560+
free_slots = ["positive", "negative", "neutral"]
1561+
for i, spec in enumerate(button_specs):
1562+
kind = spec.get("style", "default")
1563+
preferred = {
1564+
"default": "positive",
1565+
"cancel": "negative",
1566+
"destructive": "neutral",
1567+
}.get(kind, "positive")
1568+
if preferred in free_slots:
1569+
slot_for[i] = preferred
1570+
free_slots.remove(preferred)
1571+
for i, spec in enumerate(button_specs):
1572+
if i in slot_for:
1573+
continue
1574+
if not free_slots:
1575+
break
1576+
slot_for[i] = free_slots.pop(0)
1577+
1578+
OnClickListener = jclass("android.content.DialogInterface$OnClickListener")
1579+
1580+
def make_listener(button_index: int) -> Any:
1581+
class _Proxy(dynamic_proxy(OnClickListener)):
1582+
def onClick(self, dialog: Any, which: int) -> None:
1583+
_deliver(button_index)
1584+
1585+
return _Proxy()
1586+
1587+
for i, spec in enumerate(button_specs):
1588+
slot = slot_for.get(i)
1589+
if slot is None:
1590+
continue
1591+
label = str(spec.get("label", ""))
1592+
listener = make_listener(i)
1593+
if slot == "positive":
1594+
builder.setPositiveButton(label, listener)
1595+
elif slot == "negative":
1596+
builder.setNegativeButton(label, listener)
1597+
else:
1598+
builder.setNeutralButton(label, listener)
15761599

1577-
for i, spec in enumerate(buttons):
1578-
slot = slot_for.get(i)
1579-
if slot is None:
1580-
continue
1581-
label = str(spec.get("label", ""))
1582-
listener = make_listener(i)
1583-
if slot == "positive":
1584-
builder.setPositiveButton(label, listener)
1585-
elif slot == "negative":
1586-
builder.setNegativeButton(label, listener)
1587-
else:
1588-
builder.setNeutralButton(label, listener)
1600+
OnCancelListener = jclass("android.content.DialogInterface$OnCancelListener")
15891601

1590-
OnCancelListener = jclass("android.content.DialogInterface$OnCancelListener")
1602+
class _CancelProxy(dynamic_proxy(OnCancelListener)):
1603+
def onCancel(self, dialog: Any) -> None:
1604+
_deliver(-1)
15911605

1592-
class _CancelProxy(dynamic_proxy(OnCancelListener)):
1593-
def onCancel(self, dialog: Any) -> None:
1594-
_deliver(-1)
1606+
builder.setOnCancelListener(_CancelProxy())
1607+
builder.show()
1608+
except Exception:
1609+
_deliver(-1)
15951610

1596-
builder.setOnCancelListener(_CancelProxy())
1597-
builder.show()
1598-
except Exception:
1599-
_deliver(-1)
1611+
call_on_main_thread(_present_on_main)
16001612

16011613

16021614
# ======================================================================

‎src/pythonnative/native_views/ios.py‎

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,62 +2467,71 @@ def _present_alert(
24672467
) -> None:
24682468
"""Present a UIAlertController of the given style.
24692469
2470+
Safe to call from any thread — the UIKit work is automatically
2471+
marshalled to the main thread via
2472+
[`pythonnative.runtime.call_on_main_thread`][pythonnative.runtime.call_on_main_thread].
2473+
Returns immediately; the alert appears on the next main-loop tick.
2474+
24702475
``buttons`` is a list of ``{"label": str, "style":
24712476
"default"|"cancel"|"destructive"}`` dicts (no ``on_press``). When
24722477
the user picks button ``i`` the helper invokes ``on_result(i)``
24732478
exactly once. A dismiss (e.g. swipe-to-cancel on iPad) delivers
2474-
``-1``. ``on_result`` runs on the main thread.
2479+
``-1``. ``on_result`` always runs on the main thread; if it needs
2480+
to wake an asyncio.Future, use
2481+
[`pythonnative.runtime.resolve_future`][pythonnative.runtime.resolve_future]
2482+
to hop back onto the loop thread.
24752483
"""
2476-
try:
2477-
UIAlertController = ObjCClass("UIAlertController")
2478-
UIAlertAction = ObjCClass("UIAlertAction")
2479-
UIApplication = ObjCClass("UIApplication")
2480-
ctrl = UIAlertController.alertControllerWithTitle_message_preferredStyle_(
2481-
str(title or ""),
2482-
str(message) if message is not None else None,
2483-
0 if style == "alert" else 1,
2484-
)
2485-
if not buttons:
2486-
buttons = [{"label": "OK", "style": "default"}]
2484+
from ..runtime import call_on_main_thread
24872485

2488-
delivered = [False]
2486+
delivered = [False]
24892487

2490-
def _deliver(index: int) -> None:
2491-
if delivered[0]:
2492-
return
2493-
delivered[0] = True
2494-
try:
2495-
on_result(index)
2496-
except Exception:
2497-
pass
2488+
def _deliver(index: int) -> None:
2489+
if delivered[0]:
2490+
return
2491+
delivered[0] = True
2492+
try:
2493+
on_result(index)
2494+
except Exception:
2495+
pass
2496+
2497+
def _present_on_main() -> None:
2498+
try:
2499+
UIAlertController = ObjCClass("UIAlertController")
2500+
UIAlertAction = ObjCClass("UIAlertAction")
2501+
UIApplication = ObjCClass("UIApplication")
2502+
ctrl = UIAlertController.alertControllerWithTitle_message_preferredStyle_(
2503+
str(title or ""),
2504+
str(message) if message is not None else None,
2505+
0 if style == "alert" else 1,
2506+
)
2507+
button_specs = buttons or [{"label": "OK", "style": "default"}]
24982508

2499-
for i, spec in enumerate(buttons):
2500-
label = str(spec.get("label", "OK"))
2501-
kind = spec.get("style", "default")
2502-
kind_int = {"default": 0, "cancel": 1, "destructive": 2}.get(kind, 0)
2509+
for i, spec in enumerate(button_specs):
2510+
label = str(spec.get("label", "OK"))
2511+
kind = spec.get("style", "default")
2512+
kind_int = {"default": 0, "cancel": 1, "destructive": 2}.get(kind, 0)
25032513

2504-
def make_handler(button_index: int) -> Any:
2505-
def _on_action(action: _ct.c_void_p) -> None: # noqa: ARG001
2506-
_deliver(button_index)
2514+
def make_handler(button_index: int) -> Any:
2515+
def _on_action(action: _ct.c_void_p) -> None: # noqa: ARG001
2516+
_deliver(button_index)
25072517

2508-
return _on_action
2518+
return _on_action
25092519

2510-
action = UIAlertAction.actionWithTitle_style_handler_(
2511-
label,
2512-
kind_int,
2513-
make_handler(i),
2514-
)
2515-
ctrl.addAction_(action)
2516-
top = _top_view_controller_for_alert(UIApplication.sharedApplication)
2517-
if top is not None:
2518-
top.presentViewController_animated_completion_(ctrl, True, None)
2519-
else:
2520-
_deliver(-1)
2521-
except Exception:
2522-
try:
2523-
on_result(-1)
2520+
action = UIAlertAction.actionWithTitle_style_handler_(
2521+
label,
2522+
kind_int,
2523+
make_handler(i),
2524+
)
2525+
ctrl.addAction_(action)
2526+
top = _top_view_controller_for_alert(UIApplication.sharedApplication)
2527+
if top is not None:
2528+
top.presentViewController_animated_completion_(ctrl, True, None)
2529+
else:
2530+
_deliver(-1)
25242531
except Exception:
2525-
pass
2532+
_deliver(-1)
2533+
2534+
call_on_main_thread(_present_on_main)
25262535

25272536

25282537
# ======================================================================

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL