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
247 stars 37 forks source link

Why that trash symbol? #34

Open beyonlo opened 1 year ago

beyonlo commented 1 year ago

Hello @peterhinch

I'm using micro-gui with the ssd1306 mono display (128 x 64) with three buttons, but the micro-gui is showing in all demos a strange symbol (like as a trash) on the top right side. Why that symbol and how to remove?

Here a screenshot showing that trash symbol inside the green circle using demo/simple.py: WhatsApp Image 2023-07-10 at 19 55 57

As I'm using a mono display I just changed the simple.py:

  1. All places where has CWriter to Writer
  2. wri = CWriter(ssd, arial10, GREEN, BLACK) to wri = Writer(ssd, arial10)

gui/demos/simple.py:

# simple.py Minimal micro-gui demo.

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch

# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd

from gui.widgets import Label, Button, CloseButton
from gui.core.writer import Writer

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

class BaseScreen(Screen):

    def __init__(self):

        def my_callback(button, arg):
            print('Button pressed', arg)

        super().__init__()
        # verbose default indicates if fast rendering is enabled
        wri = Writer(ssd, arial10)
        col = 2
        row = 2
        Label(wri, row, col, 'Simple Demo')
        row = 50
        Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',))
        col += 60
        Button(wri, row, col, text='No', callback=my_callback, args=('No',))
        CloseButton(wri)  # Quit the application

def test():
    print('Simple demo: button presses print to REPL.')
    Screen.change(BaseScreen)  # A class is passed here, not an instance.

test()

Output:

$ mpremote run simple.py 
Using 3 switches.
Simple demo: button presses print to REPL.
Orientation: Horizontal. Reversal: False. Width: 128. Height: 64.
Start row = 0 col = 0
Warning: attempt to create Button outside screen dimensions.
Warning: attempt to create Button outside screen dimensions.

My hardware_setup.py:

from machine import Pin, SPI
import machine
import gc
import time

from ssd1306 import SSD1306_I2C as SSD

WIDTH = const(128)
HEIGHT = const(64)

i2c = machine.SoftI2C(scl=machine.Pin(18), sda=machine.Pin(17))
gc.collect()  # Precaution before instantiating framebuf
ssd = SSD(WIDTH, HEIGHT, i2c)

from gui.core.ugui import Display

# Define control buttons
nxt = Pin(42, Pin.IN, Pin.PULL_UP)  # Move to next control
sel = Pin(12, Pin.IN, Pin.PULL_UP)  # Operate current control
prev = Pin(41, Pin.IN, Pin.PULL_UP)  # Move to previous control
display = Display(ssd, nxt, sel, prev)  # 3-button mode

The ssd1306.py is from official micropython driver.

Thank you!

beyonlo commented 1 year ago

I tested with font6.py (and others fonts) on the demo/simple.py and I have same result

import gui.fonts.font6 as font6
wri = Writer(ssd, font6)

As you know, I was using the nano-gui and I was running nano-gui demos in the same display and that symbol never showed.

beyonlo commented 1 year ago

Ohh, I realized that symbol is the exit button, right? So sorry!

Is there how do not use and do not show that exit button? Because that exit button use a very large space on the display when used little display as the ssd1306 with 128x64 pixels.

beyonlo commented 1 year ago

Let me to do another question: on the nano-gui I was using the demo/mono_test.py. I would like to use that example in the micro-gui, where Next/Prev buttons change between fields(), multi_fields() and meter(). How is better way to do that?

Thank you in advance!

beyonlo commented 1 year ago

I think the micro-gui is no adapted very well to little display, like as 128x64, am I correct or not?

Mostly micro-gui demos the widgets go to out of display or I have error ValueError: row is out of range or showed message to use a display with at least 320x240 pixels. So, maybe is a good way use micro-gui working with Next/Prev between entire Screen, like as in the fields(), multi_fields() and meter() and if Select is pressed on the multi_fields() for example, another Screen can be called, or some thing like that - any idea or example that I can run as a start point?

peterhinch commented 1 year ago

The symbol is the CloseButton: just remove that line to get rid of it.

micro-gui is not optimisd for such a tiny screen, and I've never investigated the sort of modes you have in mind.

beyonlo commented 1 year ago

The symbol is the CloseButton: just remove that line to get rid of it.

Sorry for that dummy question. Now I see in the buttons.py code:

# Preferred way to close a screen or dialog. Produces an X button at the top RHS.
# Note that if the bottom screen is closed, the application terminates.
class CloseButton(Button):

micro-gui is not optimisd for such a tiny screen, and I've never investigated the sort of modes you have in mind.

Now I have three buttons on the display to use inputs Prev, Select, Next, but as the menus e etc of micro-gui are very large to that 128x64 display, I'm in a dillema what to do:

  1. Back to nano-gui (that is optimised to tiny display) and continue with re-invente the inputs Prev, Select and Next (now that I have three buttons on hardware). In that examples that I was testing in nano-gui I was already working changing of Screen.
  2. Continue on micro-gui and find a way (any idea?) to not use the standard menus (that are very large for 128x64 display). Has the micro-gui something that I can just use Prev and Next to change of Screen/Window? and use the Select on that Screen to show another Screen/Window?

Ps:

Below are two prints using nano-gui with that re-inveted inputs next, prev, select that I opened that issue in the nano-gui project: https://github.com/peterhinch/micropython-nano-gui/issues/53

Is just two Screens, but will be much more, and the idea is prev/next change of that complete screen to another and use Select to change of Screen too. WhatsApp Image 2023-07-11 at 12 26 01 WhatsApp Image 2023-07-11 at 12 26 02

Could you please give your thoughts about that?

Thank you

peterhinch commented 1 year ago

Here is the general approach I would use. Use micro-gui. Assign 3 unused pins to Button objects and use them to initialise the GUI. Their job is purely to allow the GUI to start correctly.

Assign the three pins used by your physical buttons to Button objects: let's call those Next, Prev and Sel. Note that Button instances allow you to dynamically assign callbacks.

Examine the demos to see how next and prev virtual buttons work. On your first screen, define an after_open method which assigns a callback to your Next button*. On the next screen, after_open would assign callbacks to Next, Prev and Sel. You'll need to be sure that every screen has an after_open which assigns appropriate callbacks to the three buttons.

*On the first screen you'll also need to assign callbacks to Prev and Sel, because you might run this screen in response to Prev from the next. You can also disable the callback - see docs.

I'm convinced this could work, but it will require careful coding and studying the screen change mechanism.

beyonlo commented 1 year ago

Hello @peterhinch I liked so much about your idea and I understood your rationale. I read many docs and source code as well from demos and ugui.py, but I have still some difficult to put that idea to works

Here is the general approach I would use. Use micro-gui. Assign 3 unused pins to Button objects and use them to initialise the GUI. Their job is purely to allow the GUI to start correctly.

