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

pop_until_active does not pop screens #5166

Open
jakubziebin opened this issue Oct 24, 2024 · 4 comments
Open

pop_until_active does not pop screens #5166

jakubziebin opened this issue Oct 24, 2024 · 4 comments

Comments

@jakubziebin
Copy link

jakubziebin commented Oct 24, 2024

Hello, consider the following MRE:

from __future__ import annotations

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Footer, Label


class FirstScreen(Screen):
    BINDINGS = [
        Binding("n", "second_screen", "Second screen"),
    ]

    def compose(self) -> ComposeResult:
        yield Label("First screen. Press 'n' to go to the second screen.")
        yield Footer()

    def action_second_screen(self) -> None:
        self.app.push_screen(SecondScreen())


class SecondScreen(Screen):
    BINDINGS = [
        Binding("n", "third_screen", "Third screen"),
    ]

    def compose(self) -> ComposeResult:
        yield Label("Second screen. Press 'n' to go to the third screen.")
        yield Footer()

    async def action_third_screen(self) -> None:
        # going back to first screen and then replacing it with the third screen
        # self.app.get_screen works only with "installed" screens
        self.app.screen_stack[-1].pop_until_active()
        await self.app.switch_screen(ThirdScreen())


class ThirdScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Label("Third screen.")
        yield Label(f"The screen stack looks like: {self.app.screen_stack}")
        yield Label("but should be: [Screen(id='_default'), ThirdScreen()]")


class MyApp(App):
    def on_mount(self) -> None:
        self.push_screen(FirstScreen())


MyApp().run()

version: 0.83.0

The problem is observed in situations where the next line must have the screen stack updated using the pop_until_active method.
In general, we are looking for a simple way to clear the screen stack. As it is read-only, we do not want to modify any protected attributes. It would be great to have a method to do this, it would be very helpful in applications with an onboarding process for example.

Copy link

We found the following entry in the FAQ which you may find helpful:

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by FAQtory

@jakubziebin jakubziebin changed the title problem with pop_until_active() method pop_until_active does not drop off screens Oct 24, 2024
@jakubziebin jakubziebin changed the title pop_until_active does not drop off screens pop_until_active does not pop screens Oct 24, 2024
@willmcgugan
Copy link
Collaborator

The last item in the screen stack is always the active screen. self.app.screen_stack[-1].pop_until_active() doesn't make much sense, since screen_stack[-1] already the active screen.

I'd be surprised if you need pop_until_active at all. I suspect you could use modes

@jakubziebin
Copy link
Author

jakubziebin commented Oct 25, 2024

My mistake. In MRE, of course, it's about -2. In the example below I used instance lookup instead. After the change - incorrect behavior is still observed so the issue is justified.

I guess I can also use modes, however, it seems too complicated in my case to have multiple screen stacks and I feel like pop_unitl_active method should just work fine (if this bug wasn't there).

With modes, I still need to be able to manage the screen stack and dump a few screens or restore the stack to its initial state.
The second part seems to be achievable by dynamically removing and adding a new mode like (however pop_until_active still finds a use-case in modes):

self.app.remove_mode("some_mode")
self.app.add_mode("some_mode", SomeScreen)

However I would like it to be possible to manage screens in a better way. In the current form it seems to be insufficient for applications with multiple screens as it is quite limited. I believe that the mentioned public method should simply work as expected, and there could be additional screen management options apart from it. The above-mentioned stack clearing is a common situation even with modes, and the multiple screen pops to the expected screen offered by pop_until_active is also a non-imaginary use-case.

from __future__ import annotations

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Footer, Label


class FirstScreen(Screen):
    BINDINGS = [
        Binding("n", "second_screen", "Second screen"),
    ]

    def compose(self) -> ComposeResult:
        yield Label("First screen. Press 'n' to go to the second screen.")
        yield Footer()

    def action_second_screen(self) -> None:
        self.app.push_screen(SecondScreen())


class SecondScreen(Screen):
    BINDINGS = [
        Binding("n", "third_screen", "Third screen"),
    ]

    def compose(self) -> ComposeResult:
        yield Label("Second screen. Press 'n' to go to the third screen.")
        yield Footer()

    async def action_third_screen(self) -> None:
        """Going back to FirstScreen and then replacing it with the ThirdScreen"""
        for screen in self.app.screen_stack:
            if isinstance(screen, FirstScreen):
                # self.app.get_screen works only with "installed" screens
                # so we need to refer by index or do a loop like this
                screen.pop_until_active()
        await self.app.switch_screen(ThirdScreen())


class ThirdScreen(Screen):
    def compose(self) -> ComposeResult:
        yield Label("Third screen.")
        yield Label(f"The screen stack looks like: {self.app.screen_stack}")
        yield Label("but should be: [Screen(id='_default'), ThirdScreen()]")


class MyApp(App):
    def on_mount(self) -> None:
        self.push_screen(FirstScreen())


MyApp().run()

@willmcgugan
Copy link
Collaborator

When you call pop_until_active it doesn't occur immediately. It runs after the message handler exists, to avoid blocking the message loop. If you use call_later with your ThirdScreen self.call_later(self.app.switch_screen, ThirdScreen()), it should work as you expect.

I would really like to understand why you are working with screens in this way. Without understanding what you are trying to achieve, I can't help you with a potentially better solution or confidently make changes to the API.

For instance:

With modes, I still need to be able to manage the screen stack and dump a few screens or restore the stack to its initial state.

Why? What is in those screens? You seem to be insisting you need certain functionality that I have never needed, and nobody other than yourself and @mzebrak has requested. This suggests that A) you may have a fundamental misunderstanding regarding screens and modes, or B) I don't comprehend your use case. At this point, I don't know which is the case. Until I do, I can't make any changes to the API.

This is essentially an XY Problem.

So please, describe your app. What is in the screens that you are pushing? Why do you want to discard a bunch of them at once?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants