diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cac4c421..9338f1919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [0.11.0] - Unreleased +### Changed + +- Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637 + ### Fixed - Fixed stuck screen https://github.com/Textualize/textual/issues/1632 - Fixed programmatic style changes not refreshing children layouts when parent widget did not change size https://github.com/Textualize/textual/issues/1607 - Fixed relative units in `grid-rows` and `grid-columns` being computed with respect to the wrong dimension https://github.com/Textualize/textual/issues/1406 +- Programmatically setting `overflow_x`/`overflow_y` refreshes the layout correctly https://github.com/Textualize/textual/issues/1616 +- Fixed double-paste into `Input` https://github.com/Textualize/textual/issues/1657 +- Added a workaround for an apparent Windows Terminal paste issue https://github.com/Textualize/textual/issues/1661 ## [0.10.1] - 2023-01-20 diff --git a/docs/api/tree_node.md b/docs/api/tree_node.md index ad122443e..b136c4a6a 100644 --- a/docs/api/tree_node.md +++ b/docs/api/tree_node.md @@ -1 +1 @@ -::: textual.widgets.TreeNode +::: textual.widgets.tree.TreeNode diff --git a/docs/index.md b/docs/index.md index 1d776e820..4d1c05832 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,24 @@ # Introduction -Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. Built with ❤️ by [Textualize.io](https://www.textualize.io) +Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. + +!!! tip + + See the navigation links in the header or side-bars. Click the :octicons-three-bars-16: button (top left) on mobile. -## In a hurry? +[Get started](./getting_started.md){ .md-button .md-button--primary } or go straight to the [Tutorial](./tutorial.md) -See the navigation links in the header or side-bars. Click the :octicons-three-bars-16: button (top left) on mobile. -[Get started](./getting_started.md){ .md-button .md-button--primary } or [Tutorial](./tutorial.md){ .md-button .md-button--secondary } ## What is Textual? -Textual is a framework for building applications that run within your terminal. Text User Interfaces (TUIs) have a number of advantages over web and desktop apps. +Textual is a *Rapid Application Development* framework for Python, built by [Textualize.io](https://www.textualize.io). + + +Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (*coming soon*) a web browser. + +
@@ -26,7 +33,7 @@ Textual is a framework for building applications that run within your terminal. --- - Low system requirements. Run Textual on a single board computer if you want to. + Run Textual on a single board computer if you want to. @@ -53,7 +60,7 @@ Textual is a framework for building applications that run within your terminal. -- :material-scale-balance:{ .lg .middle } __Open Source, MIT__ +- :material-scale-balance:{ .lg .middle } __Open Source__ --- diff --git a/docs/widgets/list_view.md b/docs/widgets/list_view.md index ff6b6b6eb..fe7a8c563 100644 --- a/docs/widgets/list_view.md +++ b/docs/widgets/list_view.md @@ -64,21 +64,6 @@ or by clicking on it. | `item` | `ListItem` | The item that was selected. | -### ChildrenUpdated - -The `ListView.ChildrenUpdated` message is emitted when the elements in the `ListView` -are changed (e.g. a child is added, or the list is cleared). - -- [x] Bubbles - -#### Attributes - -| attribute | type | purpose | -| ---------- | ---------------- | ------------------------- | -| `children` | `list[ListItem]` | The new ListView children | - - - ## See Also * [ListView](../api/list_view.md) code reference diff --git a/docs/widgets/tree.md b/docs/widgets/tree.md index 801d993f0..7f0772566 100644 --- a/docs/widgets/tree.md +++ b/docs/widgets/tree.md @@ -21,7 +21,7 @@ The example below creates a simple tree. --8<-- "docs/examples/widgets/tree.py" ``` -A each tree widget has a "root" attribute which is an instance of a [TreeNode][textual.widgets.TreeNode]. Call [add()][textual.widgets.TreeNode.add] or [add_leaf()][textual.widgets.TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child, so you can add more levels. +Tree widgets have a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree,TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels. ## Reactive Attributes @@ -43,9 +43,9 @@ The `Tree.NodeSelected` message is sent when the user selects a tree node. #### Attributes -| attribute | type | purpose | -| --------- | ------------------------------------ | -------------- | -| `node` | [TreeNode][textual.widgets.TreeNode] | Selected node. | +| attribute | type | purpose | +| --------- | ----------------------------------------- | -------------- | +| `node` | [TreeNode][textual.widgets.tree.TreeNode] | Selected node. | ### NodeExpanded @@ -54,9 +54,9 @@ The `Tree.NodeExpanded` message is sent when the user expands a node in the tree #### Attributes -| attribute | type | purpose | -| --------- | ------------------------------------ | -------------- | -| `node` | [TreeNode][textual.widgets.TreeNode] | Expanded node. | +| attribute | type | purpose | +| --------- | ----------------------------------------- | -------------- | +| `node` | [TreeNode][textual.widgets.tree.TreeNode] | Expanded node. | ### NodeCollapsed @@ -67,9 +67,9 @@ The `Tree.NodeCollapsed` message is sent when the user expands a node in the tre #### Attributes -| attribute | type | purpose | -| --------- | ------------------------------------ | --------------- | -| `node` | [TreeNode][textual.widgets.TreeNode] | Collapsed node. | +| attribute | type | purpose | +| --------- | ----------------------------------------- | --------------- | +| `node` | [TreeNode][textual.widgets.tree.TreeNode] | Collapsed node. | @@ -77,4 +77,4 @@ The `Tree.NodeCollapsed` message is sent when the user expands a node in the tre ## See Also * [Tree][textual.widgets.Tree] code reference -* [TreeNode][textual.widgets.TreeNode] code reference +* [TreeNode][textual.widgets.tree.TreeNode] code reference diff --git a/examples/json_tree.py b/examples/json_tree.py index 7380fcc25..45cede3c5 100644 --- a/examples/json_tree.py +++ b/examples/json_tree.py @@ -4,7 +4,8 @@ from pathlib import Path from rich.text import Text from textual.app import App, ComposeResult -from textual.widgets import Header, Footer, Tree, TreeNode +from textual.widgets import Header, Footer, Tree +from textual.widgets.tree import TreeNode class TreeApp(App): diff --git a/mkdocs.yml b/mkdocs.yml index 5c29f469c..a9b8de413 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -221,6 +221,7 @@ theme: - navigation.tabs - navigation.indexes - navigation.tabs.sticky + - navigation.footer - content.code.annotate - content.code.copy palette: diff --git a/poetry.lock b/poetry.lock index 885b92cd0..1f4cd039d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -171,7 +171,7 @@ python-versions = "*" [[package]] name = "coverage" -version = "7.0.5" +version = "7.1.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -322,7 +322,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" -version = "2.5.13" +version = "2.5.15" description = "File identification library for Python" category = "dev" optional = false @@ -521,7 +521,7 @@ doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "p [[package]] name = "mkdocstrings" -version = "0.19.1" +version = "0.20.0" description = "Automatic documentation from sources, for MkDocs." category = "dev" optional = false @@ -626,7 +626,7 @@ python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.10.3" +version = "0.11.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -1028,7 +1028,7 @@ dev = ["aiohttp", "click", "msgpack"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "0e3bcf48b37c16096a3c2b2f7d3f548494f9a22ebdee2e2c5d8ac74b80ab344e" +content-hash = "d76445ef1521cd4068907433b09d59fc1ed56f03e61063c5ad7376bb9823a8e7" [metadata.files] aiohttp = [ @@ -1193,57 +1193,57 @@ colored = [ {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"}, ] coverage = [ - {file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"}, - {file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"}, - {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"}, - {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"}, - {file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"}, - {file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"}, - {file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"}, - {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"}, - {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"}, - {file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"}, - {file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"}, - {file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"}, - {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"}, - {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"}, - {file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"}, - {file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"}, - {file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"}, - {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"}, - {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"}, - {file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"}, - {file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"}, - {file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"}, - {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"}, - {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"}, - {file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"}, - {file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"}, - {file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"}, - {file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"}, + {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, + {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, + {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, + {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, + {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, + {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, + {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, + {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, + {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, + {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, + {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, + {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, + {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, + {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, + {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, @@ -1362,8 +1362,8 @@ httpx = [ {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, ] identify = [ - {file = "identify-2.5.13-py2.py3-none-any.whl", hash = "sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7"}, - {file = "identify-2.5.13.tar.gz", hash = "sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10"}, + {file = "identify-2.5.15-py2.py3-none-any.whl", hash = "sha256:1f4b36c5f50f3f950864b2a047308743f064eaa6f6645da5e5c780d1c7125487"}, + {file = "identify-2.5.15.tar.gz", hash = "sha256:c22aa206f47cc40486ecf585d27ad5f40adbfc494a3fa41dc3ed0499a23b123f"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1470,8 +1470,8 @@ mkdocs-rss-plugin = [ {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"}, ] mkdocstrings = [ - {file = "mkdocstrings-0.19.1-py3-none-any.whl", hash = "sha256:32a38d88f67f65b264184ea71290f9332db750d189dea4200cbbe408d304c261"}, - {file = "mkdocstrings-0.19.1.tar.gz", hash = "sha256:d1037cacb4b522c1e8c164ed5d00d724a82e49dcee0af80db8fb67b384faeef9"}, + {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, + {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, ] mkdocstrings-python = [ {file = "mkdocstrings-python-0.8.3.tar.gz", hash = "sha256:9ae473f6dc599339b09eee17e4d2b05d6ac0ec29860f3fc9b7512d940fc61adf"}, @@ -1656,8 +1656,8 @@ packaging = [ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] pathspec = [ - {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, - {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, ] platformdirs = [ {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, diff --git a/pyproject.toml b/pyproject.toml index 99ee64632..e16e5253e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ black = "^22.3.0,<22.10.0" # macos wheel issue on 22.10 mypy = "^0.990" pytest-cov = "^2.12.1" mkdocs = "^1.3.0" -mkdocstrings = {extras = ["python"], version = "^0.19.0"} +mkdocstrings = {extras = ["python"], version = "^0.20.0"} mkdocs-material = "^8.2.15" pre-commit = "^2.13.0" pytest-aiohttp = "^1.0.4" diff --git a/src/textual/_context.py b/src/textual/_context.py index 04b264d33..625152a95 100644 --- a/src/textual/_context.py +++ b/src/textual/_context.py @@ -4,6 +4,7 @@ from contextvars import ContextVar if TYPE_CHECKING: from .app import App + from .message_pump import MessagePump class NoActiveAppError(RuntimeError): @@ -11,3 +12,4 @@ class NoActiveAppError(RuntimeError): active_app: ContextVar["App"] = ContextVar("active_app") +active_message_pump: ContextVar["MessagePump"] = ContextVar("active_message_pump") diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 1bbec555d..b97a1abd1 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -118,7 +118,10 @@ class XTermParser(Parser[events.Event]): # ESC from the closing bracket, since at that point we didn't know what # the full escape code was. pasted_text = "".join(paste_buffer[:-1]) - on_token(events.Paste(self.sender, text=pasted_text)) + # Note the removal of NUL characters: https://github.com/Textualize/textual/issues/1661 + on_token( + events.Paste(self.sender, text=pasted_text.replace("\x00", "")) + ) paste_buffer.clear() character = ESC if use_prior_escape else (yield read1()) diff --git a/src/textual/app.py b/src/textual/app.py index 068b94e0b..8712353e1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -46,7 +46,7 @@ from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction from ._ansi_sequences import SYNC_END, SYNC_START from ._asyncio import create_task from ._callback import invoke -from ._context import active_app +from ._context import active_app, active_message_pump from ._event_broker import NoHandler, extract_handler_actions from ._filter import LineFilter, Monochrome from ._path import _make_path_object_relative @@ -1095,8 +1095,12 @@ class App(Generic[ReturnType], DOMNode): Args: *widgets: The widget(s) to mount. - before: Optional location to mount before. - after: Optional location to mount after. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. Returns: An awaitable object that waits for widgets to be mounted. @@ -1113,6 +1117,7 @@ class App(Generic[ReturnType], DOMNode): def mount_all( self, widgets: Iterable[Widget], + *, before: int | str | Widget | None = None, after: int | str | Widget | None = None, ) -> AwaitMount: @@ -1120,8 +1125,12 @@ class App(Generic[ReturnType], DOMNode): Args: widgets: An iterable of widgets. - before: Optional location to mount before. - after: Optional location to mount after. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. Returns: An awaitable object that waits for widgets to be mounted. @@ -2103,7 +2112,7 @@ class App(Generic[ReturnType], DOMNode): """Remove nodes from DOM, and return an awaitable that awaits cleanup. Args: - widgets: List of nodes to remvoe. + widgets: List of nodes to remove. Returns: Awaitable that returns when the nodes have been fully removed. @@ -2127,17 +2136,19 @@ class App(Generic[ReturnType], DOMNode): removed_widgets = self._detach_from_dom(widgets) finished_event = asyncio.Event() - create_task( + remove_task = create_task( prune_widgets_task(removed_widgets, finished_event), name="prune nodes" ) - return AwaitRemove(finished_event) + await_remove = AwaitRemove(finished_event, remove_task) + self.call_next(await_remove) + return await_remove async def _prune_nodes(self, widgets: list[Widget]) -> None: """Remove nodes and children. Args: - widgets: _description_ + widgets: Widgets to remove. """ async with self._dom_lock: for widget in widgets: diff --git a/src/textual/await_remove.py b/src/textual/await_remove.py index cd794d8c6..f8d61e3ff 100644 --- a/src/textual/await_remove.py +++ b/src/textual/await_remove.py @@ -1,19 +1,24 @@ """Provides the type of an awaitable remove.""" -from asyncio import Event +from asyncio import Event, Task from typing import Generator class AwaitRemove: """An awaitable returned by App.remove and DOMQuery.remove.""" - def __init__(self, finished_flag: Event) -> None: + def __init__(self, finished_flag: Event, task: Task) -> None: """Initialise the instance of ``AwaitRemove``. Args: finished_flag: The asyncio event to wait on. + task: The task which does the remove (required to keep a reference). """ self.finished_flag = finished_flag + self._task = task + + async def __call__(self) -> None: + return await self def __await__(self) -> Generator[None, None, None]: async def await_prune() -> None: diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 10c8f9f8a..c06313e63 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -736,6 +736,9 @@ class StringEnumProperty: """ return obj.get_rule(self.name, self._default) + def _before_refresh(self, obj: StylesBase, value: str | None) -> None: + """Do any housekeeping before asking for a layout refresh after a value change.""" + def __set__(self, obj: StylesBase, value: str | None = None): """Set the string property and ensure it is in the set of allowed values. @@ -749,6 +752,7 @@ class StringEnumProperty: _rich_traceback_omit = True if value is None: if obj.clear_rule(self.name): + self._before_refresh(obj, value) obj.refresh(layout=self._layout, children=self._children) else: if value not in self._valid_values: @@ -761,9 +765,20 @@ class StringEnumProperty: ), ) if obj.set_rule(self.name, value): + self._before_refresh(obj, value) obj.refresh(layout=self._layout, children=self._children) +class OverflowProperty(StringEnumProperty): + """Descriptor for overflow styles that forces widgets to refresh scrollbars.""" + + def _before_refresh(self, obj: StylesBase, value: str | None) -> None: + from ..widget import Widget # Avoid circular import + + if isinstance(obj.node, Widget): + obj.node._refresh_scrollbars() + + class NameProperty: """Descriptor for getting and setting name properties.""" diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 4a3691bcc..cc9e99a6a 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -26,6 +26,7 @@ from ._style_properties import ( NameListProperty, NameProperty, OffsetProperty, + OverflowProperty, ScalarListProperty, ScalarProperty, SpacingProperty, @@ -246,12 +247,8 @@ class StylesBase(ABC): dock = DockProperty() - overflow_x = StringEnumProperty( - VALID_OVERFLOW, "hidden", layout=True, children=True - ) - overflow_y = StringEnumProperty( - VALID_OVERFLOW, "hidden", layout=True, children=True - ) + overflow_x = OverflowProperty(VALID_OVERFLOW, "hidden", layout=True, children=True) + overflow_y = OverflowProperty(VALID_OVERFLOW, "hidden", layout=True, children=True) layer = NameProperty() layers = NameListProperty() diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index f5f90ae0b..2d1b5e1a0 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -145,9 +145,14 @@ class LinuxDriver(Driver): self._request_terminal_sync_mode_support() self._enable_bracketed_paste() - def _request_terminal_sync_mode_support(self): - self.console.file.write("\033[?2026$p") - self.console.file.flush() + def _request_terminal_sync_mode_support(self) -> None: + """Writes an escape sequence to query the terminal support for the sync protocol.""" + # Terminals should ignore this sequence if not supported. + # Apple terminal doesn't, and writes a single 'p' in to the terminal, + # so we will make a special case for Apple terminal (which doesn't support sync anyway). + if self.console._environ.get("TERM_PROGRAM", "") != "Apple_Terminal": + self.console.file.write("\033[?2026$p") + self.console.file.flush() @classmethod def _patch_lflag(cls, attrs: int) -> int: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7cbaf2875..c469b381d 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -17,7 +17,7 @@ from weakref import WeakSet from . import Logger, events, log, messages from ._asyncio import create_task from ._callback import invoke -from ._context import NoActiveAppError, active_app +from ._context import NoActiveAppError, active_app, active_message_pump from ._time import time from .case import camel_to_snake from .errors import DuplicateKeyHandlers @@ -313,12 +313,18 @@ class MessagePump(metaclass=MessagePumpMeta): Reactive._reset_object(self) await self._message_queue.put(None) if wait and self._task is not None and asyncio.current_task() != self._task: - # Ensure everything is closed before returning - await self._task + try: + running_widget = active_message_pump.get() + except LookupError: + running_widget = None + + if running_widget is None or running_widget is not self: + await self._task def _start_messages(self) -> None: """Start messages task.""" if self.app._running: + active_message_pump.set(self) self._task = create_task( self._process_messages(), name=f"message pump {self}" ) @@ -357,7 +363,6 @@ class MessagePump(metaclass=MessagePumpMeta): async def _process_messages_loop(self) -> None: """Process messages until the queue is closed.""" _rich_traceback_guard = True - while not self._closed: try: message = await self._get_message() diff --git a/src/textual/widget.py b/src/textual/widget.py index e6c9c2626..ca34cd05b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1,7 +1,6 @@ from __future__ import annotations from asyncio import Lock, wait -from asyncio import Lock, create_task, wait from collections import Counter from fractions import Fraction from itertools import islice @@ -618,6 +617,37 @@ class Widget(DOMNode): self.call_next(await_mount) return await_mount + def mount_all( + self, + widgets: Iterable[Widget], + *, + before: int | str | Widget | None = None, + after: int | str | Widget | None = None, + ) -> AwaitMount: + """Mount widgets from an iterable. + + Args: + widgets: An iterable of widgets. + before: Optional location to mount before. An `int` is the index + of the child to mount before, a `str` is a `query_one` query to + find the widget to mount before. + after: Optional location to mount after. An `int` is the index + of the child to mount after, a `str` is a `query_one` query to + find the widget to mount after. + + Returns: + An awaitable object that waits for widgets to be mounted. + + Raises: + MountError: If there is a problem with the mount request. + + Note: + Only one of ``before`` or ``after`` can be provided. If both are + provided a ``MountError`` will be raised. + """ + await_mount = self.mount(*widgets, before=before, after=after) + return await_mount + def move_child( self, child: int | Widget, @@ -922,7 +952,7 @@ class Widget(DOMNode): show_horizontal = self.show_horizontal_scrollbar if overflow_x == "hidden": show_horizontal = False - if overflow_x == "scroll": + elif overflow_x == "scroll": show_horizontal = True elif overflow_x == "auto": show_horizontal = self.virtual_size.width > width @@ -1974,13 +2004,14 @@ class Widget(DOMNode): """ show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled - scrollbar_size_horizontal = self.styles.scrollbar_size_horizontal - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + styles = self.styles + scrollbar_size_horizontal = styles.scrollbar_size_horizontal + scrollbar_size_vertical = styles.scrollbar_size_vertical - if self.styles.scrollbar_gutter == "stable": + if styles.scrollbar_gutter == "stable": # Let's _always_ reserve some space, whether the scrollbar is actually displayed or not: show_vertical_scrollbar = True - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + scrollbar_size_vertical = styles.scrollbar_size_vertical if show_horizontal_scrollbar and show_vertical_scrollbar: (region, _, _, _) = region.split( diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 4628cbe92..295b8bf12 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -23,7 +23,6 @@ if typing.TYPE_CHECKING: from ._static import Static from ._text_log import TextLog from ._tree import Tree - from ._tree_node import TreeNode from ._welcome import Welcome from ..widget import Widget @@ -44,7 +43,6 @@ __all__ = [ "Static", "TextLog", "Tree", - "TreeNode", "Welcome", ] diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 497b7c671..1046c732e 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -256,6 +256,7 @@ class Input(Widget, can_focus=True): def on_paste(self, event: events.Paste) -> None: line = event.text.splitlines()[0] self.insert_text_at_cursor(line) + event.stop() def on_click(self, event: events.Click) -> None: offset = event.get_content_offset(self) diff --git a/src/textual/widgets/_tree_node.py b/src/textual/widgets/_tree_node.py deleted file mode 100644 index e6c57fb61..000000000 --- a/src/textual/widgets/_tree_node.py +++ /dev/null @@ -1 +0,0 @@ -from ._tree import TreeNode as TreeNode diff --git a/src/textual/widgets/data_table.py b/src/textual/widgets/data_table.py new file mode 100644 index 000000000..d0316f387 --- /dev/null +++ b/src/textual/widgets/data_table.py @@ -0,0 +1,5 @@ +"""Make non-widget DataTable support classes available.""" + +from ._data_table import Column, Row + +__all__ = ["Column", "Row"] diff --git a/src/textual/widgets/tree.py b/src/textual/widgets/tree.py new file mode 100644 index 000000000..2e315bc23 --- /dev/null +++ b/src/textual/widgets/tree.py @@ -0,0 +1,5 @@ +"""Make non-widget Tree support classes available.""" + +from ._tree import TreeNode + +__all__ = ["TreeNode"] diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py new file mode 100644 index 000000000..1ebd39765 --- /dev/null +++ b/tests/test_overflow_change.py @@ -0,0 +1,37 @@ +"""Regression test for #1616 https://github.com/Textualize/textual/issues/1616""" +import pytest + + +from textual.app import App +from textual.containers import Vertical + + +async def test_overflow_change_updates_virtual_size_appropriately(): + class MyApp(App): + def compose(self): + yield Vertical() + + app = MyApp() + + async with app.run_test() as pilot: + vertical = app.query_one(Vertical) + + height = vertical.virtual_size.height + + vertical.styles.overflow_x = "scroll" + await pilot.pause() # Let changes propagate. + assert vertical.virtual_size.height < height + + vertical.styles.overflow_x = "hidden" + await pilot.pause() + assert vertical.virtual_size.height == height + + width = vertical.virtual_size.width + + vertical.styles.overflow_y = "scroll" + await pilot.pause() + assert vertical.virtual_size.width < width + + vertical.styles.overflow_y = "hidden" + await pilot.pause() + assert vertical.virtual_size.width == width diff --git a/tests/test_widget.py b/tests/test_widget.py index e3c0a618c..a06cf7857 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -1,13 +1,12 @@ import pytest -import rich from textual._node_list import DuplicateIds from textual.app import App, ComposeResult from textual.css.errors import StyleValueError from textual.css.query import NoMatches -from textual.dom import DOMNode from textual.geometry import Size from textual.widget import Widget, MountError +from textual.widgets import Label @pytest.mark.parametrize( @@ -157,3 +156,28 @@ def test_widget_mount_ids_must_be_unique_mounting_multiple_calls(parent): parent.mount(widget1) with pytest.raises(DuplicateIds): parent.mount(widget2) + + +# Regression test for https://github.com/Textualize/textual/issues/1634 +async def test_remove(): + class RemoveMeLabel(Label): + async def on_mount(self) -> None: + await self.action("app.remove_all") + + class Container(Widget): + async def clear(self) -> None: + await self.query("*").remove() + + class RemoveApp(App): + def compose(self) -> ComposeResult: + yield Container(RemoveMeLabel()) + + async def action_remove_all(self) -> None: + await self.query_one(Container).clear() + self.exit(123) + + app = RemoveApp() + async with app.run_test() as pilot: + await pilot.press("r") + await pilot.pause() + assert app.return_value == 123 diff --git a/tests/tree/test_tree_node_children.py b/tests/tree/test_tree_node_children.py index eb5c949c0..d6c5c7e4e 100644 --- a/tests/tree/test_tree_node_children.py +++ b/tests/tree/test_tree_node_children.py @@ -1,5 +1,6 @@ import pytest -from textual.widgets import Tree, TreeNode +from textual.widgets import Tree +from textual.widgets.tree import TreeNode def label_of(node: TreeNode[None]): diff --git a/tests/tree/test_tree_node_label.py b/tests/tree/test_tree_node_label.py index e64fcf24d..7d7a04329 100644 --- a/tests/tree/test_tree_node_label.py +++ b/tests/tree/test_tree_node_label.py @@ -1,4 +1,5 @@ -from textual.widgets import Tree, TreeNode +from textual.widgets import Tree +from textual.widgets.tree import TreeNode from rich.text import Text diff --git a/tests/tree/test_tree_node_parent.py b/tests/tree/test_tree_node_parent.py index b9d85af43..87e66ebc4 100644 --- a/tests/tree/test_tree_node_parent.py +++ b/tests/tree/test_tree_node_parent.py @@ -1,4 +1,4 @@ -from textual.widgets import TreeNode, Tree +from textual.widgets import Tree def test_tree_node_parent() -> None: