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.DateInput, toga.TimeInput #1951

Merged
merged 28 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e872383
Added docs for DatePicker and TimePicker.
freakboy3742 May 26, 2023
390b785
Ported date/time picker tests to pytest, improved implementation.
freakboy3742 May 26, 2023
b52d3da
Updated android/winforms implementations to reflect new APIs.
freakboy3742 May 26, 2023
c89a123
Rename Date/TimePicker to Date/TimeInput.
freakboy3742 May 26, 2023
02f1036
Correct the Android implementation.
freakboy3742 May 26, 2023
111842f
Add skeleton testbed tests.
freakboy3742 May 26, 2023
ea0f7bf
Add Changenotes.
freakboy3742 May 26, 2023
a01c78c
Added pytest-freezegun to testing requirements.
freakboy3742 May 26, 2023
4ba446b
Correct problems with the Winforms implementation.
freakboy3742 May 26, 2023
99478b6
Use deferred type annotations.
freakboy3742 May 26, 2023
a99b4ee
Removed some unnecessary methods on the dummy interface, and marked t…
freakboy3742 May 26, 2023
ef36395
Merge branch 'main' into audit-datetime
freakboy3742 May 30, 2023
5238706
Correct release notes.
freakboy3742 May 30, 2023
614bea1
Documentation cleanups
mhsmith Jun 4, 2023
8128400
Merge remote-tracking branch 'origin/main' into audit-datetime
mhsmith Jun 4, 2023
7cbc5c1
Winforms DateInput at 100% coverage
mhsmith Jun 5, 2023
ce7b0a0
Winforms TimeInput at 100% coverage
mhsmith Jun 5, 2023
b265eb1
Android at 100% coverage
mhsmith Jun 6, 2023
f746aee
Skip DateInput and TimeInput tests on unsupported platforms
mhsmith Jun 6, 2023
ab3765c
Fix docstring formatting
mhsmith Jun 6, 2023
457a00b
Merge branch 'main' into audit-datetime
freakboy3742 Jun 9, 2023
7cd87bf
Merge branch 'main' into audit-datetime
mhsmith Jun 15, 2023
0eeba0a
Android: add timeout to probe.redraw
mhsmith Jun 18, 2023
fb572b8
Remove pytest-freezegun from testbed, as it sometimes causes the Andr…
mhsmith Jun 18, 2023
a6a020a
Prevent the log being cluttered with "avc: denied" messages
mhsmith Jun 18, 2023
b9fa936
DateInput min and max fixes
mhsmith Jun 19, 2023
94244f8
TimeInput min and max fixes (core)
mhsmith Jun 19, 2023
96f30ea
TimeInput min and max fixes (backends)
mhsmith Jun 20, 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
8 changes: 4 additions & 4 deletions android/src/toga_android/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .widgets.box import Box
from .widgets.button import Button
from .widgets.canvas import Canvas
from .widgets.datepicker import DatePicker
from .widgets.dateinput import DateInput
from .widgets.detailedlist import DetailedList
from .widgets.imageview import ImageView
from .widgets.label import Label
Expand All @@ -22,7 +22,7 @@
from .widgets.switch import Switch
from .widgets.table import Table
from .widgets.textinput import TextInput
from .widgets.timepicker import TimePicker
from .widgets.timeinput import TimeInput
from .widgets.webview import WebView
from .window import Window

Expand All @@ -37,7 +37,7 @@ def not_implemented(feature):
"Button",
"Canvas",
"Command",
"DatePicker",
"DateInput",
"Font",
"Icon",
"Image",
Expand All @@ -54,7 +54,7 @@ def not_implemented(feature):
"Switch",
"Table",
"TextInput",
"TimePicker",
"TimeInput",
"WebView",
"Window",
"DetailedList",
Expand Down
73 changes: 73 additions & 0 deletions android/src/toga_android/widgets/dateinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from datetime import date, datetime, time

from ..libs.android import R__drawable
from ..libs.android.widget import (
DatePickerDialog,
DatePickerDialog__OnDateSetListener as OnDateSetListener,
)
from .internal.pickers import PickerBase

NO_MIN = date(1799, 1, 1)
NO_MAX = date(9999, 1, 1)
Copy link
Member Author

Choose a reason for hiding this comment

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

Are these values with a significance to Android? We should document why these dates in particular has been picked - both in the code, and in the user-facing widget notes as a functional limitation.

Copy link
Member

Choose a reason for hiding this comment

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

As discussed, I've changed the implementation to allow years between 1800 and 8999 on all platforms, and updated the documentation to match.



def py_date(native_date):
return date.fromtimestamp(native_date / 1000)


def native_date(py_date):
return int(datetime.combine(py_date, time.min).timestamp() * 1000)


class DatePickerListener(OnDateSetListener):
def __init__(self, impl):
super().__init__()
self.impl = impl

def onDateSet(self, view, year, month_0, day):
self.impl.set_value(date(year, month_0 + 1, day))


class DateInput(PickerBase):
@classmethod
def _get_icon(cls):
return R__drawable.ic_menu_my_calendar

def create(self):
super().create()
self.native.setText("1970-01-01") # Dummy value used during initialization

def get_value(self):
return date.fromisoformat(str(self.native.getText()))

