Files
firmware/src/graphics/niche/InkHUD/WindowManager.cpp
zeropt 3371d3372c Adding support for InkHUD joystick navigation for the Seeed Wio Tracker L1 E-ink (#8678)
* TwoButtonExtened mirrors TwoButton but added joystick functionality

* basic ui navigation with a joystick

settings->joystick.enabled setting added and SETTINGS_VERSION
incremented by one in InkHUD/Persistence.h

in seeed_wio_tracker_L1_eink/nicheGraphics.h enable joystick and
disable "Next Tile" menu item in

implement prevTile and prevApplet functions in
InkHUD/WindowManager.h,cpp and InkHUD/InkHUD.h,cpp

onStickCenterShort, onStickCenterLong, onStickUp, onStickDown,
onStickLeft, and onStickRight functions added to:
- InkHUD/InkHUD.h,cpp
- InkHUD/Events.h,cpp
- InkHUD/Applet.h

change navigation actions in InkHUD/Events.cpp events based on
whether the joystick is enabled or not

in seeed_wio_tracker_L1_eink/nicheGraphics.h connect joystick events to
the new joystick handler functions

* handle joystick input in NotificationApplet and TipsApplet

Both the joystick center short press and the user button short press can
be used to advance through the Tips applet.

dismiss notifications with any joystick input

* MenuApplet controls
allows menu navigation including a back button

* add AlignStickApplet for aligning the joystick with the screen

add joystick.aligned and joystick.alignment to InkHUD/Persistence.h for
storing alignment status and relative angle

create AlignStick applet that prompts the user for a joystick input and
rotates the controls to align with the screen

AlignStick applet is run after the tips applet if the joystick is
enabled and not aligned

add menu item for opening the AlignStick applet

* update tips applet with joystick controls

* format InkHUD additions

* fix stroke consistency when resizing joystick graphic

* tweak button tips for order consistency

* increase joystick debounce

* fix comments

* remove unnecessary '+'

* remap joystick controls to match standard inkHUD behavior

Input with a joystick now behaves as follows

User Button (joystick center):
- short press in applet -> opens menu
- long press in applet -> opens menu
- short press in menu -> selects
- long press in menu -> selects

Exit Button:
- short press in applet -> switches tile
- long press in applet -> nothing for now
- short press in menu -> closes menu
- long press in menu -> nothing for now

---------

Co-authored-by: scobert <scobert57@gmail.com>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2025-12-20 15:15:42 -05:00

632 lines
25 KiB
C++

#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./WindowManager.h"
#include "./Applets/System/AlignStick/AlignStickApplet.h"
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
#include "./Applets/System/Logo/LogoApplet.h"
#include "./Applets/System/Menu/MenuApplet.h"
#include "./Applets/System/Notification/NotificationApplet.h"
#include "./Applets/System/Pairing/PairingApplet.h"
#include "./Applets/System/Placeholder/PlaceholderApplet.h"
#include "./Applets/System/Tips/TipsApplet.h"
#include "./SystemApplet.h"
using namespace NicheGraphics;
InkHUD::WindowManager::WindowManager()
{
// Convenient references
inkhud = InkHUD::getInstance();
settings = &inkhud->persistence->settings;
}
// Register a user applet with InkHUD
// This is called in setupNicheGraphics()
// This should be the only time that specific user applets are mentioned in the code
// If a user applet is not added with this method, its code should not be built
// Call before begin
void InkHUD::WindowManager::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile)
{
inkhud->userApplets.push_back(a);
// If requested, mark in settings that this applet should be active by default
// This means that it will be available for the user to cycle to with short-press of the button
// This is the default state only: user can activate or deactivate applets through the menu.
// User's choice of active applets is stored in settings, and will be honored instead of these defaults, if present
if (defaultActive)
settings->userApplets.active[inkhud->userApplets.size() - 1] = true;
// If requested, mark in settings that this applet should "autoshow" by default
// This means that the applet will be automatically brought to foreground when it has new data to show
// This is the default state only: user can select which applets have this behavior through the menu
// User's selection is stored in settings, and will be honored instead of these defaults, if present
if (defaultAutoshow)
settings->userApplets.autoshow[inkhud->userApplets.size() - 1] = true;
// If specified, mark this as the default applet for a given tile index
// Used only to avoid placeholder applet "out of the box", when default settings have more than one tile
if (onTile != (uint8_t)-1)
settings->userTiles.displayedUserApplet[onTile] = inkhud->userApplets.size() - 1;
// The label that will be show in the applet selection menu, on the device
a->name = name;
}
// Initial configuration at startup
void InkHUD::WindowManager::begin()
{
assert(inkhud);
createSystemApplets();
placeSystemTiles();
createUserApplets();
createUserTiles();
placeUserTiles();
assignUserAppletsToTiles();
refocusTile();
}
// Focus on a different tile
// The "focused tile" is the one which cycles applets on user button press,
// and the one where the menu will be displayed
void InkHUD::WindowManager::nextTile()
{
// Close the menu applet if open
// We don't *really* want to do this, but it simplifies handling *a lot*
MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu");
bool menuWasOpen = false;
if (menu->isForeground()) {
menu->sendToBackground();
menuWasOpen = true;
}
// Swap to next tile
settings->userTiles.focused = (settings->userTiles.focused + 1) % settings->userTiles.count;
// Make sure that we don't get stuck on the placeholder tile
refocusTile();
if (menuWasOpen)
menu->show(userTiles.at(settings->userTiles.focused));
// Ask the tile to draw an indicator showing which tile is now focused
// Requests a render
// We only draw this indicator if the device uses an aux button to switch tiles.
// Assume aux button is used to switch tiles if the "next tile" menu item is hidden
if (!settings->optionalMenuItems.nextTile)
userTiles.at(settings->userTiles.focused)->requestHighlight();
}
// Focus on a different tile but decrement index
void InkHUD::WindowManager::prevTile()
{
// Close the menu applet if open
// We don't *really* want to do this, but it simplifies handling *a lot*
MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu");
bool menuWasOpen = false;
if (menu->isForeground()) {
menu->sendToBackground();
menuWasOpen = true;
}
// Swap to next tile
if (settings->userTiles.focused == 0)
settings->userTiles.focused = settings->userTiles.count - 1;
else
settings->userTiles.focused--;
// Make sure that we don't get stuck on the placeholder tile
refocusTile();
if (menuWasOpen)
menu->show(userTiles.at(settings->userTiles.focused));
// Ask the tile to draw an indicator showing which tile is now focused
// Requests a render
// We only draw this indicator if the device uses an aux button to switch tiles.
// Assume aux button is used to switch tiles if the "next tile" menu item is hidden
if (!settings->optionalMenuItems.nextTile)
userTiles.at(settings->userTiles.focused)->requestHighlight();
}
// Show the menu (on the the focused tile)
// The applet previously displayed there will be restored once the menu closes
void InkHUD::WindowManager::openMenu()
{
MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu");
menu->show(userTiles.at(settings->userTiles.focused));
}
// Bring the AlignStick applet to the foreground
void InkHUD::WindowManager::openAlignStick()
{
if (settings->joystick.enabled) {
AlignStickApplet *alignStick = (AlignStickApplet *)inkhud->getSystemApplet("AlignStick");
alignStick->bringToForeground();
}
}
// On the currently focussed tile: cycle to the next available user applet
// Applets available for this must be activated, and not already displayed on another tile
void InkHUD::WindowManager::nextApplet()
{
Tile *t = userTiles.at(settings->userTiles.focused);
// Abort if zero applets available
// nullptr means WindowManager::refocusTile determined that there were no available applets
if (!t->getAssignedApplet())
return;
// Find the index of the applet currently shown on the tile
uint8_t appletIndex = -1;
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
if (inkhud->userApplets.at(i) == t->getAssignedApplet()) {
appletIndex = i;
break;
}
}
// Confirm that we did find the applet
assert(appletIndex != (uint8_t)-1);
// Iterate forward through the WindowManager::applets, looking for the next valid applet
Applet *nextValidApplet = nullptr;
for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) {
uint8_t newAppletIndex = (appletIndex + i) % inkhud->userApplets.size();
Applet *a = inkhud->userApplets.at(newAppletIndex);
// Looking for an applet which is active (enabled by user), but currently in background
if (a->isActive() && !a->isForeground()) {
nextValidApplet = a;
settings->userTiles.displayedUserApplet[settings->userTiles.focused] =
newAppletIndex; // Remember this setting between boots!
break;
}
}
// Confirm that we found another applet
if (!nextValidApplet)
return;
// Hide old applet, show new applet
t->getAssignedApplet()->sendToBackground();
t->assignApplet(nextValidApplet);
nextValidApplet->bringToForeground();
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// On the currently focussed tile: cycle to the previous available user applet
// Applets available for this must be activated, and not already displayed on another tile
void InkHUD::WindowManager::prevApplet()
{
Tile *t = userTiles.at(settings->userTiles.focused);
// Abort if zero applets available
// nullptr means WindowManager::refocusTile determined that there were no available applets
if (!t->getAssignedApplet())
return;
// Find the index of the applet currently shown on the tile
uint8_t appletIndex = -1;
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
if (inkhud->userApplets.at(i) == t->getAssignedApplet()) {
appletIndex = i;
break;
}
}
// Confirm that we did find the applet
assert(appletIndex != (uint8_t)-1);
// Iterate forward through the WindowManager::applets, looking for the previous valid applet
Applet *prevValidApplet = nullptr;
for (uint8_t i = 1; i < inkhud->userApplets.size(); i++) {
uint8_t newAppletIndex = 0;
if (i > appletIndex)
newAppletIndex = inkhud->userApplets.size() + appletIndex - i;
else
newAppletIndex = (appletIndex - i);
Applet *a = inkhud->userApplets.at(newAppletIndex);
// Looking for an applet which is active (enabled by user), but currently in background
if (a->isActive() && !a->isForeground()) {
prevValidApplet = a;
settings->userTiles.displayedUserApplet[settings->userTiles.focused] =
newAppletIndex; // Remember this setting between boots!
break;
}
}
// Confirm that we found another applet
if (!prevValidApplet)
return;
// Hide old applet, show new applet
t->getAssignedApplet()->sendToBackground();
t->assignApplet(prevValidApplet);
prevValidApplet->bringToForeground();
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
}
// Rotate the display image by 90 degrees
void InkHUD::WindowManager::rotate()
{
settings->rotation = (settings->rotation + 1) % 4;
changeLayout();
}
// Change whether the battery icon is displayed (top right corner)
// Don't toggle the OptionalFeatures value before calling this, our method handles it internally
void InkHUD::WindowManager::toggleBatteryIcon()
{
BatteryIconApplet *batteryIcon = (BatteryIconApplet *)inkhud->getSystemApplet("BatteryIcon");
settings->optionalFeatures.batteryIcon = !settings->optionalFeatures.batteryIcon; // Preserve the change between boots
// Show or hide the applet
if (settings->optionalFeatures.batteryIcon)
batteryIcon->bringToForeground();
else
batteryIcon->sendToBackground();
// Force-render
// - redraw all applets
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
// Perform necessary reconfiguration when user changes number of tiles (or rotation) at run-time
// Call after changing settings.tiles.count
void InkHUD::WindowManager::changeLayout()
{
// Recreate tiles
// - correct number created, from settings.userTiles.count
// - set dimension and position of tiles, according to layout
createUserTiles();
placeUserTiles();
placeSystemTiles();
// Handle fewer tiles
// - background any applets which have lost their tile
findOrphanApplets();
// Handle more tiles
// - create extra applets
// - assign them to the new extra tiles
createUserApplets();
assignUserAppletsToTiles();
// Focus a valid tile
// - info: focused tile is the one which cycles applets when user button pressed
// - may now be out of bounds if tile count has decreased
refocusTile();
// Restore menu
// - its tile was just destroyed and recreated (createUserTiles)
// - its assignment was cleared (assignUserAppletsToTiles)
MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu");
if (menu->isForeground()) {
Tile *ft = userTiles.at(settings->userTiles.focused);
menu->show(ft);
}
// Force-render
// - redraw all applets
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
// Perform necessary reconfiguration when user activates or deactivates applets at run-time
// Call after changing settings.userApplets.active
void InkHUD::WindowManager::changeActivatedApplets()
{
MenuApplet *menu = (MenuApplet *)inkhud->getSystemApplet("Menu");
assert(menu->isForeground());
// Activate or deactivate applets
// - to match value of settings.userApplets.active
createUserApplets();
// Assign the placeholder applet
// - if applet was foreground on a tile when deactivated, swap it with a placeholder
// - placeholder applet may be assigned to multiple tiles, if needed
assignUserAppletsToTiles();
// Ensure focused tile has a valid applet
// - if focused tile's old applet was deactivated, give it a real applet, instead of placeholder
// - reason: nextApplet() won't cycle applets if placeholder is shown
refocusTile();
// Restore menu
// - its assignment was cleared (assignUserAppletsToTiles)
if (menu->isForeground()) {
Tile *ft = userTiles.at(settings->userTiles.focused);
menu->show(ft);
}
// Force-render
// - redraw all applets
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
}
// Some applets may be permitted to bring themselves to foreground, to show new data
// User selects which applets have this permission via on-screen menu
// Priority is determined by the order which applets were added to WindowManager in setupNicheGraphics
// We will only autoshow one applet
void InkHUD::WindowManager::autoshow()
{
// Don't perform autoshow if a system applet has exclusive use of the display right now
// Note: lockRequests prevents autoshow attempting to hide menuApplet
for (SystemApplet *sa : inkhud->systemApplets) {
if (sa->lockRendering || sa->lockRequests)
return;
}
NotificationApplet *notificationApplet = (NotificationApplet *)inkhud->getSystemApplet("Notification");
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
Applet *a = inkhud->userApplets.at(i);
if (a->wantsToAutoshow() // Applet wants to become foreground
&& !a->isForeground() // Not yet foreground
&& settings->userApplets.autoshow[i]) // User permits this applet to autoshow
{
Tile *t = userTiles.at(settings->userTiles.focused); // Get focused tile
t->getAssignedApplet()->sendToBackground(); // Background whichever applet is already on the tile
t->assignApplet(a); // Assign our new applet to tile
a->bringToForeground(); // Foreground our new applet
// Check if autoshown applet shows the same information as notification intended to
// In this case, we can dismiss the notification before it is shown
// Note: we are re-running the approval process. This normally occurs when the notification is initially triggered.
if (notificationApplet->isForeground() && !notificationApplet->isApproved())
notificationApplet->dismiss();
break; // One autoshow only! Avoid conflicts
}
}
}
// A collection of any user tiles which do not have a valid user applet
// This can occur in various situations, such as when a user enables fewer applets than their layout has tiles
// The tiles (and which regions the occupy) are private information of the window manager
// The renderer needs to know which regions (if any) are empty,
// in order to fill them with a "placeholder" pattern.
// -- There may be a tidier way to accomplish this --
std::vector<InkHUD::Tile *> InkHUD::WindowManager::getEmptyTiles()
{
std::vector<Tile *> empty;
for (Tile *t : userTiles) {
Applet *a = t->getAssignedApplet();
if (!a || !a->isActive())
empty.push_back(t);
}
return empty;
}
// Complete the configuration of one newly instantiated system applet
// - link it with its tile
// Unlike user applets, most system applets have their own unique tile;
// the only reference to this tile is held by the system applet itself.
// - give it a name
// A system applet's name is its unique identifier.
// The name is our only reference to specific system applets, via InkHUD->getSystemApplet
// - add it to the list of system applets
void InkHUD::WindowManager::addSystemApplet(const char *name, SystemApplet *applet, Tile *tile)
{
// Some system applets might not have their own tile (e.g. menu, placeholder)
if (tile)
tile->assignApplet(applet);
applet->name = name;
inkhud->systemApplets.push_back(applet);
}
// Create the "system applets"
// These handle things like bootscreen, pop-up notifications etc
// They are processed separately from the user applets, because they might need to do "weird things"
void InkHUD::WindowManager::createSystemApplets()
{
addSystemApplet("Logo", new LogoApplet, new Tile);
addSystemApplet("Pairing", new PairingApplet, new Tile);
addSystemApplet("Tips", new TipsApplet, new Tile);
if (settings->joystick.enabled)
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
addSystemApplet("Menu", new MenuApplet, nullptr);
// Battery and notifications *behind* the menu
addSystemApplet("Notification", new NotificationApplet, new Tile);
addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile);
// Special handling only, via Rendering::renderPlaceholders
addSystemApplet("Placeholder", new PlaceholderApplet, nullptr);
// System applets are always active
for (SystemApplet *sa : inkhud->systemApplets)
sa->activate();
}
// Set the position and size of most system applets
// Most system applets have their own tile. We manually set the region this tile occupies
void InkHUD::WindowManager::placeSystemTiles()
{
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
if (settings->joystick.enabled)
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);
const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2;
const uint16_t batteryIconWidth = batteryIconHeight * 1.8;
inkhud->getSystemApplet("BatteryIcon")
->getTile()
->setRegion(inkhud->width() - batteryIconWidth, // x
2, // y
batteryIconWidth, // width
batteryIconHeight); // height
// Note: the tiles of placeholder and menu applets are manipulated specially
// - menuApplet borrows user tiles
// - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles
}
// Activate or deactivate user applets, to match settings
// Called at boot, or after run-time config changes via menu
// Note: this method does not instantiate the applets;
// this is done in setupNicheGraphics, when passing to InkHUD::addApplet
void InkHUD::WindowManager::createUserApplets()
{
// Deactivate and remove any no-longer-needed applets
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
Applet *a = inkhud->userApplets.at(i);
// If the applet is active, but settings say it shouldn't be:
// - run applet's custom deactivation code
// - mark applet as inactive (internally)
if (a->isActive() && !settings->userApplets.active[i])
a->deactivate();
}
// Activate and add any new applets
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
// If not activated, but it now should be:
// - run applet's custom activation code
// - mark applet as active (internally)
if (!inkhud->userApplets.at(i)->isActive() && settings->userApplets.active[i])
inkhud->userApplets.at(i)->activate();
}
}
// Creates the tiles which will host user applets
// The amount of these is controlled by the user, via "layout" option in the InkHUD menu
void InkHUD::WindowManager::createUserTiles()
{
// Delete any tiles which currently exist
for (Tile *t : userTiles)
delete t;
userTiles.clear();
// Create new tiles
for (uint8_t i = 0; i < settings->userTiles.count; i++) {
Tile *t = new Tile;
userTiles.push_back(t);
}
}
// Calculate the display region occupied by each tile
// This determines how pixels are translated from "relative" applet-space to "absolute" windowmanager-space
// The size and position depend on the amount of tiles the user prefers, set by the "layout" option
void InkHUD::WindowManager::placeUserTiles()
{
for (uint8_t i = 0; i < userTiles.size(); i++)
userTiles.at(i)->setRegion(settings->userTiles.count, i);
}
// Link "foreground" user applets with tiles
// Which applet should be *initially* shown on a tile?
// This initial state changes once WindowManager::nextApplet is called.
// Performed at startup, or during certain run-time reconfigurations (e.g number of tiles)
// This state of "which applets are foreground" is preserved between reboots, but the value needs validating at startup.
void InkHUD::WindowManager::assignUserAppletsToTiles()
{
// Each user tile
for (uint8_t i = 0; i < userTiles.size(); i++) {
Tile *t = userTiles.at(i);
// Check whether tile can display the previously shown applet again
uint8_t oldIndex = settings->userTiles.displayedUserApplet[i]; // Previous index in WindowManager::userApplets
bool canRestore = true;
if (oldIndex > inkhud->userApplets.size() - 1) // Check if old index is now out of bounds
canRestore = false;
else if (!settings->userApplets.active[oldIndex]) // Check that old applet is still activated
canRestore = false;
else { // Check that the old applet isn't now shown already on a different tile
for (uint8_t i2 = 0; i2 < i; i2++) {
if (settings->userTiles.displayedUserApplet[i2] == oldIndex) {
canRestore = false;
break;
}
}
}
// Restore previously shown applet if possible,
// otherwise assign nullptr, which will render specially using placeholderApplet
if (canRestore) {
Applet *a = inkhud->userApplets.at(oldIndex);
t->assignApplet(a);
a->bringToForeground();
} else {
t->assignApplet(nullptr);
settings->userTiles.displayedUserApplet[i] = -1; // Update settings: current tile has no valid applet
}
}
}
// During layout changes, our focused tile setting can become invalid
// This method identifies that situation and corrects for it
void InkHUD::WindowManager::refocusTile()
{
// Validate "focused tile" setting
// - info: focused tile responds to button presses: applet cycling, menu, etc
// - if number of tiles changed, might now be out of index
if (settings->userTiles.focused >= userTiles.size())
settings->userTiles.focused = 0;
// Give "focused tile" a valid applet
// - scan for another valid applet, which we can addSubstitution
// - reason: nextApplet() won't cycle if no applet is assigned
Tile *focusedTile = userTiles.at(settings->userTiles.focused);
if (!focusedTile->getAssignedApplet()) {
// Search for available applets
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
Applet *a = inkhud->userApplets.at(i);
if (a->isActive() && !a->isForeground()) {
// Found a suitable applet
// Assign it to the focused tile
focusedTile->assignApplet(a);
a->bringToForeground();
settings->userTiles.displayedUserApplet[settings->userTiles.focused] = i; // Record change: persist after reboot
break;
}
}
}
}
// Seach for any applets which believe they are foreground, but no longer have a valid tile
// Tidies up after layout changes at runtime
void InkHUD::WindowManager::findOrphanApplets()
{
for (uint8_t ia = 0; ia < inkhud->userApplets.size(); ia++) {
Applet *a = inkhud->userApplets.at(ia);
// Applet doesn't believe it is displayed: not orphaned
if (!a->isForeground())
continue;
// Check each tile, to see if anyone claims this applet
bool foundOwner = false;
for (uint8_t it = 0; it < userTiles.size(); it++) {
Tile *t = userTiles.at(it);
// A tile claims this applet: not orphaned
if (t->getAssignedApplet() == a) {
foundOwner = true;
break;
}
}
// Orphan found
// Tell the applet that no tile is currently displaying it
// This allows the focussed tile to cycle to this applet again by pressing user button
if (!foundOwner)
a->sendToBackground();
}
}
#endif