Skip to content

Commit

Permalink
Merge pull request #1708 from mhsmith/test-slider-windows-android
Browse files Browse the repository at this point in the history
[widget audit] Slider
  • Loading branch information
freakboy3742 authored Apr 7, 2023
2 parents 3ccf0ae + 90cba5d commit 85eb59c
Show file tree
Hide file tree
Showing 46 changed files with 1,355 additions and 611 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 @@ -27,6 +27,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: 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 @@ -96,5 +96,5 @@ def assert_height(self, min_height, max_height):
def background_color(self):
return toga_color(self.native.getBackground().getColor())

def press(self):
async def press(self):
self.native.performClick()
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.
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
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 @@ -5,6 +5,7 @@

class Widget:
def __init__(self, interface):
super().__init__()
self.interface = interface
self.interface._impl = self
self._container = None
Expand Down
22 changes: 16 additions & 6 deletions cocoa/src/toga_cocoa/widgets/slider.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from travertino.size import at_least

import toga
from toga_cocoa.libs import (
SEL,
NSEventType,
Expand All @@ -26,7 +27,7 @@ def onSlide_(self, sender) -> None:
self.interface.on_change(None)


class Slider(Widget):
class Slider(Widget, toga.widgets.slider.SliderImpl):
def create(self):
self.native = TogaSlider.alloc().init()
self.native.interface = self.interface
Expand All @@ -35,13 +36,19 @@ def create(self):
self.native.target = self.native
self.native.action = SEL("onSlide:")

self.set_tick_count(self.interface.tick_count)

self.add_constraints()

def get_tick_count(self):
return (
self.native.numberOfTickMarks
if self.native.allowsTickMarkValuesOnly
else None
)

def set_tick_count(self, tick_count):
if tick_count is None:
self.native.allowsTickMarkValuesOnly = False
self.native.numberOfTickMarks = 0
else:
self.native.allowsTickMarkValuesOnly = True
self.native.numberOfTickMarks = tick_count
Expand All @@ -52,11 +59,14 @@ def get_value(self):
def set_value(self, value):
self.native.doubleValue = value

def get_range(self):
return (self.native.minValue, self.native.maxValue)

def set_range(self, range):
self.native.minValue = self.interface.range[0]
self.native.maxValue = self.interface.range[1]
self.native.minValue = range[0]
self.native.maxValue = range[1]

def rehint(self):
content_size = self.native.intrinsicContentSize()
self.interface.intrinsic.height = content_size.height
self.interface.intrinsic.width = at_least(self.interface.MIN_WIDTH)
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
39 changes: 38 additions & 1 deletion cocoa/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
import asyncio
from ctypes import c_void_p

from rubicon.objc import SEL, NSArray, NSObject, ObjCClass, objc_method
from rubicon.objc.api import NSString

from toga.colors import TRANSPARENT
from toga.fonts import CURSIVE, FANTASY, MONOSPACE, SANS_SERIF, SERIF, SYSTEM
from toga_cocoa.libs.appkit import appkit

from .properties import toga_color

NSRunLoop = ObjCClass("NSRunLoop")
NSRunLoop.declare_class_property("currentRunLoop")
NSDefaultRunLoopMode = NSString(c_void_p.in_dll(appkit, "NSDefaultRunLoopMode"))


class EventListener(NSObject):
@objc_method
def init(self):
self.event = asyncio.Event()
return self

@objc_method
def onEvent(self):
self.event.set()
self.event.clear()


class SimpleProbe:
def __init__(self, widget):
self.widget = widget
self.native = widget._impl.native
assert isinstance(self.native, self.native_class)

self.event_listener = EventListener.alloc().init()

async def post_event(self, event):
self.native.window.postEvent(event, atStart=False)

# Add another event to the queue behind the original event, to notify us once
# it's been processed.
NSRunLoop.currentRunLoop.performSelector(
SEL("onEvent"),
target=self.event_listener,
argument=None,
order=0,
modes=NSArray.arrayWithObject(NSDefaultRunLoopMode),
)
await self.event_listener.event.wait()

def assert_container(self, container):
container_native = container._impl.native
for control in container_native.subviews:
Expand Down Expand Up @@ -78,5 +115,5 @@ def background_color(self):
else:
return TRANSPARENT

def press(self):
async def press(self):
self.native.performClick(None)
42 changes: 40 additions & 2 deletions cocoa/tests_backend/widgets/slider.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from toga_cocoa.libs import NSSlider
from rubicon.objc import NSPoint

from toga_cocoa.libs import NSEvent, NSEventType, NSSlider

from .base import SimpleProbe

Expand All @@ -11,7 +13,16 @@ def position(self):
return (self.native.doubleValue - self._min) / (self._max - self._min)

def change(self, position):
self.native.doubleValue = self._min + round(position * (self._max - self._min))
self.native.doubleValue = self._min + (position * (self._max - self._min))
self.native.performClick(None) # Setting the value doesn't trigger the action.

@property
def tick_count(self):
if self.native.allowsTickMarkValuesOnly:
return self.native.numberOfTickMarks
else:
assert self.native.numberOfTickMarks == 0
return None

@property
def _min(self):
Expand All @@ -20,3 +31,30 @@ def _min(self):
@property
def _max(self):
return self.native.maxValue

async def press(self):
await self.mouse_event(NSEventType.LeftMouseDown)

async def release(self):
await self.mouse_event(NSEventType.LeftMouseUp)

# Synthesizing this event doesn't trigger the action, even though a real event
# does (https://github.com/beeware/toga/pull/1708#issuecomment-1490964061).
self.native.performClick(None)

async def mouse_event(self, event_type):
await self.post_event(
NSEvent.mouseEventWithType(
event_type,
location=self.native.convertPoint(
NSPoint(self.width / 2, self.height / 2), toView=None
),
modifierFlags=0,
timestamp=0,
windowNumber=self.native.window.windowNumber,
context=None,
eventNumber=0,
clickCount=1,
pressure=1.0 if event_type == NSEventType.LeftMouseDown else 0.0,
),
)
1 change: 1 addition & 0 deletions core/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ docs =
sphinx == 6.1.3
sphinx_tabs == 3.4.1
sphinx-autobuild == 2021.3.14
sphinx-autodoc-typehints == 1.22
sphinx-csv-filter == 0.4.0
sphinxcontrib-spelling == 7.7.0

Expand Down
2 changes: 1 addition & 1 deletion core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def on_close(self, handler):


class App:
"""The App is the top level of any GUI program. It is the manager of all
"""The App is xx the top level of any GUI program. It is the manager of all
the other bits of the GUI app: the main window and events that window
generates like user input.
Expand Down
Loading

0 comments on commit 85eb59c

Please sign in to comment.