#include "QuickMenuActivity.h" #include #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(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(); }