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.MultilineTextInput #1938

Merged
merged 43 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5cac536
Audit docs and core tests for MultilineTextInput.
freakboy3742 May 9, 2023
5429acc
WIP
freakboy3742 May 9, 2023
c0cf0f3
Cocoa MultilineTextView to 100% coverage.
freakboy3742 May 10, 2023
cc80700
iOS to 100% coverage.
freakboy3742 May 10, 2023
8fa3aa9
Add Changenote.
freakboy3742 May 10, 2023
9096f52
Use the UIKeyInput protocol to fake keyboard input.
freakboy3742 May 10, 2023
cf9da9c
Ensure a redraw includes at an event loop tick on macOS.
freakboy3742 May 11, 2023
5800c40
Spelling corrections in docstrings.
freakboy3742 May 11, 2023
6a5be8a
Tweaked some test docstrings.
freakboy3742 May 11, 2023
d89e49c
Gtk implementation to 100%
freakboy3742 May 15, 2023
2f8f214
Enable GTK focus tests.
freakboy3742 May 16, 2023
7aea2d0
Add a window manager to the GTK CI config.
freakboy3742 May 16, 2023
7fceda3
Correct iOS placeholder test definition.
freakboy3742 May 16, 2023
ead982d
Android WIP
mhsmith May 17, 2023
43759f9
Merge branch 'main' into audit-multilinetext
freakboy3742 May 17, 2023
c763aa9
Add type annotations.
freakboy3742 May 17, 2023
4cdab2c
Tweaked some docs formatting.
freakboy3742 May 18, 2023
ff27cde
Simplified some probe handling.
freakboy3742 May 18, 2023
e636a1f
Tweaked handling of on-change and enabled.
freakboy3742 May 18, 2023
98343dc
Correct GTK on_change handling.
freakboy3742 May 18, 2023
375d26c
Additional tolerance for scoll size.
freakboy3742 May 18, 2023
8ef583d
Another scrollbar tolerance tweak.
freakboy3742 May 18, 2023
838c6e4
Propegate widget descriptions to the API summary page.
freakboy3742 May 18, 2023
6dff342
Actually save all the changes before pushing...
freakboy3742 May 18, 2023
80ea9f6
Update Android for changes in testbed
mhsmith May 18, 2023
b3b8ed3
Add tests for vertical alignment, and fix on Android
mhsmith May 18, 2023
cbe63ec
Implement vertical alignment checks for GTK.
freakboy3742 May 19, 2023
9eec789
Removed and no-covered some unreachable and unused content in cocoa/i…
freakboy3742 May 19, 2023
2ff9920
Probe implementations (and widget implementation) for top vertical al…
freakboy3742 May 19, 2023
08c8d50
Added Winforms implementation and fix for vertical alignment.
freakboy3742 May 19, 2023
e379547
Fix Android background color
mhsmith May 20, 2023
5db4c77
Android at 100% coverage
mhsmith May 21, 2023
be9ca2f
Deprecated the clear() method on text inputs.
freakboy3742 May 22, 2023
07abb24
Add allowance for minor GTK style differences.
freakboy3742 May 23, 2023
e9132f0
Update change note and support table
mhsmith May 23, 2023
df00658
Make libs/android/graphics/drawable match Java package structure
mhsmith May 23, 2023
bb71950
All Winforms tests passing except test_scroll_position
mhsmith May 23, 2023
3ad5352
Winforms at 100% coverage
mhsmith May 23, 2023
e687898
Correct iOS test failure caused by focus.
freakboy3742 May 24, 2023
4ca8a9b
Document Winforms issue with TRANSPARENT backgrounds, and mainline th…
freakboy3742 May 24, 2023
87fffda
Correct GTK handling of clearing while focussed.
freakboy3742 May 24, 2023
fee4bb6
Simplify winforms implementation, removing proxy value.
freakboy3742 May 24, 2023
9f871ef
Winforms: fix interactions between placeholder and on_change handler
mhsmith May 25, 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
18 changes: 15 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,21 @@ jobs:
- backend: linux
runs-on: ubuntu-22.04
# The package list should be the same as in tutorial-0.rst, and the BeeWare
# tutorial.
pre-command: "sudo apt-get update -y && sudo apt-get install -y python3-dev python3-cairo-dev python3-gi-cairo libgirepository1.0-dev libcairo2-dev libpango1.0-dev gir1.2-webkit2-4.0 pkg-config"
briefcase-run-prefix: 'xvfb-run -a -s "-screen 0 2048x1536x24"'
# tutorial, plus flwm to provide a window manager
pre-command: |
sudo apt-get update -y && sudo apt-get install -y python3-dev python3-cairo-dev python3-gi-cairo libgirepository1.0-dev libcairo2-dev libpango1.0-dev gir1.2-webkit2-4.0 pkg-config flwm

