From a1d0ddec49211f06daea31a663ecd1ca074ef66d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 26 Apr 2026 12:26:29 +0200 Subject: [PATCH] Introduce extended button handler --- lib/I18n/translations/english.yaml | 27 ++++ src/ButtonEventManager.cpp | 121 ++++++++++++++++++ src/ButtonEventManager.h | 84 ++++++++++++ src/CrossPointSettings.cpp | 5 +- src/CrossPointSettings.h | 55 +++++++- src/SettingsList.h | 114 ++++++++++++++++- src/activities/Activity.h | 9 +- src/activities/ActivityManager.cpp | 8 ++ src/activities/ActivityManager.h | 13 +- src/activities/reader/EpubReaderActivity.cpp | 116 ++++++++++++++++- src/activities/reader/EpubReaderActivity.h | 1 + src/activities/reader/MdReaderActivity.cpp | 62 ++++++++- src/activities/reader/MdReaderActivity.h | 1 + src/activities/reader/ReaderUtils.h | 17 +-- src/activities/reader/TxtReaderActivity.cpp | 48 +++++++ src/activities/reader/TxtReaderActivity.h | 1 + src/activities/reader/XtcReaderActivity.cpp | 55 ++++++-- src/activities/reader/XtcReaderActivity.h | 1 + src/main.cpp | 128 +++++++++++++++++++ 19 files changed, 833 insertions(+), 33 deletions(-) create mode 100644 src/ButtonEventManager.cpp create mode 100644 src/ButtonEventManager.h diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 8df785b2..cc4eaa80 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -547,3 +547,30 @@ STR_CAPTIVE_PORTAL_DETECTED: "Login Required" STR_CAPTIVE_PORTAL_HINT_1: "Network requires browser login. On another device," STR_CAPTIVE_PORTAL_HINT_2: "visit the URL below to authorize, then press OK." STR_CAPTIVE_PORTAL_DONE: "I'm authorized" +STR_MENU_BTN_ACTIONS: "Button Actions" +STR_BTN_SHORT_PRESS: "Short Press" +STR_BTN_DOUBLE_PRESS: "Double Press" +STR_BTN_LONG_PRESS: "Long Press" +STR_BTN_BACK: "Back Button" +STR_BTN_CONFIRM: "Confirm Button" +STR_BTN_LEFT: "Left Button" +STR_BTN_RIGHT: "Right Button" +STR_BTN_UP: "Up Button" +STR_BTN_DOWN: "Down Button" +STR_BTN_PAGE_BACK: "Page Back Button" +STR_BTN_PAGE_FORWARD: "Page Forward Button" +STR_BTN_POWER: "Power Button" +STR_BTN_ACT_DEFAULT: "Default" +STR_BTN_ACT_PAGE_FORWARD: "Next Page" +STR_BTN_ACT_PAGE_BACK: "Previous Page" +STR_BTN_ACT_PAGE_FORWARD_10: "Skip 10 Pages Forward" +STR_BTN_ACT_PAGE_BACK_10: "Skip 10 Pages Back" +STR_BTN_ACT_GO_HOME: "Go Home" +STR_BTN_ACT_SLEEP: "Sleep" +STR_BTN_ACT_FORCE_REFRESH: "Refresh Screen" +STR_BTN_ACT_OPEN_TOC: "Open Table of Contents" +STR_BTN_ACT_OPEN_BOOKMARKS: "Open Bookmarks" +STR_BTN_ACT_STAR_PAGE: "Star Page" +STR_BTN_ACT_FOOTNOTES: "Footnotes" +STR_BTN_ACT_NEXT_SECTION: "Next Section / Chapter" +STR_BTN_ACT_PREV_SECTION: "Previous Section / Chapter" diff --git a/src/ButtonEventManager.cpp b/src/ButtonEventManager.cpp new file mode 100644 index 00000000..16345b55 --- /dev/null +++ b/src/ButtonEventManager.cpp @@ -0,0 +1,121 @@ +#include "ButtonEventManager.h" + +#include "CrossPointSettings.h" + +// Required for constexpr array definition in .cpp +constexpr ButtonEventManager::Button ButtonEventManager::ALL_BUTTONS[ButtonEventManager::NUM_BUTTONS]; + +bool ButtonEventManager::hasDoubleAction(const Button button) { + using BA = CrossPointSettings::BUTTON_ACTION; + switch (button) { + case Button::Back: + return SETTINGS.btnDoubleBack != BA::BTN_DEFAULT; + case Button::Confirm: + return SETTINGS.btnDoubleConfirm != BA::BTN_DEFAULT; + case Button::Left: + return SETTINGS.btnDoubleLeft != BA::BTN_DEFAULT; + case Button::Right: + return SETTINGS.btnDoubleRight != BA::BTN_DEFAULT; + case Button::Up: + return SETTINGS.btnDoubleUp != BA::BTN_DEFAULT; + case Button::Down: + return SETTINGS.btnDoubleDown != BA::BTN_DEFAULT; + case Button::PageBack: + return SETTINGS.btnDoublePageBack != BA::BTN_DEFAULT; + case Button::PageForward: + return SETTINGS.btnDoublePageForward != BA::BTN_DEFAULT; + case Button::Power: + return SETTINGS.btnDoublePower != BA::BTN_DEFAULT; + } + return false; +} + +void ButtonEventManager::pushEvent(const Button button, const PressType type) { + const int next = (eventTail + 1) % EVENT_BUF; + if (next == eventHead) return; // buffer full, drop oldest not possible — just drop newest + eventBuf[eventTail] = {button, type}; + eventTail = next; +} + +bool ButtonEventManager::consumeEvent(ButtonEvent& out) { + if (eventHead == eventTail) return false; + out = eventBuf[eventHead]; + eventHead = (eventHead + 1) % EVENT_BUF; + return true; +} + +void ButtonEventManager::drain() { + for (auto& b : buttons) { + b.state = State::Idle; + b.pressDownTime = 0; + b.releaseTime = 0; + } + eventHead = eventTail = 0; +} + +void ButtonEventManager::processButton(const int idx, const Button btn) { + PerButton& s = buttons[idx]; + const unsigned long now = millis(); + const bool pressed = input.wasPressed(btn); + const bool released = input.wasReleased(btn); + const bool held = input.isPressed(btn); + + switch (s.state) { + case State::Idle: + if (pressed) { + s.state = State::Pressed; + s.pressDownTime = now; + } + break; + + case State::Pressed: + if (released) { + const unsigned long heldMs = now - s.pressDownTime; + if (heldMs >= LONG_PRESS_MS) { + pushEvent(btn, PressType::Long); + s.state = State::Idle; + } else if (hasDoubleAction(btn)) { + // Delay short-press decision until double-click window expires + s.releaseTime = now; + s.state = State::ReleasedOnce; + } else { + // No double action configured — fire immediately + pushEvent(btn, PressType::Short); + s.state = State::Idle; + } + } else if (!held) { + // Button disappeared without wasReleased edge (e.g. after drain) — reset + s.state = State::Idle; + } + break; + + case State::ReleasedOnce: + if (pressed) { + // Second press within window — start tracking it + s.state = State::DoublePressed; + s.pressDownTime = now; + } else if (now - s.releaseTime >= DOUBLE_WINDOW_MS) { + // Window expired without a second press — it was a short press + pushEvent(btn, PressType::Short); + s.state = State::Idle; + } + break; + + case State::DoublePressed: + if (released) { + pushEvent(btn, PressType::Double); + s.state = State::Idle; + } else if (!held) { + // Disappeared without edge — treat as double anyway + pushEvent(btn, PressType::Double); + s.state = State::Idle; + } + break; + } +} + +void ButtonEventManager::update() { + for (int i = 0; i < NUM_BUTTONS; i++) { + processButton(i, ALL_BUTTONS[i]); + } +} diff --git a/src/ButtonEventManager.h b/src/ButtonEventManager.h new file mode 100644 index 00000000..f005e3e4 --- /dev/null +++ b/src/ButtonEventManager.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include "MappedInputManager.h" + +// Forward declaration for the global accessor used by Activity.h. +// Defined in main.cpp alongside the ButtonEventManager instance. +class ButtonEventManager; +ButtonEventManager& globalButtonEvents(); + +// Classifies raw button edges into Short, Double, and Long press events. +// +// Per-button state machines run each loop() tick. The key latency rule: +// - If no double-click action is configured for a button, Short fires immediately +// on release (zero extra wait). +// - If a double-click action IS configured, Short is delayed by DOUBLE_WINDOW_MS +// to allow disambiguation. +// - Long fires on release once hold time >= LONG_PRESS_MS (no extra wait). +// - Double fires on the second release within DOUBLE_WINDOW_MS. +// +// Activities query consumeEvent() each loop tick to receive pending events. +// drain() resets all state machines — call it on activity transitions. + +class ButtonEventManager { + public: + using Button = MappedInputManager::Button; + + static constexpr int NUM_BUTTONS = 9; // matches MappedInputManager::Button count + + enum class PressType { Short, Double, Long }; + + struct ButtonEvent { + Button button; + PressType type; + }; + + // Timing constants (milliseconds) + static constexpr unsigned long LONG_PRESS_MS = 600; + static constexpr unsigned long DOUBLE_WINDOW_MS = 300; + + explicit ButtonEventManager(MappedInputManager& input) : input(input) {} + + // Call once per main loop tick, after MappedInputManager::update(). + void update(); + + // Returns the next pending event, or false if none. Call repeatedly until + // false to drain all events for this tick. + bool consumeEvent(ButtonEvent& out); + + // Reset all per-button FSMs. Call on activity transitions to prevent bleed-through. + void drain(); + + // Returns true if a double-click action is configured for this button. + // ButtonEventManager queries CrossPointSettings internally. + static bool hasDoubleAction(Button button); + + private: + static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = { + Button::Back, Button::Confirm, Button::Left, Button::Right, Button::Up, + Button::Down, Button::PageBack, Button::PageForward, Button::Power, + }; + + enum class State { Idle, Pressed, ReleasedOnce, DoublePressed }; + + struct PerButton { + State state = State::Idle; + unsigned long pressDownTime = 0; // when the current (or first) press started + unsigned long releaseTime = 0; // when the first release happened (for double-click window) + }; + + PerButton buttons[NUM_BUTTONS]; + + // Pending events ring buffer (small — at most one event per button per tick) + static constexpr int EVENT_BUF = 16; + ButtonEvent eventBuf[EVENT_BUF] = {}; + int eventHead = 0; + int eventTail = 0; + + MappedInputManager& input; + + void pushEvent(Button button, PressType type); + void processButton(int idx, Button btn); +}; diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index aca87bdc..9245d1bc 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -177,7 +177,10 @@ bool CrossPointSettings::loadFromBinaryFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, longPressChapterSkip); + { + uint8_t _unused; + serialization::readPod(inputFile, _unused); + } // was longPressChapterSkip if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, hyphenationEnabled); if (++settingsRead >= fileSettingsCount) break; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index fa9f3720..65d42b08 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -249,8 +249,6 @@ class CrossPointSettings { char opdsPassword[64] = ""; // Hide battery percentage uint8_t hideBatteryPercentage = HIDE_NEVER; - // Long-press chapter skip on side buttons - uint8_t longPressChapterSkip = 1; // UI Theme uint8_t uiTheme = LYRA; // Sunlight fading compensation @@ -279,6 +277,59 @@ class CrossPointSettings { // Show the Weather home screen menu item (1 = enabled, 0 = hidden) uint8_t useWeather = 1; + // Configurable actions for short / double / long press on each logical button. + // BTN_DEFAULT means "use the button's normal built-in behaviour". + enum BUTTON_ACTION { + BTN_DEFAULT = 0, + BTN_PAGE_FORWARD, + BTN_PAGE_BACK, + BTN_PAGE_FORWARD_10, + BTN_PAGE_BACK_10, + BTN_GO_HOME, + BTN_SLEEP, + BTN_FORCE_REFRESH, + BTN_OPEN_TOC, + BTN_OPEN_BOOKMARKS, + BTN_STAR_PAGE, + BTN_FOOTNOTES, + BTN_NEXT_SECTION, + BTN_PREV_SECTION, + BUTTON_ACTION_COUNT + }; + + // Short-press actions (default: built-in) + uint8_t btnShortBack = BTN_DEFAULT; + uint8_t btnShortConfirm = BTN_DEFAULT; + uint8_t btnShortLeft = BTN_DEFAULT; + uint8_t btnShortRight = BTN_DEFAULT; + uint8_t btnShortUp = BTN_DEFAULT; + uint8_t btnShortDown = BTN_DEFAULT; + uint8_t btnShortPageBack = BTN_DEFAULT; + uint8_t btnShortPageForward = BTN_DEFAULT; + uint8_t btnShortPower = BTN_DEFAULT; + + // Double-press actions (default: BTN_DEFAULT = disabled, no disambiguation wait) + uint8_t btnDoubleBack = BTN_DEFAULT; + uint8_t btnDoubleConfirm = BTN_DEFAULT; + uint8_t btnDoubleLeft = BTN_DEFAULT; + uint8_t btnDoubleRight = BTN_DEFAULT; + uint8_t btnDoubleUp = BTN_DEFAULT; + uint8_t btnDoubleDown = BTN_DEFAULT; + uint8_t btnDoublePageBack = BTN_DEFAULT; + uint8_t btnDoublePageForward = BTN_DEFAULT; + uint8_t btnDoublePower = BTN_DEFAULT; + + // Long-press actions (default: built-in) + uint8_t btnLongBack = BTN_DEFAULT; + uint8_t btnLongConfirm = BTN_DEFAULT; + uint8_t btnLongLeft = BTN_DEFAULT; + uint8_t btnLongRight = BTN_DEFAULT; + uint8_t btnLongUp = BTN_DEFAULT; + uint8_t btnLongDown = BTN_DEFAULT; + uint8_t btnLongPageBack = BTN_DEFAULT; + uint8_t btnLongPageForward = BTN_DEFAULT; + uint8_t btnLongPower = BTN_DEFAULT; + ~CrossPointSettings() = default; // Get singleton instance diff --git a/src/SettingsList.h b/src/SettingsList.h index 03cb30f6..68ad73e5 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -133,13 +133,123 @@ inline const std::vector list = { // --- Controls --- SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), - SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", - StrId::STR_CAT_CONTROLS), SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN, StrId::STR_FORCE_REFRESH, StrId::STR_FOOTNOTES, StrId::STR_STAR_PAGE}, "shortPwrBtn", StrId::STR_CAT_CONTROLS), +// --- Button Actions (short / double / long press per logical button) --- +// All entries share the same ordered action-label list; the submenu groups them behind +// a single placeholder row in the device UI. +#define BTN_ACTION_ENUM_VALUES \ + {StrId::STR_BTN_ACT_DEFAULT, StrId::STR_BTN_ACT_PAGE_FORWARD, StrId::STR_BTN_ACT_PAGE_BACK, \ + StrId::STR_BTN_ACT_PAGE_FORWARD_10, StrId::STR_BTN_ACT_PAGE_BACK_10, StrId::STR_BTN_ACT_GO_HOME, \ + StrId::STR_BTN_ACT_SLEEP, StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, \ + StrId::STR_BTN_ACT_OPEN_BOOKMARKS, StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, \ + StrId::STR_BTN_ACT_NEXT_SECTION, StrId::STR_BTN_ACT_PREV_SECTION} + + // Back button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, BTN_ACTION_ENUM_VALUES, + "btnShortBack", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_BACK) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleBack, BTN_ACTION_ENUM_VALUES, + "btnDoubleBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongBack, BTN_ACTION_ENUM_VALUES, + "btnLongBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Confirm button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortConfirm, BTN_ACTION_ENUM_VALUES, + "btnShortConfirm", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_CONFIRM) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleConfirm, BTN_ACTION_ENUM_VALUES, + "btnDoubleConfirm", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongConfirm, BTN_ACTION_ENUM_VALUES, + "btnLongConfirm", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Left button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortLeft, BTN_ACTION_ENUM_VALUES, + "btnShortLeft", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_LEFT) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleLeft, BTN_ACTION_ENUM_VALUES, + "btnDoubleLeft", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongLeft, BTN_ACTION_ENUM_VALUES, + "btnLongLeft", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Right button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortRight, BTN_ACTION_ENUM_VALUES, + "btnShortRight", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_RIGHT) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleRight, BTN_ACTION_ENUM_VALUES, + "btnDoubleRight", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongRight, BTN_ACTION_ENUM_VALUES, + "btnLongRight", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Up button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortUp, BTN_ACTION_ENUM_VALUES, "btnShortUp", + StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_UP) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleUp, BTN_ACTION_ENUM_VALUES, + "btnDoubleUp", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongUp, BTN_ACTION_ENUM_VALUES, "btnLongUp", + StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Down button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortDown, BTN_ACTION_ENUM_VALUES, + "btnShortDown", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_DOWN) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleDown, BTN_ACTION_ENUM_VALUES, + "btnDoubleDown", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongDown, BTN_ACTION_ENUM_VALUES, + "btnLongDown", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Page Back button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPageBack, BTN_ACTION_ENUM_VALUES, + "btnShortPageBack", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_PAGE_BACK) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePageBack, BTN_ACTION_ENUM_VALUES, + "btnDoublePageBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPageBack, BTN_ACTION_ENUM_VALUES, + "btnLongPageBack", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Page Forward button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPageForward, BTN_ACTION_ENUM_VALUES, + "btnShortPageForward", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_PAGE_FORWARD) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePageForward, BTN_ACTION_ENUM_VALUES, + "btnDoublePageForward", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPageForward, BTN_ACTION_ENUM_VALUES, + "btnLongPageForward", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + // Power button + SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortPower, BTN_ACTION_ENUM_VALUES, + "btnShortPower", StrId::STR_CAT_CONTROLS) + .withSubcategory(StrId::STR_BTN_POWER) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoublePower, BTN_ACTION_ENUM_VALUES, + "btnDoublePower", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + SettingInfo::Enum(StrId::STR_BTN_LONG_PRESS, &CrossPointSettings::btnLongPower, BTN_ACTION_ENUM_VALUES, + "btnLongPower", StrId::STR_CAT_CONTROLS) + .withSubmenu(StrId::STR_MENU_BTN_ACTIONS), + +#undef BTN_ACTION_ENUM_VALUES + // --- System --- SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", StrId::STR_CAT_SYSTEM), diff --git a/src/activities/Activity.h b/src/activities/Activity.h index c5e4949e..2eebecea 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -8,6 +8,7 @@ #include "ActivityManager.h" // for using the ActivityManager singleton #include "ActivityResult.h" +#include "ButtonEventManager.h" #include "GfxRenderer.h" #include "MappedInputManager.h" #include "RenderLock.h" @@ -19,13 +20,14 @@ class Activity { std::string name; GfxRenderer& renderer; MappedInputManager& mappedInput; + ButtonEventManager& buttonEvents; ActivityResultHandler resultHandler; ActivityResult result; public: explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) - : name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} + : name(std::move(name)), renderer(renderer), mappedInput(mappedInput), buttonEvents(globalButtonEvents()) {} virtual ~Activity() = default; const std::string& getName() const { return name; } virtual void onEnter(); @@ -45,6 +47,11 @@ class Activity { virtual bool preventAutoSleep() { return false; } virtual bool isReaderActivity() const { return false; } + // Called by ActivityManager when a globally-configured button action targets the + // current activity. Override in reader activities to handle reader-specific actions. + // Non-reader activities can ignore this (default is no-op). + virtual void onButtonAction(CrossPointSettings::BUTTON_ACTION) {} + // Start a new activity without destroying the current one // Note: requestUpdate() will be invoked automatically once resultHandler finishes void startActivityForResult(std::unique_ptr&& activity, ActivityResultHandler resultHandler); diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index d81cb491..47b67a56 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -154,6 +154,7 @@ void ActivityManager::loop() { // Arm input drain so the button that triggered the pop doesn't bleed into the // restored activity (or into a new activity the handler just pushed). drainInput = true; + buttonEvents.drain(); // Request an update to ensure the popped activity gets re-rendered if (pendingAction == PendingAction::None) { @@ -204,6 +205,7 @@ void ActivityManager::loop() { // Arm input drain so the button that triggered the transition doesn't bleed // into the new activity. drainInput = true; + buttonEvents.drain(); // onEnter may request another pending action, we will handle it in the next loop iteration continue; @@ -387,6 +389,12 @@ bool ActivityManager::isReaderActivity() const { return currentActivity && curre bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } +void ActivityManager::dispatchButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + if (currentActivity) { + currentActivity->onButtonAction(action); + } +} + void ActivityManager::requestUpdate(bool immediate) { if (immediate) { if (renderTaskHandle) { diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 5ab9d04f..1d763206 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -9,6 +9,8 @@ #include #include +#include "ButtonEventManager.h" +#include "CrossPointSettings.h" #include "GfxRenderer.h" #include "MappedInputManager.h" @@ -52,6 +54,7 @@ class ActivityManager { protected: GfxRenderer& renderer; MappedInputManager& mappedInput; + ButtonEventManager& buttonEvents; std::vector> stackActivities; std::unique_ptr currentActivity; @@ -96,7 +99,10 @@ class ActivityManager { public: explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput) - : renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { + : renderer(renderer), + mappedInput(mappedInput), + buttonEvents(globalButtonEvents()), + renderingMutex(xSemaphoreCreateMutex()) { assert(renderingMutex != nullptr && "Failed to create rendering mutex"); stackActivities.reserve(10); } @@ -160,6 +166,11 @@ class ActivityManager { bool isReaderActivity() const; bool skipLoopDelay() const; + // Dispatch a globally-configured button action to the current activity. + // Reader-specific actions (page navigation, TOC, bookmarks, footnotes) are forwarded + // only when the current activity is a reader; others are no-ops in other contexts. + void dispatchButtonAction(CrossPointSettings::BUTTON_ACTION action); + // If immediate is true, the update will be triggered immediately. // Otherwise, it will be deferred until the end of the current loop iteration. void requestUpdate(bool immediate = false); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 83bcf765..5abbaa56 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -343,7 +343,7 @@ void EpubReaderActivity::loop() { return; } - const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs; + const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs; // Chapter skip navigates by TOC entries, not spine boundaries. // Spine items without their own TOC entry inherit the previous spine's tocIndex @@ -1795,3 +1795,117 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf // No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay return true; } + +void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + switch (action) { + case BA::BTN_PAGE_FORWARD: + pageTurn(true); + break; + case BA::BTN_PAGE_BACK: + pageTurn(false); + break; + case BA::BTN_PAGE_FORWARD_10: + for (int i = 0; i < 10; i++) { + if (!stepPageState(true)) break; + } + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + for (int i = 0; i < 10; i++) { + if (!stepPageState(false)) break; + } + requestUpdate(); + break; + case BA::BTN_STAR_PAGE: + if (section) { + bookmarkStore.toggle(static_cast(currentSpineIndex), static_cast(section->currentPage)); + requestUpdate(); + } + break; + case BA::BTN_FOOTNOTES: + if (!currentPageFootnotes.empty()) { + if (currentPageFootnotes.size() == 1) { + navigateToHref(currentPageFootnotes[0].href, true); + } else { + startActivityForResult( + std::make_unique(renderer, mappedInput, currentPageFootnotes), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& footnoteResult = std::get(result.data); + navigateToHref(footnoteResult.href, true); + } + }); + } + } + break; + case BA::BTN_OPEN_TOC: + if (epub) { + const int spineIdx = currentSpineIndex; + const int tocIdx = section ? section->getTocIndexForPage(section->currentPage) + : epub->getTocIndexForSpineIndex(currentSpineIndex); + startActivityForResult(std::make_unique(renderer, mappedInput, epub, + epub->getPath(), spineIdx, tocIdx), + [this](const ActivityResult& result) { + if (result.isCancelled) return; + RenderLock lock(*this); + const auto& chapter = std::get(result.data); + auto resolvedPage = + (chapter.tocIndex && chapter.spineIndex == currentSpineIndex && section) + ? section->getPageForTocIndex(*chapter.tocIndex) + : std::nullopt; + if (resolvedPage) { + section->currentPage = *resolvedPage; + } else { + pendingTocIndex = chapter.tocIndex; + currentSpineIndex = chapter.spineIndex; + nextPageNumber = 0; + section.reset(); + } + }); + } + break; + case BA::BTN_NEXT_SECTION: + case BA::BTN_PREV_SECTION: { + const bool forward = (action == BA::BTN_NEXT_SECTION); + RenderLock lock(*this); + if (section && section->pageCount > 0) { + const int curTocIndex = section->getTocIndexForPage(section->currentPage); + const int nextTocIndex = forward ? curTocIndex + 1 : curTocIndex - 1; + if (curTocIndex < 0) { + nextPageNumber = 0; + currentSpineIndex = forward ? currentSpineIndex + 1 : currentSpineIndex - 1; + section.reset(); + } else if (nextTocIndex >= 0 && nextTocIndex < epub->getTocItemsCount()) { + const int newSpineIndex = epub->getSpineIndexForTocIndex(nextTocIndex); + if (newSpineIndex == currentSpineIndex) { + if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) { + section->currentPage = *resolvedPage; + } + } else { + pendingTocIndex = nextTocIndex; + nextPageNumber = 0; + currentSpineIndex = newSpineIndex; + section.reset(); + } + } else if (forward) { + nextPageNumber = 0; + currentSpineIndex = epub->getSpineItemsCount(); + section.reset(); + } else { + nextPageNumber = 0; + currentSpineIndex = epub->getTocItem(curTocIndex).spineIndex - 1; + section.reset(); + } + } else { + nextPageNumber = 0; + currentSpineIndex = forward ? currentSpineIndex + 1 : currentSpineIndex - 1; + section.reset(); + } + requestUpdate(); + break; + } + default: + break; + } +} diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 8185139f..42c8e332 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -188,6 +188,7 @@ class EpubReaderActivity final : public Activity { void loop() override; void render(RenderLock&& lock) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; // Renders the last saved page to the frame buffer without flushing to display. // Used by SleepActivity to prepare the background for the overlay sleep mode. diff --git a/src/activities/reader/MdReaderActivity.cpp b/src/activities/reader/MdReaderActivity.cpp index 9773a950..a2d1001d 100644 --- a/src/activities/reader/MdReaderActivity.cpp +++ b/src/activities/reader/MdReaderActivity.cpp @@ -253,7 +253,7 @@ void MdReaderActivity::loop() { return; } - const bool headingSkip = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > HEADING_SKIP_MS; + const bool headingSkip = mappedInput.getHeldTime() > HEADING_SKIP_MS; if (headingSkip && !headings.empty()) { jumpToHeading(nextTriggered); return; @@ -892,4 +892,62 @@ void MdReaderActivity::savePageIndexCache() const { } LOG_DBG("MDR", "Saved page index cache: %d pages", totalPages); -} \ No newline at end of file +} + +void MdReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + auto clampPage = [this]() { + if (currentPage < 0) currentPage = 0; + if (currentPage >= totalPages) currentPage = totalPages - 1; + }; + switch (action) { + case BA::BTN_PAGE_FORWARD: + if (currentPage < totalPages - 1) { + currentPage++; + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + break; + case BA::BTN_PAGE_BACK: + if (currentPage > 0) { + currentPage--; + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + break; + case BA::BTN_PAGE_FORWARD_10: + currentPage += 10; + clampPage(); + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + currentPage -= 10; + clampPage(); + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + break; + case BA::BTN_NEXT_SECTION: + jumpToHeading(true); + break; + case BA::BTN_PREV_SECTION: + jumpToHeading(false); + break; + case BA::BTN_OPEN_TOC: + if (!headings.empty()) { + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + startActivityForResult( + std::make_unique(renderer, mappedInput, headings, currentHeadingIndex), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + currentPage = std::get(result.data).page; + currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]); + requestUpdate(); + } + }); + } + break; + default: + break; + } +} diff --git a/src/activities/reader/MdReaderActivity.h b/src/activities/reader/MdReaderActivity.h index 604fc532..c262e9c6 100644 --- a/src/activities/reader/MdReaderActivity.h +++ b/src/activities/reader/MdReaderActivity.h @@ -89,4 +89,5 @@ class MdReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; }; \ No newline at end of file diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 7eb88c32..af8c9bc7 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -31,8 +31,8 @@ inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) { // Suppresses input processing on activity entry until the user has released all buttons and a // clean frame (no pending press/release events) has been observed. Without this, the power-button -// hold used to wake the device leaks into detectPageTurn() and triggers a page turn or, with -// longPressChapterSkip enabled, a chapter skip (the wake-hold easily exceeds skipChapterMs). +// hold used to wake the device leaks into detectPageTurn() and triggers a page turn or chapter +// skip (the wake-hold easily exceeds skipChapterMs). // Each reader holds an instance, calls arm() in onEnter(), and calls shouldDrain() at the top // of loop() — returning early when it returns true. struct InputDrainGuard { @@ -62,17 +62,12 @@ struct PageTurnResult { }; inline PageTurnResult detectPageTurn(const MappedInputManager& input) { - const bool usePress = !SETTINGS.longPressChapterSkip; - const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) || - input.wasPressed(MappedInputManager::Button::Left)) - : (input.wasReleased(MappedInputManager::Button::PageBack) || - input.wasReleased(MappedInputManager::Button::Left)); + const bool prev = + input.wasReleased(MappedInputManager::Button::PageBack) || input.wasReleased(MappedInputManager::Button::Left); const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && input.wasReleased(MappedInputManager::Button::Power); - const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn || - input.wasPressed(MappedInputManager::Button::Right)) - : (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn || - input.wasReleased(MappedInputManager::Button::Right)); + const bool next = input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn || + input.wasReleased(MappedInputManager::Button::Right); return {prev, next}; } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index f3f5cdc8..7e46540d 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -782,3 +782,51 @@ bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx } return true; } + +void TxtReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + auto clampPage = [this]() { + if (currentPage < 0) currentPage = 0; + if (currentPage >= totalPages) currentPage = totalPages - 1; + }; + switch (action) { + case BA::BTN_PAGE_FORWARD: + if (currentPage < totalPages - 1) { + currentPage++; + requestUpdate(); + } + break; + case BA::BTN_PAGE_BACK: + if (currentPage > 0) { + currentPage--; + requestUpdate(); + } + break; + case BA::BTN_PAGE_FORWARD_10: + currentPage += 10; + clampPage(); + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + currentPage -= 10; + clampPage(); + requestUpdate(); + break; + case BA::BTN_STAR_PAGE: + bookmarkStore.toggle(0, static_cast(currentPage)); + requestUpdate(); + break; + case BA::BTN_NEXT_SECTION: + currentPage += 10; + clampPage(); + requestUpdate(); + break; + case BA::BTN_PREV_SECTION: + currentPage -= 10; + clampPage(); + requestUpdate(); + break; + default: + break; + } +} diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 03449fe7..60615e7a 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -58,6 +58,7 @@ class TxtReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; // Renders the last saved page to the frame buffer without flushing to display. // Used by SleepActivity to prepare the background for the overlay sleep mode. diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index d5a631fc..cb83bf07 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -92,19 +92,12 @@ void XtcReaderActivity::loop() { return; } - // When long-press chapter skip is disabled, turn pages on press instead of release. - const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; - const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left)); + const bool prevTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || + mappedInput.wasReleased(MappedInputManager::Button::Left); const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && mappedInput.wasReleased(MappedInputManager::Button::Power); - const bool nextTriggered = usePressForPageTurn - ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasPressed(MappedInputManager::Button::Right)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasReleased(MappedInputManager::Button::Right)); + const bool nextTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || + mappedInput.wasReleased(MappedInputManager::Button::Right); if (!prevTriggered && !nextTriggered) { return; @@ -121,7 +114,7 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs; + const bool skipPages = mappedInput.getHeldTime() > skipPageMs; const int skipAmount = skipPages ? 10 : 1; if (prevTriggered) { @@ -442,3 +435,41 @@ bool XtcReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gfx free(pageBuffer); return true; } + +void XtcReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { + using BA = CrossPointSettings::BUTTON_ACTION; + if (!xtc) return; + const uint32_t pageCount = xtc->getPageCount(); + switch (action) { + case BA::BTN_PAGE_FORWARD: + if (currentPage + 1 < pageCount) { + currentPage++; + requestUpdate(); + } + break; + case BA::BTN_PAGE_BACK: + if (currentPage > 0) { + currentPage--; + requestUpdate(); + } + break; + case BA::BTN_PAGE_FORWARD_10: + currentPage = (currentPage + 10 < pageCount) ? currentPage + 10 : pageCount - 1; + requestUpdate(); + break; + case BA::BTN_PAGE_BACK_10: + currentPage = (currentPage >= 10) ? currentPage - 10 : 0; + requestUpdate(); + break; + case BA::BTN_NEXT_SECTION: + currentPage = (currentPage + 10 < pageCount) ? currentPage + 10 : pageCount - 1; + requestUpdate(); + break; + case BA::BTN_PREV_SECTION: + currentPage = (currentPage >= 10) ? currentPage - 10 : 0; + requestUpdate(); + break; + default: + break; + } +} diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 46fcabf1..b4938a52 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -31,6 +31,7 @@ class XtcReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + void onButtonAction(CrossPointSettings::BUTTON_ACTION action) override; // Renders the last saved page to the frame buffer without flushing to display. // Used by SleepActivity to prepare the background for the overlay sleep mode. diff --git a/src/main.cpp b/src/main.cpp index e79a2bf0..cc93abe2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,7 @@ #include +#include "ButtonEventManager.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "GlobalBookmarkIndex.h" @@ -33,6 +34,8 @@ #include "util/ScreenshotUtil.h" MappedInputManager mappedInputManager(gpio); +ButtonEventManager buttonEventManager(mappedInputManager); +ButtonEventManager& globalButtonEvents() { return buttonEventManager; } GfxRenderer renderer(display); ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; @@ -284,6 +287,7 @@ void loop() { static unsigned long lastMemPrint = 0; gpio.update(); + buttonEventManager.update(); HalClock::updatePeriodic(); renderer.setFadingFix(SETTINGS.fadingFix); @@ -384,6 +388,130 @@ void loop() { activityManager.requestUpdate(); } + // Dispatch globally-configured button actions before handing control to the activity. + // Only non-Default actions are intercepted here; Default falls through to the activity. + { + using BA = CrossPointSettings::BUTTON_ACTION; + using B = MappedInputManager::Button; + ButtonEventManager::ButtonEvent ev; + while (buttonEventManager.consumeEvent(ev)) { + auto actionFor = [&](B btn) -> uint8_t { + switch (ev.type) { + case ButtonEventManager::PressType::Short: + switch (btn) { + case B::Back: + return SETTINGS.btnShortBack; + case B::Confirm: + return SETTINGS.btnShortConfirm; + case B::Left: + return SETTINGS.btnShortLeft; + case B::Right: + return SETTINGS.btnShortRight; + case B::Up: + return SETTINGS.btnShortUp; + case B::Down: + return SETTINGS.btnShortDown; + case B::PageBack: + return SETTINGS.btnShortPageBack; + case B::PageForward: + return SETTINGS.btnShortPageForward; + case B::Power: + return SETTINGS.btnShortPower; + } + break; + case ButtonEventManager::PressType::Double: + switch (btn) { + case B::Back: + return SETTINGS.btnDoubleBack; + case B::Confirm: + return SETTINGS.btnDoubleConfirm; + case B::Left: + return SETTINGS.btnDoubleLeft; + case B::Right: + return SETTINGS.btnDoubleRight; + case B::Up: + return SETTINGS.btnDoubleUp; + case B::Down: + return SETTINGS.btnDoubleDown; + case B::PageBack: + return SETTINGS.btnDoublePageBack; + case B::PageForward: + return SETTINGS.btnDoublePageForward; + case B::Power: + return SETTINGS.btnDoublePower; + } + break; + case ButtonEventManager::PressType::Long: + switch (btn) { + case B::Back: + return SETTINGS.btnLongBack; + case B::Confirm: + return SETTINGS.btnLongConfirm; + case B::Left: + return SETTINGS.btnLongLeft; + case B::Right: + return SETTINGS.btnLongRight; + case B::Up: + return SETTINGS.btnLongUp; + case B::Down: + return SETTINGS.btnLongDown; + case B::PageBack: + return SETTINGS.btnLongPageBack; + case B::PageForward: + return SETTINGS.btnLongPageForward; + case B::Power: + return SETTINGS.btnLongPower; + } + break; + } + return BA::BTN_DEFAULT; + }; + + const uint8_t action = actionFor(ev.button); + if (action == BA::BTN_DEFAULT) continue; + + switch (static_cast(action)) { + case BA::BTN_PAGE_FORWARD: + activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD); + break; + case BA::BTN_PAGE_BACK: + activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK); + break; + case BA::BTN_PAGE_FORWARD_10: + activityManager.dispatchButtonAction(BA::BTN_PAGE_FORWARD_10); + break; + case BA::BTN_PAGE_BACK_10: + activityManager.dispatchButtonAction(BA::BTN_PAGE_BACK_10); + break; + case BA::BTN_GO_HOME: + activityManager.goHome(); + break; + case BA::BTN_SLEEP: + activityManager.goToSleep(); + break; + case BA::BTN_FORCE_REFRESH: { + RenderLock lock; + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + break; + } + case BA::BTN_OPEN_TOC: + activityManager.dispatchButtonAction(BA::BTN_OPEN_TOC); + break; + case BA::BTN_OPEN_BOOKMARKS: + activityManager.goToGlobalBookmarks(); + break; + case BA::BTN_STAR_PAGE: + activityManager.dispatchButtonAction(BA::BTN_STAR_PAGE); + break; + case BA::BTN_FOOTNOTES: + activityManager.dispatchButtonAction(BA::BTN_FOOTNOTES); + break; + default: + break; + } + } + } + const unsigned long activityStartTime = millis(); activityManager.loop(); const unsigned long activityDuration = millis() - activityStartTime;