Just to clarify: I already has 3 Pins dedicated for the physical Buttons, so need I more 3 Pins (even temporary Pins) to initialize the GUI? If yes, I have no more Pins unused in my board, the last two unused pins I used to complete 3 physical Buttons instead 1 Button. But, if is really necessary, I can to start the display part of code before others async applications, so I use that 3 pins in temporary mode from that others applications until GUI will be started, and after remap that 3 Pins again to others applications.

Assign the three pins used by your physical buttons to Button objects: let's call those Next, Prev and Sel. Note that Button instances allow you to dynamically assign callbacks.

I not understand how to that in code, because the hardware_setup.py already has that 3 Pins configured as Prev, Sel and Next

Examine the demos to see how next and prev virtual buttons work. On your first screen, define an after_open method which assigns a callback to your Next button*. On the next screen, after_open would assign callbacks to Next, Prev and Sel. You'll need to be sure that every screen has an after_open which assigns appropriate callbacks to the three buttons.

*On the first screen you'll also need to assign callbacks to Prev and Sel, because you might run this screen in response to Prev from the next. You can also disable the callback - see docs.

I understand very well about the after_open(self). I did some tests with after_open(self) while I was porting (with success :partying_face: ) my Screens from nano-gui to micro-gui and It's simple: just execute something after widgets are created on the Screen. So after each Screen start, I need on the after_open(self) to assign a new Screen for the Next, Prev and Sel - but how to do that (what code)?

I examined demo simple.py that has two buttons where is possible test the Prev and Next mechanism, but inside the simple.py I think that mechanismi is not showed, right? So I checked in the ugui.py where has class Display(DisplayIP): that is used to assign that 3 buttons on hardware_setup to the 3 real pins. There is the class Input: maybe is there (I think) where I need to reuse some variables to change the Prev, Sel and Next on the after_open(self)? Sorry, that is too much information for a newbie in GUI.

I'm convinced this could work, but it will require careful coding and studying the screen change mechanism.

I don't know if I'm asking too much, but honestly I need your help for an example to start. So, could you please, create a very simple example with two or 3 Screens implementing that idea to me as a start point? And maybe this example can be a official example for anyone that want to use micro-gui with Prev, Sel, Next moving over Screens (when tiny display) instead to use the menus and some others widgets that are larger too much for that tiny displays.

Thank you so much!

At least I'm glad today that I have ported successfully that two nano-gui Screens to micro-gui (bellow the code):

multi_fields.py:

# simple.py Minimal micro-gui demo.

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch

# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd

from gui.widgets import Label, Button, CloseButton, Meter
from gui.core.writer import Writer
import uasyncio as asyncio

# Font for CWriter
import gui.fonts.arial10 as arial10
import gui.fonts.font6 as small

from gui.core.colors import *

def xorshift64star(modulo, seed = 0xf9ac6ba4):
    x = seed
    def func():
        nonlocal x
        x ^= x >> 12
        x ^= ((x << 25) & 0xffffffffffffffff)  # modulo 2**64
        x ^= x >> 27
        return (x * 0x2545F4914F6CDD1D) % modulo
    return func

async def change_multi_fields(nfields):
    random = xorshift64star(2**24 - 1)
    while True:
        for field in nfields:
            value = random() / 16777
            field.value('{:5.2f}'.format(value))
        await asyncio.sleep_ms(500)

class BaseScreen(Screen):

    def __init__(self):

        super().__init__()
        Writer.set_textpos(ssd, 0, 0)  # In case previous tests have altered it
        wri = Writer(ssd, small, verbose=False)
        wri.set_clip(False, False, False)
        nfields = []
        dy = small.height() + 6
        y = 2
        col = 15
        width = wri.stringlen('999.99')
        for txt in ('1:', '2:', '3:'):
            Label(wri, y, 0, txt)
            nfields.append(Label(wri, y, col, width, bdcolor=None))  # Draw border
            y += dy

        dy = small.height() + 6
        y = 2
        col = 82
        for txt in ('4:', '5:', '6:'):
            Label(wri, y, 67, txt)
            nfields.append(Label(wri, y, col, width, bdcolor=None))  # Draw border
            y += dy

        self.reg_task(change_multi_fields(nfields))

        CloseButton(wri)

    def after_open(self):
        print('----------------------------- 1')
        ssd.hline(60, 60, 30, BLUE)
        ssd.vline(67, 0, 28, BLUE)
        print('----------------------------- 2')

def test():
    Screen.change(BaseScreen)  # A class is passed here, not an instance.

test()

meter1.py

# simple.py Minimal micro-gui demo.

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch

# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd

from gui.widgets import Label, Button, CloseButton, Meter
from gui.core.writer import Writer
import uasyncio as asyncio

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

async def change_meter(m0, m1, m2):
    steps = 10
    c = 1
    for n in range(steps + 1):
        m0.value(c/steps)
        m1.value(n/steps)
        m2.value(1 - n/steps)
        await asyncio.sleep(1)
        c += 1

class BaseScreen(Screen):

    def __init__(self):

        super().__init__()
        wri = Writer(ssd, arial10, verbose=False)
        m0 = Meter(wri, 5, 2, height = 45, divisions = 4,  legends=('0.0', '50', '100', '245!', '250!'), label='4', style=Meter.BAR)
        m1 = Meter(wri, 5, 44, height = 45, divisions = 4, legends=('0.0', '50', '200', '295!', '300!'), label='5', style=Meter.BAR)
        m2 = Meter(wri, 5, 86, height = 45, divisions = 4, legends=('0.0', '50', '200', '335!', '340!'), label='6', style=Meter.BAR)
        self.reg_task(change_meter(m0, m1, m2))
        CloseButton(wri)

    def after_open(self):
        print('----------------------------- 1')
        ssd.hline(60, 60, 30, BLUE)
        ssd.vline(67, 0, 28, BLUE)
        print('----------------------------- 2')

def test():
    Screen.change(BaseScreen)  # A class is passed here, not an instance.

test()
beyonlo commented 1 year ago

Hi @peterhinch Sorry for one more time request, but I would like to know if will be possible to you to create a very simple example with two or 3 Screens implementing that your idea as a start point to me? A good and perfect example with three screens could be that my two screens above and the third scree the simple.py example, because the simple.py is a perfect screen to execute a command (it has button yes and no) to enable/disable some feature. However as the simple.py has real (not more virtual) buttons (yes and no) maybe need to do a additional remap of pins in the after_open(), and this is another thing that I have no idea how to implement, and that's why it's so important an example with those three screens as start point :)

Don't worry if you can't to do that, is just to me know what way I will follow. Unfortunately I was no capable to implement your idea using micro-gui, and if you can't to create an example, I will back to nano-gui and and continue with re-invente the inputs Prev, Select and Next.

Thank you in advance!

peterhinch commented 1 year ago

