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

[widget Audit] toga.App #2075

Merged
merged 74 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 67 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
684a45d
Add documentation and test coverage for app and document.
freakboy3742 Aug 11, 2023
2e19256
Add an example DocumentApp
freakboy3742 Aug 11, 2023
8f77008
Add changenotes.
freakboy3742 Aug 11, 2023
d1a64a4
Correct usage of exit() by main window and quit menu items.
freakboy3742 Aug 11, 2023
2dfa2be
Correct usage of exit in testbed, and add a pause to ensure logs are …
freakboy3742 Aug 11, 2023
8f92cd3
Add docs, docstrings and type annotations for Command.
freakboy3742 Aug 11, 2023
2076dd9
Complete core test coverage for Command.
freakboy3742 Aug 12, 2023
f8c19b3
Remove CommandSet, GROUP_BREAK and SECTION_BREAK from public namespace.
freakboy3742 Aug 12, 2023
e0a46dc
Corrected some coverage issues with a late change to comparisons.
freakboy3742 Aug 12, 2023
fec495d
Factored out a common app fixture.
freakboy3742 Aug 12, 2023
dde078e
Cocoa to 100% (ish) coverage.
freakboy3742 Aug 15, 2023
d27307b
100% coverage of keys.
freakboy3742 Aug 15, 2023
d505c95
Insert a pause on app exit to make sure Briefcase gets all the app logs.
freakboy3742 Aug 16, 2023
035fc8b
Merge branch 'audit-window' into audit-app
freakboy3742 Aug 16, 2023
8219173
Merge branch 'audit-window' into audit-app
freakboy3742 Aug 16, 2023
fedd4fe
GTK app tests passing.
freakboy3742 Aug 16, 2023
6c8877c
hack around segfault for Gtk WebView tests
samschott Aug 16, 2023
596149f
Key tests at 100% on GTK.
samschott Aug 16, 2023
8140de2
Tweaks to get 100% coverage on GTK.
freakboy3742 Aug 17, 2023
adcecf2
Short delay to improve test reliability.
freakboy3742 Aug 17, 2023
ada9303
Add a missing await.
freakboy3742 Aug 17, 2023
cceeb5f
Add some CI debugging help.
freakboy3742 Aug 17, 2023
ca1471d
Merge branch 'audit-window' into audit-app
freakboy3742 Aug 23, 2023
9f24361
100% coverage of app on iOS.
freakboy3742 Aug 23, 2023
a5931ed
Merge branch 'audit-window' into audit-app
freakboy3742 Aug 24, 2023
c6fe640
Corrections to object lifecycle and test probes to pass CI.
freakboy3742 Aug 25, 2023
28771cf
Merge branch 'main' into audit-app
freakboy3742 Oct 18, 2023
1334fc6
Enable 100% coverage requirement on core tests.
freakboy3742 Oct 18, 2023
5cf47cd
Enable 100% test requirement on testbed tests.
freakboy3742 Oct 18, 2023
b7fe16c
Remove backend tests.
freakboy3742 Oct 18, 2023
98ef170
Update docs to remove references to py-core and backend tests.
freakboy3742 Oct 18, 2023
eea80a1
Correct some pre-commit issues.
freakboy3742 Oct 18, 2023
7f42fce
Tweaks for GTK test coverage.
freakboy3742 Oct 18, 2023
69e760b
Ensure constraints are retained until the container is destroyed.
freakboy3742 Oct 19, 2023
5c31e88
Merge branch 'main' into audit-app
freakboy3742 Oct 20, 2023
08b5b33
Increase tolerances for testing on cocoa.
freakboy3742 Oct 20, 2023
9f09703
Merge remote-tracking branch 'origin/main' into audit-app
mhsmith Oct 23, 2023
fb25864
Remove toga_dummy/test_implementation.py
mhsmith Oct 23, 2023
7222344
Update tox and isort comments
mhsmith Oct 23, 2023
3f67033
Clean up App constructor docs
mhsmith Oct 23, 2023
ca8410d
Document App.windows, and related fixes
mhsmith Oct 25, 2023
1a2baec
Deprecate redundant "id" and "name" properties
mhsmith Oct 25, 2023
9127104
Add `App.loop` property
mhsmith Oct 25, 2023
5fdf934
Replace uses of App.name
mhsmith Oct 25, 2023
59654ad
Merge branch 'main' into audit-app
mhsmith Oct 25, 2023
f20899a
Fix WinForms menu initialization
mhsmith Oct 25, 2023
51141bb
More App documentation cleanups
mhsmith Oct 26, 2023
b7977d2
Command documentation cleanups
mhsmith Oct 26, 2023
64064b8
DocumentApp documentation cleanups
mhsmith Oct 26, 2023
a8df430
Correct GTK file dialog usage in DocumentApp.
freakboy3742 Oct 27, 2023
1029f33
examples/command working correctly on WinForms
mhsmith Oct 27, 2023
d185acf
WinForms passing all tests
mhsmith Oct 27, 2023
3938b1e
WinForms app.py at 100% coverage
mhsmith Oct 28, 2023
688da96
All WinForms files at 100% coverage
mhsmith Oct 28, 2023
8b83148
Add debugging for cursor visibility
mhsmith Oct 28, 2023
500ec7f
More CI debugging
mhsmith Oct 28, 2023
74f79fe
More CI debugging
mhsmith Oct 29, 2023
40d7120
More CI debugging
mhsmith Oct 29, 2023
387a4aa
Remove CI debugging
mhsmith Oct 29, 2023
f063b37
examples/command working correctly on Android
mhsmith Oct 29, 2023
4cde00c
Android passing all tests
mhsmith Oct 29, 2023
2e69615
Android app.py at 100% coverage
mhsmith Oct 29, 2023
1583545
All Android files at 100% coverage
mhsmith Oct 29, 2023
ff64a99
Protect against an edge case of garbage collection.
freakboy3742 Oct 30, 2023
dd9babf
Restore WindowSet `__isub__` and `__iadd__` methods
mhsmith Oct 30, 2023
de3fff6
Revert rename of app_name to distribution_name
mhsmith Oct 30, 2023
3c2fa56
Fix link between Window.toolbar and App.commands
mhsmith Oct 30, 2023
3ba85dd
Document the behavior around implicit command registration.
freakboy3742 Oct 30, 2023
7f0b952
Make windowset +=/-= operations no-ops.
freakboy3742 Oct 30, 2023
2b05ca2
Make the app a required argument to commandset if it's going to be used.
freakboy3742 Oct 30, 2023
0df6ea2
Ensure stale references to menus are removed on re-creation.
freakboy3742 Oct 30, 2023
aaca421
An extra scrollcontainer assertion to validate an inconsistent code p…
freakboy3742 Oct 30, 2023
ce9cc85
Ensure Winforms table warnings are captured.
freakboy3742 Oct 31, 2023
207b440
Add a test of adding a command to both toolbar and menus
mhsmith Oct 31, 2023
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
57 changes: 3 additions & 54 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,8 @@ jobs:
# The $(ls ...) shell expansion is done in the Github environment;
# the value of TOGA_INSTALL_COMMAND will be a literal string,
# without any shell expansions to perform
TOGA_INSTALL_COMMAND="python -m pip install ../$(ls core/dist/toga_core-*.whl)[dev] ../$(ls dummy/dist/toga_dummy-*.whl)" tox -e py-core
cd core
mv .coverage .coverage.${{ matrix.platform }}.${{ matrix.python-version }}
TOGA_INSTALL_COMMAND="python -m pip install ../$(ls core/dist/toga_core-*.whl)[dev] ../$(ls dummy/dist/toga_dummy-*.whl)" tox -e py
mv core/.coverage core/.coverage.${{ matrix.platform }}.${{ matrix.python-version }}
- name: Store coverage data
uses: actions/upload-artifact@v3.1.3
with:
Expand Down Expand Up @@ -128,64 +127,14 @@ jobs:
cd core
python -m coverage combine
python -m coverage html --skip-covered --skip-empty
python -m coverage report --rcfile ../pyproject.toml # --fail-under=100
python -m coverage report --rcfile ../pyproject.toml --fail-under=100
- name: Upload HTML report if check failed.
uses: actions/upload-artifact@v3.1.3
with:
name: html-coverage-report
path: core/htmlcov
if: ${{ failure() }}

