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
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]]
name = "nodeenv"
version = "1.7.0"
@@ -780,7 +788,7 @@ dev = ["aiohttp", "click", "msgpack"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "2d0f99d7fb563eb0b34cda9542ecf87c35cf5944a67510625969ec7b046b6d03"
content-hash = "61db56567f708cd9ca1c27f0e4a4b4aa3dd808fc8411f80967a90995d7fdd8c8"
[metadata.files]
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.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
nanoid = []
nodeenv = [
{file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"},
{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 }
click = {version = "8.1.2", optional = true}
msgpack = { version = "^1.0.3", optional = true }
nanoid = "^2.0.0"
[tool.poetry.extras]
dev = ["aiohttp", "click", "msgpack"]

View File

@@ -17,7 +17,7 @@ class NewScreen(Screen):
yield Footer()
def on_screen_resume(self):
self.query("*").refresh()
self.query_one(Pretty).update(self.app.screen_stack)
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:
yield Static("On Screen 1")
yield Footer()
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("2", "switch_screen('2')", description="Screen 2")
self.bind("3", "switch_screen('3')", description="Screen 3")
@@ -63,4 +62,5 @@ class ScreenApp(App):
app = ScreenApp()
app.run()
if __name__ == "__main__":
app.run()

View File

@@ -21,7 +21,7 @@ from typing import (
TypeVar,
TYPE_CHECKING,
)
from weakref import WeakSet
from weakref import WeakSet, WeakValueDictionary
from ._ansi_sequences import SYNC_START, SYNC_END
@@ -30,6 +30,7 @@ if sys.version_info >= (3, 8):
else:
from typing_extensions import Literal # pragma: no cover
import nanoid
import rich
import rich.repr
from rich.console import Console, RenderableType
@@ -113,7 +114,11 @@ class ActionError(Exception):
pass
class ScreenStackError(Exception):
class ScreenError(Exception):
pass
class ScreenStackError(ScreenError):
pass
@@ -216,6 +221,10 @@ class App(Generic[ReturnType], DOMNode):
self._registry: WeakSet[DOMNode] = WeakSet()
self._installed_screens: WeakValueDictionary[
str, Screen
] = WeakValueDictionary()
self.devtools = DevtoolsClient()
self._return_value: ReturnType | None = None
@@ -614,7 +623,14 @@ class App(Generic[ReturnType], DOMNode):
for widget in widgets:
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.
Args:
@@ -628,41 +644,30 @@ class App(Generic[ReturnType], DOMNode):
"""
if isinstance(screen, str):
try:
next_screen = self.SCREENS[screen]
next_screen = self._installed_screens[screen]
except KeyError:
raise KeyError(
"No screen called {screen!r} found in {self.__class__}.SCREENS"
) from None
raise KeyError("No screen called {screen!r} installed") from None
else:
next_screen = screen
if not next_screen.is_running:
self._register(self, 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.
Args:
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:
Screen: The replaced screen
Screen: The screen that was replaced.
"""
screen.post_message_no_wait(events.ScreenSuspend(self))
if remove is None:
if screen not in self.SCREENS.values():
screen.remove()
else:
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()
self.log(f"{screen} SUSPENDED")
if not self.is_screen_installed(screen) and screen not in self._screen_stack:
screen.remove()
self.log(f"{screen} REMOVED")
return screen
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.
"""
next_screen = self._get_screen(screen)
next_screen = self.get_screen(screen)
self._screen_stack.append(next_screen)
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.
Args:
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:
Screen: The previous screen object.
"""
next_screen = self._get_screen(screen)
previous_screen = self._replace_screen(self._screen_stack.pop(), remove=remove)
previous_screen = self._replace_screen(self._screen_stack.pop())
next_screen = self.get_screen(screen)
self._screen_stack.append(next_screen)
self.screen.post_message_no_wait(events.ScreenResume(self))
self.log(f"{self.screen} is current (SWITCHED)")
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.
Returns:
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
if len(screen_stack) <= 1:
raise ScreenStackError(
"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.post_message_no_wait(events.ScreenResume(self))
self.log(f"{self.screen} is active")
return previous_screen
def set_focus(self, widget: Widget | None) -> None:

View File

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

View File

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