Merge branch 'main' into toggle-boxen

This commit is contained in:
Dave Pearson
2023-02-27 10:31:06 +00:00
8 changed files with 615 additions and 208 deletions

View File

@@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added horizontal rule to Markdown https://github.com/Textualize/textual/pull/1832
- Added `Widget.disabled` https://github.com/Textualize/textual/pull/1785
- Added `DOMNode.notify_style_update` to replace `messages.StylesUpdated` message https://github.com/Textualize/textual/pull/1861
- Added `DataTable.show_row_labels` reactive to show and hide row labels https://github.com/Textualize/textual/pull/1868
- Added `DataTable.RowLabelSelected` event, which is emitted when a row label is clicked https://github.com/Textualize/textual/pull/1868
- Added `MessagePump.prevent` context manager to temporarily suppress a given message type https://github.com/Textualize/textual/pull/1866
### Changed
@@ -38,6 +40,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Scrolling by page now adds to current position.
- Markdown lists have been polished: a selection of bullets, better alignment of numbers, style tweaks https://github.com/Textualize/textual/pull/1832
- Added alternative method of composing Widgets https://github.com/Textualize/textual/pull/1847
- Added `label` parameter to `DataTable.add_row` https://github.com/Textualize/textual/pull/1868
- Breaking change: Some `DataTable` component classes were renamed - see PR for details https://github.com/Textualize/textual/pull/1868
### Removed

View File