I'm sorry but I am far too busy on another project to write and test a demo for you.

I am convinced that micro-gui is the best, and simplest, approach. Looking at simple.py you would have two Pushbutton objects associated with your physical buttons. The Pushbutton close callbacks would be the callback currently associated with the Button objects on the screen.

In the case where screens change, the Pushbutton callbacks might need to change. This would be done in the after_open method.

beyonlo commented 1 year ago

I am convinced that micro-gui is the best, and simplest, approach.

Me too. And I did read this messages bellow dozens of times and still have questions.

Looking at simple.py you would have two Pushbutton objects associated with your physical buttons.

The two Pushbutton that you are talking about are the Button() yes on line 32 and Button() no on line 34 of the file simple.py right?

The Pushbutton close callbacks would be the callback currently associated with the Button objects on the screen.

Now you talk about again of the same Pushbutton that I believe to be the same on line 32 and 34. If yes, that Button() do not have close callbacks. They have callback, but that call just the my_callback() on line 22.

And you talk about "associated with the Button objects on the screen", but If Pushbutton are objects (line 32, 34) what line are there the Button objects?

In the case where screens change, the Pushbutton callbacks might need to change. This would be done in the after_open method.

Here I'm confused too because I do not know what lines in the simple.py arePushbutton objects and what are the Button objects.

Could you please clarify that?

Other question: for that solution need I to change some code in some file inside core/ or widgets/ directories, or all the changes needed is just on the simple.py for example?

I'm sorry but I am far too busy on another project to write and test a demo for you.

I agree, but if you could at least write directly here in the GitHub a piece of code as a draft (without any test) I can get better the idea and I will to do the tests and complete what will need.

Thank you so much!

peterhinch commented 1 year ago

Button objects are on-screen widgets. Pushbutton objects are physical pushbuttons.

In your final application you won't have any Button instances. What I am suggesting is that you implement Pushbutton instances for your electrical buttons. You study the examples to see how the callbacks for Buttons work. And you modify the example code so that the callbacks are assigned to the Pushbutton close instead of the Button. The Button instances can then be eliminated from the screen.

beyonlo commented 1 year ago

I had progress.. :partying_face: I have one Question below.

In the hardware_setup.py I used unused pins to initialize gui without errors. - Done!

Button objects are on-screen widgets. Pushbutton objects are physical pushbuttons.

Got it!

In your final application you won't have any Button instances.

Got it!

What I am suggesting is that you implement Pushbutton instances for your electrical buttons.

Done on simple.py:

from gui.primitives import Pushbutton
from machine import Pin

class BaseScreen(Screen):

    def __init__(self):

        def my_callback(button, arg):
            print('Button pressed', button, arg)

        super().__init__()
        # verbose default indicates if fast rendering is enabled
        wri = Writer(ssd, arial10)
        col = 2
        row = 2
        Label(wri, row, col, 'Simple Demo')
        row = 50
        Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',))
        col += 60
        Button(wri, row, col, text='No', callback=my_callback, args=('No',))

        # 3 Pushbuttons implemented (prev, sel, next)
        prev = Pin(41, Pin.IN, Pin.PULL_UP)
        self.btn_prev = Pushbutton(prev)
        sel = Pin(12, Pin.IN, Pin.PULL_UP)
        self.btn_sel = Pushbutton(sel)
        nxt = Pin(42, Pin.IN, Pin.PULL_UP)
        self.btn_nxt = Pushbutton(nxt)

        # test to implement callback on the pushbutton
        #self.btn_nxt = Pushbutton(b_next, callback=my_callback, args=('Test',))

You study the examples to see how the callbacks for Buttons work.

This is in progress yet... I'm stuying!

And you modify the example code so that the callbacks are assigned to the Pushbutton close instead of the Button.

Question: here I got the idea too, but the /gui/primitives/pushbutton.py do not has the close method. What or where is that close on the Pushbutton?

Any additional comments about the code above (including that line commented about callback on the pushbutton) is wellcome!

Thank you!

peterhinch commented 1 year ago

See the Pushbutton docs. You use the press_func method to assign a callback.

beyonlo commented 1 year ago

Hey @peterhinch It works like you told me! :partying_face: :balloon: Thank you for your support and especially for your patience!

I still testing and finding the best way to call each Screen in the after_open(self) I'm thinking to have a python file for each Screen and import each Python file when I will use it in the callback to sel, next or prev. Any suggestion?

A question about this error: ValueError: Screen has no active widgets. Is possible to do something to not show this error, or to create a invisible active widget? Because sometimes I do not need active widgets. For now I'm using the CloseButton(wri) to works.

Thank you very much!

peterhinch commented 1 year ago

I think importing on demand will work. If you want a modular design this is fine, but this way of working won't save resources. The GUI only instantiates the Screen instance when it is opened, and MicroPython's garbage collection will retrieve the RAM when it is closed.

Re the error, I hadn't thought of this. I think suppressing the error will only move the problem elsewhere - the GUI is built on the assumption that there is at least one active control on a screen. So the best solution is probably to make the close button invisible. Adding the following line to widgets\buttons.py should do this:

class CloseButton(Button):
    def __init__(self, writer, width=0, callback=dolittle, args=(), bgcolor=RED):
        scr = Screen.current_screen
        # Calculate the button width if not provided. Button allows
        # 5 pixels either side.
        wd = width if width else (writer.stringlen('X') + 10)
        self.user_cb = callback
        self.user_args = args
        super().__init__(writer, *scr.locn(4, scr.width - wd - 4),
                         width = wd, height = wd, bgcolor = bgcolor,
                         callback = self.cb, text = 'X')

        self.visible = False  # Add this line
beyonlo commented 1 year ago

Hi @peterhinch works with self.visible = False

Mostly Screens that I will have is just to user see the data, like as that Screens I posted above (meter, multifields), but some Screen I want a Screen exactely as the simple.py with that two active buttons (yes and no), where user can for example Enable AP mode and choose Yes or No. So I was thinking (and I already did some tests) where in the hardware_setup.py I can to use the correct pins, I mean, the same used in the pushbuttons on the Screens. So when is a Screen where there is no active widgets that pins on the hardware_setup.py will do not effect, but when I open a window like as the simple.py that hardware_setup.py pins will works automatically, and, I in this screen I do not configure the next/prev/sel on the after_open(), but only when user choose yes or not. Does that make sense for you?

IMG_20230721_111647071

Thank you in advance!

peterhinch commented 1 year ago

That sounds OK.

beyonlo commented 1 year ago

Hello @peterhinch What's the correct way to integrate my micro-gui app with other async tasks/servers?

That integrate below works, but I note that after I integrate that very simple example (the simple.py) all my system got very slow.

