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.Button #1761

Merged
merged 66 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
92fd082
Whitespace change to use as the starting point for a PR.
freakboy3742 Jan 31, 2023
d206715
Add changenote.
freakboy3742 Jan 31, 2023
9fdf46e
Fix tracing on Windows
mhsmith Jan 31, 2023
38bd4d6
Migrate dummy widget tools to pytest.
freakboy3742 Feb 1, 2023
b541c09
Remove use of shadow attribute for button label.
freakboy3742 Feb 1, 2023
7077562
Convert Button tests to pytest.
freakboy3742 Feb 1, 2023
ec04ddc
Fix Android tests, get to 100% coverage.
freakboy3742 Feb 1, 2023
859ad48
Fix get_text implementation on gtk and winforms.
freakboy3742 Feb 1, 2023
f93158e
100% coverage on iOS.
freakboy3742 Feb 1, 2023
67f3374
Remove deprecated code from Button.
freakboy3742 Feb 1, 2023
ca8a760
Winforms to 100%.
freakboy3742 Feb 1, 2023
8a03783
GTK to 100%.
freakboy3742 Feb 1, 2023
37985ae
Winforms to 100%, no skips.
freakboy3742 Feb 2, 2023
2b9714e
Add test selection from the command line, and a slow mode.
freakboy3742 Feb 3, 2023
2ea497e
Fix and mark tests that aren't reliable
freakboy3742 Feb 6, 2023
c8e41b1
Updated dummy assertions to be simple functions.
freakboy3742 Feb 6, 2023
a07ebde
GTK Button color tests passing.
freakboy3742 Feb 7, 2023
e6c3096
Add a warning about UI animations.
freakboy3742 Feb 8, 2023
163fa97
Re-enable the button size test.
freakboy3742 Feb 16, 2023
8c24cbe
Correct handling of color for non-button widgets.
freakboy3742 Feb 16, 2023
df7c13e
Remove refresh calls that aren't needed any more.
freakboy3742 Feb 16, 2023
d1fdf8b
Removed the xfail marker from some tests.
freakboy3742 Feb 16, 2023
2e7f8d0
Modified the color test to assert all components in one assertion.
freakboy3742 Feb 20, 2023
da9c1ba
Use a programmatic approach to disable animations.
freakboy3742 Feb 20, 2023
50c4fee
iOS Button tests to 100%.
freakboy3742 Feb 22, 2023
4a2944f
iOS button to 100%.
freakboy3742 Feb 24, 2023
fe92bb9
Partial fixes for Android colors and fonts.
freakboy3742 Feb 24, 2023
73f0c5a
Implement `await redraw` for Android
mhsmith Feb 26, 2023
2ff0e95
Fix Android font size calculation
mhsmith Feb 26, 2023
e6ed1c1
Fix button background color
mhsmith Feb 27, 2023
db63ecc
Modify font size and font handling.
freakboy3742 Feb 28, 2023
493e722
Add support for resetting foreground color.
freakboy3742 Feb 28, 2023
c3d5c8a
Ensure size probes return SP not DP.
freakboy3742 Feb 28, 2023
542faf7
Use del rather than assigning None to clear style.
freakboy3742 Feb 28, 2023
d3581ec
Don't allow newlines in button text.
freakboy3742 Feb 28, 2023
e1458ba
Correct label color tests.
freakboy3742 Feb 28, 2023
32451bd
Button at 100% on all tested platforms!
freakboy3742 Feb 28, 2023
c788a8c
Rework the EventLog so it is global.
freakboy3742 Feb 28, 2023
da5105a
Disallow empty labels on buttons.
freakboy3742 Feb 28, 2023
5bf1cbb
Revert a libs restructure.
freakboy3742 Feb 28, 2023
29924f0
More tweaks to the dummy test assertions.
freakboy3742 Feb 28, 2023
7e2bc2d
Removed a redundant background color implementation.
freakboy3742 Feb 28, 2023
8211f8b
Add notes about button text restrictions.
freakboy3742 Feb 28, 2023
8cd8531
Merge branch 'main' into audit-button
freakboy3742 Mar 5, 2023
857d899
Change `= None` tests to `del`, for compatibility with travertino mai…
mhsmith Mar 13, 2023
7bd87a5
Fix Android font size, and add font size reset test
mhsmith Mar 13, 2023
16ec542
Add memory retention for cached font instances.
freakboy3742 Mar 13, 2023
8bd9528
Merge branch 'system-ci' into audit-button
mhsmith Mar 14, 2023
e83f2fa
Allow default font to be returned as SANS_SERIF as well as SYSTEM
mhsmith Mar 14, 2023
81e2d29
Add examples/font_size
mhsmith Mar 14, 2023
02a345d
Add bold and italic font tests (working on Android, Cocoa and iOS)
mhsmith Mar 16, 2023
9248973
Add bold and italic font tests (working on GTK and Winforms)
mhsmith Mar 17, 2023
45e52e6
Remove redundant set_font methods in dummy widgets
mhsmith Mar 17, 2023
8445dae
Merge branch 'main' into audit-button
freakboy3742 Mar 20, 2023
c646ef0
Improve comments
mhsmith Mar 20, 2023
e2da16b
Move generic font mapping assertions to tests_backend (Android, Cocoa…
mhsmith Mar 20, 2023
926c3d6
Move generic font mapping assertions to tests_backend (GTK)
mhsmith Mar 20, 2023
7f96efa
Move generic font mapping assertions to tests_backend (Winforms)
mhsmith Mar 20, 2023
36ed554
Fix typos
mhsmith Mar 20, 2023
091e3ec
Allow Button text to be set to an empty string, and make Winforms dis…
mhsmith Mar 20, 2023
dd73a9b
Use subclassing rather than an isinstance check.
freakboy3742 Mar 20, 2023
cfdc3e5
Revert the use of an interface state variable for button text.
freakboy3742 Mar 21, 2023
ce54201
Mark some classes with Test in the name as not-tests.
freakboy3742 Mar 21, 2023
5abe2f1
Tweak section heading.
freakboy3742 Mar 21, 2023
f76c90a
Modify the winforms button probe to do ZWS normalization.
freakboy3742 Mar 21, 2023
8f55716
Use correct zero width space character, and remove unnecessary workar…
mhsmith Mar 21, 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
86 changes: 52 additions & 34 deletions android/src/toga_android/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
SYSTEM_DEFAULT_FONT_SIZE,
)
from toga_android.libs.android.graphics import Typeface
from toga_android.libs.android.util import TypedValue

