diff --git a/docs/badge/application-programming.rst b/docs/badge/application-programming.rst index 7745c246feb59c2bcbc1a9401c490379c0f2d192..785d599a651b996eb89342c726f6f1f4a291557d 100644 --- a/docs/badge/application-programming.rst +++ b/docs/badge/application-programming.rst @@ -79,9 +79,9 @@ Example 1b: React to input -------------------------- If we want to react to the user, we can use the :py:class:`InputState` which got -handed to us. In this example we look at the state of the right shoulder button. -The values contained in the input state are the same as used by the -:py:mod:`hardware` module. +handed to us. In this example we look at the state of the app (by default left) +shoulder button. The values contained in the input state are the same as used +by the :py:mod:`hardware` module. .. code-block:: python @@ -101,7 +101,7 @@ The values contained in the input state are the same as used by the ctx.rgb(255, 0, 0).rectangle(self._x, -20, 40, 40).fill() def think(self, ins: InputState, delta_ms: int) -> None: - direction = ins.left_button + direction = ins.buttons.app if direction == BUTTON_PRESSED_LEFT: self._x -= 1 @@ -111,7 +111,8 @@ The values contained in the input state are the same as used by the st3m.run.run_responder(Example()) -Try it: when you run this code, you can move the red square using the left shoulder button. +Try it: when you run this code, you can move the red square using the app (by +default left) shoulder button. Example 1c: Taking time into consideration @@ -142,7 +143,7 @@ represents the time which has passed since the last call to `think()`. ctx.rgb(255, 0, 0).rectangle(self._x, -20, 40, 40).fill() def think(self, ins: InputState, delta_ms: int) -> None: - direction = ins.left_button # -1 (left), 1 (right), or 2 (pressed) + direction = ins.buttons.app # -1 (left), 1 (right), or 2 (pressed) if direction == BUTTON_PRESSED_LEFT: self._x -= 20 * delta_ms / 1000 @@ -165,6 +166,41 @@ Working on the bare state of the buttons and the captouch petals can be cumberso the flow3r application framework gives you a bit of help in the form of the :py:class:`InputController` which processes an input state and gives you higher level information about what is happening. +The `InputController` contains multiple :py:class:`Pressable` sub-objects, for +example the app/OS buttons are available as following attributes on the +`InputController`: + ++-----------------------------------+--------------------------+ +| Attribute on ``InputControlller`` | Meaning | ++===================================+==========================+ +| ``.buttons.app.left`` | App button, pushed left | ++-----------------------------------+--------------------------+ +| ``.buttons.app.middle`` | App button, pushed down | ++-----------------------------------+--------------------------+ +| ``.buttons.app.right`` | App button, pushed right | ++-----------------------------------+--------------------------+ +| ``.buttons.os.left`` | OS button, pushed left | ++-----------------------------------+--------------------------+ +| ``.buttons.os.middle`` | OS button, pushed down | ++-----------------------------------+--------------------------+ +| ``.buttons.os.right`` | OS button, pushed right | ++-----------------------------------+--------------------------+ + +And each `Pressable` in turn contains the following attributes, all of which are +valid within the context of a single `think()` call: + ++----------------------------+--------------------------------------------------------------------+ +| Attribute on ``Pressable`` | Meaning | ++============================+====================================================================+ +| ``.pressed`` | Button has just started being pressed, ie. it's a Half Press down. | ++----------------------------+--------------------------------------------------------------------+ +| ``.down`` | Button is being held down. | ++----------------------------+--------------------------------------------------------------------+ +| ``.released`` | Button has just stopped being pressed, ie. it's a Half Press up. | ++----------------------------+--------------------------------------------------------------------+ +| ``.up`` | Button is not being held down. | ++----------------------------+--------------------------------------------------------------------+ + The following example shows how to properly react to single button presses without having to think about what happens if the user presses the button for a long time. It uses the `InputController` to detect single button presses and switches between showing a circle (by drawing a 360 deg arc) and @@ -199,14 +235,12 @@ a square. def think(self, ins: InputState, delta_ms: int) -> None: self.input.think(ins, delta_ms) # let the input controller to its magic - direction = ins.left_button # -1 (left), 1 (right), or 2 (pressed) - - if self.input.right_shoulder.middle.pressed: + if self.input.buttons.app.middle.pressed: self._draw_rectangle = not self._draw_rectangle - if direction == BUTTON_PRESSED_LEFT: + if self.input.buttons.app.left.pressed: self._x -= 20 * delta_ms / 1000 - elif direction == BUTTON_PRESSED_RIGHT: + elif self.input.buttons.app.right.pressed: self._x += 40 * delta_ms / 1000 @@ -222,8 +256,9 @@ With just the Responder class this can become a bit tricky as it never knows whe when it is not. It also doesn't directly allow you to launch a new screen. To help you with that you can use a :py:class:`View` instead. It can tell you when -it becomes visible and you can also use it to bring a new screen or widget into the foreground or remove -it again from the screen. +it becomes visible, when it is about to become inactive (invisible) and you can +also use it to bring a new screen or widget into the foreground or remove it +again from the screen. Example 2a: Managing two views -------------------------------- @@ -259,8 +294,8 @@ into the two different views. We make use of an `InputController` again to handl def think(self, ins: InputState, delta_ms: int) -> None: self.input.think(ins, delta_ms) # let the input controller to its magic - if self.input.right_shoulder.middle.pressed: - self._vm.pop() + # No need to handle returning back to Example on button press - the + # flow3r's ViewManager takes care of that automatically. class Example(View): @@ -277,11 +312,12 @@ into the two different views. We make use of an `InputController` again to handl def on_enter(self, vm: Optional[ViewManager]) -> None: self._vm = vm + self.input._ignore_pressed() def think(self, ins: InputState, delta_ms: int) -> None: self.input.think(ins, delta_ms) # let the input controller to its magic - if self.input.right_shoulder.middle.pressed: + if self.input.buttons.app.middle.pressed: self._vm.push(SecondScreen()) st3m.run.run_view(Example()) @@ -293,9 +329,12 @@ the still pressed button immediately closes `SecondScreen` we make us of a speci Example 2b: Easier view management ---------------------------------- -The idea that a button (physical or captouch) is used to enter / exit a view is so universal that -there is a special view which helps you with that: :py:class:`BaseView`. It integrates an -`InputController` and handles the ignoring of extra presses: +The above code is so universal that we provide a special view which takes care +of this boilerplate: :py:class:`BaseView`. It integrated a local +`InputController` on ``self.input`` and a copy of the :py:class:`ViewManager` +which caused the View to enter on ``self.vm``. + +Here is our previous example rewritten to make use of `BaseView`: .. code-block:: python @@ -304,10 +343,14 @@ there is a special view which helps you with that: :py:class:`BaseView`. It inte class SecondScreen(BaseView): def __init__(self) -> None: + # Remember to call super().__init__() if you implement your own + # constructor! super().__init__() def on_enter(self, vm: Optional[ViewManager]) -> None: - super().on_enter(vm) # Let BaseView do its thing + # Remember to call super().on_enter() if you implement your own + # on_enter! + super().on_enter(vm) def draw(self, ctx: Context) -> None: # Paint the background black @@ -315,27 +358,13 @@ there is a special view which helps you with that: :py:class:`BaseView`. It inte # Green square ctx.rgb(0, 255, 0).rectangle(-20, -20, 40, 40).fill() - def think(self, ins: InputState, delta_ms: int) -> None: - super().think(ins, delta_ms) # Let BaseView do its thing - - if self.input.right_shoulder.middle.pressed: - self.vm.pop() - - class Example(BaseView): - def __init__(self) -> None: - super().__init__() - def draw(self, ctx: Context) -> None: # Paint the background black ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill() # Red square ctx.rgb(255, 0, 0).rectangle(-20, -20, 40, 40).fill() - - def on_enter(self, vm: Optional[ViewManager]) -> None: - super().on_enter(vm) # Let BaseView do its thing - def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) # Let BaseView do its thing @@ -353,7 +382,11 @@ All fine and good, you were able to write an application that you can run with ` but certainly you also want to run it from flow3r's menu system. Let's introduce the final class you should actually be using for application development: -:py:class:`Application` (yeah, right). +:py:class:`Application`. It builds upon `BaseView` (so you still have access to +`self.input` and `self.vm`) but additionally is made aware of an +:py:class:`ApplicationContext` on startup and can be registered into a menu. + +Here is our previous code changed to use `Application` for the base of its main view: .. code-block:: python @@ -366,28 +399,16 @@ Let's introduce the final class you should actually be using for application dev from typing import Optional class SecondScreen(BaseView): - def __init__(self) -> None: - super().__init__() - - def on_enter(self, vm: Optional[ViewManager]) -> None: - super().on_enter(vm) # Let BaseView do its thing - def draw(self, ctx: Context) -> None: # Paint the background black ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill() # Green square ctx.rgb(0, 255, 0).rectangle(-20, -20, 40, 40).fill() - def think(self, ins: InputState, delta_ms: int) -> None: - super().think(ins, delta_ms) # Let BaseView do its thing - - if self.input.right_shoulder.middle.pressed: - self.vm.pop() - - class MyDemo(Application): def __init__(self, app_ctx: ApplicationContext) -> None: super().__init__(app_ctx) + # Ignore the app_ctx for now. def draw(self, ctx: Context) -> None: # Paint the background black @@ -395,24 +416,15 @@ Let's introduce the final class you should actually be using for application dev # Red square ctx.rgb(255, 0, 0).rectangle(-20, -20, 40, 40).fill() - def on_enter(self, vm: Optional[ViewManager]) -> None: - super().on_enter(vm) # Let Application do its thing - def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) # Let Application do its thing if self.input.right_shoulder.middle.pressed: - self._view_manager.push(SecondScreen()) - - ##### Uncomment when running the application via mpremote/Micro REPL #### - # st3m.run.run_view(MyDemo(ApplicationContext())) - -The `Application` class gives you the following extras: - - - Pressing the down the left shoulder button exits the app - - You get a `_view_manager` member to manager your views - - It can be picked up by the main menu system + self.vm.push(SecondScreen()) + if __name__ == '__main__': + # Continue to make runnable via mpremote run. + st3m.run.run_view(MyDemo(ApplicationContext())) To add the application to the menu we are missing one more thing: a `flow3r.toml` file which describes the application so flow3r knows where to put it in the menu system. diff --git a/python_payload/st3m/application.py b/python_payload/st3m/application.py index 673890cc91a5388601c76ae776de7cee9c75652f..a50a0b6870612038f7702aa21d43f0a01f4edce8 100644 --- a/python_payload/st3m/application.py +++ b/python_payload/st3m/application.py @@ -34,20 +34,9 @@ class Application(BaseView): self._app_ctx = app_ctx super().__init__() - def on_exit(self) -> None: - pass - - def on_enter(self, vm: Optional[ViewManager]) -> None: - super().on_enter(vm) - def think(self, ins: InputState, delta_ms: int) -> None: super().think(ins, delta_ms) - if self.input.buttons.os.middle.pressed: - if self.vm is not None: - self.on_exit() - self.vm.pop(ViewTransitionSwipeRight()) - class BundleLoadException(BaseException): MSG = "failed to load" diff --git a/python_payload/st3m/ui/view.py b/python_payload/st3m/ui/view.py index c0b2ee5f11edcc46c0b6f7dc313b9b92260110b3..b3d8ba013a94d279779bb2580e9f34aa9f53981c 100644 --- a/python_payload/st3m/ui/view.py +++ b/python_payload/st3m/ui/view.py @@ -15,8 +15,13 @@ class View(Responder): def on_enter(self, vm: Optional["ViewManager"]) -> None: """ - Called when the View has just become active. This is guaranteed to be - called before think(). + Called when the View has just become active. + """ + pass + + def on_exit(self) -> None: + """ + Called when the View is about to become inactive. """ pass @@ -147,8 +152,14 @@ class ViewManager(Responder): self._transitioning = False self._transition = 0.0 self._history: List[View] = [] + self._input = InputController() def think(self, ins: InputState, delta_ms: int) -> None: + self._input.think(ins, delta_ms) + + if self._input.buttons.os.middle.pressed: + self.pop(ViewTransitionSwipeRight()) + if self._transitioning: self._transition += (delta_ms / 1000.0) * (1000 / self._time_ms) if self._transition >= 1.0: @@ -186,6 +197,8 @@ class ViewManager(Responder): The new view will _not_ be added to history! """ self._outgoing = self._incoming + if self._outgoing is not None: + self._outgoing.on_exit() self._incoming = r self._incoming.on_enter(self) self._overriden_vt = overide_vt @@ -212,5 +225,7 @@ class ViewManager(Responder): override_vt will be used instead of the default ViewTransition animation. """ + if len(self._history) < 1: + return r = self._history.pop() self.replace(r, override_vt)