From 558d9f12bb190709603234789c0e98f80c91e90e Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 2 Dec 2020 14:09:34 +0100 Subject: [PATCH 01/24] * implemented Intent invocation * implemented file browing for Android * made filebrowser example to test it all --- examples/filebrowser/README.rst | 12 ++++ examples/filebrowser/filebrowser/__init__.py | 9 +++ examples/filebrowser/filebrowser/__main__.py | 4 ++ examples/filebrowser/filebrowser/app.py | 66 +++++++++++++++++++ .../filebrowser/filebrowser/resources/README | 1 + examples/filebrowser/pyproject.toml | 48 ++++++++++++++ examples/filebrowser/test-android.bat | 10 +++ src/android/toga_android/app.py | 15 +++++ src/android/toga_android/window.py | 19 ++++++ 9 files changed, 184 insertions(+) create mode 100644 examples/filebrowser/README.rst create mode 100644 examples/filebrowser/filebrowser/__init__.py create mode 100644 examples/filebrowser/filebrowser/__main__.py create mode 100644 examples/filebrowser/filebrowser/app.py create mode 100644 examples/filebrowser/filebrowser/resources/README create mode 100644 examples/filebrowser/pyproject.toml create mode 100644 examples/filebrowser/test-android.bat diff --git a/examples/filebrowser/README.rst b/examples/filebrowser/README.rst new file mode 100644 index 0000000000..efccba0ac5 --- /dev/null +++ b/examples/filebrowser/README.rst @@ -0,0 +1,12 @@ +Filebrowser Demo +================ + +Test app for the Filebrowser Demo widget. + +Quickstart +~~~~~~~~~~ + +To run this example: + + $ pip install toga + $ python -m filebrowser diff --git a/examples/filebrowser/filebrowser/__init__.py b/examples/filebrowser/filebrowser/__init__.py new file mode 100644 index 0000000000..5de825baaa --- /dev/null +++ b/examples/filebrowser/filebrowser/__init__.py @@ -0,0 +1,9 @@ +# Examples of valid version strings +# __version__ = '1.2.3.dev1' # Development release 1 +# __version__ = '1.2.3a1' # Alpha Release 1 +# __version__ = '1.2.3b1' # Beta Release 1 +# __version__ = '1.2.3rc1' # RC Release 1 +# __version__ = '1.2.3' # Final Release +# __version__ = '1.2.3.post1' # Post Release 1 + +__version__ = '0.0.1' diff --git a/examples/filebrowser/filebrowser/__main__.py b/examples/filebrowser/filebrowser/__main__.py new file mode 100644 index 0000000000..871e6e42cc --- /dev/null +++ b/examples/filebrowser/filebrowser/__main__.py @@ -0,0 +1,4 @@ +from filebrowser.app import main + +if __name__ == '__main__': + main().main_loop() diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py new file mode 100644 index 0000000000..9c3577928f --- /dev/null +++ b/examples/filebrowser/filebrowser/app.py @@ -0,0 +1,66 @@ +# For this example to work under Android, you need a briefcase android template +# which supports onActivityResult in MainActivity.java +# see https://github.com/t-arn/briefcase-android-gradle-template.git branch onActivityResult + + +import toga +from toga.style import Pack +from toga.constants import COLUMN, ROW + + +class ExampleFilebrowserApp(toga.App): + # Button callback functions + def do_stuff(self, widget, **kwargs): + print("Clicked on 'Do stuff'") + selected_uri = self.app.main_window.open_file_dialog("Choose a file") + self.label.text = "You selected: " + str(selected_uri) + + def do_clear(self, widget, **kwargs): + print('Clearing result') + self.label.text = "Ready." + + def startup(self): + # Set up main window + self.main_window = toga.MainWindow(title=self.name) + + # Label to show responses. + self.label = toga.Label('Ready.') + + # Buttons + btn_style = Pack(flex=1) + btn_do_stuff = toga.Button('Do stuff', on_press=self.do_stuff, style=btn_style) + btn_clear = toga.Button('Clear', on_press=self.do_clear, style=btn_style) + btn_box = toga.Box( + children=[ + btn_do_stuff, + btn_clear + ], + style=Pack(direction=ROW) + ) + + # Outermost box + outer_box = toga.Box( + children=[btn_box, self.label], + style=Pack( + flex=1, + direction=COLUMN, + padding=10, + width=500, + height=300 + ) + ) + + # Add the content on the main window + self.main_window.content = outer_box + + # Show the main window + self.main_window.show() + + +def main(): + return ExampleFilebrowserApp('Filebrowser Demo', 'org.beeware.widgets.filebrowser') + + +if __name__ == '__main__': + app = main() + app.main_loop() diff --git a/examples/filebrowser/filebrowser/resources/README b/examples/filebrowser/filebrowser/resources/README new file mode 100644 index 0000000000..246d0829ed --- /dev/null +++ b/examples/filebrowser/filebrowser/resources/README @@ -0,0 +1 @@ +Put any icons or images in this directory. \ No newline at end of file diff --git a/examples/filebrowser/pyproject.toml b/examples/filebrowser/pyproject.toml new file mode 100644 index 0000000000..9a35ed421c --- /dev/null +++ b/examples/filebrowser/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["briefcase"] + +[tool.briefcase] +project_name = "Filebrowser Demo" +bundle = "org.beeware" +version = "0.3.0.dev25" +url = "https://beeware.org" +license = "BSD license" +author = 'Tiberius Yak' +author_email = "tiberius@beeware.org" + +[tool.briefcase.app.filebrowser] +formal_name = "Filebrowser Demo" +description = "A testing app" +sources = ['filebrowser'] +requires = [ + 'c:/Projects/Python/Toga/src/core' +] + + +[tool.briefcase.app.filebrowser.macOS] +requires = [ + 'toga-cocoa', +] + +[tool.briefcase.app.filebrowser.linux] +requires = [ + 'toga-gtk', +] + +[tool.briefcase.app.filebrowser.windows] +requires = [ + # 'toga-winforms', + 'c:/Projects/Python/Toga/src/winforms' +] + +# Mobile deployments +[tool.briefcase.app.filebrowser.iOS] +requires = [ + 'toga-iOS', +] + +[tool.briefcase.app.filebrowser.android] +requires = [ + #'toga-android', + 'c:/Projects/Python/Toga/src/android' +] diff --git a/examples/filebrowser/test-android.bat b/examples/filebrowser/test-android.bat new file mode 100644 index 0000000000..0e4df27a00 --- /dev/null +++ b/examples/filebrowser/test-android.bat @@ -0,0 +1,10 @@ +@echo off +briefcase update android +echo. +if not "%errorlevel%"=="0" goto end + +briefcase build android +echo. +if not "%errorlevel%"=="0" goto end +briefcase run android -d @beePhone +:end diff --git a/src/android/toga_android/app.py b/src/android/toga_android/app.py index 7b456e16e7..4fdf3d0d35 100644 --- a/src/android/toga_android/app.py +++ b/src/android/toga_android/app.py @@ -12,6 +12,9 @@ class MainWindow(Window): class TogaApp(IPythonApp): + last_intent_requestcode = -1 # always increment before using it for invoking new Intents + running_intents = {} # dictionary for currently running Intents + def __init__(self, app): super().__init__() self._interface = app @@ -39,6 +42,12 @@ def onDestroy(self): def onRestart(self): print("Toga app: onRestart") + def onActivityResult(self, requestCode, resultCode, resultData): + print("Toga app: onActivityResult") + result_future = self.running_intents[str(resultCode)] + self.running_intents.pop(str(resultCode)) # remove Intent from the list of running Intents + result_future.set_result({"resultCode": resultCode, "resultData": resultData}) + @property def native(self): # We access `MainActivity.singletonThis` freshly each time, rather than @@ -92,3 +101,9 @@ def set_on_exit(self, value): def add_background_task(self, handler): self.loop.call_soon(wrapped_handler(self, handler), self) + + def invoke_intent(self, intent, result_future): + self.native.last_intent_requestcode += 1 + code = self.native.last_intent_requestcode + self.native.running_intents[str(code)] = result_future + MainActivity.startActivityForResult(intent, code) diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 5dd58700fc..cefed3f950 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -1,5 +1,9 @@ from . import dialogs +import asyncio +from rubicon.java import JavaClass, JavaInterface +Intent = JavaClass("android/content/Intent") +Activity = JavaClass("android/app/Activity") class AndroidViewport: def __init__(self, native): @@ -78,3 +82,18 @@ def stack_trace_dialog(self, title, message, content, retry=False): def save_file_dialog(self, title, suggested_filename, file_types): self.interface.factory.not_implemented('Window.save_file_dialog()') + + async def open_file_dialog(self, title, initial_directory, file_types, multiselect): + print('Invoking Intent ACTION_OPEN_DOCUMENT') + intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.setType("*/*") # allow all file types to be selectable + result_future = asyncio.Future() + self.app.invoke_intent(intent, result_future) + await result_future + selected_uri = "" + result = result_future.result() + if result["resultCode"] == Activity.RESULT_OK: + if result["resultData"] is not None: + selected_uri = result["resultData"].getData() + return selected_uri From 4343df711e6ac46577aa0643c8f23d9d3b05cef2 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 3 Dec 2020 08:52:42 +0100 Subject: [PATCH 02/24] * first working version --- examples/filebrowser/filebrowser/app.py | 4 ++-- examples/filebrowser/test-android.bat | 2 +- src/android/toga_android/app.py | 13 +++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 9c3577928f..1106ccaedd 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -10,9 +10,9 @@ class ExampleFilebrowserApp(toga.App): # Button callback functions - def do_stuff(self, widget, **kwargs): + async def do_stuff(self, widget, **kwargs): print("Clicked on 'Do stuff'") - selected_uri = self.app.main_window.open_file_dialog("Choose a file") + selected_uri = await self.app.main_window.open_file_dialog("Choose a file") self.label.text = "You selected: " + str(selected_uri) def do_clear(self, widget, **kwargs): diff --git a/examples/filebrowser/test-android.bat b/examples/filebrowser/test-android.bat index 0e4df27a00..ba559cd9fe 100644 --- a/examples/filebrowser/test-android.bat +++ b/examples/filebrowser/test-android.bat @@ -1,5 +1,5 @@ @echo off -briefcase update android +briefcase update android -d echo. if not "%errorlevel%"=="0" goto end diff --git a/src/android/toga_android/app.py b/src/android/toga_android/app.py index 4fdf3d0d35..e264d4f7aa 100644 --- a/src/android/toga_android/app.py +++ b/src/android/toga_android/app.py @@ -44,8 +44,8 @@ def onRestart(self): def onActivityResult(self, requestCode, resultCode, resultData): print("Toga app: onActivityResult") - result_future = self.running_intents[str(resultCode)] - self.running_intents.pop(str(resultCode)) # remove Intent from the list of running Intents + result_future = self.running_intents[str(requestCode)] + self.running_intents.pop(str(requestCode)) # remove Intent from the list of running Intents result_future.set_result({"resultCode": resultCode, "resultData": resultData}) @property @@ -103,7 +103,8 @@ def add_background_task(self, handler): self.loop.call_soon(wrapped_handler(self, handler), self) def invoke_intent(self, intent, result_future): - self.native.last_intent_requestcode += 1 - code = self.native.last_intent_requestcode - self.native.running_intents[str(code)] = result_future - MainActivity.startActivityForResult(intent, code) + self._listener.last_intent_requestcode += 1 + code = self._listener.last_intent_requestcode + self._listener.running_intents[str(code)] = result_future + self.native.startActivityForResult(intent, code) + From df001f5a4cd4688e1f9bc7abd7a73cdc1dbddcc2 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 3 Dec 2020 15:36:39 +0100 Subject: [PATCH 03/24] * added multiselect (working) * added generic Intent invocation method app.invoke_intent_for_result * added initial_dir (not working yet) * added extra mime types (not working yet) --- examples/filebrowser/filebrowser/app.py | 37 +++++++++++++------ examples/filebrowser/pyvenv.cmd | 2 + .../{test-android.bat => test-android.cmd} | 0 src/android/toga_android/app.py | 8 +++- src/android/toga_android/window.py | 26 +++++++++---- 5 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 examples/filebrowser/pyvenv.cmd rename examples/filebrowser/{test-android.bat => test-android.cmd} (100%) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 1106ccaedd..1068fd53b9 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -10,29 +10,42 @@ class ExampleFilebrowserApp(toga.App): # Button callback functions - async def do_stuff(self, widget, **kwargs): - print("Clicked on 'Do stuff'") - selected_uri = await self.app.main_window.open_file_dialog("Choose a file") - self.label.text = "You selected: " + str(selected_uri) + async def do_open_file(self, widget, **kwargs): + print("Clicked on 'Open file'") + multiselect = False + if self.multiselect.value == 'True': + multiselect = True + selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, self.file_types.value, multiselect) + self.multiline.value = "You selected: \n" + str(selected_uri) def do_clear(self, widget, **kwargs): print('Clearing result') - self.label.text = "Ready." + self.multiline.value = "Ready." def startup(self): # Set up main window self.main_window = toga.MainWindow(title=self.name) - # Label to show responses. - self.label = toga.Label('Ready.') + flex_style = Pack(flex=1) + + # set options + self.initial_dir = toga.TextInput(placeholder='initial directory', style=flex_style) + self.file_types = toga.TextInput(placeholder='file types', style=flex_style) + self.multiselect = toga.TextInput(placeholder='is multiselect? (True / False)', style=flex_style) + self.folder = toga.TextInput(placeholder='what to select? (file / folder)', style=flex_style) + # Toga.Switch does not seem to work on Android ... + # self.multiselect = toga.Switch('multiselect', is_on=False) + # self.folder = toga.Switch('select folder') + + # Text field to show responses. + self.multiline = toga.MultilineTextInput('Ready.', style=flex_style) # Buttons - btn_style = Pack(flex=1) - btn_do_stuff = toga.Button('Do stuff', on_press=self.do_stuff, style=btn_style) - btn_clear = toga.Button('Clear', on_press=self.do_clear, style=btn_style) + btn_open_file = toga.Button('Open file', on_press=self.do_open_file, style=flex_style) + btn_clear = toga.Button('Clear', on_press=self.do_clear, style=flex_style) btn_box = toga.Box( children=[ - btn_do_stuff, + btn_open_file, btn_clear ], style=Pack(direction=ROW) @@ -40,7 +53,7 @@ def startup(self): # Outermost box outer_box = toga.Box( - children=[btn_box, self.label], + children=[self.initial_dir, self.file_types, self.multiselect, self.folder, btn_box, self.multiline], style=Pack( flex=1, direction=COLUMN, diff --git a/examples/filebrowser/pyvenv.cmd b/examples/filebrowser/pyvenv.cmd new file mode 100644 index 0000000000..a56e56ccbc --- /dev/null +++ b/examples/filebrowser/pyvenv.cmd @@ -0,0 +1,2 @@ +@echo off +cmd /k ..\..\venv\Scripts\activate.bat diff --git a/examples/filebrowser/test-android.bat b/examples/filebrowser/test-android.cmd similarity index 100% rename from examples/filebrowser/test-android.bat rename to examples/filebrowser/test-android.cmd diff --git a/src/android/toga_android/app.py b/src/android/toga_android/app.py index e264d4f7aa..7e1a95233d 100644 --- a/src/android/toga_android/app.py +++ b/src/android/toga_android/app.py @@ -5,6 +5,7 @@ from .libs.activity import IPythonApp, MainActivity from .window import Window +import asyncio # `MainWindow` is defined here in `app.py`, not `window.py`, to mollify the test suite. class MainWindow(Window): @@ -44,6 +45,7 @@ def onRestart(self): def onActivityResult(self, requestCode, resultCode, resultData): print("Toga app: onActivityResult") + print("resultData: "+str(resultData)) result_future = self.running_intents[str(requestCode)] self.running_intents.pop(str(requestCode)) # remove Intent from the list of running Intents result_future.set_result({"resultCode": resultCode, "resultData": resultData}) @@ -102,9 +104,11 @@ def set_on_exit(self, value): def add_background_task(self, handler): self.loop.call_soon(wrapped_handler(self, handler), self) - def invoke_intent(self, intent, result_future): + async def invoke_intent_for_result(self, intent): self._listener.last_intent_requestcode += 1 code = self._listener.last_intent_requestcode + result_future = asyncio.Future() self._listener.running_intents[str(code)] = result_future self.native.startActivityForResult(intent, code) - + await result_future + return result_future.result() diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index cefed3f950..5536149476 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -1,6 +1,5 @@ from . import dialogs -import asyncio from rubicon.java import JavaClass, JavaInterface Intent = JavaClass("android/content/Intent") Activity = JavaClass("android/app/Activity") @@ -83,17 +82,28 @@ def stack_trace_dialog(self, title, message, content, retry=False): def save_file_dialog(self, title, suggested_filename, file_types): self.interface.factory.not_implemented('Window.save_file_dialog()') - async def open_file_dialog(self, title, initial_directory, file_types, multiselect): + async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselect): print('Invoking Intent ACTION_OPEN_DOCUMENT') intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.setType("*/*") # allow all file types to be selectable - result_future = asyncio.Future() - self.app.invoke_intent(intent, result_future) - await result_future - selected_uri = "" - result = result_future.result() + intent.setType("*/*") + if initial_uri is not None and initial_uri != '': + intent.putExtra("android.provider.extra.INITIAL_URI", initial_uri) + if file_mime_types is not None and file_mime_types != '': + intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) + selected_uri = None + result = await self.app.invoke_intent_for_result(intent) if result["resultCode"] == Activity.RESULT_OK: if result["resultData"] is not None: selected_uri = result["resultData"].getData() + if selected_uri is None: + selected_uri = "" + clip_data = result["resultData"].getClipData() + if clip_data is not None: + for i in range (0, clip_data.getItemCount()): + if i > 0: + selected_uri += '\n' + selected_uri += str(clip_data.getItemAt(i).getUri()) + print (selected_uri) return selected_uri From 7ecb67d1f561f03679051abd86d22de2f10d9f4a Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 3 Dec 2020 17:01:22 +0100 Subject: [PATCH 04/24] * fixed: multiselect response must always be a List --- examples/filebrowser/filebrowser/app.py | 2 +- src/android/toga_android/window.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 1068fd53b9..3ca570486f 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -24,7 +24,7 @@ def do_clear(self, widget, **kwargs): def startup(self): # Set up main window - self.main_window = toga.MainWindow(title=self.name) + self.main_window = toga.MainWindow(title=self.name, size=(400, 700)) flex_style = Pack(flex=1) diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 5536149476..ebfe5dd15a 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -97,13 +97,14 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec if result["resultCode"] == Activity.RESULT_OK: if result["resultData"] is not None: selected_uri = result["resultData"].getData() - if selected_uri is None: - selected_uri = "" - clip_data = result["resultData"].getClipData() - if clip_data is not None: - for i in range (0, clip_data.getItemCount()): - if i > 0: - selected_uri += '\n' - selected_uri += str(clip_data.getItemAt(i).getUri()) + if multiselect is True: + if selected_uri is None: + selected_uri = [] + clip_data = result["resultData"].getClipData() + if clip_data is not None: + for i in range (0, clip_data.getItemCount()): + selected_uri.append(str(clip_data.getItemAt(i).getUri())) + else: + selected_uri = [str(selected_uri)] print (selected_uri) return selected_uri From 5d8658fc645f03997c6428d01ddbdbc913cd7ca0 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 3 Dec 2020 19:32:43 +0100 Subject: [PATCH 05/24] * fixed INITIAL_URI --- src/android/toga_android/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index ebfe5dd15a..3f2c7288d3 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -3,6 +3,7 @@ from rubicon.java import JavaClass, JavaInterface Intent = JavaClass("android/content/Intent") Activity = JavaClass("android/app/Activity") +Uri = JavaClass("android/net/Uri") class AndroidViewport: def __init__(self, native): @@ -88,7 +89,7 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("*/*") if initial_uri is not None and initial_uri != '': - intent.putExtra("android.provider.extra.INITIAL_URI", initial_uri) + intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(initial_uri)) if file_mime_types is not None and file_mime_types != '': intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) From f9a1ffd25a90bea99dbb2d8e803da68719757eea Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 4 Dec 2020 12:40:06 +0100 Subject: [PATCH 06/24] * return ValueError when nothing has been selected * added documentation * disabled file types because they create a rubicon error --- examples/filebrowser/filebrowser/app.py | 8 ++++++-- src/android/toga_android/app.py | 16 +++++++++++++++- src/android/toga_android/window.py | 23 ++++++++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 3ca570486f..9b54c2f49b 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -13,9 +13,13 @@ class ExampleFilebrowserApp(toga.App): async def do_open_file(self, widget, **kwargs): print("Clicked on 'Open file'") multiselect = False + mimetypes = str(self.file_types.value).split(' ') if self.multiselect.value == 'True': multiselect = True - selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, self.file_types.value, multiselect) + try: + selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, mimetypes, multiselect) + except ValueError as e: + selected_uri = str(e) self.multiline.value = "You selected: \n" + str(selected_uri) def do_clear(self, widget, **kwargs): @@ -30,7 +34,7 @@ def startup(self): # set options self.initial_dir = toga.TextInput(placeholder='initial directory', style=flex_style) - self.file_types = toga.TextInput(placeholder='file types', style=flex_style) + self.file_types = toga.TextInput(placeholder='MIME types (blank separated)', style=flex_style) self.multiselect = toga.TextInput(placeholder='is multiselect? (True / False)', style=flex_style) self.folder = toga.TextInput(placeholder='what to select? (file / folder)', style=flex_style) # Toga.Switch does not seem to work on Android ... diff --git a/src/android/toga_android/app.py b/src/android/toga_android/app.py index 7e1a95233d..f1e8776383 100644 --- a/src/android/toga_android/app.py +++ b/src/android/toga_android/app.py @@ -44,8 +44,15 @@ def onRestart(self): print("Toga app: onRestart") def onActivityResult(self, requestCode, resultCode, resultData): + """ + Callback method, called from MainActivity when an Intent ends + + :param int requestCode: The integer request code originally supplied to startActivityForResult(), + allowing you to identify who this result came from. + :param int resultCode: The integer result code returned by the child activity through its setResult(). + :param Intent resultData: An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + """ print("Toga app: onActivityResult") - print("resultData: "+str(resultData)) result_future = self.running_intents[str(requestCode)] self.running_intents.pop(str(requestCode)) # remove Intent from the list of running Intents result_future.set_result({"resultCode": resultCode, "resultData": resultData}) @@ -105,6 +112,13 @@ def add_background_task(self, handler): self.loop.call_soon(wrapped_handler(self, handler), self) async def invoke_intent_for_result(self, intent): + """ + Calls an Intent and waits for its result + + :param Intent intent: The Intent to call + :returns: A Dictionary containing "resultCode" (int) and "resultData" (Intent or None) + :rtype: dict + """ self._listener.last_intent_requestcode += 1 code = self._listener.last_intent_requestcode result_future = asyncio.Future() diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 3f2c7288d3..71d5746565 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -84,14 +84,30 @@ def save_file_dialog(self, title, suggested_filename, file_types): self.interface.factory.not_implemented('Window.save_file_dialog()') async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselect): + """ + Opens a file chooser dialog and returns the chosen file as content URI. + Raises a ValueError when nothing has been selected + + :param str title: The title is ignored on Android + :param initial_uri: The initial location shown in the file chooser. Must be a content URI, e.g. + 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + :type initial_uri: str or None + :param file_mime_types: The file types allowed to select. Must be MIME types, e.g. ['application/pdf']. + Currently ignored to avoid error in rubicon + :type file_mime_types: list[str] or None + :param bool multiselect: If True, then several files can be selected + :returns: The content URI of the chosen file or a list of content URIs when multiselect=True. + :rtype: str or list[str] + """ print('Invoking Intent ACTION_OPEN_DOCUMENT') intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("*/*") if initial_uri is not None and initial_uri != '': intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(initial_uri)) - if file_mime_types is not None and file_mime_types != '': - intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) + if file_mime_types is not None and file_mime_types != ['']: + # intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) # currently creates an error in rubicon + pass intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) selected_uri = None result = await self.app.invoke_intent_for_result(intent) @@ -107,5 +123,6 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec selected_uri.append(str(clip_data.getItemAt(i).getUri())) else: selected_uri = [str(selected_uri)] - print (selected_uri) + if selected_uri is None: + raise ValueError("No filename provided in the open file dialog") return selected_uri From 2f33e223dad3ec5866942fe9beb929ea2e5ac7ab Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Dec 2020 07:08:06 +0100 Subject: [PATCH 07/24] * implemented select_folder_dialog --- examples/filebrowser/filebrowser/app.py | 16 ++++++++- src/android/toga_android/window.py | 44 +++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 9b54c2f49b..bb96461414 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -20,6 +20,18 @@ async def do_open_file(self, widget, **kwargs): selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, mimetypes, multiselect) except ValueError as e: selected_uri = str(e) + print (str(selected_uri)) + self.multiline.value = "You selected: \n" + str(selected_uri) + + async def do_open_folder(self, widget, **kwargs): + print("Clicked on 'Open folder'") + multiselect = False + if self.multiselect.value == 'True': + multiselect = True + try: + selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", self.initial_dir.value, multiselect) + except ValueError as e: + selected_uri = str(e) self.multiline.value = "You selected: \n" + str(selected_uri) def do_clear(self, widget, **kwargs): @@ -29,7 +41,7 @@ def do_clear(self, widget, **kwargs): def startup(self): # Set up main window self.main_window = toga.MainWindow(title=self.name, size=(400, 700)) - + self.app.main_window.size = (400, 700) flex_style = Pack(flex=1) # set options @@ -46,10 +58,12 @@ def startup(self): # Buttons btn_open_file = toga.Button('Open file', on_press=self.do_open_file, style=flex_style) + btn_open_folder = toga.Button('Open folder', on_press=self.do_open_folder, style=flex_style) btn_clear = toga.Button('Clear', on_press=self.do_clear, style=flex_style) btn_box = toga.Box( children=[ btn_open_file, + btn_open_folder, btn_clear ], style=Pack(direction=ROW) diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 71d5746565..78ec56721d 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -92,8 +92,9 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec :param initial_uri: The initial location shown in the file chooser. Must be a content URI, e.g. 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' :type initial_uri: str or None - :param file_mime_types: The file types allowed to select. Must be MIME types, e.g. ['application/pdf']. - Currently ignored to avoid error in rubicon + :param file_mime_types: The file types allowed to select. Must be MIME types, e.g. + ['application/pdf','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']. + Currently ignored to avoid error in rubicon :type file_mime_types: list[str] or None :param bool multiselect: If True, then several files can be selected :returns: The content URI of the chosen file or a list of content URIs when multiselect=True. @@ -106,7 +107,8 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec if initial_uri is not None and initial_uri != '': intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(initial_uri)) if file_mime_types is not None and file_mime_types != ['']: - # intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) # currently creates an error in rubicon + # Commented out because rubicon currently does not support arrays and nothing else works with this Intent + # intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) pass intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) selected_uri = None @@ -126,3 +128,39 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec if selected_uri is None: raise ValueError("No filename provided in the open file dialog") return selected_uri + + async def select_folder_dialog(self, title, initial_uri=None, multiselect=False): + """ + Opens a folder chooser dialog and returns the chosen folder as content URI. + Raises a ValueError when nothing has been selected + + :param str title: The title is ignored on Android + :param initial_uri: The initial location shown in the file chooser. Must be a content URI, e.g. + 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + :type initial_uri: str or None + :param bool multiselect: If True, then several files can be selected + :returns: The content URI of the chosen folder or a list of content URIs when multiselect=True. + :rtype: str or list[str] + """ + print('Invoking Intent ACTION_OPEN_DOCUMENT_TREE') + intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + if initial_uri is not None and initial_uri != '': + intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(initial_uri)) + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) + selected_uri = None + result = await self.app.invoke_intent_for_result(intent) + if result["resultCode"] == Activity.RESULT_OK: + if result["resultData"] is not None: + selected_uri = result["resultData"].getData() + if multiselect is True: + if selected_uri is None: + selected_uri = [] + clip_data = result["resultData"].getClipData() + if clip_data is not None: + for i in range (0, clip_data.getItemCount()): + selected_uri.append(str(clip_data.getItemAt(i).getUri())) + else: + selected_uri = [str(selected_uri)] + if selected_uri is None: + raise ValueError("No folder provided in the open folder dialog") + return selected_uri From b66cdccd3ded3c64afbd86c09409ab0e0a47a3ce Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Dec 2020 08:00:04 +0100 Subject: [PATCH 08/24] * added to example how to use the OpenIntent Filemanager --- examples/filebrowser/filebrowser/app.py | 41 +++++++++++++++++++------ examples/filebrowser/pyproject.toml | 4 +-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index bb96461414..7b63136adb 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -6,7 +6,9 @@ import toga from toga.style import Pack from toga.constants import COLUMN, ROW - +from rubicon.java import JavaClass, JavaInterface +Intent = JavaClass("android/content/Intent") +Activity = JavaClass("android/app/Activity") class ExampleFilebrowserApp(toga.App): # Button callback functions @@ -17,7 +19,18 @@ async def do_open_file(self, widget, **kwargs): if self.multiselect.value == 'True': multiselect = True try: - selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, mimetypes, multiselect) + selected_uri = '' + if self.use_oifm.value != 'True': + selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, mimetypes, multiselect) + else: + intent = Intent("org.openintents.action.PICK_FILE") + intent.putExtra("org.openintents.extra.TITLE", "Choose a file") + result = await self.app._impl.invoke_intent_for_result(intent) + print(str(result)) + if result["resultData"] is not None: + selected_uri = result["resultData"].getData() + else: + selected_uri = 'No file selected, ResultCode was ' + str(result["resultCode"]) + ")" except ValueError as e: selected_uri = str(e) print (str(selected_uri)) @@ -29,7 +42,18 @@ async def do_open_folder(self, widget, **kwargs): if self.multiselect.value == 'True': multiselect = True try: - selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", self.initial_dir.value, multiselect) + selected_uri = '' + if self.use_oifm.value != 'True': + selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", self.initial_dir.value, multiselect) + else: + intent = Intent("org.openintents.action.PICK_DIRECTORY") + intent.putExtra("org.openintents.extra.TITLE", "Choose a folder") + result = await self.app._impl.invoke_intent_for_result(intent) + print(str(result)) + if result["resultData"] is not None: + selected_uri = result["resultData"].getData() + else: + selected_uri = 'No folder selected, ResultCode was ' + str(result["resultCode"]) + ")" except ValueError as e: selected_uri = str(e) self.multiline.value = "You selected: \n" + str(selected_uri) @@ -41,20 +65,19 @@ def do_clear(self, widget, **kwargs): def startup(self): # Set up main window self.main_window = toga.MainWindow(title=self.name, size=(400, 700)) - self.app.main_window.size = (400, 700) flex_style = Pack(flex=1) # set options self.initial_dir = toga.TextInput(placeholder='initial directory', style=flex_style) self.file_types = toga.TextInput(placeholder='MIME types (blank separated)', style=flex_style) self.multiselect = toga.TextInput(placeholder='is multiselect? (True / False)', style=flex_style) - self.folder = toga.TextInput(placeholder='what to select? (file / folder)', style=flex_style) + self.use_oifm = toga.TextInput(placeholder='Use OI Filemanager? (True / False)', style=flex_style) # Toga.Switch does not seem to work on Android ... # self.multiselect = toga.Switch('multiselect', is_on=False) - # self.folder = toga.Switch('select folder') + # self.use_oifm = toga.Switch('Use OI Filemanager') # Text field to show responses. - self.multiline = toga.MultilineTextInput('Ready.', style=flex_style) + self.multiline = toga.MultilineTextInput('Ready.', style=(Pack(height=200))) # Buttons btn_open_file = toga.Button('Open file', on_press=self.do_open_file, style=flex_style) @@ -71,7 +94,7 @@ def startup(self): # Outermost box outer_box = toga.Box( - children=[self.initial_dir, self.file_types, self.multiselect, self.folder, btn_box, self.multiline], + children=[self.initial_dir, self.file_types, self.multiselect, self.use_oifm, btn_box, self.multiline], style=Pack( flex=1, direction=COLUMN, @@ -89,7 +112,7 @@ def startup(self): def main(): - return ExampleFilebrowserApp('Filebrowser Demo', 'org.beeware.widgets.filebrowser') + return ExampleFilebrowserApp('Android Filebrowser Demo', 'org.beeware.widgets.filebrowser') if __name__ == '__main__': diff --git a/examples/filebrowser/pyproject.toml b/examples/filebrowser/pyproject.toml index 9a35ed421c..5b87ed8d93 100644 --- a/examples/filebrowser/pyproject.toml +++ b/examples/filebrowser/pyproject.toml @@ -2,7 +2,7 @@ requires = ["briefcase"] [tool.briefcase] -project_name = "Filebrowser Demo" +project_name = "Android Filebrowser Demo" bundle = "org.beeware" version = "0.3.0.dev25" url = "https://beeware.org" @@ -11,7 +11,7 @@ author = 'Tiberius Yak' author_email = "tiberius@beeware.org" [tool.briefcase.app.filebrowser] -formal_name = "Filebrowser Demo" +formal_name = "Android Filebrowser Demo" description = "A testing app" sources = ['filebrowser'] requires = [ From 62eeca62c4523a4c35e110873cfc5406a34a4a09 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Dec 2020 08:03:45 +0100 Subject: [PATCH 09/24] * updated README --- examples/filebrowser/README.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/filebrowser/README.rst b/examples/filebrowser/README.rst index efccba0ac5..db39a0d4f5 100644 --- a/examples/filebrowser/README.rst +++ b/examples/filebrowser/README.rst @@ -1,11 +1,15 @@ -Filebrowser Demo -================ +Android Filebrowser Demo +======================== -Test app for the Filebrowser Demo widget. +Test app for the Android native file / folder chooser. Quickstart ~~~~~~~~~~ +For this example to work under Android, you need a briefcase android template +which supports onActivityResult in MainActivity.java +see https://github.com/t-arn/briefcase-android-gradle-template.git branch onActivityResult + To run this example: $ pip install toga From 26c46dc220d897c436ee27656dbf0ee0ef132bde Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Dec 2020 09:12:18 +0100 Subject: [PATCH 10/24] * fixed pre-test check issues --- examples/filebrowser/filebrowser/app.py | 11 +++++++---- src/android/toga_android/app.py | 4 +++- src/android/toga_android/window.py | 7 ++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 7b63136adb..15d4c39034 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -6,10 +6,11 @@ import toga from toga.style import Pack from toga.constants import COLUMN, ROW -from rubicon.java import JavaClass, JavaInterface +from rubicon.java import JavaClass Intent = JavaClass("android/content/Intent") Activity = JavaClass("android/app/Activity") + class ExampleFilebrowserApp(toga.App): # Button callback functions async def do_open_file(self, widget, **kwargs): @@ -21,7 +22,8 @@ async def do_open_file(self, widget, **kwargs): try: selected_uri = '' if self.use_oifm.value != 'True': - selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, mimetypes, multiselect) + selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, + mimetypes, multiselect) else: intent = Intent("org.openintents.action.PICK_FILE") intent.putExtra("org.openintents.extra.TITLE", "Choose a file") @@ -33,7 +35,7 @@ async def do_open_file(self, widget, **kwargs): selected_uri = 'No file selected, ResultCode was ' + str(result["resultCode"]) + ")" except ValueError as e: selected_uri = str(e) - print (str(selected_uri)) + print(str(selected_uri)) self.multiline.value = "You selected: \n" + str(selected_uri) async def do_open_folder(self, widget, **kwargs): @@ -44,7 +46,8 @@ async def do_open_folder(self, widget, **kwargs): try: selected_uri = '' if self.use_oifm.value != 'True': - selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", self.initial_dir.value, multiselect) + selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", + self.initial_dir.value, multiselect) else: intent = Intent("org.openintents.action.PICK_DIRECTORY") intent.putExtra("org.openintents.extra.TITLE", "Choose a folder") diff --git a/src/android/toga_android/app.py b/src/android/toga_android/app.py index f1e8776383..cf258e3995 100644 --- a/src/android/toga_android/app.py +++ b/src/android/toga_android/app.py @@ -7,6 +7,7 @@ import asyncio + # `MainWindow` is defined here in `app.py`, not `window.py`, to mollify the test suite. class MainWindow(Window): pass @@ -50,7 +51,8 @@ def onActivityResult(self, requestCode, resultCode, resultData): :param int requestCode: The integer request code originally supplied to startActivityForResult(), allowing you to identify who this result came from. :param int resultCode: The integer result code returned by the child activity through its setResult(). - :param Intent resultData: An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + :param Intent resultData: An Intent, which can return result data to the caller (various data can be attached + to Intent "extras"). """ print("Toga app: onActivityResult") result_future = self.running_intents[str(requestCode)] diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 78ec56721d..1bf411075a 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -1,10 +1,11 @@ from . import dialogs -from rubicon.java import JavaClass, JavaInterface +from rubicon.java import JavaClass Intent = JavaClass("android/content/Intent") Activity = JavaClass("android/app/Activity") Uri = JavaClass("android/net/Uri") + class AndroidViewport: def __init__(self, native): self.native = native @@ -121,7 +122,7 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec selected_uri = [] clip_data = result["resultData"].getClipData() if clip_data is not None: - for i in range (0, clip_data.getItemCount()): + for i in range(0, clip_data.getItemCount()): selected_uri.append(str(clip_data.getItemAt(i).getUri())) else: selected_uri = [str(selected_uri)] @@ -157,7 +158,7 @@ async def select_folder_dialog(self, title, initial_uri=None, multiselect=False) selected_uri = [] clip_data = result["resultData"].getClipData() if clip_data is not None: - for i in range (0, clip_data.getItemCount()): + for i in range(0, clip_data.getItemCount()): selected_uri.append(str(clip_data.getItemAt(i).getUri())) else: selected_uri = [str(selected_uri)] From 85d4f4c21575056141189bd7b12a63179507e11e Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Dec 2020 09:15:44 +0100 Subject: [PATCH 11/24] * fixed pre-test check issues --- examples/filebrowser/filebrowser/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 15d4c39034..0d62cfc2fe 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -7,6 +7,7 @@ from toga.style import Pack from toga.constants import COLUMN, ROW from rubicon.java import JavaClass + Intent = JavaClass("android/content/Intent") Activity = JavaClass("android/app/Activity") @@ -47,7 +48,7 @@ async def do_open_folder(self, widget, **kwargs): selected_uri = '' if self.use_oifm.value != 'True': selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", - self.initial_dir.value, multiselect) + self.initial_dir.value, multiselect) else: intent = Intent("org.openintents.action.PICK_DIRECTORY") intent.putExtra("org.openintents.extra.TITLE", "Choose a folder") From ed40eaa0c8b7e75d74999c598f69781ca5b21cc7 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Dec 2020 09:22:02 +0100 Subject: [PATCH 12/24] Removed unneeded Windows scripts --- examples/filebrowser/pyvenv.cmd | 2 -- examples/filebrowser/test-android.cmd | 10 ---------- 2 files changed, 12 deletions(-) delete mode 100644 examples/filebrowser/pyvenv.cmd delete mode 100644 examples/filebrowser/test-android.cmd diff --git a/examples/filebrowser/pyvenv.cmd b/examples/filebrowser/pyvenv.cmd deleted file mode 100644 index a56e56ccbc..0000000000 --- a/examples/filebrowser/pyvenv.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -cmd /k ..\..\venv\Scripts\activate.bat diff --git a/examples/filebrowser/test-android.cmd b/examples/filebrowser/test-android.cmd deleted file mode 100644 index ba559cd9fe..0000000000 --- a/examples/filebrowser/test-android.cmd +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -briefcase update android -d -echo. -if not "%errorlevel%"=="0" goto end - -briefcase build android -echo. -if not "%errorlevel%"=="0" goto end -briefcase run android -d @beePhone -:end From bc61b056017d586ca6372efde34c84b0841f6eae Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 9 Dec 2020 08:06:10 +0100 Subject: [PATCH 13/24] * addressed all issues raised by reviewers --- examples/filebrowser/filebrowser/app.py | 1 + src/android/toga_android/app.py | 7 +++---- src/android/toga_android/libs/android.py | 5 +++++ src/android/toga_android/window.py | 19 +++++++++---------- src/core/toga/window.py | 15 ++++++++++----- 5 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 src/android/toga_android/libs/android.py diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py index 0d62cfc2fe..933d7119f8 100644 --- a/examples/filebrowser/filebrowser/app.py +++ b/examples/filebrowser/filebrowser/app.py @@ -22,6 +22,7 @@ async def do_open_file(self, widget, **kwargs): multiselect = True try: selected_uri = '' + print(mimetypes) if self.use_oifm.value != 'True': selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, mimetypes, multiselect) diff --git a/src/android/toga_android/app.py b/src/android/toga_android/app.py index cf258e3995..fae17442a0 100644 --- a/src/android/toga_android/app.py +++ b/src/android/toga_android/app.py @@ -54,9 +54,8 @@ def onActivityResult(self, requestCode, resultCode, resultData): :param Intent resultData: An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). """ - print("Toga app: onActivityResult") - result_future = self.running_intents[str(requestCode)] - self.running_intents.pop(str(requestCode)) # remove Intent from the list of running Intents + print("Toga app: onActivityResult, requestCode={0}, resultData={1}".format(requestCode, resultData)) + result_future = self.running_intents.pop(requestCode) # remove Intent from the list of running Intents result_future.set_result({"resultCode": resultCode, "resultData": resultData}) @property @@ -124,7 +123,7 @@ async def invoke_intent_for_result(self, intent): self._listener.last_intent_requestcode += 1 code = self._listener.last_intent_requestcode result_future = asyncio.Future() - self._listener.running_intents[str(code)] = result_future + self._listener.running_intents[code] = result_future self.native.startActivityForResult(intent, code) await result_future return result_future.result() diff --git a/src/android/toga_android/libs/android.py b/src/android/toga_android/libs/android.py new file mode 100644 index 0000000000..f9067a40f1 --- /dev/null +++ b/src/android/toga_android/libs/android.py @@ -0,0 +1,5 @@ +from rubicon.java import JavaClass + +Activity = JavaClass("android/app/Activity") +Intent = JavaClass("android/content/Intent") +Uri = JavaClass("android/net/Uri") diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 1bf411075a..83287220ba 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -1,10 +1,5 @@ from . import dialogs - -from rubicon.java import JavaClass -Intent = JavaClass("android/content/Intent") -Activity = JavaClass("android/app/Activity") -Uri = JavaClass("android/net/Uri") - +from .libs.android import Activity, Intent, Uri class AndroidViewport: def __init__(self, native): @@ -109,19 +104,23 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(initial_uri)) if file_mime_types is not None and file_mime_types != ['']: # Commented out because rubicon currently does not support arrays and nothing else works with this Intent + # see https://github.com/beeware/rubicon-java/pull/53 # intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) - pass + self.interface.factory.not_implemented( + 'Window.open_file_dialog() on Android currently does not support the file_type parameter') intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) selected_uri = None result = await self.app.invoke_intent_for_result(intent) if result["resultCode"] == Activity.RESULT_OK: if result["resultData"] is not None: selected_uri = result["resultData"].getData() - if multiselect is True: + if multiselect: if selected_uri is None: + # when the user selects more than 1 file, getData() will be None. Instead, getClipData() will + # contain the list of chosen files selected_uri = [] clip_data = result["resultData"].getClipData() - if clip_data is not None: + if clip_data is not None: # just to be sure there will never be a null reference exception... for i in range(0, clip_data.getItemCount()): selected_uri.append(str(clip_data.getItemAt(i).getUri())) else: @@ -140,7 +139,7 @@ async def select_folder_dialog(self, title, initial_uri=None, multiselect=False) 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' :type initial_uri: str or None :param bool multiselect: If True, then several files can be selected - :returns: The content URI of the chosen folder or a list of content URIs when multiselect=True. + :returns: The content tree URI of the chosen folder or a list of content URIs when multiselect=True. :rtype: str or list[str] """ print('Invoking Intent ACTION_OPEN_DOCUMENT_TREE') diff --git a/src/core/toga/window.py b/src/core/toga/window.py index 2f67154835..b5a411de78 100644 --- a/src/core/toga/window.py +++ b/src/core/toga/window.py @@ -272,12 +272,15 @@ def open_file_dialog(self, title, initial_directory=None, file_types=None, multi If no path is returned (eg. dialog is canceled), a ValueError is raised. Args: title (str): The title of the dialog window. - initial_directory(str): Initial folder displayed in the dialog. - file_types: A list of strings with the allowed file extensions. + initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, + e.g. 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + file_types: A list of strings with the allowed file extensions. On Android, these must be MIME types, + e.g. ['application/pdf','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']. multiselect: Value showing whether a user can select multiple files. Returns: - The absolute path(str) to the selected file or a list(str) if multiselect + The absolute path(str) to the selected file or a list(str) if multiselect. On Android, you will get back + content URIs. """ return self._impl.open_file_dialog(title, initial_directory, file_types, multiselect) @@ -287,10 +290,12 @@ def select_folder_dialog(self, title, initial_directory=None, multiselect=False) If no path is returned (eg. dialog is canceled), a ValueError is raised. Args: title (str): The title of the dialog window. - initial_directory(str): Initial folder displayed in the dialog. + initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, + e.g. 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' multiselect (bool): Value showing whether a user can select multiple files. Returns: - The absolute path(str) to the selected file or None. + The absolute path(str) to the selected file or None. On Android, you will get back + content tree URIs. """ return self._impl.select_folder_dialog(title, initial_directory, multiselect) From 842e2a1131b84061b769caa6926ab7b7ea480fa1 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 9 Dec 2020 08:19:18 +0100 Subject: [PATCH 14/24] * updated method documentation --- src/core/toga/window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/toga/window.py b/src/core/toga/window.py index b5a411de78..2c70803021 100644 --- a/src/core/toga/window.py +++ b/src/core/toga/window.py @@ -271,7 +271,7 @@ def open_file_dialog(self, title, initial_directory=None, file_types=None, multi It is possible to set the initial folder and only show files with specified file extensions. If no path is returned (eg. dialog is canceled), a ValueError is raised. Args: - title (str): The title of the dialog window. + title (str): The title of the dialog window (ignored on Android) initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, e.g. 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' file_types: A list of strings with the allowed file extensions. On Android, these must be MIME types, @@ -289,10 +289,10 @@ def select_folder_dialog(self, title, initial_directory=None, multiselect=False) It is possible to set the initial folder. If no path is returned (eg. dialog is canceled), a ValueError is raised. Args: - title (str): The title of the dialog window. + title (str): The title of the dialog window (ignored on Android) initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, e.g. 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' - multiselect (bool): Value showing whether a user can select multiple files. + multiselect (bool): Value showing whether a user can select multiple folders (ignored on Android) Returns: The absolute path(str) to the selected file or None. On Android, you will get back From 37b9ca4c3f539ed9e8aaba5d3f2bdc6b6100d853 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 9 Dec 2020 08:28:46 +0100 Subject: [PATCH 15/24] * clean up of pyproject.toml --- examples/filebrowser/pyproject.toml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/filebrowser/pyproject.toml b/examples/filebrowser/pyproject.toml index 5b87ed8d93..46e5da4338 100644 --- a/examples/filebrowser/pyproject.toml +++ b/examples/filebrowser/pyproject.toml @@ -14,9 +14,7 @@ author_email = "tiberius@beeware.org" formal_name = "Android Filebrowser Demo" description = "A testing app" sources = ['filebrowser'] -requires = [ - 'c:/Projects/Python/Toga/src/core' -] +requires = [] [tool.briefcase.app.filebrowser.macOS] @@ -31,8 +29,7 @@ requires = [ [tool.briefcase.app.filebrowser.windows] requires = [ - # 'toga-winforms', - 'c:/Projects/Python/Toga/src/winforms' + 'toga-winforms', ] # Mobile deployments @@ -43,6 +40,5 @@ requires = [ [tool.briefcase.app.filebrowser.android] requires = [ - #'toga-android', - 'c:/Projects/Python/Toga/src/android' + 'toga-android', ] From 2ffc67ff6c54acb554306c267ddbc8f3405ae8fc Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 9 Dec 2020 08:35:36 +0100 Subject: [PATCH 16/24] * fixed pre-test check issue --- src/android/toga_android/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 83287220ba..b76b3c287e 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -1,6 +1,7 @@ from . import dialogs from .libs.android import Activity, Intent, Uri + class AndroidViewport: def __init__(self, native): self.native = native From 2fc59404e3c6a4d534aee3d76c5517697758fb47 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 9 Dec 2020 08:43:10 +0100 Subject: [PATCH 17/24] * fixed pre-test check issue --- src/android/toga_android/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index b76b3c287e..e2fdf83f6c 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -87,7 +87,7 @@ async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselec :param str title: The title is ignored on Android :param initial_uri: The initial location shown in the file chooser. Must be a content URI, e.g. - 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' :type initial_uri: str or None :param file_mime_types: The file types allowed to select. Must be MIME types, e.g. ['application/pdf','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']. From 7e177f36a2cd586be71e33cef7e764eea7072db7 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 9 Dec 2020 08:44:45 +0100 Subject: [PATCH 18/24] * fixed pre-test check issue --- src/android/toga_android/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index e2fdf83f6c..28600343ed 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -137,7 +137,7 @@ async def select_folder_dialog(self, title, initial_uri=None, multiselect=False) :param str title: The title is ignored on Android :param initial_uri: The initial location shown in the file chooser. Must be a content URI, e.g. - 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' :type initial_uri: str or None :param bool multiselect: If True, then several files can be selected :returns: The content tree URI of the chosen folder or a list of content URIs when multiselect=True. From 868d06f19dea2ec14345f4e6be32944974b398c6 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 9 Dec 2020 08:51:40 +0100 Subject: [PATCH 19/24] * fixed pre-test check issue --- src/core/toga/window.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/toga/window.py b/src/core/toga/window.py index 2c70803021..5c8715b93d 100644 --- a/src/core/toga/window.py +++ b/src/core/toga/window.py @@ -270,6 +270,7 @@ def open_file_dialog(self, title, initial_directory=None, file_types=None, multi """ This opens a native dialog where the user can select the file to open. It is possible to set the initial folder and only show files with specified file extensions. If no path is returned (eg. dialog is canceled), a ValueError is raised. + Args: title (str): The title of the dialog window (ignored on Android) initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, @@ -280,7 +281,7 @@ def open_file_dialog(self, title, initial_directory=None, file_types=None, multi Returns: The absolute path(str) to the selected file or a list(str) if multiselect. On Android, you will get back - content URIs. + content URIs. """ return self._impl.open_file_dialog(title, initial_directory, file_types, multiselect) @@ -288,6 +289,7 @@ def select_folder_dialog(self, title, initial_directory=None, multiselect=False) """ This opens a native dialog where the user can select a folder. It is possible to set the initial folder. If no path is returned (eg. dialog is canceled), a ValueError is raised. + Args: title (str): The title of the dialog window (ignored on Android) initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, @@ -296,6 +298,6 @@ def select_folder_dialog(self, title, initial_directory=None, multiselect=False) Returns: The absolute path(str) to the selected file or None. On Android, you will get back - content tree URIs. + content tree URIs. """ return self._impl.select_folder_dialog(title, initial_directory, multiselect) From b4bc0665bb98bae77bbff128a8332e8970656aa8 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 16 Dec 2020 07:57:37 +0100 Subject: [PATCH 20/24] * Removed filebrowser example * Extended dialogs example for use with Android --- examples/dialogs/dialogs/app.py | 94 ++++++++++--- examples/dialogs/pyproject.toml | 7 +- examples/filebrowser/README.rst | 16 --- examples/filebrowser/filebrowser/__init__.py | 9 -- examples/filebrowser/filebrowser/__main__.py | 4 - examples/filebrowser/filebrowser/app.py | 125 ------------------ .../filebrowser/filebrowser/resources/README | 1 - examples/filebrowser/pyproject.toml | 44 ------ 8 files changed, 82 insertions(+), 218 deletions(-) delete mode 100644 examples/filebrowser/README.rst delete mode 100644 examples/filebrowser/filebrowser/__init__.py delete mode 100644 examples/filebrowser/filebrowser/__main__.py delete mode 100644 examples/filebrowser/filebrowser/app.py delete mode 100644 examples/filebrowser/filebrowser/resources/README delete mode 100644 examples/filebrowser/pyproject.toml diff --git a/examples/dialogs/dialogs/app.py b/examples/dialogs/dialogs/app.py index a5e598f859..7f87544e03 100644 --- a/examples/dialogs/dialogs/app.py +++ b/examples/dialogs/dialogs/app.py @@ -43,6 +43,18 @@ def action_open_file_dialog(self, widget): except ValueError: self.label.text = "Open file dialog was canceled" + async def action_open_file_dialog_android(self, widget): + try: + selected_uri = '' + selected_uri = await self.app.main_window.open_file_dialog( + title="Choose a file", + multiselect=False) + self.label.text = "You selected: " + str(selected_uri) + except ValueError as e: + selected_uri = str(e) + self.label.text = selected_uri + print(str(selected_uri)) + def action_open_file_filtered_dialog(self, widget): try: fname = self.main_window.open_file_dialog( @@ -57,6 +69,9 @@ def action_open_file_filtered_dialog(self, widget): except ValueError: self.label.text = "Open file dialog was canceled" + def action_open_file_filtered_dialog_android(self, widget): + self.label.text = "file_types currently not supported by rubicon java" + def action_open_file_dialog_multi(self, widget): try: filenames = self.main_window.open_file_dialog( @@ -72,6 +87,18 @@ def action_open_file_dialog_multi(self, widget): except ValueError: self.label.text = "Open file dialog was canceled" + async def action_open_file_dialog_multi_android(self, widget): + try: + selected_uri = '' + selected_uri = await self.app.main_window.open_file_dialog( + title="Choose a file", + multiselect=True) + self.label.text = "You selected: " + str(selected_uri) + except ValueError as e: + selected_uri = str(e) + self.label.text = selected_uri + print(str(selected_uri)) + def action_select_folder_dialog(self, widget): try: path_names = self.main_window.select_folder_dialog( @@ -81,6 +108,17 @@ def action_select_folder_dialog(self, widget): except ValueError: self.label.text = "Folder select dialog was canceled" + async def action_select_folder_dialog_android(self, widget): + try: + selected_uri = '' + selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", + multiselect=False) + self.label.text = "You selected: " + str(selected_uri) + except ValueError as e: + selected_uri = str(e) + self.label.text = selected_uri + print(str(selected_uri)) + def action_select_folder_dialog_multi(self, widget): try: path_names = self.main_window.select_folder_dialog( @@ -91,6 +129,9 @@ def action_select_folder_dialog_multi(self, widget): except ValueError: self.label.text = "Folders select dialog was canceled" + async def action_select_folder_dialog_multi_android(self, widget): + self.label.text = "Multiple folder selection is not supported" + def action_save_file_dialog(self, widget): fname = 'Toga_file.txt' try: @@ -117,24 +158,43 @@ 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_open = toga.Button('Open File', on_press=self.action_open_file_dialog, style=btn_style) - btn_open_filtered = toga.Button( - 'Open File (Filtered)', - on_press=self.action_open_file_filtered_dialog, - style=btn_style - ) - btn_open_multi = toga.Button( - 'Open File (Multiple)', - on_press=self.action_open_file_dialog_multi, - style=btn_style - ) + if toga.platform.current_platform == 'android': + btn_open = toga.Button('Open File', on_press=self.action_open_file_dialog_android, style=btn_style) + btn_open_filtered = toga.Button( + 'Open File (Filtered)', + on_press=self.action_open_file_filtered_dialog_android, + style=btn_style + ) + btn_open_multi = toga.Button( + 'Open File (Multiple)', + on_press=self.action_open_file_dialog_multi_android, + style=btn_style + ) + btn_select = toga.Button('Select Folder', + on_press=self.action_select_folder_dialog_android, style=btn_style) + btn_select_multi = toga.Button( + 'Select Folders', + on_press=self.action_select_folder_dialog_multi_android, style=btn_style + ) + else: + btn_open = toga.Button('Open File', on_press=self.action_open_file_dialog, style=btn_style) + btn_open_filtered = toga.Button( + 'Open File (Filtered)', + on_press=self.action_open_file_filtered_dialog, + style=btn_style + ) + btn_open_multi = toga.Button( + 'Open File (Multiple)', + on_press=self.action_open_file_dialog_multi, + style=btn_style + ) + btn_select = toga.Button('Select Folder', on_press=self.action_select_folder_dialog, style=btn_style) + btn_select_multi = toga.Button( + 'Select Folders', + on_press=self.action_select_folder_dialog_multi, + style=btn_style + ) btn_save = toga.Button('Save File', on_press=self.action_save_file_dialog, style=btn_style) - btn_select = toga.Button('Select Folder', on_press=self.action_select_folder_dialog, style=btn_style) - btn_select_multi = toga.Button( - 'Select Folders', - on_press=self.action_select_folder_dialog_multi, - style=btn_style - ) btn_clear = toga.Button('Clear', on_press=self.do_clear, style=btn_style) diff --git a/examples/dialogs/pyproject.toml b/examples/dialogs/pyproject.toml index 41be7248c9..6ae5394e03 100644 --- a/examples/dialogs/pyproject.toml +++ b/examples/dialogs/pyproject.toml @@ -14,7 +14,9 @@ author_email = "tiberius@beeware.org" formal_name = "Dialog Demo" description = "A testing app" sources = ['dialogs'] -requires = [] +requires = [ + 'c:/Projects/Python/Toga/src/core' +] [tool.briefcase.app.dialogs.macOS] @@ -40,5 +42,6 @@ requires = [ [tool.briefcase.app.dialogs.android] requires = [ - 'toga-android', + #'toga-android', + 'c:/Projects/Python/Toga/src/android' ] diff --git a/examples/filebrowser/README.rst b/examples/filebrowser/README.rst deleted file mode 100644 index db39a0d4f5..0000000000 --- a/examples/filebrowser/README.rst +++ /dev/null @@ -1,16 +0,0 @@ -Android Filebrowser Demo -======================== - -Test app for the Android native file / folder chooser. - -Quickstart -~~~~~~~~~~ - -For this example to work under Android, you need a briefcase android template -which supports onActivityResult in MainActivity.java -see https://github.com/t-arn/briefcase-android-gradle-template.git branch onActivityResult - -To run this example: - - $ pip install toga - $ python -m filebrowser diff --git a/examples/filebrowser/filebrowser/__init__.py b/examples/filebrowser/filebrowser/__init__.py deleted file mode 100644 index 5de825baaa..0000000000 --- a/examples/filebrowser/filebrowser/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# Examples of valid version strings -# __version__ = '1.2.3.dev1' # Development release 1 -# __version__ = '1.2.3a1' # Alpha Release 1 -# __version__ = '1.2.3b1' # Beta Release 1 -# __version__ = '1.2.3rc1' # RC Release 1 -# __version__ = '1.2.3' # Final Release -# __version__ = '1.2.3.post1' # Post Release 1 - -__version__ = '0.0.1' diff --git a/examples/filebrowser/filebrowser/__main__.py b/examples/filebrowser/filebrowser/__main__.py deleted file mode 100644 index 871e6e42cc..0000000000 --- a/examples/filebrowser/filebrowser/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from filebrowser.app import main - -if __name__ == '__main__': - main().main_loop() diff --git a/examples/filebrowser/filebrowser/app.py b/examples/filebrowser/filebrowser/app.py deleted file mode 100644 index 933d7119f8..0000000000 --- a/examples/filebrowser/filebrowser/app.py +++ /dev/null @@ -1,125 +0,0 @@ -# For this example to work under Android, you need a briefcase android template -# which supports onActivityResult in MainActivity.java -# see https://github.com/t-arn/briefcase-android-gradle-template.git branch onActivityResult - - -import toga -from toga.style import Pack -from toga.constants import COLUMN, ROW -from rubicon.java import JavaClass - -Intent = JavaClass("android/content/Intent") -Activity = JavaClass("android/app/Activity") - - -class ExampleFilebrowserApp(toga.App): - # Button callback functions - async def do_open_file(self, widget, **kwargs): - print("Clicked on 'Open file'") - multiselect = False - mimetypes = str(self.file_types.value).split(' ') - if self.multiselect.value == 'True': - multiselect = True - try: - selected_uri = '' - print(mimetypes) - if self.use_oifm.value != 'True': - selected_uri = await self.app.main_window.open_file_dialog("Choose a file", self.initial_dir.value, - mimetypes, multiselect) - else: - intent = Intent("org.openintents.action.PICK_FILE") - intent.putExtra("org.openintents.extra.TITLE", "Choose a file") - result = await self.app._impl.invoke_intent_for_result(intent) - print(str(result)) - if result["resultData"] is not None: - selected_uri = result["resultData"].getData() - else: - selected_uri = 'No file selected, ResultCode was ' + str(result["resultCode"]) + ")" - except ValueError as e: - selected_uri = str(e) - print(str(selected_uri)) - self.multiline.value = "You selected: \n" + str(selected_uri) - - async def do_open_folder(self, widget, **kwargs): - print("Clicked on 'Open folder'") - multiselect = False - if self.multiselect.value == 'True': - multiselect = True - try: - selected_uri = '' - if self.use_oifm.value != 'True': - selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", - self.initial_dir.value, multiselect) - else: - intent = Intent("org.openintents.action.PICK_DIRECTORY") - intent.putExtra("org.openintents.extra.TITLE", "Choose a folder") - result = await self.app._impl.invoke_intent_for_result(intent) - print(str(result)) - if result["resultData"] is not None: - selected_uri = result["resultData"].getData() - else: - selected_uri = 'No folder selected, ResultCode was ' + str(result["resultCode"]) + ")" - except ValueError as e: - selected_uri = str(e) - self.multiline.value = "You selected: \n" + str(selected_uri) - - def do_clear(self, widget, **kwargs): - print('Clearing result') - self.multiline.value = "Ready." - - def startup(self): - # Set up main window - self.main_window = toga.MainWindow(title=self.name, size=(400, 700)) - flex_style = Pack(flex=1) - - # set options - self.initial_dir = toga.TextInput(placeholder='initial directory', style=flex_style) - self.file_types = toga.TextInput(placeholder='MIME types (blank separated)', style=flex_style) - self.multiselect = toga.TextInput(placeholder='is multiselect? (True / False)', style=flex_style) - self.use_oifm = toga.TextInput(placeholder='Use OI Filemanager? (True / False)', style=flex_style) - # Toga.Switch does not seem to work on Android ... - # self.multiselect = toga.Switch('multiselect', is_on=False) - # self.use_oifm = toga.Switch('Use OI Filemanager') - - # Text field to show responses. - self.multiline = toga.MultilineTextInput('Ready.', style=(Pack(height=200))) - - # Buttons - btn_open_file = toga.Button('Open file', on_press=self.do_open_file, style=flex_style) - btn_open_folder = toga.Button('Open folder', on_press=self.do_open_folder, style=flex_style) - btn_clear = toga.Button('Clear', on_press=self.do_clear, style=flex_style) - btn_box = toga.Box( - children=[ - btn_open_file, - btn_open_folder, - btn_clear - ], - style=Pack(direction=ROW) - ) - - # Outermost box - outer_box = toga.Box( - children=[self.initial_dir, self.file_types, self.multiselect, self.use_oifm, btn_box, self.multiline], - style=Pack( - flex=1, - direction=COLUMN, - padding=10, - width=500, - height=300 - ) - ) - - # Add the content on the main window - self.main_window.content = outer_box - - # Show the main window - self.main_window.show() - - -def main(): - return ExampleFilebrowserApp('Android Filebrowser Demo', 'org.beeware.widgets.filebrowser') - - -if __name__ == '__main__': - app = main() - app.main_loop() diff --git a/examples/filebrowser/filebrowser/resources/README b/examples/filebrowser/filebrowser/resources/README deleted file mode 100644 index 246d0829ed..0000000000 --- a/examples/filebrowser/filebrowser/resources/README +++ /dev/null @@ -1 +0,0 @@ -Put any icons or images in this directory. \ No newline at end of file diff --git a/examples/filebrowser/pyproject.toml b/examples/filebrowser/pyproject.toml deleted file mode 100644 index 46e5da4338..0000000000 --- a/examples/filebrowser/pyproject.toml +++ /dev/null @@ -1,44 +0,0 @@ -[build-system] -requires = ["briefcase"] - -[tool.briefcase] -project_name = "Android Filebrowser Demo" -bundle = "org.beeware" -version = "0.3.0.dev25" -url = "https://beeware.org" -license = "BSD license" -author = 'Tiberius Yak' -author_email = "tiberius@beeware.org" - -[tool.briefcase.app.filebrowser] -formal_name = "Android Filebrowser Demo" -description = "A testing app" -sources = ['filebrowser'] -requires = [] - - -[tool.briefcase.app.filebrowser.macOS] -requires = [ - 'toga-cocoa', -] - -[tool.briefcase.app.filebrowser.linux] -requires = [ - 'toga-gtk', -] - -[tool.briefcase.app.filebrowser.windows] -requires = [ - 'toga-winforms', -] - -# Mobile deployments -[tool.briefcase.app.filebrowser.iOS] -requires = [ - 'toga-iOS', -] - -[tool.briefcase.app.filebrowser.android] -requires = [ - 'toga-android', -] From 2009c6ebf6d9d8e7f645c64f35d3651a8370d736 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 16 Dec 2020 08:18:23 +0100 Subject: [PATCH 21/24] * fixed pre-test checks --- examples/dialogs/dialogs/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/dialogs/dialogs/app.py b/examples/dialogs/dialogs/app.py index 7f87544e03..ef9784ef41 100644 --- a/examples/dialogs/dialogs/app.py +++ b/examples/dialogs/dialogs/app.py @@ -112,7 +112,7 @@ async def action_select_folder_dialog_android(self, widget): try: selected_uri = '' selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", - multiselect=False) + multiselect=False) self.label.text = "You selected: " + str(selected_uri) except ValueError as e: selected_uri = str(e) @@ -171,7 +171,7 @@ def startup(self): style=btn_style ) btn_select = toga.Button('Select Folder', - on_press=self.action_select_folder_dialog_android, style=btn_style) + on_press=self.action_select_folder_dialog_android, style=btn_style) btn_select_multi = toga.Button( 'Select Folders', on_press=self.action_select_folder_dialog_multi_android, style=btn_style From 32cb78eacdaf088f21e86e9542e6eeaefbc572ce Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 12 Aug 2021 15:33:12 +0200 Subject: [PATCH 22/24] Fixed bugs --- examples/dialogs/dialogs/app.py | 22 +++++++++++----------- src/android/toga_android/window.py | 2 ++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/examples/dialogs/dialogs/app.py b/examples/dialogs/dialogs/app.py index bf277a1277..840aecf6fc 100644 --- a/examples/dialogs/dialogs/app.py +++ b/examples/dialogs/dialogs/app.py @@ -240,17 +240,17 @@ def startup(self): on_press=self.action_select_folder_dialog_multi_android, style=btn_style ) else: - btn_open = toga.Button('Open File', on_press=self.action_open_file_dialog, style=btn_style) - btn_open_filtered = toga.Button( - 'Open File (Filtered)', - on_press=self.action_open_file_filtered_dialog, - style=btn_style - ) - btn_open_multi = toga.Button( - 'Open File (Multiple)', - on_press=self.action_open_file_dialog_multi, - 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)', + on_press=self.action_open_file_filtered_dialog, + style=btn_style + ) + btn_open_multi = toga.Button( + 'Open File (Multiple)', + on_press=self.action_open_file_dialog_multi, + style=btn_style + ) btn_save = toga.Button('Save File', on_press=self.action_save_file_dialog, style=btn_style) btn_select = toga.Button('Select Folder', on_press=self.action_select_folder_dialog, style=btn_style) btn_select_multi = toga.Button( diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 0cf1ffc71d..d268477f0a 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -1,7 +1,9 @@ from . import dialogs from .libs.activity import Activity +from .libs.android import R__attr from .libs.android.content import Intent from .libs.android.net import Uri +from .libs.android.util import TypedValue class AndroidViewport: From f2ae7c1a99b449ce7d60907add494f2cab6b58de Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 13 Aug 2021 08:20:08 +0200 Subject: [PATCH 23/24] Unsupported buttons are now hidden on Android --- examples/dialogs/dialogs/app.py | 100 +++++++++++++++++++------------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/examples/dialogs/dialogs/app.py b/examples/dialogs/dialogs/app.py index 840aecf6fc..327586dbd1 100644 --- a/examples/dialogs/dialogs/app.py +++ b/examples/dialogs/dialogs/app.py @@ -251,51 +251,71 @@ def startup(self): on_press=self.action_open_file_dialog_multi, style=btn_style ) - btn_save = toga.Button('Save File', on_press=self.action_save_file_dialog, style=btn_style) - btn_select = toga.Button('Select Folder', on_press=self.action_select_folder_dialog, style=btn_style) - btn_select_multi = toga.Button( - 'Select Folders', - on_press=self.action_select_folder_dialog_multi, - style=btn_style - ) - btn_open_secondary_window = toga.Button( - 'Open Secondary Window', - on_press=self.action_open_secondary_window, - style=btn_style - ) - btn_close_secondary_window = toga.Button( - 'Close All Secondary Windows', - on_press=self.action_close_secondary_windows, - style=btn_style - ) + btn_save = toga.Button('Save File', on_press=self.action_save_file_dialog, style=btn_style) + btn_select = toga.Button('Select Folder', on_press=self.action_select_folder_dialog, style=btn_style) + btn_select_multi = toga.Button( + 'Select Folders', + on_press=self.action_select_folder_dialog_multi, + style=btn_style + ) + btn_open_secondary_window = toga.Button( + 'Open Secondary Window', + on_press=self.action_open_secondary_window, + style=btn_style + ) + btn_close_secondary_window = toga.Button( + 'Close All Secondary Windows', + on_press=self.action_close_secondary_windows, + style=btn_style + ) btn_clear = toga.Button('Clear', on_press=self.do_clear, style=btn_style) # Outermost box - box = toga.Box( - children=[ - btn_info, - btn_question, - btn_confirm, - btn_error, - btn_open, - btn_open_filtered, - btn_save, - btn_select, - btn_select_multi, - btn_open_multi, - btn_open_secondary_window, - btn_close_secondary_window, - btn_clear, - self.label, - self.window_label - ], - style=Pack( - flex=1, - direction=COLUMN, - padding=10 + if toga.platform.current_platform == 'android': + box = toga.Box( + children=[ + btn_info, + btn_open, + btn_open_filtered, + btn_select, + btn_select_multi, + btn_open_multi, + btn_clear, + self.label, + self.window_label + ], + style=Pack( + flex=1, + direction=COLUMN, + padding=10 + ) + ) + else: + box = toga.Box( + children=[ + btn_info, + btn_question, + btn_confirm, + btn_error, + btn_open, + btn_open_filtered, + btn_save, + btn_select, + btn_select_multi, + btn_open_multi, + btn_open_secondary_window, + btn_close_secondary_window, + btn_clear, + self.label, + self.window_label + ], + style=Pack( + flex=1, + direction=COLUMN, + padding=10 + ) ) - ) # Add the content on the main window self.main_window.content = box From aad9eab195580120052894d0c4e3c9755569538e Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 13 Aug 2021 09:31:18 +0200 Subject: [PATCH 24/24] Removed android.py which is not needed anymore --- src/android/toga_android/libs/android.py | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 src/android/toga_android/libs/android.py diff --git a/src/android/toga_android/libs/android.py b/src/android/toga_android/libs/android.py deleted file mode 100644 index f9067a40f1..0000000000 --- a/src/android/toga_android/libs/android.py +++ /dev/null @@ -1,5 +0,0 @@ -from rubicon.java import JavaClass - -Activity = JavaClass("android/app/Activity") -Intent = JavaClass("android/content/Intent") -Uri = JavaClass("android/net/Uri")