backend:
runs-on: ${{ matrix.runs-on }}
needs: [package, core]
strategy:
matrix:
backend: [ "android", "cocoa", "gtk", "iOS", "web", "winforms" ]
include:
- runs-on: ubuntu-latest
- python-version: "3.8" # Should be env.min_python_version (https://github.com/actions/runner/issues/480)
- pre-command:

- backend: cocoa
runs-on: macos-latest

- backend: gtk
pre-command: |
sudo apt update -y
sudo apt install -y pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-gtk-3.0

- backend: iOS
runs-on: macos-latest

- backend: winforms
runs-on: windows-latest
# Py3.9 is the first Python version for which
# a wheel of pythonnet isn't available on PyPI.
python-version: "3.9"
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.7.1
with:
python-version: ${{ matrix.python-version }}
- name: Get packages
uses: actions/download-artifact@v3.0.2
with:
name: ${{ needs.package.outputs.artifact-name }}
- name: Install dev dependencies
run: |
${{ matrix.pre-command }}
# We don't actually want to install toga-core;
# we just want the dev extras so we have a known version of tox
python -m pip install ./core[dev]
- name: Test
run: |
# The $(ls ...) shell expansion is done in the Github environment;
# the value of TOGA_INSTALL_COMMAND will be a literal string,
# without any shell expansions to perform
TOGA_INSTALL_COMMAND="python -m pip install ../$(ls core/dist/toga_core-*.whl)[dev] ../$(ls dummy/dist/toga_dummy-*.whl) ../$(ls ${{ matrix.backend }}/dist/toga_${{ matrix.backend }}-*.whl)" tox -e py-${{ matrix.backend }}