Ps: I changed on the /gui/core/ugui.py from do_gc = True to do_gc = False but do not solve the problem :(

Any idea why integrate the example simple.py got all system slow?

main.py:

async def main():
    await start._setup()

    # Start ModBus Slave TCP
    slave_tcp_task = _start_slave_tcp('192.168.43.143', 502, 2)
    asyncio.run(slave_tcp_task)

    # Start micro-gui simple.py
    print('starting display... ---------------------')
    from main_simple import test
    t1 = asyncio.create_task(test())
    print('Display started! ---------------------')

    # Start Microdot
    await app.start_server(host='0.0.0.0', port=80, debug=False)
try:
    asyncio.run(main())
except KeyboardInterrupt:
    print('Stopped')

main_simple.py:

import hardware_setup  # Create a display instance
from gui.core.ugui import Screen

from simple import BaseScreen

async def test():
    print('Simple demo: button presses print to REPL.')
    Screen.change(BaseScreen)  # A class is passed here, not an instance.

#test()

simple.py:

# simple.py Minimal micro-gui demo.

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch

# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd

from gui.widgets import Label, Button, CloseButton
from gui.core.writer import Writer

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

class BaseScreen(Screen):

    def __init__(self):

        def my_callback(button, arg):
            print('Button pressed', arg)

        super().__init__()
        # verbose default indicates if fast rendering is enabled
        wri = Writer(ssd, arial10)
        col = 2
        row = 2
        Label(wri, row, col, 'Simple Demo')
        row = 50
        Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',))
        col += 60
        Button(wri, row, col, text='No', callback=my_callback, args=('No',))
        CloseButton(wri)  # Quit the application

#def test():
#    print('Simple demo: button presses print to REPL.')
#    Screen.change(BaseScreen)  # A class is passed here, not an instance.
#
#test()

EDIT: my ESP32-S3 is clocked at full speed: 240 MHz

peterhinch commented 1 year ago

micro-gui automatically starts uasyncio - the intention being for it to be accessible by means of synchronous coding only.

Applications therefore shouldn't use asyncio.run(). The way to run asynchronous code within a micro-gui application is to launch tasks from a callback (e.g. after_open) or from the base screen's __init__.py - see for example the aclock.py demo. See the docs for a way to ensure tasks are cancelled on exit.

beyonlo commented 1 year ago

Hello @peterhinch

micro-gui automatically starts uasyncio - the intention being for it to be accessible by means of synchronous coding only.

Yes, I see that in the docs. But please, think in a very simple scenario, if I use Microdot (or/and other servers) I need to start the micro-gui in asyncronous mode, otherwise the micro-gui will block all async applications, that's why I started the micro-gui application as a task - t1 = asyncio.create_task(test_display())

Let me see if I understood what you are saying: software that need to run the micro-gui, need to have micro-gui as the main (sync) started application and the async servers/tasks need only be started from the micro-gui using the self.reg_task()?

Applications therefore shouldn't use asyncio.run(). The way to run asynchronous code within a micro-gui application is to launch tasks from a callback (e.g. after_open) or from the base screen's __init__.py - see for example the aclock.py demo. See the docs for a way to ensure tasks are cancelled on exit.

Yes, I already following that example (for the self.reg_task()) when I ported the nano-gui examples to micro-gui - I pasted this thread above.

But I can't see how I can to start all async servers like Microdot and others from the self.reg_task(), any example as start Microdot for example?

  1. Is a good idea to support the micro-gui to be started from an async code? Because I can to start all my async servers correctly as their docs say and start micro-gui as a task!
  2. Starting micro-gui in async mode (as a task - as my example above) apparently works ( t1 = asyncio.create_task(test_display())), the problem is just that all applications got slow. But if the applications do not got to slow, can I use in that way? I'm asking this because I found that if I change the line await asyncio.sleep_ms(0) # Let user code respond to event in the /core/ugui.py from await asyncio.sleep_ms(0) to await asyncio.sleep_ms(50) all system works perfect - not got slow anymore. Is necessary to be 0 in that sleep or can it be changed to other value, like as 50? Or I will have some problem in the future? I not understand very well, but I think that sleep_ms(0) means that refresh data on the display will be done as soon is possible, but if I use sleep_ms(50) will refresh only each 50 ms, is that correct?

Thank you very much

EDIT:

About the Item 2. above: I changed to asyncio.sleep_ms(100) to have better results, because 50ms was still a bit slow.

beyonlo commented 1 year ago

@peterhinch There is a situation with self.visible = False # Add this line on the /gui/widgets/buttons.py at the class CloseButton(Button). It works (CloseButton is invible now), but that Screen stop to call the NEXT Screen configured in the after_open(self)

If I comment line #self.visible = False # Add this line the NEXT back to works, I mean, the next Screen now show when I click in the NEXT pushbutton

        CloseButton(wri)

    def next_screen(self, arg):
        print('Pushbutton pressed', arg)
        Screen.change(ScreenTempMeter01)

    def after_open(self):
        nxt = Pin(42, Pin.IN, Pin.PULL_UP)
        self.btn_nxt = Pushbutton(nxt)
        self.btn_nxt.press_func(self.next_screen, ('next screen',))
beyonlo commented 1 year ago

@peterhinch one more question: how is possible to pass params to the class BaseScreen(Screen):?

I'm using the simple.py as reference.

I added a msg param this in the init:

    def __init__(self, msg=None):
        print(msg)

And where the Screen is called I changed from Screen.change(BaseScreen) to Screen.change(BaseScreen(msg='test1')) but I have this error: ValueError: Must pass Screen class or subclass (not instance). Yes, I understand that it accept just class, and not an instance. So, what is the best way to pass params to be used inside the BaseScreen code?

simple.py changed:

# simple.py Minimal micro-gui demo.

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2021 Peter Hinch

# hardware_setup must be imported before other modules because of RAM use.
import hardware_setup  # Create a display instance
from gui.core.ugui import Screen, ssd

from gui.widgets import Label, Button, CloseButton
from gui.core.writer import Writer

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

class BaseScreen(Screen):

    def __init__(self, msg=None):
        print(msg)

        def my_callback(button, arg):
            print('Button pressed', arg)

        super().__init__()
        # verbose default indicates if fast rendering is enabled
        wri = Writer(ssd, arial10)
        col = 2
        row = 2
        Label(wri, row, col, 'Simple Demo')
        row = 50
        Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',))
        col += 60
        Button(wri, row, col, text='No', callback=my_callback, args=('No',))
        CloseButton(wri)  # Quit the application

def test():
    print('Simple demo: button presses print to REPL.')
    Screen.change(BaseScreen(msg='test1'))  # A class is passed here, not an instance.

test()

Thank you again :)

peterhinch commented 1 year ago

Please see the docs. You must pass the args to the Screen.change() method and they will be picked up by BaseScreen.__init__.

Screen.change(BaseScreen, msg='test1')

The code may be seen here

beyonlo commented 1 year ago

@peterhinch The args works - thanks :)

