Skip to content

Commit

Permalink
Merge pull request #1946 from freakboy3742/audit-numberinput
Browse files Browse the repository at this point in the history
[widget Audit] toga.NumberInput
  • Loading branch information
freakboy3742 committed Jun 2, 2023
2 parents 488b417 + 4141589 commit 0c527bb
Show file tree
Hide file tree
Showing 38 changed files with 1,614 additions and 571 deletions.
16 changes: 13 additions & 3 deletions android/src/toga_android/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@
class MultilineTextInput(TextInput):
def create(self):
super().create(
input_type=InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE,
handle_confirm=False,
handle_focus=False,
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE,
)

def _on_change(self):
self.interface.on_change(None)

def _on_confirm(self): # pragma: nocover
pass # The interface doesn't support this event.

def _on_gain_focus(self):
pass # The interface doesn't support this event.

def _on_lose_focus(self):
pass # The interface doesn't support this event.

def set_alignment(self, value):
self.set_textview_alignment(value, Gravity.TOP)

Expand Down
109 changes: 28 additions & 81 deletions android/src/toga_android/widgets/numberinput.py
Original file line number Diff line number Diff line change
@@ -1,100 +1,47 @@
from decimal import Decimal, InvalidOperation
from decimal import InvalidOperation

from travertino.size import at_least
from toga.widgets.numberinput import _clean_decimal

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


def decimal_from_string(s):
"""Convert s to a `Decimal`, returning `None` if it's not a valid number."""
try:
return Decimal(s)
except InvalidOperation:
return None


def string_from_decimal(d):
"""Implement the inverse of `decimal_from_string()`.
This way, Toga's `NumericInput` can pass us a `None` or `Decimal`, and we can always
place a String in the Android `EditText`.
"""
if d is None:
return ""
return str(d)


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

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

def afterTextChanged(self, editable):
# Toga `NumberInput` stores the value as a property on the `interface`.
self.interface._value = decimal_from_string(editable.toString())
# Call the user on_change callback, if it exists.
if self.interface.on_change:
self.interface.on_change(widget=self.interface)

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


class NumberInput(TextViewWidget):
class NumberInput(TextInput):
def create(self):
self.native = EditText(self._native_activity)
self.native.addTextChangedListener(TogaNumberInputWatcher(self))

# A `NumberInput` in Toga supports signed decimal numbers.
self.native.setInputType(
super().create(
InputType.TYPE_CLASS_NUMBER
| InputType.TYPE_NUMBER_FLAG_DECIMAL
| InputType.TYPE_NUMBER_FLAG_SIGNED
| InputType.TYPE_NUMBER_FLAG_SIGNED,
)
self.cache_textview_defaults()

def set_readonly(self, value):
self.native.setFocusable(not value)

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

def set_alignment(self, value):
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))
def get_value(self):
try:
return _clean_decimal(super().get_value(), self.interface.step)
except InvalidOperation:
return None

def set_value(self, value):
# Store a string in the Android widget. The `afterTextChanged` method
# will call the user on_change handler.
self.native.setText(string_from_decimal(value))
super().set_value("" if value is None else str(value))

def set_step(self, step):
self.interface.factory.not_implemented("NumberInput.set_step()")
pass # This backend doesn't support stepped increments.

def set_max_value(self, value):
self.interface.factory.not_implemented("NumberInput.set_max_value()")
pass # This backend doesn't support stepped increments.

def set_min_value(self, value):
self.interface.factory.not_implemented("NumberInput.set_min_value()")
pass # This backend doesn't support stepped increments.

def set_on_change(self, handler):
# No special handling required.
pass
def _on_change(self):
self.interface.on_change(None)

def rehint(self):
# On Android, EditText's measure() throws NullPointerException if the widget has no
# LayoutParams.
if not self.native.getLayoutParams():
return
self.native.measure(
View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED
)
self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth())
self.interface.intrinsic.height = self.native.getMeasuredHeight()
def _on_confirm(self): # pragma: nocover
pass # The interface doesn't support this event.

def _on_gain_focus(self):
pass # The interface doesn't support this event.

def _on_lose_focus(self):
# The interface doesn't support this event, but we should still clip the
# displayed value.
self.set_value(self.interface.value)
4 changes: 1 addition & 3 deletions android/src/toga_android/widgets/passwordinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,5 @@
class PasswordInput(TextInput):
def create(self):
super().create(
input_type=(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
)
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD,
)
39 changes: 20 additions & 19 deletions android/src/toga_android/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ 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):
self.interface.on_change(None)
self.interface._validate()
self.impl._on_change()

def onTextChanged(self, _charSequence, _start, _before, _count):
pass
Expand All @@ -34,7 +32,6 @@ class TogaKeyListener(OnKeyListener):
def __init__(self, impl):
super().__init__()
self.impl = impl
self.interface = impl.interface

def onKey(self, _view, _key, _event):
event_info = toga_key(_event)
Expand All @@ -45,40 +42,31 @@ def onKey(self, _view, _key, _event):
if (key_pressed == "<enter>" or key_pressed == "numpad:enter") and (
int(_event.getAction()) == 1
):
self.interface.on_confirm(None)
self.impl._on_confirm()
return False


class TogaFocusListener(View__OnFocusChangeListener):
def __init__(self, impl):
super().__init__()
self.impl = impl
self.interface = impl.interface

def onFocusChange(self, view, has_focus):
if has_focus:
self.interface.on_gain_focus(None)
self.impl._on_gain_focus()
else:
self.interface.on_lose_focus(None)
self.impl._on_lose_focus()


class TextInput(TextViewWidget):
def create(
self,
*,
input_type=InputType.TYPE_CLASS_TEXT,
handle_confirm=True,
handle_focus=True,
):
def create(self, input_type=InputType.TYPE_CLASS_TEXT):
self.native = EditText(self._native_activity)
self.native.setInputType(input_type)
self.cache_textview_defaults()

self.native.addTextChangedListener(TogaTextWatcher(self))
if handle_confirm:
self.native.setOnKeyListener(TogaKeyListener(self))
if handle_focus:
self.native.setOnFocusChangeListener(TogaFocusListener(self))
self.native.setOnKeyListener(TogaKeyListener(self))
self.native.setOnFocusChangeListener(TogaFocusListener(self))

def get_value(self):
return str(self.native.getText())
Expand Down Expand Up @@ -120,6 +108,19 @@ def clear_error(self):
def is_valid(self):
return self.native.getError() is None

def _on_change(self):
self.interface.on_change(None)
self.interface._validate()

def _on_confirm(self):
self.interface.on_confirm(None)

def _on_gain_focus(self):
self.interface.on_gain_focus(None)

def _on_lose_focus(self):
self.interface.on_lose_focus(None)

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
22 changes: 22 additions & 0 deletions android/tests_backend/widgets/numberinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pytest import xfail

from .textinput import TextInputProbe


class NumberInputProbe(TextInputProbe):
allows_invalid_value = False
allows_empty_value = True
allows_extra_digits = True

@property
def value(self):
return str(self.native.getText())

def clear_input(self):
self.native.setText("")

async def increment(self):
xfail("This backend doesn't support stepped increments")

async def decrement(self):
xfail("This backend doesn't support stepped increments")
1 change: 1 addition & 0 deletions changes/1946.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The NumberInput widget now has 100% test coverage, and complete API documentation.
Loading

0 comments on commit 0c527bb

Please sign in to comment.