diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd4b7f829..0856eca6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - grid-columns and grid-rows now accept an `auto` token to detect the optimal size https://github.com/Textualize/textual/pull/3107 +### Fixed + +- Fixed auto height container with default grid-rows https://github.com/Textualize/textual/issues/1597 +- Fixed `page_up` and `page_down` bug in `DataTable` when `show_header = False` https://github.com/Textualize/textual/pull/3093 + ## [0.33.0] - 2023-08-15 + ### Fixed - Fixed unintuitive sizing behaviour of TabbedContent https://github.com/Textualize/textual/issues/2411 @@ -58,6 +64,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added App.begin_capture_print, App.end_capture_print, Widget.begin_capture_print, Widget.end_capture_print https://github.com/Textualize/textual/issues/2952 - Added the ability to run async methods as thread workers https://github.com/Textualize/textual/pull/2938 +- Added `ListView.extend` method to append multiple items https://github.com/Textualize/textual/pull/3012 - Added `App.stop_animation` https://github.com/Textualize/textual/issues/2786 - Added `Widget.stop_animation` https://github.com/Textualize/textual/issues/2786 diff --git a/docs/examples/widgets/header_app_title.py b/docs/examples/widgets/header_app_title.py new file mode 100644 index 0000000000..1b433148e2 --- /dev/null +++ b/docs/examples/widgets/header_app_title.py @@ -0,0 +1,16 @@ +from textual.app import App, ComposeResult +from textual.widgets import Header + + +class HeaderApp(App): + def compose(self) -> ComposeResult: + yield Header() + + def on_mount(self) -> None: + self.title = "Header Application" + self.sub_title = "With title and sub-title" + + +if __name__ == "__main__": + app = HeaderApp() + app.run() diff --git a/docs/widgets/header.md b/docs/widgets/header.md index 3286b3e8c1..c589ddcf00 100644 --- a/docs/widgets/header.md +++ b/docs/widgets/header.md @@ -2,6 +2,10 @@ A simple header widget which docks itself to the top of the parent container. +!!! note + + The application title which is shown in the header is taken from the [`title`][textual.app.App.title] and [`sub_title`][textual.app.App.sub_title] of the application. + - [ ] Focusable - [ ] Container @@ -20,6 +24,19 @@ The example below shows an app with a `Header`. --8<-- "docs/examples/widgets/header.py" ``` +This example shows how to set the text in the `Header` using `App.title` and `App.sub_title`: + +=== "Output" + + ```{.textual path="docs/examples/widgets/header_app_title.py"} + ``` + +=== "header_app_title.py" + + ```python + --8<-- "docs/examples/widgets/header_app_title.py" + ``` + ## Reactive Attributes | Name | Type | Default | Description | diff --git a/src/textual/demo.py b/src/textual/demo.py index df63b53235..0ec59483f2 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -7,6 +7,7 @@ from rich.console import RenderableType from rich.json import JSON from rich.markdown import Markdown +from rich.markup import escape from rich.pretty import Pretty from rich.syntax import Syntax from rich.table import Table @@ -269,14 +270,6 @@ class SubTitle(Static): pass -class Notification(Static): - def on_mount(self) -> None: - self.set_timer(3, self.remove) - - def on_click(self) -> None: - self.remove() - - class DemoApp(App[None]): CSS_PATH = "demo.css" TITLE = "Textual Demo" @@ -285,7 +278,7 @@ class DemoApp(App[None]): ("ctrl+t", "app.toggle_dark", "Toggle Dark mode"), ("ctrl+s", "app.screenshot()", "Screenshot"), ("f1", "app.toggle_class('RichLog', '-hidden')", "Notes"), - Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True), + Binding("ctrl+q", "app.quit", "Quit", show=True), ] show_sidebar = reactive(False) @@ -390,9 +383,9 @@ def action_screenshot(self, filename: str | None = None, path: str = "./") -> No """ self.bell() path = self.save_screenshot(filename, path) - message = Text.assemble("Screenshot saved to ", (f"'{path}'", "bold green")) - self.add_note(message) - self.screen.mount(Notification(message)) + message = f"Screenshot saved to [bold green]'{escape(str(path))}'[/]" + self.add_note(Text.from_markup(message)) + self.notify(message) app = DemoApp() diff --git a/src/textual/events.py b/src/textual/events.py index 0a24c034e1..94e13df6f5 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -535,7 +535,7 @@ class Leave(Event, bubble=False, verbose=True): class Focus(Event, bubble=False): """Sent when a widget is focussed. - - [X] Bubbles + - [ ] Bubbles - [ ] Verbose """ @@ -543,7 +543,7 @@ class Focus(Event, bubble=False): class Blur(Event, bubble=False): """Sent when a widget is blurred (un-focussed). - - [X] Bubbles + - [ ] Bubbles - [ ] Verbose """ diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index c1bb3ff47e..3ef2e315f8 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -21,7 +21,9 @@ def arrange( self, parent: Widget, children: list[Widget], size: Size ) -> ArrangeResult: styles = parent.styles - row_scalars = styles.grid_rows or [Scalar.parse("1fr")] + row_scalars = styles.grid_rows or ( + [Scalar.parse("1fr")] if size.height else [Scalar.parse("auto")] + ) column_scalars = styles.grid_columns or [Scalar.parse("1fr")] gutter_horizontal = styles.grid_gutter_horizontal gutter_vertical = styles.grid_gutter_vertical diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index db3b2801d4..6201983dd6 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -2205,9 +2205,8 @@ async def _on_click(self, event: events.Click) -> None: def action_page_down(self) -> None: """Move the cursor one page down.""" self._set_hover_cursor(False) - cursor_type = self.cursor_type - if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): - height = self.size.height - self.header_height if self.show_header else 0 + if self.show_cursor and self.cursor_type in ("cell", "row"): + height = self.size.height - (self.header_height if self.show_header else 0) # Determine how many rows constitutes a "page" offset = 0 @@ -2228,9 +2227,8 @@ def action_page_down(self) -> None: def action_page_up(self) -> None: """Move the cursor one page up.""" self._set_hover_cursor(False) - cursor_type = self.cursor_type - if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): - height = self.size.height - self.header_height if self.show_header else 0 + if self.show_cursor and self.cursor_type in ("cell", "row"): + height = self.size.height - (self.header_height if self.show_header else 0) # Determine how many rows constitutes a "page" offset = 0 diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index f9f423b53a..a3bf67b896 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import ClassVar, Optional +from typing import ClassVar, Iterable, Optional from textual.await_remove import AwaitRemove from textual.binding import Binding, BindingType @@ -172,6 +172,21 @@ def watch_index(self, old_index: int, new_index: int) -> None: self._scroll_highlighted_region() self.post_message(self.Highlighted(self, new_child)) + def extend(self, items: Iterable[ListItem]) -> AwaitMount: + """Append multiple new ListItems to the end of the ListView. + + Args: + items: The ListItems to append. + + Returns: + An awaitable that yields control to the event loop + until the DOM has been updated with the new child items. + """ + await_mount = self.mount(*items) + if len(self) == 1: + self.index = 0 + return await_mount + def append(self, item: ListItem) -> AwaitMount: """Append a new ListItem to the end of the ListView. @@ -182,10 +197,7 @@ def append(self, item: ListItem) -> AwaitMount: An awaitable that yields control to the event loop until the DOM has been updated with the new child item. """ - await_mount = self.mount(item) - if len(self) == 1: - self.index = 0 - return await_mount + return self.extend([item]) def clear(self) -> AwaitRemove: """Clear all items from the ListView. diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index f68d2917b0..4fe8a4f049 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -484,6 +484,165 @@ ''' # --- +# name: test_auto_grid_default_height + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GridHeightAuto + + + + + + + + + + GridHeightAuto + Here is some text before the grid + ────────────────────────────────────────────────────────────────────────────── + Cell #0Cell #1Cell #2 + Cell #3Cell #4Cell #5 + Cell #6Cell #7Cell #8 + ────────────────────────────────────────────────────────────────────────────── + Here is some text after the grid + + + + + + + + + + + + + + + +  G  Grid  V  Vertical  H  Horizontal  C  Container  + + + + + ''' +# --- # name: test_auto_table ''' @@ -13711,168 +13870,168 @@ font-weight: 700; } - .terminal-1845817647-matrix { + .terminal-2095575413-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1845817647-title { + .terminal-2095575413-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1845817647-r1 { fill: #c5c8c6 } - .terminal-1845817647-r2 { fill: #e3e3e3 } - .terminal-1845817647-r3 { fill: #e1e1e1 } - .terminal-1845817647-r4 { fill: #e2e2e2 } - .terminal-1845817647-r5 { fill: #14191f } - .terminal-1845817647-r6 { fill: #004578 } - .terminal-1845817647-r7 { fill: #262626 } - .terminal-1845817647-r8 { fill: #e2e2e2;font-weight: bold;text-decoration: underline; } - .terminal-1845817647-r9 { fill: #e2e2e2;font-weight: bold } - .terminal-1845817647-r10 { fill: #7ae998 } - .terminal-1845817647-r11 { fill: #4ebf71;font-weight: bold } - .terminal-1845817647-r12 { fill: #008139 } - .terminal-1845817647-r13 { fill: #dde8f3;font-weight: bold } - .terminal-1845817647-r14 { fill: #ddedf9 } + .terminal-2095575413-r1 { fill: #c5c8c6 } + .terminal-2095575413-r2 { fill: #e3e3e3 } + .terminal-2095575413-r3 { fill: #e1e1e1 } + .terminal-2095575413-r4 { fill: #e2e2e2 } + .terminal-2095575413-r5 { fill: #14191f } + .terminal-2095575413-r6 { fill: #004578 } + .terminal-2095575413-r7 { fill: #262626 } + .terminal-2095575413-r8 { fill: #e2e2e2;font-weight: bold;text-decoration: underline; } + .terminal-2095575413-r9 { fill: #e2e2e2;font-weight: bold } + .terminal-2095575413-r10 { fill: #7ae998 } + .terminal-2095575413-r11 { fill: #4ebf71;font-weight: bold } + .terminal-2095575413-r12 { fill: #008139 } + .terminal-2095575413-r13 { fill: #dde8f3;font-weight: bold } + .terminal-2095575413-r14 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual Demo + Textual Demo - - - - Textual Demo - - - TOP - - ▆▆ - - Widgets - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - Rich contentTextual Demo - - Welcome! Textual is a framework for creating sophisticated - applications with the terminal.                            - CSS - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Start  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - - - - -  CTRL+C  Quit  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  + + + + Textual Demo + + + TOP + + ▆▆ + + Widgets + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + Rich contentTextual Demo + + Welcome! Textual is a framework for creating sophisticated + applications with the terminal.                            + CSS + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Start  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + +  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  CTRL+Q  Quit  diff --git a/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py b/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py new file mode 100644 index 0000000000..1b5883466e --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Type +from textual.app import App, ComposeResult +from textual.containers import Container, Horizontal, Vertical, Grid +from textual.widgets import Header, Footer, Label +from textual.binding import Binding + + +class GridHeightAuto(App[None]): + CSS = """ + #test-area { + + border: solid red; + height: auto; + } + + Grid { + grid-size: 3; + # grid-rows: auto; + } + """ + + BINDINGS = [ + Binding("g", "grid", "Grid"), + Binding("v", "vertical", "Vertical"), + Binding("h", "horizontal", "Horizontal"), + Binding("c", "container", "Container"), + ] + + def compose(self) -> ComposeResult: + yield Header() + yield Vertical(Label("Select a container to test (see footer)"), id="sandbox") + yield Footer() + + def build(self, out_of: Type[Container | Grid | Horizontal | Vertical]) -> None: + self.query("#sandbox > *").remove() + self.query_one("#sandbox", Vertical).mount( + Label("Here is some text before the grid"), + out_of(*[Label(f"Cell #{n}") for n in range(9)], id="test-area"), + Label("Here is some text after the grid"), + ) + + def action_grid(self): + self.build(Grid) + + def action_vertical(self): + self.build(Vertical) + + def action_horizontal(self): + self.build(Horizontal) + + def action_container(self): + self.build(Container) + + +if __name__ == "__main__": + GridHeightAuto().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 5ad871c740..36b003bb7e 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -641,3 +641,7 @@ def test_digits(snap_compare) -> None: def test_auto_grid(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "auto_grid.py") + + +def test_auto_grid_default_height(snap_compare) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "auto_grid_default_height.py", press=["g"]) diff --git a/tests/test_data_table.py b/tests/test_data_table.py index a1c08edc7c..e00b9432a4 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -173,11 +173,13 @@ async def test_empty_table_interactions(): assert app.message_names == [] -async def test_cursor_movement_with_home_pagedown_etc(): +@pytest.mark.parametrize("show_header", [True, False]) +async def test_cursor_movement_with_home_pagedown_etc(show_header): app = DataTableApp() async with app.run_test() as pilot: table = app.query_one(DataTable) + table.show_header = show_header table.add_columns("A", "B") table.add_rows(ROWS) await pilot.press("right", "pagedown")