@@ -39,6 +39,8 @@ RowCacheKey: TypeAlias = (
CursorType = Literal["cell", "row", "column", "none"]
CellType = TypeVar("CellType")
CELL_X_PADDING = 2
class CellDoesNotExist(Exception):
"""The cell key/index was invalid.
@@ -171,9 +173,9 @@ class Column:
"""Width in cells, required to render a column."""
# +2 is to account for space padding either side of the cell
if self.auto_width:
return self.content_width + 2
return self.content_width + CELL_X_PADDING
else:
return self.width + 2
return self.width + CELL_X_PADDING
@dataclass
@@ -182,6 +184,14 @@ class Row:
key: RowKey
height: int
label: Text | None = None
class RowRenderables(NamedTuple):
"""Container for a row, which contains an optional label and some data cells."""
label: RenderableType | None
cells: list[RenderableType]
class DataTable(ScrollView, Generic[CellType], can_focus=True):
@@ -205,25 +215,27 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
"""
COMPONENT_CLASSES: ClassVar[set[str]] = {
"datatable--header",
"datatable--cursor-fixed",
"datatable--highlight-fixed",
"datatable--cursor",
"datatable--hover",
"datatable--fixed",
"datatable--fixed-cursor",
"datatable--header",
"datatable--header-cursor",
"datatable--header-hover",
"datatable--odd-row",
"datatable--even-row",
"datatable--highlight",
"datatable--cursor",
}
"""
| Class | Description |
| :- | :- |
| `datatable--cursor` | Target the cursor. |
| `datatable--cursor-fixed` | Target fixed columns or header under the cursor. |
| `datatable--even-row` | Target even rows (row indices start at 0). |
| `datatable--fixed` | Target fixed columns or header. |
| `datatable--hover` | Target the cells under the hover cursor. |
| `datatable--fixed` | Target fixed columns and fixed rows. |
| `datatable--fixed-cursor` | Target highlighted and fixed columns or header. |
| `datatable--header` | Target the header of the data table. |
| `datatable--highlight` | Target the highlighted cell(s). |
| `datatable--highlight-fixed` | Target highlighted and fixed columns or header. |
| `datatable--header-cursor` | Target cells highlighted by the cursor. |
| `datatable--header-hover` | Target hovered header or row label cells. |
| `datatable--even-row` | Target even rows (row indices start at 0). |
| `datatable--odd-row` | Target odd rows (row indices start at 0). |
"""
@@ -241,8 +253,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
color: $text;
}
DataTable > .datatable--fixed {
text-style: bold;
background: $primary;
background: $primary 50%;
color: $text;
}
@@ -259,12 +270,17 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
color: $text;
}
DataTable > .datatable--cursor-fixed {
DataTable > .datatable--fixed-cursor {
background: $secondary 92%;
color: $text;
}
DataTable > .datatable--header-cursor {
background: $secondary-darken-1;
color: $text;
}
DataTable > .datatable--highlight-fixed {
DataTable > .datatable--header-hover {
background: $secondary 30%;
}
@@ -272,12 +288,13 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
background: $primary 15%;
}
DataTable > .datatable--highlight {
DataTable > .datatable--hover {
background: $secondary 20%;
}
"""
show_header = Reactive(True)
show_row_labels = Reactive(True)
fixed_rows = Reactive(0)
fixed_columns = Reactive(0)
zebra_stripes = Reactive(False)
@@ -459,12 +476,38 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
def __rich_repr__(self) -> rich.repr.Result:
yield "sender", self.sender
yield "column_key", self.column_key
yield "column_index", self.column_index
yield "label", self.label.plain
class RowLabelSelected(Message, bubble=True):
"""Posted when a row label is clicked."""
def __init__(
self,
sender: DataTable,
row_key: RowKey,
row_index: int,
label: Text,
):
self.row_key = row_key
"""The key for the column."""
self.row_index = row_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 "row_key", self.row_key
yield "row_index", self.row_index
yield "label", self.label.plain
def __init__(
self,
*,
show_header: bool = True,
show_row_labels: bool = True,
fixed_rows: int = 0,
fixed_columns: int = 0,
zebra_stripes: bool = False,
@@ -519,8 +562,23 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self._updated_cells: set[CellKey] = set()
"""Track which cells were updated, so that we can refresh them once on idle."""
self._show_hover_cursor = False
"""Used to hide the mouse hover cursor when the user uses the keyboard."""
self._update_count = 0
"""Number of update (INCLUDING SORT) operations so far. Used for cache invalidation."""
self._header_row_key = RowKey()
"""The header is a special row - not part of the data. Retrieve via this key."""
self._label_column_key = ColumnKey()
"""The column containing row labels is not part of the data. This key identifies it."""
self._labelled_row_exists = False
"""Whether or not the user has supplied any rows with labels."""
self._label_column = Column(self._label_column_key, Text(), auto_width=True)
"""The largest content width out of all row labels in the table."""
self.show_header = show_header
"""Show/hide the header row (the row of column labels)."""
self.show_row_labels = show_row_labels
"""Show/hide the column containing the labels of rows."""
self.header_height = header_height
"""The height of the header row (the row of column labels)."""
self.fixed_rows = fixed_rows
@@ -531,12 +589,6 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
"""Apply zebra effect on row backgrounds (light, dark, light, dark, ...)."""
self.show_cursor = show_cursor
"""Show/hide both the keyboard and hover cursor."""
self._show_hover_cursor = False
"""Used to hide the mouse hover cursor when the user uses the keyboard."""
self._update_count = 0
"""Number of update (INCLUDING SORT) operations so far. Used for cache invalidation."""
self._header_row_key = RowKey()
"""The header is a special row - not part of the data. Retrieve via this key."""
@property
def hover_row(self) -> int:
@@ -796,9 +848,20 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self._scroll_cursor_into_view()
self._clear_caches()
def watch_show_row_labels(self, show: bool) -> None:
width, height = self.virtual_size
column_width = self._label_column.render_width
width_change = column_width if show else -column_width
self.virtual_size = Size(width + width_change, height)
self._scroll_cursor_into_view()
self._clear_caches()
def watch_fixed_rows(self) -> None:
self._clear_caches()
def watch_fixed_columns(self) -> None:
self._clear_caches()
def watch_zebra_stripes(self) -> None:
self._clear_caches()
@@ -919,6 +982,11 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
elif cursor_type == "column":
self._highlight_column(column_index)
@property
def _row_label_column_width(self) -> int:
"""The render width of the column containing row labels"""
return self._label_column.render_width if self._should_render_row_labels else 0
def _update_column_widths(self, updated_cells: set[CellKey]) -> None:
"""Update the widths of the columns based on the newly updated cell widths."""
for row_key, column_key in updated_cells:
@@ -944,18 +1012,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None:
"""Called to recalculate the virtual (scrollable) size."""
console = self.app.console
for row_key in new_rows:
row_index = self._row_locations.get(row_key)
row = self.rows.get(row_key)
if row.label is not None:
self._labelled_row_exists = True
row_label, cells_in_row = self._get_row_renderables(row_index)
label_content_width = measure(console, row_label, 1) if row_label else 0
self._label_column.content_width = max(
self._label_column.content_width, label_content_width
)
if row_index is None:
continue
for column, renderable in zip(
self.ordered_columns, self._get_row_renderables(row_index)
):
content_width = measure(self.app.console, renderable, 1)
for column, renderable in zip(self.ordered_columns, cells_in_row):
content_width = measure(console, renderable, 1)
column.content_width = max(column.content_width, content_width)
self._clear_caches()
total_width = sum(column.render_width for column in self.columns.values())
data_cells_width = sum(column.render_width for column in self.columns.values())
total_width = data_cells_width + self._row_label_column_width
header_height = self.header_height if self.show_header else 0
self.virtual_size = Size(
total_width,
@@ -971,8 +1052,12 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
row_key = self._row_locations.get_key(row_index)
row = self.rows[row_key]
# The x-coordinate of a cell is the sum of widths of cells to the left.
x = sum(column.render_width for column in self.ordered_columns[:column_index])
# The x-coordinate of a cell is the sum of widths of the data cells to the left
# plus the width of the render width of the longest row label.
x = (
sum(column.render_width for column in self.ordered_columns[:column_index])
+ self._row_label_column_width
)
column_key = self._column_locations.get_key(column_index)
width = self.columns[column_key].render_width
height = row.height
@@ -1003,7 +1088,10 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
return Region(0, 0, 0, 0)
columns = self.columns
x = sum(column.render_width for column in self.ordered_columns[:column_index])
x = (
sum(column.render_width for column in self.ordered_columns[:column_index])
+ self._row_label_column_width
)
column_key = self._column_locations.get_key(column_index)
width = columns[column_key].render_width
header_height = self.header_height if self.show_header else 0
@@ -1028,6 +1116,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self._require_update_dimensions = True
self.cursor_coordinate = Coordinate(0, 0)
self.hover_coordinate = Coordinate(0, 0)
self._label_column = Column(self._label_column_key, Text(), auto_width=True)
self._labelled_row_exists = False
self.refresh()
def add_column(
@@ -1076,7 +1166,11 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
return column_key
def add_row(
self, *cells: CellType, height: int = 1, key: str | None = None
self,
*cells: CellType,
height: int = 1,
key: str | None = None,
label: TextType | None = None,
) -> RowKey:
"""Add a row at the bottom of the DataTable.
@@ -1085,6 +1179,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
height: The height of a row (in lines).
key: A key which uniquely identifies this row. If None, it will be generated
for you and returned.
label: The label for the row. Will be displayed to the left if supplied.
Returns:
Uniquely identifies this row. Can be used to retrieve this row regardless
@@ -1106,7 +1201,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
column.key: cell
for column, cell in zip_longest(self.ordered_columns, cells)
}
self.rows[row_key] = Row(row_key, height)
label = Text.from_markup(label) if isinstance(label, str) else label
self.rows[row_key] = Row(row_key, height, label)
self._new_rows.add(row_key)
self._require_update_dimensions = True
self.cursor_coordinate = self.cursor_coordinate
@@ -1288,26 +1384,45 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self._ordered_row_cache[cache_key] = ordered_rows
return ordered_rows
def _get_row_renderables(self, row_index: int) -> list[RenderableType]:
"""Get renderables for the row currently at the given row index.
@property
def _should_render_row_labels(self) -> bool:
"""Whether row labels should be rendered or not."""
return self._labelled_row_exists and self.show_row_labels
def _get_row_renderables(self, row_index: int) -> RowRenderables:
"""Get renderables for the row currently at the given row index. The renderables
returned here have already been passed through the default_cell_formatter.
Args:
row_index: Index of the row.
Returns:
List of renderables
A RowRenderables containing the optional label and the rendered cells.
"""
ordered_columns = self.ordered_columns
if row_index == -1:
row: list[RenderableType] = [column.label for column in ordered_columns]
return row
header_row: list[RenderableType] = [
column.label for column in ordered_columns
]
# This is the cell where header and row labels intersect
return RowRenderables(None, header_row)
ordered_row = self.get_row_at(row_index)
empty = Text()
return [
formatted_row_cells = [
Text() if datum is None else default_cell_formatter(datum) or empty
for datum, _ in zip_longest(ordered_row, range(len(self.columns)))
]
label = None
if self._should_render_row_labels:
row_metadata = self.rows.get(self._row_locations.get_key(row_index))
label = (
default_cell_formatter(row_metadata.label)
if row_metadata.label
else None
)
return RowRenderables(label, formatted_row_cells)
def _render_cell(
self,
@@ -1331,44 +1446,59 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
Returns:
A list of segments per line.
"""
is_header_row = row_index == -1
is_header_cell = row_index == -1
is_row_label_cell = column_index == -1
# The header row *and* fixed columns both have a different style (blue bg)
is_fixed_style = is_header_row or column_index < self.fixed_columns
is_fixed_style_cell = (
not is_header_cell
and not is_row_label_cell
and (row_index < self.fixed_rows or column_index < self.fixed_columns)
)
get_component = self.get_component_styles
show_cursor = self.show_cursor
if hover and show_cursor and self._show_hover_cursor:
style += self.get_component_styles("datatable--highlight").rich_style
if is_fixed_style:
# Apply subtle variation in style for the fixed (blue background by
style += get_component("datatable--hover").rich_style
if is_header_cell or is_row_label_cell:
# Apply subtle variation in style for the header/label (blue background by
# default) rows and columns affected by the cursor, to ensure we can
# still differentiate between the labels and the data.
style += self.get_component_styles(
"datatable--highlight-fixed"
).rich_style
style += get_component("datatable--header-hover").rich_style
if cursor and show_cursor:
style += self.get_component_styles("datatable--cursor").rich_style
if is_fixed_style:
style += self.get_component_styles("datatable--cursor-fixed").rich_style
style += get_component("datatable--cursor").rich_style
if is_header_cell or is_row_label_cell:
style += get_component("datatable--header-cursor").rich_style
elif is_fixed_style_cell:
style += get_component("datatable--fixed-cursor").rich_style
if is_header_row:
if is_header_cell:
row_key = self._header_row_key
else:
row_key = self._row_locations.get_key(row_index)
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:
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]
height = self.header_height if is_header_cell else self.rows[row_key].height
row_label, row_cells = self._get_row_renderables(row_index)
if is_row_label_cell:
cell = row_label if row_label is not None else ""
else:
cell = row_cells[column_index]
lines = self.app.console.render_lines(
Padding(cell, (0, 1)),
self.app.console.options.update_dimensions(width, height),
style=style,
)
self._cell_render_cache[cell_cache_key] = lines
return self._cell_render_cache[cell_cache_key]
def _render_line_in_row(
@@ -1431,16 +1561,34 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
else:
return False
is_header_row = row_key is self._header_row_key
render_cell = self._render_cell
if row_key in self._row_locations:
row_index = self._row_locations.get(row_key)
else:
row_index = -1
render_cell = self._render_cell
# If the row has a label, add it to fixed_row here with correct style.
fixed_row = []
header_style = self.get_component_styles("datatable--header").rich_style
if self._labelled_row_exists and self.show_row_labels:
# The width of the row label is updated again on idle
cell_location = Coordinate(row_index, -1)
label_cell_lines = render_cell(
row_index,
-1,
header_style,
width=self._row_label_column_width,
cursor=_should_highlight(cursor_location, cell_location, cursor_type),
hover=_should_highlight(hover_location, cell_location, cursor_type),
)[line_no]
fixed_row.append(label_cell_lines)
if self.fixed_columns:
fixed_style = self.get_component_styles("datatable--fixed").rich_style
fixed_style += Style.from_meta({"fixed": True})
fixed_row = []
for column_index, column in enumerate(
self.ordered_columns[: self.fixed_columns]
):
@@ -1448,7 +1596,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
fixed_cell_lines = render_cell(
row_index,
column_index,
fixed_style,
header_style if is_header_row else fixed_style,
column.render_width,
cursor=_should_highlight(
cursor_location, cell_location, cursor_type
@@ -1456,12 +1604,12 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
hover=_should_highlight(hover_location, cell_location, cursor_type),
)[line_no]
fixed_row.append(fixed_cell_lines)
else:
fixed_row = []
is_header_row = row_key is self._header_row_key
if is_header_row:
row_style = self.get_component_styles("datatable--header").rich_style
elif row_index < self.fixed_rows:
row_style = self.get_component_styles("datatable--fixed").rich_style
else:
if self.zebra_stripes:
component_row_style = (
@@ -1601,8 +1749,12 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
are rows and columns that do not participate in scrolling."""
top = self.header_height if self.show_header else 0
top += sum(row.height for row in self.ordered_rows[: self.fixed_rows])
left = sum(
column.render_width for column in self.ordered_columns[: self.fixed_columns]
left = (
sum(
column.render_width
for column in self.ordered_columns[: self.fixed_columns]
)
+ self._row_label_column_width
)
return Spacing(top, 0, 0, left)
@@ -1678,6 +1830,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
row_index = meta["row"]
column_index = meta["column"]
is_header_click = self.show_header and row_index == -1
is_row_label_click = self.show_row_labels and column_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]
@@ -1685,6 +1838,12 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self, column.key, column_index, label=column.label
)
self.post_message_no_wait(message)
elif is_row_label_click:
row = self.ordered_rows[row_index]
message = DataTable.RowLabelSelected(
self, row.key, row_index, label=row.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.
self.cursor_coordinate = Coordinate(row_index, column_index)

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