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.Window #2058

Merged
merged 47 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d3fd16d
Update docstrings and documentation for Window and MainWindow.
freakboy3742 Jul 31, 2023
8c09ef1
Add changenote.
freakboy3742 Jul 31, 2023
d2eec81
Migrated core tests to pytest.
freakboy3742 Aug 1, 2023
7ee6369
Complete coverage of core dialogs.
freakboy3742 Aug 1, 2023
3e6bf86
Remove GTK window tests.
freakboy3742 Aug 1, 2023
cec300d
Correct a spelling issue, and document an API rename.
freakboy3742 Aug 1, 2023
a13683b
Exclude some clases from interface tests.
freakboy3742 Aug 1, 2023
32097ca
Cocoa GUI tests for Window (excluding dialogs and toolbars.
freakboy3742 Aug 1, 2023
212fb89
Add menu items and keyboard shortcuts for close/minimize on macOS.
freakboy3742 Aug 1, 2023
f3bffde
Add GUI test of full screen mode.
freakboy3742 Aug 2, 2023
86e91ae
Converge on US spelling for resizable, closable
freakboy3742 Aug 2, 2023
9f719f6
Cocoa dialogs 100% coverage.
freakboy3742 Aug 2, 2023
e3613f2
100% coverage for Window on iOS.
freakboy3742 Aug 2, 2023
7cd3fde
Minor coverage tweaks.
freakboy3742 Aug 2, 2023
c867585
GTK Window and Dialogs to 100%.
freakboy3742 Aug 3, 2023
066a384
Tweak visibility handling to ensure complete coverage in testbed cond…
freakboy3742 Aug 3, 2023
3bd0b05
Use blackbox as a testing WM.
freakboy3742 Aug 4, 2023
054658d
Rework file dialog setup handling to avoid CI failure.
freakboy3742 Aug 4, 2023
e18c5b1
More test updates for CI's benefit.
freakboy3742 Aug 4, 2023
59cfb50
Restructure menu creation for cocoa.
freakboy3742 Aug 5, 2023
4fffdbe
Rework default title handling so MainWindow defaults to the formal na…
freakboy3742 Aug 5, 2023
ce4130d
Add a test of closing window explicitly.
freakboy3742 Aug 11, 2023
a04ad63
Merge branch 'main' into audit-window
freakboy3742 Aug 16, 2023
cc2df8b
Insert a pause on app exit to make sure Briefcase gets all the app logs.
freakboy3742 Aug 16, 2023
85f537f
Merge branch 'main' into audit-window
freakboy3742 Aug 23, 2023
34d40c5
Merge branch 'main' into audit-window
freakboy3742 Aug 24, 2023
f370c31
Merge branch 'main' into audit-window
freakboy3742 Sep 10, 2023
d63b462
Merge branch 'main' into audit-window
freakboy3742 Oct 5, 2023
5f126da
Slow down test suite startup until the main window is visible.
freakboy3742 Oct 6, 2023
a44043e
Window documentation cleanups
mhsmith Oct 14, 2023
c9d0e1e
Move common Android test methods to BaseProbe
mhsmith Oct 15, 2023
916ab25
Android Window at 100% coverage (excluding toolbar)
mhsmith Oct 15, 2023
9266dea
Merge remote-tracking branch 'origin/main' into audit-window
mhsmith Oct 15, 2023
5c55dea
Make core tests expect Window.app to be assigned in Window constructor
mhsmith Oct 15, 2023
01e71f0
Update other Android BaseProbe subclasses
mhsmith Oct 15, 2023
1a27d71
WinForms Window passing all tests except dialogs
mhsmith Oct 15, 2023
dda9a5f
WinForms info, question, confirm and error dialogs passing
mhsmith Oct 16, 2023
a293a89
WinForms stack trace dialog passing
mhsmith Oct 16, 2023
117f5d2
Fix expected OptionContainer sizes
mhsmith Oct 16, 2023
030c775
WinForms save file and select folder dialogs passing
mhsmith Oct 16, 2023
ff59e95
WinForms open file dialog passing
mhsmith Oct 17, 2023
4cbb49f
Fix WinForms select folder dialog title
mhsmith Oct 17, 2023
432e7c9
Empty multiple selection is not actually supported by any backend
mhsmith Oct 17, 2023
8d66d76
WinForms Window at 100% coverage (excluding toolbar)
mhsmith Oct 17, 2023
480580b
file_types should not include leading dots
mhsmith Oct 17, 2023
2ff3835
Remove unreachable code
mhsmith Oct 17, 2023
270d009
Update support table; run readthedocs build on Python 3.11
mhsmith Oct 17, 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
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,15 @@ jobs:
- backend: linux
runs-on: ubuntu-22.04
# The package list should be the same as in tutorial-0.rst, and the BeeWare
# tutorial, plus flwm to provide a window manager
# tutorial, plus blackbox to provide a window manager. We need a window
# manager that is reasonably lightweight, honors full screen mode, and
# treats the window position as the top-left corner of the *window*, not the
# top-left corner of the window *content*. The default GNOME window managers of
# most distros meet these requirementt, but they're heavyweight; flwm doesn't
# work either. Blackbox is the lightest WM we've found that works.
pre-command: |
sudo apt update -y
sudo apt install -y flwm pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.0
sudo apt install -y blackbox pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.0

# Start Virtual X server
echo "Start X server..."
Expand All @@ -221,7 +226,7 @@ jobs:

# Start Window manager
echo "Start window manager..."
DISPLAY=:99 flwm &
DISPLAY=:99 blackbox &
sleep 1

briefcase-run-prefix: 'DISPLAY=:99'
Expand Down
4 changes: 3 additions & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: "3"
# On Python 3.12, pip fails with AttributeError: module 'pkgutil' has no attribute
# 'ImpImporter', probably because readthedocs provides an old version of pip.
python: "3.11"
jobs:
post_checkout:
# RTD defaults to a depth of 50 but Toga versioning may require
Expand Down
5 changes: 3 additions & 2 deletions android/src/toga_android/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from .libs import events
from .window import Window

# `MainWindow` is defined here in `app.py`, not `window.py`, to mollify the test suite.
MainWindow = Window

class MainWindow(Window):
_is_main_window = True


class TogaApp(dynamic_proxy(IPythonApp)):
Expand Down
26 changes: 10 additions & 16 deletions android/src/toga_android/dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def onClick(self, _dialog, _which):
class BaseDialog(ABC):
def __init__(self, interface):
self.interface = interface
self.interface.impl = self
self.interface._impl = self


class TextDialog(BaseDialog):
Expand All @@ -34,15 +34,6 @@ def __init__(
icon=None,
on_result=None,
):
"""Create Android textual dialog.

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

Expand All @@ -53,10 +44,13 @@ def __init__(
if icon is not None:
self.native.setIcon(icon)

if positive_text is not None:
self.native.setPositiveButton(
positive_text, OnClickListener(self.completion_handler, True)
)
self.native.setPositiveButton(
positive_text,
OnClickListener(
self.completion_handler,
True if (negative_text is not None) else None,
),
)
if negative_text is not None:
self.native.setNegativeButton(
negative_text, OnClickListener(self.completion_handler, False)
Expand Down Expand Up @@ -149,7 +143,7 @@ def __init__(
title,
initial_directory,
file_types,
multiselect,
multiple_select,
on_result=None,
):
super().__init__(interface=interface)
Expand All @@ -162,7 +156,7 @@ def __init__(
interface,
title,
initial_directory,
multiselect,
multiple_select,
on_result=None,
):
super().__init__(interface=interface)
Expand Down
10 changes: 9 additions & 1 deletion android/src/toga_android/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@ def onGlobalLayout(self):


class Window(Container):
_is_main_window = False

def __init__(self, interface, title, position, size):
super().__init__()
self.interface = interface
self.interface._impl = self
# self.set_title(title)
self._initial_title = title

if not self._is_main_window:
raise RuntimeError(
"Secondary windows cannot be created on mobile platforms"
)

def set_app(self, app):
self.app = app
Expand All @@ -36,6 +43,7 @@ def set_app(self, app):
native_parent.getViewTreeObserver().addOnGlobalLayoutListener(
LayoutListener(self)
)
self.set_title(self._initial_title)

def get_title(self):
return str(self.app.native.getTitle())
Expand Down
3 changes: 1 addition & 2 deletions android/tests_backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

class AppProbe(BaseProbe):
def __init__(self, app):
super().__init__()
self.app = app
super().__init__(app)
assert isinstance(self.app._impl.native, MainActivity)

def get_app_context(self):
Expand Down
3 changes: 1 addition & 2 deletions android/tests_backend/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ class IconProbe(BaseProbe):
alternate_resource = "resources/icons/blue"

def __init__(self, app, icon):
super().__init__()
self.app = app
super().__init__(app)
self.icon = icon
assert isinstance(self.icon._impl.native, Bitmap)

Expand Down
3 changes: 1 addition & 2 deletions android/tests_backend/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

class ImageProbe(BaseProbe):
def __init__(self, app, image):
super().__init__()
self.app = app
super().__init__(app)
self.image = image
assert isinstance(self.image._impl.native, Bitmap)

Expand Down
90 changes: 86 additions & 4 deletions android/tests_backend/probe.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,95 @@
import asyncio

from java import dynamic_proxy
from org.beeware.android import MainActivity

from android import R
from android.view import View, ViewTreeObserver, WindowManagerGlobal
from android.widget import Button


class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)):
def __init__(self):
super().__init__()
self.event = asyncio.Event()

def onGlobalLayout(self):
self.event.set()


class BaseProbe:
async def redraw(self, message=None, delay=None):
def __init__(self, app):
self.app = app
activity = MainActivity.singletonThis
self.root_view = activity.findViewById(R.id.content)

self.layout_listener = LayoutListener()
self.root_view.getViewTreeObserver().addOnGlobalLayoutListener(
self.layout_listener
)

self.window_manager = WindowManagerGlobal.getInstance()
self.original_window_names = self.window_manager.getViewRootNames()

self.dpi = activity.getResources().getDisplayMetrics().densityDpi
self.scale_factor = self.dpi / 160

def __del__(self):
self.root_view.getViewTreeObserver().removeOnGlobalLayoutListener(
self.layout_listener
)

def get_dialog_view(self):
new_windows = [
name
for name in self.window_manager.getViewRootNames()
if name not in self.original_window_names
]
if len(new_windows) == 0:
return None
elif len(new_windows) == 1:
return self.window_manager.getRootView(new_windows[0])
else:
raise RuntimeError(f"More than one new window: {new_windows}")

def get_dialog_buttons(self, dialog_view):
button_panel = dialog_view.findViewById(R.id.button1).getParent()
return [
child
for i in range(button_panel.getChildCount())
if (
isinstance(child := button_panel.getChildAt(i), Button)
and child.getVisibility() == View.VISIBLE
)
]

def assert_dialog_buttons(self, dialog_view, captions):
assert [
str(b.getText()) for b in self.get_dialog_buttons(dialog_view)
] == captions

async def press_dialog_button(self, dialog_view, caption):
for b in self.get_dialog_buttons(dialog_view):
if str(b.getText()) == caption:
b.performClick()
await self.redraw(f"Click dialog button '{caption}'")
assert self.get_dialog_view() is None
break
else:
raise ValueError(f"Couldn't find dialog button '{caption}'")

async def redraw(self, message=None, delay=0):
"""Request a redraw of the app, waiting until that redraw has completed."""
# If we're running slow, wait for a second
if self.app.run_slow:
delay = 1
self.root_view.requestLayout()
try:
event = self.layout_listener.event
event.clear()
await asyncio.wait_for(event.wait(), 5)
except asyncio.TimeoutError:
print("Redraw timed out")

if self.app.run_slow:
delay = min(delay, 1)
if delay:
print("Waiting for redraw" if message is None else message)
await asyncio.sleep(delay)
64 changes: 2 additions & 62 deletions android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio

import pytest
from java import dynamic_proxy
from pytest import approx

from android.graphics.drawable import (
Expand All @@ -11,13 +10,7 @@
LayerDrawable,
)
from android.os import Build, SystemClock
from android.view import (
MotionEvent,
View,
ViewGroup,
ViewTreeObserver,
WindowManagerGlobal,
)
from android.view import MotionEvent, View, ViewGroup
from toga.colors import TRANSPARENT
from toga.style.pack import JUSTIFY, LEFT

Expand All @@ -26,45 +19,17 @@
from .properties import toga_color, toga_vertical_alignment


class LayoutListener(dynamic_proxy(ViewTreeObserver.OnGlobalLayoutListener)):
def __init__(self):
super().__init__()
self.event = asyncio.Event()

def onGlobalLayout(self):
self.event.set()


class SimpleProbe(BaseProbe, FontMixin):
default_font_family = "sans-serif"
default_font_size = 14

def __init__(self, widget):
super().__init__()
self.app = widget.app
super().__init__(widget.app)
self.widget = widget
self.impl = widget._impl
self.native = widget._impl.native
self.layout_listener = LayoutListener()
self.native.getViewTreeObserver().addOnGlobalLayoutListener(
self.layout_listener
)
self.window_manager = WindowManagerGlobal.getInstance()
self.original_window_names = self.window_manager.getViewRootNames()

# Store the device DPI, as it will be needed to scale some values
self.dpi = (
self.native.getContext().getResources().getDisplayMetrics().densityDpi
)
self.scale_factor = self.dpi / 160

assert isinstance(self.native, self.native_class)

def __del__(self):
self.native.getViewTreeObserver().removeOnGlobalLayoutListener(
self.layout_listener
)

def assert_container(self, container):
assert self.widget._impl.container is container._impl.container
assert self.native.getParent() is container._impl.container.native_content
Expand All @@ -85,18 +50,6 @@ def assert_alignment(self, expected):
def assert_vertical_alignment(self, expected):
assert toga_vertical_alignment(self.native.getGravity()) == expected

async def redraw(self, message=None, delay=None):
"""Request a redraw of the app, waiting until that redraw has completed."""
self.native.requestLayout()
try:
event = self.layout_listener.event
event.clear()
await asyncio.wait_for(event.wait(), 5)
except asyncio.TimeoutError:
print("Redraw timed out")

await super().redraw(message=message, delay=delay)

@property
def enabled(self):
return self.native.isEnabled()
Expand Down Expand Up @@ -172,19 +125,6 @@ def background_color(self):
else:
return TRANSPARENT

def find_dialog(self):
new_windows = [
name
for name in self.window_manager.getViewRootNames()
if name not in self.original_window_names
]
if len(new_windows) == 0:
return None
elif len(new_windows) == 1:
return self.window_manager.getRootView(new_windows[0])
else:
raise RuntimeError(f"More than one new window: {new_windows}")

async def press(self):
self.native.performClick()

Expand Down
Loading