kopia lustrzana https://github.com/peterhinch/micropython-micro-gui
Add Screen replace non-tree navigation.
rodzic
b937d88e13
commit
96c7bda61f
38
README.md
38
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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