The proof of concept about your pushbutton idea works, but I have two problems and one question.

I did a very very simple code - it has screen-1, screen-2, screen-3, and screen-4, where pushbutton prev and next change (forward or backwards) between screen-1, screen-2 and the screen-3. The screen-4 is just changed when in the screen-3 user click in the sel pushbutton. And to go out from screen-4 user need just to press Button Yes - so, that will back to screen-1. The code is pasted below, but I attached in tar.gz format the files to be easy to test. There is attached as well a video with the idea running and showing the problem-2

Could you please to check my code if there is something wrong? I did a very simple code because if you want to test, you need just to call the screen_main.py

Problems:

  1. self.visible = False on the class CloseButton(Button) works, CloseButton turn on invisible, but that stop to work the prev, sel and the next of the pushbuttons (to change the screens). Any idea how to fix that? I pasted details about this problem in this thread The example below (including the video) are using CloseButton visible, otherwise will not works the idea.
  2. When I press sel on Screen-3 to change to Screen-4 (that has Button Yes and No) , automatically Button Yes is pressed - But I do not pressed anything. Do you know what is the problem?

Question: I'm changing to new Screen from inside another Screen (button.press_func()), so every time that a Screen is changed to another, the previous Screen will not closed and never will. I checked that if I close (click in the CloseButton) the Screen, that Screen is closed and back to last Screen and so on, until all Screens are closed. So, using this idea (like as a carousel - without CloseButton) the same Screens will be called many times and they will never close. Can that be a problem? The same question is for when used self.reg_task() in the Screens - since the screens will never close, each time that Screen will be called, a new self.reg_task() will be executed. This code is not using self.reg_task(), because is just a Proof of Concept, but I will use it.

screen_main.py:

import hardware_setup  # Create a display instance
from gui.core.ugui import Screen
from gui.primitives import Pushbutton
from machine import Pin

from screen_1   import BaseScreen_1
from screen_2   import BaseScreen_2
from screen_3   import BaseScreen_3
from screen_4   import BaseScreen_4

screens = {}
screens['BaseScreen_1']      = BaseScreen_1
screens['BaseScreen_2']      = BaseScreen_2
screens['BaseScreen_3']      = BaseScreen_3
screens['BaseScreen_4']      = BaseScreen_4

prev_pin = Pin(41, Pin.IN, Pin.PULL_UP)
sel_pin =  Pin(12, Pin.IN, Pin.PULL_UP)
next_pin = Pin(42, Pin.IN, Pin.PULL_UP)
prev_btn = Pushbutton(prev_pin)
sel_btn  = Pushbutton(sel_pin)
next_btn = Pushbutton(next_pin)

buttons = {}
buttons['prev'] = prev_btn
buttons['sel']  = sel_btn
buttons['next'] = next_btn

def main_screen():
    params = {'screens': screens, 'buttons': buttons}
    kwargs = {'params': params}
    Screen.change(screens['BaseScreen_1'], kwargs=kwargs)

main_screen()

screen_1.py:

from gui.core.ugui import Screen, ssd
from gui.widgets import Label, Button, CloseButton
from gui.core.writer import Writer
import gui.fonts.arial10 as arial10

class BaseScreen_1(Screen):

    def __init__(self, params):
        self.kwargs         = {'params': params}
        self.params         = params
        self.screens        = params['screens']
        self.prev_btn       = params['buttons']['prev']
        self.sel_btn        = params['buttons']['sel']
        self.next_btn       = params['buttons']['next']

        super().__init__()
        wri = Writer(ssd, arial10, verbose=False)
        col = 2
        row = 2
        screen_name = 'Screen-1'
        print(f'I am screen: {screen_name}')
        Label(wri, row, col, screen_name)
        CloseButton(wri)

    def prev_screen(self, arg):
        screen = self.screens['BaseScreen_3']
        print(f'Pushbutton pressed: {arg}. Changing to the screen: {screen}')
        Screen.change(screen, kwargs=self.kwargs)

    def sel_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print(f'This Screen has no function to the: {arg}')

    def next_screen(self, arg):
        screen = self.screens['BaseScreen_2']
        print(f'Pushbutton pressed: {arg}. Changing to the screen: {screen}')
        Screen.change(screen, kwargs=self.kwargs)

    def after_open(self):
        self.prev_btn.press_func(self.prev_screen, ('prev screen',))
        self.sel_btn.press_func(self.sel_screen,   ('sel screen',))
        self.next_btn.press_func(self.next_screen, ('next screen',))

screen_2.py:

from gui.core.ugui import Screen, ssd
from gui.widgets import Label, Button, CloseButton
from gui.core.writer import Writer
import gui.fonts.arial10 as arial10

class BaseScreen_2(Screen):

    def __init__(self, params):
        self.kwargs         = {'params': params}
        self.params         = params
        self.screens        = params['screens']
        self.prev_btn       = params['buttons']['prev']
        self.sel_btn        = params['buttons']['sel']
        self.next_btn       = params['buttons']['next']

        super().__init__()
        wri = Writer(ssd, arial10, verbose=False)
        col = 2
        row = 2
        screen_name = 'Screen-2'
        print(f'I am screen: {screen_name}')
        Label(wri, row, col, screen_name)
        CloseButton(wri)

    def prev_screen(self, arg):
        screen = self.screens['BaseScreen_1']
        print(f'Pushbutton pressed: {arg}. Changing to the screen: {screen}')
        Screen.change(screen, kwargs=self.kwargs)

    def sel_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print(f'This Screen has no function to the: {arg}')

    def next_screen(self, arg):
        screen = self.screens['BaseScreen_3']
        print(f'Pushbutton pressed: {arg}. Changing to the screen: {screen}')
        Screen.change(screen, kwargs=self.kwargs)

    def after_open(self):
        self.prev_btn.press_func(self.prev_screen, ('prev screen',))
        self.sel_btn.press_func(self.sel_screen,   ('sel screen',))
        self.next_btn.press_func(self.next_screen, ('next screen',))

screen_3.py:

from gui.core.ugui import Screen, ssd
from gui.widgets import Label, Button, CloseButton
from gui.core.writer import Writer
import gui.fonts.arial10 as arial10

