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

Modify dialogs to allow for async usage. #1464

Merged
merged 23 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
02ea81e
Add async Android text dialogs
paulproteus Jul 26, 2020
619425d
async dialogs in cocoa
Sep 3, 2020
2892306
corrected return types of selection dialogs
Sep 3, 2020
9fa7550
improved dialog return type documentation
Sep 3, 2020
d4af8b3
Merge branch 'master' into cocoa-async-dialogs
freakboy3742 Apr 12, 2022
0ae6fdc
Modify dialogs example to be async.
freakboy3742 Apr 12, 2022
e290987
Merge branch 'master' into async-dialogs
freakboy3742 Apr 12, 2022
f4d7970
Merge branch 'add-android-async-text-dialogs' into async-dialogs
freakboy3742 Apr 12, 2022
87d503e
Add stack trace dialog, plus other output cleanups.
freakboy3742 Apr 12, 2022
2170407
Modify Cocoa dialogs to be async-with-fallback.
freakboy3742 Apr 12, 2022
f20c51b
Modify Android dialogs to be async-with-fallback.
freakboy3742 Apr 12, 2022
ad1bfc9
Cleaned up some flake8 errors.
freakboy3742 Apr 12, 2022
c8e224b
Corrected handling of on_result handlers.
freakboy3742 Apr 12, 2022
803a104
Simplify invocation of dialogs.
freakboy3742 Apr 13, 2022
8391d73
Updated Android to use new dialog creation path.
freakboy3742 Apr 13, 2022
a433cfd
Modify GTK dialogs to be async and new layout.
freakboy3742 Apr 13, 2022
329280a
Modify Winforms dialogs to be async and new layout.
freakboy3742 Apr 13, 2022
f647549
Cleaned up flake8 issues.
freakboy3742 Apr 13, 2022
0c8236c
Updated web backend to use new dialog layout.
freakboy3742 Apr 13, 2022
208546a
Factored out some common path conversion.
freakboy3742 Apr 13, 2022
e65f030
Migrated iOS to async and new dialog format.
freakboy3742 Apr 13, 2022
1f1d092
Tweaked expected test results.
freakboy3742 Apr 13, 2022
ff89491
More test tweaks.
freakboy3742 Apr 13, 2022
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
96 changes: 56 additions & 40 deletions examples/dialogs/dialogs/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import toga
import traceback
from pathlib import Path

import toga
from toga.constants import COLUMN
from toga.style import Pack

Expand All @@ -9,127 +11,139 @@ class ExampledialogsApp(toga.App):
def do_clear(self, widget, **kwargs):
self.label.text = "Ready."

def action_info_dialog(self, widget):
self.main_window.info_dialog('Toga', 'THIS! IS! TOGA!!')
async def action_info_dialog(self, widget):
await self.main_window.info_dialog('Toga', 'THIS! IS! TOGA!!')
self.label.text = 'Information was provided.'

def action_question_dialog(self, widget):
if self.main_window.question_dialog('Toga', 'Is this cool or what?'):
async def action_question_dialog(self, widget):
if await self.main_window.question_dialog('Toga', 'Is this cool or what?'):
self.label.text = 'User said yes!'
self.main_window.info_dialog('Happiness', 'I know, right! :-)')
await self.main_window.info_dialog('Happiness', 'I know, right! :-)')
else:
self.label.text = 'User says no...'
self.main_window.info_dialog('Shucks...', "Well aren't you a spoilsport... :-(")
await self.main_window.info_dialog('Shucks...', "Well aren't you a spoilsport... :-(")

def action_confirm_dialog(self, widget):
if self.main_window.question_dialog('Toga', 'Are you sure you want to?'):
async def action_confirm_dialog(self, widget):
if await self.main_window.question_dialog('Toga', 'Are you sure you want to?'):
self.label.text = 'Lets do it!'
else:
self.label.text = "Left it as it was."

def action_error_dialog(self, widget):
self.main_window.error_dialog('Toga', "Well that didn't work... or did it?")
async def action_error_dialog(self, widget):
await self.main_window.error_dialog('Toga', "Well that didn't work... or did it?")
self.label.text = 'Oh noes...'

def action_open_file_dialog(self, widget):
async def action_stack_trace(self, widget):
await self.main_window.stack_trace_dialog(
'Toga',
"Here's where you were when it went bad:",
''.join(traceback.format_stack())
)
self.label.text = 'Stack traced...'

