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

Provide additional methods to manage the screen stack #3127

Closed
mzebrak opened this issue Aug 21, 2023 · 5 comments
Closed

Provide additional methods to manage the screen stack #3127

mzebrak opened this issue Aug 21, 2023 · 5 comments

Comments

@mzebrak
Copy link

mzebrak commented Aug 21, 2023

Related: #3041 (comment), #3126, #1792

Currently, textual has too little ability to manage the stack of screens. - switch_screen, pop_screen and push_screen.

In the case of implementing forms, it is required, for example, to dump several screens at once, so calling pop_screen in the loop makes sense but old screens are visible for a moment because there is no method to dump several screens at one (like pop_screen_until).

Here are some methods I had to implement using the currently available interface or private parts from textual (which I'm not happy with and that's why I'm posting this issue). It is possible that more methods of this type would be useful, so far the following ones have been noticed, but I think that push_screen_at_index would allow you to implement your own methods easily.

1. pop_screen_until

def pop_screen_until(self, screen: str | type[Screen[ScreenResultType]]) -> None:
    ...

Pop all screens until the given screen is on top of the stack.

E.g While many screens are on the stack but we want to return to the dashboard one (notice there is DashboardBase and not DashbaordActive or DashboardInactive):

self.app.pop_screen_until(DashboardBase)

2. replace_screen

def replace_screen(self, old: str | type[Screen[ScreenResultType]], new: str | Screen[ScreenResultType]) -> None:
    ...

which looks for a certain screen on the screen stack and replaces it with the given one.

Usage example: self.app.replace_screen(DashboardInactive, "dashboard_active")

dashboard_active is a registered screen via the SCREENS classvar, and we want to look for any DashboardInactive instance on the screen stack to replace it with the active version.

3. push_screen_at

def push_screen_at(
    self,
    index: int,
    screen: str | Screen[ScreenResultType],
    callback: ScreenResultCallbackType[ScreenResultType] | None = None,
) -> None:
    ...

which will allow for pushing a screen to the screens stack at a certain position.

Actually this looks like a workaround for with self.batch_update() not working on pop_screen/ push_screen because
we call it this way:

self.app.switch_screen(TransactionSummary())
self.app.push_screen_at(-1, Cart())

and this should be done via:

with self.batch_update()
    self.pop_screen()
    self.push_screen(Cart())
    self.push_screen(TransactionSummary())

But I think such a method can be handy and will developers to more easily create their own methods to manage the stack of screens.

@github-actions
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

@davep
Copy link
Contributor

davep commented Aug 26, 2023

While it isn't fully-documented yet, so is hard to discover, I'm wondering if modes will help you here.

@willmcgugan
Copy link
Collaborator

I think we can address the issue of multiple pops causing flicker, but these methods break the concept of a stack. If you feel you need these, then it might be an indication you need the modes functionality that Dave linked to. They have yet to be fully documented.

@github-actions
Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

@mzebrak
Copy link
Author

mzebrak commented Aug 29, 2023

but these methods break the concept of a stack

pop_screen_until in my opinion, doesn't break anything, it's just multiple calls to pop_screen and a check if actual screen is the expected one, - I don't agree that the idea of the stack is broken

I think we can address the issue of multiple pops causing flicker, but these methods break the concept of a stack. If you feel you need these, then it might be an indication you need the modes functionality that Dave linked to. They have yet to be fully documented.

Unfortunately, I don't see how the modes are supposed to help me with this. A simple situation - you have a Dashboard screen and several other screens - on one of them you enter your password and log in, when leaving these screens you want the dashboard to take into account that you are logged in. You are in the same application mode all the time - you only need to update the dashboard, so you need a method like replace_screen which I mentioned.

I can't imagine it any other way - because what should I create 2 separate modes for the application after logging in and not logging in? it's best to be able to distinguish it at the level of a single screen, which is prevented by the lack of dynamic screens, but keeping the dashbaord screen for the unlogged in a different MODE than the logged-in only complicates the matter - at some point you would have to check if the next screen we go to is the dashboard and if we are logged in, change the mode[1], this is unacceptable to me and the modes don't seem to solve the problem.

Seems like modes are only for memorizing screen stack state, but correct me if I am wrong. Maybe @rodrigogiraoserrao can see a solution here?


Let's consider an online shop like app and the screen stack might look like:

  • Summary
  • Cart
  • Search
  • DashboardInactive

Then to proceed on Summary you have to be logged so the Application asks you to provide a password, and some actions happen like:

  1. log you in
  2. submit your order
  3. replace the DashboardInactive with DashboardActive (a.k.a call to replace_screen("DashboardInactive", DashboardActive())
  4. pop all screens back to Dashboard (a.k.a. call pop_screen_until("DashboardBase")

actually we handle the 3. in every place of the app just via the following hook, while with modes you cant do this since switching a mode changes the current screen also, and what if we want to login without immidietally switching to dashboard, now it works, and with modes it would require additional logic [1]:

    @on(Activate.Succeeded)
    def activate_succeeded(self) -> None:
        self.app.replace_screen("DashboardInactive", "dashboard_active")

and now how do you handle it with modes?

  1. log you in
  2. submit the order
  3. switch mode to active
  4. clean up the "inactive mode"

in this situation, it works, but you have to keep in mind, that the "inactive mode" screen stack has additional screens that you probably won't ever need (another call for clearing/rebuilding the inactive mode so it contains just the DashboardInactive is needed)

Now let's add to this information that at any time you may receive a notification about the application going into inactive mode - because for security reasons, activation is handled by a separate service that handles temporary activation.

A very simple thing in our current implementation - you react to this notification in such a way that you immediately replace the dashbaord screen, and the modes? - What if you are on the screen where you spent 10 minutes to fill in all the fields, you would need somehow store this information and change the mode only when popping back to dashboard [1].

In general, I think that modes in this situation complicate something that is very simple.

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

3 participants