class BaseScreen_3(Screen):

    def __init__(self, params):
        self.kwargs         = {'params': params}
        self.params         = params
        self.screens        = params['screens']
        self.prev_btn       = params['buttons']['prev']
        self.sel_btn        = params['buttons']['sel']
        self.next_btn       = params['buttons']['next']

        super().__init__()
        wri = Writer(ssd, arial10, verbose=False)
        col = 2
        row = 2
        screen_name = 'Screen-3'
        print(f'I am screen: {screen_name}')
        Label(wri, row, col, screen_name)
        col = 2
        row = 20
        Label(wri, row, col, 'Press *SEL* to open')
        col = 2
        row = 40
        Label(wri, row, col, 'the Screen-4')

        CloseButton(wri)

    def prev_screen(self, arg):
        screen = self.screens['BaseScreen_2']
        print(f'Pushbutton pressed: {arg}. Changing to the screen: {screen}')
        Screen.change(screen, kwargs=self.kwargs)

    def sel_screen(self, arg):
        screen = self.screens['BaseScreen_4']
        print(f'Pushbutton pressed: {arg}. Changing to the screen: {screen}')
        Screen.change(screen, kwargs=self.kwargs)

    def next_screen(self, arg):
        screen = self.screens['BaseScreen_1']
        print(f'Pushbutton pressed: {arg}. Changing to the screen: {screen}')
        Screen.change(screen, kwargs=self.kwargs)

    def after_open(self):
        self.prev_btn.press_func(self.prev_screen, ('prev screen',))
        self.sel_btn.press_func(self.sel_screen,   ('sel screen',))
        self.next_btn.press_func(self.next_screen, ('next screen',))

screen_4.py:

from gui.core.ugui import Screen, ssd
from gui.widgets import Label, Button, CloseButton
from gui.core.writer import Writer
import gui.fonts.arial10 as arial10

class BaseScreen_4(Screen):

    def __init__(self, params):
        self.kwargs         = {'params': params}
        self.params         = params
        self.screens        = params['screens']
        self.prev_btn       = params['buttons']['prev']
        self.sel_btn        = params['buttons']['sel']
        self.next_btn       = params['buttons']['next']

        def my_callback(button, arg):
            print('Button pressed', arg)
            if arg == 'Yes':
                screen = self.screens['BaseScreen_1']
                print(f'Your choose was *{arg}*, so Changing to the screen: {screen}')
                Screen.change(screen, kwargs=self.kwargs)

        super().__init__()
        wri = Writer(ssd, arial10, verbose=False)
        col = 2
        row = 2
        screen_name = 'Screen-4'
        print(f'I am screen: {screen_name}')
        Label(wri, row, col, screen_name)
        col = 2
        row = 14
        Label(wri, row, col, 'Press Yes')
        col = 2
        row = 28
        Label(wri, row, col, 'to go to Screen-1')
        row = 40
        Button(wri, row, col, text='Yes', callback=my_callback, args=('Yes',))
        col += 50
        Button(wri, row, col, text='No', callback=my_callback, args=('No',))
        CloseButton(wri)  # Quit the application

    def prev_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print(f'This Screen has no function to the: {arg}')

    def sel_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print(f'This Screen has no function to the: {arg}')

    def next_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print(f'This Screen has no function to the: {arg}')

    def after_open(self):
        self.prev_btn.press_func(self.prev_screen, ('prev screen',))
        self.sel_btn.press_func(self.sel_screen,   ('sel screen',))
        self.next_btn.press_func(self.next_screen, ('next screen',))

hardware_setup.py

from machine import Pin, SPI
import machine
import gc
import time

from ssd1306 import SSD1306_I2C as SSD

WIDTH = const(128)
HEIGHT = const(64)

i2c = machine.SoftI2C(scl=machine.Pin(18), sda=machine.Pin(17))
gc.collect()  # Precaution before instantiating framebuf
ssd = SSD(WIDTH, HEIGHT, i2c)

from gui.core.ugui import Display

# Define control buttons
nxt = Pin(42, Pin.IN, Pin.PULL_UP)  # Move to next control
#nxt = Pin(10, Pin.IN, Pin.PULL_UP)  # Move to next control
sel = Pin(12, Pin.IN, Pin.PULL_UP)  # Operate current control
#sel = Pin(11, Pin.IN, Pin.PULL_UP)  # Operate current control
prev = Pin(41, Pin.IN, Pin.PULL_UP)  # Move to previous control
#prev = Pin(14, Pin.IN, Pin.PULL_UP)  # Move to previous control
display = Display(ssd, nxt, sel, prev)  # 3-button mode

Output: this is the same output that show in the video (attached):

$ mpremote run screen_main.py
Using 3 switches.
I am screen: Screen-1
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_2'>
I am screen: Screen-2
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_3'>
I am screen: Screen-3
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_1'>
I am screen: Screen-1
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_2'>
I am screen: Screen-2
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_3'>
I am screen: Screen-3
Pushbutton pressed: sel screen. Changing to the screen: <class 'BaseScreen_4'>
I am screen: Screen-4
Button pressed Yes
Your choose was *Yes*, so Changing to the screen: <class 'BaseScreen_1'>
I am screen: Screen-1
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_2'>
I am screen: Screen-2
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_3'>
I am screen: Screen-3
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_1'>
I am screen: Screen-1
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_2'>
I am screen: Screen-2
Pushbutton pressed: next screen. Changing to the screen: <class 'BaseScreen_3'>
I am screen: Screen-3
Pushbutton pressed: sel screen. Changing to the screen: <class 'BaseScreen_4'>
I am screen: Screen-4
Button pressed Yes
Your choose was *Yes*, so Changing to the screen: <class 'BaseScreen_1'>
I am screen: Screen-1

poc.tar.gz

https://github.com/peterhinch/micropython-micro-gui/assets/630637/c760edb1-e05a-44e3-a03c-d1fc2711c0a7

peterhinch commented 1 year ago

GitHub complains that the video file is corrupt.

I am very busy at the moment - I'm not sure when I'll find time to debug all that code.

beyonlo commented 1 year ago

Good morning @peterhinch :)

GitHub complains that the video file is corrupt.

That's strange, because I watched it directly from github after I posted the thread - I'm using Chromium. Anyway here is a new url for it (shared in the google drive)

I am very busy at the moment - I'm not sure when I'll find time to debug all that code.

All right, take your time! I did a simpler code as possible as prof of concept about the idea, and to not use much your time!

Thank you so much for all your help!

EDIT: is possible, maybe, for now, at least just to answer that Question Item? That will clarify much things about the design in this Proof of Concept about the idea!

peterhinch commented 1 year ago

The design assumes that screens are stacked. If screen A opens screen B, which opens screen C, the stack is ABC. If screen C is closed, screen B is visible and the stack is AB. When screen B is closed, only screen A exists. Visually each screen overlays the previous one, in the expectation that the top one will be closed revealing the one below.

This is fundamental to the design. If screen C opens another instance of A - carousel mode - you would end up with ABCA which would pave the way to ABCAB - ABCABC and so on. There would indeed be an ever growing number of instances. Supporting that would require a substantial redesign, a task that I don't intend to undertake. Perhaps there is a way where, when in screen C, on closure the underlying screen B also closes. This is way beyond anything I've tried, or anything any other user has requested.

I'm happy to answer queries on design, such as this one. I'm less happy when the answer is in the docs, which I've had cause to point out several times. My least favourite is being presented with pages of code and an invitation to fix it. This would involve setting up hardware and a substantial commitment of time. Time that I don't have.

