Files
crosspoint-reader-mod/src/activities/reader/EpubReaderMenuActivity.cpp
cottongin 422cad7bc5 fix: resolve three reader bugs (confirm eaten, footnotes menu, phantom render)
1. Clear ignoreNextConfirmRelease after transferring state to child
   activity, so the next Confirm press isn't silently consumed.
2. Add conditional FOOTNOTES entry to reader menu when the current
   book has footnotes.
3. Guard clock-minute requestUpdate() with !isReaderActivity() to
   prevent full e-ink re-renders every minute while reading.

Made-with: Cursor
2026-03-08 05:56:10 -04:00

190 lines
8.3 KiB
C++

#include "EpubReaderMenuActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& title, const int currentPage, const int totalPages,
const int bookProgressPercent, const uint8_t currentOrientation,
const bool hasFootnotes, bool isBookmarked, uint8_t currentFontSize,
const std::string& bookCachePath)
: Activity("EpubReaderMenu", renderer, mappedInput),
menuItems(buildMenuItems(hasFootnotes, isBookmarked)),
title(title),
pendingOrientation(currentOrientation),
pendingFontSize(currentFontSize < CrossPointSettings::FONT_SIZE_COUNT ? currentFontSize : 0),
currentPage(currentPage),
totalPages(totalPages),
bookProgressPercent(bookProgressPercent),
bookCachePath(bookCachePath) {
if (!bookCachePath.empty()) {
auto bookSettings = BookSettings::load(bookCachePath);
pendingLetterboxFill = bookSettings.letterboxFillOverride;
}
}
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
bool isBookmarked) {
std::vector<MenuItem> items;
items.reserve(16);
// Mod menu order
if (isBookmarked) {
items.push_back({MenuAction::REMOVE_BOOKMARK, StrId::STR_REMOVE_BOOKMARK});
} else {
items.push_back({MenuAction::ADD_BOOKMARK, StrId::STR_ADD_BOOKMARK});
}
items.push_back({MenuAction::DICTIONARY, StrId::STR_DICTIONARY});
if (hasFootnotes) {
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
}
items.push_back({MenuAction::TOGGLE_ORIENTATION, StrId::STR_TOGGLE_ORIENTATION});
items.push_back({MenuAction::TOGGLE_FONT_SIZE, StrId::STR_TOGGLE_FONT_SIZE});
items.push_back({MenuAction::LETTERBOX_FILL, StrId::STR_OVERRIDE_LETTERBOX_FILL});
items.push_back({MenuAction::TABLE_OF_CONTENTS, StrId::STR_TABLE_OF_CONTENTS});
items.push_back({MenuAction::GO_TO_BOOKMARK, StrId::STR_GO_TO_BOOKMARK});
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
items.push_back({MenuAction::CLOSE_BOOK, StrId::STR_CLOSE_BOOK});
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
items.push_back({MenuAction::PUSH_AND_SLEEP, StrId::STR_PUSH_AND_SLEEP});
items.push_back({MenuAction::MANAGE_BOOK, StrId::STR_MANAGE_BOOK});
return items;
}
void EpubReaderMenuActivity::onEnter() {
Activity::onEnter();
requestUpdate();
}
void EpubReaderMenuActivity::onExit() { Activity::onExit(); }
void EpubReaderMenuActivity::loop() {
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto selectedAction = menuItems[selectedIndex].action;
if (selectedAction == MenuAction::TOGGLE_ORIENTATION) {
// Toggle between preferred portrait and preferred landscape.
const bool isCurrentlyPortrait =
(pendingOrientation == CrossPointSettings::PORTRAIT || pendingOrientation == CrossPointSettings::INVERTED);
pendingOrientation = isCurrentlyPortrait ? SETTINGS.preferredLandscape : SETTINGS.preferredPortrait;
requestUpdate();
return;
}
if (selectedAction == MenuAction::TOGGLE_FONT_SIZE) {
pendingFontSize = (pendingFontSize + 1) % CrossPointSettings::FONT_SIZE_COUNT;
requestUpdate();
return;
}
if (selectedAction == MenuAction::LETTERBOX_FILL) {
int idx = (letterboxFillToIndex() + 1) % LETTERBOX_FILL_OPTION_COUNT;
pendingLetterboxFill = indexToLetterboxFill(idx);
if (!bookCachePath.empty()) saveLetterboxFill();
requestUpdate();
return;
}
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption, pendingFontSize});
finish();
return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult result;
result.isCancelled = true;
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption, pendingFontSize};
setResult(std::move(result));
finish();
return;
}
}
void EpubReaderMenuActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto orientation = renderer.getOrientation();
// Landscape orientation: button hints are drawn along a vertical edge, so we
// reserve a horizontal gutter to prevent overlap with menu content.
const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise;
const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise;
// Inverted portrait: button hints appear near the logical top, so we reserve
// vertical space to keep the header and list clear.
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0;
// Landscape CW places hints on the left edge; CCW keeps them on the right.
const int contentX = isLandscapeCw ? hintGutterWidth : 0;
const int contentWidth = pageWidth - hintGutterWidth;
const int hintGutterHeight = isPortraitInverted ? 50 : 0;
const int contentY = hintGutterHeight;
// Title
const std::string truncTitle =
renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentWidth - 40, EpdFontFamily::BOLD);
// Manual centering so we can respect the content gutter.
const int titleX =
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, truncTitle.c_str(), true, EpdFontFamily::BOLD);
// Progress summary
std::string progressLine;
if (totalPages > 0) {
progressLine = std::string(tr(STR_CHAPTER_PREFIX)) + std::to_string(currentPage) + "/" +
std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR));
}
progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%";
renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str());
// Menu Items
const int startY = 75 + contentY;
constexpr int lineHeight = 30;
for (size_t i = 0; i < menuItems.size(); ++i) {
const int displayY = startY + (i * lineHeight);
const bool isSelected = (static_cast<int>(i) == selectedIndex);
if (isSelected) {
// Highlight only the content area so we don't paint over hint gutters.
renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true);
}
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, I18N.get(menuItems[i].labelId), !isSelected);
if (menuItems[i].action == MenuAction::TOGGLE_ORIENTATION) {
const char* value = I18N.get(orientationLabels[pendingOrientation]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
}
if (menuItems[i].action == MenuAction::TOGGLE_FONT_SIZE) {
const char* value = I18N.get(fontSizeLabels[pendingFontSize]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
}
if (menuItems[i].action == MenuAction::LETTERBOX_FILL) {
const char* value = I18N.get(letterboxFillLabels[letterboxFillToIndex()]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
}
}
// Footer / Hints
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();
}