From b474f98ec686eba28a79cdf75a8090015251987f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 4 May 2023 11:55:31 +0100 Subject: [PATCH 1/4] Add 'DataTable.move_cursor'. Related issues: #2472. --- CHANGELOG.md | 5 +++ src/textual/widgets/_data_table.py | 59 ++++++++++++++++++++++++++++++ tests/test_data_table.py | 54 +++++++++++++++++++++------ 3 files changed, 106 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b6aff249..f92571c1c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - The DataTable cursor is now scrolled into view when the cursor coordinate is changed programmatically https://github.com/Textualize/textual/issues/2459 + +### Added + +- Method `DataTable.move_cursor` https://github.com/Textualize/textual/issues/2472 + ## [0.23.0] - 2023-05-03 ### Fixed diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 5991c98db9..7550a95d11 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -309,6 +309,11 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): cursor_coordinate: Reactive[Coordinate] = Reactive( Coordinate(0, 0), repaint=False, always_update=True ) + """Current cursor [`Coordinate`][textual.coordinate.Coordinate]. + + This can be set programmatically or changed via the method + [`move_cursor`][textual.widgets.DataTable.move_cursor]. + """ hover_coordinate: Reactive[Coordinate] = Reactive( Coordinate(0, 0), repaint=False, always_update=True ) @@ -952,6 +957,60 @@ def watch_cursor_coordinate( self._highlight_column(new_coordinate.column) self._scroll_cursor_into_view() + def move_cursor( + self, + coordinate: Coordinate | None = None, + *, + row: int | None = None, + column: int | None = None, + animate: bool = False, + ) -> None: + """Move the cursor to the given position. + + The new cursor position can be specified as a coordinate or you can specify + only the new row/column for the cursor. + Specifying a coordinate is mutually exclusive with specifying either a new + row or new column. + + Example: + ```py + datatable = app.query_one(DataTable) + datatable.move_cursor(Coordinate(5, 7)) + # datatable.cursor_coordinate == Coordinate(5, 7) + datatable.move_cursor(row=3) + # datatable.cursor_coordinate == Coordinate(3, 7) + datatable.move_cursor(row=4, column=6) + # datatable.cursor_coordinate == Coordinate(4, 6) + datatable.move_cursor(Coordinate(0, 0), row=3) # RuntimeError + ``` + + Args: + coordinate: The new coordinate to move the cursor to. + row: The new row to move the cursor to. + column: The new column to move the cursor to. + animate: Whether to animate the change of coordinates. + + Raises: + RuntimeError: If the parameter `coordinate` is specified together with any + of `row` or `column`, or if no new position is specified. + """ + if coordinate is not None and (row is not None or column is not None): + raise RuntimeError("Can't specify `coordinate` and `row`/`column`.") + if coordinate is None and row is None and column is None: + raise RuntimeError("You must specify a new position to move the cursor to.") + + if coordinate is not None: + destination = coordinate + else: + cursor_row, cursor_column = self.cursor_coordinate + if row is not None: + cursor_row = row + if column is not None: + cursor_column = column + destination = Coordinate(cursor_row, cursor_column) + self.cursor_coordinate = destination + self._scroll_cursor_into_view(animate=animate) + def _highlight_coordinate(self, coordinate: Coordinate) -> None: """Apply highlighting to the cell at the coordinate, and post event.""" self.refresh_coordinate(coordinate) diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 0bba677cf7..60f4a26c1f 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -996,23 +996,53 @@ def test_key_string_lookup(): async def test_scrolling_cursor_into_view(): """Regression test for https://github.com/Textualize/textual/issues/2459""" - class TableApp(App): + class ScrollingApp(DataTableApp): CSS = "DataTable { height: 100%; }" - def compose(self): - yield DataTable() - - def on_mount(self) -> None: - table = self.query_one(DataTable) - table.add_column("n") - table.add_rows([(n,) for n in range(300)]) - def key_c(self): self.query_one(DataTable).cursor_coordinate = Coordinate(200, 0) - app = TableApp() + app = ScrollingApp() async with app.run_test() as pilot: + table = app.query_one(DataTable) + table.add_column("n") + table.add_rows([(n,) for n in range(300)]) + await pilot.press("c") - await pilot.pause() - assert app.query_one(DataTable).scroll_y > 100 + assert table.scroll_y > 100 + + +async def test_move_cursor(): + app = DataTableApp() + + async with app.run_test(): + table = app.query_one(DataTable) + table.add_columns(*"These are some columns in your nice table".split()) + table.add_rows(["These are some columns in your nice table".split()] * 10) + + table.move_cursor(Coordinate(5, 7)) + assert table.cursor_coordinate == Coordinate(5, 7) + table.move_cursor(row=3) + assert table.cursor_coordinate == Coordinate(3, 7) + table.move_cursor(row=4, column=6) + assert table.cursor_coordinate == Coordinate(4, 6) + table.move_cursor(column=3) + assert table.cursor_coordinate == Coordinate(4, 3) + + +async def test_move_cursor_raises_error(): + app = DataTableApp() + + async with app.run_test(): + table = app.query_one(DataTable) + table.add_columns(*"These are some columns in your nice table".split()) + table.add_rows(["These are some columns in your nice table".split()] * 10) + + coordinate = Coordinate(0, 0) + with pytest.raises(RuntimeError): + table.move_cursor(coordinate, row=1) + with pytest.raises(RuntimeError): + table.move_cursor(coordinate, column=1) + with pytest.raises(RuntimeError): + table.move_cursor(coordinate, row=1, column=1) From c1f52a6c5e76442fc3f76250d36a4c970ed6cb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 4 May 2023 12:07:46 +0100 Subject: [PATCH 2/4] Fix #2471. --- src/textual/widgets/_data_table.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 7550a95d11..40b4a83ccd 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -955,7 +955,9 @@ def watch_cursor_coordinate( elif self.cursor_type == "column": self.refresh_column(old_coordinate.column) self._highlight_column(new_coordinate.column) - self._scroll_cursor_into_view() + # If the coordinate was changed via `move_cursor`, give priority to its + # scrolling because it may be animated. + self.call_next(self._scroll_cursor_into_view) def move_cursor( self, From 65b3c37c81674db51c3fa0f402190b46f5726cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 4 May 2023 12:08:06 +0100 Subject: [PATCH 3/4] Simplify cursor changes. --- src/textual/widgets/_data_table.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 40b4a83ccd..5f14767efb 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -2113,7 +2113,6 @@ def action_page_down(self) -> None: self.cursor_coordinate = Coordinate( row_index + rows_to_scroll - 1, column_index ) - self._scroll_cursor_into_view() else: super().action_page_down() @@ -2137,7 +2136,6 @@ def action_page_up(self) -> None: self.cursor_coordinate = Coordinate( row_index - rows_to_scroll + 1, column_index ) - self._scroll_cursor_into_view() else: super().action_page_up() @@ -2148,7 +2146,6 @@ def action_scroll_home(self) -> None: 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() @@ -2159,7 +2156,6 @@ def action_scroll_end(self) -> None: 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() @@ -2168,7 +2164,6 @@ def action_cursor_up(self) -> None: cursor_type = self.cursor_type if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): self.cursor_coordinate = self.cursor_coordinate.up() - self._scroll_cursor_into_view() else: # If the cursor doesn't move up (e.g. column cursor can't go up), # then ensure that we instead scroll the DataTable. @@ -2179,7 +2174,6 @@ def action_cursor_down(self) -> None: cursor_type = self.cursor_type if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): self.cursor_coordinate = self.cursor_coordinate.down() - self._scroll_cursor_into_view() else: super().action_scroll_down() @@ -2187,8 +2181,7 @@ def action_cursor_right(self) -> None: self._set_hover_cursor(False) cursor_type = self.cursor_type if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"): - self.cursor_coordinate = self.cursor_coordinate.right() - self._scroll_cursor_into_view(animate=True) + self.move_cursor(self.cursor_coordinate.right(), animate=True) else: super().action_scroll_right() @@ -2196,8 +2189,7 @@ def action_cursor_left(self) -> None: self._set_hover_cursor(False) cursor_type = self.cursor_type if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"): - self.cursor_coordinate = self.cursor_coordinate.left() - self._scroll_cursor_into_view(animate=True) + self.move_cursor(self.cursor_coordinate.left(), animate=True) else: super().action_scroll_left() From b56c0e999eeafc96eb35c7ae6d25df460a280ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 4 May 2023 14:30:02 +0100 Subject: [PATCH 4/4] Address review feedback. Related comments: https://github.com/Textualize/textual/pull/2479\#discussion_r1185016002 --- src/textual/widgets/_data_table.py | 44 ++++++++---------------------- tests/test_data_table.py | 25 ++--------------- 2 files changed, 15 insertions(+), 54 deletions(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 5f14767efb..bc29490ef4 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -961,7 +961,6 @@ def watch_cursor_coordinate( def move_cursor( self, - coordinate: Coordinate | None = None, *, row: int | None = None, column: int | None = None, @@ -969,47 +968,26 @@ def move_cursor( ) -> None: """Move the cursor to the given position. - The new cursor position can be specified as a coordinate or you can specify - only the new row/column for the cursor. - Specifying a coordinate is mutually exclusive with specifying either a new - row or new column. - Example: ```py datatable = app.query_one(DataTable) - datatable.move_cursor(Coordinate(5, 7)) - # datatable.cursor_coordinate == Coordinate(5, 7) - datatable.move_cursor(row=3) - # datatable.cursor_coordinate == Coordinate(3, 7) datatable.move_cursor(row=4, column=6) # datatable.cursor_coordinate == Coordinate(4, 6) - datatable.move_cursor(Coordinate(0, 0), row=3) # RuntimeError + datatable.move_cursor(row=3) + # datatable.cursor_coordinate == Coordinate(3, 6) ``` Args: - coordinate: The new coordinate to move the cursor to. row: The new row to move the cursor to. column: The new column to move the cursor to. animate: Whether to animate the change of coordinates. - - Raises: - RuntimeError: If the parameter `coordinate` is specified together with any - of `row` or `column`, or if no new position is specified. """ - if coordinate is not None and (row is not None or column is not None): - raise RuntimeError("Can't specify `coordinate` and `row`/`column`.") - if coordinate is None and row is None and column is None: - raise RuntimeError("You must specify a new position to move the cursor to.") - - if coordinate is not None: - destination = coordinate - else: - cursor_row, cursor_column = self.cursor_coordinate - if row is not None: - cursor_row = row - if column is not None: - cursor_column = column - destination = Coordinate(cursor_row, cursor_column) + cursor_row, cursor_column = self.cursor_coordinate + if row is not None: + cursor_row = row + if column is not None: + cursor_column = column + destination = Coordinate(cursor_row, cursor_column) self.cursor_coordinate = destination self._scroll_cursor_into_view(animate=animate) @@ -2181,7 +2159,8 @@ def action_cursor_right(self) -> None: self._set_hover_cursor(False) cursor_type = self.cursor_type if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"): - self.move_cursor(self.cursor_coordinate.right(), animate=True) + self.cursor_coordinate = self.cursor_coordinate.right() + self._scroll_cursor_into_view(animate=True) else: super().action_scroll_right() @@ -2189,7 +2168,8 @@ def action_cursor_left(self) -> None: self._set_hover_cursor(False) cursor_type = self.cursor_type if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"): - self.move_cursor(self.cursor_coordinate.left(), animate=True) + self.cursor_coordinate = self.cursor_coordinate.left() + self._scroll_cursor_into_view(animate=True) else: super().action_scroll_left() diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 60f4a26c1f..a00da77afc 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -1021,28 +1021,9 @@ async def test_move_cursor(): table.add_columns(*"These are some columns in your nice table".split()) table.add_rows(["These are some columns in your nice table".split()] * 10) - table.move_cursor(Coordinate(5, 7)) - assert table.cursor_coordinate == Coordinate(5, 7) - table.move_cursor(row=3) - assert table.cursor_coordinate == Coordinate(3, 7) table.move_cursor(row=4, column=6) assert table.cursor_coordinate == Coordinate(4, 6) + table.move_cursor(row=3) + assert table.cursor_coordinate == Coordinate(3, 6) table.move_cursor(column=3) - assert table.cursor_coordinate == Coordinate(4, 3) - - -async def test_move_cursor_raises_error(): - app = DataTableApp() - - async with app.run_test(): - table = app.query_one(DataTable) - table.add_columns(*"These are some columns in your nice table".split()) - table.add_rows(["These are some columns in your nice table".split()] * 10) - - coordinate = Coordinate(0, 0) - with pytest.raises(RuntimeError): - table.move_cursor(coordinate, row=1) - with pytest.raises(RuntimeError): - table.move_cursor(coordinate, column=1) - with pytest.raises(RuntimeError): - table.move_cursor(coordinate, row=1, column=1) + assert table.cursor_coordinate == Coordinate(3, 3)