_FONT_CACHE = {}

Expand All @@ -21,31 +22,27 @@ 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 apply(self, tv, default_size, default_typeface):
"""Apply the font to the given native widget.

def get_style(self):
if self.interface.weight == BOLD:
if self.interface.style == ITALIC:
return Typeface.BOLD_ITALIC
else:
return Typeface.BOLD
elif self.interface.style == ITALIC:
return Typeface.ITALIC
:param tv: A native instance of TextView, or one of its subclasses.
freakboy3742 marked this conversation as resolved.
Show resolved Hide resolved
:param default_size: The default font size of this widget, in pixels.
:param default_typeface: The default Typeface of this widget.
"""
if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE:
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, default_size)
else:
return Typeface.NORMAL
# The default size for most widgets is 14sp, so mapping 1 Toga "point" to 1sp
# will give relative sizes that are consistent with desktop platforms. Using
# SP means font sizes will all change proportionately if the user adjusts the
# text size in the system settings.
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, self.interface.size)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely a good approximation; I only wonder if it might be worth introducing a fudge factor so that "12pt == 14sp" (i.e., "size in toga points" * 1.1666 == size in dp).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(for the record - this is on the "strong opinion, weakly held" list; I won't give much pushback on a different fudge factor, or no fudge factor at all).

Copy link
Member

@mhsmith mhsmith Mar 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows defaults to 8 points (see above), and according to the fonts.py files in the respective backends, Cocoa defaults to 12 points and iOS 17. So I don't see any strong reason to fix 12 as the default. Android is already within the range of the other platforms, and making 1 Toga pt = 1sp brings it closer to iOS, which is probably the one it's most useful to be consistent with.

In the long run I think the best solution to this issue is the CSS font size keywords, as mentioned above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - that makes sense.


def get_typeface(self):
cache_key = (self.interface, default_typeface)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the default typeface part of the cache key?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it may affect the resulting font when font_family is SYSTEM. The default font on most widgets is sans-serif, but on buttons it's sans-serif-medium, which is intermediate between normal and bold.

try:
family = _FONT_CACHE[self.interface]
typeface = _FONT_CACHE[cache_key]
except KeyError:
family = None
typeface = None
font_key = self.interface.registered_font_key(
self.interface.family,
weight=self.interface.weight,
Expand All @@ -57,36 +54,57 @@ def get_typeface(self):
self.interface.factory.paths.app / _REGISTERED_FONT_CACHE[font_key]
)
if os.path.isfile(font_path):
family = Typeface.createFromFile(font_path)
typeface = Typeface.createFromFile(font_path)
# If the typeface cannot be created, following Exception is thrown:
# E/Minikin: addFont failed to create font, invalid request
# It does not kill the app, but there is currently no way to
# catch this Exception on Android
else:
print(f"Registered font path {font_path!r} could not be found")

if family is None:
if typeface is None:
if self.interface.family is SYSTEM:
family = Typeface.DEFAULT
# The default button font is not marked as bold, but it has a weight
# of "medium" (500), which is in between "normal" (400), and "bold"
# (600 or 700). To preserve this, we use the widget's original
# typeface as a starting point rather than Typeface.DEFAULT.
typeface = default_typeface
elif self.interface.family is SERIF:
family = Typeface.SERIF
typeface = Typeface.SERIF
elif self.interface.family is SANS_SERIF:
family = Typeface.SANS_SERIF
typeface = Typeface.SANS_SERIF
elif self.interface.family is MONOSPACE:
family = Typeface.MONOSPACE
typeface = Typeface.MONOSPACE
elif self.interface.family is CURSIVE:
family = Typeface.create("cursive", Typeface.NORMAL)
typeface = 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)
typeface = Typeface.create("fantasy", Typeface.NORMAL)
else:
family = Typeface.create(self.interface.family, Typeface.NORMAL)
typeface = Typeface.create(self.interface.family, Typeface.NORMAL)

