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

Add app-level dialogs. #2669

Merged
merged 17 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 11 additions & 2 deletions android/src/toga_android/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from org.beeware.android import IPythonApp, MainActivity

from toga.command import Command, Group, Separator
from toga.dialogs import InfoDialog
from toga.handlers import simple_handler

from .libs import events
Expand Down Expand Up @@ -280,8 +281,16 @@ def show_about_dialog(self):
message_parts.append(f"Author: {self.interface.author}")
if self.interface.description is not None:
message_parts.append(f"\n{self.interface.description}")
self.interface.main_window.info_dialog(
f"About {self.interface.formal_name}", "\n".join(message_parts)

# Create and show an info dialog as the about dialog.
# We don't care about the response.
asyncio.create_task(
self.interface.dialog(
InfoDialog(
f"About {self.interface.formal_name}",
"\n".join(message_parts),
)
)
)

######################################################################
Expand Down
67 changes: 36 additions & 31 deletions android/src/toga_android/dialogs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from abc import ABC

from android import R
from android.app import AlertDialog
from android.content import DialogInterface
from java import dynamic_proxy

import toga


class OnClickListener(dynamic_proxy(DialogInterface.OnClickListener)):
def __init__(self, fn=None, value=None):
Expand All @@ -16,25 +16,31 @@ def onClick(self, _dialog, _which):
self._fn(self._value)


class BaseDialog(ABC):
def __init__(self, interface):
self.interface = interface
self.interface._impl = self
class BaseDialog:
def show(self, host_window, future):
self.future = future

if self.native:
# Show the dialog. Don't differentiate between app and window modal dialogs.
self.native.show()
else:
# Dialog doesn't have an implementation. This can't be covered, as
# the testbed shortcuts the test before showing the dialog.
self.future.set_result(None) # pragma: no cover


class TextDialog(BaseDialog):
def __init__(
self,
interface,
title,
message,
positive_text,
negative_text=None,
icon=None,
):
super().__init__(interface=interface)
super().__init__()

self.native = AlertDialog.Builder(interface.window._impl.app.native)
self.native = AlertDialog.Builder(toga.App.app.current_window._impl.app.native)
self.native.setCancelable(False)
self.native.setTitle(title)
self.native.setMessage(message)
Expand All @@ -52,26 +58,23 @@ def __init__(
self.native.setNegativeButton(
negative_text, OnClickListener(self.completion_handler, False)
)
self.native.show()

def completion_handler(self, return_value: bool) -> None:
self.interface.set_result(return_value)
self.future.set_result(return_value)


class InfoDialog(TextDialog):
def __init__(self, interface, title, message):
def __init__(self, title, message):
super().__init__(
interface=interface,
title=title,
message=message,
positive_text="OK",
)


class QuestionDialog(TextDialog):
def __init__(self, interface, title, message):
def __init__(self, title, message):
super().__init__(
interface=interface,
title=title,
message=message,
positive_text="Yes",
Expand All @@ -80,9 +83,8 @@ def __init__(self, interface, title, message):


class ConfirmDialog(TextDialog):
def __init__(self, interface, title, message):
def __init__(self, title, message):
super().__init__(
interface=interface,
title=title,
message=message,
positive_text="OK",
Expand All @@ -91,9 +93,8 @@ def __init__(self, interface, title, message):


class ErrorDialog(TextDialog):
def __init__(self, interface, title, message):
def __init__(self, title, message):
super().__init__(
interface=interface,
title=title,
message=message,
positive_text="OK",
Expand All @@ -104,48 +105,52 @@ def __init__(self, interface, title, message):
class StackTraceDialog(BaseDialog):
def __init__(
self,
interface,
title,
message,
**kwargs,
):
super().__init__(interface=interface)
interface.window.factory.not_implemented("Window.stack_trace_dialog()")
super().__init__()

toga.App.app.factory.not_implemented("dialogs.StackTraceDialog()")
self.native = None


class SaveFileDialog(BaseDialog):
def __init__(
self,
interface,
title,
filename,
initial_directory,
file_types=None,
):
super().__init__(interface=interface)
interface.window.factory.not_implemented("Window.save_file_dialog()")
super().__init__()

toga.App.app.factory.not_implemented("dialogs.SaveFileDialog()")
self.native = None


class OpenFileDialog(BaseDialog):
def __init__(
self,
interface,
title,
initial_directory,
file_types,
multiple_select,
):
super().__init__(interface=interface)
interface.window.factory.not_implemented("Window.open_file_dialog()")
super().__init__()

toga.App.app.factory.not_implemented("dialogs.OpenFileDialog()")
self.native = None


class SelectFolderDialog(BaseDialog):
def __init__(
self,
interface,
title,
initial_directory,
multiple_select,
):
super().__init__(interface=interface)
interface.window.factory.not_implemented("Window.select_folder_dialog()")
super().__init__()

toga.App.app.factory.not_implemented("dialogs.SelectFolderDialog()")
self.native = None
7 changes: 5 additions & 2 deletions android/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

from toga import Group

from .dialogs import DialogsMixin
from .probe import BaseProbe
from .window import WindowProbe


class AppProbe(BaseProbe):
class AppProbe(BaseProbe, DialogsMixin):
supports_key = False

def __init__(self, app):
Expand Down Expand Up @@ -73,7 +74,9 @@ def activate_menu_about(self):
self._activate_menu_item(["About Toga Testbed"])

async def close_about_dialog(self):
await self.main_window_probe.close_info_dialog(None)
about_dialog = self.get_dialog_view()
assert about_dialog is not None, "No about dialog displayed"
await self.press_dialog_button(about_dialog, "OK")

def activate_menu_visit_homepage(self):
xfail("This backend doesn't have a visit homepage command")
Expand Down
56 changes: 56 additions & 0 deletions android/tests_backend/dialogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import asyncio

import pytest


class DialogsMixin:
def _setup_alert_dialog_result(self, dialog, buttons, selected_index):
# Install an overridden show method that invokes the original,
# but then closes the open dialog.
orig_show = dialog._impl.show

def automated_show(host_window, future):
orig_show(host_window, future)

async def _close_dialog():
# Inject a small pause without blocking the event loop
await asyncio.sleep(1.0 if self.app.run_slow else 0.2)
try:
dialog_view = self.get_dialog_view()
self.assert_dialog_buttons(dialog_view, buttons)
await self.press_dialog_button(dialog_view, buttons[selected_index])
except Exception as e:
# An error occurred closing the dialog; that means the dialog
# isn't what as expected, so record that in the future.
future.set_exception(e)

asyncio.ensure_future(_close_dialog())

dialog._impl.show = automated_show

def setup_info_dialog_result(self, dialog):
self._setup_alert_dialog_result(dialog, ["OK"], 0)

def setup_question_dialog_result(self, dialog, result):
self._setup_alert_dialog_result(dialog, ["No", "Yes"], 1 if result else 0)

def setup_confirm_dialog_result(self, dialog, result):
self._setup_alert_dialog_result(dialog, ["Cancel", "OK"], 1 if result else 0)

def setup_error_dialog_result(self, dialog):
self._setup_alert_dialog_result(dialog, ["OK"], 0)

def setup_stack_trace_dialog_result(self, dialog, result):
pytest.skip("Stack Trace dialog not implemented on Android")

def setup_save_file_dialog_result(self, dialog, result):
pytest.skip("Save File dialog not implemented on Android")

def setup_open_file_dialog_result(self, dialog, result, multiple_select):
pytest.skip("Open File dialog not implemented on Android")

def setup_select_folder_dialog_result(self, dialog, result, multiple_select):
pytest.skip("Select Folder dialog not implemented on Android")

def is_modal_dialog(self, dialog):
return True
39 changes: 2 additions & 37 deletions android/tests_backend/window.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import pytest
from androidx.appcompat import R as appcompat_R

from .dialogs import DialogsMixin
from .probe import BaseProbe


class WindowProbe(BaseProbe):
class WindowProbe(BaseProbe, DialogsMixin):
def __init__(self, app, window):
super().__init__(app)
self.native = self.app._impl.native
Expand All @@ -20,38 +20,6 @@ def content_size(self):
self.root_view.getHeight() / self.scale_factor,
)

async def close_info_dialog(self, dialog):
dialog_view = self.get_dialog_view()
self.assert_dialog_buttons(dialog_view, ["OK"])
await self.press_dialog_button(dialog_view, "OK")

async def close_question_dialog(self, dialog, result):
dialog_view = self.get_dialog_view()
self.assert_dialog_buttons(dialog_view, ["No", "Yes"])
await self.press_dialog_button(dialog_view, "Yes" if result else "No")

async def close_confirm_dialog(self, dialog, result):
dialog_view = self.get_dialog_view()
self.assert_dialog_buttons(dialog_view, ["Cancel", "OK"])
await self.press_dialog_button(dialog_view, "OK" if result else "Cancel")

async def close_error_dialog(self, dialog):
dialog_view = self.get_dialog_view()
self.assert_dialog_buttons(dialog_view, ["OK"])
await self.press_dialog_button(dialog_view, "OK")

async def close_stack_trace_dialog(self, dialog, result):
pytest.skip("Stack Trace dialog not implemented on Android")

async def close_save_file_dialog(self, dialog, result):
pytest.skip("Save File dialog not implemented on Android")

async def close_open_file_dialog(self, dialog, result, multiple_select):
pytest.skip("Open File dialog not implemented on Android")

async def close_select_folder_dialog(self, dialog, result, multiple_select):
pytest.skip("Select Folder dialog not implemented on Android")

def _native_menu(self):
return self.native.findViewById(appcompat_R.id.action_bar).getMenu()

Expand Down Expand Up @@ -88,6 +56,3 @@ def assert_toolbar_item(self, index, label, tooltip, has_icon, enabled):

def press_toolbar_button(self, index):
self.native.onOptionsItemSelected(self._toolbar_items()[index])

def is_modal_dialog(self, dialog):
return True
1 change: 1 addition & 0 deletions changes/2669.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dialogs can now be displayed relative to an app, in addition to be being modal to a window.
1 change: 1 addition & 0 deletions changes/2669.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The APIs on ``Window`` for displaying dialogs (``info_dialog()``, ``question_dialog()``, etc) have been deprecated. They can be replaced with creating an instance of a ``Dialog`` class (e.g., ``InfoDialog``), and passing that instance to ``window.dialog()``.
Loading
Loading