From 7dfc3e57a1f042fd78ef1b72188105c973e6c0c9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Wed, 3 May 2023 20:54:54 +0100 Subject: [PATCH 01/17] Fix a crash when DirectoryTree starts out anywhere other than . A hangover from the previous DirectoryTree, where setting the path didn't matter. This now sets it *after* calling Tree's __init__, thus ensuring the line cache and other related things have been created. --- CHANGELOG.md | 4 ++++ src/textual/widgets/_directory_tree.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b6aff24..798fb051d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Fixed + +- Fixed crash when creating a `DirectoryTree` starting anywhere other than `.` + ### Changed - The DataTable cursor is now scrolled into view when the cursor coordinate is changed programmatically https://github.com/Textualize/textual/issues/2459 diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 0090717d7..b2a8ab692 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -103,7 +103,6 @@ class DirectoryTree(Tree[DirEntry]): classes: A space-separated list of classes, or None for no classes. disabled: Whether the directory tree is disabled or not. """ - self.path = path super().__init__( str(path), data=DirEntry(Path(path)), @@ -112,6 +111,7 @@ class DirectoryTree(Tree[DirEntry]): classes=classes, disabled=disabled, ) + self.path = path def reload(self) -> None: """Reload the `DirectoryTree` contents.""" From 71c5a44fdb82f194db17f7ae4759f353fd70c7b2 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 08:53:24 +0100 Subject: [PATCH 02/17] Take Tree.show_root into account when drawing guides Rather than always start at the root, the code should start at the beginning of the path. See #2397. --- src/textual/widgets/_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 91ffe794e..9d5d9e0d2 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -976,8 +976,8 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): "tree--guides-selected", partial=True ) - hover = self.root._hover - selected = self.root._selected and self.has_focus + hover = line.path[0]._hover + selected = line.path[0]._selected and self.has_focus def get_guides(style: Style) -> tuple[str, str, str, str]: """Get the guide strings for a given style. From 38592c34bda68baeeb840bc61f77cdabe2ccf0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 4 May 2023 10:35:39 +0100 Subject: [PATCH 03/17] Add FAQ about DataTable scrolling. (#2466) Related issues: #2458 --- FAQ.md | 45 ++++++++++++++++++- questions/README.md | 4 +- questions/datatable-doesnt-scroll.question.md | 45 +++++++++++++++++++ questions/images.question.md | 3 +- 4 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 questions/datatable-doesnt-scroll.question.md diff --git a/FAQ.md b/FAQ.md index ffb91b1d7..72879e9d2 100644 --- a/FAQ.md +++ b/FAQ.md @@ -11,11 +11,12 @@ - [Why do some key combinations never make it to my app?](#why-do-some-key-combinations-never-make-it-to-my-app) - [Why doesn't Textual look good on macOS?](#why-doesn't-textual-look-good-on-macos) - [Why doesn't Textual support ANSI themes?](#why-doesn't-textual-support-ansi-themes) +- [Why doesn't the `DataTable` scroll programmatically?](#why-doesn't-the-`datatable`-scroll-programmatically) ## Does Textual support images? -Textual doesn't have built in support for images yet, but it is on the [Roadmap](https://textual.textualize.io/roadmap/). +Textual doesn't have built-in support for images yet, but it is on the [Roadmap](https://textual.textualize.io/roadmap/). See also the [rich-pixels](https://github.com/darrenburns/rich-pixels) project for a Rich renderable for images that works with Textual. @@ -231,6 +232,48 @@ Textual has a design system which guarantees apps will be readable on all platfo There is currently a light and dark version of the design system, but more are planned. It will also be possible for users to customize the source colors on a per-app or per-system basis. This means that in the future you will be able to modify the core colors to blend in with your chosen terminal theme. + +## Why doesn't the `DataTable` scroll programmatically? + +If it looks like the scrolling in your `DataTable` is broken, it may be because your `DataTable` does not have its height set, which means it is using the default value of `height: auto`. +In turn, this means that the `DataTable` itself does not have a scrollbar and, hence, it cannot scroll. + +If it looks like your `DataTable` has scrollbars, those might belong to the container(s) of the `DataTable`, which in turn makes it look like the scrolling of the `DataTable` is broken. + +To see the difference, try running the app below with and without the comment in the attribute `TableApp.CSS`. +Press E to scroll the `DataTable` to the end. +If the `CSS` is commented out, the `DataTable` does not have a scrollbar and, therefore, there is nothing to scroll. + +
+Example app. + +```py +from textual.app import App, ComposeResult +from textual.widgets import DataTable + + +class TableApp(App): + # CSS = "DataTable { height: 100% }" + + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.add_column("n") + table.add_rows([(n,) for n in range(300)]) + + def key_e(self) -> None: + self.query_one(DataTable).action_scroll_end() + + +app = TableApp() +if __name__ == "__main__": + app.run() +``` + +
+
Generated by [FAQtory](https://github.com/willmcgugan/faqtory) diff --git a/questions/README.md b/questions/README.md index bb201b128..f4b1f622d 100644 --- a/questions/README.md +++ b/questions/README.md @@ -5,13 +5,13 @@ Your questions should go in this directory. Question files should be named with the extension ".question.md". -To build the faq, install [faqtory](https://github.com/willmcgugan/faqtory) if you haven't already: +To build the FAQ, install [faqtory](https://github.com/willmcgugan/faqtory) if you haven't already: ``` pip install faqtory ``` -The run the following from the top of the repository: +Then run the following from the top of the repository: ``` faqtory build diff --git a/questions/datatable-doesnt-scroll.question.md b/questions/datatable-doesnt-scroll.question.md new file mode 100644 index 000000000..868d04c20 --- /dev/null +++ b/questions/datatable-doesnt-scroll.question.md @@ -0,0 +1,45 @@ +--- +title: "Why doesn't the `DataTable` scroll programmatically?" +alt_titles: + - "Scroll bindings from `DataTable` not working." + - "Datatable cursor goes off screen and doesn't scroll." +--- + +If it looks like the scrolling in your `DataTable` is broken, it may be because your `DataTable` does not have its height set, which means it is using the default value of `height: auto`. +In turn, this means that the `DataTable` itself does not have a scrollbar and, hence, it cannot scroll. + +If it looks like your `DataTable` has scrollbars, those might belong to the container(s) of the `DataTable`, which in turn makes it look like the scrolling of the `DataTable` is broken. + +To see the difference, try running the app below with and without the comment in the attribute `TableApp.CSS`. +Press E to scroll the `DataTable` to the end. +If the `CSS` is commented out, the `DataTable` does not have a scrollbar and, therefore, there is nothing to scroll. + +
+Example app. + +```py +from textual.app import App, ComposeResult +from textual.widgets import DataTable + + +class TableApp(App): + # CSS = "DataTable { height: 100% }" + + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.add_column("n") + table.add_rows([(n,) for n in range(300)]) + + def key_e(self) -> None: + self.query_one(DataTable).action_scroll_end() + + +app = TableApp() +if __name__ == "__main__": + app.run() +``` + +
diff --git a/questions/images.question.md b/questions/images.question.md index 6d86ea2f5..c6a1e3dc8 100644 --- a/questions/images.question.md +++ b/questions/images.question.md @@ -3,9 +3,8 @@ title: "Does Textual support images?" alt_titles: - "Can Textual display PNG / SVG files?" - "Render images" - --- -Textual doesn't have built in support for images yet, but it is on the [Roadmap](https://textual.textualize.io/roadmap/). +Textual doesn't have built-in support for images yet, but it is on the [Roadmap](https://textual.textualize.io/roadmap/). See also the [rich-pixels](https://github.com/darrenburns/rich-pixels) project for a Rich renderable for images that works with Textual. From cbd68b20dfd8da4c50ad54573609070e0957d9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 4 May 2023 11:42:19 +0100 Subject: [PATCH 04/17] Datatable scrolling faq (#2477) * Add FAQ about DataTable scrolling. Related issues: #2458 * Write concisely. * Update questions/datatable-doesnt-scroll.question.md Co-authored-by: Will McGugan * Remove example. * Add recommendation. --------- Co-authored-by: Will McGugan --- FAQ.md | 62 +++++-------------- questions/datatable-doesnt-scroll.question.md | 41 +----------- 2 files changed, 17 insertions(+), 86 deletions(-) diff --git a/FAQ.md b/FAQ.md index 72879e9d2..c2b268c85 100644 --- a/FAQ.md +++ b/FAQ.md @@ -2,16 +2,17 @@ # Frequently Asked Questions -- [Does Textual support images?](#does-textual-support-images) -- [How can I fix ImportError cannot import name ComposeResult from textual.app ?](#how-can-i-fix-importerror-cannot-import-name-composeresult-from-textualapp-) -- [How can I select and copy text in a Textual app?](#how-can-i-select-and-copy-text-in-a-textual-app) -- [How can I set a translucent app background?](#how-can-i-set-a-translucent-app-background) -- [How do I center a widget in a screen?](#how-do-i-center-a-widget-in-a-screen) -- [How do I pass arguments to an app?](#how-do-i-pass-arguments-to-an-app) -- [Why do some key combinations never make it to my app?](#why-do-some-key-combinations-never-make-it-to-my-app) -- [Why doesn't Textual look good on macOS?](#why-doesn't-textual-look-good-on-macos) -- [Why doesn't Textual support ANSI themes?](#why-doesn't-textual-support-ansi-themes) -- [Why doesn't the `DataTable` scroll programmatically?](#why-doesn't-the-`datatable`-scroll-programmatically) +- [Frequently Asked Questions](#frequently-asked-questions) + - [Does Textual support images?](#does-textual-support-images) + - [How can I fix ImportError cannot import name ComposeResult from textual.app ?](#how-can-i-fix-importerror-cannot-import-name-composeresult-from-textualapp-) + - [How can I select and copy text in a Textual app?](#how-can-i-select-and-copy-text-in-a-textual-app) + - [How can I set a translucent app background?](#how-can-i-set-a-translucent-app-background) + - [How do I center a widget in a screen?](#how-do-i-center-a-widget-in-a-screen) + - [How do I pass arguments to an app?](#how-do-i-pass-arguments-to-an-app) + - [Why do some key combinations never make it to my app?](#why-do-some-key-combinations-never-make-it-to-my-app) + - [Why doesn't Textual look good on macOS?](#why-doesnt-textual-look-good-on-macos) + - [Why doesn't Textual support ANSI themes?](#why-doesnt-textual-support-ansi-themes) + - [Why doesn't the `DataTable` scroll programmatically?](#why-doesnt-the-datatable-scroll-programmatically) ## Does Textual support images? @@ -235,44 +236,9 @@ There is currently a light and dark version of the design system, but more are p ## Why doesn't the `DataTable` scroll programmatically? -If it looks like the scrolling in your `DataTable` is broken, it may be because your `DataTable` does not have its height set, which means it is using the default value of `height: auto`. -In turn, this means that the `DataTable` itself does not have a scrollbar and, hence, it cannot scroll. - -If it looks like your `DataTable` has scrollbars, those might belong to the container(s) of the `DataTable`, which in turn makes it look like the scrolling of the `DataTable` is broken. - -To see the difference, try running the app below with and without the comment in the attribute `TableApp.CSS`. -Press E to scroll the `DataTable` to the end. -If the `CSS` is commented out, the `DataTable` does not have a scrollbar and, therefore, there is nothing to scroll. - -
-Example app. - -```py -from textual.app import App, ComposeResult -from textual.widgets import DataTable - - -class TableApp(App): - # CSS = "DataTable { height: 100% }" - - def compose(self) -> ComposeResult: - yield DataTable() - - def on_mount(self) -> None: - table = self.query_one(DataTable) - table.add_column("n") - table.add_rows([(n,) for n in range(300)]) - - def key_e(self) -> None: - self.query_one(DataTable).action_scroll_end() - - -app = TableApp() -if __name__ == "__main__": - app.run() -``` - -
+If scrolling in your `DataTable` is _apparently_ broken, it may be because your `DataTable` is using the default value of `height: auto`. +This means that the table will be sized to fit its rows without scrolling, which may cause the *container* (typically the screen) to scroll. +If you would like the table itself to scroll, set the height to something other than `auto`, like `100%`.
diff --git a/questions/datatable-doesnt-scroll.question.md b/questions/datatable-doesnt-scroll.question.md index 868d04c20..7cd2ca296 100644 --- a/questions/datatable-doesnt-scroll.question.md +++ b/questions/datatable-doesnt-scroll.question.md @@ -5,41 +5,6 @@ alt_titles: - "Datatable cursor goes off screen and doesn't scroll." --- -If it looks like the scrolling in your `DataTable` is broken, it may be because your `DataTable` does not have its height set, which means it is using the default value of `height: auto`. -In turn, this means that the `DataTable` itself does not have a scrollbar and, hence, it cannot scroll. - -If it looks like your `DataTable` has scrollbars, those might belong to the container(s) of the `DataTable`, which in turn makes it look like the scrolling of the `DataTable` is broken. - -To see the difference, try running the app below with and without the comment in the attribute `TableApp.CSS`. -Press E to scroll the `DataTable` to the end. -If the `CSS` is commented out, the `DataTable` does not have a scrollbar and, therefore, there is nothing to scroll. - -
-Example app. - -```py -from textual.app import App, ComposeResult -from textual.widgets import DataTable - - -class TableApp(App): - # CSS = "DataTable { height: 100% }" - - def compose(self) -> ComposeResult: - yield DataTable() - - def on_mount(self) -> None: - table = self.query_one(DataTable) - table.add_column("n") - table.add_rows([(n,) for n in range(300)]) - - def key_e(self) -> None: - self.query_one(DataTable).action_scroll_end() - - -app = TableApp() -if __name__ == "__main__": - app.run() -``` - -
+If scrolling in your `DataTable` is _apparently_ broken, it may be because your `DataTable` is using the default value of `height: auto`. +This means that the table will be sized to fit its rows without scrolling, which may cause the *container* (typically the screen) to scroll. +If you would like the table itself to scroll, set the height to something other than `auto`, like `100%`. From 04083a73f8b0abcc98c3fe9fd220f0c7ae4542b4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 4 May 2023 11:46:20 +0100 Subject: [PATCH 05/17] exclusive false (#2470) * exclusive false * changelog --- CHANGELOG.md | 1 + src/textual/dom.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798fb051d..fbc8be178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - The DataTable cursor is now scrolled into view when the cursor coordinate is changed programmatically https://github.com/Textualize/textual/issues/2459 +- run_worker exclusive parameter is now `False` by default https://github.com/Textualize/textual/pull/2470 ## [0.23.0] - 2023-05-03 diff --git a/src/textual/dom.py b/src/textual/dom.py index 3f549b059..1c8512f11 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -240,7 +240,7 @@ class DOMNode(MessagePump): description: str = "", exit_on_error: bool = True, start: bool = True, - exclusive: bool = True, + exclusive: bool = False, ) -> Worker[ResultType]: """Run work in a worker. From f6da3e9fb2bb58a728cfa66e58a79bcf2ba7564b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 14:16:34 +0100 Subject: [PATCH 06/17] Add always_update as a parameter for a var reactive --- CHANGELOG.md | 1 + src/textual/widgets/_tree.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56b6aff24..9de584fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - The DataTable cursor is now scrolled into view when the cursor coordinate is changed programmatically https://github.com/Textualize/textual/issues/2459 +- Added `always_update` as an optional argument for `reactive.var` ## [0.23.0] - 2023-05-03 diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 9d5d9e0d2..85255cf1e 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -429,7 +429,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): """Show the root of the tree.""" hover_line = var(-1) """The line number under the mouse pointer, or -1 if not under the mouse pointer.""" - cursor_line = var(-1) + cursor_line = var(-1, always_update=True) """The line with the cursor, or -1 if no cursor.""" show_guides = reactive(True) """Enable display of tree guide lines.""" From 10e61987e36a45b8a174d179f86e2280e66cfb94 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 14:17:20 +0100 Subject: [PATCH 07/17] Add always_update as a parameter for a var reactive Redux. I managed to commit the wrong thing last time; although it was using this and this was done for that. --- src/textual/reactive.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 08259200d..998d4079b 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -76,11 +76,13 @@ class Reactive(Generic[ReactiveType]): def var( cls, default: ReactiveType | Callable[[], ReactiveType], + always_update: bool = False, ) -> Reactive: """A reactive variable that doesn't update or layout. Args: default: A default value or callable that returns a default. + always_update: Call watchers even when the new value equals the old value. Returns: A Reactive descriptor. @@ -326,18 +328,21 @@ class var(Reactive[ReactiveType]): Args: default: A default value or callable that returns a default. init: Call watchers on initialize (post mount). + always_update: Call watchers even when the new value equals the old value. """ def __init__( self, default: ReactiveType | Callable[[], ReactiveType], init: bool = True, + always_update: bool = False, ) -> None: super().__init__( default, layout=False, repaint=False, init=init, + always_update=always_update, ) From bba694e93af0a7f33c288e2530849e169d0ba4ab Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 14:30:20 +0100 Subject: [PATCH 08/17] Update the ChangeLog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d584709d8..53070b80f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed crash when creating a `DirectoryTree` starting anywhere other than `.` +- Fixed line drawing in `Tree` when `Tree.show_root` is `True` https://github.com/Textualize/textual/issues/2397 +- Fixed line drawing in `Tree` not marking branches as selected when first getting focus https://github.com/Textualize/textual/issues/2397 ### Changed From c4eda48a0a0dedf0ac54eeaaa2ce651ec01ed486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 4 May 2023 14:55:14 +0100 Subject: [PATCH 09/17] Tweaks to DataTable docs. (#2481) * Tweaks to DataTable docs. Related PRs: #2479. * Fix link. --- docs/widgets/data_table.md | 4 ++-- src/textual/types.py | 2 ++ src/textual/widgets/_data_table.py | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/widgets/data_table.md b/docs/widgets/data_table.md index 215d0541b..6d2100a26 100644 --- a/docs/widgets/data_table.md +++ b/docs/widgets/data_table.md @@ -59,9 +59,9 @@ If you want to change the table based solely on coordinates, you can use the [co ### Cursors -The coordinate of the cursor is exposed via the `cursor_coordinate` reactive attribute. +The coordinate of the cursor is exposed via the [`cursor_coordinate`][textual.widgets.DataTable.cursor_coordinate] reactive attribute. Three types of cursors are supported: `cell`, `row`, and `column`. -Change the cursor type by assigning to the `cursor_type` reactive attribute. +Change the cursor type by assigning to the [`cursor_type`][textual.widgets.DataTable.cursor_type] reactive attribute. === "Column Cursor" diff --git a/src/textual/types.py b/src/textual/types.py index dd681f657..1b5d3ea25 100644 --- a/src/textual/types.py +++ b/src/textual/types.py @@ -7,11 +7,13 @@ from ._context import NoActiveAppError from ._types import CallbackType, MessageTarget, WatchCallbackType from .actions import ActionParseResult from .css.styles import RenderStyles +from .widgets._data_table import CursorType __all__ = [ "ActionParseResult", "Animatable", "CallbackType", + "CursorType", "EasingFunction", "MessageTarget", "NoActiveAppError", diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 5991c98db..bbe07ebc6 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -38,6 +38,7 @@ CellCacheKey: TypeAlias = ( LineCacheKey: TypeAlias = "tuple[int, int, int, int, Coordinate, Coordinate, Style, CursorType, bool, int, PseudoClasses]" RowCacheKey: TypeAlias = "tuple[RowKey, int, Style, Coordinate, Coordinate, CursorType, bool, bool, int, PseudoClasses]" CursorType = Literal["cell", "row", "column", "none"] +"""The legal types of cursors for [`DataTable.cursor_type`][textual.widgets.DataTable.cursor_type].""" CellType = TypeVar("CellType") CELL_X_PADDING = 2 @@ -304,7 +305,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): zebra_stripes = Reactive(False) header_height = Reactive(1) show_cursor = Reactive(True) - cursor_type = Reactive("cell") + cursor_type: Reactive[CursorType] = Reactive[CursorType]("cell") + """The type of the cursor of the `DataTable`.""" cursor_coordinate: Reactive[Coordinate] = Reactive( Coordinate(0, 0), repaint=False, always_update=True @@ -312,6 +314,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): hover_coordinate: Reactive[Coordinate] = Reactive( Coordinate(0, 0), repaint=False, always_update=True ) + """The coordinate of the `DataTable` that is being hovered.""" class CellHighlighted(Message, bubble=True): """Posted when the cursor moves to highlight a new cell. From 0b4d7fb091dfc84dd7ec446002c48d38f1a64f2e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 15:10:45 +0100 Subject: [PATCH 10/17] Test to ensure that Changed.control is Control.radio_button --- tests/toggles/test_radiobutton.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/toggles/test_radiobutton.py b/tests/toggles/test_radiobutton.py index dc32e8c0f..de4340c0c 100644 --- a/tests/toggles/test_radiobutton.py +++ b/tests/toggles/test_radiobutton.py @@ -15,7 +15,13 @@ class RadioButtonApp(App[None]): yield RadioButton(value=True, id="rb3") def on_radio_button_changed(self, event: RadioButton.Changed) -> None: - self.events_received.append((event.radio_button.id, event.radio_button.value)) + self.events_received.append( + ( + event.radio_button.id, + event.radio_button.value, + event.radio_button is event.control, + ) + ) async def test_radio_button_initial_state() -> None: @@ -51,7 +57,7 @@ async def test_radio_button_toggle() -> None: ] await pilot.pause() assert pilot.app.events_received == [ - ("rb1", True), - ("rb2", True), - ("rb3", False), + ("rb1", True, True), + ("rb2", True, True), + ("rb3", False, True), ] From b7cdbb0baad4ed3f12723885249a582877b3fc19 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 15:12:15 +0100 Subject: [PATCH 11/17] Test to ensure that Changed.control is Control.checkbox --- tests/toggles/test_checkbox.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/toggles/test_checkbox.py b/tests/toggles/test_checkbox.py index 573953f23..e0ae1e1fb 100644 --- a/tests/toggles/test_checkbox.py +++ b/tests/toggles/test_checkbox.py @@ -15,7 +15,9 @@ class CheckboxApp(App[None]): yield Checkbox(value=True, id="cb3") def on_checkbox_changed(self, event: Checkbox.Changed) -> None: - self.events_received.append((event.checkbox.id, event.checkbox.value)) + self.events_received.append( + (event.checkbox.id, event.checkbox.value, event.checkbox is event.control) + ) async def test_checkbox_initial_state() -> None: @@ -43,7 +45,7 @@ async def test_checkbox_toggle() -> None: ] await pilot.pause() assert pilot.app.events_received == [ - ("cb1", True), - ("cb2", True), - ("cb3", False), + ("cb1", True, True), + ("cb2", True, True), + ("cb3", False, True), ] From 2113f415a0318a9737d560aa373f55cb7e7ad426 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 15:17:52 +0100 Subject: [PATCH 12/17] Add a test that toggling a pressed radio button has no effect --- tests/toggles/test_radioset.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index dc9de5b1b..20980b8d9 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -52,6 +52,15 @@ async def test_radio_sets_toggle(): ] +async def test_radioset_same_button_mash(): + """Mashing the same button should have no effect.""" + async with RadioSetApp().run_test() as pilot: + assert pilot.app.query_one("#from_buttons", RadioSet).pressed_index == 2 + pilot.app.query_one("#from_buttons", RadioSet)._nodes[2].toggle() + assert pilot.app.query_one("#from_buttons", RadioSet).pressed_index == 2 + assert pilot.app.events_received == [] + + async def test_radioset_inner_navigation(): """Using the cursor keys should navigate between buttons in a set.""" async with RadioSetApp().run_test() as pilot: From b1443c01622f6bd5f3eaa75950d9732c177613a4 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 15:22:30 +0100 Subject: [PATCH 13/17] Test that radioset wraps around when going off the top --- tests/toggles/test_radioset.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index 20980b8d9..66140e252 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -66,7 +66,13 @@ async def test_radioset_inner_navigation(): async with RadioSetApp().run_test() as pilot: assert pilot.app.screen.focused is None await pilot.press("tab") - for key, landing in (("down", 1), ("up", 0), ("right", 1), ("left", 0)): + for key, landing in ( + ("down", 1), + ("up", 0), + ("right", 1), + ("left", 0), + ("up", 2), + ): await pilot.press(key, "enter") assert ( pilot.app.query_one("#from_buttons", RadioSet).pressed_button From e7d3b94334383631cf6f41fe69acae35a80dfc8b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 15:25:33 +0100 Subject: [PATCH 14/17] Test that radioset wraps around when going off the bottom --- tests/toggles/test_radioset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index 66140e252..66cfb80e7 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -72,6 +72,7 @@ async def test_radioset_inner_navigation(): ("right", 1), ("left", 0), ("up", 2), + ("down", 0), ): await pilot.press(key, "enter") assert ( From 19f4f64d96585ffe555dcab4c30b7b88e4845572 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 15:39:33 +0100 Subject: [PATCH 15/17] Add tests for selection navigation in a radioset with no buttons pressed --- tests/toggles/test_radioset.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index 66cfb80e7..b2869eb5a 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -79,6 +79,15 @@ async def test_radioset_inner_navigation(): pilot.app.query_one("#from_buttons", RadioSet).pressed_button == pilot.app.query_one("#from_buttons").children[landing] ) + async with RadioSetApp().run_test() as pilot: + assert pilot.app.screen.focused is None + await pilot.press("tab") + assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_buttons") + await pilot.press("tab") + assert pilot.app.screen.focused is pilot.app.screen.query_one("#from_strings") + assert pilot.app.query_one("#from_strings", RadioSet)._selected == 0 + await pilot.press("down") + assert pilot.app.query_one("#from_strings", RadioSet)._selected == 1 async def test_radioset_breakout_navigation(): From 8b36d29e74412729d67d1da151d7a3532a83d83f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 16:22:39 +0100 Subject: [PATCH 16/17] Add a test for a radio set getting focus when a button gets clicked --- tests/toggles/test_radioset.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/toggles/test_radioset.py b/tests/toggles/test_radioset.py index b2869eb5a..95025bf37 100644 --- a/tests/toggles/test_radioset.py +++ b/tests/toggles/test_radioset.py @@ -11,7 +11,7 @@ class RadioSetApp(App[None]): def compose(self) -> ComposeResult: with RadioSet(id="from_buttons"): - yield RadioButton() + yield RadioButton(id="clickme") yield RadioButton() yield RadioButton(value=True) yield RadioSet("One", "True", "Three", id="from_strings") @@ -36,6 +36,14 @@ async def test_radio_sets_initial_state(): assert pilot.app.events_received == [] +async def test_click_sets_focus(): + """Clicking within a radio set should set focus.""" + async with RadioSetApp().run_test() as pilot: + assert pilot.app.screen.focused is None + await pilot.click("#clickme") + assert pilot.app.screen.focused == pilot.app.query_one("#from_buttons") + + async def test_radio_sets_toggle(): """Test the status of the radio sets after they've been toggled.""" async with RadioSetApp().run_test() as pilot: From 6139c95f3aa068b319d850d776a15b7137788377 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 4 May 2023 16:43:20 +0100 Subject: [PATCH 17/17] Test that the event aliases are actually the same reference I thought I thought I wanted what I thought but now I think about it I think I thought wrong and now I think better. --- tests/toggles/test_checkbox.py | 2 +- tests/toggles/test_radiobutton.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/toggles/test_checkbox.py b/tests/toggles/test_checkbox.py index e0ae1e1fb..095f87485 100644 --- a/tests/toggles/test_checkbox.py +++ b/tests/toggles/test_checkbox.py @@ -16,7 +16,7 @@ class CheckboxApp(App[None]): def on_checkbox_changed(self, event: Checkbox.Changed) -> None: self.events_received.append( - (event.checkbox.id, event.checkbox.value, event.checkbox is event.control) + (event.checkbox.id, event.checkbox.value, event.checkbox == event.control) ) diff --git a/tests/toggles/test_radiobutton.py b/tests/toggles/test_radiobutton.py index de4340c0c..0dc4fda05 100644 --- a/tests/toggles/test_radiobutton.py +++ b/tests/toggles/test_radiobutton.py @@ -19,7 +19,7 @@ class RadioButtonApp(App[None]): ( event.radio_button.id, event.radio_button.value, - event.radio_button is event.control, + event.radio_button == event.control, ) )