diff --git a/CHANGELOG.md b/CHANGELOG.md index 650cc0371..e08383776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `DataTable.get_row_at` to retrieve the values from a row by index https://github.com/Textualize/textual/pull/1786 - Added `DataTable.get_column` to retrieve the values from a column by key https://github.com/Textualize/textual/pull/1786 - Added `DataTable.get_column_at` to retrieve the values from a column by index https://github.com/Textualize/textual/pull/1786 +- Added `DataTable.HeaderSelected` which is posted when header label clicked https://github.com/Textualize/textual/pull/1788 - Added `DOMNode.watch` and `DOMNode.is_attached` methods https://github.com/Textualize/textual/pull/1750 - Added `DOMNode.css_tree` which is a renderable that shows the DOM and CSS https://github.com/Textualize/textual/pull/1778 - Added `DOMNode.children_view` which is a view on to a nodes children list, use for querying https://github.com/Textualize/textual/pull/1778 diff --git a/docs/widgets/data_table.md b/docs/widgets/data_table.md index 289636afc..7cc0a00d6 100644 --- a/docs/widgets/data_table.md +++ b/docs/widgets/data_table.md @@ -48,6 +48,8 @@ The example below populates a table with CSV data. ### ::: textual.widgets.DataTable.ColumnSelected +### ::: textual.widgets.DataTable.HeaderSelected + ## Bindings The data table widget defines directly the following bindings: diff --git a/src/textual/message.py b/src/textual/message.py index 5f46cc97a..7ed72789b 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -10,7 +10,6 @@ from .case import camel_to_snake if TYPE_CHECKING: from .message_pump import MessagePump - from .widget import Widget @rich.repr.auto diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index fe80b409d..b9e48d326 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -438,6 +438,29 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): yield "cursor_column", self.cursor_column yield "column_key", self.column_key + class HeaderSelected(Message, bubble=True): + """Posted when a column header/label is clicked.""" + + def __init__( + self, + sender: DataTable, + column_key: ColumnKey, + column_index: int, + label: Text, + ): + self.column_key = column_key + """The key for the column.""" + self.column_index = column_index + """The index for the column.""" + self.label = label + """The text of the label.""" + super().__init__(sender) + + def __rich_repr__(self) -> rich.repr.Result: + yield "sender", self.sender + yield "column_key", self.column_key + yield "label", self.label.plain + def __init__( self, *, @@ -1327,8 +1350,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): column_key = self._column_locations.get_key(column_index) cell_cache_key = (row_key, column_key, style, cursor, hover, self._update_count) if cell_cache_key not in self._cell_render_cache: - if not is_header_row: - style += Style.from_meta({"row": row_index, "column": column_index}) + style += Style.from_meta({"row": row_index, "column": column_index}) height = self.header_height if is_header_row else self.rows[row_key].height cell = self._get_row_renderables(row_index)[column_index] lines = self.app.console.render_lines( @@ -1643,14 +1665,26 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): def on_click(self, event: events.Click) -> None: self._set_hover_cursor(True) - if self.show_cursor and self.cursor_type != "none": + meta = self.get_style_at(event.x, event.y).meta + if not meta: + return + + row_index = meta["row"] + column_index = meta["column"] + is_header_click = self.show_header and row_index == -1 + if is_header_click: + # Header clicks work even if cursor is off, and doesn't move the cursor. + column = self.ordered_columns[column_index] + message = DataTable.HeaderSelected( + self, column.key, column_index, label=column.label + ) + self.post_message_no_wait(message) + elif self.show_cursor and self.cursor_type != "none": # Only post selection events if there is a visible row/col/cell cursor. - meta = self.get_style_at(event.x, event.y).meta - if meta: - self.cursor_coordinate = Coordinate(meta["row"], meta["column"]) - self._post_selected_message() - self._scroll_cursor_into_view(animate=True) - event.stop() + self.cursor_coordinate = Coordinate(row_index, column_index) + self._post_selected_message() + self._scroll_cursor_into_view(animate=True) + event.stop() def action_cursor_up(self) -> None: self._set_hover_cursor(False) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 075a214a7..8fc2f8904 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -10015,134 +10015,134 @@ font-weight: 700; } - .terminal-3633944034-matrix { + .terminal-3966238525-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3633944034-title { + .terminal-3966238525-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3633944034-r1 { fill: #dde6ed;font-weight: bold } - .terminal-3633944034-r2 { fill: #1e1201;font-weight: bold } - .terminal-3633944034-r3 { fill: #e1e1e1 } - .terminal-3633944034-r4 { fill: #c5c8c6 } - .terminal-3633944034-r5 { fill: #211505 } + .terminal-3966238525-r1 { fill: #dde6ed;font-weight: bold } + .terminal-3966238525-r2 { fill: #1e1201;font-weight: bold } + .terminal-3966238525-r3 { fill: #e1e1e1 } + .terminal-3966238525-r4 { fill: #c5c8c6 } + .terminal-3966238525-r5 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - 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  + + + + + + + + + + + + + + @@ -10173,133 +10173,133 @@ font-weight: 700; } - .terminal-108526495-matrix { + .terminal-1288566407-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-108526495-title { + .terminal-1288566407-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-108526495-r1 { fill: #dde6ed;font-weight: bold } - .terminal-108526495-r2 { fill: #e1e1e1 } - .terminal-108526495-r3 { fill: #c5c8c6 } - .terminal-108526495-r4 { fill: #211505 } + .terminal-1288566407-r1 { fill: #dde6ed;font-weight: bold } + .terminal-1288566407-r2 { fill: #e1e1e1 } + .terminal-1288566407-r3 { fill: #c5c8c6 } + .terminal-1288566407-r4 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - 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  -  10    Darren Burns          Scotland       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  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + @@ -10330,133 +10330,133 @@ font-weight: 700; } - .terminal-512278738-matrix { + .terminal-3001793466-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-512278738-title { + .terminal-3001793466-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-512278738-r1 { fill: #dde6ed;font-weight: bold } - .terminal-512278738-r2 { fill: #e1e1e1 } - .terminal-512278738-r3 { fill: #c5c8c6 } - .terminal-512278738-r4 { fill: #211505 } + .terminal-3001793466-r1 { fill: #dde6ed;font-weight: bold } + .terminal-3001793466-r2 { fill: #e1e1e1 } + .terminal-3001793466-r3 { fill: #c5c8c6 } + .terminal-3001793466-r4 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - 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  + + + + + + + + + + + + + + @@ -10487,133 +10487,133 @@ font-weight: 700; } - .terminal-480181151-matrix { + .terminal-1660221063-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-480181151-title { + .terminal-1660221063-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-480181151-r1 { fill: #dde6ed;font-weight: bold } - .terminal-480181151-r2 { fill: #e1e1e1 } - .terminal-480181151-r3 { fill: #c5c8c6 } - .terminal-480181151-r4 { fill: #211505 } + .terminal-1660221063-r1 { fill: #dde6ed;font-weight: bold } + .terminal-1660221063-r2 { fill: #e1e1e1 } + .terminal-1660221063-r3 { fill: #c5c8c6 } + .terminal-1660221063-r4 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - 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  -  10    Darren Burns          Scotland       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  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 5dc6abaf2..2a000332d 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -2,6 +2,7 @@ from __future__ import annotations import pytest from rich.style import Style +from rich.text import Text from textual._wait import wait_for_idle from textual.actions import SkipAction @@ -33,6 +34,7 @@ class DataTableApp(App): "RowSelected", "ColumnHighlighted", "ColumnSelected", + "HeaderSelected", } def __init__(self): @@ -73,11 +75,12 @@ async def test_datatable_message_emission(): # 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(2 / 100) + await wait_for_idle(0) assert app.message_names == expected_messages # Pressing Enter when the cursor is on a cell emits a CellSelected await pilot.press("enter") + await wait_for_idle(0) expected_messages.append("CellSelected") assert app.message_names == expected_messages @@ -90,11 +93,12 @@ async def test_datatable_message_emission(): # Switch over to the row cursor... should emit a `RowHighlighted` table.cursor_type = "row" expected_messages.append("RowHighlighted") - await pilot.pause(2 / 100) + await wait_for_idle(0) assert app.message_names == expected_messages # Select the row... await pilot.press("enter") + await wait_for_idle(0) expected_messages.append("RowSelected") assert app.message_names == expected_messages @@ -102,18 +106,20 @@ async def test_datatable_message_emission(): # Switching to the column cursor emits a `ColumnHighlighted` table.cursor_type = "column" expected_messages.append("ColumnHighlighted") - await pilot.pause(2 / 100) + await wait_for_idle(0) assert app.message_names == expected_messages # Select the column... await pilot.press("enter") expected_messages.append("ColumnSelected") + await wait_for_idle(0) assert app.message_names == expected_messages # NONE CURSOR # No messages get emitted at all... table.cursor_type = "none" await pilot.press("up", "down", "left", "right", "enter") + await wait_for_idle(0) # No new messages since cursor not visible assert app.message_names == expected_messages @@ -123,6 +129,7 @@ async def test_datatable_message_emission(): table.show_cursor = False table.cursor_type = "cell" await pilot.press("up", "down", "left", "right", "enter") + await wait_for_idle(0) # No new messages since show_cursor = False assert app.message_names == expected_messages @@ -130,7 +137,7 @@ async def test_datatable_message_emission(): # message should be emitted for highlighting the cell. table.show_cursor = True expected_messages.append("CellHighlighted") - await pilot.pause(2 / 100) + await wait_for_idle(0) assert app.message_names == expected_messages # Similarly for showing the cursor again when row or column @@ -139,14 +146,14 @@ async def test_datatable_message_emission(): table.cursor_type = "row" table.show_cursor = True expected_messages.append("RowHighlighted") - await pilot.pause(2 / 100) + await wait_for_idle(0) assert app.message_names == expected_messages table.show_cursor = False table.cursor_type = "column" table.show_cursor = True expected_messages.append("ColumnHighlighted") - await pilot.pause(2 / 100) + await wait_for_idle(0) assert app.message_names == expected_messages # Likewise, if the cursor_type is "none", and we change the @@ -154,6 +161,7 @@ async def test_datatable_message_emission(): # the cursor is still not visible to the user. table.cursor_type = "none" await pilot.press("up", "down", "left", "right", "enter") + await wait_for_idle(0) assert app.message_names == expected_messages @@ -673,6 +681,40 @@ async def test_hover_coordinate(): assert table.hover_coordinate == Coordinate(1, 2) +async def test_header_selected(): + """Ensure that a HeaderSelected event gets posted when we click + on the header in the DataTable.""" + app = DataTableApp() + async with app.run_test(): + table = app.query_one(DataTable) + column = table.add_column("number") + table.add_row(3) + click_event = Click( + sender=table, + x=3, + y=0, + delta_x=0, + delta_y=0, + button=0, + shift=False, + meta=False, + ctrl=False, + ) + table.on_click(click_event) + await wait_for_idle(0) + message: DataTable.HeaderSelected = app.messages[-1] + assert message.sender is table + assert message.label == Text("number") + assert message.column_index == 0 + assert message.column_key == column + + # Now hide the header and click in the exact same place - no additional message emitted. + table.show_header = False + table.on_click(click_event) + await wait_for_idle(0) + assert app.message_names.count("HeaderSelected") == 1 + + async def test_sort_coordinate_and_key_access(): """Ensure that, after sorting, that coordinates and cell keys can still be used to retrieve the correct cell."""