# Start Virtual X server
echo "Start X server..."
Xvfb :99 -screen 0 2048x1536x24 &
sleep 1

# Start Window manager
echo "Start window manager..."
DISPLAY=:99 flwm &
sleep 1

briefcase-run-prefix: 'DISPLAY=:99'
setup-python: false # Use the system Python packages.

- backend: windows
Expand Down
4 changes: 4 additions & 0 deletions android/src/toga_android/libs/android/drawable.py
Copy link
Member Author

Choose a reason for hiding this comment

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

Should these be here, or in android.graphics?

Copy link
Member

Choose a reason for hiding this comment

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

At some point we should remove the libs folder and use Chaquopy's from ... import ... syntax, so we don't need to edit 2 files every time we import a new class. To make it easy to do that with a simple search and replace, I've made drawable a submodule of graphics.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from rubicon.java import JavaClass

ColorDrawable = JavaClass("android/graphics/drawable/ColorDrawable")
InsetDrawable = JavaClass("android/graphics/drawable/InsetDrawable")
39 changes: 31 additions & 8 deletions android/src/toga_android/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from ..colors import native_color
from ..libs.activity import MainActivity
from ..libs.android.drawable import ColorDrawable, InsetDrawable
from ..libs.android.graphics import PorterDuff__Mode, PorterDuffColorFilter, Rect
from ..libs.android.view import Gravity, View


Expand Down Expand Up @@ -109,18 +111,39 @@ def set_font(self, font):
# By default, font can't be changed
pass

# Although setBackgroundColor is defined in the View base class, we can't use it as
# a default implementation because it often overwrites other aspects of the widget's
# appearance. So each widget must decide how to implement this method, possibly
# using one of the utility functions below.
def set_background_color(self, color):
# By default, background color can't be changed.
pass

# Although setBackgroundColor is defined in the View base class, we can't use it as
# a default implementation because it often overwrites other aspects of the widget's
# appearance.
def set_background_color_simple(self, value):
if value is None:
self.native.setBackgroundColor(native_color(TRANSPARENT))
def set_background_simple(self, value):
if not hasattr(self, "_default_background"):
self._default_background = self.native.getBackground()

if value in (None, TRANSPARENT):
self.native.setBackground(self._default_background)
else:
self.native.setBackgroundColor(native_color(value))
background = ColorDrawable(native_color(value))
if isinstance(self._default_background, InsetDrawable):
outer_padding = Rect()
inner_padding = Rect()
self._default_background.getPadding(outer_padding)
self._default_background.getDrawable().getPadding(inner_padding)
insets = [
getattr(outer_padding, name) - getattr(inner_padding, name)
for name in ["left", "top", "right", "bottom"]
]
background = InsetDrawable(background, *insets)
self.native.setBackground(background)

def set_background_filter(self, value):
self.native.getBackground().setColorFilter(
None
if value in (None, TRANSPARENT)
else PorterDuffColorFilter(native_color(value), PorterDuff__Mode.SRC_IN)
)

