Merge pull request #138 from jpirnay/feat-button-handler
feat: extended button handler
This commit is contained in:
@@ -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>&& activity, ActivityResultHandler resultHandler);
|
||||
|
||||
@@ -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->isReaderActivity()) {
|
||||
currentActivity->onButtonAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityManager::requestUpdate(bool immediate) {
|
||||
if (immediate) {
|
||||
if (renderTaskHandle) {
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::unique_ptr<Activity>> stackActivities;
|
||||
std::unique_ptr<Activity> 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);
|
||||
|
||||
@@ -295,37 +295,6 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool screenshotChordReleased = gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN);
|
||||
|
||||
// Handle short power button press for footnotes
|
||||
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::FOOTNOTES &&
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Power) && !screenshotChordReleased) {
|
||||
if (currentPageFootnotes.size() == 1) {
|
||||
navigateToHref(currentPageFootnotes[0].href, true);
|
||||
} else if (currentPageFootnotes.size() > 1) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& footnoteResult = std::get<FootnoteResult>(result.data);
|
||||
navigateToHref(footnoteResult.href, true);
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Star page toggle via short power button press
|
||||
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE &&
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
|
||||
if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) {
|
||||
bookmarkStore.toggle(static_cast<uint16_t>(currentSpineIndex), static_cast<uint16_t>(section->currentPage));
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
@@ -343,7 +312,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 +1764,160 @@ 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<uint16_t>(currentSpineIndex), static_cast<uint16_t>(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<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& footnoteResult = std::get<FootnoteResult>(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);
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(std::make_unique<EpubReaderChapterSelectionActivity>(renderer, mappedInput, epub,
|
||||
epub->getPath(), spineIdx, tocIdx),
|
||||
[this](const ActivityResult& result) {
|
||||
if (result.isCancelled) return;
|
||||
RenderLock lock(*this);
|
||||
const auto& chapter = std::get<ChapterResult>(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;
|
||||
}
|
||||
case BA::BTN_EXIT_READER:
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
break;
|
||||
case BA::BTN_READER_MENU:
|
||||
if (epub) {
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case BA::BTN_KOREADER_SYNC:
|
||||
launchKOReaderSync(SyncLaunchMode::COMPARE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,70 @@ void MdReaderActivity::savePageIndexCache() const {
|
||||
}
|
||||
|
||||
LOG_DBG("MDR", "Saved page index cache: %d pages", totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
void MdReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
auto clampPage = [this]() {
|
||||
if (totalPages == 0) {
|
||||
currentPage = 0;
|
||||
return;
|
||||
}
|
||||
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<MdReaderTocSelectionActivity>(renderer, mappedInput, headings, currentHeadingIndex),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
currentHeadingIndex = pageOffsets.empty() ? -1 : getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case BA::BTN_EXIT_READER:
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,16 @@ 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 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));
|
||||
// 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;
|
||||
// counting wasReleased as well would double-fire the action.
|
||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||
const bool prev =
|
||||
(SETTINGS.btnShortPageBack == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
||||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Left));
|
||||
const bool next =
|
||||
(SETTINGS.btnShortPageForward == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
||||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT && input.wasReleased(MappedInputManager::Button::Right));
|
||||
return {prev, next};
|
||||
}
|
||||
|
||||
|
||||
@@ -155,16 +155,6 @@ void TxtReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Star page toggle via short power button press
|
||||
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::STAR_PAGE &&
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
|
||||
if (currentPage >= 0) {
|
||||
bookmarkStore.toggle(0, static_cast<uint16_t>(currentPage));
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Open starred pages list via Confirm button
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !bookmarkStore.isEmpty()) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
@@ -782,3 +772,62 @@ 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<uint16_t>(currentPage));
|
||||
requestUpdate();
|
||||
break;
|
||||
case BA::BTN_OPEN_BOOKMARKS:
|
||||
if (!bookmarkStore.isEmpty()) {
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(std::make_unique<StarredPagesActivity>(renderer, mappedInput, bookmarkStore),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& starred = std::get<StarredPageResult>(result.data);
|
||||
currentPage = starred.pageNumber;
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case BA::BTN_NEXT_SECTION:
|
||||
case BA::BTN_PREV_SECTION:
|
||||
// TXT files have no headings/chapters; treat as unsupported (no-op).
|
||||
break;
|
||||
case BA::BTN_EXIT_READER:
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -92,19 +92,10 @@ 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 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 prevTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||
const bool nextTriggered = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
||||
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
@@ -121,7 +112,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 +433,61 @@ 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:
|
||||
if (xtc->hasChapters()) {
|
||||
const auto& chapters = xtc->getChapters();
|
||||
for (const auto& ch : chapters) {
|
||||
if (ch.startPage > currentPage) {
|
||||
currentPage = ch.startPage;
|
||||
requestUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BA::BTN_PREV_SECTION:
|
||||
if (xtc->hasChapters()) {
|
||||
const auto& chapters = xtc->getChapters();
|
||||
for (int i = static_cast<int>(chapters.size()) - 1; i >= 0; i--) {
|
||||
if (chapters[i].startPage < currentPage) {
|
||||
currentPage = chapters[i].startPage;
|
||||
requestUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case BA::BTN_EXIT_READER:
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
finish();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -264,7 +264,9 @@ inline void SettingInfo::prepareSubmenus(std::vector<SettingInfo>& items,
|
||||
auto it = std::find_if(preparedSubmenus.begin(), preparedSubmenus.end(),
|
||||
[&item](const SubmenuData& d) { return d.id == item.submenu; });
|
||||
if (it == preparedSubmenus.end()) {
|
||||
preparedItems.push_back(SettingInfo::SubmenuEntry(item.submenu));
|
||||
auto placeholder = SettingInfo::SubmenuEntry(item.submenu);
|
||||
placeholder.subcategory = item.subcategory; // inherit so addTo inserts the separator
|
||||
preparedItems.push_back(std::move(placeholder));
|
||||
preparedSubmenus.push_back({item.submenu, {}});
|
||||
it = preparedSubmenus.end() - 1;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalClock.h>
|
||||
#include <HalDisplay.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
@@ -21,6 +23,7 @@ bool SettingsActivity::isListItemSelectable(int settingIdx) const {
|
||||
|
||||
void SettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
needsHalfRefresh = true;
|
||||
|
||||
// Build per-category vectors from the shared settings list.
|
||||
// addTo tracks the last subcategory per vector and automatically inserts a separator
|
||||
@@ -78,6 +81,7 @@ void SettingsActivity::onEnter() {
|
||||
// Device-only ACTION items — subcategory drives separator insertion automatically.
|
||||
controlsSettings.insert(controlsSettings.begin(),
|
||||
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
|
||||
controlsSettings.insert(controlsSettings.begin(), SettingInfo::Separator(StrId::STR_MENU_BTN_PHYSICAL));
|
||||
|
||||
addToMoved(readerSettings, lastReaderSub,
|
||||
SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
|
||||
@@ -215,11 +219,15 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
if (setting.type == SettingType::ACTION) {
|
||||
auto resultHandler = [this](const ActivityResult& result) {
|
||||
SETTINGS.saveToFile();
|
||||
needsHalfRefresh = true;
|
||||
const auto* menuResult = std::get_if<MenuResult>(&result.data);
|
||||
if (menuResult && menuResult->action != -1) {
|
||||
auto activity = createActivityForAction(static_cast<SettingAction>(menuResult->action), renderer, mappedInput);
|
||||
if (activity) {
|
||||
startActivityForResult(std::move(activity), [this](const ActivityResult&) { SETTINGS.saveToFile(); });
|
||||
startActivityForResult(std::move(activity), [this](const ActivityResult&) {
|
||||
SETTINGS.saveToFile();
|
||||
needsHalfRefresh = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -277,6 +285,7 @@ void SettingsActivity::render(RenderLock&&) {
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
// Always use standard refresh for settings screen
|
||||
renderer.displayBuffer();
|
||||
const bool halfRefresh = gpio.deviceIsX3() && needsHalfRefresh;
|
||||
needsHalfRefresh = false;
|
||||
renderer.displayBuffer(halfRefresh ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class SettingsActivity final : public Activity {
|
||||
static const StrId categoryNames[categoryCount];
|
||||
|
||||
std::vector<SettingInfo::SubmenuData> submenuData;
|
||||
bool needsHalfRefresh = false;
|
||||
|
||||
void enterCategory(int categoryIndex);
|
||||
void toggleCurrentSetting();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "SettingsSubmenuActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalDisplay.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
@@ -11,6 +13,7 @@
|
||||
|
||||
void SettingsSubmenuActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
needsHalfRefresh = true;
|
||||
initMenuList();
|
||||
requestUpdate();
|
||||
}
|
||||
@@ -63,5 +66,7 @@ void SettingsSubmenuActivity::render(RenderLock&&) {
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
const bool halfRefresh = gpio.deviceIsX3() && needsHalfRefresh;
|
||||
needsHalfRefresh = false;
|
||||
renderer.displayBuffer(halfRefresh ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
class SettingsSubmenuActivity final : public MenuListActivity {
|
||||
StrId titleId;
|
||||
std::function<std::string(const SettingInfo&)> itemValueStringOverride;
|
||||
bool needsHalfRefresh = false;
|
||||
|
||||
// MenuListActivity overrides
|
||||
void onEnter() override;
|
||||
|
||||
Reference in New Issue
Block a user