beyonlo commented 1 year ago

Hi @peterhinch

This is fundamental to the design. If screen C opens another instance of A - carousel mode - you would end up with ABCA which would pave the way to ABCAB - ABCABC and so on. There would indeed be an ever growing number of instances. Supporting that would require a substantial redesign, a task that I don't intend to undertake. Perhaps there is a way where, when in screen C, on closure the underlying screen B also closes. This is way beyond anything I've tried, or anything any other user has requested.

To solve this problem I used this strategy: I have a main class that change to all Screens, so each Screen that user click on any pushbutton (prev, sel, next) or Buttons (Yes, No) I set that user option in a variable accessible by that main class and call the Screen.shutdown() and the control back to main class that will change to new Screen that user choose (from that variable). In this way I never will have a stack of Screens, and will never worry by the self.reg_task() because as say this doc about the Shutdown: "any tasks registered to the base screen are cancelled" I did some tests and works - using this approach also fixed that problem 2 as well. If you have some consideration about this approach, please, let me know

I'm less happy when the answer is in the docs, which I've had cause to point out several times. My least favourite is being presented with pages of code and an invitation to fix it. This would involve setting up hardware and a substantial commitment of time. Time that I don't have.

I totally agree. Apologize for that. Is my responsibility read the docs, also write the code and test it. Please, ignore my invite to check my code. I will contact you just if the docs do not exists or docs is not well understand by me, or a bug, otherwise you are correct, I need to do my home work. You already help so much about design asking, strategy, suggestion, examples, docs, new features, bug fix, etc!

Well, I have just one problem yet. When I use self.visible = False on CloseButton the pushbutton prev, sel, next do not works anymore, I mean, after I click in pushbutton next for example, everything congeals/stop. But I did a detailed debug and find exactly where it is stopping, and I would like your help how can be fixed that.

It stop in the launch /gui/primitives/__init__.py:

def launch(func, tup_args):
    print('----------- 1')
    print(func.__name__)
    print(*tup_args)
    res = func(*tup_args)
    print('----------- 2')
    if isinstance(res, type_coro):
        res = asyncio.create_task(res)
    return res

Test with self.visible = False:

$ mpremote run screen_main.py 
----------- 1
ctrl_move
1

Ps: Look that it stop/congeal in the res = func(*tup_args) because do not print the '----------- 2' I see that ctrl_move will move the pushbutton next. Maybe as the only active widget on the screen is the CloseButton, as it is invisible the pushbutton next can't go to that invisible widget and happen this bug.

Test with self.visible = True:

$ mpremote run screen_main.py 
----------- 1
ctrl_move
1
----------- 2
----------- 1
next_screen
next screen
----------- 2

Ps: this works ok

Thank you in advance!

peterhinch commented 1 year ago

Perhaps setting visible=False was not a good idea. You're going to have to experiment because I have never tried the case where a screen has no visible controls.

To create an invisible control you might like to try the Checkbox widget. I suggest this because it is extremely simple, in case you need to modify the code in checkbox.py. If you created a checkbox that was very small, and set its colors to match the screen background, perhaps this will fit the bill.

beyonlo commented 1 year ago

Hello @peterhinch

I did set all colors possible as BLACK, but still have a border in WHITE.

I tried this three ways: Checkbox(wri, row, col, height=1, fillcolor=BLACK, fgcolor=BLACK, bgcolor=BLACK, bdcolor=False) Checkbox(wri, row, col, height=1, fillcolor=BLACK, fgcolor=BLACK, bgcolor=BLACK, bdcolor=None) Checkbox(wri, row, col, height=1, fillcolor=BLACK, fgcolor=BLACK, bgcolor=BLACK, bdcolor=True)

but all cases still have a little WHITE border. I used all colors params possible that there is in the class Checkbox(Widget) on the file /gui/widgets/checkbox.py

The docs say that setting bdcolor=False no border will be drawn, but still exists a WHITE border.

So to get out that WHITE border I did set an arg 'invisible': Checkbox(wri, row, col, height=1, fillcolor=BLACK, fgcolor=BLACK, bgcolor=BLACK, bdcolor=False, args=('invisible',)) And added this code in the /gui/core/ugui.py in the def draw_border(self)

                if self.args:
                    if self.args[0] == 'invisible':
                        color = BLACK
    def draw_border(self):
        if self.screen is Screen.current_screen:
            dev = display.usegrey(self._greyed_out)
            x = self.col - 2
            y = self.row - 2
            w = self.width + 4
            h = self.height + 4
            # print('border', self, display.ipdev.is_adjust())
            if self.has_focus() and not isinstance(self, DummyWidget):
                color = color_map[FOCUS]
                if self.args:
                    if self.args[0] == 'invisible':
                        color = BLACK
$ diff ugui.py ugui.py.ORIG 
242c242
<     do_gc = False  # Allow user to take control of GC
---
>     do_gc = True  # Allow user to take control of GC
693,695d692
<                 if self.args:
<                     if self.args[0] == 'invisible':
<                         color = BLACK

With this change in the ugui.py works fine (checkbox is hide and the pushbuttons works), but I do not know if I'm doing the correct way, or if I missing something to set the correct BLACK colors to hide the checkbox widget.

I believe, maybe, that should be possible to set from the widgets the correct color (like as BLACK) to that line color = color_map[FOCUS] on the def draw_border(self)

Thank you!

peterhinch commented 1 year ago

The white border indicates that a control has the focus. On a screen with multiple active controls, the white border moves when you press NEXT and PREV pushbuttons. This white border is not the same as the border created by draw_border() which is permanently attached to the control. For example, in the Label widget, a text string can be drawn with or without a border.

Only active controls can take the focus. Because you have only one active control it always has the focus - there is nowhere else for the focus to go. The color of this border is set here at offset 0 into the list (FOCUS in line 46). You might like to try changing this from WHITE to BLACK.

beyonlo commented 1 year ago

Hi @peterhinch

Only active controls can take the focus. Because you have only one active control it always has the focus - there is nowhere else for the focus to go. The color of this border is set here at offset 0 into the list (FOCUS in line 46). You might like to try changing this from WHITE to BLACK.

I saw that in the colors docs too, and I already tried that, and works, but problem that I have some Screens that has active buttons, exactly as the simple.py, to user choose Yes or No to Enable/Disable some feature. And if I change that color_map on the position 0 do BLACK, all Screens with active widgets will not be possible to user see the focus - because now is BLACK. But using that way that I pasted to you, only widgets that I pass as argument 'invisible' will have Focus BLACK color, otherwise will be the standard WHILE color. If you have some more idea, please, let me know :)

