textual icon indicating copy to clipboard operation
textual copied to clipboard

Select a range of rows in a DataTable

Open davetapley opened this issue 7 months ago • 3 comments

Implements:

  • https://github.com/Textualize/textual/discussions/3606

davetapley avatar Dec 06 '23 22:12 davetapley

(FYI 3965 has been merged.)

rodrigogiraoserrao avatar Jan 08 '24 10:01 rodrigogiraoserrao

Thanks @rodrigogiraoserrao, your fix also fixed it for this 🙏🏻

davetapley avatar Jan 08 '24 17:01 davetapley

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()
Screenshot 2024-01-09 at 4 45 17 PM

sstoops avatar Jan 10 '24 00:01 sstoops