def set_value(self, value):
self.native.setText(value.isoformat())
self._dialog.updateDate(value.year, value.month - 1, value.day)
self.interface.on_change(None)

def get_min_date(self):
result = py_date(self._picker.getMinDate())
return None if (result == NO_MIN) else result

def set_min_date(self, value):
self._picker.setMinDate(native_date(NO_MIN if value is None else value))

def get_max_date(self):
result = py_date(self._picker.getMaxDate())
return None if (result == NO_MAX) else result

def set_max_date(self, value):
self._picker.setMaxDate(native_date(NO_MAX if value is None else value))

def _create_dialog(self):
return DatePickerDialog(
self._native_activity,
DatePickerListener(self),
2000, # year
0, # month (0 = January)
1, # day
)

@property
def _picker(self):
return self._dialog.getDatePicker()
76 changes: 0 additions & 76 deletions android/src/toga_android/widgets/datepicker.py

This file was deleted.

22 changes: 11 additions & 11 deletions android/src/toga_android/widgets/internal/pickers.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
from abc import ABC, abstractmethod

from travertino.size import at_least

from ...libs.android.view import OnClickListener, View__MeasureSpec
from ...libs.android.widget import EditText
from ..base import Widget
from ..label import TextViewWidget


class TogaPickerClickListener(OnClickListener):
def __init__(self, picker_impl):
def __init__(self, impl):
super().__init__()
self.picker_impl = picker_impl
self.impl = impl

def onClick(self, _):
self.picker_impl._create_dialog()
self.impl._dialog.show()


class PickerBase(Widget, ABC):
class PickerBase(TextViewWidget, ABC):
@classmethod
@abstractmethod
def _get_icon(cls):
raise NotImplementedError

@classmethod
@abstractmethod
def _get_hint(cls):
def _create_dialog(self):
raise NotImplementedError

def create(self):
self._value = None
self._dialog = None
self._dialog = self._create_dialog()
self.native = EditText(self._native_activity)
self.native.setFocusable(False)
self.native.setClickable(False)
Expand All @@ -36,10 +36,10 @@ def create(self):
self.native.setLongClickable(False)
self.native.setOnClickListener(TogaPickerClickListener(self))
self.native.setCompoundDrawablesWithIntrinsicBounds(self._get_icon(), 0, 0, 0)
self.native.setHint(self._get_hint())
self.cache_textview_defaults()

def rehint(self):
self.interface.intrinsic.width = self.native.getMeasuredWidth()
self.interface.intrinsic.width = at_least(300)
# 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.
Expand Down
9 changes: 6 additions & 3 deletions android/src/toga_android/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ def cache_textview_defaults(self):
def set_font(self, font):
font._impl.apply(self.native, self._default_text_size, self._default_typeface)

def set_background_color(self, value):
# In the case of EditText, his causes any custom color to hide the bottom border
mhsmith marked this conversation as resolved.
Show resolved Hide resolved
# line, but it's better than set_background_filter, which affects *only* the
# bottom border line.
self.set_background_simple(value)

def set_color(self, value):
if value is None:
self.native.setTextColor(self._default_text_color)
Expand Down Expand Up @@ -49,9 +55,6 @@ def get_text(self):
def set_text(self, value):
self.native.setText(value)

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

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
5 changes: 0 additions & 5 deletions android/src/toga_android/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,6 @@ def set_placeholder(self, value):
def set_alignment(self, value):
self.set_textview_alignment(value, Gravity.CENTER_VERTICAL)

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 set_error(self, error_message):
self.native.setError(error_message)

Expand Down
62 changes: 62 additions & 0 deletions android/src/toga_android/widgets/timeinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import time

from ..libs.android import R__drawable
from ..libs.android.widget import (
TimePickerDialog,
TimePickerDialog__OnTimeSetListener as OnTimeSetListener,
)
from .internal.pickers import PickerBase


class TimePickerListener(OnTimeSetListener):
def __init__(self, impl):
super().__init__()
self.impl = impl

def onTimeSet(self, view, hour, minute):
self.impl.set_value(time(hour, minute))


class TimeInput(PickerBase):
@classmethod
def _get_icon(cls):
return R__drawable.ic_menu_recent_history

def create(self):
super().create()

# Dummy values used during initialization
self.native.setText("00:00")
self._min_time = None
self._max_time = None

def get_value(self):
return time.fromisoformat(str(self.native.getText()))

def set_value(self, value):
self.native.setText(value.isoformat(timespec="minutes"))
self._dialog.updateTime(value.hour, value.minute)
self.interface.on_change(None)

# Unlike DatePicker, TimePicker does not natively support min or max, so these
# properties currently have no effect.
Copy link
Member Author

Choose a reason for hiding this comment

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

Would it be worth doing a "soft" min/max? i.e., we have control over get_value(); if there's a min_value of 6AM, and the user picks 3AM, we could modify get_value() to return 6AM. It's not ideal UX, but it would at ensure the Android implementation doesn't return values that aren't invalid.

def get_min_time(self):
return self._min_time

def set_min_time(self, value):
self._min_time = value

def get_max_time(self):
return self._max_time

def set_max_time(self, value):
self._max_time = value

def _create_dialog(self):
return TimePickerDialog(
self._native_activity,
TimePickerListener(self),
0, # hour
0, # minute
True, # is24HourView
)
Loading