Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implements screens view #1778

Merged
merged 18 commits into from
Feb 14, 2023
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally
- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
- Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437
- Added DOMNode.watch and DOMNode.is_attached methods https://github.com/Textualize/textual/pull/1750
- Added `DOMNode.watch` and `DOMNode.is_attached` methods https://github.com/Textualize/textual/pull/1750
- Added `DOMNode.css_tree` which is a renderable that shows the DOM and CSS https://github.com/Textualize/textual/pull/1778
- Added `DOMNode.children_view` which is a view on to a nodes children list, use for querying https://github.com/Textualize/textual/pull/1778

### 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
- `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471
- Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746
- `App.install_screen` name is no longer optional https://github.com/Textualize/textual/pull/1778
- `App.query` now only includes the current screen https://github.com/Textualize/textual/pull/1778
- `DOMNode.tree` now displays simple DOM structure only https://github.com/Textualize/textual/pull/1778
- `App.install_screen` now returns None rather than AwaitMount https://github.com/Textualize/textual/pull/1778
- `DOMNode.children` is now a simple sequence, the NodesList is exposed as `DOMNode._nodes` https://github.com/Textualize/textual/pull/1778

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion docs/widgets/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The example below creates a simple tree.
--8<-- "docs/examples/widgets/tree.py"
```

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.
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
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ typing-extensions = "^4.0.0"
aiohttp = { version = ">=3.8.1", optional = true }
click = {version = ">=8.1.2", optional = true}
msgpack = { version = ">=1.0.3", optional = true }
nanoid = ">=2.0.0"
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved
mkdocs-exclude = "^1.0.2"

[tool.poetry.extras]
Expand Down
4 changes: 2 additions & 2 deletions src/textual/_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def get_content_width(self, widget: Widget, container: Size, viewport: Size) ->
Returns:
Width of the content.
"""
if not widget.children:
if not widget._nodes:
width = 0
else:
# Use a size of 0, 0 to ignore relative sizes, since those are flexible anyway
Expand Down Expand Up @@ -85,7 +85,7 @@ def get_content_height(
Returns:
Content height (in lines).
"""
if not widget.children:
if not widget._nodes:
height = 0
else:
# Use a height of zero to ignore relative heights
Expand Down
56 changes: 33 additions & 23 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
Generic,
Iterable,
List,
Sequence,
Type,
TypeVar,
Union,
cast,
overload,
)
from weakref import WeakSet, WeakValueDictionary
from weakref import WeakSet

