revised screens api

This commit is contained in:
Will McGugan
2022-08-17 08:46:06 +01:00
parent efe0342a6f
commit 97a9619d59
6 changed files with 107 additions and 43 deletions

11
poetry.lock generated
View File

@@ -444,6 +444,14 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "nanoid"
version = "2.0.0"
description = "A tiny, secure, URL-friendly, unique string ID generator for Python"
category = "main"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.7.0" version = "1.7.0"
@@ -780,7 +788,7 @@ dev = ["aiohttp", "click", "msgpack"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "2d0f99d7fb563eb0b34cda9542ecf87c35cf5944a67510625969ec7b046b6d03" content-hash = "61db56567f708cd9ca1c27f0e4a4b4aa3dd808fc8411f80967a90995d7fdd8c8"
[metadata.files] [metadata.files]
aiohttp = [ aiohttp = [
@@ -1275,6 +1283,7 @@ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
] ]
nanoid = []
nodeenv = [ nodeenv = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"},

View File

@@ -31,6 +31,7 @@ typing-extensions = { version = "^4.0.0", python = "<3.8" }
aiohttp = { version = "^3.8.1", optional = true } aiohttp = { version = "^3.8.1", optional = true }
click = {version = "8.1.2", optional = true} click = {version = "8.1.2", optional = true}
msgpack = { version = "^1.0.3", optional = true } msgpack = { version = "^1.0.3", optional = true }
nanoid = "^2.0.0"
[tool.poetry.extras] [tool.poetry.extras]
dev = ["aiohttp", "click", "msgpack"] dev = ["aiohttp", "click", "msgpack"]

View File

@@ -17,7 +17,7 @@ class NewScreen(Screen):
yield Footer() yield Footer()
def on_screen_resume(self): def on_screen_resume(self):
self.query("*").refresh() self.query_one(Pretty).update(self.app.screen_stack)
class ScreenApp(App): class ScreenApp(App):
@@ -41,17 +41,16 @@ class ScreenApp(App):
} }
""" """
SCREENS = {
"1": NewScreen("screen 1"),
"2": NewScreen("screen 2"),
"3": NewScreen("screen 3"),
}
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Static("On Screen 1") yield Static("On Screen 1")
yield Footer() yield Footer()
def on_mount(self) -> None: def on_mount(self) -> None:
self.install_screen(NewScreen("Screen1"), name="1")
self.install_screen(NewScreen("Screen2"), name="2")
self.install_screen(NewScreen("Screen3"), name="3")
self.bind("1", "switch_screen('1')", description="Screen 1") self.bind("1", "switch_screen('1')", description="Screen 1")
self.bind("2", "switch_screen('2')", description="Screen 2") self.bind("2", "switch_screen('2')", description="Screen 2")
self.bind("3", "switch_screen('3')", description="Screen 3") self.bind("3", "switch_screen('3')", description="Screen 3")
@@ -63,4 +62,5 @@ class ScreenApp(App):
app = ScreenApp() app = ScreenApp()
app.run() if __name__ == "__main__":
app.run()

View File

