#include "OperatingModes.h" #include "ScrollMessage.hpp" #define HELP_TEXT_TIMEOUT_TICKS (TICKS_SECOND * 3) /* * The settings menu is the most complex bit of GUI code we have * The menu consists of a two tier menu * Main menu -> Categories * Secondary menu -> Settings * * For each entry in the menu */ /** * Prints two small lines (or one line for CJK) of short description for * setting items and prepares cursor after it. * @param settingsItemIndex Index of the setting item. * @param cursorCharPosition Custom cursor char position to set after printing * description. */ static void printShortDescription(SettingsItemIndex settingsItemIndex, uint16_t cursorCharPosition) { // print short description (default single line, explicit double line) uint8_t shortDescIndex = static_cast(settingsItemIndex); OLED::printWholeScreen(translatedString(Tr->SettingsShortNames[shortDescIndex])); // prepare cursor for value // make room for scroll indicator OLED::setCursor(cursorCharPosition * FONT_12_WIDTH - 2, 0); } // Render a menu, based on the position given // This will either draw the menu item, or the help text depending on how long its been since button press void render_menu(const menuitem *item, guiContext *cxt) { // If recent interaction or not help text draw the entry if ((xTaskGetTickCount() - lastButtonTime < HELP_TEXT_TIMEOUT_TICKS) || item->description == 0) { if (item->shortDescriptionSize > 0) { printShortDescription(item->shortDescriptionIndex, item->shortDescriptionSize); } item->draw(); } else { uint16_t *isRenderingHelp = &(cxt->scratch_state.state6); *isRenderingHelp = 1; // Draw description const char *description = translatedString(Tr->SettingsDescriptions[item->description - 1]); drawScrollingText(description, (xTaskGetTickCount() - lastButtonTime) - HELP_TEXT_TIMEOUT_TICKS); } } uint16_t getMenuLength(const menuitem *menu, const uint16_t stop) { // walk this menu to find the length uint16_t counter = 0; for (uint16_t pos = 0; pos < stop; pos++) { // End of list if (menu[pos].draw == nullptr) { return counter; } // Otherwise increment for each visible item (null == always, or if not check function) if (menu[pos].isVisible == nullptr || menu[pos].isVisible()) { counter++; } } return counter; } OperatingMode moveToNextEntry(guiContext *cxt) { uint16_t *mainEntry = &(cxt->scratch_state.state1); uint16_t *subEntry = &(cxt->scratch_state.state2); uint16_t *currentMenuLength = &(cxt->scratch_state.state5); uint16_t *isRenderingHelp = &(cxt->scratch_state.state6); if (*isRenderingHelp) { *isRenderingHelp = 0; } else { *currentMenuLength = 0; // Reset menu length // Scroll down // We can increment freely _once_ cxt->transitionMode = TransitionAnimation::Down; if (*subEntry == 0) { (*mainEntry) += 1; if (rootSettingsMenu[*mainEntry].draw == nullptr) { // We are off the end of the menu now saveSettings(); cxt->transitionMode = TransitionAnimation::Left; return OperatingMode::HomeScreen; } // Check if visible if (rootSettingsMenu[*mainEntry].isVisible != nullptr && !rootSettingsMenu[*mainEntry].isVisible()) { // We need to move on as this one isn't visible return moveToNextEntry(cxt); } } else { (*subEntry) += 1; // If the new entry is null, we need to exit if (subSettingsMenus[*mainEntry][(*subEntry) - 1].draw == nullptr) { (*subEntry) = 0; // Reset back to the main menu cxt->transitionMode = TransitionAnimation::Left; // Have to break early to avoid the below check underflowing return OperatingMode::SettingsMenu; } // Check if visible if (subSettingsMenus[*mainEntry][(*subEntry) - 1].isVisible != nullptr && !subSettingsMenus[*mainEntry][(*subEntry) - 1].isVisible()) { // We need to move on as this one isn't visible return moveToNextEntry(cxt); } } } return OperatingMode::SettingsMenu; } OperatingMode gui_SettingsMenu(const ButtonState buttons, guiContext *cxt) { // Render out the current settings menu // State 1 -> Root menu // State 2 -> Sub entry // Draw main entry if sub-entry is 0, otherwise draw sub-entry uint16_t *mainEntry = &(cxt->scratch_state.state1); uint16_t *subEntry = &(cxt->scratch_state.state2); uint32_t *autoRepeatAcceleration = &(cxt->scratch_state.state3); uint32_t *autoRepeatTimer = &(cxt->scratch_state.state4); uint16_t *currentMenuLength = &(cxt->scratch_state.state5); uint16_t *isRenderingHelp = &(cxt->scratch_state.state6); const menuitem *currentMenu; // Draw the currently on screen item uint16_t currentScreen; if (*subEntry == 0) { // Drawing main menu currentMenu = rootSettingsMenu; currentScreen = *mainEntry; } else { // Drawing sub menu currentMenu = subSettingsMenus[*mainEntry]; currentScreen = (*subEntry) - 1; } render_menu(&(currentMenu[currentScreen]), cxt); // Update the cached menu length if unknown if (*currentMenuLength == 0) { // We walk the current menu to find the length *currentMenuLength = getMenuLength(currentMenu, 128 /* Max length of any menu*/); } if (*isRenderingHelp == 0) { // Draw scroll // Get virtual pos by counting entries from start to _here_ uint16_t currentVirtualPosition = getMenuLength(currentMenu, currentScreen + 1); if (currentVirtualPosition > 0) { currentVirtualPosition--; } // The height of the indicator is screen res height / total menu entries uint8_t indicatorHeight = OLED_HEIGHT / *currentMenuLength; if (indicatorHeight == 0) { indicatorHeight = 1; // always at least 1 pixel } uint16_t position = (OLED_HEIGHT * (uint16_t)currentVirtualPosition) / *currentMenuLength; bool showScrollbar = true; // Store if its the last option for this setting bool isLastOptionForSetting = false; if ((int)currentMenu[currentScreen].autoSettingOption < (int)SettingsOptions::SettingsOptionsLength) { isLastOptionForSetting = isLastSettingValue(currentMenu[currentScreen].autoSettingOption); } // Last settings menu entry, reset scroll show back so it flashes if (isLastOptionForSetting) { showScrollbar = false; } // Or Flash it showScrollbar |= (xTaskGetTickCount() % (TICKS_SECOND / 4) < (TICKS_SECOND / 8)); if (showScrollbar) { OLED::drawScrollIndicator((uint8_t)position, indicatorHeight); } } // Now handle user button input auto callIncrementHandler = [&]() { if (currentMenu[currentScreen].incrementHandler != nullptr) { currentMenu[currentScreen].incrementHandler(); } else if ((int)currentMenu[currentScreen].autoSettingOption < (int)SettingsOptions::SettingsOptionsLength) { nextSettingValue(currentMenu[currentScreen].autoSettingOption); } return false; }; // Set buttons actions according to the settings bool swapButtonSettings = getSettingValue(SettingsOptions::ReverseButtonSettings); uint8_t button_enter = swapButtonSettings ? BUTTON_B_SHORT : BUTTON_F_SHORT; uint8_t button_enter_long = swapButtonSettings ? BUTTON_B_LONG : BUTTON_F_LONG; uint8_t button_next = swapButtonSettings ? BUTTON_F_SHORT : BUTTON_B_SHORT; uint8_t button_next_long = swapButtonSettings ? BUTTON_F_LONG : BUTTON_B_LONG; OperatingMode newMode = OperatingMode::SettingsMenu; if (BUTTON_NONE == buttons) { (*autoRepeatAcceleration) = 0; // reset acceleration (*autoRepeatTimer) = 0; // reset acceleration } else if (BUTTON_BOTH == buttons) { if (*subEntry == 0) { saveSettings(); cxt->transitionMode = TransitionAnimation::Left; return OperatingMode::HomeScreen; } else { cxt->transitionMode = TransitionAnimation::Left; *subEntry = 0; return OperatingMode::SettingsMenu; } } else if (button_enter_long == buttons) { if (xTaskGetTickCount() + (*autoRepeatAcceleration) > (*autoRepeatTimer) + PRESS_ACCEL_INTERVAL_MAX) { callIncrementHandler(); // Update the check for if its the last version bool isLastOptionForSetting = false; if ((int)currentMenu[currentScreen].autoSettingOption < (int)SettingsOptions::SettingsOptionsLength) { isLastOptionForSetting = isLastSettingValue(currentMenu[currentScreen].autoSettingOption); } if (isLastOptionForSetting) { (*autoRepeatTimer) = TICKS_SECOND * 2; } else { (*autoRepeatTimer) = 0; } (*autoRepeatTimer) += xTaskGetTickCount(); (*autoRepeatAcceleration) += PRESS_ACCEL_STEP; *currentMenuLength = 0; // Reset incase menu visible changes } } else if (button_enter == buttons) { // Increment setting if (*isRenderingHelp) { *isRenderingHelp = 0; } else { *currentMenuLength = 0; // Reset incase menu visible changes if (*subEntry == 0) { // In a root menu, if its null handler we enter the menu if (currentMenu[currentScreen].incrementHandler != nullptr) { currentMenu[currentScreen].incrementHandler(); } else { (*subEntry) += 1; cxt->transitionMode = TransitionAnimation::Right; } } else { callIncrementHandler(); } } } else if (button_next_long == buttons) { if (xTaskGetTickCount() + (*autoRepeatAcceleration) > (*autoRepeatTimer) + PRESS_ACCEL_INTERVAL_MAX) { (*autoRepeatTimer) = xTaskGetTickCount(); (*autoRepeatAcceleration) += PRESS_ACCEL_STEP; newMode = moveToNextEntry(cxt); } } else if (button_next == buttons) { // Increment menu item newMode = moveToNextEntry(cxt); } else { // default } if ((PRESS_ACCEL_INTERVAL_MAX - (*autoRepeatAcceleration)) < PRESS_ACCEL_INTERVAL_MIN) { (*autoRepeatAcceleration) = PRESS_ACCEL_INTERVAL_MAX - PRESS_ACCEL_INTERVAL_MIN; } // Otherwise we stay put for next render iteration return newMode; }