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.Label #1799

Merged
merged 24 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bb522f4
Update tests for label.
freakboy3742 Mar 1, 2023
7ff8548
Add Android text alignment probe, and support for text justification.
freakboy3742 Mar 1, 2023
4363a98
Correct label implementation on iOS.
freakboy3742 Mar 1, 2023
af06298
Migrate label tests to pytest.
freakboy3742 Mar 1, 2023
71b523c
Update documentation for Label.
freakboy3742 Mar 1, 2023
35f3f6c
Add changenote for label bugfix.
freakboy3742 Mar 1, 2023
0a83350
Add changenote for Label audit.
freakboy3742 Mar 1, 2023
4238cb6
Add GTK alignment probe.
freakboy3742 Mar 1, 2023
d0b3da0
Update widget support chart.
freakboy3742 Mar 1, 2023
2550362
Merge branch 'audit-button' into audit-label
freakboy3742 Mar 20, 2023
4476d98
Merge branch 'audit-button' into audit-label
freakboy3742 Mar 21, 2023
1d5f74e
Merge branch 'main' into audit-label
mhsmith Mar 21, 2023
4691de8
Tighten up Android alignment tests
mhsmith Mar 21, 2023
1b70eee
Remove change note for issue which may not be completely fixed yet
mhsmith Mar 21, 2023
cdedf68
Remove unnecessary skips in base probes
mhsmith Mar 21, 2023
0f798cc
iOS: round up size in Label.rehint to prevent lines being omitted
mhsmith Mar 21, 2023
025259d
Add empty label height test
mhsmith Mar 21, 2023
f2fb171
Remove the use of shadow handling in label.
freakboy3742 Mar 22, 2023
28d3704
Add test resilience against changes in text length.
freakboy3742 Mar 22, 2023
572b07b
Correct handling of empty labels on iOS.
freakboy3742 Mar 22, 2023
7864c97
Correct the Pango negative size warning.
freakboy3742 Mar 22, 2023
2ffd585
Correct probe/API handling of ZWS.
freakboy3742 Mar 22, 2023
879f63e
Remove a redundant documentation note.
freakboy3742 Mar 22, 2023
15c3515
Relax width and height constraint during iOS rehinting to avoid word …
freakboy3742 Mar 22, 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
2 changes: 2 additions & 0 deletions android/src/toga_android/libs/android/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
PorterDuffColorFilter = JavaClass("android/graphics/PorterDuffColorFilter")
Rect = JavaClass("android/graphics/Rect")
Typeface = JavaClass("android/graphics/Typeface")

LineBreaker = JavaClass("android/graphics/text/LineBreaker")
3 changes: 3 additions & 0 deletions android/src/toga_android/libs/android/os.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from rubicon.java import JavaClass

Build = JavaClass("android/os/Build")
15 changes: 12 additions & 3 deletions android/src/toga_android/widgets/label.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from travertino.size import at_least

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

from ..libs.android.graphics import LineBreaker
from ..libs.android.os import Build
from ..libs.android.view import Gravity, View__MeasureSpec
from ..libs.android.widget import TextView
from .base import Widget, align
Expand All @@ -28,6 +31,9 @@ def create(self):
self.native = TextView(self._native_activity)
self.cache_textview_defaults()

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

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

Expand All @@ -54,12 +60,15 @@ 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 not self.native.getLayoutParams():
return

# Justified text wasn't added until Android O (SDK 26)
if value == JUSTIFY and Build.VERSION.SDK_INT >= Build.VERSION_CODES.O:
self.native.setJustificationMode(LineBreaker.JUSTIFICATION_MODE_INTER_WORD)
else:
self.native.setJustificationMode(LineBreaker.JUSTIFICATION_MODE_NONE)
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))
Copy link
Member

@mhsmith mhsmith Mar 21, 2023

Choose a reason for hiding this comment

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

With the previous code, I wasn't sure whether setting a label to centered and then to justified would give you a result with short lines still centered. Turns out it doesn't, because the native widget's horizontal gravity defaults to LEFT if unspecified, but this isn't clearly documented, so it's better to be explicit.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I follow what you're saying here. Isn't this setting horizontal gravity every time?

Copy link
Member

Choose a reason for hiding this comment

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

It is now, but previously it was leaving the horizontal gravity unspecified when value == JUSTIFY.

17 changes: 2 additions & 15 deletions android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import asyncio

from java import dynamic_proxy
from pytest import skip

from android.view import ViewTreeObserver
from toga.fonts import SYSTEM
Expand Down Expand Up @@ -48,8 +47,8 @@ def assert_container(self, container):
else:
raise AssertionError(f"cannot find {self.native} in {container_native}")

def assert_alignment_equivalent(self, actual, expected):
assert actual == expected
def assert_alignment(self, expected):
assert self.alignment == expected

def assert_font_family(self, expected):
actual = self.font.family
Expand All @@ -71,18 +70,6 @@ async def redraw(self):
def enabled(self):
return self.native.isEnabled()

