ppizarror / pygame-menu

A menu for pygame. Simple, and easy to use
https://pygame-menu.readthedocs.io/
Other
555 stars 141 forks source link

Navigating back from a nested submenu forgets the widget selection in the parent menu #471

Closed ahmadaal closed 2 weeks ago

ahmadaal commented 1 year ago

Environment information Describe your environment information, such as:

Describe the bug When you have a nested menu structure that is more than one level deep, the parent menu "forgets" which widget was selected before you navigated into a submenu. This means that when you navigate back to the parent, the 0th widget is always selected. This creates a usability issue because the user's selection is not remembered.

To Reproduce Run the sample code here. This has 2 levels of nesting, and has some extra buttons so that the bug doesn't get hidden because the 0th button itself is the one that should be "remembered" :)

menu
-> sub
  -> sub2

To trigger the bug, press "Sub", and then press "Sub2". Then navigate back by either pressing the "Back" button, or F1. You will see that instead of the selection of the "Sub2" button being remembered, the selection has reset to the "Back" button. I would expect it not to reset like this.

Note that doing only one level of navigation does NOT trigger this bug. Meaning, if you simply press "Sub" and then go back to the root menu, the selection is remembered. I believe this because for the root menu only, self._top == self.

import pygame_menu
import pygame
import pdb

def show():
    pygame.init()
    screen = pygame.display.set_mode((400, 400))
    clock = pygame.time.Clock()
    pygame_menu.controls.KEY_BACK = pygame.K_F1

    menu = pygame_menu.Menu("test", 400, 400)  # the main menu
    sub = pygame_menu.Menu("sub", 400, 400)  # sub is nested under menu
    sub2 = pygame_menu.Menu("sub2", 400, 400)  # sub2 is nested under sub

    # Add "sub" as a link within "menu"
    sub_link = menu.add.menu_link(sub)
    sub.add.button("Back", pygame_menu.events.BACK)

    # Add "sub2" as a link within "sub"
    sub2_link = sub.add.menu_link(sub2)
    sub2.add.button("Back", pygame_menu.events.BACK)

    def opensub(menu=menu, sub=sub, sub2=sub2, sub_link=sub_link):
        print(
            f"BEFORE: current={menu.get_current().get_title()} link_menu={sub_link._menu.get_title()} menu: {menu._index}/{len(menu._widgets)}, sub={sub._index}/{len(sub._widgets)}, sub2={sub2._index}/{len(sub2._widgets)})"
        )

        # pdb.set_trace()
        sub_link.open()
        print(
            f"AFTER: current={menu.get_current().get_title()} link_menu={sub_link._menu.get_title()} menu: {menu._index}/{len(menu._widgets)}, sub={sub._index}/{len(sub._widgets)}, sub2={sub2._index}/{len(sub2._widgets)})"
        )

    def opensub2(menu=menu, sub=sub, sub2=sub2, sub2_link=sub2_link):
        print(
            f"BEFORE: current={menu.get_current().get_title()} link_menu={sub2_link._menu.get_title()} menu: {menu._index}/{len(menu._widgets)}, sub={sub._index}/{len(sub._widgets)}, sub2={sub2._index}/{len(sub2._widgets)})"
        )

        # pdb.set_trace()
        sub2_link.open()
        print(
            f"AFTER: current={menu.get_current().get_title()} link_menu={sub2_link._menu.get_title()} menu: {menu._index}/{len(menu._widgets)}, sub={sub._index}/{len(sub._widgets)}, sub2={sub2._index}/{len(sub2._widgets)})"
        )

    menu.add.button("No-op Button")

    # To see logging/breakpoints replace with:
    # menu.add.button("Sub", opensub)
    menu.add.button("Sub", sub_link.open)

    # To see logging/breakpoints replace with:
    # sub.add.button("Sub2", opensub2)
    sub.add.button("Sub2", sub2_link.open)

    running = True

    while running:
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT or (
                event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
            ):
                running = False

        screen.fill("black")
        menu.update(events)
        menu.draw(screen)
        pygame.display.flip()
        clock.tick(60)

    pygame.quit()

if __name__ == "__main__":
    show()

Expected behavior When I navigate into a submenu, and then navigate back out to to the parent, the parent should always remember which widget was selected before the navigation took place.

Additional context I suspect this bug may be because of this line in _open() in menu.py:3556: self._current._select(0, 1, SELECT_OPEN, False, update_mouse_position=False) I won't speculate too much about the purpose of this line of code since I'm not very familiar with the codebase, but if I hack this and change it to self._top._current._select(...) it does seem to fix the issue. I don't know if that's the correct fix though.

It seems that the lines right before attempt to change the current menu to be the one that is about to be opened by doing:

self._top._current = menu._current

but does the following _select() call on self._current (vs self._top._current or even menu._current), which is still the parent menu, thus selecting the 0th widget of the parent menu before opening the child.

And in the root case, self._top == self. Thus setting self._top._current actually sets self._current. Thus self._current IS actually the submenu (not the parent), and thus the submenu is the one who's widget index gets reset.

ahmadaal commented 1 year ago

@ppizarror , have any thoughts? :)

ahmadaal commented 1 year ago

I also wonder if I may just be doing something wrong with how I set up the menu hierarchy...

ppizarror commented 2 weeks ago

Hi, I Added a new version of 4.4.8that adds theremember_selection`` to the Menu constructor. I used the same philosophy as discussed here. Thanks, and I'm sorry for the delay in finding a solution.