Add Screen replace non-tree navigation.

main
Peter Hinch 2024-04-06 17:15:01 +01:00
rodzic b937d88e13
commit 96c7bda61f
3 zmienionych plików z 152 dodań i 32 usunięć

Wyświetl plik

@ -39,7 +39,7 @@ non-touch solution avoids the need for calibration and can also save cost. Cheap
Chinese touch displays often marry a good display to a poor touch overlay. It
can make sense to use such a screen with micro-gui, ignoring the touch overlay.
For touch support it is worth spending money on a good quality device (for
example Adafruit).
example Adafruit).
The micro-gui input options work well and can yield inexpensive solutions. A
network-connected board with a 135x240 color display can be built for under £20
@ -65,6 +65,7 @@ target and a C device driver (unless you can acquire a suitable binary).
# Project status
April 2024: Add screen replace feature for non-tree navigation.
Sept 2023: Add "encoder only" mode suggested by @eudoxos.
April 2023: Add limited ePaper support, grid widget, calendar and epaper demos.
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
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
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.
* `screen_change.py` A `Pushbutton` causing a screen change using a re-usable
"forward" button.
* `screen_replace.py` A more complex (non-tree) screen layout.
* `primitives.py` Use of graphics primitives.
* `aclock.py` An analog clock using the `Dial` vector display. Also shows
screen layout using widget metrics. Has a simple `asyncio` task.
@ -977,14 +984,23 @@ communication between them.
## 4.1 Class methods
In normal use the following methods only are required:
* `change(cls, cls_new_screen, *, forward=True, args=[], kwargs={})` Change
screen, refreshing the display. Mandatory positional argument: the new screen
class name. This must be a class subclassed from `Screen`. The class 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.
* `back(cls)` Restore previous screen.
In normal use only `change` and `back` are required, to move to a new `Screen`
and to drop back to the previous `Screen` in a tree (or to quit the application
if there is no predecessor).
* `change(cls, cls_new_screen, mode=Screen.STACK, *, args=[], kwargs={})`
Change screen, refreshing the display. Mandatory positional argument: the new
screen class name. This must be a class subclassed from `Screen`. The class
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:
* `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
* `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
tasks. If a screen runs independent tasks it can opt to register these. If the

Wyświetl plik

@ -309,6 +309,9 @@ class Screen:
# to a realtime process.
rfsh_start = asyncio.Event() # Refresh pauses until set (set by default).
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
def redraw_co(cls):
@ -351,39 +354,41 @@ class Screen:
obj.show()
@classmethod
def change(cls, cls_new_screen, *, forward=True, args=[], kwargs={}):
cs_old = cls.current_screen
def change(cls, cls_new_screen, mode=1, *, args=[], kwargs={}):
ins_old = cls.current_screen
# If initialising ensure there is an event loop before instantiating the
# first Screen: it may create tasks in the constructor.
if cs_old is None:
if ins_old is None:
loop = asyncio.get_event_loop()
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.
if entry[1] or not forward:
if entry[1] or not mode:
entry[0].cancel()
cls.current_screen.tasks.remove(entry) # remove from list
cs_old.on_hide() # Optional method in subclass
if forward:
ins_old.tasks.remove(entry) # remove from list
ins_old.on_hide() # Optional method in subclass
if mode:
if isinstance(cls_new_screen, type):
if isinstance(cs_old, Window):
if isinstance(ins_old, Window):
raise ValueError("Windows are modal.")
new_screen = cls_new_screen(*args, **kwargs)
if not len(new_screen.lstactive):
if mode == cls.REPLACE and isinstance(cls_new_screen, Window):
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.")
else:
raise ValueError("Must pass Screen class or subclass (not instance)")
new_screen.parent = cs_old
cs_new = new_screen
# REPLACE: parent of new screen is parent of current screen
ins_new.parent = ins_old if mode == cls.STACK else ins_old.parent
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
cls.current_screen = cs_new
cs_new.on_open() # Optional subclass method
cs_new._do_open(cs_old) # Clear and redraw
cs_new.after_open() # Optional subclass method
if cs_old is None: # Initialising
loop.run_until_complete(Screen.monitor()) # Starts and ends uasyncio
cls.current_screen = ins_new
ins_new.on_open() # Optional subclass method
ins_new._do_open(ins_old) # Clear and redraw
ins_new.after_open() # Optional subclass method
if ins_old is None: # Initialising
loop.run_until_complete(cls.monitor()) # Starts and ends uasyncio
# Don't do asyncio.new_event_loop() as it prevents re-running
# the same app.
@ -433,7 +438,7 @@ class Screen:
if parent is None: # Closing base screen. Quit.
cls.shutdown()
else:
cls.change(parent, forward=False)
cls.change(parent, cls.BACK)
@classmethod
def addobject(cls, obj):
@ -568,6 +573,7 @@ class Screen:
if isinstance(task, type_coro):
task = asyncio.create_task(task)
self.tasks.append((task, on_change))
return task
async def _garbage_collect(self):
n = 0

Wyświetl plik

@ -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()