testbed:
runs-on: ${{ matrix.runs-on }}
needs: core
Expand Down
159 changes: 94 additions & 65 deletions android/src/toga_android/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import asyncio
import sys

from android.graphics.drawable import BitmapDrawable
from android.media import RingtoneManager
from android.view import Menu, MenuItem
from java import dynamic_proxy
from org.beeware.android import IPythonApp, MainActivity

import toga
from android.graphics.drawable import Drawable
from android.media import RingtoneManager
from android.view import Menu, MenuItem
from toga.command import Group
from toga.command import GROUP_BREAK, SECTION_BREAK, Command, Group

from .libs import events
from .window import Window
Expand Down Expand Up @@ -41,18 +41,19 @@ def onResume(self):
print("Toga app: onResume")

def onPause(self):
print("Toga app: onPause")
print("Toga app: onPause") # pragma: no cover

def onStop(self):
print("Toga app: onStop")
print("Toga app: onStop") # pragma: no cover

def onDestroy(self):
print("Toga app: onDestroy")
print("Toga app: onDestroy") # pragma: no cover

def onRestart(self):
print("Toga app: onRestart")
print("Toga app: onRestart") # pragma: no cover

def onActivityResult(self, requestCode, resultCode, resultData):
# TODO #1798: document and test this somehow
def onActivityResult(self, requestCode, resultCode, resultData): # pragma: no cover
"""Callback method, called from MainActivity when an Intent ends.

:param int requestCode: The integer request code originally supplied to startActivityForResult(),
Expand All @@ -75,94 +76,86 @@ def onActivityResult(self, requestCode, resultCode, resultData):
print("No intent matching request code {requestCode}")

def onConfigurationChanged(self, new_config):
pass
pass # pragma: no cover

def onOptionsItemSelected(self, menuitem):
consumed = False
try:
cmd = self.menuitem_mapping[menuitem.getItemId()]
consumed = True
if cmd.action is not None:
cmd.action(menuitem)
except KeyError:
print("menu item id not found in menuitem_mapping dictionary!")
return consumed
itemid = menuitem.getItemId()
if itemid == Menu.NONE:
# This method also fires when opening submenus
return False
else:
self.menuitem_mapping[itemid].action(None)
return True

def onPrepareOptionsMenu(self, menu):
menu.clear()
itemid = 0
itemid = 1 # 0 is the same as Menu.NONE.
groupid = 1
menulist = {} # dictionary with all menus
self.menuitem_mapping.clear()

# create option menu
for cmd in self._impl.interface.commands:
if cmd == toga.SECTION_BREAK or cmd == toga.GROUP_BREAK:
if cmd == SECTION_BREAK or cmd == GROUP_BREAK:
groupid += 1
continue
if cmd in self._impl.interface.main_window.toolbar:
continue # do not show toolbar commands in the option menu (except when overflowing)

grouppath = cmd.group.path
if grouppath[0] != Group.COMMANDS:
# only the Commands group (and its subgroups) are supported
# other groups should eventually go into the navigation drawer
# Toolbar commands are added below.
if cmd in self._impl.interface.main_window.toolbar:
continue

if cmd.group.key in menulist:
menugroup = menulist[cmd.group.key]
else:
# create all missing submenus
parentmenu = menu
for group in grouppath:
groupkey = group.key
groupkey = ()
for section, order, text in cmd.group.key:
groupkey += ((section, order, text),)
if groupkey in menulist:
menugroup = menulist[groupkey]
else:
if group.text == toga.Group.COMMANDS.text:
if len(groupkey) == 1 and text == Group.COMMANDS.text:
# Add this group directly to the top-level menu
menulist[groupkey] = menu
menugroup = menu
else:
itemid += 1
order = Menu.NONE if group.order is None else group.order
# Add all other groups as submenus
menugroup = parentmenu.addSubMenu(
Menu.NONE, itemid, order, group.text
) # groupId, itemId, order, title
groupid, Menu.NONE, Menu.NONE, text
)
menulist[groupkey] = menugroup
parentmenu = menugroup

# create menu item
itemid += 1
order = Menu.NONE if cmd.order is None else cmd.order
menuitem = menugroup.add(
Menu.NONE, itemid, order, cmd.text
) # groupId, itemId, order, title
menuitem = menugroup.add(groupid, itemid, Menu.NONE, cmd.text)
menuitem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_NEVER)
menuitem.setEnabled(cmd.enabled)
self.menuitem_mapping[
itemid
] = cmd # store itemid for use in onOptionsItemSelected
self.menuitem_mapping[itemid] = cmd
itemid += 1

# create toolbar actions
if self._impl.interface.main_window:
if self._impl.interface.main_window: # pragma: no branch
for cmd in self._impl.interface.main_window.toolbar:
if cmd == toga.SECTION_BREAK or cmd == toga.GROUP_BREAK:
if cmd == SECTION_BREAK or cmd == GROUP_BREAK:
groupid += 1
continue
itemid += 1
order = Menu.NONE if cmd.order is None else cmd.order
menuitem = menu.add(
Menu.NONE, itemid, order, cmd.text
) # groupId, itemId, order, title
menuitem.setShowAsActionFlags(
MenuItem.SHOW_AS_ACTION_IF_ROOM
) # toolbar button / item in options menu on overflow

menuitem = menu.add(groupid, itemid, Menu.NONE, cmd.text)
# SHOW_AS_ACTION_IF_ROOM is too conservative, showing only 2 items on
# a medium-size screen in portrait.
menuitem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
menuitem.setEnabled(cmd.enabled)
if cmd.icon:
icon = Drawable.createFromPath(str(cmd.icon._impl.path))
if icon:
menuitem.setIcon(icon)
else:
print("Could not create icon: " + str(cmd.icon._impl.path))
self.menuitem_mapping[
itemid
] = cmd # store itemid for use in onOptionsItemSelected
menuitem.setIcon(
BitmapDrawable(
self.native.getResources(), cmd.icon._impl.native
)
)
self.menuitem_mapping[itemid] = cmd
itemid += 1

# Display the menu.
return True


Expand All @@ -185,8 +178,17 @@ def create(self):
# Call user code to populate the main window
self.interface._startup()

def open_document(self, fileURL):
print("Can't open document %s (yet)" % fileURL)
self.interface.commands.add(
# About should be the last item in the menu, in a section on its own.
Command(
lambda _: self.interface.about(),
f"About {self.interface.formal_name}",
section=sys.maxsize,
),
)

def create_menus(self):
self.native.invalidateOptionsMenu() # Triggers onPrepareOptionsMenu

def main_loop(self):
# In order to support user asyncio code, start the Python/Android cooperative event loop.
Expand All @@ -200,7 +202,21 @@ def set_main_window(self, window):
pass

def show_about_dialog(self):
self.interface.factory.not_implemented("App.show_about_dialog()")
message_parts = []
if self.interface.version is not None:
message_parts.append(
f"{self.interface.formal_name} v{self.interface.version}"
)
else:
message_parts.append(self.interface.formal_name)

if self.interface.author is not None:
message_parts.append(f"Author: {self.interface.author}")
if self.interface.description is not None:
message_parts.append(f"\n{self.interface.description}")
self.interface.main_window.info_dialog(
f"About {self.interface.formal_name}", "\n".join(message_parts)
)

def beep(self):
uri = RingtoneManager.getActualDefaultRingtoneUri(
Expand All @@ -210,9 +226,16 @@ def beep(self):
ringtone.play()

def exit(self):
pass # pragma: no cover

def get_current_window(self):
return self.interface.main_window._impl

def set_current_window(self, window):
pass

async def intent_result(self, intent):
# TODO #1798: document and test this somehow
async def intent_result(self, intent): # pragma: no cover
"""Calls an Intent and waits for its result.

