Merge pull request #138 from jpirnay/feat-button-handler

feat: extended button handler
This commit is contained in:
jpirnay
2026-04-26 18:19:09 +02:00
committed by GitHub
47 changed files with 956 additions and 313 deletions
+8 -1
View File
@@ -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);
+8
View File
@@ -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) {
+12 -1
View File
@@ -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);
+158 -32
View File
@@ -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.
+68 -2
View File
@@ -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;
}
}
+1
View File
@@ -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;
};
+12 -13
View File
@@ -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};
}
+59 -10
View File
@@ -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.
+63 -14
View File
@@ -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.
+3 -1
View File
@@ -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;
}
+12 -3
View File
@@ -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;