Merge pull request #143 from jpirnay/fix-stalebuttons

fix: Refactor / remove duplicate / stale button treatments
This commit is contained in:
jpirnay
2026-04-27 14:51:04 +02:00
committed by GitHub
22 changed files with 712 additions and 596 deletions
+7
View File
@@ -33,6 +33,13 @@ void ButtonEventManager::pushEvent(const Button button, const PressType type) {
eventTail = next; eventTail = next;
} }
void ButtonEventManager::pushEventFront(const Button button, const PressType type) {
const int prev = (eventHead - 1 + EVENT_BUF) % EVENT_BUF;
if (prev == eventTail) return; // buffer full
eventHead = prev;
eventBuf[eventHead] = {button, type};
}
bool ButtonEventManager::consumeEvent(ButtonEvent& out) { bool ButtonEventManager::consumeEvent(ButtonEvent& out) {
if (eventHead == eventTail) return false; if (eventHead == eventTail) return false;
out = eventBuf[eventHead]; out = eventBuf[eventHead];
+5 -1
View File
@@ -34,7 +34,7 @@ class ButtonEventManager {
}; };
// Timing constants (milliseconds) // Timing constants (milliseconds)
static constexpr unsigned long LONG_PRESS_MS = 600; static constexpr unsigned long LONG_PRESS_MS = 1000;
static constexpr unsigned long DOUBLE_WINDOW_MS = 300; static constexpr unsigned long DOUBLE_WINDOW_MS = 300;
explicit ButtonEventManager(MappedInputManager& input) : input(input) {} explicit ButtonEventManager(MappedInputManager& input) : input(input) {}
@@ -49,6 +49,10 @@ class ButtonEventManager {
// Reset all per-button FSMs. Call on activity transitions to prevent bleed-through. // Reset all per-button FSMs. Call on activity transitions to prevent bleed-through.
void drain(); void drain();
// Preserve a default event for activity processing after main loop dispatch.
// This is used when the configured action is BTN_DEFAULT.
void pushEventFront(Button button, PressType type);
// Returns true if a double-click action is configured for this button. // Returns true if a double-click action is configured for this button.
// ButtonEventManager queries CrossPointSettings internally. // ButtonEventManager queries CrossPointSettings internally.
static bool hasDoubleAction(Button button); static bool hasDoubleAction(Button button);
+10 -1
View File
@@ -58,6 +58,11 @@ void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
} }
} }
void enforceFixedShortActions(CrossPointSettings& settings) {
settings.btnShortBack = static_cast<uint8_t>(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT);
settings.btnShortConfirm = static_cast<uint8_t>(CrossPointSettings::BUTTON_ACTION::BTN_DEFAULT);
}
} // namespace } // namespace
void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) { void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) {
@@ -88,13 +93,16 @@ bool CrossPointSettings::loadFromFile() {
if (!json.isEmpty()) { if (!json.isEmpty()) {
bool resave = false; bool resave = false;
bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave); bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave);
if (result && resave) { if (result) {
enforceFixedShortActions(*this);
if (resave) {
if (saveToFile()) { if (saveToFile()) {
LOG_DBG("CPS", "Resaved settings to update format"); LOG_DBG("CPS", "Resaved settings to update format");
} else { } else {
LOG_ERR("CPS", "Failed to resave settings after format update"); LOG_ERR("CPS", "Failed to resave settings after format update");
} }
} }
}
return result; return result;
} }
} }
@@ -102,6 +110,7 @@ bool CrossPointSettings::loadFromFile() {
// Fall back to binary migration // Fall back to binary migration
if (Storage.exists(SETTINGS_FILE_BIN)) { if (Storage.exists(SETTINGS_FILE_BIN)) {
if (loadFromBinaryFile()) { if (loadFromBinaryFile()) {
enforceFixedShortActions(*this);
if (saveToFile()) { if (saveToFile()) {
Storage.rename(SETTINGS_FILE_BIN, SETTINGS_FILE_BAK); Storage.rename(SETTINGS_FILE_BIN, SETTINGS_FILE_BAK);
LOG_DBG("CPS", "Migrated settings.bin to settings.json"); LOG_DBG("CPS", "Migrated settings.bin to settings.json");
+3 -3
View File
@@ -144,8 +144,8 @@ inline const std::vector<SettingInfo> list = {
StrId::STR_BTN_ACT_KOREADER_SYNC StrId::STR_BTN_ACT_KOREADER_SYNC
// Back button: short=exit reader, double=ignore, long=go home // Back button: short=exit reader, double=ignore, long=go home
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, {StrId::STR_BTN_DEF_EXIT_READER},
{StrId::STR_BTN_DEF_EXIT_READER, BTN_ACT_OPTIONS}, "btnShortBack", StrId::STR_CAT_CONTROLS) "btnShortBack", StrId::STR_CAT_CONTROLS)
.withSubcategory(StrId::STR_MENU_BTN_ACTIONS) .withSubcategory(StrId::STR_MENU_BTN_ACTIONS)
.withSubmenu(StrId::STR_BTN_BACK), .withSubmenu(StrId::STR_BTN_BACK),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleBack, SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleBack,
@@ -156,7 +156,7 @@ inline const std::vector<SettingInfo> list = {
.withSubmenu(StrId::STR_BTN_BACK), .withSubmenu(StrId::STR_BTN_BACK),
// Confirm button: short=reader menu, double=ignore, long=KOReader sync // Confirm button: short=reader menu, double=ignore, long=KOReader sync
SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortConfirm, SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortConfirm,
{StrId::STR_BTN_DEF_READER_MENU, BTN_ACT_OPTIONS}, "btnShortConfirm", StrId::STR_CAT_CONTROLS) {StrId::STR_BTN_DEF_READER_MENU}, "btnShortConfirm", StrId::STR_CAT_CONTROLS)
.withSubmenu(StrId::STR_BTN_CONFIRM), .withSubmenu(StrId::STR_BTN_CONFIRM),
SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleConfirm, SettingInfo::Enum(StrId::STR_BTN_DOUBLE_PRESS, &CrossPointSettings::btnDoubleConfirm,
{StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleConfirm", StrId::STR_CAT_CONTROLS) {StrId::STR_BTN_DEF_IGNORE, BTN_ACT_OPTIONS}, "btnDoubleConfirm", StrId::STR_CAT_CONTROLS)
+11 -16
View File
@@ -16,10 +16,6 @@
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
namespace {
constexpr unsigned long GO_HOME_MS = 1000;
} // namespace
void sortFileList(std::vector<std::string>& strs) { void sortFileList(std::vector<std::string>& strs) {
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
// Directories first // Directories first
@@ -141,14 +137,14 @@ void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) {
void FileBrowserActivity::loop() { void FileBrowserActivity::loop() {
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
// Long press BACK always navigates to home ButtonEventManager::ButtonEvent ev;
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Back) {
if (ev.type == ButtonEventManager::PressType::Long) {
onGoHome(); onGoHome();
return; return;
} }
if (ev.type == ButtonEventManager::PressType::Short) {
// Short press BACK goes up one directory (if not root) or home (at root)
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < GO_HOME_MS) {
if (basepath != "/") { if (basepath != "/") {
const std::string oldPath = basepath; const std::string oldPath = basepath;
basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
@@ -164,9 +160,9 @@ void FileBrowserActivity::loop() {
} }
return; return;
} }
}
// Confirm short press opens selected entry; long press does nothing if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() < GO_HOME_MS) {
if (files.empty()) return; if (files.empty()) return;
const std::string& entry = files[selectorIndex]; const std::string& entry = files[selectorIndex];
@@ -191,9 +187,8 @@ void FileBrowserActivity::loop() {
return; return;
} }
// Left short press does nothing; long press deletes selected file after confirmation if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Long) {
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { if (files.empty()) {
if (mappedInput.getHeldTime() < GO_HOME_MS || files.empty()) {
return; return;
} }
@@ -234,8 +229,7 @@ void FileBrowserActivity::loop() {
return; return;
} }
// Right opens the info page for epub files if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) {
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
if (files.empty()) return; if (files.empty()) return;
const std::string& entry = files[selectorIndex]; const std::string& entry = files[selectorIndex];
if (entry.back() != '/' && (FsHelpers::hasEpubExtension(entry) || FsHelpers::hasXtcExtension(entry))) { if (entry.back() != '/' && (FsHelpers::hasEpubExtension(entry) || FsHelpers::hasXtcExtension(entry))) {
@@ -246,6 +240,7 @@ void FileBrowserActivity::loop() {
} }
return; return;
} }
}
// Up/Down side buttons navigate the list // Up/Down side buttons navigate the list
const int listSize = static_cast<int>(files.size()); const int listSize = static_cast<int>(files.size());
+73 -131
View File
@@ -219,12 +219,14 @@ void EpubReaderActivity::loop() {
} }
if (inputDrainGuard.shouldDrain(mappedInput)) { if (inputDrainGuard.shouldDrain(mappedInput)) {
buttonEvents.drain();
return; return;
} }
if (automaticPageTurnActive) { if (automaticPageTurnActive) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) ||
mappedInput.wasReleased(MappedInputManager::Button::Back)) { mappedInput.wasReleased(MappedInputManager::Button::Back)) {
buttonEvents.drain();
automaticPageTurnActive = false; automaticPageTurnActive = false;
// updates chapter title space to indicate page turn disabled // updates chapter title space to indicate page turn disabled
requestUpdate(); requestUpdate();
@@ -248,59 +250,30 @@ void EpubReaderActivity::loop() {
} }
} }
// Long press CONFIRM (1s+) goes directly to KOReader sync when credentials are configured. bool delayedPrevTurn = false;
// We intentionally keep long-press on the richer compare flow so advanced bool delayedNextTurn = false;
// conflict-resolution behavior stays available even after simplifying menu UX. using BA = CrossPointSettings::BUTTON_ACTION;
// Without credentials, fall through to the regular menu on release.
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && ButtonEventManager::ButtonEvent ev;
mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) { while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Confirm) {
if (ev.type == ButtonEventManager::PressType::Long && KOREADER_STORE.hasCredentials()) {
launchKOReaderSync(SyncLaunchMode::COMPARE); launchKOReaderSync(SyncLaunchMode::COMPARE);
return; return;
} }
if (ev.type == ButtonEventManager::PressType::Short) {
// Short press CONFIRM enters reader menu activity. openReaderMenu();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && return;
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
} }
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
static_cast<uint16_t>(section->currentPage));
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride,
SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred),
[this](const ActivityResult& result) {
// Always apply orientation/darkness change even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride,
menu.fontFamilyOverride, menu.fontSizeOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
} }
// Long press BACK (1s+) goes to home screen if (ev.button == MappedInputManager::Button::Back) {
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { if (ev.type == ButtonEventManager::PressType::Long) {
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
onGoHome(); onGoHome();
return; return;
} }
if (ev.type == ButtonEventManager::PressType::Short) {
// Short press BACK returns to the calling activity (or restores position if viewing footnote)
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
if (footnoteDepth > 0) { if (footnoteDepth > 0) {
restoreSavedPosition(); restoreSavedPosition();
return; return;
@@ -309,8 +282,34 @@ void EpubReaderActivity::loop() {
finish(); finish();
return; return;
} }
}
if (ev.type == ButtonEventManager::PressType::Short) {
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
delayedPrevTurn = true;
continue;
}
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
delayedNextTurn = true;
continue;
}
}
}
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
if (!prevTriggered && !nextTriggered) {
if (!delayedPrevTurn && !delayedNextTurn) {
return;
}
prevTriggered = delayedPrevTurn;
nextTriggered = delayedNextTurn;
}
if (!prevTriggered && !nextTriggered) { if (!prevTriggered && !nextTriggered) {
return; return;
} }
@@ -327,65 +326,6 @@ void EpubReaderActivity::loop() {
return; return;
} }
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
// (see BookMetadataCache), so they're treated as continuations of the last chapter.
// At the boundaries: skipping forward past the last TOC entry jumps to end-of-book
// (clamped in render()); skipping backward before the first TOC entry jumps to the
// spine before the current chapter's first spine (clamped to 0 in render()).
if (skipChapter) {
lastPageTurnTime = millis();
{
RenderLock lock(*this);
if (section && section->pageCount > 0) {
const int curTocIndex = section->getTocIndexForPage(section->currentPage);
const int nextTocIndex = nextTriggered ? curTocIndex + 1 : curTocIndex - 1;
if (curTocIndex < 0) {
// No TOC entry for this spine, fall back to spine-level skip
nextPageNumber = 0;
currentSpineIndex = nextTriggered ? 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 {
LOG_DBG("ERS", "No page boundary for TOC %d in spine %d, staying on current page", nextTocIndex,
currentSpineIndex);
}
} else {
pendingTocIndex = nextTocIndex;
nextPageNumber = 0;
currentSpineIndex = newSpineIndex;
section.reset();
}
} else if (nextTriggered) {
// Beyond last TOC entry, go to end of book
nextPageNumber = 0;
currentSpineIndex = epub->getSpineItemsCount();
section.reset();
} else {
// Before first TOC entry, skip to spine before the current chapter
nextPageNumber = 0;
currentSpineIndex = epub->getTocItem(curTocIndex).spineIndex - 1;
section.reset();
}
} else {
nextPageNumber = 0;
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
section.reset();
}
}
requestUpdate();
return;
}
// No current section, attempt to rerender the book // No current section, attempt to rerender the book
if (!section) { if (!section) {
requestUpdate(); requestUpdate();
@@ -1774,6 +1714,36 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
return true; return true;
} }
void EpubReaderActivity::openReaderMenu() {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
static_cast<uint16_t>(section->currentPage));
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
bookFontSizeOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred),
[this](const ActivityResult& result) {
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
menu.fontSizeOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
}
void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) { void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
using BA = CrossPointSettings::BUTTON_ACTION; using BA = CrossPointSettings::BUTTON_ACTION;
switch (action) { switch (action) {
@@ -1892,35 +1862,7 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
break; break;
case BA::BTN_READER_MENU: case BA::BTN_READER_MENU:
if (epub) { if (epub) {
const int currentPage = section ? section->currentPage + 1 : 0; openReaderMenu();
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress =
static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
static_cast<uint16_t>(section->currentPage));
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness,
!bookmarkStore.isEmpty(), isCurrentPageStarred),
[this](const ActivityResult& result) {
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
menu.fontSizeOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
} }
break; break;
case BA::BTN_KOREADER_SYNC: case BA::BTN_KOREADER_SYNC:
@@ -166,6 +166,7 @@ class EpubReaderActivity final : public Activity {
void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride, void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride,
int8_t fontSizeOverride); int8_t fontSizeOverride);
void openReaderMenu();
bool getEffectiveEmbeddedStyle() const; bool getEffectiveEmbeddedStyle() const;
uint8_t getEffectiveImageRendering() const; uint8_t getEffectiveImageRendering() const;
int getEffectiveReaderFontId() const; int getEffectiveReaderFontId() const;
@@ -42,7 +42,9 @@ void EpubReaderChapterSelectionActivity::loop() {
const int pageItems = getPageItems(); const int pageItems = getPageItems();
const int totalItems = getTotalItems(); const int totalItems = getTotalItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex); const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex);
if (newSpineIndex == -1) { if (newSpineIndex == -1) {
ActivityResult result; ActivityResult result;
@@ -53,11 +55,27 @@ void EpubReaderChapterSelectionActivity::loop() {
setResult(ChapterResult{newSpineIndex, selectorIndex}); setResult(ChapterResult{newSpineIndex, selectorIndex});
finish(); finish();
} }
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { return;
}
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
ActivityResult result; ActivityResult result;
result.isCancelled = true; result.isCancelled = true;
setResult(std::move(result)); setResult(std::move(result));
finish(); finish();
return;
}
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
ev.type == ButtonEventManager::PressType::Short) {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
requestUpdate();
return;
}
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
ev.type == ButtonEventManager::PressType::Short) {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
requestUpdate();
return;
}
} }
buttonNavigator.onNextRelease([this, totalItems] { buttonNavigator.onNextRelease([this, totalItems] {
@@ -18,7 +18,9 @@ void EpubReaderFootnotesActivity::onEnter() {
void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); } void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); }
void EpubReaderFootnotesActivity::loop() { void EpubReaderFootnotesActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
ActivityResult result; ActivityResult result;
result.isCancelled = true; result.isCancelled = true;
setResult(std::move(result)); setResult(std::move(result));
@@ -26,7 +28,7 @@ void EpubReaderFootnotesActivity::loop() {
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
if (selectedIndex >= 0 && selectedIndex < static_cast<int>(footnotes.size())) { if (selectedIndex >= 0 && selectedIndex < static_cast<int>(footnotes.size())) {
setResult(FootnoteResult{footnotes[selectedIndex].href}); setResult(FootnoteResult{footnotes[selectedIndex].href});
finish(); finish();
@@ -34,19 +36,35 @@ void EpubReaderFootnotesActivity::loop() {
return; return;
} }
buttonNavigator.onNext([this] { if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
if (!footnotes.empty()) { ev.type == ButtonEventManager::PressType::Short) {
selectedIndex = (selectedIndex + 1) % footnotes.size(); advanceSelection(-1);
requestUpdate(); return;
} }
});
buttonNavigator.onPrevious([this] { if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
if (!footnotes.empty()) { ev.type == ButtonEventManager::PressType::Short) {
selectedIndex = (selectedIndex - 1 + footnotes.size()) % footnotes.size(); advanceSelection(1);
requestUpdate(); return;
} }
}); }
buttonNavigator.onNextRelease([this] { advanceSelection(1); });
buttonNavigator.onPreviousRelease([this] { advanceSelection(-1); });
buttonNavigator.onNextContinuous([this] { advanceSelection(1); });
buttonNavigator.onPreviousContinuous([this] { advanceSelection(-1); });
}
void EpubReaderFootnotesActivity::advanceSelection(int delta) {
if (footnotes.empty()) {
return;
}
const int n = static_cast<int>(footnotes.size());
selectedIndex = ((selectedIndex + delta) % n + n) % n;
requestUpdate();
} }
void EpubReaderFootnotesActivity::render(RenderLock&&) { void EpubReaderFootnotesActivity::render(RenderLock&&) {
@@ -21,6 +21,7 @@ class EpubReaderFootnotesActivity final : public Activity {
void render(RenderLock&&) override; void render(RenderLock&&) override;
private: private:
void advanceSelection(int delta);
const std::vector<FootnoteEntry>& footnotes; const std::vector<FootnoteEntry>& footnotes;
int selectedIndex = 0; int selectedIndex = 0;
int scrollOffset = 0; int scrollOffset = 0;
@@ -34,7 +34,9 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
void EpubReaderPercentSelectionActivity::loop() { void EpubReaderPercentSelectionActivity::loop() {
// Back cancels, confirm selects, arrows adjust the percent. // Back cancels, confirm selects, arrows adjust the percent.
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
ActivityResult result; ActivityResult result;
result.isCancelled = true; result.isCancelled = true;
setResult(std::move(result)); setResult(std::move(result));
@@ -42,14 +44,24 @@ void EpubReaderPercentSelectionActivity::loop() {
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
setResult(PercentResult{percent}); setResult(PercentResult{percent});
finish(); finish();
return; return;
} }
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); }); if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); }); ev.type == ButtonEventManager::PressType::Short) {
adjustPercent(-kSmallStep);
return;
}
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
ev.type == ButtonEventManager::PressType::Short) {
adjustPercent(kSmallStep);
return;
}
}
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); });
+26 -19
View File
@@ -23,7 +23,6 @@
namespace { namespace {
constexpr size_t CHUNK_SIZE = 8 * 1024; constexpr size_t CHUNK_SIZE = 8 * 1024;
constexpr size_t MAX_LINE_LENGTH = 64 * 1024; constexpr size_t MAX_LINE_LENGTH = 64 * 1024;
constexpr unsigned long HEADING_SKIP_MS = 700;
constexpr uint32_t CACHE_MAGIC = 0x4D4B4449; // "MKDI" constexpr uint32_t CACHE_MAGIC = 0x4D4B4449; // "MKDI"
constexpr uint8_t CACHE_VERSION = 3; // Bumped: nested list indent + task checkboxes constexpr uint8_t CACHE_VERSION = 3; // Bumped: nested list indent + task checkboxes
@@ -39,6 +38,8 @@ static std::string flattenHeadingText(const MdParser::ParsedLine& parsed) {
void MdReaderActivity::onEnter() { void MdReaderActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
inputDrainGuard.arm();
if (!txt) { if (!txt) {
return; return;
} }
@@ -222,18 +223,26 @@ void MdReaderActivity::onExit() {
} }
void MdReaderActivity::loop() { void MdReaderActivity::loop() {
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { if (inputDrainGuard.shouldDrain(mappedInput)) {
buttonEvents.drain();
return;
}
ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Back) {
if (ev.type == ButtonEventManager::PressType::Long) {
activityManager.goToFileBrowser(txt ? txt->getPath() : ""); activityManager.goToFileBrowser(txt ? txt->getPath() : "");
return; return;
} }
if (ev.type == ButtonEventManager::PressType::Short) {
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
onGoHome(); onGoHome();
return; return;
} }
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !headings.empty()) { if (!headings.empty() && ev.button == MappedInputManager::Button::Confirm &&
ev.type == ButtonEventManager::PressType::Short) {
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult( startActivityForResult(
@@ -248,22 +257,18 @@ void MdReaderActivity::loop() {
return; return;
} }
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
if (!prevTriggered && !nextTriggered) { ev.type == ButtonEventManager::PressType::Short) {
return; if (currentPage > 0) {
}
const bool headingSkip = mappedInput.getHeldTime() > HEADING_SKIP_MS;
if (headingSkip && !headings.empty()) {
jumpToHeading(nextTriggered);
return;
}
if (prevTriggered && currentPage > 0) {
currentPage--; currentPage--;
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
requestUpdate(); requestUpdate();
} else if (nextTriggered) { }
return;
}
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
ev.type == ButtonEventManager::PressType::Short) {
if (currentPage < totalPages - 1) { if (currentPage < totalPages - 1) {
currentPage++; currentPage++;
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]); currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
@@ -271,6 +276,8 @@ void MdReaderActivity::loop() {
} else { } else {
onGoHome(); onGoHome();
} }
return;
}
} }
} }
+2
View File
@@ -6,6 +6,7 @@
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "ReaderUtils.h"
#include "activities/Activity.h" #include "activities/Activity.h"
struct MdHeading { struct MdHeading {
@@ -72,6 +73,7 @@ class MdReaderActivity final : public Activity {
void assignHeadingPageNumbers(); void assignHeadingPageNumbers();
int getHeadingIndexForOffset(size_t offset) const; int getHeadingIndexForOffset(size_t offset) const;
void jumpToHeading(bool next); void jumpToHeading(bool next);
ReaderUtils::InputDrainGuard inputDrainGuard;
// Word-wrap a parsed markdown line into one or more RenderedLines. // Word-wrap a parsed markdown line into one or more RenderedLines.
// Returns true if all content was emitted, false if truncated by maxLines. // Returns true if all content was emitted, false if truncated by maxLines.
@@ -34,7 +34,9 @@ void MdReaderTocSelectionActivity::loop() {
const int pageItems = getPageItems(); const int pageItems = getPageItems();
const int totalItems = getTotalItems(); const int totalItems = getTotalItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
if (selectorIndex >= 0 && selectorIndex < totalItems) { if (selectorIndex >= 0 && selectorIndex < totalItems) {
setResult(PageResult{static_cast<uint32_t>(headings[selectorIndex].pageIndex)}); setResult(PageResult{static_cast<uint32_t>(headings[selectorIndex].pageIndex)});
} else { } else {
@@ -46,7 +48,7 @@ void MdReaderTocSelectionActivity::loop() {
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
ActivityResult result; ActivityResult result;
result.isCancelled = true; result.isCancelled = true;
setResult(std::move(result)); setResult(std::move(result));
@@ -54,6 +56,21 @@ void MdReaderTocSelectionActivity::loop() {
return; return;
} }
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
ev.type == ButtonEventManager::PressType::Short) {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
requestUpdate();
return;
}
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
ev.type == ButtonEventManager::PressType::Short) {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
requestUpdate();
return;
}
}
buttonNavigator.onNextRelease([this, totalItems] { buttonNavigator.onNextRelease([this, totalItems] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
requestUpdate(); requestUpdate();
+5 -2
View File
@@ -16,12 +16,15 @@ void QrDisplayActivity::onEnter() {
void QrDisplayActivity::onExit() { Activity::onExit(); } void QrDisplayActivity::onExit() { Activity::onExit(); }
void QrDisplayActivity::loop() { void QrDisplayActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Back) || ButtonEventManager::ButtonEvent ev;
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { while (buttonEvents.consumeEvent(ev)) {
if ((ev.button == MappedInputManager::Button::Back || ev.button == MappedInputManager::Button::Confirm) &&
ev.type == ButtonEventManager::PressType::Short) {
finish(); finish();
return; return;
} }
} }
}
void QrDisplayActivity::render(RenderLock&&) { void QrDisplayActivity::render(RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
+15 -6
View File
@@ -6,6 +6,7 @@
#include <cstdint> #include <cstdint>
#include "ButtonEventManager.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
namespace ReaderUtils { namespace ReaderUtils {
@@ -90,13 +91,21 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
// Only treat wasReleased as a page turn when the button's short-press action is default. // Only treat wasReleased as a page turn when the button's short-press action is default.
// Non-default short-press actions are dispatched by the global dispatcher in main.cpp; // Non-default short-press actions are dispatched by the global dispatcher in main.cpp;
// counting wasReleased as well would double-fire the action. // counting wasReleased as well would double-fire the action.
// Also suppress immediate page-turns if a double-click action is configured for the button,
// because the button event system delays short events until the double-click window expires.
using BA = CrossPointSettings::BUTTON_ACTION; using BA = CrossPointSettings::BUTTON_ACTION;
const bool prev = const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
(SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageBack)) || !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack) &&
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Left)); input.wasReleased(MappedInputManager::Button::PageBack)) ||
const bool next = (SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
(SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageForward)) || !ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) &&
(SETTINGS.btnShortRight == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Right)); input.wasReleased(MappedInputManager::Button::Left));
const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward) &&
input.wasReleased(MappedInputManager::Button::PageForward)) ||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) &&
input.wasReleased(MappedInputManager::Button::Right));
return {prev, next}; return {prev, next};
} }
+14 -6
View File
@@ -78,7 +78,9 @@ void StarredPagesActivity::deleteSelected() {
void StarredPagesActivity::loop() { void StarredPagesActivity::loop() {
const int totalItems = static_cast<int>(bookmarkStore.getAll().size()); const int totalItems = static_cast<int>(bookmarkStore.getAll().size());
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
ActivityResult result; ActivityResult result;
result.isCancelled = true; result.isCancelled = true;
setResult(std::move(result)); setResult(std::move(result));
@@ -86,24 +88,30 @@ void StarredPagesActivity::loop() {
return; return;
} }
if (totalItems == 0) return; if (totalItems > 0 && ev.button == MappedInputManager::Button::Confirm &&
ev.type == ButtonEventManager::PressType::Short) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto& bm = bookmarkStore.getAll()[selectorIndex]; const auto& bm = bookmarkStore.getAll()[selectorIndex];
setResult(StarredPageResult{bm.spineIndex, bm.pageNumber}); setResult(StarredPageResult{bm.spineIndex, bm.pageNumber});
finish(); finish();
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { if (totalItems > 0 &&
(ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
ev.type == ButtonEventManager::PressType::Short) {
startRename(); startRename();
return; return;
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { if (totalItems > 0 &&
(ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
ev.type == ButtonEventManager::PressType::Short) {
deleteSelected(); deleteSelected();
return; return;
} }
}
if (totalItems == 0) return;
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
+19 -14
View File
@@ -137,26 +137,27 @@ void TxtReaderActivity::onExit() {
void TxtReaderActivity::loop() { void TxtReaderActivity::loop() {
if (inputDrainGuard.shouldDrain(mappedInput)) { if (inputDrainGuard.shouldDrain(mappedInput)) {
buttonEvents.drain();
return; return;
} }
// Long press BACK (1s+) goes to home screen ButtonEventManager::ButtonEvent ev;
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Back) {
if (ev.type == ButtonEventManager::PressType::Long) {
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
onGoHome(); onGoHome();
return; return;
} }
if (ev.type == ButtonEventManager::PressType::Short) {
// Short press BACK returns to the calling activity
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
finish(); finish();
return; return;
} }
}
// Open starred pages list via Confirm button if (!bookmarkStore.isEmpty() && ev.button == MappedInputManager::Button::Confirm &&
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) { ev.type == ButtonEventManager::PressType::Short) {
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore), startActivityForResult(std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore),
[this](const ActivityResult& result) { [this](const ActivityResult& result) {
@@ -171,21 +172,25 @@ void TxtReaderActivity::loop() {
return; return;
} }
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
if (!prevTriggered && !nextTriggered) { ev.type == ButtonEventManager::PressType::Short) {
if (currentPage > 0) {
currentPage--;
requestUpdate();
}
return; return;
} }
if (prevTriggered && currentPage > 0) { if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
currentPage--; ev.type == ButtonEventManager::PressType::Short) {
requestUpdate();
} else if (nextTriggered) {
if (currentPage < totalPages - 1) { if (currentPage < totalPages - 1) {
currentPage++; currentPage++;
requestUpdate(); requestUpdate();
} else { } else {
finish(); finish();
} }
return;
}
} }
} }
+43 -28
View File
@@ -22,11 +22,6 @@
#include "XtcReaderChapterSelectionActivity.h" #include "XtcReaderChapterSelectionActivity.h"
#include "fontIds.h" #include "fontIds.h"
namespace {
constexpr unsigned long skipPageMs = 700;
constexpr unsigned long goHomeMs = 1000;
} // namespace
void XtcReaderActivity::onEnter() { void XtcReaderActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
@@ -62,11 +57,17 @@ void XtcReaderActivity::onExit() {
void XtcReaderActivity::loop() { void XtcReaderActivity::loop() {
if (inputDrainGuard.shouldDrain(mappedInput)) { if (inputDrainGuard.shouldDrain(mappedInput)) {
buttonEvents.drain();
return; return;
} }
// Enter chapter selection activity bool delayedPrevTurn = false;
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { bool delayedNextTurn = false;
using BA = CrossPointSettings::BUTTON_ACTION;
ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult( startActivityForResult(
@@ -76,27 +77,49 @@ void XtcReaderActivity::loop() {
currentPage = std::get<PageResult>(result.data).page; currentPage = std::get<PageResult>(result.data).page;
} }
}); });
return;
} }
} }
// Long press BACK (1s+) goes to home screen if (ev.button == MappedInputManager::Button::Back) {
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { if (ev.type == ButtonEventManager::PressType::Long) {
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
onGoHome(); onGoHome();
return; return;
} }
if (ev.type == ButtonEventManager::PressType::Short) {
// Short press BACK returns to the calling activity
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
ReaderUtils::enforceExitFullRefresh(renderer); ReaderUtils::enforceExitFullRefresh(renderer);
finish(); finish();
return; return;
} }
}
const bool prevTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || if (ev.type == ButtonEventManager::PressType::Short) {
mappedInput.wasReleased(MappedInputManager::Button::Left); if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
const bool nextTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
mappedInput.wasReleased(MappedInputManager::Button::Right); (ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
delayedPrevTurn = true;
continue;
}
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
delayedNextTurn = true;
continue;
}
}
}
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
if (!prevTriggered && !nextTriggered) {
if (!delayedPrevTurn && !delayedNextTurn) {
return;
}
prevTriggered = delayedPrevTurn;
nextTriggered = delayedNextTurn;
}
if (!prevTriggered && !nextTriggered) { if (!prevTriggered && !nextTriggered) {
return; return;
@@ -113,21 +136,13 @@ void XtcReaderActivity::loop() {
return; return;
} }
const bool skipPages = mappedInput.getHeldTime() > skipPageMs;
const int skipAmount = skipPages ? 10 : 1;
if (prevTriggered) { if (prevTriggered) {
if (currentPage >= static_cast<uint32_t>(skipAmount)) { if (currentPage > 0) {
currentPage -= skipAmount; currentPage--;
} else {
currentPage = 0;
}
requestUpdate(); requestUpdate();
} else if (nextTriggered) {
currentPage += skipAmount;
if (currentPage >= xtc->getPageCount()) {
currentPage = xtc->getPageCount(); // Allow showing "End of book"
} }
} else if (nextTriggered) {
currentPage++;
requestUpdate(); requestUpdate();
} }
} }
@@ -5,6 +5,7 @@
#include <algorithm> #include <algorithm>
#include "ButtonEventManager.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
@@ -47,28 +48,58 @@ void XtcReaderChapterSelectionActivity::onEnter() {
void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); } void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); }
void XtcReaderChapterSelectionActivity::loop() { void XtcReaderChapterSelectionActivity::loop() {
if (!xtc) {
return;
}
const int pageItems = getPageItems(); const int pageItems = getPageItems();
const int totalItems = static_cast<int>(xtc->getChapters().size()); const int totalItems = static_cast<int>(xtc->getChapters().size());
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
const auto& chapters = xtc->getChapters(); const auto& chapters = xtc->getChapters();
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) { if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
setResult(PageResult{chapters[selectorIndex].startPage}); setResult(PageResult{chapters[selectorIndex].startPage});
finish(); finish();
} }
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { return;
}
if (ev.button == MappedInputManager::Button::Back && ev.type == ButtonEventManager::PressType::Short) {
ActivityResult result; ActivityResult result;
result.isCancelled = true; result.isCancelled = true;
setResult(std::move(result)); setResult(std::move(result));
finish(); finish();
return;
}
if ((ev.button == MappedInputManager::Button::PageBack || ev.button == MappedInputManager::Button::Left) &&
ev.type == ButtonEventManager::PressType::Short) {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
requestUpdate();
return;
}
if ((ev.button == MappedInputManager::Button::PageForward || ev.button == MappedInputManager::Button::Right) &&
ev.type == ButtonEventManager::PressType::Short) {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
requestUpdate();
return;
}
} }
buttonNavigator.onNextRelease([this, totalItems] { buttonNavigator.onNextRelease([this, totalItems] {
if (ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) ||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) {
return;
}
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
requestUpdate(); requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this, totalItems] { buttonNavigator.onPreviousRelease([this, totalItems] {
if (ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) ||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) {
return;
}
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
requestUpdate(); requestUpdate();
}); });
+14 -14
View File
@@ -388,38 +388,38 @@ void BmpViewerActivity::loop() {
// Keep CPU awake/polling so 1st click works // Keep CPU awake/polling so 1st click works
Activity::loop(); Activity::loop();
// Long press BACK (1s+) goes to home screen const bool toggleSupported = isBmpFile(filePath) ? bmpHasGreyscale : true;
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Back) {
if (ev.type == ButtonEventManager::PressType::Long) {
onGoHome(); onGoHome();
return; return;
} }
if (ev.type == ButtonEventManager::PressType::Short) {
// Short press BACK returns to the calling activity
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
finish(); finish();
return; return;
} }
}
// Confirm: toggle between 1-bit B&W and 4-level grayscale display. if (ev.button == MappedInputManager::Button::Confirm && ev.type == ButtonEventManager::PressType::Short) {
// For decoded images this always applies; for BMPs it only makes sense when the bitmap if (toggleSupported) {
// actually carries greyscale data (1-bit BMPs have nothing to toggle).
const bool toggleSupported = isBmpFile(filePath) ? bmpHasGreyscale : true;
if (toggleSupported && mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
toggleDisplayMode(); toggleDisplayMode();
}
return; return;
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION #ifdef ENABLE_IMAGE_DITHERING_EXTENSION
if (!isBmpFile(filePath) && grayscaleDisplay && mappedInput.wasReleased(MappedInputManager::Button::Left)) { if (ev.button == MappedInputManager::Button::Left && ev.type == ButtonEventManager::PressType::Short &&
!isBmpFile(filePath) && grayscaleDisplay) {
cycleDitherMode(); cycleDitherMode();
return; return;
} }
#endif #endif
// Next/Right button: set this image as the sleep screen if (ev.button == MappedInputManager::Button::Right && ev.type == ButtonEventManager::PressType::Short) {
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
setAsSleepScreen(); setAsSleepScreen();
return; return;
} }
} }
}
+19 -7
View File
@@ -16,6 +16,7 @@
#include <esp_ota_ops.h> #include <esp_ota_ops.h>
#include <cstring> #include <cstring>
#include <vector>
#include "ButtonEventManager.h" #include "ButtonEventManager.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@@ -387,10 +388,8 @@ void loop() {
{ {
using BA = CrossPointSettings::BUTTON_ACTION; using BA = CrossPointSettings::BUTTON_ACTION;
using B = MappedInputManager::Button; using B = MappedInputManager::Button;
ButtonEventManager::ButtonEvent ev; auto actionFor = [&](const ButtonEventManager::ButtonEvent& ev) -> uint8_t {
while (buttonEventManager.consumeEvent(ev)) { switch (ev.button) {
auto actionFor = [&](B btn) -> uint8_t {
switch (btn) {
case B::Back: case B::Back:
switch (ev.type) { switch (ev.type) {
case ButtonEventManager::PressType::Short: case ButtonEventManager::PressType::Short:
@@ -466,9 +465,18 @@ void loop() {
} }
return BA::BTN_DEFAULT; return BA::BTN_DEFAULT;
}; };
ButtonEventManager::ButtonEvent ev;
const uint8_t action = actionFor(ev.button); std::vector<ButtonEventManager::ButtonEvent> defaultEvents;
if (action == BA::BTN_DEFAULT) continue; defaultEvents.reserve(8);
while (buttonEventManager.consumeEvent(ev)) {
const uint8_t action = actionFor(ev);
if (action == BA::BTN_DEFAULT) {
if (ev.type == ButtonEventManager::PressType::Double) {
continue;
}
defaultEvents.push_back(ev);
continue;
}
switch (static_cast<BA>(action)) { switch (static_cast<BA>(action)) {
case BA::BTN_PAGE_FORWARD: case BA::BTN_PAGE_FORWARD:
@@ -525,6 +533,10 @@ void loop() {
break; break;
} }
} }
for (auto it = defaultEvents.rbegin(); it != defaultEvents.rend(); ++it) {
buttonEventManager.pushEventFront(it->button, it->type);
}
} }
const unsigned long activityStartTime = millis(); const unsigned long activityStartTime = millis();