From 3a931e8e4a2400088f0b2cb18efb5fbbdf8e3cec Mon Sep 17 00:00:00 2001 From: Asheesh Laroia Date: Sun, 26 Jul 2020 16:20:52 -0700 Subject: [PATCH 1/5] Add initial Android font support --- src/android/toga_android/factory.py | 2 + src/android/toga_android/fonts.py | 48 +++++++++++++++++++ .../toga_android/libs/android_widgets.py | 2 + src/android/toga_android/widgets/base.py | 9 ++++ src/android/toga_android/widgets/box.py | 3 ++ src/android/toga_android/widgets/label.py | 22 ++++++++- .../toga_android/widgets/numberinput.py | 5 +- src/android/toga_android/widgets/textinput.py | 17 ++++++- 8 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/android/toga_android/fonts.py diff --git a/src/android/toga_android/factory.py b/src/android/toga_android/factory.py index 9bf29967c1..c173453365 100644 --- a/src/android/toga_android/factory.py +++ b/src/android/toga_android/factory.py @@ -1,4 +1,5 @@ from .app import App, MainWindow +from .fonts import Font from .icons import Icon from .images import Image from .paths import paths @@ -24,6 +25,7 @@ def not_implemented(feature): "App", "Box", "Button", + "Font", "Icon", "Image", "ImageView", diff --git a/src/android/toga_android/fonts.py b/src/android/toga_android/fonts.py new file mode 100644 index 0000000000..ca09c88930 --- /dev/null +++ b/src/android/toga_android/fonts.py @@ -0,0 +1,48 @@ +from toga.fonts import ( + BOLD, + ITALIC, + MONOSPACE, + SANS_SERIF, + SERIF, + SYSTEM_DEFAULT_FONT_SIZE, +) + +from .libs.android_widgets import ( + Typeface, +) + + +class Font: + def __init__(self, interface): + self.interface = interface + + def get_size(self): + # Default system font size on Android is 14sp (sp = dp, but is separately + # scalable in user settings). For what it's worth, Toga's default is 12pt. + if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE: + font_size = 14 + else: + font_size = self.interface.size + return float(font_size) + + def get_style(self): + if self.interface.weight == BOLD: + if self.interface.style == ITALIC: + return Typeface.BOLD_ITALIC + else: + return Typeface.BOLD + if self.interface.style == ITALIC: + return Typeface.ITALIC + return Typeface.NORMAL + + def get_typeface(self): + if self.interface.family is SERIF: + family = Typeface.SERIF + elif self.interface.family is SANS_SERIF: + family = Typeface.SANS_SERIF + elif self.interface.family is MONOSPACE: + family = Typeface.MONOSPACE + else: + family = Typeface.DEFAULT + + return family diff --git a/src/android/toga_android/libs/android_widgets.py b/src/android/toga_android/libs/android_widgets.py index 9d24b73a93..fc46481d0e 100644 --- a/src/android/toga_android/libs/android_widgets.py +++ b/src/android/toga_android/libs/android_widgets.py @@ -23,6 +23,8 @@ Spinner = JavaClass("android/widget/Spinner") TextView = JavaClass("android/widget/TextView") TextWatcher = JavaInterface("android/text/TextWatcher") +TypedValue = JavaClass("android/util/TypedValue") +Typeface = JavaClass("android/graphics/Typeface") ViewGroup__LayoutParams = JavaClass("android/view/ViewGroup$LayoutParams") View__MeasureSpec = JavaClass("android/view/View$MeasureSpec") diff --git a/src/android/toga_android/widgets/base.py b/src/android/toga_android/widgets/base.py index 2fba7d016c..f6a51ac3de 100644 --- a/src/android/toga_android/widgets/base.py +++ b/src/android/toga_android/widgets/base.py @@ -14,6 +14,9 @@ def __init__(self, interface): # can pass it as `context` when creating native Android widgets. self._native_activity = MainActivity.singletonThis self.create() + # Immediately re-apply styles. Some widgets may defer style application until + # they have been added to a container. + self.interface.style.reapply() def set_app(self, app): pass @@ -62,6 +65,12 @@ def set_background_color(self, color): # By default, background color can't be changed. pass + def set_alignment(self, alignment): + pass # If appropriate, a widget subclass will implement this. + + def set_color(self, color): + pass # If appropriate, a widget subclass will implement this. + # INTERFACE def add_child(self, child): diff --git a/src/android/toga_android/widgets/box.py b/src/android/toga_android/widgets/box.py index 555642ea3a..8a4d2b1ac9 100644 --- a/src/android/toga_android/widgets/box.py +++ b/src/android/toga_android/widgets/box.py @@ -8,6 +8,9 @@ def create(self): self.native = RelativeLayout(MainActivity.singletonThis) def set_child_bounds(self, widget, x, y, width, height): + # Avoid setting child boundaries if `create()` has not been called. + if not widget.native: + return # We assume `widget.native` has already been added to this `RelativeLayout`. # # We use `topMargin` and `leftMargin` to perform absolute layout. Not very diff --git a/src/android/toga_android/widgets/label.py b/src/android/toga_android/widgets/label.py index 63cad0ddc4..7d84101d96 100644 --- a/src/android/toga_android/widgets/label.py +++ b/src/android/toga_android/widgets/label.py @@ -1,6 +1,6 @@ from travertino.size import at_least -from ..libs.android_widgets import Gravity, TextView, View__MeasureSpec +from ..libs.android_widgets import Gravity, TextView, TypedValue, View__MeasureSpec from .base import Widget, align @@ -12,7 +12,19 @@ def create(self): def set_text(self, value): self.native.setText(value) + def set_font(self, value): + if not value: + return + + value.bind(self.interface.factory) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, value._impl.get_size()) + self.native.setTypeface(value._impl.get_typeface(), value._impl.get_style()) + 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. + if self.native.getLayoutParams() is None: + return # Ask the Android TextView first for the height it would use in its # wildest dreams. This is the height of one line of text. self.native.measure( @@ -30,4 +42,12 @@ def rehint(self): self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) def set_alignment(self, value): + # Refuse to set alignment if create() has not been called. + if self.native is None: + return + # Refuse to set alignment if widget has no container. + # On Android, calling setGravity() when the widget has no LayoutParams + # results in a NullPointerException. + if self.native.getLayoutParams() is None: + return self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) diff --git a/src/android/toga_android/widgets/numberinput.py b/src/android/toga_android/widgets/numberinput.py index 7f407573a1..49a346715b 100644 --- a/src/android/toga_android/widgets/numberinput.py +++ b/src/android/toga_android/widgets/numberinput.py @@ -7,6 +7,7 @@ Gravity, InputType, TextWatcher, + TypedValue, View__MeasureSpec ) from .base import Widget, align @@ -71,7 +72,9 @@ def set_alignment(self, value): self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) def set_font(self, font): - self.interface.factory.not_implemented("NumberInput.set_font()") + font.bind(factory=self.interface.factory) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font._impl.get_size()) + self.native.setTypeface(font._impl.get_typeface(), font._impl.get_style()) def set_value(self, value): # Store a string in the Android widget. The `afterTextChanged` method diff --git a/src/android/toga_android/widgets/textinput.py b/src/android/toga_android/widgets/textinput.py index 182732f3b8..44cd3e8056 100644 --- a/src/android/toga_android/widgets/textinput.py +++ b/src/android/toga_android/widgets/textinput.py @@ -4,7 +4,8 @@ EditText, Gravity, TextWatcher, - View__MeasureSpec + TypedValue, + View__MeasureSpec, ) from .base import Widget, align @@ -42,10 +43,17 @@ def set_placeholder(self, value): self.native.setHint(value if value is not None else "") def set_alignment(self, value): + # Refuse to set alignment unless widget has been added to a container. + # This is because Android EditText requires LayoutParams before + # setGravity() can be called. + if self.native.getLayoutParams() is None: + return self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) def set_font(self, font): - self.interface.factory.not_implemented("TextInput.set_font()") + font.bind(factory=self.interface.factory) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font._impl.get_size()) + self.native.setTypeface(font._impl.get_typeface(), font._impl.get_style()) def set_value(self, value): self.native.setText(value) @@ -56,6 +64,11 @@ def set_on_change(self, handler): def rehint(self): self.interface.intrinsic.width = at_least(self.interface.MIN_WIDTH) + # Refuse to call measure() if widget has no container, i.e., has no LayoutParams. + # On Android, EditText's measure() throws NullPointerException if the widget has no + # LayoutParams. + if self.native.getLayoutParams() is None: + return self.native.measure( View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED ) From 6cff8c0fa5f2898180b65a1fc7ee96643ef1632d Mon Sep 17 00:00:00 2001 From: Asheesh Laroia Date: Wed, 29 Jul 2020 08:00:23 -0700 Subject: [PATCH 2/5] Add support for cursive & fantasy fonts --- src/android/toga_android/fonts.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/android/toga_android/fonts.py b/src/android/toga_android/fonts.py index ca09c88930..e5549e9f60 100644 --- a/src/android/toga_android/fonts.py +++ b/src/android/toga_android/fonts.py @@ -1,5 +1,7 @@ from toga.fonts import ( BOLD, + CURSIVE, + FANTASY, ITALIC, MONOSPACE, SANS_SERIF, @@ -42,6 +44,13 @@ def get_typeface(self): family = Typeface.SANS_SERIF elif self.interface.family is MONOSPACE: family = Typeface.MONOSPACE + elif self.interface.family is CURSIVE: + family = Typeface.create("cursive", Typeface.NORMAL) + elif self.interface.family is FANTASY: + # Android appears to not have a fantasy font available by default, + # but if it ever does, we'll start using it. Android seems to choose + # a serif font when asked for a fantasy font. + family = Typeface.create("fantasy", Typeface.NORMAL) else: family = Typeface.DEFAULT From 68946582c188e255f44a766ff2da901e2bb288bb Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 31 Jul 2020 12:42:07 +0800 Subject: [PATCH 3/5] Add explicit support for system font, plus custom fonts. --- src/android/toga_android/fonts.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/android/toga_android/fonts.py b/src/android/toga_android/fonts.py index e5549e9f60..c17e042e32 100644 --- a/src/android/toga_android/fonts.py +++ b/src/android/toga_android/fonts.py @@ -6,6 +6,7 @@ MONOSPACE, SANS_SERIF, SERIF, + SYSTEM, SYSTEM_DEFAULT_FONT_SIZE, ) @@ -38,7 +39,9 @@ def get_style(self): return Typeface.NORMAL def get_typeface(self): - if self.interface.family is SERIF: + if self.interface.family is SYSTEM: + family = Typeface.DEFAULT + elif self.interface.family is SERIF: family = Typeface.SERIF elif self.interface.family is SANS_SERIF: family = Typeface.SANS_SERIF @@ -52,6 +55,6 @@ def get_typeface(self): # a serif font when asked for a fantasy font. family = Typeface.create("fantasy", Typeface.NORMAL) else: - family = Typeface.DEFAULT + family = Typeface.create(self.interface.family, Typeface.NORMAL) return family From 168d7881e291eb01c59b593786857fe3e186e942 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 31 Jul 2020 12:43:01 +0800 Subject: [PATCH 4/5] Small cleanup to font binding handling. --- src/android/toga_android/widgets/label.py | 12 +++++------- src/android/toga_android/widgets/numberinput.py | 7 ++++--- src/android/toga_android/widgets/textinput.py | 7 ++++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/android/toga_android/widgets/label.py b/src/android/toga_android/widgets/label.py index 7d84101d96..68c54692a4 100644 --- a/src/android/toga_android/widgets/label.py +++ b/src/android/toga_android/widgets/label.py @@ -12,13 +12,11 @@ def create(self): def set_text(self, value): self.native.setText(value) - def set_font(self, value): - if not value: - return - - value.bind(self.interface.factory) - self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, value._impl.get_size()) - self.native.setTypeface(value._impl.get_typeface(), value._impl.get_style()) + def set_font(self, font): + if font: + font_impl = font.bind(self.interface.factory) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size()) + self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style()) def rehint(self): # Refuse to rehint an Android TextView if it has no LayoutParams yet. diff --git a/src/android/toga_android/widgets/numberinput.py b/src/android/toga_android/widgets/numberinput.py index 49a346715b..bd564f2425 100644 --- a/src/android/toga_android/widgets/numberinput.py +++ b/src/android/toga_android/widgets/numberinput.py @@ -72,9 +72,10 @@ def set_alignment(self, value): self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) def set_font(self, font): - font.bind(factory=self.interface.factory) - self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font._impl.get_size()) - self.native.setTypeface(font._impl.get_typeface(), font._impl.get_style()) + if font: + font_impl = font.bind(self.interface.factory) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size()) + self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style()) def set_value(self, value): # Store a string in the Android widget. The `afterTextChanged` method diff --git a/src/android/toga_android/widgets/textinput.py b/src/android/toga_android/widgets/textinput.py index 44cd3e8056..517a844a9c 100644 --- a/src/android/toga_android/widgets/textinput.py +++ b/src/android/toga_android/widgets/textinput.py @@ -51,9 +51,10 @@ def set_alignment(self, value): self.native.setGravity(Gravity.CENTER_VERTICAL | align(value)) def set_font(self, font): - font.bind(factory=self.interface.factory) - self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font._impl.get_size()) - self.native.setTypeface(font._impl.get_typeface(), font._impl.get_style()) + if font: + font_impl = font.bind(self.interface.factory) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size()) + self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style()) def set_value(self, value): self.native.setText(value) From 2c1fa585b4641ddd7afe7b0afedc1ae80732aa40 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 31 Jul 2020 12:43:14 +0800 Subject: [PATCH 5/5] Add font support for multilinetextinput. --- src/android/toga_android/widgets/multilinetextinput.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/android/toga_android/widgets/multilinetextinput.py b/src/android/toga_android/widgets/multilinetextinput.py index f2d3543f11..f67407f088 100644 --- a/src/android/toga_android/widgets/multilinetextinput.py +++ b/src/android/toga_android/widgets/multilinetextinput.py @@ -6,6 +6,7 @@ EditText, Gravity, InputType, + TypedValue, ) from .base import Widget, align @@ -32,8 +33,11 @@ def set_placeholder(self, value): def set_alignment(self, value): self.native.setGravity(Gravity.TOP | align(value)) - def set_font(self, value): - self.interface.factory.not_implemented("MutlineTextInput.set_font()") + def set_font(self, font): + if font: + font_impl = font.bind(self.interface.factory) + self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size()) + self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style()) def set_value(self, value): self.native.setText(value)