diff --git a/README.md b/README.md index abdb0fa..3ca1dba 100644 --- a/README.md +++ b/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 diff --git a/gui/core/ugui.py b/gui/core/ugui.py index d65c417..63957a3 100644 --- a/gui/core/ugui.py +++ b/gui/core/ugui.py @@ -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 diff --git a/gui/demos/screen_replace.py b/gui/demos/screen_replace.py new file mode 100644 index 0000000..483a026 --- /dev/null +++ b/gui/demos/screen_replace.py @@ -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()