@property
def background_color(self):
skip("not implemented: background_color")

@property
def color(self):
skip("not implemented: color")

@property
def hidden(self):
skip("not implemented: hidden")

@property
def width(self):
# Return the value in DP
Expand Down
7 changes: 4 additions & 3 deletions android/tests_backend/widgets/label.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from java import jclass
from pytest import skip

from .base import SimpleProbe
from .properties import toga_color, toga_font
from .properties import toga_alignment, toga_color, toga_font


class LabelProbe(SimpleProbe):
Expand Down Expand Up @@ -30,4 +29,6 @@ def font(self):

@property
def alignment(self):
skip("Alignment probe not implemented")
return toga_alignment(
self.native.getGravity(), self.native.getJustificationMode()
)
20 changes: 20 additions & 0 deletions android/tests_backend/widgets/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
from travertino.fonts import Font

from android.graphics import Color, Typeface
from android.graphics.text import LineBreaker
from android.util import TypedValue
from android.view import Gravity
from toga.colors import TRANSPARENT, rgba
from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT
from toga.fonts import (
BOLD,
ITALIC,
Expand Down Expand Up @@ -62,3 +65,20 @@ def toga_font(typeface, size, resources):
variant=NORMAL,
weight=BOLD if typeface.isBold() else NORMAL,
)


def toga_alignment(gravity, justification_mode):
horizontal_gravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK
if (
justification_mode == LineBreaker.JUSTIFICATION_MODE_INTER_WORD
and horizontal_gravity == Gravity.LEFT
):
return JUSTIFY
elif justification_mode == LineBreaker.JUSTIFICATION_MODE_NONE:
return {
Gravity.LEFT: LEFT,
Gravity.RIGHT: RIGHT,
Gravity.CENTER_HORIZONTAL: CENTER,
}[horizontal_gravity]
else:
raise ValueError(f"unknown combination: {gravity=}, {justification_mode=}")
1 change: 1 addition & 0 deletions changes/1501.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
iOS now supports newlines in Labels.
1 change: 1 addition & 0 deletions changes/1799.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Label widget now has 100% test coverage, and complete API documentation.
5 changes: 4 additions & 1 deletion cocoa/src/toga_cocoa/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ def set_color(self, value):
def set_font(self, font):
self.native.font = font._impl.native

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

def set_text(self, value):
self.native.stringValue = self.interface._text
self.native.stringValue = value

def rehint(self):
# Width & height of a label is known and fixed.
Expand Down
4 changes: 2 additions & 2 deletions cocoa/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ def assert_container(self, container):
else:
raise ValueError(f"cannot find {self.native} in {container_native}")

def assert_alignment_equivalent(self, actual, expected):
assert actual == expected
def assert_alignment(self, expected):
assert self.alignment == expected

