From 976bd2f5c25fcc0eff0cbb717502804dde0e9f30 Mon Sep 17 00:00:00 2001 From: darrenburns Date: Thu, 6 Apr 2023 21:22:31 +0100 Subject: [PATCH] Move DataTable cursor with page up/down, home, end (#2228) * Add pageup and pagedown actions to DataTable, with no impls * Pagedown moves DataTable cursor now * Account for header height in pagedown action * Page Up support in the DataTable * Fix and off-by-1, ensure page up/down works on col cursor * Add placeholder scroll home/end action handlers to datatable * Add scroll home and scroll end * Hide hover cursor when home or end is used * Ensure home and end work correctly with all curosrs * Testing home/end/pagedown/pageup cursor movement in DataTable * Docstrings for new datatable actions * Fix a broken unit test for the DataTable --- src/textual/widgets/_data_table.py | 74 +++++++++++++++++++++++++++++- tests/test_data_table.py | 28 ++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 23cc42459..885a4963c 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -203,6 +203,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): Binding("down", "cursor_down", "Cursor Down", show=False), Binding("right", "cursor_right", "Cursor Right", show=False), Binding("left", "cursor_left", "Cursor Left", show=False), + Binding("pageup", "page_up", "Page Up", show=False), + Binding("pagedown", "page_down", "Page Down", show=False), ] """ | Key(s) | Description | @@ -247,7 +249,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): background: $surface ; color: $text; height: auto; - max-height: 100vh + max-height: 100vh; } DataTable > .datatable--header { text-style: bold; @@ -1976,6 +1978,76 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): self._scroll_cursor_into_view(animate=True) event.stop() + def action_page_down(self) -> None: + """Move the cursor one page down.""" + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): + height = self.size.height - self.header_height if self.show_header else 0 + + # Determine how many rows constitutes a "page" + offset = 0 + rows_to_scroll = 0 + row_index, column_index = self.cursor_coordinate + for ordered_row in self.ordered_rows[row_index:]: + offset += ordered_row.height + if offset > height: + break + rows_to_scroll += 1 + + self.cursor_coordinate = Coordinate( + row_index + rows_to_scroll - 1, column_index + ) + self._scroll_cursor_into_view() + else: + super().action_page_down() + + def action_page_up(self) -> None: + """Move the cursor one page up.""" + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): + height = self.size.height - self.header_height if self.show_header else 0 + + # Determine how many rows constitutes a "page" + offset = 0 + rows_to_scroll = 0 + row_index, column_index = self.cursor_coordinate + for ordered_row in self.ordered_rows[: row_index + 1]: + offset += ordered_row.height + if offset > height: + break + rows_to_scroll += 1 + + self.cursor_coordinate = Coordinate( + row_index - rows_to_scroll + 1, column_index + ) + self._scroll_cursor_into_view() + else: + super().action_page_up() + + def action_scroll_home(self) -> None: + """Scroll to the top of the data table.""" + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): + row_index, column_index = self.cursor_coordinate + self.cursor_coordinate = Coordinate(0, column_index) + self._scroll_cursor_into_view() + else: + super().action_scroll_home() + + def action_scroll_end(self) -> None: + """Scroll to the bottom of the data table.""" + self._set_hover_cursor(False) + cursor_type = self.cursor_type + if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): + row_index, column_index = self.cursor_coordinate + self.cursor_coordinate = Coordinate(self.row_count - 1, column_index) + self._scroll_cursor_into_view() + else: + super().action_scroll_end() + def action_cursor_up(self) -> None: self._set_hover_cursor(False) cursor_type = self.cursor_type diff --git a/tests/test_data_table.py b/tests/test_data_table.py index faea0436f..a7ae484a4 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -167,10 +167,36 @@ async def test_datatable_message_emission(): async def test_empty_table_interactions(): app = DataTableApp() async with app.run_test() as pilot: - await pilot.press("enter", "up", "down", "left", "right") + await pilot.press( + "enter", "up", "down", "left", "right", "home", "end", "pagedown", "pageup" + ) assert app.message_names == [] +async def test_cursor_movement_with_home_pagedown_etc(): + app = DataTableApp() + + async with app.run_test() as pilot: + table = app.query_one(DataTable) + table.add_columns("A", "B") + table.add_rows(ROWS) + await pilot.press("right", "pagedown") + await pilot.pause() + assert table.cursor_coordinate == Coordinate(2, 1) + + await pilot.press("pageup") + await pilot.pause() + assert table.cursor_coordinate == Coordinate(0, 1) + + await pilot.press("end") + await pilot.pause() + assert table.cursor_coordinate == Coordinate(2, 1) + + await pilot.press("home") + await pilot.pause() + assert table.cursor_coordinate == Coordinate(0, 1) + + async def test_add_rows(): app = DataTableApp() async with app.run_test():