Add navigation option
This commit is contained in:
@@ -42,6 +42,10 @@ struct PercentResult {
|
|||||||
int percent = 0;
|
int percent = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PrintedPageResult {
|
||||||
|
std::string label;
|
||||||
|
};
|
||||||
|
|
||||||
struct PageResult {
|
struct PageResult {
|
||||||
uint32_t page = 0;
|
uint32_t page = 0;
|
||||||
};
|
};
|
||||||
@@ -76,7 +80,7 @@ struct StarredPageResult {
|
|||||||
|
|
||||||
using ResultVariant =
|
using ResultVariant =
|
||||||
std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult, PageResult,
|
std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult, PageResult,
|
||||||
SyncResult, NetworkModeResult, FootnoteResult, FilePathResult, StarredPageResult>;
|
SyncResult, NetworkModeResult, FootnoteResult, FilePathResult, StarredPageResult, PrintedPageResult>;
|
||||||
|
|
||||||
struct ActivityResult {
|
struct ActivityResult {
|
||||||
bool isCancelled = false;
|
bool isCancelled = false;
|
||||||
|
|||||||
@@ -19,13 +19,16 @@
|
|||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderChapterSelectionActivity.h"
|
||||||
#include "EpubReaderFootnotesActivity.h"
|
#include "EpubReaderFootnotesActivity.h"
|
||||||
#include "EpubReaderPercentSelectionActivity.h"
|
#include "EpubReaderPercentSelectionActivity.h"
|
||||||
|
#include "EpubReaderPrintedPageInputActivity.h"
|
||||||
#include "EpubRenderBenchmarkActivity.h"
|
#include "EpubRenderBenchmarkActivity.h"
|
||||||
#include "FinishedBookActivity.h"
|
#include "FinishedBookActivity.h"
|
||||||
#include "GlobalBookmarkIndex.h"
|
#include "GlobalBookmarkIndex.h"
|
||||||
@@ -47,6 +50,20 @@
|
|||||||
namespace {
|
namespace {
|
||||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||||
constexpr unsigned long skipChapterMs = 700;
|
constexpr unsigned long skipChapterMs = 700;
|
||||||
|
|
||||||
|
// Parse a printed-page label as a non-negative integer. Returns nullopt for empty strings,
|
||||||
|
// strings with non-digit characters (e.g. roman "iv"), and overflow. Used both to gate the
|
||||||
|
// "go to printed page" menu item and to compute min/max for the numeric input.
|
||||||
|
std::optional<int> parsePrintedPageLabel(const std::string& label) {
|
||||||
|
if (label.empty()) return std::nullopt;
|
||||||
|
int value = 0;
|
||||||
|
for (char c : label) {
|
||||||
|
if (c < '0' || c > '9') return std::nullopt;
|
||||||
|
value = value * 10 + (c - '0');
|
||||||
|
if (value > 999999) return std::nullopt; // sanity
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||||
constexpr int PAGE_TURN_LABELS[] = {1, 1, 3, 6, 12};
|
constexpr int PAGE_TURN_LABELS[] = {1, 1, 3, 6, 12};
|
||||||
|
|
||||||
@@ -683,6 +700,64 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EpubReaderMenuActivity::MenuAction::GO_TO_PRINTED_PAGE: {
|
||||||
|
if (!epub) break;
|
||||||
|
auto entries = epub->loadPrintedPageList();
|
||||||
|
// Compute the integer label range from parseable entries; non-integer labels are
|
||||||
|
// ignored (the dialog is numeric-only).
|
||||||
|
int minLabel = std::numeric_limits<int>::max();
|
||||||
|
int maxLabel = std::numeric_limits<int>::min();
|
||||||
|
for (const auto& entry : entries) {
|
||||||
|
if (const auto n = parsePrintedPageLabel(entry.label)) {
|
||||||
|
if (*n < minLabel) minLabel = *n;
|
||||||
|
if (*n > maxLabel) maxLabel = *n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxLabel < minLabel) break; // no integer labels — shouldn't happen if menu item was shown
|
||||||
|
|
||||||
|
// Pre-fill with the printed page the reader is currently on (or the nearest one before
|
||||||
|
// it — rendered device pages rarely carry an anchor themselves, but they sit between
|
||||||
|
// two printed pages, so the closest prior anchor is the "you're here" hint). Falls
|
||||||
|
// back to the lowest integer label in the book if no prior anchor exists.
|
||||||
|
int initialValue = minLabel;
|
||||||
|
if (section) {
|
||||||
|
if (const auto rawLabel =
|
||||||
|
section->getNearestPrintedPageLabelAtOrBefore(static_cast<uint16_t>(section->currentPage))) {
|
||||||
|
if (const auto n = parsePrintedPageLabel(*rawLabel)) {
|
||||||
|
initialValue = *n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(
|
||||||
|
std::make_unique<EpubReaderPrintedPageInputActivity>(renderer, mappedInput, initialValue, minLabel, maxLabel),
|
||||||
|
[this, entries = std::move(entries)](const ActivityResult& result) {
|
||||||
|
if (result.isCancelled) return;
|
||||||
|
const auto& pick = std::get<PrintedPageResult>(result.data);
|
||||||
|
// Resolve the typed label back to a (href, anchor) by linear scan. Entries are
|
||||||
|
// small (typically <500 even for long books) and this fires once per user action.
|
||||||
|
for (const auto& entry : entries) {
|
||||||
|
if (entry.label == pick.label) {
|
||||||
|
const int spineIdx = epub->resolveHrefToSpineIndex(entry.href);
|
||||||
|
if (spineIdx < 0) {
|
||||||
|
LOG_DBG("ERS", "printed-page jump: could not resolve spine for href=%s", entry.href.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
currentSpineIndex = spineIdx;
|
||||||
|
navTarget =
|
||||||
|
entry.anchor.empty() ? NavigationTarget::makePage(0) : NavigationTarget::makeAnchor(entry.anchor);
|
||||||
|
section.reset();
|
||||||
|
}
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_DBG("ERS", "printed-page jump: label '%s' not found in pagelist", pick.label.c_str());
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: {
|
case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: {
|
||||||
if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) {
|
if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) {
|
||||||
auto p = section->loadPageFromSectionFile();
|
auto p = section->loadPageFromSectionFile();
|
||||||
@@ -2592,13 +2667,27 @@ void EpubReaderActivity::openReaderMenu() {
|
|||||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||||
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
|
||||||
static_cast<uint16_t>(section->currentPage));
|
static_cast<uint16_t>(section->currentPage));
|
||||||
|
|
||||||
|
// Show the "Go to printed page" item only when this book has at least one integer-labelled
|
||||||
|
// entry in pagelist.bin. Roman-only or empty page lists are excluded — the numeric input
|
||||||
|
// dialog can't address them anyway.
|
||||||
|
bool hasPrintedPages = false;
|
||||||
|
if (epub) {
|
||||||
|
for (const auto& entry : epub->loadPrintedPageList()) {
|
||||||
|
if (parsePrintedPageLabel(entry.label).has_value()) {
|
||||||
|
hasPrintedPages = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||||
startActivityForResult(
|
startActivityForResult(
|
||||||
std::make_unique<EpubReaderMenuActivity>(
|
std::make_unique<EpubReaderMenuActivity>(
|
||||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
|
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
|
||||||
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
|
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
|
||||||
bookSdFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness, bookBionicReadingOverride,
|
bookSdFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness, bookBionicReadingOverride,
|
||||||
bookParagraphAlignmentOverride, !bookmarkStore.isEmpty(), isCurrentPageStarred),
|
bookParagraphAlignmentOverride, !bookmarkStore.isEmpty(), isCurrentPageStarred, hasPrintedPages),
|
||||||
[this](const ActivityResult& result) {
|
[this](const ActivityResult& result) {
|
||||||
const auto& menu = std::get<MenuResult>(result.data);
|
const auto& menu = std::get<MenuResult>(result.data);
|
||||||
applyOrientation(menu.orientation);
|
applyOrientation(menu.orientation);
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(
|
|||||||
const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
|
const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride,
|
||||||
const int8_t initialFontFamilyOverride, const std::string& initialSdFontFamilyOverride,
|
const int8_t initialFontFamilyOverride, const std::string& initialSdFontFamilyOverride,
|
||||||
const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
|
const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
|
||||||
const int8_t initialParagraphAlignmentOverride, const bool hasStarredPages, const bool isCurrentPageStarred)
|
const int8_t initialParagraphAlignmentOverride, const bool hasStarredPages, const bool isCurrentPageStarred,
|
||||||
|
const bool hasPrintedPages)
|
||||||
: MenuListActivity("EpubReaderMenu", renderer, mappedInput),
|
: MenuListActivity("EpubReaderMenu", renderer, mappedInput),
|
||||||
currentPageStarred(isCurrentPageStarred),
|
currentPageStarred(isCurrentPageStarred),
|
||||||
pendingOrientation(currentOrientation),
|
pendingOrientation(currentOrientation),
|
||||||
@@ -56,16 +57,19 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(
|
|||||||
currentPage(currentPage),
|
currentPage(currentPage),
|
||||||
totalPages(totalPages),
|
totalPages(totalPages),
|
||||||
bookProgressPercent(bookProgressPercent) {
|
bookProgressPercent(bookProgressPercent) {
|
||||||
buildMenuItems(hasFootnotes, hasStarredPages);
|
buildMenuItems(hasFootnotes, hasStarredPages, hasPrintedPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPages) {
|
void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPages, bool hasPrintedPages) {
|
||||||
menuItems.reserve(20);
|
menuItems.reserve(20);
|
||||||
|
|
||||||
// --- Navigation ---
|
// --- Navigation ---
|
||||||
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION));
|
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION));
|
||||||
menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None));
|
menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None));
|
||||||
menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None));
|
menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None));
|
||||||
|
if (hasPrintedPages) {
|
||||||
|
menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PRINTED_PAGE, SettingAction::None));
|
||||||
|
}
|
||||||
|
|
||||||
// Bookmarks, footnotes
|
// Bookmarks, footnotes
|
||||||
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_BOOKMARKS));
|
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_BOOKMARKS));
|
||||||
@@ -277,6 +281,8 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId
|
|||||||
return MenuAction::SELECT_CHAPTER;
|
return MenuAction::SELECT_CHAPTER;
|
||||||
case StrId::STR_GO_TO_PERCENT:
|
case StrId::STR_GO_TO_PERCENT:
|
||||||
return MenuAction::GO_TO_PERCENT;
|
return MenuAction::GO_TO_PERCENT;
|
||||||
|
case StrId::STR_GO_TO_PRINTED_PAGE:
|
||||||
|
return MenuAction::GO_TO_PRINTED_PAGE;
|
||||||
case StrId::STR_STARRED_PAGES:
|
case StrId::STR_STARRED_PAGES:
|
||||||
return MenuAction::STARRED_PAGES;
|
return MenuAction::STARRED_PAGES;
|
||||||
case StrId::STR_STAR_PAGE:
|
case StrId::STR_STAR_PAGE:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class EpubReaderMenuActivity final : public MenuListActivity {
|
|||||||
IMAGE_RENDERING,
|
IMAGE_RENDERING,
|
||||||
TEXT_DARKNESS,
|
TEXT_DARKNESS,
|
||||||
GO_TO_PERCENT,
|
GO_TO_PERCENT,
|
||||||
|
GO_TO_PRINTED_PAGE,
|
||||||
AUTO_PAGE_TURN,
|
AUTO_PAGE_TURN,
|
||||||
ROTATE_SCREEN,
|
ROTATE_SCREEN,
|
||||||
SCREENSHOT,
|
SCREENSHOT,
|
||||||
@@ -42,13 +43,13 @@ class EpubReaderMenuActivity final : public MenuListActivity {
|
|||||||
const std::string& initialSdFontFamilyOverride, const int8_t initialFontSizeOverride,
|
const std::string& initialSdFontFamilyOverride, const int8_t initialFontSizeOverride,
|
||||||
const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
|
const uint8_t initialTextDarkness, const bool initialBionicReadingOverride,
|
||||||
const int8_t initialParagraphAlignmentOverride, const bool hasStarredPages,
|
const int8_t initialParagraphAlignmentOverride, const bool hasStarredPages,
|
||||||
const bool isCurrentPageStarred);
|
const bool isCurrentPageStarred, const bool hasPrintedPages);
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void render(RenderLock&&) override;
|
void render(RenderLock&&) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildMenuItems(bool hasFootnotes, bool hasStarredPages);
|
void buildMenuItems(bool hasFootnotes, bool hasStarredPages, bool hasPrintedPages);
|
||||||
|
|
||||||
bool currentPageStarred = false;
|
bool currentPageStarred = false;
|
||||||
void finishWithAction(MenuAction action);
|
void finishWithAction(MenuAction action);
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
#include "EpubReaderPrintedPageInputActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "ButtonEventManager.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
int EpubReaderPrintedPageInputActivity::powTen(int exponent) {
|
||||||
|
int result = 1;
|
||||||
|
for (int i = 0; i < exponent; i++) result *= 10;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EpubReaderPrintedPageInputActivity::digitCount() const {
|
||||||
|
int n = (value > 0) ? value : 1;
|
||||||
|
int count = 0;
|
||||||
|
while (n > 0) {
|
||||||
|
count++;
|
||||||
|
n /= 10;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EpubReaderPrintedPageInputActivity::maxCursorDigit() const {
|
||||||
|
// Bound the reachable cursor position by maxValue's digit count, not the current value's.
|
||||||
|
// This lets the user step into "empty" higher digits (e.g. cursor sits over the tens
|
||||||
|
// place while value is still 1, and pressing Up turns it into 11). Without this you
|
||||||
|
// could never grow a 1 into a 3-digit number without first single-pressing into double
|
||||||
|
// digits, which defeats the point of having a digit cursor.
|
||||||
|
int n = (maxValue > 0) ? maxValue : 1;
|
||||||
|
int count = 0;
|
||||||
|
while (n > 0) {
|
||||||
|
count++;
|
||||||
|
n /= 10;
|
||||||
|
}
|
||||||
|
return count - 1; // 0-based: ones=0, tens=1, hundreds=2, ...
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::clampValue() {
|
||||||
|
if (value < minValue) value = minValue;
|
||||||
|
if (value > maxValue) value = maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::adjustDigit(int delta) {
|
||||||
|
value += delta * powTen(cursorDigit);
|
||||||
|
clampValue();
|
||||||
|
// Don't pull the cursor in toward the new digit count — leave it where the user put it.
|
||||||
|
// The cursor is a position the user navigated to, not a property of the value.
|
||||||
|
if (cursorDigit > maxCursorDigit()) cursorDigit = maxCursorDigit();
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::adjustDigitTimes(int multiplier, int sign) {
|
||||||
|
// Used by the double-click handler: step is multiplier × 10^cursorDigit (e.g. 10 at the
|
||||||
|
// ones place, 100 at the tens place). Sign is +1 or -1.
|
||||||
|
value += sign * multiplier * powTen(cursorDigit);
|
||||||
|
clampValue();
|
||||||
|
if (cursorDigit > maxCursorDigit()) cursorDigit = maxCursorDigit();
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::moveCursor(int delta) {
|
||||||
|
cursorDigit += delta;
|
||||||
|
if (cursorDigit < 0) cursorDigit = 0;
|
||||||
|
if (cursorDigit > maxCursorDigit()) cursorDigit = maxCursorDigit();
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
// Force the FSM to wait for the double-click window on Up/Down so we can distinguish
|
||||||
|
// Short (±1) from Double (±10). Adds ~300ms latency to single Up/Down presses; that's
|
||||||
|
// the price for the larger step. Back/Confirm/Left/Right stay on the immediate
|
||||||
|
// wasPressed path — no latency for navigation.
|
||||||
|
buttonEvents.forceDoubleAction(MappedInputManager::Button::Up, true);
|
||||||
|
buttonEvents.forceDoubleAction(MappedInputManager::Button::Down, true);
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::onExit() {
|
||||||
|
buttonEvents.forceDoubleAction(MappedInputManager::Button::Up, false);
|
||||||
|
buttonEvents.forceDoubleAction(MappedInputManager::Button::Down, false);
|
||||||
|
Activity::onExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::loop() {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
ActivityResult result;
|
||||||
|
result.isCancelled = true;
|
||||||
|
setResult(std::move(result));
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
setResult(PrintedPageResult{std::to_string(value)});
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||||
|
moveCursor(1); // cursor moves toward higher digits on Left, matching screen layout
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
|
moveCursor(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up/Down come through the FSM-backed event queue so we can react to Short vs Double.
|
||||||
|
// Short = ±1, Double = ±10. Both Up and PageBack map to the same hardware button; the
|
||||||
|
// FSM emits an event for each logical button, but we only handle the Up/Down variants
|
||||||
|
// here. The global dispatcher consumes PageBack/PageForward variants (they're configured
|
||||||
|
// as page-turn actions in the reader) and dispatch is a no-op while this dialog is
|
||||||
|
// current, so they're effectively swallowed.
|
||||||
|
ButtonEventManager::ButtonEvent ev;
|
||||||
|
while (buttonEvents.consumeEvent(ev)) {
|
||||||
|
if (ev.button == MappedInputManager::Button::Up) {
|
||||||
|
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||||
|
adjustDigit(1);
|
||||||
|
} else if (ev.type == ButtonEventManager::PressType::Double) {
|
||||||
|
adjustDigitTimes(10, 1);
|
||||||
|
}
|
||||||
|
} else if (ev.button == MappedInputManager::Button::Down) {
|
||||||
|
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||||
|
adjustDigit(-1);
|
||||||
|
} else if (ev.type == ButtonEventManager::PressType::Double) {
|
||||||
|
adjustDigitTimes(10, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Other queued events (PageBack/PageForward variants and any stray) are discarded;
|
||||||
|
// the wasPressed path above already handled Back/Confirm/Left/Right.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPrintedPageInputActivity::render(RenderLock&&) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_GO_TO_PRINTED_PAGE), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Big centred numeric value with an underline under the active digit position.
|
||||||
|
// When the cursor sits over a digit position past the current value (e.g. cursor at the
|
||||||
|
// tens place while value is still 1), we pad the displayed number with leading "·" dots
|
||||||
|
// so the underline can mark the empty position it'll grow into on the next Up press.
|
||||||
|
const std::string rawValueText = std::to_string(value);
|
||||||
|
const int visibleDigits = std::max(digitCount(), cursorDigit + 1);
|
||||||
|
std::string valueText;
|
||||||
|
for (int i = 0; i < visibleDigits - digitCount(); i++) valueText += "0"; // leading zeros
|
||||||
|
valueText += rawValueText;
|
||||||
|
const int valueY = 110;
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, valueY, valueText.c_str(), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Place the underline under the active digit. Width-based positioning approximates a
|
||||||
|
// monospaced grid using the total rendered width / digit count; small visual mismatch on
|
||||||
|
// proportional fonts is acceptable for a one-character indicator.
|
||||||
|
const int totalWidth = renderer.getTextWidth(UI_12_FONT_ID, valueText.c_str());
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
const int startX = (screenWidth - totalWidth) / 2;
|
||||||
|
const int digitIndexFromLeft = visibleDigits - 1 - cursorDigit; // 0-based, from the left
|
||||||
|
const int avgDigitWidth = (visibleDigits > 0) ? totalWidth / visibleDigits : 0;
|
||||||
|
const int underlineX = startX + digitIndexFromLeft * avgDigitWidth;
|
||||||
|
const int underlineWidth = avgDigitWidth;
|
||||||
|
const int underlineY = valueY + renderer.getLineHeight(UI_12_FONT_ID) + 2;
|
||||||
|
renderer.fillRect(underlineX, underlineY, underlineWidth, 3, true);
|
||||||
|
|
||||||
|
// Range hint underneath: "Range: 1 - 305"
|
||||||
|
char rangeBuf[48];
|
||||||
|
snprintf(rangeBuf, sizeof(rangeBuf), tr(STR_GO_TO_PRINTED_PAGE_RANGE), (unsigned)minValue, (unsigned)maxValue);
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, underlineY + 25, rangeBuf, true);
|
||||||
|
|
||||||
|
// Step hint.
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, underlineY + 50, tr(STR_GO_TO_PRINTED_PAGE_HINT), true);
|
||||||
|
|
||||||
|
// Button hints.
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "activities/Activity.h"
|
||||||
|
|
||||||
|
// Numeric input dialog for "jump to printed page".
|
||||||
|
// User adjusts a single integer (mirroring the printed-page label as shown in the book).
|
||||||
|
// Up/Down change the digit under the cursor by ±1 (single) or ±10 (double-click).
|
||||||
|
// Left/Right move the cursor between digits; Confirm returns the typed string.
|
||||||
|
// Books with non-integer labels (roman numerals, etc.) are not addressable via this dialog;
|
||||||
|
// the menu item is hidden if the book has no integer-parseable printed-page labels.
|
||||||
|
class EpubReaderPrintedPageInputActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
explicit EpubReaderPrintedPageInputActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, int initialValue,
|
||||||
|
int minValue, int maxValue)
|
||||||
|
: Activity("EpubReaderPrintedPageInput", renderer, mappedInput),
|
||||||
|
value(initialValue),
|
||||||
|
minValue(minValue),
|
||||||
|
maxValue(maxValue) {
|
||||||
|
clampValue();
|
||||||
|
cursorDigit = 0; // ones place
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int value = 0;
|
||||||
|
int minValue = 1;
|
||||||
|
int maxValue = 1;
|
||||||
|
int cursorDigit = 0; // 0 = ones, 1 = tens, 2 = hundreds, ...
|
||||||
|
|
||||||
|
void clampValue();
|
||||||
|
void adjustDigit(int delta);
|
||||||
|
void adjustDigitTimes(int multiplier, int sign); // delta = sign * multiplier * 10^cursorDigit
|
||||||
|
void moveCursor(int delta);
|
||||||
|
static int powTen(int exponent);
|
||||||
|
int digitCount() const;
|
||||||
|
// Highest cursor position the user can reach: one less than the digit count of maxValue.
|
||||||
|
// Lets the user move the cursor "past" the current value to higher digit positions that
|
||||||
|
// don't exist yet, so they can grow the number quickly (e.g. start at 1, move cursor left,
|
||||||
|
// press Up to make 11, etc.) without single-pressing dozens of times.
|
||||||
|
int maxCursorDigit() const;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user