import nanoid
import rich
import rich.repr
from rich.console import Console, RenderableType
Expand Down Expand Up @@ -385,9 +385,7 @@ def __init__(
self.scroll_sensitivity_y: float = 2.0
"""Number of lines to scroll in the Y direction with wheel or trackpad."""

self._installed_screens: WeakValueDictionary[
str, Screen | Callable[[], Screen]
] = WeakValueDictionary()
self._installed_screens: dict[str, Screen | Callable[[], Screen]] = {}
self._installed_screens.update(**self.SCREENS)

self.devtools: DevtoolsClient | None = None
Expand Down Expand Up @@ -420,6 +418,14 @@ def return_value(self) -> ReturnType | None:
"""ReturnType | None: The return type of the app."""
return self._return_value

@property
def children(self) -> Sequence["Widget"]:
"""A view on to the children which contains just the screen."""
try:
return (self.screen,)
except ScreenError:
return ()

def animate(
self,
attribute: str,
Expand Down Expand Up @@ -1265,38 +1271,41 @@ def switch_screen(self, screen: Screen | str) -> AwaitMount:
return await_mount
return AwaitMount(self.screen, [])

def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount:
def install_screen(self, screen: Screen, name: str) -> None:
"""Install a screen.

Installing a screen prevents Textual from destroying it when it is no longer on the screen stack.
Note that you don't need to install a screen to use it. See [push_screen][textual.app.App.push_screen]
or [switch_screen][textual.app.App.switch_screen] to make a new screen current.

Args:
screen: Screen to install.
name: Unique name of screen or None to auto-generate.
Defaults to None.
name: Unique name to identify the screen.

Raises:
ScreenError: If the screen can't be installed.

Returns:
An awaitable that awaits the mounting of the screen and its children.
"""
if name is None:
name = nanoid.generate()
if name in self._installed_screens:
raise ScreenError(f"Can't install screen; {name!r} is already installed")
if screen in self._installed_screens.values():
raise ScreenError(
"Can't install screen; {screen!r} has already been installed"
)
self._installed_screens[name] = screen
_screen, await_mount = self._get_screen(name) # Ensures screen is running
self.log.system(f"{screen} INSTALLED name={name!r}")
return await_mount

def uninstall_screen(self, screen: Screen | str) -> str | None:
"""Uninstall a screen. If the screen was not previously installed then this
method is a null-op.
"""Uninstall a screen.

If the screen was not previously installed then this method is a null-op.
Uninstalling a screen allows Textual to delete it when it is popped or switched.
Note that uninstalling a screen is only required if you have previously installed it
with [install_screen][textual.app.App.install_screen].
Textual will also uninstall screens automatically on exit.


Args:
screen: The screen to uninstall or the name of a installed screen.
Expand Down Expand Up @@ -1641,19 +1650,19 @@ def _register_child(
# Now to figure out where to place it. If we've got a `before`...
if before is not None:
# ...it's safe to NodeList._insert before that location.
parent.children._insert(before, child)
parent._nodes._insert(before, child)
elif after is not None and after != -1:
# In this case we've got an after. -1 holds the special
# position (for now) of meaning "okay really what I mean is
# do an append, like if I'd asked to add with no before or
# after". So... we insert before the next item in the node
# list, iff after isn't -1.
parent.children._insert(after + 1, child)
parent._nodes._insert(after + 1, child)
else:
# At this point we appear to not be adding before or after,
# or we've got a before/after value that really means
# "please append". So...
parent.children._append(child)
parent._nodes._append(child)

# Now that the widget is in the NodeList of its parent, sort out
# the rest of the admin.
Expand Down Expand Up @@ -1698,8 +1707,8 @@ def _register(
raise AppError(f"Can't register {widget!r}; expected a Widget instance")
if widget not in self._registry:
self._register_child(parent, widget, before, after)
if widget.children:
self._register(widget, *widget.children)
if widget._nodes:
self._register(widget, *widget._nodes)
apply_stylesheet(widget)

if not self._running:
Expand All @@ -1716,7 +1725,7 @@ def _unregister(self, widget: Widget) -> None:
"""
widget.reset_focus()
if isinstance(widget._parent, Widget):
widget._parent.children._remove(widget)
widget._parent._nodes._remove(widget)
widget._detach()
self._registry.discard(widget)

Expand Down Expand Up @@ -1894,6 +1903,7 @@ async def on_event(self, event: events.Event) -> None:
# Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Compose):
self.log(event)
screen = Screen(id="_default")
self._register(self, screen)
self._screen_stack.append(screen)
Expand Down Expand Up @@ -2098,7 +2108,7 @@ def _detach_from_dom(self, widgets: list[Widget]) -> list[Widget]:
# snipping each affected branch from the DOM.
for widget in pruned_remove:
if widget.parent is not None:
widget.parent.children._remove(widget)
widget.parent._nodes._remove(widget)

# Return the list of widgets that should end up being sent off in a
# prune event.
Expand All @@ -2117,10 +2127,10 @@ def _walk_children(self, root: Widget) -> Iterable[list[Widget]]:

while stack:
widget = pop()
children = [*widget.children, *widget._get_virtual_dom()]
children = [*widget._nodes, *widget._get_virtual_dom()]
if children:
yield children
for child in widget.children:
for child in widget._nodes:
push(child)

def _remove_nodes(self, widgets: list[Widget]) -> AwaitRemove:
Expand Down
39 changes: 31 additions & 8 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
ClassVar,
Iterable,
Iterator,
Sequence,
Type,
TypeVar,
cast,
Expand Down Expand Up @@ -132,7 +133,7 @@ def __init__(
check_identifiers("class name", *_classes)
self._classes.update(_classes)

self.children: NodeList = NodeList()
self._nodes: NodeList = NodeList()
self._css_styles: Styles = Styles(self)
self._inline_styles: Styles = Styles(self)
self.styles: RenderStyles = RenderStyles(
Expand All @@ -150,6 +151,11 @@ def __init__(

super().__init__()

@property
def children(self) -> Sequence["Widget"]:
"""A view on to the children."""
return self._nodes

@property
def auto_refresh(self) -> float | None:
return self._auto_refresh
Expand Down Expand Up @@ -484,7 +490,7 @@ def visible(self) -> bool:
return self.styles.visibility != "hidden"

@visible.setter
def visible(self, new_value: bool) -> None:
def visible(self, new_value: bool | str) -> None:
if isinstance(new_value, bool):
self.styles.visibility = "visible" if new_value else "hidden"
elif new_value in VALID_VISIBILITY:
Expand All @@ -497,10 +503,27 @@ def visible(self, new_value: bool) -> None:

@property
def tree(self) -> Tree:
"""Get a Rich tree object which will recursively render the structure of the node tree.
"""Get a Rich tree object which will recursively render the structure of the node tree."""

Returns:
A Rich object which may be printed.
def render_info(node: DOMNode) -> Pretty:
return Pretty(node)

tree = Tree(render_info(self))

def add_children(tree, node):
for child in node.children:
info = render_info(child)
branch = tree.add(info)
if tree.children:
add_children(branch, child)

add_children(tree, self)
return tree

@property
def css_tree(self) -> Tree:
"""Get a Rich tree object which will recursively render the structure of the node tree,
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved
which also displays CSS and size information.
"""
from rich.columns import Columns
from rich.console import Group
Expand Down Expand Up @@ -648,7 +671,7 @@ def displayed_children(self) -> list[Widget]:
Children of this widget which will be displayed.

"""
return [child for child in self.children if child.display]
return [child for child in self._nodes if child.display]

def watch(
self,
Expand Down Expand Up @@ -691,7 +714,7 @@ def _add_child(self, node: Widget) -> None:
Args:
node: A DOM node.
"""
self.children._append(node)
self._nodes._append(node)
node._attach(self)

def _add_children(self, *nodes: Widget) -> None:
Expand All @@ -700,7 +723,7 @@ def _add_children(self, *nodes: Widget) -> None:
Args:
*nodes: Positional args should be new DOM nodes.
"""
_append = self.children._append
_append = self._nodes._append
for node in nodes:
node._attach(self)
_append(node)
Expand Down
3 changes: 1 addition & 2 deletions src/textual/drivers/linux_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,5 +236,4 @@ def more_data() -> bool:
except Exception as error:
log(error)
finally:
with timer("selector.close"):
selector.close()
selector.close()
2 changes: 1 addition & 1 deletion src/textual/walk.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def walk_depth_first(
"""
from textual.dom import DOMNode

stack: list[Iterator[DOMNode]] = [iter(root.children)]
stack: list[Iterator[DOMNode]] = [iter(root._nodes)]
pop = stack.pop
push = stack.append
check_type = filter_type or DOMNode
Expand Down
Loading