Skip to content

Commit

Permalink
Merge pull request #2058 from freakboy3742/audit-window
Browse files Browse the repository at this point in the history
[widget Audit] toga.Window
  • Loading branch information
freakboy3742 authored Oct 17, 2023
2 parents 4b335e3 + 270d009 commit b81f1ef
Show file tree
Hide file tree
Showing 67 changed files with 3,257 additions and 1,262 deletions.
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

0 comments on commit b81f1ef

Please sign in to comment.