textual
textual copied to clipboard
Select a range of rows in a DataTable
Implements:
- https://github.com/Textualize/textual/discussions/3606
(FYI 3965 has been merged.)
Thanks @rodrigogiraoserrao, your fix also fixed it for this 🙏🏻
Awesome work! I'm eager to see some sort of native multi-selection abilities within the DataTable
widget. The contiguous requirement of this PR is, unfortunately, a non-starter for my particular needs.
I have a working version that I built for an internal tool at my company, but I must warn you, I wrote this about a day after I started using Textual. So I already know it's poorly implemented in many ways, but it works for our use-case and it's been in use for about a month now.
I'd like to take a closer look at your implementation and think through my own to see how I could "meet in the middle".
class MultiSelectDataTable(DataTable):
_row_selected = Text("[X]")
_row_unselected = Text("[ ]")
selected: var[list[RowKey]] = var([])
BINDINGS = [
("c", "clear()", "Clear selection"),
("a", "select_all()", "Select all"),
]
def action_clear(self) -> None:
"""Clear the list of selected rows."""
self.selected = []
def action_select_all(self) -> None:
"""Select all rows."""
self.selected = list(self.rows.keys())
def on_key(self, event: events.Key) -> None:
row_key, _ = self.coordinate_to_cell_key(self.cursor_coordinate)
if event.key == "enter":
# We catch the "selection" of a row here instead of the RowSelected
# event because the latter will be fired upon a single mouse click of a
# row, which isn't what we want.
event.stop()
self.post_message(FilterTable.Submitted(self.selected if self.selected else [row_key]))
elif event.key == "space":
event.stop()
self.toggle_row_selection(row_key)
@on(DataTable.RowLabelSelected)
def row_label_selected(self, event: DataTable.RowLabelSelected) -> None:
"""Toggle the selection of a row when the label is clicked."""
self.toggle_row_selection(event.row_key)
def toggle_row_selection(self, row_key: RowKey) -> None:
"""Toggle the selection of a row."""
row = self.rows[row_key]
if row.label == self._row_unselected:
self.selected = self.selected + [row_key]
else:
# Rewrite the value to trigger reactivity
self.selected = [x for x in self.selected if x != row_key]
def watch_selected(self, value: list[RowKey]) -> None:
"""Change the selection label of the selected rows."""
# Clear the render caches to get the updated label to render
self._clear_caches()
for row_key, row in self.rows.items():
if row_key in value:
row.label = self._row_selected
else:
row.label = self._row_unselected
# Refresh the table to redraw the selection boxes
self.refresh()