Skip to content

Commit

Permalink
Merge branch 'main' into m1-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
mhsmith committed Apr 11, 2023
2 parents f430115 + acc5cbe commit 05a196f
Show file tree
Hide file tree
Showing 106 changed files with 2,211 additions and 843 deletions.
40 changes: 19 additions & 21 deletions android/src/toga_android/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from abc import abstractmethod

from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT, TRANSPARENT

from ..colors import native_color
Expand All @@ -16,7 +18,8 @@ def _get_activity(_cache=[]):
return _cache[0]
# See MainActivity.onCreate() for initialization of .singletonThis:
# https://github.com/beeware/briefcase-android-gradle-template/blob/3.7/%7B%7B%20cookiecutter.formal_name%20%7D%7D/app/src/main/java/org/beeware/android/MainActivity.java
if not MainActivity.singletonThis:
# This can't be tested because if it isn't set, nothing else will work.
if not MainActivity.singletonThis: # pragma: no cover
raise ValueError(
"Unable to find MainActivity.singletonThis from Python. This is typically set by "
"org.beeware.android.MainActivity.onCreate()."
Expand All @@ -39,8 +42,9 @@ def __init__(self, interface):
# they have been added to a container.
self.interface.style.reapply()

@abstractmethod
def create(self):
pass
...

def set_app(self, app):
pass
Expand All @@ -55,20 +59,18 @@ def container(self):
@container.setter
def container(self, container):
if self.container:
if container:
raise RuntimeError("Already have a container")
else:
# container is set to None, removing self from the container.native
self._container.native.removeView(self.native)
self._container.native.invalidate()
self._container = None
assert container is None, "Widget already has a container"

# container is set to None, removing self from the container.native
self._container.native.removeView(self.native)
self._container.native.invalidate()
self._container = None
elif container:
self._container = container
if self.native:
# When initially setting the container and adding widgets to the container,
# we provide no `LayoutParams`. Those are promptly added when Toga
# calls `widget.rehint()` and `widget.set_bounds()`.
self._container.native.addView(self.native)
# When initially setting the container and adding widgets to the container,
# we provide no `LayoutParams`. Those are promptly added when Toga
# calls `widget.rehint()` and `widget.set_bounds()`.
self._container.native.addView(self.native)

for child in self.interface.children:
child._impl.container = container
Expand All @@ -85,10 +87,10 @@ def focus(self):
self.native.requestFocus()

def get_tab_index(self):
self.interface.factory.not_implementated("Widget.get_tab_index()")
self.interface.factory.not_implemented("Widget.get_tab_index()")

def set_tab_index(self, tab_index):
self.interface.factory.not_implementated("Widget.set_tab_index()")
self.interface.factory.not_implemented("Widget.set_tab_index()")

# APPLICATOR

Expand All @@ -98,11 +100,6 @@ def set_bounds(self, x, y, width, height):
self.container.set_child_bounds(self, x, y, width, height)

def set_hidden(self, hidden):
view = View(self._native_activity)
if not view.getClass().isInstance(self.native):
# save guard for Widgets like Canvas that are not based on View
self.interface.factory.not_implemented("Widget.set_hidden()")
return
if hidden:
self.native.setVisibility(View.INVISIBLE)
else:
Expand Down Expand Up @@ -149,6 +146,7 @@ def remove_child(self, child):
def refresh(self):
self.rehint()

@abstractmethod
def rehint(self):
pass

Expand Down
3 changes: 3 additions & 0 deletions android/src/toga_android/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ def create(self):
)
self.native.setDrawHandler(DrawHandler(self.interface))

def set_hidden(self, hidden):
self.interface.factory.not_implemented("Canvas.set_hidden()")

def redraw(self):
pass

