Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3334.5.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Web backend now supports Table widgets.
6 changes: 4 additions & 2 deletions docs/reference/api/widgets/table.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ necessary.

Not supported

.. group-tab:: Web |no|
.. group-tab:: Web

Not supported
.. figure:: /reference/images/table-web.png
:align: center
:width: 450px

.. group-tab:: Textual |no|

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/data/widgets_by_platform.csv
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ProgressBar,General Widget,:class:`~toga.ProgressBar`,Progress Bar,|y|,|y|,|y|,|
Selection,General Widget,:class:`~toga.Selection`,A widget to select a single option from a list of alternatives.,|y|,|y|,|y|,|y|,|y|,,
Slider,General Widget,:class:`~toga.Slider`,Slider,|y|,|y|,|y|,|y|,|y|,,
Switch,General Widget,:class:`~toga.Switch`,Switch,|y|,|y|,|y|,|y|,|y|,|b|,
Table,General Widget,:class:`~toga.Table`,A widget for displaying columns of tabular data.,|y|,|y|,|y|,,|b|,,
Table,General Widget,:class:`~toga.Table`,A widget for displaying columns of tabular data.,|y|,|y|,|y|,,|b|,|b|,
TextInput,General Widget,:class:`~toga.TextInput`,A widget for the display and editing of a single line of text.,|y|,|y|,|y|,|y|,|y|,|b|,|b|
TimeInput,General Widget,:class:`~toga.TimeInput`,A widget to select a clock time,,,|y|,,|y|,|b|,
Tree,General Widget,:class:`~toga.Tree`,A widget for displaying a hierarchical tree of tabular data.,|y|,|y|,,,,,
Expand Down
Binary file added docs/reference/images/table-web.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 2 additions & 3 deletions web/src/toga_web/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
# from .widgets.slider import Slider
# from .widgets.splitcontainer import SplitContainer
from .widgets.switch import Switch

# from .widgets.table import Table
from .widgets.table import Table
from .widgets.textinput import TextInput
from .widgets.timeinput import TimeInput

Expand Down Expand Up @@ -80,7 +79,7 @@ def not_implemented(feature):
# 'Slider',
# 'SplitContainer',
"Switch",
# 'Table',
"Table",
"TextInput",
# 'Tree',
# 'WebView',
Expand Down
179 changes: 179 additions & 0 deletions web/src/toga_web/widgets/table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import warnings

import toga
from toga_web.libs import create_proxy

from .base import Widget


# placeholder from gtk
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to describe provenance here; it's either a common utility (in which case it should be factored out into core), or it's a standalone platform-specific implementation.

In this case, I'd lean to the latter.

class TogaRow:
def __init__(self, value):
super().__init__()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no base class, the call to super() is a no-op.

self.value = value

# All paths return none as Icon is not implemented in web.
def icon(self, attr):
data = getattr(self.value, attr, None)
if isinstance(data, tuple):
if data[0] is not None:
return None
return None
else:
try:
return None
except AttributeError:
return None

def text(self, attr, missing_value):
data = getattr(self.value, attr, None)

if isinstance(data, toga.Widget):
warnings.warn("Web does not support the use of widgets in cells")
text = None
elif isinstance(data, tuple):
text = data[1]
else:
text = data

if text is None:
return missing_value

return str(text)


class Table(Widget):
def create(self):

self.native = self._create_native_widget(
"div", classes=["toga-table-container"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this class? It's not a container, and .toga table will provide a unique match from a style perspective.

)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this outer container required? Why can the <table> not be the container?


self.table = self._create_native_widget(
"table",
)
self.native.appendChild(self.table)

self.table_header_group = self._create_native_widget(
"thead", classes=["table-header"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again - why the class here? table.toga thead will match here.

)
self.table.appendChild(self.table_header_group)

self.table_body = self._create_native_widget("tbody", classes=["table-body"])
self.table.appendChild(self.table_body)

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

# remove old table data
for row_child in list(self.table_body.children):
for td_child in list(row_child.children):
row_child.removeChild(td_child)
self.table_body.removeChild(row_child)

for row_child in list(self.table_header_group.children):
for td_child in list(row_child.children):
row_child.removeChild(td_child)
self.table_header_group.removeChild(row_child)

if source is not None:
self._create_table_headers()

for i, row in enumerate(source):
self._create_table_row(row, i)

# set table here
self.refresh()

def get_selection(self):
selection = sorted(self.selection)
if self.interface.multiple_select:
return selection
elif len(selection) == 0:
return None
else:
return selection[0]

def add_selection(self, index, table_row):
self.selection[index] = table_row
table_row.style.backgroundColor = "lightblue"
# set colour
Comment on lines +99 to +100
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to do this with a stylesheet - add a selected class, and then add to the default CSS stylesheet that tr.selected has a background color of lightblue.

We should probably also get a better color match than CSS lightblue. Shoelace provides default colors - the titlebar is --sl-color-primary-800. I'd suggest selection is likely --sl-color-primary-100 or -200.


def remove_selection(self, index):
table_row = self.selection.pop(index)
table_row.style.backgroundColor = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added bonus here if you use a style-based approach - you can delete a class and have all the style changes re-apply.


def clear_selection(self):
for index in list(self.selection):
self.remove_selection(index)

def _create_table_headers(self):
if self.interface.headings:
headings = self.interface.headings
else:
headings = self.interface.accessors
self.table_header_row = self._create_native_widget(
"tr", classes=["table-header-row"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the class needed here? It can already be targeted with thead tr

)

for heading in headings:
th = self._create_native_widget("th", content=heading)
self.table_header_row.appendChild(th)

self.table_header_group.appendChild(self.table_header_row)

def _create_table_row(self, item, index):
row = TogaRow(item)
values = []
for accessor in self.interface.accessors:
values.extend(
[
# Removed icon accessor for now as not sure how to handle icon
# row.icon(accessor),
row.text(accessor, self.interface.missing_value),
]
)
tr = self._create_native_widget(
"tr",
)
Comment on lines +136 to +138
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for this to be split over 3 lines. Drop the comma, and black will collapse the definition.


tr.addEventListener(
"click", create_proxy(lambda event: self.dom_row_click(event, index, tr))
)

for value in values:
td = self._create_native_widget("td", content=value)
tr.appendChild(td)
self.table_body.appendChild(tr)

def dom_row_click(self, event, index, table_row):
print("row_click listener! row:", index)
if index in self.selection:
self.remove_selection(index)
print("removing row ", index, " from selection")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can delete these stray debug lines

else:
if not self.interface.multiple_select:
self.clear_selection()
self.add_selection(index, table_row)
print("adding row ", index, " to selection")

# if self.interface.on_select:
# self.interface.on_select(self.interface)

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clearly works; but is it not possible to do a more selective index-based child insert? self.table_body.insertBefore(...new row..., self.table_body.childNodes[index]);

Similarly for remove and change.


def clear(self):
self.change_source(self.interface.data)

def change(self, item):
self.change_source(self.interface.data)

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

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

def remove_column(self, accessor):
self.change_source(self.interface.data)