#include "EndOfBookOptions.h" #include #include #include #include #include "CrossPointSettings.h" #include "ReaderUtils.h" // ReaderUtils.h pulls in ActivityManager.h, which only forward-declares Activity while holding // std::unique_ptr members. Destroying that unique_ptr needs the complete type, so the // definition must be visible here. #include "activities/Activity.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/ButtonNavigator.h" #include "util/NextBookFinder.h" namespace fui = freeink::ui; namespace { constexpr fui::ActionId ACTION_ROW = 1; // Display name without the file extension, mirroring the file browser rows std::string displayName(const std::string& filename) { const auto pos = filename.rfind('.'); return filename.substr(0, pos); } } // namespace EndOfBookOptions::EndOfBookOptions(GfxRenderer& renderer) : UiAppHost(renderer), renderer(renderer) {} void EndOfBookOptions::loadOnce(const std::string& currentBookPath) { if (isLoaded.load(std::memory_order_acquire)) { return; } folder = FsHelpers::extractFolderPath(currentBookPath); names = NextBookFinder::findNextBooks(currentBookPath, MAX_SUGGESTIONS); selector.store(0, std::memory_order_relaxed); if (!names.empty()) { // One-time app setup on the render task, before the first render/route. resetUi(); app.on(ACTION_ROW, &EndOfBookOptions::onRowEvent, this); app.setScreen(&EndOfBookOptions::listScreen, this); buildRowItems(); } // Release-publish so the main task, which gates all access on isLoaded, never // observes a partially built list (rowItems/rowLabels included) isLoaded.store(true, std::memory_order_release); } // Populates rowLabels/rowItems from names + the trailing "Home" row. Called // once here since names never changes after loadOnce() completes. void EndOfBookOptions::buildRowItems() { rowCount = 0; for (const auto& name : names) { if (rowCount >= MAX_ROWS) break; rowLabels[rowCount] = displayName(name); fui::ListItem item; item.label = rowLabels[rowCount].c_str(); item.actionValue = static_cast(rowCount); rowItems[rowCount] = item; rowCount++; } if (rowCount < MAX_ROWS) { rowLabels[rowCount] = tr(STR_EOB_HOME); fui::ListItem item; item.label = rowLabels[rowCount].c_str(); item.actionValue = static_cast(rowCount); rowItems[rowCount] = item; rowCount++; } } bool EndOfBookOptions::menuActive() const { return isLoaded.load(std::memory_order_acquire) && !names.empty(); } std::string EndOfBookOptions::fullPath(const size_t index) const { if (index >= names.size()) { return {}; } return folder == "/" ? "/" + names[index] : folder + "/" + names[index]; } void EndOfBookOptions::onRowEvent(const fui::ActionEvent& event, void* user) { auto* self = static_cast(user); if (event.value < 0 || event.value > static_cast(self->names.size())) return; self->selector.store(event.value, std::memory_order_relaxed); // The tapped row leaves this screen (open book or home); a lingering flash // would gray an unrelated element on the next render. self->app.clearTapFlash(); self->tappedRow = event.value; } EndOfBookOptions::Action EndOfBookOptions::handleMenuInput(const MappedInputManager& input, std::string* openPath) { // Touch goes through the FreeInkApp: render() registered the row hit rects; // route the snapshot and let onRowEvent record the tapped row. tappedRow = -1; const auto route = routeTouch(input); // cppcheck can't see that route() dispatches into onRowEvent (registered via // app.on(ACTION_ROW, ...)), which sets tappedRow, so it flags this as always false. // cppcheck-suppress knownConditionTrueFalse if (route && tappedRow >= 0) { if (tappedRow < static_cast(names.size())) { if (openPath) { *openPath = fullPath(tappedRow); } return Action::OpenBook; } return Action::GoHome; // "Home" row tapped } if (route.routed && app.invalidated()) { return Action::Redraw; } const int selectedIndex = selector.load(std::memory_order_relaxed); if (input.wasReleased(MappedInputManager::Button::Confirm)) { if (selectedIndex < static_cast(names.size())) { if (openPath) { *openPath = fullPath(selectedIndex); } return Action::OpenBook; } return Action::GoHome; // "Home" entry selected } // Short-press Back returns to the last page; a long press falls through to the // reader's own handler (file browser). Home is reached through the list's Home entry. if (input.wasReleased(MappedInputManager::Button::Back) && input.getHeldTime() < ReaderUtils::GO_HOME_MS) { return Action::LastPage; } // Selection movement on the standard list navigation buttons (side Up/Down plus front // Left/Right, orientation swap included). It follows the reader's page-turn semantics // (press-triggered by default, release-triggered when a long-press behavior is // configured, same rule as ReaderUtils::detectPageTurn). This matters on entry: with // press-triggered turns, the press that turned the final page already fired in the // reader, and its release must not double-fire into this menu. const bool usePress = SETTINGS.longPressButtonBehavior == CrossPointSettings::OFF; const auto triggered = [&](const MappedInputManager::Button button) { return usePress ? input.wasPressed(button) : input.wasReleased(button); }; const int itemCount = static_cast(names.size()) + 1; // + "Home" entry if (triggered(MappedInputManager::Button::NavPrevious)) { selector.store(ButtonNavigator::previousIndex(selectedIndex, itemCount), std::memory_order_relaxed); return Action::Redraw; } if (triggered(MappedInputManager::Button::NavNext)) { selector.store(ButtonNavigator::nextIndex(selectedIndex, itemCount), std::memory_order_relaxed); return Action::Redraw; } return Action::None; } void EndOfBookOptions::listScreen(UiScreen& screen, void* user) { static_cast(user)->buildListScreen(screen); } void EndOfBookOptions::buildListScreen(UiScreen& screen) { const auto& metrics = UITheme::getInstance().getMetrics(); // Same layout math as render(): the list band starts under the title/subtitle it // draws, and stops above the button hints (the safe-area bottom edge). const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); const int titleY = safe.y + safe.height / 8; const int subtitleY = titleY + renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing; const int listTop = subtitleY + renderer.getLineHeight(UI_10_FONT_ID) + metrics.verticalSpacing * 2; screen.setContentMarginFromScreen(fui::Insets{ static_cast(listTop), static_cast(renderer.getScreenWidth() - (safe.x + safe.width)), static_cast(renderer.getScreenHeight() - (safe.y + safe.height) + metrics.verticalSpacing), static_cast(safe.x)}); // rowLabels/rowItems were built once in loadOnce() (see buildRowItems()) // and reused here on every repaint. fui::ListProps props; props.items = rowItems; props.count = static_cast(rowCount); props.selectedIndex = static_cast(selector.load(std::memory_order_relaxed)); props.action = ACTION_ROW; props.inputMask = fui::InputTouch; // physical buttons stay in handleMenuInput() screen.list(props); } void EndOfBookOptions::render(GfxRenderer& renderer, const MappedInputManager& input) { const auto& metrics = UITheme::getInstance().getMetrics(); if (!menuActive()) { // No suggestions: the historical plain end screen. 3/8 of the screen height matches // the previous fixed position on the 480x800 panel and scales to other resolutions. renderer.drawCenteredText(UI_12_FONT_ID, renderer.getScreenHeight() * 3 / 8, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); return; } // Suggestion menu: title, list (+ Home entry) and button hints. The hints are drawn at // the physical front buttons, which is a logical side/top edge in the rotated // orientations — lay out inside the safe area so nothing hides behind them. Vertical // positions derive from the safe-area height and font line heights so other panel // resolutions scale (review request on #2532). const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); const int titleY = safe.y + safe.height / 8; const int subtitleY = titleY + renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing; UITheme::drawCenteredText(renderer, safe, UI_12_FONT_ID, titleY, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); UITheme::drawCenteredText(renderer, safe, UI_10_FONT_ID, subtitleY, tr(STR_EOB_CONTINUE_WITH)); // The list renders through the FreeInkApp so its rows register touch hit // rects; renderUi re-derives the device context, picking up any rotation // since construction (reader menu rotate). renderUi(); const auto labels = input.mapLabels(tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); }