From 1445275220aa9f149e18cdd7de73305c204a1c45 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 14:05:49 +0000 Subject: [PATCH 1/8] Add HeaderSelected message to DataTable (but don't emit yet). --- src/textual/message.py | 1 - src/textual/widgets/_data_table.py | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/textual/message.py b/src/textual/message.py index 5f46cc97a5..7ed72789b5 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -10,7 +10,6 @@ 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 69288744d3..4530eba632 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -445,6 +445,32 @@ def __rich_repr__(self) -> rich.repr.Result: 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. + + Attributes: + column_key: The key for the column. + column_index: The index for the column. + label: The text of the label. + """ + + def __init__( + self, + sender: DataTable, + column_key: ColumnKey, + column_index: int, + label: str, + ): + self.column_key = column_key + self.column_index = column_index + self.label = 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 + def __init__( self, *, From df1116d7760ebc41a85f78e0e914abc574d4be89 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 15:01:02 +0000 Subject: [PATCH 2/8] Post an event when DataTable column header clicked --- src/textual/widgets/_data_table.py | 33 +- .../__snapshots__/test_snapshots.ambr | 458 +++++++++--------- 2 files changed, 251 insertions(+), 240 deletions(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 4530eba632..bbe7659de5 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -459,7 +459,7 @@ def __init__( sender: DataTable, column_key: ColumnKey, column_index: int, - label: str, + label: Text, ): self.column_key = column_key self.column_index = column_index @@ -469,7 +469,7 @@ def __init__( def __rich_repr__(self) -> rich.repr.Result: yield "sender", self.sender yield "column_key", self.column_key - yield "label", self.label + yield "label", self.label.plain def __init__( self, @@ -1360,8 +1360,7 @@ def _render_cell( 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( @@ -1676,14 +1675,26 @@ def _set_hover_cursor(self, active: bool) -> None: 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 don'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 075a214a71..8fc2f8904d 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  + + + + + + + + + + + + + From 5cf1be1cbcb9c620154534a6a3643fe056041854 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 15:16:22 +0000 Subject: [PATCH 3/8] Adding test for HeaderSelected event in DataTable --- tests/test_data_table.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 5dc6abaf23..44b7e5d555 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -2,6 +2,7 @@ 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): @@ -673,6 +675,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.""" From 50898c2d07311cabe5f1be219d4cf06b20c63018 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 15:17:49 +0000 Subject: [PATCH 4/8] Added note to changelog about HeaderSelected event in DataTable --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 650cc03711..e083837761 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 From 49b78daa04a160449d32b6782747359f3d57e4eb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 15:18:32 +0000 Subject: [PATCH 5/8] Update docs to include `DataTable.HeaderSelected` --- docs/widgets/data_table.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/widgets/data_table.md b/docs/widgets/data_table.md index 9dd918d9d5..c4d94fab0f 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: From 7824942f820698c049da0515b699de1a5c10da4e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 15:19:30 +0000 Subject: [PATCH 6/8] Typo fix in comment --- src/textual/widgets/_data_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index bbe7659de5..366c188167 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -1683,7 +1683,7 @@ def on_click(self, event: events.Click) -> None: 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 don't move the cursor. + # 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 From 4ec5d3f9db31bf38c972e3f0790f3ffe0cbbb9a1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 15:25:33 +0000 Subject: [PATCH 7/8] Migrate some DataTable tests from pilot.pause to wait_for_idle --- tests/test_data_table.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 44b7e5d555..2a000332d2 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -75,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 @@ -92,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 @@ -104,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 @@ -125,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 @@ -132,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 @@ -141,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 @@ -156,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 From c80299ea0908112a9e80fbf2a22345f78686e5c4 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 14 Feb 2023 16:35:35 +0000 Subject: [PATCH 8/8] Use new docstring format --- src/textual/widgets/_data_table.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 366c188167..0c8839c6b6 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -446,13 +446,7 @@ def __rich_repr__(self) -> rich.repr.Result: yield "column_key", self.column_key class HeaderSelected(Message, bubble=True): - """Posted when a column header/label is clicked. - - Attributes: - column_key: The key for the column. - column_index: The index for the column. - label: The text of the label. - """ + """Posted when a column header/label is clicked.""" def __init__( self, @@ -462,8 +456,11 @@ def __init__( 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: