Merge pull request #1788 from Textualize/datatable-select-headers

DataTable - Message emitted when header selected
This commit is contained in:
Will McGugan
2023-02-14 16:42:01 +00:00
committed by GitHub
6 changed files with 323 additions and 245 deletions

View File

@@ -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_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` 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.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.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.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 - Added `DOMNode.children_view` which is a view on to a nodes children list, use for querying https://github.com/Textualize/textual/pull/1778

View File

@@ -48,6 +48,8 @@ The example below populates a table with CSV data.
### ::: textual.widgets.DataTable.ColumnSelected ### ::: textual.widgets.DataTable.ColumnSelected
### ::: textual.widgets.DataTable.HeaderSelected
## Bindings ## Bindings
The data table widget defines directly the following bindings: The data table widget defines directly the following bindings:

View File

@@ -10,7 +10,6 @@ from .case import camel_to_snake
if TYPE_CHECKING: if TYPE_CHECKING:
from .message_pump import MessagePump from .message_pump import MessagePump
from .widget import Widget
@rich.repr.auto @rich.repr.auto

View File

@@ -438,6 +438,29 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
yield "cursor_column", self.cursor_column yield "cursor_column", self.cursor_column
yield "column_key", self.column_key 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__( def __init__(
self, self,
*, *,
@@ -1327,8 +1350,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
column_key = self._column_locations.get_key(column_index) column_key = self._column_locations.get_key(column_index)
cell_cache_key = (row_key, column_key, style, cursor, hover, self._update_count) cell_cache_key = (row_key, column_key, style, cursor, hover, self._update_count)
if cell_cache_key not in self._cell_render_cache: 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 height = self.header_height if is_header_row else self.rows[row_key].height
cell = self._get_row_renderables(row_index)[column_index] cell = self._get_row_renderables(row_index)[column_index]
lines = self.app.console.render_lines( 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: def on_click(self, event: events.Click) -> None:
self._set_hover_cursor(True) 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. # Only post selection events if there is a visible row/col/cell cursor.
meta = self.get_style_at(event.x, event.y).meta self.cursor_coordinate = Coordinate(row_index, column_index)
if meta: self._post_selected_message()
self.cursor_coordinate = Coordinate(meta["row"], meta["column"]) self._scroll_cursor_into_view(animate=True)
self._post_selected_message() event.stop()
self._scroll_cursor_into_view(animate=True)
event.stop()
def action_cursor_up(self) -> None: def action_cursor_up(self) -> None:
self._set_hover_cursor(False) self._set_hover_cursor(False)

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import pytest import pytest
from rich.style import Style from rich.style import Style
from rich.text import Text
from textual._wait import wait_for_idle from textual._wait import wait_for_idle
from textual.actions import SkipAction from textual.actions import SkipAction
@@ -33,6 +34,7 @@ class DataTableApp(App):
"RowSelected", "RowSelected",
"ColumnHighlighted", "ColumnHighlighted",
"ColumnSelected", "ColumnSelected",
"HeaderSelected",
} }
def __init__(self): def __init__(self):
@@ -73,11 +75,12 @@ async def test_datatable_message_emission():
# therefore no highlighted cells), but then a row was added, and # therefore no highlighted cells), but then a row was added, and
# so the cell at (0, 0) became highlighted. # so the cell at (0, 0) became highlighted.
expected_messages.append("CellHighlighted") expected_messages.append("CellHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
# Pressing Enter when the cursor is on a cell emits a CellSelected # Pressing Enter when the cursor is on a cell emits a CellSelected
await pilot.press("enter") await pilot.press("enter")
await wait_for_idle(0)
expected_messages.append("CellSelected") expected_messages.append("CellSelected")
assert app.message_names == expected_messages 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` # Switch over to the row cursor... should emit a `RowHighlighted`
table.cursor_type = "row" table.cursor_type = "row"
expected_messages.append("RowHighlighted") expected_messages.append("RowHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
# Select the row... # Select the row...
await pilot.press("enter") await pilot.press("enter")
await wait_for_idle(0)
expected_messages.append("RowSelected") expected_messages.append("RowSelected")
assert app.message_names == expected_messages assert app.message_names == expected_messages
@@ -102,18 +106,20 @@ async def test_datatable_message_emission():
# Switching to the column cursor emits a `ColumnHighlighted` # Switching to the column cursor emits a `ColumnHighlighted`
table.cursor_type = "column" table.cursor_type = "column"
expected_messages.append("ColumnHighlighted") expected_messages.append("ColumnHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
# Select the column... # Select the column...
await pilot.press("enter") await pilot.press("enter")
expected_messages.append("ColumnSelected") expected_messages.append("ColumnSelected")
await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
# NONE CURSOR # NONE CURSOR
# No messages get emitted at all... # No messages get emitted at all...
table.cursor_type = "none" table.cursor_type = "none"
await pilot.press("up", "down", "left", "right", "enter") await pilot.press("up", "down", "left", "right", "enter")
await wait_for_idle(0)
# No new messages since cursor not visible # No new messages since cursor not visible
assert app.message_names == expected_messages assert app.message_names == expected_messages
@@ -123,6 +129,7 @@ async def test_datatable_message_emission():
table.show_cursor = False table.show_cursor = False
table.cursor_type = "cell" table.cursor_type = "cell"
await pilot.press("up", "down", "left", "right", "enter") await pilot.press("up", "down", "left", "right", "enter")
await wait_for_idle(0)
# No new messages since show_cursor = False # No new messages since show_cursor = False
assert app.message_names == expected_messages assert app.message_names == expected_messages
@@ -130,7 +137,7 @@ async def test_datatable_message_emission():
# message should be emitted for highlighting the cell. # message should be emitted for highlighting the cell.
table.show_cursor = True table.show_cursor = True
expected_messages.append("CellHighlighted") expected_messages.append("CellHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
# Similarly for showing the cursor again when row or column # 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.cursor_type = "row"
table.show_cursor = True table.show_cursor = True
expected_messages.append("RowHighlighted") expected_messages.append("RowHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
table.show_cursor = False table.show_cursor = False
table.cursor_type = "column" table.cursor_type = "column"
table.show_cursor = True table.show_cursor = True
expected_messages.append("ColumnHighlighted") expected_messages.append("ColumnHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
# Likewise, if the cursor_type is "none", and we change the # 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. # the cursor is still not visible to the user.
table.cursor_type = "none" table.cursor_type = "none"
await pilot.press("up", "down", "left", "right", "enter") await pilot.press("up", "down", "left", "right", "enter")
await wait_for_idle(0)
assert app.message_names == expected_messages assert app.message_names == expected_messages
@@ -673,6 +681,40 @@ async def test_hover_coordinate():
assert table.hover_coordinate == Coordinate(1, 2) 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(): async def test_sort_coordinate_and_key_access():
"""Ensure that, after sorting, that coordinates and cell keys """Ensure that, after sorting, that coordinates and cell keys
can still be used to retrieve the correct cell.""" can still be used to retrieve the correct cell."""