kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
Add Screen replace non-tree navigation.
rodzic
b937d88e13
commit
96c7bda61f
36
README.md
36
README.md
|
@ -65,6 +65,7 @@ target and a C device driver (unless you can acquire a suitable binary).
|
||||||
|
|
||||||
# Project status
|
# Project status
|
||||||
|
|
||||||
|
April 2024: Add screen replace feature for non-tree navigation.
|
||||||
Sept 2023: Add "encoder only" mode suggested by @eudoxos.
|
Sept 2023: Add "encoder only" mode suggested by @eudoxos.
|
||||||
April 2023: Add limited ePaper support, grid widget, calendar and epaper demos.
|
April 2023: Add limited ePaper support, grid widget, calendar and epaper demos.
|
||||||
Now requires firmware >= V1.20.
|
Now requires firmware >= V1.20.
|
||||||
|
@ -233,7 +234,12 @@ conventions within graphs.
|
||||||
|
|
||||||
A `Screen` is a window which occupies the entire display. A `Screen` can
|
A `Screen` is a window which occupies the entire display. A `Screen` can
|
||||||
overlay another, replacing all its contents. When closed, the `Screen` below is
|
overlay another, replacing all its contents. When closed, the `Screen` below is
|
||||||
re-displayed.
|
re-displayed. This default method of navigation results in a tree structure of
|
||||||
|
`Screen` instances where the screen below retains state. An alternative allows
|
||||||
|
a `Screen` to replace another, allowing `Screen` instances to be navigated in an
|
||||||
|
arbitrary way. For example a set of `Screen` instances might be navigated in a
|
||||||
|
circular fashion. The penalty is that, to save RAM, state is not retained when a
|
||||||
|
`Screen` is replaced
|
||||||
|
|
||||||
A `Window` is a subclass of `Screen` but is smaller, with size and location
|
A `Window` is a subclass of `Screen` but is smaller, with size and location
|
||||||
attributes. It can overlay part of an underlying `Screen` and is typically used
|
attributes. It can overlay part of an underlying `Screen` and is typically used
|
||||||
|
@ -643,6 +649,7 @@ minimal and aim to demonstrate a single technique.
|
||||||
* `dialog.py` `DialogBox` demo. Illustrates the screen change mechanism.
|
* `dialog.py` `DialogBox` demo. Illustrates the screen change mechanism.
|
||||||
* `screen_change.py` A `Pushbutton` causing a screen change using a re-usable
|
* `screen_change.py` A `Pushbutton` causing a screen change using a re-usable
|
||||||
"forward" button.
|
"forward" button.
|
||||||
|
* `screen_replace.py` A more complex (non-tree) screen layout.
|
||||||
* `primitives.py` Use of graphics primitives.
|
* `primitives.py` Use of graphics primitives.
|
||||||
* `aclock.py` An analog clock using the `Dial` vector display. Also shows
|
* `aclock.py` An analog clock using the `Dial` vector display. Also shows
|
||||||
screen layout using widget metrics. Has a simple `asyncio` task.
|
screen layout using widget metrics. Has a simple `asyncio` task.
|
||||||
|
@ -977,14 +984,23 @@ communication between them.
|
||||||
|
|
||||||
## 4.1 Class methods
|
## 4.1 Class methods
|
||||||
|
|
||||||
In normal use the following methods only are required:
|
In normal use only `change` and `back` are required, to move to a new `Screen`
|
||||||
* `change(cls, cls_new_screen, *, forward=True, args=[], kwargs={})` Change
|
and to drop back to the previous `Screen` in a tree (or to quit the application
|
||||||
screen, refreshing the display. Mandatory positional argument: the new screen
|
if there is no predecessor).
|
||||||
class name. This must be a class subclassed from `Screen`. The class will be
|
|
||||||
instantiated and displayed. Optional keyword arguments `args`, `kwargs` enable
|
* `change(cls, cls_new_screen, mode=Screen.STACK, *, args=[], kwargs={})`
|
||||||
passing positional and keyword arguments to the constructor of the new, user
|
Change screen, refreshing the display. Mandatory positional argument: the new
|
||||||
defined, screen.
|
screen class name. This must be a class subclassed from `Screen`. The class
|
||||||
* `back(cls)` Restore previous screen.
|
will be instantiated and displayed. Optional keyword arguments `args`, `kwargs`
|
||||||
|
enable passing positional and keyword arguments to the constructor of the new,
|
||||||
|
user defined, screen. By default the new screen overlays the old. When the new
|
||||||
|
`Screen` is closed (via `back`) the old is re-displayed having retained state.
|
||||||
|
If `mode=Screen.REPLACE` is passed the old screen instance is deleted. The new
|
||||||
|
one retains the parent of the old, so if it is closed that parent is
|
||||||
|
re-displayed with its state retained. This enables arbitrary navigation between
|
||||||
|
screens (directed graph rather than tree structure). See demo `screen_replace`.
|
||||||
|
* `back(cls)` Restore previous screen. If there is no parent, quits the
|
||||||
|
application.
|
||||||
|
|
||||||
These are uncommon:
|
These are uncommon:
|
||||||
* `shutdown(cls)` Clear the screen and shut down the GUI. Normally done by a
|
* `shutdown(cls)` Clear the screen and shut down the GUI. Normally done by a
|
||||||
|
@ -1015,7 +1031,7 @@ See `demos/plot.py` for examples of usage of `after_open`.
|
||||||
## 4.4 Method
|
## 4.4 Method
|
||||||
|
|
||||||
* `reg_task(self, task, on_change=False)` The first arg may be a `Task`
|
* `reg_task(self, task, on_change=False)` The first arg may be a `Task`
|
||||||
instance or a coroutine.
|
instance or a coroutine. Returns the passed `task` object.
|
||||||
|
|
||||||
This is a convenience method which provides for the automatic cancellation of
|
This is a convenience method which provides for the automatic cancellation of
|
||||||
tasks. If a screen runs independent tasks it can opt to register these. If the
|
tasks. If a screen runs independent tasks it can opt to register these. If the
|
||||||
|
|
|
@ -309,6 +309,9 @@ class Screen:
|
||||||
# to a realtime process.
|
# to a realtime process.
|
||||||
rfsh_start = asyncio.Event() # Refresh pauses until set (set by default).
|
rfsh_start = asyncio.Event() # Refresh pauses until set (set by default).
|
||||||
rfsh_done = asyncio.Event() # Flag a user task that a refresh was done.
|
rfsh_done = asyncio.Event() # Flag a user task that a refresh was done.
|
||||||
|
BACK = 0
|
||||||
|
STACK = 1
|
||||||
|
REPLACE = 2
|
||||||
|
|
||||||
@classmethod # Called by Input when status change needs redraw of current obj
|
@classmethod # Called by Input when status change needs redraw of current obj
|
||||||
def redraw_co(cls):
|
def redraw_co(cls):
|
||||||
|
@ -351,39 +354,41 @@ class Screen:
|
||||||
obj.show()
|
obj.show()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def change(cls, cls_new_screen, *, forward=True, args=[], kwargs={}):
|
def change(cls, cls_new_screen, mode=1, *, args=[], kwargs={}):
|
||||||
cs_old = cls.current_screen
|
ins_old = cls.current_screen
|
||||||
# If initialising ensure there is an event loop before instantiating the
|
# If initialising ensure there is an event loop before instantiating the
|
||||||
# first Screen: it may create tasks in the constructor.
|
# first Screen: it may create tasks in the constructor.
|
||||||
if cs_old is None:
|
if ins_old is None:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
else: # Leaving an existing screen
|
else: # Leaving an existing screen
|
||||||
for entry in cls.current_screen.tasks:
|
for entry in ins_old.tasks:
|
||||||
# Always cancel on back. Also on forward if requested.
|
# Always cancel on back. Also on forward if requested.
|
||||||
if entry[1] or not forward:
|
if entry[1] or not mode:
|
||||||
entry[0].cancel()
|
entry[0].cancel()
|
||||||
cls.current_screen.tasks.remove(entry) # remove from list
|
ins_old.tasks.remove(entry) # remove from list
|
||||||
cs_old.on_hide() # Optional method in subclass
|
ins_old.on_hide() # Optional method in subclass
|
||||||
if forward:
|
if mode:
|
||||||
if isinstance(cls_new_screen, type):
|
if isinstance(cls_new_screen, type):
|
||||||
if isinstance(cs_old, Window):
|
if isinstance(ins_old, Window):
|
||||||
raise ValueError("Windows are modal.")
|
raise ValueError("Windows are modal.")
|
||||||
new_screen = cls_new_screen(*args, **kwargs)
|
if mode == cls.REPLACE and isinstance(cls_new_screen, Window):
|
||||||
if not len(new_screen.lstactive):
|
raise ValueError("Windows must be stacked.")
|
||||||
|
ins_new = cls_new_screen(*args, **kwargs)
|
||||||
|
if not len(ins_new.lstactive):
|
||||||
raise ValueError("Screen has no active widgets.")
|
raise ValueError("Screen has no active widgets.")
|
||||||
else:
|
else:
|
||||||
raise ValueError("Must pass Screen class or subclass (not instance)")
|
raise ValueError("Must pass Screen class or subclass (not instance)")
|
||||||
new_screen.parent = cs_old
|
# REPLACE: parent of new screen is parent of current screen
|
||||||
cs_new = new_screen
|
ins_new.parent = ins_old if mode == cls.STACK else ins_old.parent
|
||||||
else:
|
else:
|
||||||
cs_new = cls_new_screen # An object, not a class
|
ins_new = cls_new_screen # cls_new_screen is an object, not a class
|
||||||
display.ipdev.adj_mode(False) # Ensure normal mode
|
display.ipdev.adj_mode(False) # Ensure normal mode
|
||||||
cls.current_screen = cs_new
|
cls.current_screen = ins_new
|
||||||
cs_new.on_open() # Optional subclass method
|
ins_new.on_open() # Optional subclass method
|
||||||
cs_new._do_open(cs_old) # Clear and redraw
|
ins_new._do_open(ins_old) # Clear and redraw
|
||||||
cs_new.after_open() # Optional subclass method
|
ins_new.after_open() # Optional subclass method
|
||||||
if cs_old is None: # Initialising
|
if ins_old is None: # Initialising
|
||||||
loop.run_until_complete(Screen.monitor()) # Starts and ends uasyncio
|
loop.run_until_complete(cls.monitor()) # Starts and ends uasyncio
|
||||||
# Don't do asyncio.new_event_loop() as it prevents re-running
|
# Don't do asyncio.new_event_loop() as it prevents re-running
|
||||||
# the same app.
|
# the same app.
|
||||||
|
|
||||||
|
@ -433,7 +438,7 @@ class Screen:
|
||||||
if parent is None: # Closing base screen. Quit.
|
if parent is None: # Closing base screen. Quit.
|
||||||
cls.shutdown()
|
cls.shutdown()
|
||||||
else:
|
else:
|
||||||
cls.change(parent, forward=False)
|
cls.change(parent, cls.BACK)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def addobject(cls, obj):
|
def addobject(cls, obj):
|
||||||
|
@ -568,6 +573,7 @@ class Screen:
|
||||||
if isinstance(task, type_coro):
|
if isinstance(task, type_coro):
|
||||||
task = asyncio.create_task(task)
|
task = asyncio.create_task(task)
|
||||||
self.tasks.append((task, on_change))
|
self.tasks.append((task, on_change))
|
||||||
|
return task
|
||||||
|
|
||||||
async def _garbage_collect(self):
|
async def _garbage_collect(self):
|
||||||
n = 0
|
n = 0
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
# screen_replace.py deemo showing non-stacked screens
|
||||||
|
|
||||||
|
# Released under the MIT License (MIT). See LICENSE.
|
||||||
|
# Copyright (c) 2024 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 Button, CloseButton, Label
|
||||||
|
from gui.core.writer import CWriter
|
||||||
|
|
||||||
|
# Font for CWriter
|
||||||
|
import gui.fonts.freesans20 as font
|
||||||
|
from gui.core.colors import *
|
||||||
|
|
||||||
|
wri = CWriter(ssd, font, GREEN, BLACK, verbose=False)
|
||||||
|
|
||||||
|
# Defining a button in this way enables it to be re-used on
|
||||||
|
# multiple Screen instances. Note that a Screen class is
|
||||||
|
# passed, not an instance.
|
||||||
|
def fwdbutton(wri, row, col, cls_screen, text="Next", args=()):
|
||||||
|
def fwd(button):
|
||||||
|
Screen.change(cls_screen, args=args) # Callback
|
||||||
|
|
||||||
|
Button(wri, row, col, height=30, callback=fwd, fgcolor=BLACK, bgcolor=GREEN, text=text)
|
||||||
|
|
||||||
|
|
||||||
|
def navbutton(wri, row, col, gen, delta, text):
|
||||||
|
def nav(button):
|
||||||
|
cls_screen, num = gen(delta) # gen.send(delta)
|
||||||
|
Screen.change(cls_screen, mode=Screen.REPLACE, args=(num,)) # Callback
|
||||||
|
|
||||||
|
Button(wri, row, col, height=30, callback=nav, fgcolor=BLACK, bgcolor=YELLOW, text=text)
|
||||||
|
|
||||||
|
|
||||||
|
class RingScreen(Screen):
|
||||||
|
def __init__(self, num):
|
||||||
|
super().__init__()
|
||||||
|
Label(wri, 2, 2, f"Ring screen no. {num}.")
|
||||||
|
navbutton(wri, 40, 2, nav, -1, "Left")
|
||||||
|
navbutton(wri, 40, 80, nav, 1, "Right")
|
||||||
|
CloseButton(wri)
|
||||||
|
|
||||||
|
|
||||||
|
# Create a tuple of Screen subclasses (in this case all are identical).
|
||||||
|
ring = ((RingScreen, 0), (RingScreen, 1), (RingScreen, 2))
|
||||||
|
|
||||||
|
# Define a means of navigating between these classes
|
||||||
|
def navigator():
|
||||||
|
x = 0
|
||||||
|
|
||||||
|
def nav(delta):
|
||||||
|
nonlocal x
|
||||||
|
v = x
|
||||||
|
x = (x + delta) % len(ring)
|
||||||
|
return ring[x]
|
||||||
|
|
||||||
|
return nav
|
||||||
|
|
||||||
|
|
||||||
|
nav = navigator()
|
||||||
|
|
||||||
|
# This screen overlays BaseScreen.
|
||||||
|
class StackScreen(Screen):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
Label(wri, 2, 2, "Stacked screen.")
|
||||||
|
fwdbutton(wri, 40, 2, RingScreen, args=(0,))
|
||||||
|
CloseButton(wri)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseScreen(Screen):
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
Label(wri, 2, 2, "Base screen.")
|
||||||
|
fwdbutton(wri, 40, 2, StackScreen)
|
||||||
|
CloseButton(wri)
|
||||||
|
|
||||||
|
|
||||||
|
s = """
|
||||||
|
Demo of screen replace. Screen hierarchy:
|
||||||
|
|
||||||
|
Base Screen
|
||||||
|
|
|
||||||
|
Stacked Screen
|
||||||
|
|
|
||||||
|
<- Ring Screen 0 <-> Ring Screen 1 <-> Ring Screen 2 ->
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test():
|
||||||
|
print(s)
|
||||||
|
Screen.change(BaseScreen) # Pass class, not instance!
|
||||||
|
|
||||||
|
|
||||||
|
test()
|
Ładowanie…
Reference in New Issue