def set_alignment(self, alignment):
pass # If appropriate, a widget subclass will implement this.
Expand Down
2 changes: 1 addition & 1 deletion android/src/toga_android/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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.set_background_color_simple(value)
self.set_background_simple(value)

def rehint(self):
self.interface.intrinsic.width = at_least(0)
Expand Down
11 changes: 1 addition & 10 deletions android/src/toga_android/widgets/button.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
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, PorterDuffColorFilter
from ..libs.android.view import OnClickListener, View__MeasureSpec
from ..libs.android.widget import Button as A_Button
from .label import TextViewWidget
Expand Down Expand Up @@ -34,12 +30,7 @@ def set_enabled(self, value):
self.native.setEnabled(value)

def set_background_color(self, value):
# 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)
)
self.set_background_filter(value)

def rehint(self):
# Like other text-viewing widgets, Android crashes when rendering
Expand Down
31 changes: 14 additions & 17 deletions android/src/toga_android/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ def set_color(self, value):
else:
self.native.setTextColor(native_color(value))

def set_textview_alignment(self, value, vertical_gravity):
# Justified text wasn't added until API level 26.
# We only run the test suite on API 31, so we need to disable branch coverage.
if Build.VERSION.SDK_INT >= 26: # pragma: no branch
self.native.setJustificationMode(
Layout.JUSTIFICATION_MODE_INTER_WORD
if value == JUSTIFY
else Layout.JUSTIFICATION_MODE_NONE
)

self.native.setGravity(vertical_gravity | align(value))


class Label(TextViewWidget):
def create(self):
Expand All @@ -38,7 +50,7 @@ def set_text(self, value):
self.native.setText(value)

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

def rehint(self):
# Refuse to rehint an Android TextView if it has no LayoutParams yet.
Expand All @@ -60,19 +72,4 @@ def rehint(self):
self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth())

def set_alignment(self, value):
# 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 API level 26.
# We only run the test suite on API 31, so we need to disable branch coverage.
if Build.VERSION.SDK_INT >= 26: # pragma: no branch
self.native.setJustificationMode(
Layout.JUSTIFICATION_MODE_INTER_WORD
if value == JUSTIFY
else Layout.JUSTIFICATION_MODE_NONE
)

self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))
self.set_textview_alignment(value, Gravity.TOP)
53 changes: 27 additions & 26 deletions android/src/toga_android/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,72 @@
from travertino.size import at_least

from toga.constants import LEFT

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


class TogaTextWatcher(TextWatcher):
def __init__(self, impl):
super().__init__()
self.impl = impl
self.interface = impl.interface

def beforeTextChanged(self, _charSequence, _start, _count, _after):
pass

def afterTextChanged(self, _editable):
if self.interface.on_change:
self.interface.on_change(widget=self.interface)
self.interface.on_change(widget=self.interface)

def onTextChanged(self, _charSequence, _start, _before, _count):
pass


class MultilineTextInput(TextViewWidget):
def create(self):
self._textChangedListener = None
self.native = EditText(self._native_activity)
self.native.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
)
# Set default alignment
self.set_alignment(LEFT)
self.native.addTextChangedListener(TogaTextWatcher(self))
self.cache_textview_defaults()

def get_value(self):
return self.native.getText().toString()
return str(self.native.getText())

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

def get_readonly(self):
return not self.native.isFocusable()

def set_readonly(self, value):
self.native.setFocusable(not value)
def set_readonly(self, readonly):
if readonly:
# Implicitly calls setFocusableInTouchMode(False)
self.native.setFocusable(False)
else:
# Implicitly calls setFocusable(True)
self.native.setFocusableInTouchMode(True)

def get_placeholder(self):
return str(self.native.getHint())

def set_placeholder(self, value):
# Android EditText's setHint() requires a Python string.
self.native.setHint(value if value is not None else "")
self.native.setHint(value)

def set_alignment(self, value):
self.native.setGravity(Gravity.TOP | align(value))

def set_value(self, value):
self.native.setText(value)
self.set_textview_alignment(value, Gravity.TOP)

def set_on_change(self, handler):
if self._textChangedListener:
self.native.removeTextChangedListener(self._textChangedListener)
self._textChangedListener = TogaTextWatcher(self)
self.native.addTextChangedListener(self._textChangedListener)
def set_background_color(self, value):
# This causes any custom color to hide the bottom border line, but it's better
# than set_background_filter, which affects *only* the bottom border line.
self.set_background_simple(value)

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT)

def scroll_to_bottom(self):
last_line = (self.native.getLineCount() - 1) * self.native.getLineHeight()
self.native.scrollTo(0, last_line)
self.native.setSelection(self.native.length())

def scroll_to_top(self):
self.native.scrollTo(0, 0)
self.native.setSelection(0)
8 changes: 1 addition & 7 deletions android/src/toga_android/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from ..libs.android.text import InputType, TextWatcher
from ..libs.android.view import Gravity, OnKeyListener, View__MeasureSpec
from ..libs.android.widget import EditText
from .base import align
from .label import TextViewWidget


Expand Down Expand Up @@ -64,12 +63,7 @@ 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 not self.native.getLayoutParams():
return
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))
self.set_textview_alignment(value, Gravity.CENTER_VERTICAL)

def set_value(self, value):
self.native.setText(value)
Expand Down
11 changes: 2 additions & 9 deletions android/src/toga_android/widgets/webview.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

from travertino.size import at_least

from ..libs.android.view import Gravity, View__MeasureSpec
from ..libs.android.view import View__MeasureSpec
from ..libs.android.webkit import ValueCallback, WebView as A_WebView, WebViewClient
from .base import Widget, align
from .base import Widget


class ReceiveString(ValueCallback):
Expand Down Expand Up @@ -72,13 +72,6 @@ async def evaluate_javascript(self, javascript):
def invoke_javascript(self, javascript):
self.native.evaluateJavascript(str(javascript), ReceiveString())

def set_alignment(self, value):
# Refuse to set alignment unless widget has been added to a container.
# This is because this widget's setGravity() requires LayoutParams before it can be called.
if not self.native.getLayoutParams():
return
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))
Comment on lines -75 to -80
Copy link
Member

Choose a reason for hiding this comment

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

Because of the getLayoutParams issue, this code was never previously running during widget creation. It turns out WebView doesn't even have a setGravity method.

It doesn't make any sense for WebView to support alignment anyway, so I've just removed the method.


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.
Expand Down
36 changes: 35 additions & 1 deletion android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
from java import dynamic_proxy
from pytest import approx

from android.graphics.drawable import (
ColorDrawable,
DrawableContainer,
DrawableWrapper,
LayerDrawable,
)
from android.os import Build
from android.view import View, ViewTreeObserver
from toga.colors import TRANSPARENT
from toga.fonts import SYSTEM
from toga.style.pack import JUSTIFY, LEFT

Expand Down Expand Up @@ -122,7 +129,34 @@ def assert_layout(self, size, position):

@property
def background_color(self):
return toga_color(self.native.getBackground().getColor())
background = self.native.getBackground()
while True:
if isinstance(background, ColorDrawable):
return toga_color(background.getColor())

# The following complex Drawables all apply color filters to their children,
# but they don't implement getColorFilter, at least not in our current
# minimum API level.
elif isinstance(background, LayerDrawable):
background = background.getDrawable(0)
elif isinstance(background, DrawableContainer):
background = background.getCurrent()
elif isinstance(background, DrawableWrapper):
background = background.getDrawable()

else:
break

if background is None:
return TRANSPARENT
filter = background.getColorFilter()
if filter:
# PorterDuffColorFilter.getColor is undocumented, but continues to work for
# now. If this method is blocked in the future, another option is to use the
# filter to draw something and see what color comes out.
return toga_color(filter.getColor())
else:
return TRANSPARENT

async def press(self):
self.native.performClick()
Expand Down
Loading