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.Table #2011

Merged
merged 46 commits into from Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
88d8b29
Update docstrings and port Table tests to pytest.
freakboy3742 Jun 23, 2023
de52103
Add docs for Table.
freakboy3742 Jun 23, 2023
e6137c4
Add changenotes.
freakboy3742 Jun 23, 2023
0ddc6ab
Add deletion by index to sources, and notifications for new attribute…
freakboy3742 Jun 24, 2023
e36ca88
Cocoa Table tested for all but icons and widget cells.
freakboy3742 Jun 24, 2023
f63c715
100% coverage and testbed for icons.
freakboy3742 Jun 25, 2023
7f1d922
Run spell check before links; it's faster, and more likely to fail.
freakboy3742 Jun 25, 2023
c7dba15
iOS doesn't have a Table widget.
freakboy3742 Jun 25, 2023
a474991
Include test files in manifest.
freakboy3742 Jun 25, 2023
1e6e44d
Test fixes for py3.7 and tox.
freakboy3742 Jun 25, 2023
3be907c
Cocoa Table to 100% coverage.
freakboy3742 Jun 26, 2023
399baf3
Correct row position calculation.
freakboy3742 Jun 26, 2023
564ab0f
Simplify activation handler.
freakboy3742 Jun 26, 2023
8e9e521
Update examples to reflect changes to table.
freakboy3742 Jun 26, 2023
cf121f4
Fix Windows failure
mhsmith Jun 26, 2023
bf7404a
Make Selection consistently unfocusable on Android
mhsmith Jun 26, 2023
e8c8983
Add explicit warnings for handler name change.
freakboy3742 Jun 26, 2023
dd70006
Add an explicit note that ListSource is list-like.
freakboy3742 Jun 26, 2023
81a4f25
Remove a platform test that is no longer needed.
freakboy3742 Jun 27, 2023
358cdc1
Switch GTK to use GdkPixBuf as the native Icon widget, and require a …
freakboy3742 Jun 27, 2023
0e4c2ad
Make the pause after a keystroke a default behavior.
freakboy3742 Jun 27, 2023
e05b675
GTK Table widget to 100%.
freakboy3742 Jun 27, 2023
77a7e4d
Add documentation notes about icons and widgets in Table.
freakboy3742 Jun 27, 2023
0e09760
Mark Android as 'done'; the implementation isn't complete enough for …
freakboy3742 Jun 27, 2023
8c57ecf
Add test that columns fill the available space.
freakboy3742 Jun 27, 2023
903fb17
Increase the allowance for inter-column padding.
freakboy3742 Jun 28, 2023
80b9853
Small cleanups of core API tests.
freakboy3742 Jun 29, 2023
210402d
Add keyboard shortcuts for select-all on Cocoa (copied from Tree)
freakboy3742 Jun 29, 2023
c92f36a
Add error handling for bad icon files.
freakboy3742 Jul 8, 2023
347abf9
Merge branch 'main' into audit-table
freakboy3742 Jul 17, 2023
fef0258
Merge branch 'main' into audit-table
freakboy3742 Jul 26, 2023
e0124c7
Merge branch 'main' into audit-table
freakboy3742 Jul 26, 2023
b23d4d5
Merge branch 'main' into audit-table
freakboy3742 Aug 4, 2023
ec1ac83
Merge branch 'main' into audit-table
mhsmith Aug 22, 2023
1e8deea
Documentation cleanups
mhsmith Aug 22, 2023
0c17678
Icon documentation cleanups
mhsmith Aug 22, 2023
b76e453
Apply suggestions from code review
mhsmith Aug 23, 2023
7c67ded
Avoid wrapping in examples
mhsmith Aug 23, 2023
ef88f58
More docs clarifications
mhsmith Aug 23, 2023
673b061
Merge branch 'main' into audit-table
mhsmith Aug 23, 2023
3d3641c
Winforms Table to 100% coverage
mhsmith Aug 24, 2023
37540d1
Winforms Icon to 100% coverage
mhsmith Aug 24, 2023
284b13f
Don't require final scroll position to be equal to starting position
mhsmith Aug 24, 2023
6e116c9
Android Table to 100% coverage
mhsmith Aug 27, 2023
1784000
Android Icon to 100% coverage
mhsmith Aug 27, 2023
b6c0d04
Add tests for accessors returning None, and fix on Cocoa and Winforms
mhsmith Aug 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions android/src/toga_android/icons.py
@@ -1,3 +1,6 @@
from android.graphics import BitmapFactory


