diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 7a94295de..62f0200de 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -336,18 +336,12 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): def watch_cursor_type(self, old: str, new: str) -> None: self._set_hover_cursor(False) - row_index, column_index = self.cursor_cell - - # Apply the highlighting to the newly relevant cells - if new == "cell": - self._highlight_cell(self.cursor_cell) - elif new == "row": - self._highlight_row(row_index) - elif new == "column": - self._highlight_column(column_index) + if self.show_cursor: + self._highlight_cursor() # Refresh cells that were previously impacted by the cursor # but may no longer be. + row_index, column_index = self.cursor_cell if old == "cell": self.refresh_cell(row_index, column_index) elif old == "row": @@ -357,6 +351,17 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): self._scroll_cursor_into_view() + def _highlight_cursor(self) -> None: + row_index, column_index = self.cursor_cell + cursor_type = self.cursor_type + # Apply the highlighting to the newly relevant cells + if cursor_type == "cell": + self._highlight_cell(self.cursor_cell) + elif cursor_type == "row": + self._highlight_row(row_index) + elif cursor_type == "column": + self._highlight_column(column_index) + def _update_dimensions(self, new_rows: Iterable[int]) -> None: """Called to recalculate the virtual (scrollable) size.""" for row_index in new_rows: @@ -492,6 +497,15 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): self.cursor_cell = self.cursor_cell self.check_idle() + # If a position has opened for the cursor to appear, where it previously + # could not (e.g. when there's no data in the table), then a highlighted + # event is emitted, since there's now a highlighted cell when there wasn't + # before. + cell_now_available = self.row_count == 1 and len(self.columns) > 0 + visible_cursor = self.show_cursor and self.cursor_type != "none" + if cell_now_available and visible_cursor: + self._highlight_cursor() + def add_rows(self, rows: Iterable[Iterable[CellType]]) -> None: """Add a number of rows. diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 78e9b7743..c511bf55a 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -1,11 +1,124 @@ from textual.app import App from textual.coordinate import Coordinate +from textual.message import Message from textual.widgets import DataTable class DataTableApp(App): + messages = [] + messages_to_record = { + "CellHighlighted", + "CellSelected", + "RowHighlighted", + "RowSelected", + "ColumnHighlighted", + "ColumnSelected", + } + def compose(self): - yield DataTable() + table = DataTable() + table.focus() + yield table + + def record_data_table_event(self, message: Message) -> None: + name = message.__class__.__name__ + if name in self.messages_to_record: + self.messages.append(name) + + async def _on_message(self, message: Message) -> None: + await super()._on_message(message) + self.record_data_table_event(message) + + +async def test_datatable_message_emission(): + app = DataTableApp() + messages = app.messages + expected_messages = [] + async with app.run_test() as pilot: + table = app.query_one(DataTable) + + assert messages == expected_messages + + table.add_columns("Column0", "Column1") + table.add_rows([["0/0", "0/1"], ["1/0", "1/1"], ["2/0", "2/1"]]) + + # A CellHighlighted is emitted because there were no rows (and + # therefore no highlighted cells), but then a row was added, and + # so the cell at (0, 0) became highlighted. + expected_messages.append("CellHighlighted") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # Pressing Enter when the cursor is on a cell emits a CellSelected + await pilot.press("enter") + expected_messages.append("CellSelected") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # Moving the cursor left and up when the cursor is at origin + # emits no events, since the cursor doesn't move at all. + await pilot.press("left", "up") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # ROW CURSOR + # Switch over to the row cursor... should emit a `RowHighlighted` + table.cursor_type = "row" + expected_messages.append("RowHighlighted") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # Select the row... + await pilot.press("enter") + expected_messages.append("RowSelected") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # COLUMN CURSOR + # Switching to the column cursor emits a `ColumnHighlighted` + table.cursor_type = "column" + expected_messages.append("ColumnHighlighted") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # Select the column... + await pilot.press("enter") + expected_messages.append("ColumnSelected") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # NONE CURSOR + # No messages get emitted at all... + table.cursor_type = "none" + await pilot.press("up", "down", "left", "right", "enter") + await pilot.pause(2 / 100) + # No new messages since cursor not visible + assert messages == expected_messages + + # Edge case - if show_cursor is False, and the cursor type + # is changed back to a visible type, then no messages should + # be emitted since the cursor is still not visible. + table.show_cursor = False + table.cursor_type = "cell" + await pilot.press("up", "down", "left", "right", "enter") + await pilot.pause(2 / 100) + # No new messages since show_cursor = False + assert messages == expected_messages + + # Now when show_cursor is set back to True, the appropriate + # message should be emitted for highlighting the cell. + table.show_cursor = True + expected_messages.append("CellHighlighted") + await pilot.pause(1 / 100) + assert messages == expected_messages + + # Likewise, if the cursor_type is "none", and we change the + # show_cursor to True, then no events should be raised since + # the cursor is still not visible to the user. + table.cursor_type = "none" + await pilot.press("up", "down", "left", "right", "enter") + await pilot.pause(2 / 100) + assert messages == expected_messages async def test_clear():