-
-
Notifications
You must be signed in to change notification settings - Fork 780
Added table widget to web backend #3425
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
base: main
Are you sure you want to change the base?
Changes from all commits
1e345ac
979333b
1e515a6
aaac0a7
0ca9ef0
eceef8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
The Web backend now supports Table widgets. |
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 | ||
class TogaRow: | ||
def __init__(self, value): | ||
super().__init__() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this class? It's not a container, and |
||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this outer container required? Why can the |
||
|
||
self.table = self._create_native_widget( | ||
"table", | ||
) | ||
self.native.appendChild(self.table) | ||
|
||
self.table_header_group = self._create_native_widget( | ||
"thead", classes=["table-header"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again - why the class 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be better to do this with a stylesheet - add a We should probably also get a better color match than CSS |
||
|
||
def remove_selection(self, index): | ||
table_row = self.selection.pop(index) | ||
table_row.style.backgroundColor = "" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the class needed here? It can already be targeted with |
||
) | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? 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) |
There was a problem hiding this comment.
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.