async def action_open_file_dialog(self, widget):
try:
fname = self.main_window.open_file_dialog(
fname = await self.main_window.open_file_dialog(
title="Open file with Toga",
multiselect=False
)
if fname is not None:
self.label.text = "File to open:" + fname
self.label.text = f"File to open: {fname}"
else:
self.label.text = "No file selected!"
except ValueError:
self.label.text = "Open file dialog was canceled"

def action_open_file_filtered_dialog(self, widget):
async def action_open_file_filtered_dialog(self, widget):
try:
fname = self.main_window.open_file_dialog(
fname = await self.main_window.open_file_dialog(
title="Open file with Toga",
multiselect=False,
file_types=['doc', 'txt'],
)
if fname is not None:
self.label.text = "File to open:" + fname
self.label.text = f"File to open: {fname}"
else:
self.label.text = "No file selected!"
except ValueError:
self.label.text = "Open file dialog was canceled"

def action_open_file_dialog_multi(self, widget):
async def action_open_file_dialog_multi(self, widget):
try:
filenames = self.main_window.open_file_dialog(
filenames = await self.main_window.open_file_dialog(
title="Open file with Toga",
multiselect=True
)
if filenames is not None:
msg = "Files to open: {}".format(', '.join(filenames))
msg = f"Files to open: {', '.join(str(f) for f in filenames)}"
self.label.text = msg
else:
self.label.text = "No files selected!"

except ValueError:
self.label.text = "Open file dialog was canceled"

def action_open_file_dialog_with_inital_folder(self, widget):
async def action_open_file_dialog_with_inital_folder(self, widget):
try:
fname = self.main_window.open_file_dialog(
fname = await self.main_window.open_file_dialog(
title="Open file with Toga in home folder",
initial_directory=Path.home(),
multiselect=False
)
if fname is not None:
self.label.text = "File to open:" + fname
self.label.text = f"File to open: {fname}"
else:
self.label.text = "No file selected!"
except ValueError:
self.label.text = "Open file dialog was canceled"

def action_select_folder_dialog(self, widget):
async def action_select_folder_dialog(self, widget):
try:
path_names = self.main_window.select_folder_dialog(
path_name = await self.main_window.select_folder_dialog(
title="Select folder with Toga"
)
if path_names is not None:
self.label.text = "Folder selected:" + ','.join([path for path in path_names])
if path_name is not None:
self.label.text = f"Folder selected: {path_name}"
else:
self.label.text = "No folder selected!"
except ValueError:
self.label.text = "Folder select dialog was canceled"

def action_select_folder_dialog_multi(self, widget):
async def action_select_folder_dialog_multi(self, widget):
try:
path_names = self.main_window.select_folder_dialog(
path_names = await self.main_window.select_folder_dialog(
title="Select multiple folders with Toga",
multiselect=True
)
self.label.text = "Folders selected:" + ','.join([path for path in path_names])
if path_names is not None:
self.label.text = f"Folders selected: {','.join([str(p) for p in path_names])}"
else:
self.label.text = "No fodlers selected!"
except ValueError:
self.label.text = "Folders select dialog was canceled"

def action_select_folder_dialog_with_initial_folder(self, widget):
async def action_select_folder_dialog_with_initial_folder(self, widget):
try:
path_names = self.main_window.select_folder_dialog(
path_name = await self.main_window.select_folder_dialog(
title="Select folder with Toga in current folder",
initial_directory=Path.cwd(),
)
self.label.text = "Folder selected:" + ','.join([path for path in path_names])
self.label.text = f"Folder selected: {path_name}"
except ValueError:
self.label.text = "Folder select dialog was canceled"

def action_save_file_dialog(self, widget):
async def action_save_file_dialog(self, widget):
fname = 'Toga_file.txt'
try:
save_path = self.main_window.save_file_dialog(
save_path = await self.main_window.save_file_dialog(
"Save file with Toga",
suggested_filename=fname
suggested_filename=fname,
)
if save_path is not None:
self.label.text = "File saved with Toga:" + save_path
self.label.text = f"File saved with Toga: {save_path}"
else:
self.label.text = "Save file dialog was canceled"

except ValueError:
self.label.text = "Save file dialog was canceled"

Expand All @@ -148,7 +162,7 @@ async def window_close_handler(self, window):
self.set_window_label_text(len(self.windows) - 2)
return True
else:
window.info_dialog(f'Abort {window.title}!', 'Maybe try that again...')
await window.info_dialog(f'Abort {window.title}!', 'Maybe try that again...')
self.close_attempts.add(window)
return False

Expand Down Expand Up @@ -181,7 +195,7 @@ def action_close_secondary_windows(self, widget):

async def exit_handler(self, app):
# Return True if app should close, and False if it should remain open
if self.main_window.confirm_dialog('Toga', 'Are you sure you want to quit?'):
if await self.main_window.confirm_dialog('Toga', 'Are you sure you want to quit?'):
print(f"Label text was '{self.label.text}' when you quit the app")
return True
else:
Expand Down Expand Up @@ -209,6 +223,7 @@ def startup(self):
btn_question = toga.Button('Question', on_press=self.action_question_dialog, style=btn_style)
btn_confirm = toga.Button('Confirm', on_press=self.action_confirm_dialog, style=btn_style)
btn_error = toga.Button('Error', on_press=self.action_error_dialog, style=btn_style)
btn_stack_trace = toga.Button('Stack Trace', on_press=self.action_stack_trace, style=btn_style)
btn_open = toga.Button('Open File', on_press=self.action_open_file_dialog, style=btn_style)
btn_open_filtered = toga.Button(
'Open File (Filtered)',
Expand All @@ -221,7 +236,7 @@ def startup(self):
style=btn_style
)
btn_open_init_folder = toga.Button(
'Open File In Current Folder',
'Open File In Home Folder',
on_press=self.action_open_file_dialog_with_inital_folder,
)

Expand Down Expand Up @@ -258,6 +273,7 @@ def startup(self):
btn_question,
btn_confirm,
btn_error,
btn_stack_trace,
btn_open,
btn_open_filtered,
btn_open_multi,
Expand Down
121 changes: 102 additions & 19 deletions src/android/toga_android/dialogs.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,118 @@
import asyncio

from .libs.android import R__drawable
from .libs.android.app import AlertDialog__Builder
from .libs.android.content import DialogInterface__OnClickListener


class NoOpListener(DialogInterface__OnClickListener):
def onClick(self, dialog, which):
pass
class OnClickListener(DialogInterface__OnClickListener):
def __init__(self, fn=None, value=None):
super().__init__()
self._fn = fn
self._value = value

def onClick(self, _dialog, _which):
if self._fn:
self._fn(self._value)


class BaseDialog:
def __init__(self):
loop = asyncio.get_event_loop()
self.future = loop.create_future()
# self.future = asyncio.create_future()

def __eq__(self, other):
raise RuntimeError("Can't check dialog result directly; use await or an on_result handler")

def __bool__(self):
raise RuntimeError("Can't check dialog result directly; use await or an on_result handler")

def __await__(self):
return self.future.__await__()


class TextDialog(BaseDialog):
def __init__(self, window, title, message, positive_text, negative_text=None, icon=None, on_result=None):
"""Create Android textual dialog.

- window: Toga Window
- title: Title of dialog
- message: Message of dialog
- positive_text: Button label where clicking it returns True (or None to skip)
- negative_text: Button label where clicking it returns False (or None to skip)
- icon: Integer used as an Android resource ID number for dialog icon (or None to skip)
"""
super().__init__()

builder = AlertDialog__Builder(window.app.native)
builder.setCancelable(False)
builder.setTitle(title)
builder.setMessage(message)
if icon is not None:
builder.setIcon(icon)

def info(window, title, message):
builder = AlertDialog__Builder(window.app.native)
builder.setTitle(title)
builder.setMessage(message)
builder.setPositiveButton("OK", NoOpListener())
builder.show()
if positive_text is not None:
builder.setPositiveButton(
positive_text,
OnClickListener(self.completion_handler, True)
)
if negative_text is not None:
builder.setNegativeButton(
negative_text,
OnClickListener(self.completion_handler, False)
)
builder.show()

def completion_handler(self, return_value: bool) -> None:
if self.on_result:
self.on_result(self, return_value)

def question(window, title, message):
window.platform.not_implemented("dialogs.question()")
self.future.set_result(return_value)


def confirm(window, title, message):
window.platform.not_implemented("dialogs.confirm()")
class InfoDialog(TextDialog):
def __init__(self, window, title, message, on_result=None):
super().__init__(
window=window,
title=title,
message=message,
positive_text="OK",
on_result=on_result,
)


def error(window, title, message):
window.platform.not_implemented("dialogs.error()")
class QuestionDialog(TextDialog):
def __init__(self, window, title, message, on_result=None):
super().__init__(
window=window,
title=title,
message=message,
positive_text="Yes",
negative_text="No",
on_result=on_result,
)


def stack_trace(window, title, message, content, retry=False):
window.platform.not_implemented("dialogs.stack_trace()")
class ConfirmDialog(TextDialog):
def __init__(self, window, title, message, on_result=None):
super().__init__(
window=window,
title=title,
message=message,
positive_text="OK",
negative_text="Cancel",
on_result=on_result,
)


def save_file(window, title, suggested_filename, file_types):
window.platform.not_implemented("dialogs.save_file()")
class ErrorDialog(TextDialog):
def __init__(self, window, title, message, on_result=None):
super().__init__(
window=window,
title=title,
message=message,
positive_text="OK",
icon=R__drawable.ic_dialog_alert,
on_result=on_result,
)
8 changes: 4 additions & 4 deletions src/android/toga_android/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,16 @@ def set_full_screen(self, is_full_screen):
self.interface.factory.not_implemented('Window.set_full_screen()')

def info_dialog(self, title, message):
dialogs.info(self, title, message)
return dialogs.InfoDialog(self, title, message)

def question_dialog(self, title, message):
self.interface.factory.not_implemented('Window.question_dialog()')
return dialogs.QuestionDialog(self, title, message)

def confirm_dialog(self, title, message):
self.interface.factory.not_implemented('Window.confirm_dialog()')
return dialogs.ConfirmDialog(self, title, message)

def error_dialog(self, title, message):
self.interface.factory.not_implemented('Window.error_dialog()')
return dialogs.ErrorDialog(self, title, message)

def stack_trace_dialog(self, title, message, content, retry=False):
self.interface.factory.not_implemented('Window.stack_trace_dialog()')
Expand Down
Loading