mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
revised screens api
This commit is contained in:
11
poetry.lock
generated
11
poetry.lock
generated
@@ -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"},
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
@@ -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():
|
||||
self.log(f"{screen} SUSPENDED")
|
||||
if not self.is_screen_installed(screen) and screen not in self._screen_stack:
|
||||
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} 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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -33,4 +33,4 @@ class Pretty(Widget):
|
||||
|
||||
def update(self, object: Any) -> None:
|
||||
self._renderable = PrettyRenderable(object)
|
||||
self.refresh()
|
||||
self.refresh(layout=True)
|
||||
|
||||
Reference in New Issue
Block a user