class Icon:
EXTENSIONS = [".png"]
SIZES = None
Expand All @@ -6,3 +9,7 @@ def __init__(self, interface, path):
self.interface = interface
self.interface._impl = self
self.path = path

self.native = BitmapFactory.decodeFile(str(path))
if self.native is None:
raise ValueError(f"Unable to load icon from {path}")
1 change: 1 addition & 0 deletions android/src/toga_android/libs/android/view.py
Expand Up @@ -2,6 +2,7 @@

Gravity = JavaClass("android/view/Gravity")
OnClickListener = JavaInterface("android/view/View$OnClickListener")
OnLongClickListener = JavaInterface("android/view/View$OnLongClickListener")
Menu = JavaClass("android/view/Menu")
MenuItem = JavaClass("android/view/MenuItem")
MotionEvent = JavaClass("android/view/MotionEvent")
Expand Down
178 changes: 82 additions & 96 deletions android/src/toga_android/widgets/table.py
Expand Up @@ -2,11 +2,9 @@

from ..libs.activity import MainActivity
from ..libs.android import R__attr
from ..libs.android.graphics import Typeface
from ..libs.android.view import Gravity, OnClickListener, View__MeasureSpec
from ..libs.android.graphics import Rect, Typeface
from ..libs.android.view import Gravity, OnClickListener, OnLongClickListener
from ..libs.android.widget import (
HorizontalScrollView,
LinearLayout,
LinearLayout__LayoutParams,
ScrollView,
TableLayout,
Expand All @@ -25,92 +23,90 @@ def __init__(self, impl):

def onClick(self, view):
tr_id = view.getId()
row = self.impl.interface.data[tr_id]
if self.impl.interface.multiple_select:
if tr_id in self.impl.selection:
self.impl.selection.pop(tr_id)
view.setBackgroundColor(self.impl.color_unselected)
self.impl.remove_selection(tr_id)
else:
self.impl.selection[tr_id] = row
view.setBackgroundColor(self.impl.color_selected)
self.impl.add_selection(tr_id, view)
else:
self.impl.clear_selection()
self.impl.selection[tr_id] = row
view.setBackgroundColor(self.impl.color_selected)
if self.impl.interface.on_select:
self.impl.interface.on_select(self.impl.interface, row=row)
self.impl.add_selection(tr_id, view)
self.impl.interface.on_select(None)


class TogaOnLongClickListener(OnLongClickListener):
def __init__(self, impl):
super().__init__()
self.impl = impl

def onLongClick(self, view):
self.impl.clear_selection()
index = view.getId()
self.impl.add_selection(index, view)
self.impl.interface.on_select(None)
self.impl.interface.on_activate(None, row=self.impl.interface.data[index])
return True


class Table(Widget):
table_layout = None
color_selected = None
color_unselected = None
selection = {}
_deleted_column = None
_font_impl = None

def create(self):
# get the selection color from the current theme
current_theme = MainActivity.singletonThis.getApplication().getTheme()
attrs = [R__attr.colorBackground, R__attr.colorControlHighlight]
typed_array = current_theme.obtainStyledAttributes(attrs)
typed_array = self._native_activity.obtainStyledAttributes(attrs)
self.color_unselected = typed_array.getColor(0, 0)
self.color_selected = typed_array.getColor(1, 0)
typed_array.recycle()

parent = LinearLayout(self._native_activity)
parent.setOrientation(LinearLayout.VERTICAL)
parent_layout_params = LinearLayout__LayoutParams(
LinearLayout__LayoutParams.MATCH_PARENT,
LinearLayout__LayoutParams.MATCH_PARENT,
)
parent_layout_params.gravity = Gravity.TOP
parent.setLayoutParams(parent_layout_params)
vscroll_view = ScrollView(self._native_activity)
# add vertical scroll view
self.native = vscroll_view = ScrollView(self._native_activity)
vscroll_view_layout_params = LinearLayout__LayoutParams(
LinearLayout__LayoutParams.MATCH_PARENT,
LinearLayout__LayoutParams.MATCH_PARENT,
)
vscroll_view_layout_params.gravity = Gravity.TOP
vscroll_view.setLayoutParams(vscroll_view_layout_params)

self.table_layout = TableLayout(MainActivity.singletonThis)
table_layout_params = TableLayout__Layoutparams(
TableLayout__Layoutparams.MATCH_PARENT,
TableLayout__Layoutparams.WRAP_CONTENT,
)
# add horizontal scroll view
hscroll_view = HorizontalScrollView(self._native_activity)
hscroll_view_layout_params = LinearLayout__LayoutParams(
LinearLayout__LayoutParams.MATCH_PARENT,
LinearLayout__LayoutParams.MATCH_PARENT,
)
hscroll_view_layout_params.gravity = Gravity.LEFT
vscroll_view.addView(hscroll_view, hscroll_view_layout_params)

# add table layout to scrollbox
self.table_layout.setLayoutParams(table_layout_params)
hscroll_view.addView(self.table_layout)
# add scroll box to parent layout
parent.addView(vscroll_view, vscroll_view_layout_params)
self.native = parent
if self.interface.data is not None:
self.change_source(self.interface.data)
vscroll_view.addView(self.table_layout)

def change_source(self, source):
self.selection = {}
self.table_layout.removeAllViews()

# StretchAllColumns mode causes a divide by zero error if there are no columns.
self.table_layout.setStretchAllColumns(bool(self.interface.accessors))

if source is not None:
self.table_layout.addView(self.create_table_header())
if self.interface.headings is not None:
self.table_layout.addView(self.create_table_header())
for row_index in range(len(source)):
table_row = self.create_table_row(row_index)
self.table_layout.addView(table_row)
self.table_layout.invalidate()

def add_selection(self, index, table_row):
self.selection[index] = table_row
table_row.setBackgroundColor(self.color_selected)

def remove_selection(self, index):
table_row = self.selection.pop(index)
table_row.setBackgroundColor(self.color_unselected)

def clear_selection(self):
for i in range(self.table_layout.getChildCount()):
row = self.table_layout.getChildAt(i)
row.setBackgroundColor(self.color_unselected)
self.selection = {}
for index in list(self.selection):
self.remove_selection(index)

def create_table_header(self):
table_row = TableRow(MainActivity.singletonThis)
Expand All @@ -119,14 +115,11 @@ def create_table_header(self):
)
table_row.setLayoutParams(table_row_params)
for col_index in range(len(self.interface._accessors)):
if self.interface._accessors[col_index] == self._deleted_column:
continue
text_view = TextView(MainActivity.singletonThis)
text_view.setText(self.interface.headings[col_index])
if self._font_impl:
self._font_impl.apply(
text_view, text_view.getTextSize(), text_view.getTypeface()
)
self._font_impl.apply(
text_view, text_view.getTextSize(), text_view.getTypeface()
)
text_view.setTypeface(
Typeface.create(
text_view.getTypeface(),
Expand All @@ -150,16 +143,15 @@ def create_table_row(self, row_index):
table_row.setLayoutParams(table_row_params)
table_row.setClickable(True)
table_row.setOnClickListener(TogaOnClickListener(impl=self))
table_row.setLongClickable(True)
table_row.setOnLongClickListener(TogaOnLongClickListener(impl=self))
table_row.setId(row_index)
for col_index in range(len(self.interface._accessors)):
if self.interface._accessors[col_index] == self._deleted_column:
continue
text_view = TextView(MainActivity.singletonThis)
text_view.setText(self.get_data_value(row_index, col_index))
if self._font_impl:
self._font_impl.apply(
text_view, text_view.getTextSize(), text_view.getTypeface()
)
self._font_impl.apply(
text_view, text_view.getTextSize(), text_view.getTypeface()
)
text_view_params = TableRow__Layoutparams(
TableRow__Layoutparams.MATCH_PARENT, TableRow__Layoutparams.WRAP_CONTENT
)
Expand All @@ -170,68 +162,62 @@ def create_table_row(self, row_index):
return table_row

def get_data_value(self, row_index, col_index):
if self.interface.data is None or self.interface._accessors is None:
return None
row_object = self.interface.data[row_index]
value = getattr(
row_object,
self.interface._accessors[col_index],
self.interface.missing_value,
None,
)
return value

if isinstance(value, tuple): # TODO: support icons
value = value[1]
if value is None:
value = self.interface.missing_value
return str(value)

def get_selection(self):
selection = []
for row_index in range(len(self.interface.data)):
if row_index in self.selection:
selection.append(self.selection[row_index])
if len(selection) == 0:
selection = None
elif not self.interface.multiple_select:
selection = selection[0]
return selection

# data listener method
selection = sorted(self.selection)
if self.interface.multiple_select:
return selection
elif len(selection) == 0:
return None
else:
return selection[0]

def insert(self, index, item):
self.change_source(self.interface.data)

# data listener method
def clear(self):
self.change_source(self.interface.data)

def change(self, item):
self.interface.factory.not_implemented("Table.change()")

# data listener method
def remove(self, item, index):
self.change_source(self.interface.data)

def scroll_to_row(self, row):
pass

def set_on_select(self, handler):
pass
def remove(self, index, item):
self.change_source(self.interface.data)

def set_on_double_click(self, handler):
self.interface.factory.not_implemented("Table.set_on_double_click()")
def scroll_to_row(self, index):
if (index != 0) and (self.interface.headings is not None):
index += 1
table_row = self.table_layout.getChildAt(index)
table_row.requestRectangleOnScreen(
Rect(0, 0, 0, table_row.getHeight()),
True, # Immediate, not animated
)

def add_column(self, heading, accessor):
def insert_column(self, index, heading, accessor):
self.change_source(self.interface.data)

def remove_column(self, accessor):
self._deleted_column = accessor
def remove_column(self, index):
self.change_source(self.interface.data)
self._deleted_column = None

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

def set_font(self, font):
self._font_impl = font._impl
if self.interface.data is not None:
self.change_source(self.interface.data)
self.change_source(self.interface.data)

def rehint(self):
self.native.measure(
View__MeasureSpec.UNSPECIFIED,
View__MeasureSpec.UNSPECIFIED,
)
self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth())
self.interface.intrinsic.height = at_least(self.native.getMeasuredHeight())
self.interface.intrinsic.width = at_least(self.interface._MIN_WIDTH)
self.interface.intrinsic.height = at_least(self.interface._MIN_HEIGHT)
33 changes: 33 additions & 0 deletions android/tests_backend/icons.py
@@ -0,0 +1,33 @@
import pytest

from android.graphics import Bitmap

from .probe import BaseProbe


class IconProbe(BaseProbe):
# Android only supports 1 format, so the alternate is the same as the primary.
alternate_resource = "resources/icons/blue"

def __init__(self, app, icon):
super().__init__()
self.app = app
self.icon = icon
assert isinstance(self.icon._impl.native, Bitmap)

def assert_icon_content(self, path):
if path == "resources/icons/green":
assert (
self.icon._impl.path
== self.app.paths.app / "resources" / "icons" / "green.png"
)
elif path == "resources/icons/blue":
assert (
self.icon._impl.path
== self.app.paths.app / "resources" / "icons" / "blue.png"
)
else:
pytest.fail("Unknown icon resource")

def assert_default_icon_content(self):
assert self.icon._impl.path == self.app.paths.toga / "resources" / "toga.png"