diff --git a/android/src/toga_android/libs/android/content.py b/android/src/toga_android/libs/android/content/__init__.py similarity index 100% rename from android/src/toga_android/libs/android/content.py rename to android/src/toga_android/libs/android/content/__init__.py diff --git a/android/src/toga_android/libs/android/content/res.py b/android/src/toga_android/libs/android/content/res.py new file mode 100644 index 0000000000..932d7fd4cc --- /dev/null +++ b/android/src/toga_android/libs/android/content/res.py @@ -0,0 +1,3 @@ +from rubicon.java import JavaClass + +ColorStateList = JavaClass("android/content/res/ColorStateList") diff --git a/android/src/toga_android/libs/android/graphics.py b/android/src/toga_android/libs/android/graphics.py index 69322e4c82..8ff19da30e 100644 --- a/android/src/toga_android/libs/android/graphics.py +++ b/android/src/toga_android/libs/android/graphics.py @@ -9,6 +9,5 @@ Path = JavaClass("android/graphics/Path") Path__Direction = JavaClass("android/graphics/Path$Direction") Paint__Style = JavaClass("android/graphics/Paint$Style") -PorterDuff__Mode = JavaClass("android/graphics/PorterDuff$Mode") Rect = JavaClass("android/graphics/Rect") Typeface = JavaClass("android/graphics/Typeface") diff --git a/android/src/toga_android/widgets/box.py b/android/src/toga_android/widgets/box.py index f7f194029d..9727024fd6 100644 --- a/android/src/toga_android/widgets/box.py +++ b/android/src/toga_android/widgets/box.py @@ -24,6 +24,7 @@ def set_child_bounds(self, widget, x, y, width, height): self.native.updateViewLayout(widget.native, layout_params) def set_background_color(self, value): - self.native.setBackgroundColor( - native_color(TRANSPARENT if (value is None) else value) - ) + if value is None: + self.native.setBackgroundColor(native_color(TRANSPARENT)) + else: + self.native.setBackgroundColor(native_color(value)) diff --git a/android/src/toga_android/widgets/button.py b/android/src/toga_android/widgets/button.py index ab9bfab545..5c1ede821c 100644 --- a/android/src/toga_android/widgets/button.py +++ b/android/src/toga_android/widgets/button.py @@ -1,8 +1,9 @@ from travertino.size import at_least +from toga.colors import TRANSPARENT from toga_android.colors import native_color -from ..libs.android.graphics import PorterDuff__Mode +from ..libs.android.content.res import ColorStateList from ..libs.android.util import TypedValue from ..libs.android.view import OnClickListener, View__MeasureSpec from ..libs.android.widget import Button as A_Button @@ -42,14 +43,20 @@ def set_on_press(self, handler): pass def set_color(self, value): - if value: + if value is None: + pass + # TODO: set to system foreground + # self.native.setTextColor(native_color(value)) + else: self.native.setTextColor(native_color(value)) def set_background_color(self, value): - if value: + if value is None or value == TRANSPARENT: + self.native.setBackgroundTintList(None) + else: # do not use self.native.setBackgroundColor - this messes with the button style! - self.native.getBackground().setColorFilter( - native_color(value), PorterDuff__Mode.MULTIPLY + self.native.setBackgroundTintList( + ColorStateList.valueOf(native_color(value)) ) def rehint(self): diff --git a/android/src/toga_android/widgets/label.py b/android/src/toga_android/widgets/label.py index c80f321da1..2d7b46afa3 100644 --- a/android/src/toga_android/widgets/label.py +++ b/android/src/toga_android/widgets/label.py @@ -1,5 +1,6 @@ from travertino.size import at_least +from toga.colors import TRANSPARENT from toga_android.colors import native_color from ..libs.android.util import TypedValue @@ -16,14 +17,23 @@ def set_text(self, value): self.native.setText(value) def set_font(self, font): - if font: - self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font._impl.get_size()) - self.native.setTypeface(font._impl.get_typeface(), font._impl.get_style()) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font._impl.get_size()) + self.native.setTypeface(font._impl.get_typeface(), font._impl.get_style()) def set_color(self, color): - if color: + if color is None: + pass + # TODO: set to system foreground + # self.native.setTextColor(native_color(value)) + else: self.native.setTextColor(native_color(color)) + def set_background_color(self, value): + if value is None: + self.native.setBackgroundColor(native_color(TRANSPARENT)) + else: + self.native.setBackgroundColor(native_color(value)) + def rehint(self): # Refuse to rehint an Android TextView if it has no LayoutParams yet. # Calling measure() on an Android TextView w/o LayoutParams raises NullPointerException. diff --git a/android/tests_backend/widgets/base.py b/android/tests_backend/widgets/base.py index dc89eb436d..cf9795c2fd 100644 --- a/android/tests_backend/widgets/base.py +++ b/android/tests_backend/widgets/base.py @@ -7,6 +7,12 @@ class SimpleProbe: def __init__(self, widget): self.widget = widget self.native = widget._impl.native + + # Store the device DPI, as it will be needed to scale some values + self.dpi = ( + self.native.getContext().getResources().getDisplayMetrics().densityDpi + ) + assert isinstance(self.native, self.native_class) def assert_container(self, container): diff --git a/android/tests_backend/widgets/button.py b/android/tests_backend/widgets/button.py index d707f8374d..f8580b517f 100644 --- a/android/tests_backend/widgets/button.py +++ b/android/tests_backend/widgets/button.py @@ -1,8 +1,19 @@ from java import jclass +from toga_android.libs.android import R__attr + from .label import LabelProbe +from .properties import toga_color # On Android, a Button is just a TextView with a state-dependent background image. class ButtonProbe(LabelProbe): native_class = jclass("android.widget.Button") + + @property + def background_color(self): + tint_list = self.native.getBackgroundTintList() + if tint_list: + return toga_color(tint_list.getColorForState([R__attr.state_enabled], 0)) + else: + return None diff --git a/android/tests_backend/widgets/label.py b/android/tests_backend/widgets/label.py index 9112abb49e..af662e7969 100644 --- a/android/tests_backend/widgets/label.py +++ b/android/tests_backend/widgets/label.py @@ -2,7 +2,7 @@ from pytest import skip from .base import SimpleProbe -from .properties import toga_color +from .properties import toga_color, toga_font class LabelProbe(SimpleProbe): @@ -12,13 +12,17 @@ class LabelProbe(SimpleProbe): def color(self): return toga_color(self.native.getCurrentTextColor()) + @property + def background_color(self): + return toga_color(self.native.getBackgroundColor()) + @property def text(self): return str(self.native.getText()) @property def font(self): - skip("Font probe not implemented") + return toga_font(self.native.getTypeface(), self.native.getTextSize(), self.dpi) @property def alignment(self): diff --git a/android/tests_backend/widgets/properties.py b/android/tests_backend/widgets/properties.py index 95a56903dd..cd4002c6a8 100644 --- a/android/tests_backend/widgets/properties.py +++ b/android/tests_backend/widgets/properties.py @@ -1,7 +1,9 @@ from java import jint +from travertino.fonts import Font -from android.graphics import Color +from android.graphics import Color, Typeface from toga.colors import rgba +from toga.fonts import BOLD, ITALIC, MONOSPACE, NORMAL, SANS_SERIF, SERIF, SYSTEM def toga_color(color_int): @@ -13,3 +15,24 @@ def toga_color(color_int): Color.blue(color_int), Color.alpha(color_int) / 255, ) + + +def toga_font(typeface, size, dpi): + # Android provides font details in pixels; that size needs to be converted + # using the ratio between the display DPI and the baseline DPI (160). + size_in_points = size / (dpi * 160) + + return Font( + family={ + Typeface.DEFAULT: SYSTEM, + Typeface.MONOSPACE: MONOSPACE, + Typeface.SANS_SERIF: SANS_SERIF, + Typeface.SERIF: SERIF, + # : CURSIVE ?? + # : FANTASY ?? + }.get(typeface, "Unknown"), + size=size_in_points, + style=ITALIC if typeface.isItalic() else NORMAL, + variant=NORMAL, + weight=BOLD if typeface.isBold() else NORMAL, + ) diff --git a/iOS/tests_backend/widgets/properties.py b/iOS/tests_backend/widgets/properties.py index 9fdd70671d..a717fe89e4 100644 --- a/iOS/tests_backend/widgets/properties.py +++ b/iOS/tests_backend/widgets/properties.py @@ -1,7 +1,7 @@ from ctypes import byref -from dataclasses import dataclass from rubicon.objc import CGFloat +from travertino.fonts import Font from toga.colors import rgba from toga.fonts import FANTASY, NORMAL, SYSTEM @@ -28,15 +28,6 @@ def toga_color(color): return None -@dataclass -class Font: - family: str - size: int - style: str = NORMAL - variant: str = NORMAL - weight: str = NORMAL - - def toga_font(font): return Font( family={ @@ -44,6 +35,9 @@ def toga_font(font): "Papyrus": FANTASY, }.get(str(font.familyName), str(font.familyName)), size=font.pointSize, + style=NORMAL, # TODO: ITALIC if..., SMALL_CAPS if ... + variant=NORMAL, + weight=NORMAL, # TODO: BOLD if ... ) diff --git a/testbed/pyproject.toml b/testbed/pyproject.toml index 0392c89485..aff4d0a428 100644 --- a/testbed/pyproject.toml +++ b/testbed/pyproject.toml @@ -22,7 +22,7 @@ requires = [ "../core", ] test_requires = [ - "coverage==7.0.5", + "coverage==7.2.0", "pytest==7.2.0", "pytest-asyncio==0.20.3", ] @@ -88,7 +88,7 @@ test_sources = [ "../android/tests_backend", ] requires = [ - "../android" + "../android", ] # Coverage requires access to individual .py files. build_gradle_extra_content = "android.defaultConfig.python.extractPackages 'toga_android'" diff --git a/testbed/tests/widgets/properties.py b/testbed/tests/widgets/properties.py index 06acb3d912..b47ddbd9a7 100644 --- a/testbed/tests/widgets/properties.py +++ b/testbed/tests/widgets/properties.py @@ -1,8 +1,5 @@ -from pytest import mark - from toga.colors import RED, TRANSPARENT, color as named_color from toga.fonts import FANTASY -from toga.platform import current_platform from ..assertions import assert_color from ..data import COLORS, TEXTS @@ -18,10 +15,6 @@ async def test_text(widget, probe): assert probe.text == text -@mark.skipif( - current_platform in {"android"}, - reason="text width resizes don't work", -) async def test_text_width_change(widget, probe): "If the widget text is changed, the width of the widget changes" orig_width = probe.width @@ -76,10 +69,6 @@ async def test_color(widget, probe): assert_color(probe.color, color) -@mark.skipif( - current_platform in {"android"}, - reason="color resets don't work", -) async def test_color_reset(widget, probe): "The foreground color of a widget can be reset" # Get the original color