A RuntimeError will be raised when the Intent cannot be invoked.
Expand All @@ -234,6 +257,12 @@ async def intent_result(self, intent):
except AttributeError:
raise RuntimeError("No appropriate Activity found to handle this intent.")

def enter_full_screen(self, windows):
pass

def exit_full_screen(self, windows):
pass

def hide_cursor(self):
pass

Expand Down
5 changes: 1 addition & 4 deletions android/src/toga_android/colors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from travertino.colors import NAMED_COLOR, TRANSPARENT

from android.graphics import Color
from travertino.colors import TRANSPARENT

CACHE = {TRANSPARENT: Color.TRANSPARENT}

Expand All @@ -9,8 +8,6 @@ def native_color(c):
try:
color = CACHE[c]
except KeyError:
if isinstance(c, str):
c = NAMED_COLOR[c]
color = Color.argb(
int(c.rgba.a * 255), int(c.rgba.r), int(c.rgba.g), int(c.rgba.b)
)
Expand Down
5 changes: 4 additions & 1 deletion android/src/toga_android/command.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from org.beeware.android import MainActivity


class Command:
def __init__(self, interface):
self.interface = interface
self.native = []

def set_enabled(self, value):
pass
MainActivity.singletonThis.invalidateOptionsMenu()
3 changes: 1 addition & 2 deletions android/src/toga_android/dialogs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from abc import ABC

from java import dynamic_proxy

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


class OnClickListener(dynamic_proxy(DialogInterface.OnClickListener)):
Expand Down
4 changes: 2 additions & 2 deletions android/src/toga_android/fonts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from pathlib import Path

from org.beeware.android import MainActivity

from android import R
from android.graphics import Typeface
from android.util import TypedValue
from org.beeware.android import MainActivity

from toga.fonts import (
_REGISTERED_FONT_CACHE,
BOLD,
Expand Down
3 changes: 1 addition & 2 deletions android/src/toga_android/images.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from pathlib import Path

from java.io import FileOutputStream

from android.graphics import Bitmap, BitmapFactory
from java.io import FileOutputStream


class Image:
Expand Down
Loading
Loading