@@ -21,7 +21,7 @@ from typing import (
TypeVar, TypeVar,
TYPE_CHECKING, TYPE_CHECKING,
) )
from weakref import WeakSet from weakref import WeakSet, WeakValueDictionary
from ._ansi_sequences import SYNC_START, SYNC_END from ._ansi_sequences import SYNC_START, SYNC_END
@@ -30,6 +30,7 @@ if sys.version_info >= (3, 8):
else: else:
from typing_extensions import Literal # pragma: no cover from typing_extensions import Literal # pragma: no cover
import nanoid
import rich import rich
import rich.repr import rich.repr
from rich.console import Console, RenderableType from rich.console import Console, RenderableType
@@ -113,7 +114,11 @@ class ActionError(Exception):
pass pass
class ScreenStackError(Exception): class ScreenError(Exception):
pass
class ScreenStackError(ScreenError):
pass pass
@@ -216,6 +221,10 @@ class App(Generic[ReturnType], DOMNode):
self._registry: WeakSet[DOMNode] = WeakSet() self._registry: WeakSet[DOMNode] = WeakSet()
self._installed_screens: WeakValueDictionary[
str, Screen
] = WeakValueDictionary()
self.devtools = DevtoolsClient() self.devtools = DevtoolsClient()
self._return_value: ReturnType | None = None self._return_value: ReturnType | None = None
@@ -614,7 +623,14 @@ class App(Generic[ReturnType], DOMNode):
for widget in widgets: for widget in widgets:
self._register(self.screen, widget) self._register(self.screen, widget)
def _get_screen(self, screen: Screen | str) -> Screen: def is_screen_installed(self, screen: Screen | str) -> bool:
"""Check if a given screen has been installed."""
if isinstance(screen, str):
return screen in self._installed_screens
else:
return screen in self._installed_screens.values()
def get_screen(self, screen: Screen | str) -> Screen:
"""Get a screen and ensure it is registered. """Get a screen and ensure it is registered.
Args: Args:
@@ -628,41 +644,30 @@ class App(Generic[ReturnType], DOMNode):
""" """
if isinstance(screen, str): if isinstance(screen, str):
try: try:
next_screen = self.SCREENS[screen] next_screen = self._installed_screens[screen]
except KeyError: except KeyError:
raise KeyError( raise KeyError("No screen called {screen!r} installed") from None
"No screen called {screen!r} found in {self.__class__}.SCREENS"
) from None
else: else:
next_screen = screen next_screen = screen
if not next_screen.is_running: if not next_screen.is_running:
self._register(self, next_screen) self._register(self, next_screen)
return next_screen return next_screen
def _replace_screen(self, screen: Screen, remove: bool | None = None) -> Screen: def _replace_screen(self, screen: Screen) -> Screen:
"""Handle the replaced screen. """Handle the replaced screen.
Args: Args:
screen (Screen): A screen object. screen (Screen): A screen object.
remove (bool | None): Remove replaced screen if True. Don't remove if False.
If None, remove screens not in self.SCREENS.
Returns: Returns:
Screen: The replaced screen Screen: The screen that was replaced.
""" """
screen.post_message_no_wait(events.ScreenSuspend(self)) screen.post_message_no_wait(events.ScreenSuspend(self))
if remove is None: self.log(f"{screen} SUSPENDED")
if screen not in self.SCREENS.values(): if not self.is_screen_installed(screen) and screen not in self._screen_stack:
screen.remove() screen.remove()
else: self.log(f"{screen} REMOVED")
screen.detach()
else:
if remove:
if screen in self.SCREENS.values():
raise ScreenStackError("Can't remove screen set in App.SCREENS")
screen.remove()
else:
screen.detach()
return screen return screen
def push_screen(self, screen: Screen | str) -> None: def push_screen(self, screen: Screen | str) -> None:
@@ -672,43 +677,91 @@ class App(Generic[ReturnType], DOMNode):
screen (Screen | str): A Screen instance or an id. screen (Screen | str): A Screen instance or an id.
""" """
next_screen = self._get_screen(screen) next_screen = self.get_screen(screen)
self._screen_stack.append(next_screen) self._screen_stack.append(next_screen)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message_no_wait(events.ScreenResume(self))
self.log(f"{self.screen} is current (PUSHED)")
def switch_screen(self, screen: Screen | str, remove: bool | None) -> Screen: def switch_screen(self, screen: Screen | str) -> Screen:
"""Switch to a another screen. """Switch to a another screen.
Args: Args:
screen (Screen | str): A screen instance or a named of a screen. screen (Screen | str): A screen instance or a named of a screen.
remove (bool | None): Remove replaced screen if True. Don't remove if False.
If None, remove screens not in self.SCREENS.
Returns: Returns:
Screen: The previous screen object. Screen: The previous screen object.
""" """
next_screen = self._get_screen(screen) previous_screen = self._replace_screen(self._screen_stack.pop())
previous_screen = self._replace_screen(self._screen_stack.pop(), remove=remove) next_screen = self.get_screen(screen)
self._screen_stack.append(next_screen) self._screen_stack.append(next_screen)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message_no_wait(events.ScreenResume(self))
self.log(f"{self.screen} is current (SWITCHED)")
return previous_screen return previous_screen
def pop_screen(self, remove: bool | None = None) -> Screen: def install_screen(self, screen: Screen, name: str | None = None) -> str:
"""Install a screen.
Args:
screen (Screen): Screen to install.
name (str | None, optional): Unique name of screen or None to auto-generate.
Defaults to None.
Raises:
ScreenError: If the screen can't be installed.
Returns:
str: The name of the screen
"""
if name is None:
name = nanoid.generate()
if name in self._installed_screens:
raise ScreenError(f"Can't install screen; {name!r} is already registered")
if screen in self._installed_screens.values():
raise ScreenError(
"Can't install screen; {screen!r} has already been installed"
)
self._installed_screens[name] = screen
self.get_screen(name) # Ensures screen is running
self.log(f"{screen} INSTALLED name={name!r}")
return name
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.
Args:
screen (Screen | str): The screen to uninstall or the name of a installed screen.
Returns:
str | None: The name of the screen that was uninstalled, or None if no screen was uninstalled.
"""
if isinstance(screen, str):
uninstalled_screen = self._installed_screens.pop(screen)
self.log(f"{uninstalled_screen} UNINSTALLED name={screen!r}")
return screen
else:
for name, installed_screen in self._installed_screens.items():
if installed_screen is screen:
self._installed_screens.pop(name)
self.log(f"{screen} UNINSTALLED name={name!r}")
return name
return None
def pop_screen(self) -> Screen:
"""Pop the current screen from the stack, and switch to the previous screen. """Pop the current screen from the stack, and switch to the previous screen.
Returns: Returns:
Screen: The screen that was replaced. Screen: The screen that was replaced.
remove (bool | None): Remove replaced screen if True. Don't remove if False.
If None, remove screens not in self.SCREENS.
""" """
screen_stack = self._screen_stack screen_stack = self._screen_stack
if len(screen_stack) <= 1: if len(screen_stack) <= 1:
raise ScreenStackError( raise ScreenStackError(
"Can't pop screen; there must be at least one screen on the stack" "Can't pop screen; there must be at least one screen on the stack"
) )
previous_screen = self._replace_screen(screen_stack.pop(), remove) previous_screen = self._replace_screen(screen_stack.pop())
self.screen._screen_resized(self.size) self.screen._screen_resized(self.size)
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message_no_wait(events.ScreenResume(self))
self.log(f"{self.screen} is active")
return previous_screen return previous_screen
def set_focus(self, widget: Widget | None) -> None: def set_focus(self, widget: Widget | None) -> None:

View File

@@ -232,6 +232,7 @@ class Screen(Widget):
def _on_screen_resume(self) -> None: def _on_screen_resume(self) -> None:
"""Called by the App""" """Called by the App"""
size = self.app.size size = self.app.size
self._refresh_layout(size, full=True) self._refresh_layout(size, full=True)

View File

@@ -33,4 +33,4 @@ class Pretty(Widget):
def update(self, object: Any) -> None: def update(self, object: Any) -> None:
self._renderable = PrettyRenderable(object) self._renderable = PrettyRenderable(object)
self.refresh() self.refresh(layout=True)