Skip to content

Commit

Permalink
Merge branch 'main' into audit-base
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Apr 7, 2023
2 parents 43a4fd9 + 85eb59c commit d4dac30
Show file tree
Hide file tree
Showing 81 changed files with 2,323 additions and 1,008 deletions.
1 change: 1 addition & 0 deletions android/src/toga_android/libs/android/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
R__drawable = JavaClass("android/R$drawable")
R__id = JavaClass("android/R$id")
R__layout = JavaClass("android/R$layout")
R__style = JavaClass("android/R$style")
1 change: 1 addition & 0 deletions android/src/toga_android/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def _get_activity(_cache=[]):

class Widget:
def __init__(self, interface):
super().__init__()
self.interface = interface
self.interface._impl = self
self._container = None
Expand Down
81 changes: 62 additions & 19 deletions android/src/toga_android/widgets/progressbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,33 @@
from ..libs.android.widget import ProgressBar as A_ProgressBar
from .base import Widget

# Implementation notes
# ====================
#
# * Android ProgressBar doesn't have a "running" mode; the closest it gets is
# "animating", which only really applies to indeterminate progress bars. We
# track running status independently for interface compliance.
#
# * Android ProgressBar has an "indeterminate" mode, but that's really a proxy
# for "am I animating", not "am I an indeterminate progress bar". The
# setIndeterminate() API essentially needs to be used as "start/stop
# indeterminate animation". This needs to be invoked on start/stop, but also
# when we move into an indeterminate state when already running.
# setIndeterminate(False) can be safely called on non-determinate progress bars.
#
# * There's no native way to identify a stopped indeterminate progress bar. We
# use a max value of 0 as a marker for an indeterminate progress bar. This
# value won't be legal from the Toga interface, as Toga requires a positive
# max value.
#
# * Android ProgressBar uses an integer to track for progress. Toga's values are
# floats; so we multiply the Toga value by 1000 so that we get 3dp of
# functional progress fidelity.


class ProgressBar(Widget):
TOGA_SCALE = 1000

def create(self):
progressbar = A_ProgressBar(
self._native_activity,
Expand All @@ -16,36 +41,54 @@ def create(self):
)
self.native = progressbar

self._running = False

def is_running(self):
return self._running

def start(self):
self.set_running_style()
self._running = True
if self.get_max() is None:
self.native.setIndeterminate(True)

def stop(self):
self.set_stopping_style()
self._running = False
self.native.setIndeterminate(False)

def get_max(self):
# Indeterminate progress bar; return a max of None.
if self.native.getMax() == 0:
return None

@property
def max(self):
return self.interface.max
return float(self.native.getMax() / self.TOGA_SCALE)

def set_max(self, value):
if value is not None:
self.native.setMax(int(value))
if self.interface.is_running:
self.set_running_style()
else:
self.set_stopping_style()
if value is None:
# Indeterminate progress bar; set the max to the marker value of 0,
# and ensure that the bar won't show any meaningful progress value.
self.native.setProgress(0)
self.native.setMax(0)

def set_running_style(self):
if self.max is None:
self.native.setIndeterminate(True)
# If we're already running, put the bar into
# indeterminate animation mode.
if self._running:
self.start()
else:
self.stop()
else:
# Make sure we're not in indeterminate mode. Android's
# indeterminate mode is really an "is animating indeterminate",
# so we don't care whether we're running or not.
self.native.setIndeterminate(False)
self.native.setMax(int(value * self.TOGA_SCALE))

def set_stopping_style(self):
self.native.setIndeterminate(False)
def get_value(self):
if self.native.getMax() == 0:
return None
return float(self.native.getProgress() / self.TOGA_SCALE)

def set_value(self, value):
if value is not None:
self.native.setProgress(int(value))
self.native.setProgress(int(value * self.TOGA_SCALE))

