Skip to content

Commit

Permalink
Merge pull request #2011 from freakboy3742/audit-table
Browse files Browse the repository at this point in the history
[widget audit] toga.Table
  • Loading branch information
freakboy3742 committed Aug 28, 2023
2 parents 65bb12e + b6c0d04 commit da6e7b3
Show file tree
Hide file tree
Showing 93 changed files with 3,047 additions and 1,440 deletions.
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"

0 comments on commit da6e7b3

Please sign in to comment.