peterbrittain / asciimatics

A cross platform package to do curses-like operations, plus higher level APIs and widgets to create text UIs and ASCII art animations
Apache License 2.0
3.61k stars 238 forks source link

Load another form after initialization #361

Closed kudrin-alex closed 1 year ago

kudrin-alex commented 1 year ago

Tell me what's the problem. It is necessary to change its content after form initialization. I do it with super().init() Everything is drawn correctly, but when you press Tab or Down, an error occurs:

File "test1.py", line 168, in start_scene Screen.wrapper(demo, catch_interrupt=False, arguments=[last_scene]) File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\screen.py", line 1433, in wrapper return func(screen, *arguments) File "test1.py", line 161, in demo screen.play(scenes, stop_on_resize=True, start_scene=scene, allow_int=True) File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\screen.py", line 1606, in play self.draw_next_frame(repeat=repeat) File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\screen.py", line 1694, in draw_next_frame event = scene.process_event(event) File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\scene.py", line 105, in process_event event = effect.process_event(event) File "test1.py", line 141, in process_event return super(NodeView, self).process_event(event) File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\widgets\frame.py", line 665, in process_event self._layouts[self._focus].focus() File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\widgets\layout.py", line 159, in focus raise IndexError("No live widgets") IndexError: No live widgets

Here is the code where the error occurs

class NodeView(Frame):
    def __init__(self, screen):
        global time_now, current_id
        self._last_frame = 0
        super(NodeView, self).__init__(screen,
                                       screen.height,
                                       screen.width,
                                       hover_focus=False,
                                       on_load=self._reload_prog,
                                       can_scroll=False,
                                       title="Selected")
        self._ncb = CheckBox("check1",
                                    label="Label check1",
                                    name="ncb",
                                    on_change=self._on_change)
        self._ncb._value = False
        layout = Layout([1, 20, 1])
        self.add_layout(layout)
        layout.add_widget(self._ncb, 1)
        layout.add_widget(Divider(height=2), 1)
        self._nowa =  Text(label="Readonly:", name="RO", readonly=True)
        layout.add_widget(Label(f"Time now: {self._nowa.value}"), 1)
        layout.add_widget(self._nowa, 1)
        self._nowa.value = str(time_now)
        self.fix()

    def _on_change(self):
        global time_now
        self._nowa.value = str(time_now)

    def _reload_prog(self, new_value=None):
        global current_id, time_now
        self._last_frame = 0
        super(NodeView, self).__init__(self.screen,
                                       self.screen.height,
                                       self.screen.width,
                                       hover_focus=False,
                                       on_load=self._reload_prog,
                                       can_scroll=False,
                                       title="Selected")
        self._ncb2 = CheckBox("check2",
                                    label="Another label check2",
                                    name="ncb2",
                                    on_change=self._on_change)
        self._ncb2._value = True
        layout = Layout([1, 20, 1])
        self.add_layout(layout)
        layout.add_widget(self._ncb2, 1)
        layout.add_widget(Divider(height=2), 1)
        self._nowa =  Text(label="Readonly:", name="RO", readonly=True)
        layout.add_widget(Label(f"Time now: {self._nowa.value}"), 1)
        layout.add_widget(self._nowa, 1)
        self._nowa.value = str(time_now)
        self.fix()

    def process_event(self, event):
        if isinstance(event, KeyboardEvent):
            if event.key_code in [ord('q'), ord('Q'), Screen.ctrl("c"), Screen.KEY_F10, Screen.KEY_ESCAPE]:
                raise NextScene("Main")
            else:
                return super(NodeView, self).process_event(event)
        else:
            return super(NodeView, self).process_event(event)

    def _update(self, frame_no):
        global time_now
        self._nowa.value = str(time_now)
        super(NodeView, self)._update(frame_no)

If you remove block

     super(NodeView, self).__init__(self.screen,
                                       self.screen.height,
                                       self.screen.width,
                                       hover_focus=False,
                                       on_load=self._reload_prog,
                                       can_scroll=False,
                                       title="Selected") 

in _reload_prog, then there is no error, then each time a new element is added to infinity. How to properly clear the list of layout?

peterbrittain commented 1 year ago