def rehint(self):
# Android can crash when rendering some widgets until
Expand All @@ -57,4 +100,4 @@ def rehint(self):
View__MeasureSpec.UNSPECIFIED,
)
self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth())
self.interface.intrinsic.height = at_least(self.native.getMeasuredHeight())
self.interface.intrinsic.height = self.native.getMeasuredHeight()
81 changes: 39 additions & 42 deletions android/src/toga_android/widgets/slider.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,67 @@
from travertino.size import at_least

import toga

from ..libs.android import R__attr, R__style
from ..libs.android.view import View__MeasureSpec
from ..libs.android.widget import SeekBar, SeekBar__OnSeekBarChangeListener
from .base import Widget

# Implementation notes
# ====================
#
# The native widget represents values as integers, so the IntSliderImpl base class is
# used to convert between integers and floats.


class TogaOnSeekBarChangeListener(SeekBar__OnSeekBarChangeListener):
def __init__(self, impl):
super().__init__()
self.impl = impl

def onProgressChanged(self, _view, _progress, _from_user):
self.impl.interface.on_change(None)
self.impl.on_change()

# Add two unused methods so that the Java interface is completely implemented.
def onStartTrackingTouch(self, native_seekbar):
pass
self.impl.interface.on_press(None)

def onStopTrackingTouch(self, native_seekbar):
pass

self.impl.interface.on_release(None)

# Since Android's SeekBar is always discrete,
# use a high number of steps for a "continuous" slider.
DEFAULT_NUMBER_OF_TICKS = 10000

class Slider(Widget, toga.widgets.slider.IntSliderImpl):
TICK_DRAWABLE = None

class Slider(Widget):
def create(self):
self.native = SeekBar(self._native_activity)
self.native.setMax(DEFAULT_NUMBER_OF_TICKS)
self.native.setOnSeekBarChangeListener(TogaOnSeekBarChangeListener(self))

def get_value(self):
minimum, maximum = self.interface.range
if self.interface.tick_count is not None and self.interface.tick_count <= 1:
return minimum
toga_tick_count = self.interface.tick_count or DEFAULT_NUMBER_OF_TICKS
android_slider_max = toga_tick_count - 1
tick_factor = (maximum - minimum) / android_slider_max
progress_scaled = self.native.getProgress() * tick_factor
result = progress_scaled + minimum
return result

def set_value(self, value):
minimum, maximum = self.interface.range
if self.interface.tick_count is not None and self.interface.tick_count <= 1:
android_progress = 0
else:
toga_tick_count = self.interface.tick_count or DEFAULT_NUMBER_OF_TICKS
android_slider_max = toga_tick_count - 1
tick_factor = (maximum - minimum) / android_slider_max
android_progress = int((value - minimum) * tick_factor)
self.native.setProgress(android_progress)

def set_range(self, range):
pass

def set_tick_count(self, tick_count):
# Since the Android slider slides from 0 to max inclusive, always subtract 1 from tick_count.
if self.interface.tick_count is None:
android_slider_max = DEFAULT_NUMBER_OF_TICKS - 1
def get_int_value(self):
return self.native.getProgress()

def set_int_value(self, value):
self.native.setProgress(value)

def get_int_max(self):
return self.native.getMax()

def set_int_max(self, max):
self.native.setMax(max)

def set_ticks_visible(self, visible):
if visible:
if Slider.TICK_DRAWABLE is None:
self._load_tick_drawable()
self.native.setTickMark(Slider.TICK_DRAWABLE)
else:
android_slider_max = int(self.interface.tick_count - 1)
# Set the Android SeekBar max, clamping so it's non-negative.
self.native.setMax(max(0, android_slider_max))
self.native.setTickMark(None)

def _load_tick_drawable(self):
attrs = self._native_activity.obtainStyledAttributes(
R__style.Widget_Material_SeekBar_Discrete, [R__attr.tickMark]
)
Slider.TICK_DRAWABLE = attrs.getDrawable(0)
attrs.recycle()

