diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e1bd2dd9..b4ec7ccd57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### [7.5.0] - 2026-01-29 + +- The DataTable row cursor will extend to the full width if there is excess space https://github.com/Textualize/textual/pull/6345 +- The DataTable will send a selected event on click, only if the cell / row / column is currently highlighted https://github.com/Textualize/textual/pull/6345 + ## [7.4.0] - 2026-01-25 ### Added @@ -3328,6 +3335,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[7.5.0]: https://github.com/Textualize/textual/compare/v7.4.0...v7.5.0 [7.4.0]: https://github.com/Textualize/textual/compare/v7.3.0...v7.4.0 [7.3.0]: https://github.com/Textualize/textual/compare/v7.2.0...v7.3.0 [7.2.0]: https://github.com/Textualize/textual/compare/v7.1.0...v7.2.0 diff --git a/pyproject.toml b/pyproject.toml index 02d2ee9c33..9479fc774d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "7.4.0" +version = "7.5.0" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 04e9b0599e..dd5f9c6d72 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -268,6 +268,8 @@ class RowRenderables(NamedTuple): class DataTable(ScrollView, Generic[CellType], can_focus=True): """A tabular widget that contains data.""" + ALLOW_SELECT = False + BINDINGS: ClassVar[list[BindingType]] = [ Binding("enter", "select_cursor", "Select", show=False), Binding("up", "cursor_up", "Cursor up", show=False), @@ -328,7 +330,6 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): color: $foreground; height: auto; max-height: 100%; - &.datatable--fixed-cursor { background: $block-cursor-blurred-background; } @@ -450,7 +451,7 @@ def __init__( ) -> None: self.data_table = data_table """The data table.""" - self.value: CellType = value + self.value = value """The value in the highlighted cell.""" self.coordinate: Coordinate = coordinate """The coordinate of the highlighted cell.""" @@ -1555,7 +1556,7 @@ def _get_row_region(self, row_index: int) -> Region: y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index]) if self.show_header: y += self.header_height - row_region = Region(0, y, row_width, row.height) + row_region = Region(0, y, max(self.size.width, row_width), row.height) return row_region def _get_column_region(self, column_index: int) -> Region: @@ -2357,17 +2358,36 @@ def _render_line_in_row( ) remaining_space = max(0, widget_width - table_width) background_color = self.background_colors[1] - if row_style.bgcolor is not None: - # TODO: This should really be in a component class - faded_color = Color.from_rich_color(row_style.bgcolor).blend( - background_color, factor=0.25 - ) - faded_style = Style.from_color( - color=row_style.color, bgcolor=faded_color.rich_color + if self.cursor_type == "row": + extend_style, _ = self._get_styles_to_render_cell( + row_index == -1, + False, + False, + should_highlight( + hover_location, Coordinate(row_index or 0, 0), cursor_type + ), + row_index == cursor_location.row, + self.show_cursor, + self._show_hover_cursor, + False, + False, ) + extend_style = row_style + extend_style else: - faded_style = Style.from_color(row_style.color, row_style.bgcolor) - scrollable_row.append([Segment(" " * remaining_space, faded_style)]) + if row_style.bgcolor is not None: + # TODO: This should really be in a component class + faded_color = Color.from_rich_color(row_style.bgcolor).blend( + background_color, factor=0.25 + ) + extend_style = Style.from_color( + color=row_style.color, bgcolor=faded_color.rich_color + ) + else: + extend_style = Style.from_color(row_style.color, row_style.bgcolor) + extend_style += Style.from_meta( + {"row": row_index, "column": 0, "out_of_bounds": True} + ) + scrollable_row.append([Segment(" " * remaining_space, extend_style)]) row_pair = (fixed_row, scrollable_row) self._row_render_cache[cache_key] = row_pair @@ -2389,7 +2409,7 @@ def _get_offsets(self, y: int) -> tuple[RowKey, int]: return self._header_row_key, y y -= header_height if y > len(y_offsets): - raise LookupError("Y coord {y!r} is greater than total height") + raise LookupError(f"Y coord {y!r} is greater than total height") return y_offsets[y] @@ -2541,6 +2561,10 @@ def _on_mouse_move(self, event: events.MouseMove): self._set_hover_cursor(False) return + if self.cursor_type != "row" and meta.get("out_of_bounds", False): + self._set_hover_cursor(False) + return + if self.show_cursor and self.cursor_type != "none": try: self.hover_coordinate = Coordinate(meta["row"], meta["column"]) @@ -2647,6 +2671,8 @@ async def _on_click(self, event: events.Click) -> None: meta = event.style.meta if "row" not in meta or "column" not in meta: return + if self.cursor_type != "row" and meta.get("out_of_bounds", False): + return row_index = meta["row"] column_index = meta["column"] @@ -2667,8 +2693,11 @@ async def _on_click(self, event: events.Click) -> None: self.post_message(message) elif self.show_cursor and self.cursor_type != "none": # Only post selection events if there is a visible row/col/cell cursor. - self.cursor_coordinate = Coordinate(row_index, column_index) - self._post_selected_message() + new_coordinate = Coordinate(row_index, column_index) + highlight_click = new_coordinate == self.cursor_coordinate + self.cursor_coordinate = new_coordinate + if highlight_click: + self._post_selected_message() self._scroll_cursor_into_view(animate=True) event.stop() diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg index 77272ea020..24e5fcd323 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_hot_reloading.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-1293670551-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1293670551-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1293670551-r1 { fill: #e0e0e0;font-weight: bold } -.terminal-1293670551-r2 { fill: #e0e0e0 } -.terminal-1293670551-r3 { fill: #c5c8c6 } -.terminal-1293670551-r4 { fill: #ddedf9;font-weight: bold } + .terminal-r1 { fill: #e0e0e0;font-weight: bold } +.terminal-r2 { fill: #c5c8c6 } +.terminal-r3 { fill: #ddedf9;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DataTableHotReloadingApp + DataTableHotReloadingApp - - - -  A           B     - one         two   - three       four  - five        six   - - - - - - - - - - - - - - - - - - - + + + +  A           B     + one         two   + three       four  + five        six   + + + + + + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg index c207da3cb4..26d705a735 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_datatable_row_cursor_render.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-2406117197-matrix { + .terminal-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2406117197-title { + .terminal-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2406117197-r1 { fill: #e0e0e0;font-weight: bold } -.terminal-2406117197-r2 { fill: #e0e0e0 } -.terminal-2406117197-r3 { fill: #c5c8c6 } -.terminal-2406117197-r4 { fill: #ddedf9;font-weight: bold } + .terminal-r1 { fill: #e0e0e0;font-weight: bold } +.terminal-r2 { fill: #c5c8c6 } +.terminal-r3 { fill: #e0e0e0 } +.terminal-r4 { fill: #ddedf9;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + TableApp - - - -  lane  swimmer               country        time   - 4     Joseph Schooling      Singapore      50.39  - 2     Michael Phelps        United States  51.14  - 5     Chad le Clos          South Africa   51.14  - 6     László Cseh           Hungary        51.14  - 3     Li Zhuhao             China          51.26  - 8     Mehdy Metella         France         51.58  - 7     Tom Shields           United States  51.73  - 1     Aleksandr Sadovnikov  Russia         51.84  - - - - - - - - - - - - - - + + + +  lane  swimmer               country        time   + 4     Joseph Schooling      Singapore      50.39  + 2     Michael Phelps        United States  51.14  + 5     Chad le Clos          South Africa   51.14  + 6     László Cseh           Hungary        51.14  + 3     Li Zhuhao             China          51.26  + 8     Mehdy Metella         France         51.58  + 7     Tom Shields           United States  51.73  + 1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 63032c67b0..6750c5ddbc 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -29,6 +29,7 @@ class DataTableApp(App): + AUTO_FOCUS = None messages_to_record = { "CellHighlighted", "CellSelected", @@ -787,8 +788,8 @@ async def test_coordinate_to_cell_key_invalid_coordinate(): async def test_datatable_click_cell_cursor(): - """When the cell cursor is used, and we click, we emit a CellHighlighted - *and* a CellSelected message for the cell that was clicked. + """When the cell cursor is used, and we click, we emit a CellHighlighted message, + a second click emits a CellSelected message. Regression test for https://github.com/Textualize/textual/issues/1723""" app = DataTableApp() async with app.run_test() as pilot: @@ -797,6 +798,7 @@ async def test_datatable_click_cell_cursor(): table.add_row("123") row_key = table.add_row("456") await pilot.click(offset=Offset(1, 2)) + await pilot.click(offset=Offset(1, 2)) # There's two CellHighlighted events since a cell is highlighted on initial load, # then when we click, another cell is highlighted (and selected). assert app.message_names == [ @@ -825,15 +827,22 @@ async def test_click_row_cursor(): table.add_column("ABC") table.add_row("123") row_key = table.add_row("456") + await pilot.click(offset=Offset(1, 2)) - assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"] + print(repr(app.message_names)) + assert app.message_names == ["RowHighlighted", "RowHighlighted"] row_highlighted: DataTable.RowHighlighted = app.messages[1] assert row_highlighted.row_key == row_key + assert row_highlighted.cursor_row == 1 - row_selected: DataTable.RowSelected = app.messages[2] + app.messages.clear() + await pilot.click(offset=Offset(1, 2)) + assert app.message_names == ["RowSelected"] + + row_selected: DataTable.RowSelected = app.messages[0] assert row_selected.row_key == row_key assert row_highlighted.cursor_row == 1 @@ -849,6 +858,7 @@ async def test_click_column_cursor(): table.add_row("123") table.add_row("456") await pilot.click(offset=Offset(1, 2)) + await pilot.click(offset=Offset(1, 2)) assert app.message_names == [ "ColumnHighlighted", "ColumnHighlighted",