I have a bigger problem with this design that is when I have a simple.py Screen - that has active buttons. I do not put the "invisible" Checkbox in that Screens and I remove the CloseButton too. So, think about the simple.py just without the CloseButton, that when this Screen is opened, I set in the after_open() all my pushbutton objects prev, sel and next to a callback that will not change to new Screen, because I want that when user are inside of Screen simple.py user can to change of Screen only when user choose Yes or No. Problem is when I change to the simple.py Screen, as the focus are by default in the Yes Button, magically the Yes Button is pressed. In the hardware_setup.py the prev, sel, next are using the same pins used in the my pushbuttons objects prev, sel and next. So when is a Screen that do not has active widgets, the pushbuttons objects are functional and is possible to Change the Screens. But when is changed to a Screen that has active widgets like as the simple.py, I set in the after_open() a callback that not exists to my pushbutton objects, so in this Screen the hardware_setup.py prev, sel, next automatically woks on the active widgets, moving (prev and next) between the Buttons and selecting (sel) it. Well, I'm supposing that problem can be that my pushbuttons objects are interfering with the simple.py Screen Buttons at the moment that I change to this screen, like as the Button Yes (as it has the focus by default) still is recognized the pin as pressed and magically press the Yes Button. Any idea how to solve that?

I did a horrible and very ugly thing to try to fix this problem. In the def my_callback(button, arg): of simple.py I check the diff time between when the def after_open(self): is executed and when the def my_callback(button, arg): is executed. If that time is less than 1 second (by guarantee), means that the Yes button was pressed "magically" and it is ignored.

        def my_callback(button, arg):
            print('Button pressed', arg)
            end_time = time.ticks_ms()
            delta_time = time.ticks_diff(end_time, self.start_time)
            if delta_time > 1000: # 1 second by guarantee?
                if arg == 'Yes':
                    self.env.change_to_screen = 'ScreenTempValues'
                    Screen.shutdown()
                elif arg == 'No':
                    self.env.change_to_screen = 'ScreenResetAlPtSel'
                    Screen.shutdown()

    def prev_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print('Do nothing')

    def sel_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print('Do nothing')

    def next_screen(self, arg):
        print(f'Pushbutton pressed: {arg}')
        print('Do nothing')

    def after_open(self):
        self.prev_btn.press_func(self.prev_screen, ('prev screen',))
        self.sel_btn.press_func(self.sel_screen,   ('sel screen',))
        self.next_btn.press_func(self.next_screen, ('next screen',))
        self.start_time = time.ticks_ms()

Do you have a better way to solve that?

Thank you so much!

beyonlo commented 1 year ago

@peterhinch I'm sorry, I just realized now that I can to change the color_map[FOCUS] in each Screen at the real time, so yes, I using now the color_map[FOCUS] = BLACK on the Screens that do not have active widgets (just that invisible Checkbox) and using color_map[FOCUS] = WHITE on the Screens with active widgets like as the simple.py

All are working perfect, except that problem when I change do Screen simple.py, where the Yes Button is pressed automatically/magically. I did more research and tests, but I still not figure out how to solve that design. If you have any idea about that, please, let me know and I can to test/implement. I think that fixing this last one problem, maybe you can to put in the docs that new users that want to use micro-gui in the tiny Display using this idea is excellent. I can to share a very simple code as start point for people that want to use this idea!

Thank you so much

Edit: I'm almost sure that Yes Button is pressed automatically because when the my pushbutton object sel select to change to Screen simple.py, when the Screen simple.py loads, the pin sel pushbutton object still is pressed and as the Yes Button has the focus, and the sel of the hardware_setup.py use the same pin of my pushbutton object sel, so the Yes Button is pressed. Well, do you have any idea how to solve this problem?

peterhinch commented 1 year ago

I can to change the color_map[FOCUS] in each Screen at the real time

That was what I had in mind. Sorry if I didn't make this clear.

Re pushbutton, screen controls such as Button instances are activated on pushbutton release (code). This is done to cater for the case where (say) the close button on a screen is pressed. Typically the close button on the screen below would be in the same place. If buttons operated on pushbutton press, all screens below would close and the application would terminate.

To fit this paradigm you want the action which closes an overlaying screen to take place on pushbutton release.

beyonlo commented 1 year ago

Hi @peterhinch

Re pushbutton, screen controls such as Button instances are activated on pushbutton release (code). This is done to cater for the case where (say) the close button on a screen is pressed. Typically the close button on the screen below would be in the same place. If buttons operated on pushbutton press, all screens below would close and the application would terminate.

I understood perfectly well, thanks for that explanation.

To fit this paradigm you want the action which closes an overlaying screen to take place on pushbutton release.

Now I changed to do exactly that: now the simple.py Screen is changed just on the pushbutton release self.sel_btn.release_func() from the previous Screen. But that do not solved the problem - still pressing automatically the Yes Button on the simple.py. Maybe is that still happening because I have two pushbutton sel on the same pin, and the hardware_setup_.py sel pushbutton still recognizing that pin released with a delay?

beyonlo commented 1 year ago

Hello @peterhinch

I did a test to check if the pin still pressed just before to change to the simple.py Screen. I put a print(sel_pin.value()) on the pushbutton release callback on the previous Screen at the simple.py Screen. I can to confirm that the pin used by my pushbutton sel and hardware_setup.py pushbutton sel is NOT pressed anymore. So, how is possible when change to the simple.py Screen it is pressing automatically the Yes Button?

Previous Screen of simple.py:

    def release_sel_screen(self, arg):
        print(f'Pushbutton released: {arg}')
        self.env.change_to_screen = 'simple_screen'
        print(sel_pin.value())
        Screen.shutdown()

    def after_open(self):
        self.sel_btn.release_func(self.release_sel_screen,   ('sel screen - release',))

After that shutdown(), the control back to main_screen() function that change to Screen passed to the self.env.change_to_screen variable - that is the simple.py Screen

beyonlo commented 1 year ago

Hello Peter

I did read many docs and many tests but unfortunately I yet do not found how to solve that problem that still pressing automatically the Yes Button on the simple.py even that the previous Screen is changing to the simple.py just on the pushbutton release

The only ugly work around that I found to try to solve that is checking if Yes and No Button on the simple.py was pressed after a time (using time.ticks_diff), like as 1200ms, as follow. Below is just the relevant code:

class BaseScreen(Screen):
    def __init__(self):
        self.start_time     = None

        def my_callback(button, arg):
            end_time = time.ticks_ms()
            delta_time = time.ticks_diff(end_time, self.start_time)
            if delta_time > 1200:
                if arg == 'yes':
                    self.env.change_to_screen = 'ScreenTempValues'
                elif arg == 'no':
                    self.env.change_to_screen = 'ScreenResetAlPtSel'
                Screen.shutdown()
            else:
                print('Button pressed automatically')

    def after_open(self):
        self.start_time = time.ticks_ms()

Can that be a Bug?

Please, if you have any idea how to solve that, or any suggestion, I will appreciate!

Thank you