Expand Down
4 changes: 2 additions & 2 deletions android/src/toga_android/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ def set_on_change(self, handler):
self.native.addTextChangedListener(self._textChangedListener)

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface.MIN_WIDTH)
self.interface.intrinsic.height = at_least(self.interface.MIN_HEIGHT)
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()
Expand Down
2 changes: 1 addition & 1 deletion android/src/toga_android/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def is_valid(self):
self.interface.factory.not_implemented("TextInput.is_valid()")

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface.MIN_WIDTH)
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
# 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
2 changes: 1 addition & 1 deletion android/src/toga_android/widgets/webview.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def set_alignment(self, value):
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface.MIN_WIDTH)
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
# Refuse to call measure() if widget has no container, i.e., has no LayoutParams.
# Android's measure() throws NullPointerException if the widget has no LayoutParams.
if not self.native.getLayoutParams():
Expand Down
31 changes: 30 additions & 1 deletion android/tests_backend/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import asyncio

from java import dynamic_proxy
from pytest import approx

from android.view import ViewTreeObserver
from android.view import View, ViewTreeObserver
from toga.fonts import SYSTEM

from .properties import toga_color
Expand Down Expand Up @@ -49,6 +50,10 @@ def assert_container(self, container):
else:
raise AssertionError(f"cannot find {self.native} in {container_native}")

def assert_not_contained(self):
assert self.widget._impl.container is None
assert self.native.getParent() is None

def assert_alignment(self, expected):
assert self.alignment == expected

Expand Down Expand Up @@ -92,9 +97,33 @@ def assert_height(self, min_height, max_height):
min_height <= self.height <= max_height
), f"Height ({self.height}) not in range ({min_height}, {max_height})"

def assert_layout(self, size, position):
# Widget is contained
assert self.widget._impl.container is not None
assert self.native.getParent() is not None

# Size and position is as expected. Values must be scaled from DP, and
# compared inexactly due to pixel scaling
assert (
approx(self.native.getWidth() / self.scale_factor, rel=0.01),
approx(self.native.getHeight() / self.scale_factor, rel=0.01),
) == size
assert (
approx(self.native.getLeft() / self.scale_factor, rel=0.01),
approx(self.native.getTop() / self.scale_factor, rel=0.01),
) == position

@property
def background_color(self):
return toga_color(self.native.getBackground().getColor())

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

@property
def is_hidden(self):
return self.native.getVisibility() == View.INVISIBLE

@property
def has_focus(self):
return self.widget.app._impl.native.getCurrentFocus() == self.native
8 changes: 8 additions & 0 deletions android/tests_backend/widgets/textinput.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from java import jclass

from .label import LabelProbe


# On Android, a TextInput is just an editable TextView
class TextInputProbe(LabelProbe):
native_class = jclass("android.widget.EditText")
1 change: 1 addition & 0 deletions changes/1718.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The usage of the deprecated ``set_wmclass`` API by the GTK backend has been removed.
File renamed without changes.
58 changes: 37 additions & 21 deletions cocoa/src/toga_cocoa/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
class Constraints:
def __init__(self, widget):
"""
A wrapper object storing the constraints required to position a widget
at a precise location in its container.
Args:
widget (:class: toga-cocoa.Widget): The widget that should be constrained.
:param widget: The Widget implementation to be constrained.
"""
self.widget = widget
self._container = None
Expand All @@ -24,69 +25,84 @@ def __init__(self, widget):
self.left_constraint = None
self.top_constraint = None

# Deletion isn't an event we can programatically invoke; deletion
# of constraints can take several iterations before it occurs.
def __del__(self): # pragma: nocover
self._remove_constraints()

def _remove_constraints(self):
if self.container:
# print(f"Remove constraints for {self.widget} in {self.container}")
self.container.native.removeConstraint(self.width_constraint)
self.container.native.removeConstraint(self.height_constraint)
self.container.native.removeConstraint(self.left_constraint)
self.container.native.removeConstraint(self.top_constraint)

self.width_constraint.release()
self.height_constraint.release()
self.left_constraint.release()
self.top_constraint.release()

@property
def container(self):
return self._container