def rehint(self):
self.native.measure(
Expand Down
2 changes: 1 addition & 1 deletion android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def assert_layout(self, size, position):
def background_color(self):
return toga_color(self.native.getBackground().getColor())

def press(self):
async def press(self):
self.native.performClick()

@property
Expand Down
5 changes: 0 additions & 5 deletions android/tests_backend/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,3 @@

class BoxProbe(SimpleProbe):
native_class = jclass("android.widget.RelativeLayout")

@property
def enabled(self):
# A box is always enabled.
return True
20 changes: 20 additions & 0 deletions android/tests_backend/widgets/progressbar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from android.widget import ProgressBar

from .base import SimpleProbe


class ProgressBarProbe(SimpleProbe):
native_class = ProgressBar

@property
def is_determinate(self):
return self.native.getMax() != 0

@property
def is_animating_indeterminate(self):
# The Android "isIndeterminate" attribute encompasses animation status
return self.native.isIndeterminate()

@property
def position(self):
return self.native.getProgress() / self.native.getMax()
27 changes: 26 additions & 1 deletion android/tests_backend/widgets/slider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from java import jclass

from android.os import Build
from android.os import Build, SystemClock
from android.view import MotionEvent

from .base import SimpleProbe

Expand All @@ -15,10 +16,34 @@ def position(self):
def change(self, position):
self.native.setProgress(self._min + round(position * (self._max - self._min)))

@property
def tick_count(self):
if self.native.getTickMark():
return self._max - self._min + 1
else:
return None

@property
def _min(self):
return 0 if (Build.VERSION.SDK_INT < 26) else self.native.getMin()

@property
def _max(self):
return self.native.getMax()

async def press(self):
self.native.onTouchEvent(self.motion_event(MotionEvent.ACTION_DOWN))

async def release(self):
self.native.onTouchEvent(self.motion_event(MotionEvent.ACTION_UP))

def motion_event(self, action):
time = SystemClock.uptimeMillis()
return MotionEvent.obtain(
time, # downTime
time, # eventTime
action,
self.width / 2,
self.height / 2,
0, # metaState
)
1 change: 1 addition & 0 deletions changes/1708.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Slider widget now has 100% test coverage, and complete API documentation.
1 change: 1 addition & 0 deletions changes/1825.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ProgressBar widget now has 100% test coverage, and complete API documentation.
5 changes: 0 additions & 5 deletions cocoa/src/toga_cocoa/libs/appkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,11 +621,6 @@ class NSLineBreakMode(Enum):
NSProgressIndicatorBarStyle = 0
NSProgressIndicatorSpinningStyle = 1

######################################################################
# NSRunLoop.h

NSDefaultRunLoopMode = c_void_p.in_dll(appkit, "NSDefaultRunLoopMode")

######################################################################
# NSRunningApplication.h

Expand Down
4 changes: 0 additions & 4 deletions cocoa/src/toga_cocoa/widgets/activityindicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ def create(self):
# Add the layout constraints
self.add_constraints()

def get_enabled(self):
# An activity indicator is always enabled
return True

def is_running(self):
return self._is_running

Expand Down
1 change: 1 addition & 0 deletions cocoa/src/toga_cocoa/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

class Widget:
def __init__(self, interface):
super().__init__()
self.interface = interface
self.interface._impl = self
self._container = None
Expand Down
4 changes: 0 additions & 4 deletions cocoa/src/toga_cocoa/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ def create(self):
# Add the layout constraints
self.add_constraints()

def get_enabled(self):
# A box is always enabled
return True

def rehint(self):
content_size = self.native.intrinsicContentSize()
self.interface.intrinsic.width = at_least(content_size.width)
Expand Down
4 changes: 0 additions & 4 deletions cocoa/src/toga_cocoa/widgets/divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ def create(self):
# Set the initial direction
self._direction = self.interface.HORIZONTAL

def get_enabled(self):
# A Divider is always enabled
return True

def rehint(self):
content_size = self.native.intrinsicContentSize()

Expand Down
Loading

0 comments on commit d4dac30

Please sign in to comment.