mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #1788 from Textualize/datatable-select-headers
DataTable - Message emitted when header selected
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user