@container.setter
def container(self, value):
if value is None and self.container:
# print("Remove constraints for", self.widget, 'in', self.container)
self.container.native.removeConstraint(self.width_constraint)
self.container.native.removeConstraint(self.height_constraint)
self.container.native.removeConstraint(self.left_constraint)
self.container.native.removeConstraint(self.top_constraint)
self._container = value
else:
self._container = value
# print("Add constraints for", self.widget, 'in', self.container, self.widget.interface.layout)
self.left_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # NOQA:E501
# This will *always* remove and then add constraints. It relies on the base widget to
# *not* invoke this setter unless the container is actually changing.

self._remove_constraints()
self._container = value
if value is not None:
# print(f"Add constraints for {self.widget} in {self.container} {self.widget.interface.layout})
self.left_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # noqa: E501
self.widget.native,
NSLayoutAttributeLeft,
NSLayoutRelationEqual,
self.container.native,
NSLayoutAttributeLeft,
1.0,
10, # Use a dummy, non-zero value for now
)
).retain()
self.container.native.addConstraint(self.left_constraint)

self.top_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # NOQA:E501
self.top_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # noqa: E501
self.widget.native,
NSLayoutAttributeTop,
NSLayoutRelationEqual,
self.container.native,
NSLayoutAttributeTop,
1.0,
5, # Use a dummy, non-zero value for now
)
).retain()
self.container.native.addConstraint(self.top_constraint)

self.width_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # NOQA:E501
self.width_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # noqa: E501
self.widget.native,
NSLayoutAttributeRight,
NSLayoutRelationEqual,
self.widget.native,
NSLayoutAttributeLeft,
1.0,
50, # Use a dummy, non-zero value for now
)
).retain()
self.container.native.addConstraint(self.width_constraint)

self.height_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # NOQA:E501
self.height_constraint = NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant_( # noqa: E501
self.widget.native,
NSLayoutAttributeBottom,
NSLayoutRelationEqual,
self.widget.native,
NSLayoutAttributeTop,
1.0,
30, # Use a dummy, non-zero value for now
)
).retain()
self.container.native.addConstraint(self.height_constraint)

def update(self, x, y, width, height):
if self.container:
# print("UPDATE", self.widget, 'in', self.container, 'to', x, y, width, height)
# print(f"UPDATE CONSTRAINTS {self.widget} in {self.container} {width}x{height}@{x},{y}")
self.left_constraint.constant = x
self.top_constraint.constant = y

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
28 changes: 15 additions & 13 deletions cocoa/src/toga_cocoa/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from abc import abstractmethod

from toga.colors import TRANSPARENT
from toga_cocoa.colors import native_color
from toga_cocoa.constraints import Constraints
Expand All @@ -15,8 +17,9 @@ def __init__(self, interface):
self.create()
self.interface.style.reapply()

@abstractmethod
def create(self):
raise NotImplementedError()
...

def set_app(self, app):
pass
Expand All @@ -31,13 +34,12 @@ def container(self):
@container.setter
def container(self, container):
if self.container:
if container:
raise RuntimeError("Already have a container")
else:
# existing container should be removed
self.constraints.container = None
self._container = None
self.native.removeFromSuperview()
assert container is None, "Widget already has a container"

# Existing container should be removed
self.constraints.container = None
self._container = None
self.native.removeFromSuperview()
elif container:
# setting container
self._container = container
Expand Down Expand Up @@ -73,8 +75,7 @@ def set_alignment(self, alignment):
pass

def set_hidden(self, hidden):
if self.native:
self.native.setHidden(hidden)
self.native.setHidden(hidden)

def set_font(self, font):
pass
Expand All @@ -93,10 +94,10 @@ def focus(self):
self.interface.window._impl.native.makeFirstResponder(self.native)

def get_tab_index(self):
self.interface.factory.not_implementated("Widget.get_tab_index()")
self.interface.factory.not_implemented("Widget.get_tab_index()")

def set_tab_index(self, tab_index):
self.interface.factory.not_implementated("Widget.set_tab_index()")
self.interface.factory.not_implemented("Widget.set_tab_index()")

# INTERFACE

Expand All @@ -120,5 +121,6 @@ def add_constraints(self):
def refresh(self):
self.rehint()

@abstractmethod
def rehint(self):
pass
...
Loading

0 comments on commit 05a196f

Please sign in to comment.