native_style = typeface.getStyle()
if self.interface.weight is not None:
native_style = set_bits(
native_style, Typeface.BOLD, self.interface.weight == BOLD
)
if self.interface.style is not None:
native_style = set_bits(
native_style, Typeface.ITALIC, self.interface.style == ITALIC
)
if native_style != typeface.getStyle():
typeface = Typeface.create(typeface, native_style)

_FONT_CACHE[cache_key] = typeface

tv.setTypeface(typeface)

family = (
family.__global__()
) # store a JNI global reference to prevent objects from becoming stale
_FONT_CACHE[self.interface] = family

return family
def set_bits(input, mask, enable=True):
if enable:
output = input | mask
else:
output = input & ~mask
return output
1 change: 1 addition & 0 deletions android/src/toga_android/libs/android/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
Path__Direction = JavaClass("android/graphics/Path$Direction")
Paint__Style = JavaClass("android/graphics/Paint$Style")
PorterDuff__Mode = JavaClass("android/graphics/PorterDuff$Mode")
PorterDuffColorFilter = JavaClass("android/graphics/PorterDuffColorFilter")
Rect = JavaClass("android/graphics/Rect")
Typeface = JavaClass("android/graphics/Typeface")
7 changes: 4 additions & 3 deletions android/src/toga_android/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,10 @@ def set_background_color(self, color):
# a default implementation because it often overwrites other aspects of the widget's
# appearance.
def set_background_color_simple(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))

def set_alignment(self, alignment):
pass # If appropriate, a widget subclass will implement this.
Expand Down
34 changes: 15 additions & 19 deletions android/src/toga_android/widgets/button.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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.util import TypedValue
from ..libs.android.graphics import PorterDuff__Mode, PorterDuffColorFilter
from ..libs.android.view import OnClickListener, View__MeasureSpec
from ..libs.android.widget import Button as A_Button
from .base import Widget
from .label import TextViewWidget


class TogaOnClickListener(OnClickListener):
Expand All @@ -19,36 +19,32 @@ def onClick(self, _view):
self.button_impl.interface.on_press(widget=self.button_impl.interface)


class Button(Widget):
class Button(TextViewWidget):
def create(self):
self.native = A_Button(self._native_activity)
self.native.setOnClickListener(TogaOnClickListener(button_impl=self))
self.cache_textview_defaults()

def get_text(self):
return str(self.native.getText())

def set_text(self, text):
self.native.setText(self.interface.text)
self.native.setText(text)

def set_enabled(self, value):
self.native.setEnabled(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())

def set_on_press(self, handler):
# No special handling required
pass

def set_color(self, value):
if value:
self.native.setTextColor(native_color(value))

def set_background_color(self, value):
if value:
# do not use self.native.setBackgroundColor - this messes with the button style!
self.native.getBackground().setColorFilter(
native_color(value), PorterDuff__Mode.MULTIPLY
)
# Do not use self.native.setBackgroundColor - this messes with the button style!
self.native.getBackground().setColorFilter(
None
if value is None or value == TRANSPARENT
else PorterDuffColorFilter(native_color(value), PorterDuff__Mode.SRC_IN)
)

def rehint(self):
# Like other text-viewing widgets, Android crashes when rendering
Expand Down
29 changes: 18 additions & 11 deletions android/src/toga_android/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,38 @@

from toga_android.colors import native_color

from ..libs.android.util import TypedValue
from ..libs.android.view import Gravity, View__MeasureSpec
from ..libs.android.widget import TextView
from .base import Widget, align


class Label(Widget):
class TextViewWidget(Widget):
def cache_textview_defaults(self):
self._default_text_color = self.native.getCurrentTextColor()
self._default_text_size = self.native.getTextSize()
self._default_typeface = self.native.getTypeface()

def set_font(self, font):
font._impl.apply(self.native, self._default_text_size, self._default_typeface)

def set_color(self, value):
if value is None:
self.native.setTextColor(self._default_text_color)
else:
self.native.setTextColor(native_color(value))


class Label(TextViewWidget):
def create(self):
self.native = TextView(self._native_activity)
self.cache_textview_defaults()

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())

def set_background_color(self, value):
self.set_background_color_simple(value)

def set_color(self, color):
if color:
self.native.setTextColor(native_color(color))

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.
Expand Down
17 changes: 4 additions & 13 deletions android/src/toga_android/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from travertino.size import at_least

from toga.constants import LEFT
from toga_android.colors import native_color

from ..libs.android.text import InputType, TextWatcher
from ..libs.android.util import TypedValue
from ..libs.android.view import Gravity
from ..libs.android.widget import EditText
from .base import Widget, align
from .base import align
from .label import TextViewWidget


class TogaTextWatcher(TextWatcher):
Expand All @@ -27,7 +26,7 @@ def onTextChanged(self, _charSequence, _start, _before, _count):
pass


class MultilineTextInput(Widget):
class MultilineTextInput(TextViewWidget):
def create(self):
self._textChangedListener = None
self.native = EditText(self._native_activity)
Expand All @@ -36,6 +35,7 @@ def create(self):
)
# Set default alignment
self.set_alignment(LEFT)
self.cache_textview_defaults()

def get_value(self):
return self.native.getText().toString()
Expand All @@ -55,15 +55,6 @@ def set_alignment(self, value):
return
self.native.setGravity(Gravity.TOP | align(value))

def set_color(self, color):
if color:
self.native.setTextColor(native_color(color))

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())

def set_value(self, value):
self.native.setText(value)

Expand Down
12 changes: 4 additions & 8 deletions android/src/toga_android/widgets/numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from travertino.size import at_least

from ..libs.android.text import InputType, TextWatcher
from ..libs.android.util import TypedValue
from ..libs.android.view import Gravity, View__MeasureSpec
from ..libs.android.widget import EditText
from .base import Widget, align
from .base import align
from .label import TextViewWidget


def decimal_from_string(s):
Expand Down Expand Up @@ -48,7 +48,7 @@ def onTextChanged(self, _charSequence, _start, _before, _count):
pass


class NumberInput(Widget):
class NumberInput(TextViewWidget):
def create(self):
self.native = EditText(self._native_activity)
self.native.addTextChangedListener(TogaNumberInputWatcher(self))
Expand All @@ -59,6 +59,7 @@ def create(self):
| InputType.TYPE_NUMBER_FLAG_DECIMAL
| InputType.TYPE_NUMBER_FLAG_SIGNED
)
self.cache_textview_defaults()

def set_readonly(self, value):
self.native.setFocusable(not value)
Expand All @@ -70,11 +71,6 @@ def set_placeholder(self, value):
def set_alignment(self, value):
self.native.setGravity(Gravity.CENTER_VERTICAL | align(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())

def set_value(self, value):
# Store a string in the Android widget. The `afterTextChanged` method
# will call the user on_change handler.
Expand Down
11 changes: 3 additions & 8 deletions android/src/toga_android/widgets/switch.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from travertino.size import at_least

from ..libs.android.util import TypedValue
from ..libs.android.view import View__MeasureSpec
from ..libs.android.widget import (
CompoundButton__OnCheckedChangeListener,
Switch as A_Switch,
)
from .base import Widget
from .label import TextViewWidget


class OnCheckedChangeListener(CompoundButton__OnCheckedChangeListener):
Expand All @@ -19,10 +18,11 @@ def onCheckedChanged(self, _button, _checked):
self._impl.interface.on_change(widget=self._impl.interface)


class Switch(Widget):
class Switch(TextViewWidget):
def create(self):
self.native = A_Switch(self._native_activity)
self.native.setOnCheckedChangeListener(OnCheckedChangeListener(self))
self.cache_textview_defaults()

def set_text(self, text):
# When changing the text, Android needs a `setSingleLine(False)` call in order
Expand All @@ -39,11 +39,6 @@ def set_value(self, value):
def get_value(self):
return self.native.isChecked()

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())

def set_on_change(self, handler):
# No special handling required
pass
Expand Down
Loading