diff --git a/docs/examples/guide/screens/modal02.py b/docs/examples/guide/screens/modal02.py index bb6c49c59..418030f80 100644 --- a/docs/examples/guide/screens/modal02.py +++ b/docs/examples/guide/screens/modal02.py @@ -42,6 +42,7 @@ class ModalApp(App): yield Footer() def action_request_quit(self) -> None: + """Action to display the quit dialog.""" self.push_screen(QuitScreen()) diff --git a/docs/examples/guide/screens/modal03.py b/docs/examples/guide/screens/modal03.py new file mode 100644 index 000000000..e19fc527b --- /dev/null +++ b/docs/examples/guide/screens/modal03.py @@ -0,0 +1,57 @@ +from textual.app import App, ComposeResult +from textual.containers import Grid +from textual.screen import ModalScreen +from textual.widgets import Button, Footer, Header, Label + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +I will permit it to pass over me and through me. +And when it has gone past, I will turn the inner eye to see its path. +Where the fear has gone there will be nothing. Only I will remain.""" + + +class QuitScreen(ModalScreen[bool]): # (1)! + """Screen with a dialog to quit.""" + + def compose(self) -> ComposeResult: + yield Grid( + Label("Are you sure you want to quit?", id="question"), + Button("Quit", variant="error", id="quit"), + Button("Cancel", variant="primary", id="cancel"), + id="dialog", + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "quit": + self.dismiss(True) + else: + self.dismiss(False) + + +class ModalApp(App): + """An app with a modal dialog.""" + + CSS_PATH = "modal01.css" + BINDINGS = [("q", "request_quit", "Quit")] + + def compose(self) -> ComposeResult: + yield Header() + yield Label(TEXT * 8) + yield Footer() + + def action_request_quit(self) -> None: + """Action to display the quit dialog.""" + + def check_quit(quit: bool) -> None: + """Called when QuitScreen is dismissed.""" + if quit: + self.exit() + + self.push_screen(QuitScreen(), check_quit) + + +if __name__ == "__main__": + app = ModalApp() + app.run() diff --git a/docs/guide/screens.md b/docs/guide/screens.md index fe8e3616f..0119ef655 100644 --- a/docs/guide/screens.md +++ b/docs/guide/screens.md @@ -219,3 +219,40 @@ Let's see what happens when we use `ModalScreen`. Now when we press ++q++, the dialog is displayed over the main screen. The main screen is darkened to indicate to the user that it is not active, and only the dialog will respond to input. + +## Returning data from screens + +It is a common requirement for screens to be able to return data. +For instance, you may want a screen to show a dialog and have the result of that dialog processed *after* the screen has been popped. + +To return data from a screen, call [`dismiss()`][textual.screen.dismiss] on the screen with the data you wish to return. +This will pop the screen and invoke a callback set when the screen was pushed (with [`push_screen`][textual.app.push_screen]). + +Let's modify the previous example to use `dismiss` rather than an explicit `pop_screen`. + +=== "modal03.py" + + ```python title="modal03.py" hl_lines="15 27-30 47-50 52" + --8<-- "docs/examples/guide/screens/modal03.py" + ``` + + 1. See below for an explanation of the `[bool]` + +=== "modal01.css" + + ```sass title="modal01.css" + --8<-- "docs/examples/guide/screens/modal01.css" + ``` + +In the `on_button_pressed` message handler we call `dismiss` with a boolean that indicates if the user has chosen to quit the app. +This boolean is passed to the `check_quit` function we provided when `QuitScreen` was pushed. + +Although this example behaves the same as the previous code, it is more flexible because it has removed responsibility for exiting from the modal screen to the caller. +This makes it easier for the app to perform any cleanup actions prior to exiting, for example. + +Returning data in this way can help keep your code manageable by making it easy to re-use your `Screen` classes in other contexts. + +### Typing screen results + +You may have noticed in the previous example that we changed the base class to `ModalScreen[bool]`. +The addition of `[bool]` adds typing information that tells the type checker to expect a boolean in the call to `dismiss`, and that any callback set in `push_screen` should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs. diff --git a/src/textual/app.py b/src/textual/app.py index c1376cd22..841d854e2 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1416,7 +1416,7 @@ class App(Generic[ReturnType], DOMNode): Args: screen: A Screen instance or the name of an installed screen. - callback: An optional callback function that is called if the screen is dismissed with a result. + callback: An optional callback function that will be called if the screen is [dismissed][textual.screen.Screen.dismiss] with a result. Returns: An optional awaitable that awaits the mounting of the screen and its children. diff --git a/src/textual/screen.py b/src/textual/screen.py index 09505cc53..535b621ea 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -768,6 +768,9 @@ class Screen(Generic[ScreenResultType], Widget): def dismiss(self, result: ScreenResultType | Type[_NoResult] = _NoResult) -> None: """Dismiss the screen, optionally with a result. + If `result` is provided and a callback was set when the screen was [pushed][textual.app.push_screen], then + the callback will be invoked with `result`. + Args: result: The optional result to be passed to the result callback. @@ -775,12 +778,6 @@ class Screen(Generic[ScreenResultType], Widget): ScreenStackError: If trying to dismiss a screen that is not at the top of the stack. - Note: - If the screen was pushed with a callback, the callback will be - called with the given result and then a call to - [`App.pop_screen`][textual.app.App.pop_screen] is performed. If - no callback was provided calling this method is the same as - simply calling [`App.pop_screen`][textual.app.App.pop_screen]. """ if self is not self.app.screen: from .app import ScreenStackError