Datatable labelling rows (#1868)

* Renaming some component classes in DataTable

* Some more renaming of DataTable component classes

* Separate styling for fixed rows/columns from labels

* Highlight fixed rows/cols affected by colour differently

* Tweaking styles for fixed data in DataTable

* Update DataTable snapshots

* Add row label to Row metadata

* Wiring up some labelled row logic behind flags

* Renaming variable in DataTable

* Variable renaming in DataTable

* [no ci] Labelling rows progress

* Add RenderedRow abstraction to DataTable

* Computing label widths

* Use the Column object to represent row label column

* Ability to toggle row labels reactively

* Adjust width calculation for label widths

* Add DataTable.RowLabelSelected

* Posting the RowLabelClick message

* Hovering of row labels applies new style

* Remove a print

* Ensure horizontal scrolling with column cursor accounts for row label column

* Account for possible row labels in cell cursor horizontal scrolling

* Ensure cursor highlighting is correct on row label cells

* Document component class for DataTable label hover

* Test to ensure clicking row label emits correct event

* Add snapshot test for DataTable with fixed rows/cols and row labels

* Using pilot pause instead of wait_for_idle directly

* Update CHANGELOG

* Add a docstring

* Add a note to CHANGELOG.md about RowLabelSelected
This commit is contained in:
darrenburns
2023-02-27 10:29:17 +00:00
committed by GitHub
parent f16f9c2a5c
commit cbe2ab87c6
8 changed files with 615 additions and 208 deletions

File diff suppressed because one or more lines are too long

View File

@@ -21,6 +21,7 @@ class TableApp(App):
table.focus()
table.cursor_type = "column"
table.fixed_columns = 1
table.fixed_rows = 1
yield table
def on_mount(self) -> None:

View File

@@ -20,6 +20,8 @@ class TableApp(App):
table = DataTable()
table.focus()
table.cursor_type = "row"
table.fixed_columns = 1
table.fixed_rows = 1
yield table
def on_mount(self) -> None:

View File

@@ -0,0 +1,37 @@
from textual.app import App, ComposeResult
from textual.widgets import DataTable
ROWS = [
("lane", "swimmer", "country", "time"),
(5, "Chad le Clos", "South Africa", 51.14),
(4, "Joseph Schooling", "Singapore", 50.39),
(2, "Michael Phelps", "United States", 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),
(10, "Darren Burns", "Scotland", 51.84),
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
]
class TableApp(App):
def compose(self) -> ComposeResult:
yield DataTable()
def on_mount(self) -> None:
table = self.query_one(DataTable)
table.fixed_rows = 1
table.fixed_columns = 1
table.focus()
rows = iter(ROWS)
column_labels = next(rows)
for column in column_labels:
table.add_column(column, key=column)
for index, row in enumerate(rows):
table.add_row(*row, label=str(index))
app = TableApp()
if __name__ == "__main__":
app.run()

View File

@@ -108,6 +108,11 @@ def test_datatable_sort_multikey(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_sort.py", press=press)
def test_datatable_labels_and_fixed_data(snap_compare):
# Ensure that we render correctly when there are fixed rows/cols and labels.
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_row_labels.py")
def test_footer_render(snap_compare):
assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py")

View File

@@ -35,6 +35,7 @@ class DataTableApp(App):
"ColumnHighlighted",
"ColumnSelected",
"HeaderSelected",
"RowLabelSelected",
}
def __init__(self):
@@ -75,12 +76,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 wait_for_idle(0)
await pilot.pause()
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)
await pilot.pause()
expected_messages.append("CellSelected")
assert app.message_names == expected_messages
@@ -93,12 +94,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 wait_for_idle(0)
await pilot.pause()
assert app.message_names == expected_messages
# Select the row...
await pilot.press("enter")
await wait_for_idle(0)
await pilot.pause()
expected_messages.append("RowSelected")
assert app.message_names == expected_messages
@@ -106,20 +107,20 @@ async def test_datatable_message_emission():
# Switching to the column cursor emits a `ColumnHighlighted`
table.cursor_type = "column"
expected_messages.append("ColumnHighlighted")
await wait_for_idle(0)
await pilot.pause()
assert app.message_names == expected_messages
# Select the column...
await pilot.press("enter")
expected_messages.append("ColumnSelected")
await wait_for_idle(0)
await pilot.pause()
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)
await pilot.pause()
# No new messages since cursor not visible
assert app.message_names == expected_messages
@@ -129,7 +130,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)
await pilot.pause()
# No new messages since show_cursor = False
assert app.message_names == expected_messages
@@ -137,7 +138,7 @@ async def test_datatable_message_emission():
# message should be emitted for highlighting the cell.
table.show_cursor = True
expected_messages.append("CellHighlighted")
await wait_for_idle(0)
await pilot.pause()
assert app.message_names == expected_messages
# Similarly for showing the cursor again when row or column
@@ -146,14 +147,14 @@ async def test_datatable_message_emission():
table.cursor_type = "row"
table.show_cursor = True
expected_messages.append("RowHighlighted")
await wait_for_idle(0)
await pilot.pause()
assert app.message_names == expected_messages
table.show_cursor = False
table.cursor_type = "column"
table.show_cursor = True
expected_messages.append("ColumnHighlighted")
await wait_for_idle(0)
await pilot.pause()
assert app.message_names == expected_messages
# Likewise, if the cursor_type is "none", and we change the
@@ -161,7 +162,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)
await pilot.pause()
assert app.message_names == expected_messages
@@ -574,14 +575,14 @@ async def test_datatable_on_click_cell_cursor():
*and* a CellSelected message for the cell that was clicked.
Regression test for https://github.com/Textualize/textual/issues/1723"""
app = DataTableApp()
async with app.run_test():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
click = make_click_event(app)
column_key = table.add_column("ABC")
table.add_row("123")
row_key = table.add_row("456")
table.on_click(event=click)
await wait_for_idle(0)
await pilot.pause()
# 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 == [
@@ -688,9 +689,9 @@ 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():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
column = table.add_column("number")
column_key = table.add_column("number")
table.add_row(3)
click_event = Click(
sender=table,
@@ -698,26 +699,62 @@ async def test_header_selected():
y=0,
delta_x=0,
delta_y=0,
button=0,
button=1,
shift=False,
meta=False,
ctrl=False,
)
await pilot.pause()
table.on_click(click_event)
await wait_for_idle(0)
await pilot.pause()
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
assert message.column_key == column_key
# 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)
await pilot.pause()
assert app.message_names.count("HeaderSelected") == 1
async def test_row_label_selected():
"""Ensure that the DataTable sends a RowLabelSelected event when
the user clicks on a row label."""
app = DataTableApp()
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.add_column("number")
row_key = table.add_row(3, label="A")
click_event = Click(
sender=table,
x=1,
y=1,
delta_x=0,
delta_y=0,
button=1,
shift=False,
meta=False,
ctrl=False,
)
await pilot.pause()
table.on_click(click_event)
await pilot.pause()
message: DataTable.RowLabelSelected = app.messages[-1]
assert message.sender is table
assert message.label == Text("A")
assert message.row_index == 0
assert message.row_key == row_key
# Now hide the row label and click in the same place - no additional message emitted.
table.show_row_labels = False
table.on_click(click_event)
await pilot.pause()
assert app.message_names.count("RowLabelSelected") == 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."""
@@ -786,7 +823,7 @@ async def test_sort_reverse_coordinate_and_key_access():
async def test_cell_cursor_highlight_events():
app = DataTableApp()
async with app.run_test():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
column_one_key, column_two_key = table.add_columns("A", "B")
_ = table.add_row(0, 1)
@@ -796,14 +833,14 @@ async def test_cell_cursor_highlight_events():
table.action_cursor_up()
table.action_cursor_left()
await wait_for_idle(0)
await pilot.pause()
assert table.app.message_names == [
"CellHighlighted"
] # Initial highlight on load
# Move the cursor one cell down, and check the highlighted event posted
table.action_cursor_down()
await wait_for_idle(0)
await pilot.pause()
assert len(table.app.messages) == 2
latest_message: DataTable.CellHighlighted = table.app.messages[-1]
assert isinstance(latest_message, DataTable.CellHighlighted)
@@ -813,7 +850,7 @@ async def test_cell_cursor_highlight_events():
# Now move the cursor to the right, and check highlighted event posted
table.action_cursor_right()
await wait_for_idle(0)
await pilot.pause()
assert len(table.app.messages) == 3
latest_message = table.app.messages[-1]
assert latest_message.coordinate == Coordinate(1, 1)
@@ -822,7 +859,7 @@ async def test_cell_cursor_highlight_events():
async def test_row_cursor_highlight_events():
app = DataTableApp()
async with app.run_test():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.cursor_type = "row"
table.add_columns("A", "B")
@@ -835,12 +872,12 @@ async def test_row_cursor_highlight_events():
table.action_cursor_left()
table.action_cursor_right()
await wait_for_idle(0)
await pilot.pause()
assert table.app.message_names == ["RowHighlighted"] # Initial highlight
# Move the row cursor from row 0 to row 1, check the highlighted event posted
table.action_cursor_down()
await wait_for_idle(0)
await pilot.pause()
assert len(table.app.messages) == 2
latest_message: DataTable.RowHighlighted = table.app.messages[-1]
assert isinstance(latest_message, DataTable.RowHighlighted)
@@ -849,7 +886,7 @@ async def test_row_cursor_highlight_events():
# Move the row cursor back up to row 0, check the highlighted event posted
table.action_cursor_up()
await wait_for_idle(0)
await pilot.pause()
assert len(table.app.messages) == 3
latest_message = table.app.messages[-1]
assert latest_message.row_key == row_one_key
@@ -858,7 +895,7 @@ async def test_row_cursor_highlight_events():
async def test_column_cursor_highlight_events():
app = DataTableApp()
async with app.run_test():
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.cursor_type = "column"
column_one_key, column_two_key = table.add_columns("A", "B")
@@ -871,13 +908,13 @@ async def test_column_cursor_highlight_events():
table.action_cursor_up()
table.action_cursor_down()
await wait_for_idle(0)
await pilot.pause()
assert table.app.message_names == ["ColumnHighlighted"] # Initial highlight
# Move the column cursor from column 0 to column 1,
# check the highlighted event posted
table.action_cursor_right()
await wait_for_idle(0)
await pilot.pause()
assert len(table.app.messages) == 2
latest_message: DataTable.ColumnHighlighted = table.app.messages[-1]
assert isinstance(latest_message, DataTable.ColumnHighlighted)
@@ -887,7 +924,7 @@ async def test_column_cursor_highlight_events():
# Move the column cursor left, back to column 0,
# check the highlighted event posted again.
table.action_cursor_left()
await wait_for_idle(0)
await pilot.pause()
assert len(table.app.messages) == 3
latest_message = table.app.messages[-1]
assert latest_message.column_key == column_one_key