peterhinch / micropython-micro-gui

A lightweight MicroPython GUI library for display drivers based on framebuf, allows input via pushbuttons. See also micropython-touch.
MIT License
270 stars 40 forks source link

Window: Parent's Screen after_open is called during nav when only 1 button #7

Closed petrkr closed 1 year ago

petrkr commented 2 years ago

If I will do windows with only one button ("OK") then if I will use next/prev/inc/dec buttons. It will for every push call parent's after_open method

If there are more buttons, this does not happend

class TestWindow(Window):
    def __init__(self, row, col, height, width):
        super().__init__(row, col, height, width, bgcolor = BLACK)

        def cb_btn_ok(btn):
            print("Pressed button {} amount: {}".format(btn, "text123"))
            Window.value("text123")
            Screen.back()

        wribtn = CWriter(ssd, arial10, WHITE, BLACK)

        col = 2
        row = 2
        col += 20
        row += 50
        Button(wribtn, *self.locn(row, col), text='OK', bgcolor = GREEN, textcolor = BLACK, callback=cb_btn_ok)

then Screen

class TestScreen(Screen):
    def __init__(self):
        super().__init__()

    def after_open(self):  # < --- This one gets call if window has only one button everytime
        print("Amount entry after open") 
        if Window.value() is None:
            Screen.change(TestWindow, args=(2, 2, 124, 155))

        if (v := Window.value()) is not None:
            print('Result: {}'.format(v))
Screen.change(TestScreen)
peterhinch commented 2 years ago

I'll investigate.

peterhinch commented 2 years ago

Thank you for the report. I've pushed an update which fixes this bug. It also throws a ValueError if you attempt to open a window with no active widgets, because doing caused erroneous behaviour.

I will try to produce a fix to allow popup windows.

petrkr commented 2 years ago

I will try, thanks. I have to do some dummy acrive widget to achieve that workflow where I really do not want user to interact and learn how to close those popup by program.

I will think about that "close" button at wifi connect screen, there maybe it could be, so user do not need wait timeout if already know that wifi does not in range.

But mostly I want that device to have standalone for "BFU" users who do can not broke many things by pressing buttons etc... It shall be terminal for peoples who are NOT programmers and have problem with basics.. I know it's hard to do software for this kind of users, but less buttons makes it better to them and mostly for support.

Imagine for example trezor, there is also screen, which sign transaction and user can not abort it during process... Lot of windows/linux dialogs also does not have allowed to interrupt by user, because it could lead to strange beaviours.

I will try to think how to do it...

But I want to have some prototype end of month (actually it's open source project, I have no benefit from it).

If I will not be able to it with microgui, I will try to do some Lite version (without menus, or draw menus like "press 1", "press 2"...) and base it on that.

Meanwhile regards and thanks

peterhinch commented 2 years ago

I should be able to produce something quickly.

What I have in mind is a subclass of Window which requires no active widgets. Typically you'd populate it with one or more Label instances. It would support a close method so that a coroutine could cause the popup to disappear with focus returning to the underlying Screen.

peterhinch commented 2 years ago

I have pushed a version which supports popup widows. Example usage:

import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd, Window
import uasyncio as asyncio

from gui.widgets.label import Label
from gui.widgets.buttons import Button, CloseButton
from gui.core.writer import CWriter

# Font for CWriter
import gui.fonts.arial10 as arial10
from gui.core.colors import *

class PopUp(Window):
    def __init__(self, row, col, height, width):
        wri = CWriter(ssd, arial10, WHITE, BLACK, False)
        super().__init__(row, col, height, width, bgcolor = BLACK, fgcolor=YELLOW, writer=wri)  # See README
        Label(wri, 10, 10, "Popup window")

async def do_popup():
    await asyncio.sleep(1)
    Screen.change(PopUp, args=(2, 2, 124, 155))
    await asyncio.sleep(2)
    PopUp.close()

class TestScreen(Screen):
    def __init__(self):
        super().__init__()
        wri = CWriter(ssd, arial10, WHITE, BLACK, False)
        CloseButton(wri)
        Label(wri, 20, 20, "Underneath")
        self.reg_task(do_popup())

    def after_open(self):
        print("After open") 

Screen.change(TestScreen)

Note that using after_open to launch the popup won't produce the desired result. This is because after_open also runs when an overlaying window is closed: the window underneath is re-displayed and after_open runs a second time. This is by design.

Please let me know what you think.

petrkr commented 2 years ago

Yes, it seems nice, also I can define textbox in this async.

async def do_bootscreen():
    global wifi, rfid
    await asyncio.sleep_ms(10)
    writb = CWriter(ssd, arial10, GREEN, BLACK)
    Screen.change(BootScreen, args=(2, 2, 124, 155, writb))
    _tb = Textbox(writb, 42, 6, width=147, nlines=8)

    print("  -- Mifare NFC")
    _tb.append("Loading RFID")
    await asyncio.sleep_ms(0)

    from components.rfid import PN532_UART
    from components.rfid.pn532mifareio import PN532MifareIO
    uart2 = UART(2, 115200, tx = NFC_TX, rx=NFC_RX, timeout=100)
    rfid = PN532_UART(uart2)

    from utils.wifi_connect import WiFiConnect
    wifi = WiFiConnect()
    #wifi.events_add_connecting(self.wifi_cb_connecting)
    #wifi.events_add_connected(self.wifi_cb_connected)

    _tb.append("Connecting wifi")
    await asyncio.sleep_ms(0)

    wifi.connect()

    _tb.append("Connected")
    await asyncio.sleep(2)

    BootScreen.close()

Just since WiFiConnect does not support asyncio, I can not use "connecting" callbacks as they do not refresh screen (of course). also "wc.connect" is blocking call.

I have to do some callbacks etc to let know main-screen about situation "wifi" connected, so it can continue to do some other init stages (like connect to server etc.)

But That I think I will do by some semaphor flags in While True in main app.

petrkr commented 2 years ago

meanwhile I did this construct... Maybe in asyncio can be better way? But for my knowledge this is best what I did

    def boot_done(self, obj):
        self._boot_done = True
        self._wifi = obj[0]
        self._rfid = obj[1]

        self.reg_task(self.main())

    def after_open(self):
        if not self._boot_done:
            self.reg_task(do_bootscreen(self.boot_done))
peterhinch commented 2 years ago

The solution above is fine, but here is alternative:

from uasyncio import Event

async def main(evt):
    await evt.wait()
    # Code runs after BaseScreen initialisation is complete

class BaseScreen(Screen):
    def __init__(self):
        super().__init__()
        # Code omitted
        self.started = Event()
        self.reg_task(main(self.started))

    def after_open(self):
        self.started.set()

Of course you could cheat and just issue await asyncio.sleep(1) at the start of main() on the assumption that screen show will be complete by then. But an Event is nicer.