def assert_font_family(self, expected):
assert self.font.family == {
Expand Down
33 changes: 13 additions & 20 deletions core/src/toga/widgets/label.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import warnings

from .base import Widget


Expand All @@ -9,7 +7,6 @@ def __init__(
text,
id=None,
style=None,
factory=None, # DEPRECATED!
):
"""A text label.

Expand All @@ -19,35 +16,31 @@ def __init__(
:param id: The ID for the widget.
:param style: A style object. If no style is provided, a default style
will be applied to the widget.
:param factory: *Deprecated*
"""
super().__init__(id=id, style=style)

######################################################################
# 2022-09: Backwards compatibility
######################################################################
# factory no longer used
if factory:
warnings.warn("The factory argument is no longer used.", DeprecationWarning)
######################################################################
# End backwards compatibility.
######################################################################

# Create a platform specific implementation of a Label
self._impl = self.factory.Label(interface=self)

self.text = text

@property
def text(self):
"""The text displayed by the label."""
return self._text
"""The text displayed by the label.

``None``, and the Unicode codepoint U+200B (ZERO WIDTH SPACE), will be
interpreted and returned as an empty string. Any other object will be
converted to a string using ``str()``.

"""
return self._impl.get_text()

@text.setter
def text(self, value):
if value is None:
self._text = ""
if value is None or value == "\u200B":
text = ""
else:
self._text = str(value)
self._impl.set_text(value)
text = str(value)

self._impl.set_text(text)
self.refresh()
6 changes: 0 additions & 6 deletions core/tests/test_deprecated_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,6 @@ def test_image_view_created(self):
self.assertEqual(widget._impl.interface, widget)
self.assertNotEqual(widget.factory, self.factory)

def test_label_created(self):
with self.assertWarns(DeprecationWarning):
widget = toga.Label("Test", factory=self.factory)
self.assertEqual(widget._impl.interface, widget)
self.assertNotEqual(widget.factory, self.factory)

def test_multiline_text_input_created(self):
with self.assertWarns(DeprecationWarning):
widget = toga.MultilineTextInput(factory=self.factory)
Expand Down
55 changes: 37 additions & 18 deletions core/tests/widgets/test_label.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import pytest

import toga
from toga_dummy.utils import TestCase
from toga_dummy.utils import (
EventLog,
assert_action_performed,
attribute_value,
)


@pytest.fixture
def label():
return toga.Label("Test Label")


class LabelTests(TestCase):
def setUp(self):
super().setUp()
def test_label_created(label):
"A label can be created."
# Round trip the impl/interface
assert label._impl.interface == label
assert_action_performed(label, "create Label")

self.text = "test text"

self.label = toga.Label(self.text)
@pytest.mark.parametrize(
"value, expected",
[
("New Text", "New Text"),
(12345, "12345"),
(None, ""),
("\u200B", ""),
("Contains\nsome\nnewlines", "Contains\nsome\nnewlines"),
],
)
def test_update_label_text(label, value, expected):
assert label.text == "Test Label"

def test_widget_created(self):
self.assertEqual(self.label._impl.interface, self.label)
self.assertActionPerformed(self.label, "create Label")
# Clear the event log
EventLog.reset()

def test_update_label_text(self):
new_text = "updated text"
self.label.text = new_text
self.assertEqual(self.label.text, new_text)
self.assertValueSet(self.label, "text", new_text)
self.assertActionPerformed(self.label, "refresh")
label.text = value
assert label.text == expected

self.label.text = None
self.assertEqual(self.label.text, "")
# test backend has the right value
assert attribute_value(label, "text") == expected

self.assertValueSet(self.label, "text", "")
# A rehint was performed
assert_action_performed(label, "refresh")
2 changes: 1 addition & 1 deletion docs/reference/api/widgets/button.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ A button has a text label. A handler can be associated with button press events.
# handle event
pass

button = toga.Button('Click me', on_press=my_callback)
button = toga.Button("Click me", on_press=my_callback)

Notes
-----
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/api/widgets/label.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Usage

import toga

label = toga.Label('Hello world')
label = toga.Label("Hello world")

Notes
-----
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/data/widgets_by_platform.csv
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DatePicker,General Widget,:class:`~toga.widgets.datepicker.DatePicker`,An input
DetailedList,General Widget,:class:`~toga.widgets.detailedlist.DetailedList`,A list of complex content,|b|,|b|,,|b|,|b|,
Divider,General Widget,:class:`~toga.widgets.divider.Divider`,A horizontal or vertical line,|b|,|b|,|b|,,,
ImageView,General Widget,:class:`~toga.widgets.imageview.ImageView`,Image Viewer,|b|,|b|,|b|,|b|,|b|,
Label,General Widget,:class:`~toga.widgets.label.Label`,Text label,|b|,|b|,|b|,|b|,|b|,|b|
Label,General Widget,:class:`~toga.widgets.label.Label`,Text label,|y|,|y|,|y|,|y|,|y|,|b|
MultilineTextInput,General Widget,:class:`~toga.widgets.multilinetextinput.MultilineTextInput`,Multi-line Text Input field,|b|,|b|,|b|,|b|,|b|,
NumberInput,General Widget,:class:`~toga.widgets.numberinput.NumberInput`,Number Input field,|b|,|b|,|b|,|b|,|b|,
PasswordInput,General Widget,:class:`~toga.widgets.passwordinput.PasswordInput`,A text input that hides it’s input,|b|,|b|,|b|,|b|,|b|,
Expand Down
5 changes: 4 additions & 1 deletion dummy/src/toga_dummy/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ def create(self):
def set_alignment(self, value):
self._set_value("alignment", value)

def get_text(self):
return self._get_value("text")

def set_text(self, value):
self._set_value("text", self.interface._text)
self._set_value("text", value)
14 changes: 11 additions & 3 deletions gtk/src/toga_gtk/fonts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from toga.constants import BOLD, ITALIC, OBLIQUE, SMALL_CAPS, SYSTEM
from toga.constants import (
BOLD,
ITALIC,
OBLIQUE,
SMALL_CAPS,
SYSTEM,
SYSTEM_DEFAULT_FONT_SIZE,
)

from .libs import Pango

Expand Down Expand Up @@ -27,8 +34,9 @@ def __init__(self, interface):

font.set_family(family)

# Set font size
font.set_size(self.interface.size * Pango.SCALE)
# If this is a non-default font size, set the font size
if self.interface.size != SYSTEM_DEFAULT_FONT_SIZE:
font.set_size(self.interface.size * Pango.SCALE)

# Set font style
if self.interface.style == ITALIC:
Expand Down
1 change: 0 additions & 1 deletion gtk/src/toga_gtk/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ def create(self):
self.native.get_style_context().add_class("toga")
self.native.interface = self.interface

self.native.connect("show", lambda event: self.refresh())
self.native.connect("clicked", self.gtk_on_press)

def get_text(self):
Expand Down
Loading