There is no public API to completely reset the Frame content because I didn't expect people to want to do that. Either you're recreating much the same content (and so wasting effort) or creating a whole new Frame, at which point why isn't it a separate class.

In your case, your code is creating exactly the same content with a different checkbox. You can do that by simply clearing the layout and re-populating it with the new widgets.

All you need to do is save off your original layout from __init__ and then use that inside _reload_prog instead of a new layout. Oh and call layout.clear_widgets before adding the new widgets.

kudrin-alex commented 1 year ago

It is necessary to display different elements depending on the values of the variables. I tried self._layouts.clear()

    def _reload_prog(self, new_value=None):
        self._layouts.clear()
    layout = Layout([1, 20, 1])
        self.add_layout(layout)
        ...

But the program ends with the same error raise IndexError("No live widgets") IndexError: No live widgets

How to use layout.clear_widgets in _reload_prog?

    def _reload_prog(self, new_value=None):
        layout = Layout([1, 20, 1])
        self.add_layout(layout)
        layout.clear_widgets
        ...

Then an error occurs

  File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\widgets\layout.py", line 159, in focus
    raise IndexError("No live widgets")
IndexError: No live widgets
kudrin-alex commented 1 year ago

Sorry, I misused clear_widgets. But with this option - the same error

    def __init__(self, screen):
        global time_now, current_id
        self._last_frame = 0
        super(NodeView, self).__init__(screen,
                                       screen.height,
                                       screen.width,
                                       hover_focus=False,
                                       on_load=self._reload_prog,
                                       can_scroll=False,
                                       title="Selected")
        self._ncb = CheckBox("check1",
                                    label="Label check1",
                                    name="ncb",
                                    on_change=self._on_change)
        self._lay1 = Layout([1, 20, 1])
        self.add_layout(self._lay1)
        self._lay1.add_widget(self._ncb, 1)
        self.fix()

    def _reload_prog(self, new_value=None):
        global current_id, time_now
        self._last_frame = 0

        self._ncb2 = CheckBox("check2",
                                    label="Another label check2",
                                    name="ncb2",
                                    on_change=self._on_change)
        self._ncb2._value = True
        self._ncb3 = CheckBox("check3",
                                    label="Another label check3",
                                    name="ncb3",
                                    on_change=self._on_change)
        self._ncb3._value = False
        self._lay1.clear_widgets()
        self._lay1.add_widget(self._ncb2, 1)
        self._lay1.add_widget(self._ncb3, 1)
        self._lay1.add_widget(Divider(height=2), 1)
        self._nowa =  Text(label="Readonly:", name="RO", readonly=True)
        self._lay1.add_widget(Label(f"Time now: {self._nowa.value}"), 1)
        self._lay1.add_widget(self._nowa, 1)
        self._nowa.value = str(time_now)
        self.fix()
    Screen.wrapper(demo, catch_interrupt=False, arguments=[last_scene])
  File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\screen.py", line 1433, in wrapper
    return func(screen, *arguments)
  File "test1.py", line 153, in demo
    screen.play(scenes, stop_on_resize=True, start_scene=scene, allow_int=True)
  File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\screen.py", line 1606, in play
    self.draw_next_frame(repeat=repeat)
  File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\screen.py", line 1694, in draw_next_frame
    event = scene.process_event(event)
  File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\scene.py", line 105, in process_event
    event = effect.process_event(event)
  File "test1.py", line 133, in process_event
    return super(NodeView, self).process_event(event)
  File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\widgets\frame.py", line 665, in process_event
    self._layouts[self._focus].focus()
  File "C:\Users\z43\AppData\Local\Programs\Python\Python38\lib\site-packages\asciimatics\widgets\layout.py", line 159, in focus
    raise IndexError("No live widgets")
IndexError: No live widgets
peterbrittain commented 1 year ago

Took me a while to figure this one out... It's because you are changing the layout inside the on_load callback.

The current code sets up the focus before calling that callback. Your code then removes it by clearing the layout. Try calling self._lay1.focus(force_first=True) after the call to fix the frame. This should get you back into a good state.

kudrin-alex commented 1 year ago

Yes, it worked! Thank you very much!