Quick Menu UI Improvements: - Add navigation button hints (prev/next on front buttons, up/down on side buttons) - Fix orientation-aware margins for button hint areas in landscape modes Screen Rotation Toggle: - Add "Rotate Screen" option to toggle between Portrait and Landscape CCW - Force section reindex when orientation changes to properly reflow content - Position is automatically restored via content offset after reindex Customizable Menu Order: - Add "Edit List Order" option (fixed at bottom of menu) - Pick-and-place reordering: select item to move, navigate to destination, place - Visual feedback: filled highlight for cursor, outlined box for item being moved - Menu order persists in settings (quickMenuOrder array in CrossPointSettings) - New default order: Bookmark, Dictionary, Rotate Screen, Settings, Clear Cache Files changed: - CrossPointSettings.h: Add quickMenuOrder[5] setting - QuickMenuActivity.h/cpp: Edit mode, order rendering, pick-and-place logic - EpubReaderActivity.cpp: Handle TOGGLE_ORIENTATION action with section reset
325 lines
12 KiB
C++
325 lines
12 KiB
C++
#include "QuickMenuActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "MappedInputManager.h"
|
|
#include "fontIds.h"
|
|
|
|
namespace {
|
|
// Base menu item count (reorderable items)
|
|
constexpr int BASE_MENU_ITEM_COUNT = 5;
|
|
// Total display count including "Edit List Order"
|
|
constexpr int DISPLAY_ITEM_COUNT = 6;
|
|
|
|
// Menu items indexed by QuickMenuAction enum value
|
|
// 0=Dictionary, 1=Bookmark, 2=ClearCache, 3=Orientation, 4=Settings
|
|
const char* MENU_ITEMS[BASE_MENU_ITEM_COUNT] = {"Dictionary", "Bookmark", "Clear Cache", "Rotate Screen", "Settings"};
|
|
const char* MENU_DESCRIPTIONS_ADD[BASE_MENU_ITEM_COUNT] = {"Look up a word", "Add bookmark to this page",
|
|
"Free up storage space", "Toggle screen orientation",
|
|
"Open settings menu"};
|
|
const char* MENU_DESCRIPTIONS_REMOVE[BASE_MENU_ITEM_COUNT] = {"Look up a word", "Remove bookmark from this page",
|
|
"Free up storage space", "Toggle screen orientation",
|
|
"Open settings menu"};
|
|
} // namespace
|
|
|
|
void QuickMenuActivity::taskTrampoline(void* param) {
|
|
auto* self = static_cast<QuickMenuActivity*>(param);
|
|
self->displayTaskLoop();
|
|
}
|
|
|
|
void QuickMenuActivity::onEnter() {
|
|
Activity::onEnter();
|
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
|
|
|
// Reset selection
|
|
selectedIndex = 0;
|
|
|
|
// Trigger first update
|
|
updateRequired = true;
|
|
|
|
xTaskCreate(&QuickMenuActivity::taskTrampoline, "QuickMenuTask",
|
|
2048, // Stack size
|
|
this, // Parameters
|
|
1, // Priority
|
|
&displayTaskHandle // Task handle
|
|
);
|
|
}
|
|
|
|
void QuickMenuActivity::onExit() {
|
|
Activity::onExit();
|
|
|
|
// Wait until not rendering to delete task
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
if (displayTaskHandle) {
|
|
vTaskDelete(displayTaskHandle);
|
|
displayTaskHandle = nullptr;
|
|
vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack
|
|
}
|
|
vSemaphoreDelete(renderingMutex);
|
|
renderingMutex = nullptr;
|
|
}
|
|
|
|
void QuickMenuActivity::loop() {
|
|
if (editMode) {
|
|
// Edit mode logic
|
|
handleEditMode();
|
|
} else {
|
|
// Normal mode logic
|
|
handleNormalMode();
|
|
}
|
|
}
|
|
|
|
void QuickMenuActivity::handleNormalMode() {
|
|
// Handle back button - cancel
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
onCancel();
|
|
return;
|
|
}
|
|
|
|
// Handle confirm button - select current option
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
// Last item is "Edit List Order"
|
|
if (selectedIndex == DISPLAY_ITEM_COUNT - 1) {
|
|
// Enter edit mode - copy current order to local buffer
|
|
for (int i = 0; i < BASE_MENU_ITEM_COUNT; i++) {
|
|
localOrder[i] = SETTINGS.quickMenuOrder[i];
|
|
}
|
|
editMode = true;
|
|
selectedIndex = 0; // Start at first item in edit mode
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
// Get the action from the order array
|
|
const int actionIndex = SETTINGS.quickMenuOrder[selectedIndex];
|
|
QuickMenuAction action;
|
|
switch (actionIndex) {
|
|
case 0:
|
|
action = QuickMenuAction::DICTIONARY;
|
|
break;
|
|
case 1:
|
|
action = QuickMenuAction::ADD_BOOKMARK;
|
|
break;
|
|
case 2:
|
|
action = QuickMenuAction::CLEAR_CACHE;
|
|
break;
|
|
case 3:
|
|
action = QuickMenuAction::TOGGLE_ORIENTATION;
|
|
break;
|
|
case 4:
|
|
default:
|
|
action = QuickMenuAction::GO_TO_SETTINGS;
|
|
break;
|
|
}
|
|
onActionSelected(action);
|
|
return;
|
|
}
|
|
|
|
// Handle navigation
|
|
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
|
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
|
|
|
if (prevPressed) {
|
|
selectedIndex = (selectedIndex + DISPLAY_ITEM_COUNT - 1) % DISPLAY_ITEM_COUNT;
|
|
updateRequired = true;
|
|
} else if (nextPressed) {
|
|
selectedIndex = (selectedIndex + 1) % DISPLAY_ITEM_COUNT;
|
|
updateRequired = true;
|
|
}
|
|
}
|
|
|
|
void QuickMenuActivity::handleEditMode() {
|
|
// Handle back button - save and exit edit mode
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
// Save the local order to settings
|
|
for (int i = 0; i < BASE_MENU_ITEM_COUNT; i++) {
|
|
SETTINGS.quickMenuOrder[i] = localOrder[i];
|
|
}
|
|
SETTINGS.saveToFile();
|
|
editMode = false;
|
|
movingIndex = -1;
|
|
selectedIndex = DISPLAY_ITEM_COUNT - 1; // Select "Edit List Order" when exiting
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
// Handle confirm button - pick or place item
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
if (movingIndex < 0) {
|
|
// No item selected yet - pick up the current item
|
|
movingIndex = selectedIndex;
|
|
} else {
|
|
// Item is being moved - place it at the current position
|
|
if (movingIndex != selectedIndex) {
|
|
// Remove item from old position and insert at new position
|
|
const uint8_t movingItem = localOrder[movingIndex];
|
|
if (movingIndex < selectedIndex) {
|
|
// Moving down - shift items up
|
|
for (int i = movingIndex; i < selectedIndex; i++) {
|
|
localOrder[i] = localOrder[i + 1];
|
|
}
|
|
} else {
|
|
// Moving up - shift items down
|
|
for (int i = movingIndex; i > selectedIndex; i--) {
|
|
localOrder[i] = localOrder[i - 1];
|
|
}
|
|
}
|
|
localOrder[selectedIndex] = movingItem;
|
|
}
|
|
movingIndex = -1; // Deselect
|
|
}
|
|
updateRequired = true;
|
|
return;
|
|
}
|
|
|
|
// Handle navigation - just move cursor
|
|
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
|
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
|
|
|
if (prevPressed && selectedIndex > 0) {
|
|
selectedIndex--;
|
|
updateRequired = true;
|
|
} else if (nextPressed && selectedIndex < BASE_MENU_ITEM_COUNT - 1) {
|
|
selectedIndex++;
|
|
updateRequired = true;
|
|
}
|
|
}
|
|
|
|
void QuickMenuActivity::displayTaskLoop() {
|
|
while (true) {
|
|
if (updateRequired) {
|
|
updateRequired = false;
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
render();
|
|
xSemaphoreGive(renderingMutex);
|
|
}
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
}
|
|
}
|
|
|
|
void QuickMenuActivity::render() const {
|
|
renderer.clearScreen();
|
|
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
|
|
// Get bezel offsets
|
|
const int bezelTop = renderer.getBezelOffsetTop();
|
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
|
const int bezelRight = renderer.getBezelOffsetRight();
|
|
const int bezelBottom = renderer.getBezelOffsetBottom();
|
|
|
|
// Button hint space constants
|
|
constexpr int FRONT_BUTTON_SPACE = 45; // 40px button height + 5px padding
|
|
constexpr int SIDE_BUTTON_SPACE = 50; // 45px button area + 5px padding
|
|
|
|
// Calculate button hint margins based on orientation
|
|
// Physical button locations (fixed on device):
|
|
// - Front buttons: physical bottom in portrait
|
|
// - Side buttons: physical right in portrait
|
|
// These map to different logical edges depending on orientation
|
|
int frontBtnMarginTop = 0, frontBtnMarginBottom = 0, frontBtnMarginLeft = 0, frontBtnMarginRight = 0;
|
|
int sideBtnMarginTop = 0, sideBtnMarginBottom = 0, sideBtnMarginLeft = 0, sideBtnMarginRight = 0;
|
|
|
|
switch (renderer.getOrientation()) {
|
|
case GfxRenderer::Portrait:
|
|
// Front buttons at logical BOTTOM, Side buttons at logical RIGHT
|
|
frontBtnMarginBottom = FRONT_BUTTON_SPACE;
|
|
sideBtnMarginRight = SIDE_BUTTON_SPACE;
|
|
break;
|
|
case GfxRenderer::LandscapeClockwise:
|
|
// Front buttons at logical LEFT, Side buttons at logical BOTTOM
|
|
frontBtnMarginLeft = FRONT_BUTTON_SPACE;
|
|
sideBtnMarginBottom = SIDE_BUTTON_SPACE;
|
|
break;
|
|
case GfxRenderer::PortraitInverted:
|
|
// Front buttons at logical TOP, Side buttons at logical LEFT
|
|
frontBtnMarginTop = FRONT_BUTTON_SPACE;
|
|
sideBtnMarginLeft = SIDE_BUTTON_SPACE;
|
|
break;
|
|
case GfxRenderer::LandscapeCounterClockwise:
|
|
// Front buttons at logical RIGHT, Side buttons at logical TOP
|
|
frontBtnMarginRight = FRONT_BUTTON_SPACE;
|
|
sideBtnMarginTop = SIDE_BUTTON_SPACE;
|
|
break;
|
|
}
|
|
|
|
// Calculate usable content area with bezel and button hint margins
|
|
const int marginLeft = 20 + bezelLeft + frontBtnMarginLeft + sideBtnMarginLeft;
|
|
const int marginRight = 20 + bezelRight + frontBtnMarginRight + sideBtnMarginRight;
|
|
const int marginTop = 15 + bezelTop + frontBtnMarginTop + sideBtnMarginTop;
|
|
const int marginBottom = 15 + bezelBottom + frontBtnMarginBottom + sideBtnMarginBottom;
|
|
const int contentWidth = pageWidth - marginLeft - marginRight;
|
|
const int contentHeight = pageHeight - marginTop - marginBottom;
|
|
|
|
// Draw header - different text in edit mode
|
|
const char* headerText = editMode ? "Edit Menu Order" : "Quick Menu";
|
|
renderer.drawCenteredText(UI_12_FONT_ID, marginTop, headerText, true, EpdFontFamily::BOLD);
|
|
|
|
// Select descriptions based on bookmark state
|
|
const char* const* descriptions = isPageBookmarked ? MENU_DESCRIPTIONS_REMOVE : MENU_DESCRIPTIONS_ADD;
|
|
|
|
// Get the order array to use (local copy in edit mode, settings otherwise)
|
|
const uint8_t* order = editMode ? localOrder : SETTINGS.quickMenuOrder;
|
|
|
|
// Draw menu items centered in content area
|
|
constexpr int itemHeight = 50; // Height for each menu item (including description)
|
|
const int startY = marginTop + (contentHeight - (DISPLAY_ITEM_COUNT * itemHeight)) / 2;
|
|
|
|
for (int i = 0; i < DISPLAY_ITEM_COUNT; i++) {
|
|
const int itemY = startY + i * itemHeight;
|
|
const bool isSelected = (i == selectedIndex);
|
|
const bool isBeingMoved = (editMode && i == movingIndex);
|
|
|
|
// Draw selection highlight (black fill) for selected item
|
|
if (isSelected) {
|
|
renderer.fillRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6);
|
|
}
|
|
// Draw outline for item being moved (when cursor is elsewhere)
|
|
if (isBeingMoved && !isSelected) {
|
|
renderer.drawRect(marginLeft + 10, itemY - 2, contentWidth - 20, itemHeight - 6);
|
|
}
|
|
|
|
// Last item is always "Edit List Order" (fixed, not in the order array)
|
|
if (i == DISPLAY_ITEM_COUNT - 1) {
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, "- Edit List Order -", !isSelected);
|
|
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, "Customize menu order", !isSelected);
|
|
} else {
|
|
// Get the action index from the order array
|
|
const int actionIndex = order[i];
|
|
|
|
// Draw menu item text - add indicator for item being moved
|
|
const char* itemText = MENU_ITEMS[actionIndex];
|
|
// For bookmark item (action index 1), show different text based on state
|
|
if (actionIndex == 1) {
|
|
itemText = isPageBookmarked ? "Remove Bookmark" : "Add Bookmark";
|
|
}
|
|
|
|
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected);
|
|
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[actionIndex], !isSelected);
|
|
}
|
|
}
|
|
|
|
// Draw help text at bottom - different hints for edit mode
|
|
if (editMode) {
|
|
const char* confirmLabel = (movingIndex < 0) ? "Pick" : "Place";
|
|
const auto labels = mappedInput.mapLabels("\xc2\xab Done", confirmLabel, "", "");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
// Side button hints for navigation
|
|
renderer.drawSideButtonHints(UI_10_FONT_ID, "<", ">");
|
|
} else {
|
|
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "Select", "< Prev", "Next >");
|
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
// Side button hints for up/down navigation
|
|
renderer.drawSideButtonHints(UI_10_FONT_ID, "<", ">");
|
|
}
|
|
|
|
renderer.displayBuffer();
|
|
}
|