Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
19 kB · 464 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465#include "MappedInputManager.h"
#include <BoardConfig.h>#include <FreeInkUICore.h>#include <GfxRenderer.h>#include <HalFrontlight.h>
#include <algorithm>#include <cstdlib>
#include "CrossPointSettings.h"#include "components/HeaderBackTapTarget.h"#include "components/UITheme.h"
namespace fui = freeink::ui;
void MappedInputManager::update(const bool deferHomeButtonAction) const { gpio.update(); homeAction = HomeButtonAction::Ignore; if (gpio.hasHomeKey()) { homeAction = homeButtonInput.update(millis(), gpio.wasHomeKeyTapped(), gpio.wasHomeKeyLongPressed(), wasSwipe() != SwipeDir::None, gpio.wasHomeKeyPressed(), static_cast<HomeButtonAction>(SETTINGS.homeButtonTapAction), static_cast<HomeButtonAction>(SETTINGS.homeButtonDoubleTapAction), static_cast<HomeButtonAction>(SETTINGS.homeButtonLongPressAction)); } if (deferHomeButtonAction) { // Keep the first action observed during a synchronous transfer. Home must // still be visible now so the transfer can cancel and unwind promptly. if (homeAction != HomeButtonAction::Ignore && deferredHomeAction == HomeButtonAction::Ignore) { deferredHomeAction = homeAction; } } else if (deferredHomeAction != HomeButtonAction::Ignore) { homeAction = deferredHomeAction; deferredHomeAction = HomeButtonAction::Ignore; } for (uint8_t value = 0; value <= static_cast<uint8_t>(Button::ScreenDown); ++value) { if (!isPressed(static_cast<Button>(value))) longPressFiredButtons &= ~(1u << value); }}
bool MappedInputManager::isNavDirectionSwapped() const { // Touch boards always follow the rendered orientation; button-only boards keep the user toggle. // Home and settings render in portrait, so neither path swaps them. const auto orientation = renderer.getOrientation(); return (gpio.hasTouch() || SETTINGS.frontButtonFollowOrientation) && (orientation == GfxRenderer::PortraitInverted || orientation == GfxRenderer::LandscapeCounterClockwise);}
MappedInputManager::Button MappedInputManager::mapScreenDirection(const Button button) const { // Rows follow GfxRenderer::Orientation's declared order. static constexpr Button directions[][4] = { {Button::Left, Button::Right, Button::Up, Button::Down}, {Button::Down, Button::Up, Button::Left, Button::Right}, {Button::Right, Button::Left, Button::Down, Button::Up}, {Button::Up, Button::Down, Button::Right, Button::Left}, };
uint8_t direction = 0; switch (button) { case Button::ScreenLeft: direction = 0; break; case Button::ScreenRight: direction = 1; break; case Button::ScreenUp: direction = 2; break; case Button::ScreenDown: direction = 3; break; default: return button; }
const uint8_t orientation = SETTINGS.frontButtonFollowOrientation ? static_cast<uint8_t>(renderer.getOrientation()) : 0; return directions[orientation][direction];}
bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { const auto sideLayout = SETTINGS.sideButtonLayout;
switch (button) { case Button::Back: // Logical Back maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonBack); case Button::Confirm: // Logical Confirm maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonConfirm); case Button::Left: // Logical Left maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonLeft); case Button::Right: // Logical Right maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonRight); case Button::Up: // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_UP); case Button::Down: // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_DOWN); case Button::Power: // Power button bypasses remapping. return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: // Reader page navigation uses side buttons and can be swapped via settings. switch (sideLayout) { case CrossPointSettings::PREV_NEXT: return (gpio.*fn)(isNavDirectionSwapped() ? HalGPIO::BTN_DOWN : HalGPIO::BTN_UP); case CrossPointSettings::NEXT_PREV: return (gpio.*fn)(isNavDirectionSwapped() ? HalGPIO::BTN_UP : HalGPIO::BTN_DOWN); case CrossPointSettings::PREV_PREV: return (gpio.*fn)(HalGPIO::BTN_UP) || (gpio.*fn)(HalGPIO::BTN_DOWN); case CrossPointSettings::NEXT_NEXT: case CrossPointSettings::SIDE_BUTTONS_DISABLED: default: return false; } case Button::PageForward: // Reader page navigation uses side buttons and can be swapped via settings. switch (sideLayout) { case CrossPointSettings::PREV_NEXT: return (gpio.*fn)(isNavDirectionSwapped() ? HalGPIO::BTN_UP : HalGPIO::BTN_DOWN); case CrossPointSettings::NEXT_PREV: return (gpio.*fn)(isNavDirectionSwapped() ? HalGPIO::BTN_DOWN : HalGPIO::BTN_UP); case CrossPointSettings::NEXT_NEXT: return (gpio.*fn)(HalGPIO::BTN_UP) || (gpio.*fn)(HalGPIO::BTN_DOWN); case CrossPointSettings::PREV_PREV: case CrossPointSettings::SIDE_BUTTONS_DISABLED: default: return false; } case Button::NavNext: // Logical "next item" navigation: side Down + front Right, with the control axis flipped in // INVERTED / LANDSCAPE_CCW under the live orientation policy, matching the rotated hint labels. return isNavDirectionSwapped() ? (mapButton(Button::Up, fn) || mapButton(Button::Left, fn)) : (mapButton(Button::Down, fn) || mapButton(Button::Right, fn)); case Button::NavPrevious: // Logical "previous item" navigation: side Up + front Left, axis-flipped in the same orientations. return isNavDirectionSwapped() ? (mapButton(Button::Down, fn) || mapButton(Button::Right, fn)) : (mapButton(Button::Up, fn) || mapButton(Button::Left, fn)); case Button::ScreenLeft: case Button::ScreenRight: case Button::ScreenUp: case Button::ScreenDown: return mapButton(mapScreenDirection(button), fn); }
return false;}
namespace {constexpr unsigned long TOUCH_DOWN_SELECT_DELAY_MS = 90;constexpr unsigned long TOUCH_HELD_OVERRIDE_WINDOW_MS = 250;} // namespace
bool MappedInputManager::hasTouch() const { return gpio.hasTouch(); }
void MappedInputManager::rememberTouchHeldTime() const { touchHeldOverrideValid = true; touchHeldOverrideMs = gpio.lastTouchHeldMs(); touchHeldOverrideAt = millis();}
bool MappedInputManager::wasScreenTapped(int& x, int& y) const { float nx = 0.0f; float ny = 0.0f; if (!gpio.wasTouchTap(nx, ny)) return false; renderer.tapToLogical(nx, ny, x, y); rememberTouchHeldTime(); return true;}
bool MappedInputManager::wasScreenTouchDown(int& x, int& y) const { float nx = 0.0f; float ny = 0.0f; unsigned long heldMs = 0; if (!gpio.isTouchTapCandidate(nx, ny, heldMs)) return false; if (heldMs < TOUCH_DOWN_SELECT_DELAY_MS) return false; renderer.tapToLogical(nx, ny, x, y); return true;}
bool MappedInputManager::wasScreenLongPress(int& x, int& y) const { float nx = 0.0f; float ny = 0.0f; if (!gpio.wasTouchLongPress(nx, ny)) return false; // Consuming the long-press implies acting on it: suppress the rest of the // contact so the finger lift can't also tap whatever the action opened. gpio.suppressTouchContact(); renderer.tapToLogical(nx, ny, x, y); return true;}
bool MappedInputManager::isScreenTouchHeld(int& x, int& y) const { // Live contact position while the finger is down (no tap-slop gate) — drag tracking. float nx = 0.0f; float ny = 0.0f; if (!gpio.isTouchHeldAt(nx, ny)) return false; renderer.tapToLogical(nx, ny, x, y); return true;}
bool MappedInputManager::wasScreenTouchReleased() const { return gpio.wasTouchReleased(); }
bool MappedInputManager::wasTapInRect(const int x, const int y, const int width, const int height) const { int tx = 0; int ty = 0; return wasScreenTapped(tx, ty) && tx >= x && tx < x + width && ty >= y && ty < y + height;}
MappedInputManager::RowTouch MappedInputManager::rowTouch(int& row, const int top, const int rowStep, const int rowCount, const int xStart, const int xEnd, const int rowHeight) const { if (rowStep <= 0 || rowCount <= 0) return RowTouch::None; const auto hit = [&](const int x, const int y) { if (x < xStart || x >= xEnd || y < top) return false; const int r = (y - top) / rowStep; if (r >= rowCount) return false; if (rowHeight > 0 && (y - top) % rowStep >= rowHeight) return false; row = r; return true; }; int x = 0; int y = 0; if (wasScreenTouchDown(x, y) && hit(x, y)) return RowTouch::Down; if (wasScreenTapped(x, y) && hit(x, y)) return RowTouch::Tap; return RowTouch::None;}
MappedInputManager::RowTouch MappedInputManager::colTouch(int& col, const int left, const int colStep, const int colCount, const int yStart, const int yEnd, const int colWidth) const { if (colStep <= 0 || colCount <= 0) return RowTouch::None; const auto hit = [&](const int x, const int y) { if (y < yStart || y >= yEnd || x < left) return false; const int c = (x - left) / colStep; if (c >= colCount) return false; if (colWidth > 0 && (x - left) % colStep >= colWidth) return false; col = c; return true; }; int x = 0; int y = 0; if (wasScreenTouchDown(x, y) && hit(x, y)) return RowTouch::Down; if (wasScreenTapped(x, y) && hit(x, y)) return RowTouch::Tap; return RowTouch::None;}
bool MappedInputManager::decodeSwipe(int& sx, int& sy, int& ex, int& ey) const { float nxs = 0.0f; float nys = 0.0f; float nxe = 0.0f; float nye = 0.0f; if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return false; renderer.tapToLogical(nxs, nys, sx, sy); renderer.tapToLogical(nxe, nye, ex, ey); return true;}
MappedInputManager::SwipeDir MappedInputManager::wasSwipe() const { int sx = 0; int sy = 0; int ex = 0; int ey = 0; if (!decodeSwipe(sx, sy, ex, ey)) return SwipeDir::None; switch (fui::swipeDirection(sx, sy, ex, ey)) { case fui::SwipeDir::Left: return SwipeDir::Left; case fui::SwipeDir::Right: return SwipeDir::Right; case fui::SwipeDir::Up: return SwipeDir::Up; case fui::SwipeDir::Down: return SwipeDir::Down; default: return SwipeDir::None; }}
// Edge classification (which swipe counts as an edge gesture) lives in the// SDK; only the MEANING of each edge — back, menu, home, light panel, and the// home-key remap — is decided here.bool MappedInputManager::wasEdgeSwipe(const freeink::ui::ScreenEdge edge) const { int sx = 0; int sy = 0; int ex = 0; int ey = 0; if (!decodeSwipe(sx, sy, ex, ey)) return false; const bool hit = fui::edgeSwipe(edge, sx, sy, ex, ey, renderer.getScreenWidth(), renderer.getScreenHeight()); if (hit) rememberTouchHeldTime(); return hit;}
bool MappedInputManager::wasBackGesture() const { // Tap on the header back button (rect recorded by BaseTheme::drawHeader; // empty on screens without one). Folded into Button::Back alongside the // swipe so every activity's existing Back handling picks it up. int tapX = 0; int tapY = 0; if (wasScreenTapped(tapX, tapY) && HeaderBackTapTarget::contains(tapX, tapY)) { rememberTouchHeldTime(); return true; } // Back = left-to-right swipe starting near the left edge. Edge-anchored so that // mid-screen horizontal swipes stay available to activities that consume // SwipeDir::Left/Right (e.g. percent selection, image viewer). return wasEdgeSwipe(fui::ScreenEdge::Left);}
bool MappedInputManager::wasTopEdgeDownSwipe() const { return wasEdgeSwipe(fui::ScreenEdge::Top); }
bool MappedInputManager::wasBottomEdgeUpSwipe() const { return wasEdgeSwipe(fui::ScreenEdge::Bottom); }
bool MappedInputManager::wasMenuGesture() const { return wasTopEdgeDownSwipe(); }
bool MappedInputManager::wasReaderMenuSwipeUp() const { return gpio.hasHomeKey() && wasBottomEdgeUpSwipe(); }
bool MappedInputManager::wasHomeGesture() const { return gpio.hasHomeKey() ? homeAction == HomeButtonAction::Home : wasBottomEdgeUpSwipe();}
bool MappedInputManager::wasLightPanelGesture() const { // On lightless boards the same edge remains available to the reader menu. return Frontlight.present() && wasTopEdgeDownSwipe();}
#if FREEINK_CAP_TOUCHbool MappedInputManager::wasPowerConfirmClick() const { if (!gpio.hasTouch() || SETTINGS.shortPwrBtn != CrossPointSettings::SHORT_PWRBTN::PWR_CONFIRM) return false; // Wait out the X4 Pro's frontlight double-click window before treating its // first release as Confirm. With the shortcut disabled, and on other touch // boards, the release counts directly. if (BoardConfig::isX4Pro() && SETTINGS.doubleClickPwrLight) return powerConfirmClickFrame; return gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() <= SETTINGS.getPowerButtonDuration();}#endif
bool MappedInputManager::wasPressed(const Button button) const { if (button == Button::Confirm && homeAction == HomeButtonAction::Confirm) return true; if (button == Button::Back && wasBackGesture()) return true;#if FREEINK_CAP_TOUCH if (button == Button::Confirm && wasPowerConfirmClick()) return true;#endif return mapButton(button, &HalGPIO::wasPressed);}
bool MappedInputManager::wasReleased(const Button button) const { if (button == Button::Confirm && homeAction == HomeButtonAction::Confirm) return true; if (button == Button::Back && wasBackGesture()) return true;#if FREEINK_CAP_TOUCH if (button == Button::Confirm && wasPowerConfirmClick()) return true;#endif return mapButton(button, &HalGPIO::wasReleased);}
bool MappedInputManager::wasLongPressed(const Button button, const unsigned long thresholdMs) const { if (!isPressed(button)) return false; const uint16_t bit = 1u << static_cast<uint8_t>(button); if ((longPressFiredButtons & bit) != 0 || getHeldTime() < thresholdMs) return false; longPressFiredButtons |= bit; suppressNextRelease(button); return true;}
void MappedInputManager::suppressNextRelease(const Button button) const { suppressedReleaseButtons |= 1u << static_cast<uint8_t>(button);}
bool MappedInputManager::consumeSuppressedRelease() const { uint16_t released = 0; for (uint8_t value = 0; value <= static_cast<uint8_t>(Button::ScreenDown); ++value) { const uint16_t bit = 1u << value; if ((suppressedReleaseButtons & bit) != 0 && mapButton(static_cast<Button>(value), &HalGPIO::wasReleased)) { released |= bit; } } suppressedReleaseButtons &= ~released; return released != 0;}
bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); }
bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); }
bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); }
unsigned long MappedInputManager::getHeldTime() const { // A mapped action has its own meaning, independent of the contact duration. if (homeAction != HomeButtonAction::Ignore) return 0; if (!gpio.wasAnyPressed() && !gpio.wasAnyReleased() && touchHeldOverrideValid && millis() - touchHeldOverrideAt <= TOUCH_HELD_OVERRIDE_WINDOW_MS) { return touchHeldOverrideMs; } touchHeldOverrideValid = false; return gpio.getHeldTime();}
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const { // Swap previous/next labels to match the page turn direction swap in INVERTED and LANDSCAPE_CCW. const bool swapLabels = isNavDirectionSwapped(); const char* leftLabel = swapLabels ? next : previous; const char* rightLabel = swapLabels ? previous : next;
return mapFrontLabels(back, confirm, leftLabel, rightLabel);}
MappedInputManager::Labels MappedInputManager::mapDirectionalLabels(const char* back, const char* confirm, const char* left, const char* right, const char* up, const char* down) const { const auto labelForButton = [&](const Button rawButton) { if (mapScreenDirection(Button::ScreenLeft) == rawButton) return left; if (mapScreenDirection(Button::ScreenRight) == rawButton) return right; if (mapScreenDirection(Button::ScreenUp) == rawButton) return up; if (mapScreenDirection(Button::ScreenDown) == rawButton) return down; return ""; }; return mapFrontLabels(back, confirm, labelForButton(Button::Left), labelForButton(Button::Right));}
MappedInputManager::Labels MappedInputManager::mapFrontLabels(const char* back, const char* confirm, const char* left, const char* right) const { // Build the label order based on the configured hardware mapping. auto labelForHardware = [&](uint8_t hw) -> const char* { // Compare against configured logical roles and return the matching label. if (hw == SETTINGS.frontButtonBack) { return back; } if (hw == SETTINGS.frontButtonConfirm) { return confirm; } if (hw == SETTINGS.frontButtonLeft) { return left; } if (hw == SETTINGS.frontButtonRight) { return right; } return ""; };
return {labelForHardware(HalGPIO::BTN_BACK), labelForHardware(HalGPIO::BTN_CONFIRM), labelForHardware(HalGPIO::BTN_LEFT), labelForHardware(HalGPIO::BTN_RIGHT)};}
int MappedInputManager::getPressedFrontButton() const { // Scan the raw front buttons in hardware order. // This bypasses remapping so the remap activity can capture physical presses. if (gpio.wasPressed(HalGPIO::BTN_BACK)) { return HalGPIO::BTN_BACK; } if (gpio.wasPressed(HalGPIO::BTN_CONFIRM)) { return HalGPIO::BTN_CONFIRM; } if (gpio.wasPressed(HalGPIO::BTN_LEFT)) { return HalGPIO::BTN_LEFT; } if (gpio.wasPressed(HalGPIO::BTN_RIGHT)) { return HalGPIO::BTN_RIGHT; } return -1;}