diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 108a4aa5..f9ec3312 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -257,6 +257,9 @@ STR_YES: "Yes" STR_NO: "No" STR_SHOW: "Show" STR_HIDE: "Hide" +STR_SHIFT: "shift" +STR_SHIFT_CAPS: "SHIFT" +STR_ABC: "abc" STR_STATE_ON: "ON" STR_STATE_OFF: "OFF" STR_NOT_SET: "Not Set" diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index c112537e..62613ff4 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -2,188 +2,206 @@ #include +#include + #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" -// Keyboard layouts - lowercase -const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = { - "`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./", - "^ _____?", "SPECIAL ROW"}; - -// Shift state strings -const char* const KeyboardEntryActivity::shiftString[3] = {"shift", "SHIFT", "LOCK"}; - void KeyboardEntryActivity::onEnter() { Activity::onEnter(); - - // Trigger first update requestUpdate(); } void KeyboardEntryActivity::onExit() { Activity::onExit(); } -int KeyboardEntryActivity::getRowLength(const int row) const { - if (row < 0 || row >= NUM_ROWS) return 0; +int KeyboardEntryActivity::getContentRowCount() const { return ABC_ROWS; } - // Return actual length of each row based on keyboard layout - switch (row) { - case 0: - return 13; // `1234567890-= - case 1: - return 13; // qwertyuiop[]backslash - case 2: - return 11; // asdfghjkl;' - case 3: - return 10; // zxcvbnm,./ - case 4: - return 11; // shift (2 wide), space (5 wide), backspace (2 wide), OK (2 wide) - default: - return 0; - } +int KeyboardEntryActivity::getTotalRowCount() const { return getContentRowCount() + 1; } + +int KeyboardEntryActivity::getBottomKeyCount() const { return isPassword ? 6 : 5; } + +bool KeyboardEntryActivity::isBottomRow(const int row) const { return row == getContentRowCount(); } + +const char* KeyboardEntryActivity::getShiftLabel() const { + const StrId labelId = shiftState > 0 ? StrId::STR_SHIFT_CAPS : StrId::STR_SHIFT; + return I18n::getInstance().get(labelId); } char KeyboardEntryActivity::getSelectedChar() const { - const char* const* layout = shiftState ? keyboardShift : keyboard; + const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; - if (selectedRow < 0 || selectedRow >= NUM_ROWS) return '\0'; - if (selectedCol < 0 || selectedCol >= getRowLength(selectedRow)) return '\0'; + if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; + if (selectedCol < 0 || selectedCol >= COLS) return '\0'; - return layout[selectedRow][selectedCol]; + const KeyDef& key = layout[selectedRow][selectedCol]; + const bool useSecondary = !symMode && shiftState > 0 && key.secondary != '\0'; + return useSecondary ? key.secondary : key.primary; } -bool KeyboardEntryActivity::handleKeyPress() { - // Handle special row (bottom row with shift, space, backspace, done) - if (selectedRow == SPECIAL_ROW) { - if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { - // Shift toggle (0 = lower case, 1 = upper case, 2 = shift lock) - shiftState = (shiftState + 1) % 3; - return true; - } +char KeyboardEntryActivity::getAlternativeChar() const { + if (symMode) return '\0'; - if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { - // Space bar - if (maxLength == 0 || text.length() < maxLength) { - text += ' '; - } - return true; - } + const KeyDef(*layout)[COLS] = abcLayout; - if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { - // Backspace - if (!text.empty()) { - text.pop_back(); - } - return true; - } + if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; + if (selectedCol < 0 || selectedCol >= COLS) return '\0'; - if (selectedCol >= DONE_COL) { - // Done button - onComplete(text); - return false; - } - } + const KeyDef& key = layout[selectedRow][selectedCol]; + const char current = getSelectedChar(); + if (current == key.primary && key.secondary != '\0') return key.secondary; + if (current == key.secondary) return key.primary; + return '\0'; +} - // Regular character - const char c = getSelectedChar(); - if (c == '\0') { - return true; - } - - if (maxLength == 0 || text.length() < maxLength) { - text += c; - // Auto-disable shift after typing a character in non-lock mode - if (shiftState == 1) { - shiftState = 0; - } - } +bool KeyboardEntryActivity::insertChar(char c) { + if (c == '\0') return true; + if (maxLength != 0 && text.length() >= maxLength) return true; + text += c; return true; } -void KeyboardEntryActivity::loop() { - // Handle navigation - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { - selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS); +SpecialKeyType KeyboardEntryActivity::getBottomSpecialKey(int index) const { + if (isPassword) { + switch (index) { + case 0: + return SpecShift; + case 1: + return SpecMode; + case 2: + return SpecReveal; + case 3: + return SpecSpace; + case 4: + return SpecDel; + case 5: + default: + return SpecOk; + } + } - const int maxCol = getRowLength(selectedRow) - 1; + switch (index) { + case 0: + return SpecShift; + case 1: + return SpecMode; + case 2: + return SpecSpace; + case 3: + return SpecDel; + case 4: + default: + return SpecOk; + } +} + +bool KeyboardEntryActivity::handleKeyPress() { + if (isBottomRow(selectedRow)) { + switch (getBottomSpecialKey(selectedCol)) { + case SpecShift: + if (symMode) return true; + shiftState = (shiftState + 1) % 2; + return true; + case SpecMode: { + symMode = !symMode; + int maxRow = getTotalRowCount() - 1; + if (selectedRow > maxRow) selectedRow = maxRow; + if (isBottomRow(selectedRow)) { + int bottomCount = getBottomKeyCount(); + if (selectedCol >= bottomCount) selectedCol = bottomCount - 1; + } else { + if (selectedCol >= COLS) selectedCol = COLS - 1; + } + return true; + } + case SpecReveal: + passwordVisible = !passwordVisible; + return true; + case SpecSpace: + return insertChar(' '); + case SpecDel: + if (!text.empty()) { + text.pop_back(); + } + return true; + case SpecOk: + onComplete(text); + return false; + default: + return true; + } + } + + return insertChar(getSelectedChar()); +} + +void KeyboardEntryActivity::loop() { + const int totalRows = getTotalRowCount(); + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this, totalRows] { + bool wasBottom = isBottomRow(selectedRow); + selectedRow = ButtonNavigator::previousIndex(selectedRow, totalRows); + if (wasBottom && !isBottomRow(selectedRow)) { + selectedCol = selectedCol * COLS / getBottomKeyCount(); + } else if (!wasBottom && isBottomRow(selectedRow)) { + selectedCol = selectedCol * getBottomKeyCount() / COLS; + } + int maxCol = isBottomRow(selectedRow) ? getBottomKeyCount() - 1 : COLS - 1; if (selectedCol > maxCol) selectedCol = maxCol; requestUpdate(); }); - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { - selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS); - - const int maxCol = getRowLength(selectedRow) - 1; + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this, totalRows] { + bool wasBottom = isBottomRow(selectedRow); + selectedRow = ButtonNavigator::nextIndex(selectedRow, totalRows); + if (wasBottom && !isBottomRow(selectedRow)) { + selectedCol = selectedCol * COLS / getBottomKeyCount(); + } else if (!wasBottom && isBottomRow(selectedRow)) { + selectedCol = selectedCol * getBottomKeyCount() / COLS; + } + int maxCol = isBottomRow(selectedRow) ? getBottomKeyCount() - 1 : COLS - 1; if (selectedCol > maxCol) selectedCol = maxCol; requestUpdate(); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { - const int maxCol = getRowLength(selectedRow) - 1; - - // Special bottom row case - if (selectedRow == SPECIAL_ROW) { - // Bottom row has special key widths - if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { - // In shift key, wrap to end of row - selectedCol = maxCol; - } else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { - // In space bar, move to shift - selectedCol = SHIFT_COL; - } else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { - // In backspace, move to space - selectedCol = SPACE_COL; - } else if (selectedCol >= DONE_COL) { - // At done button, move to backspace - selectedCol = BACKSPACE_COL; - } - } else { - selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); - } - + int maxCol = isBottomRow(selectedRow) ? getBottomKeyCount() - 1 : COLS - 1; + selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); requestUpdate(); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { - const int maxCol = getRowLength(selectedRow) - 1; - - // Special bottom row case - if (selectedRow == SPECIAL_ROW) { - // Bottom row has special key widths - if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { - // In shift key, move to space - selectedCol = SPACE_COL; - } else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { - // In space bar, move to backspace - selectedCol = BACKSPACE_COL; - } else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { - // In backspace, move to done - selectedCol = DONE_COL; - } else if (selectedCol >= DONE_COL) { - // At done button, wrap to beginning of row - selectedCol = SHIFT_COL; - } - } else { - selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); - } + int maxCol = isBottomRow(selectedRow) ? getBottomKeyCount() - 1 : COLS - 1; + selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); requestUpdate(); }); - // Selection if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - if (handleKeyPress()) { + confirmHeld = true; + confirmLongHandled = false; + } + + if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && + mappedInput.getHeldTime() > LONG_PRESS_MS) { + char alt = getAlternativeChar(); + if (alt != '\0') { + insertChar(alt); requestUpdate(); + confirmLongHandled = true; } - // If handleKeyPress returns false, it means onComplete was triggered, no update needed + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (confirmHeld && !confirmLongHandled) { + if (handleKeyPress()) { + requestUpdate(); + } + } + confirmHeld = false; + confirmLongHandled = false; } - // Cancel if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onCancel(); } @@ -192,29 +210,28 @@ void KeyboardEntryActivity::loop() { void KeyboardEntryActivity::render(RenderLock&&) { renderer.clearScreen(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); const auto& metrics = UITheme::getInstance().getMetrics(); const Rect contentRect = UITheme::getContentRect(renderer, true, true); GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight}, title.c_str()); - // Draw input field const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int inputStartY = - metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4; + const int inputStartY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; int inputHeight = 0; std::string displayText; - if (isPassword) { + if (isPassword && !passwordVisible) { displayText = std::string(text.length(), '*'); } else { displayText = text; } - // Show cursor at end displayText += "_"; - // Render input text across multiple lines int lineStartIdx = 0; int lineEndIdx = displayText.length(); int textWidth = 0; @@ -243,81 +260,78 @@ void KeyboardEntryActivity::render(RenderLock&&) { GUI.drawTextField(renderer, Rect{contentRect.x, inputStartY, contentRect.width, inputHeight}, textWidth); - // Draw keyboard - use compact spacing to fit 5 rows on screen - const int keyboardStartY = metrics.keyboardBottomAligned - ? contentRect.y + contentRect.height - metrics.verticalSpacing - - (metrics.keyboardKeyHeight + metrics.keyboardKeySpacing) * NUM_ROWS - : inputStartY + inputHeight + metrics.verticalSpacing * 4; - const int keyWidth = metrics.keyboardKeyWidth; const int keyHeight = metrics.keyboardKeyHeight; const int keySpacing = metrics.keyboardKeySpacing; + const int keyWidth = (pageWidth * 95 / 100 - (COLS - 1) * keySpacing) / COLS; + const int leftMargin = (pageWidth - (COLS * keyWidth + (COLS - 1) * keySpacing)) / 2; - const char* const* layout = shiftState ? keyboardShift : keyboard; + const int keyboardStartY = metrics.keyboardBottomAligned + ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - + (keyHeight + keySpacing) * getTotalRowCount() + metrics.keyboardVerticalOffset + : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; + const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; + const int contentRows = getContentRowCount(); - // Calculate left margin to center the longest row (13 keys) within the content area - const int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); - const int leftMargin = contentRect.x + (contentRect.width - maxRowWidth) / 2; - - for (int row = 0; row < NUM_ROWS; row++) { + for (int row = 0; row < contentRows; row++) { const int rowY = keyboardStartY + row * (keyHeight + keySpacing); - // Left-align all rows for consistent navigation - const int startX = leftMargin; + for (int col = 0; col < COLS; col++) { + const KeyDef& key = layout[row][col]; + const int keyX = leftMargin + col * (keyWidth + keySpacing); + const bool isSelected = row == selectedRow && col == selectedCol; - // Handle bottom row (row 4) specially with proper multi-column keys - if (row == SPECIAL_ROW) { - // Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols) - // Total: 11 visual columns, but we use logical positions for selection + char primaryChar = key.primary; + char secondaryChar = key.secondary; - int currentX = startX; - - // SHIFT key (logical col 0, spans 2 key widths) - const bool shiftSelected = (selectedRow == SPECIAL_ROW && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); - const int shiftWidth = SPACE_COL - SHIFT_COL; - const int shiftXWidth = shiftWidth * (keyWidth + keySpacing); - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, shiftXWidth, keyHeight}, shiftString[shiftState], - shiftSelected); - currentX += shiftXWidth; - - // Space bar (logical cols 2-6, spans 5 key widths) - const bool spaceSelected = - (selectedRow == SPECIAL_ROW && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); - const int spaceWidth = BACKSPACE_COL - SPACE_COL; - const int spaceXWidth = spaceWidth * (keyWidth + keySpacing); - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, spaceXWidth, keyHeight}, "_____", spaceSelected); - currentX += spaceXWidth; - - // Backspace key (logical col 7, spans 2 key widths) - const bool bsSelected = (selectedRow == SPECIAL_ROW && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); - const int backspaceWidth = DONE_COL - BACKSPACE_COL; - const int backspaceXWidth = backspaceWidth * (keyWidth + keySpacing); - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, backspaceXWidth, keyHeight}, "<-", bsSelected); - currentX += backspaceXWidth; - - // OK button (logical col 9, spans 2 key widths) - const bool okSelected = (selectedRow == SPECIAL_ROW && selectedCol >= DONE_COL); - const int okWidth = getRowLength(row) - DONE_COL; - const int okXWidth = okWidth * (keyWidth + keySpacing); - GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, okXWidth, keyHeight}, tr(STR_OK_BUTTON), okSelected); - } else { - // Regular rows: render each key individually - for (int col = 0; col < getRowLength(row); col++) { - // Get the character to display - const char c = layout[row][col]; - std::string keyLabel(1, c); - - const int keyX = startX + col * (keyWidth + keySpacing); - const bool isSelected = row == selectedRow && col == selectedCol; - GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, keyLabel.c_str(), isSelected); + if (!symMode && shiftState > 0 && key.secondary != '\0') { + primaryChar = key.secondary; + secondaryChar = key.primary; } + + const char primaryBuf[2] = {primaryChar, '\0'}; + const char secondaryBuf[2] = {secondaryChar, '\0'}; + + const bool showSecondary = !symMode && row == 0 && secondaryChar != '\0'; + GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, primaryBuf, isSelected, + showSecondary ? secondaryBuf : nullptr); } } - // Draw help text + const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; + const int bottomRowY = keyboardStartY + contentRows * (keyHeight + keySpacing) + bottomRowGap; + const int bkSpacing = metrics.keyboardBottomKeySpacing; + const int contentTotalWidth = COLS * keyWidth + (COLS - 1) * keySpacing; + const int bottomKeyCount = getBottomKeyCount(); + const int bottomKeyWidth = (contentTotalWidth - (bottomKeyCount - 1) * bkSpacing) / bottomKeyCount; + const bool bottomSelected = isBottomRow(selectedRow); + + struct BottomKeyInfo { + KeyboardKeyType themeType; + const char* label; + }; + + std::vector bottomKeys; + bottomKeys.reserve(6); + bottomKeys.push_back({KeyboardKeyType::Shift, getShiftLabel()}); + bottomKeys.push_back({KeyboardKeyType::Mode, symMode ? tr(STR_ABC) : "#@!"}); + if (isPassword) { + bottomKeys.push_back({KeyboardKeyType::Reveal, passwordVisible ? tr(STR_HIDE) : tr(STR_SHOW)}); + } + bottomKeys.push_back({KeyboardKeyType::Space, nullptr}); + bottomKeys.push_back({KeyboardKeyType::Del, nullptr}); + bottomKeys.push_back({KeyboardKeyType::Ok, tr(STR_OK_BUTTON)}); + + for (int i = 0; i < bottomKeyCount; i++) { + const int keyX = leftMargin + i * (bottomKeyWidth + bkSpacing); + const bool isSelected = bottomSelected && i == selectedCol; + + GUI.drawKeyboardKey(renderer, Rect{keyX, bottomRowY, bottomKeyWidth, keyHeight}, bottomKeys[i].label, isSelected, + nullptr, bottomKeys[i].themeType); + } + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - // Draw side button hints for Up/Down navigation GUI.drawSideButtonHints(renderer, ">", "<"); renderer.displayBuffer(); diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index e7e6b8cf..c5e29765 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -8,21 +9,15 @@ #include "../Activity.h" #include "util/ButtonNavigator.h" -/** - * Reusable keyboard entry activity for text input. - * Can be started from any activity that needs text entry via startActivityForResult() - */ +struct KeyDef { + char primary; + char secondary; +}; + +enum SpecialKeyType { SpecShift, SpecMode, SpecReveal, SpecSpace, SpecDel, SpecOk }; + class KeyboardEntryActivity : public Activity { public: - /** - * Constructor - * @param renderer Reference to the GfxRenderer for drawing - * @param mappedInput Reference to MappedInputManager for handling input - * @param title Title to display above the keyboard - * @param initialText Initial text to show in the input field - * @param maxLength Maximum length of input text (0 for unlimited) - * @param isPassword If true, display asterisks instead of actual characters - */ explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string title = "Enter Text", std::string initialText = "", const size_t maxLength = 0, const bool isPassword = false) @@ -32,7 +27,6 @@ class KeyboardEntryActivity : public Activity { maxLength(maxLength), isPassword(isPassword) {} - // Activity overrides void onEnter() override; void onExit() override; void loop() override; @@ -43,33 +37,121 @@ class KeyboardEntryActivity : public Activity { std::string text; size_t maxLength; bool isPassword; + bool passwordVisible = false; ButtonNavigator buttonNavigator; - // Keyboard state int selectedRow = 0; int selectedCol = 0; - int shiftState = 0; // 0 = lower case, 1 = upper case, 2 = shift lock) + int shiftState = 0; + bool symMode = false; + bool confirmHeld = false; + bool confirmLongHandled = false; - // Handlers void onComplete(std::string text); void onCancel(); - // Keyboard layout - static constexpr int NUM_ROWS = 5; - static constexpr int KEYS_PER_ROW = 13; // Max keys per row (rows 0 and 1 have 13 keys) - static const char* const keyboard[NUM_ROWS]; - static const char* const keyboardShift[NUM_ROWS]; - static const char* const shiftString[3]; + static constexpr uint16_t LONG_PRESS_MS = 500; - // Special key positions (bottom row) - static constexpr int SPECIAL_ROW = 4; - static constexpr int SHIFT_COL = 0; - static constexpr int SPACE_COL = 2; - static constexpr int BACKSPACE_COL = 7; - static constexpr int DONE_COL = 9; + static constexpr int COLS = 10; + static constexpr int ABC_ROWS = 4; + static constexpr int SYM_ROWS = 4; + static constexpr KeyDef abcLayout[ABC_ROWS][COLS] = { + {{'0', ')'}, + {'1', '!'}, + {'2', '@'}, + {'3', '#'}, + {'4', '$'}, + {'5', '%'}, + {'6', '^'}, + {'7', '&'}, + {'8', '*'}, + {'9', '('}}, + {{'q', 'Q'}, + {'w', 'W'}, + {'e', 'E'}, + {'r', 'R'}, + {'t', 'T'}, + {'y', 'Y'}, + {'u', 'U'}, + {'i', 'I'}, + {'o', 'O'}, + {'p', 'P'}}, + {{'a', 'A'}, + {'s', 'S'}, + {'d', 'D'}, + {'f', 'F'}, + {'g', 'G'}, + {'h', 'H'}, + {'j', 'J'}, + {'k', 'K'}, + {'l', 'L'}, + {'-', '_'}}, + {{'z', 'Z'}, + {'x', 'X'}, + {'c', 'C'}, + {'v', 'V'}, + {'b', 'B'}, + {'n', 'N'}, + {'m', 'M'}, + {'=', '+'}, + {'.', '>'}, + {',', '<'}}, + }; + + static constexpr KeyDef symLayout[SYM_ROWS][COLS] = { + {{'0', '\0'}, + {'1', '\0'}, + {'2', '\0'}, + {'3', '\0'}, + {'4', '\0'}, + {'5', '\0'}, + {'6', '\0'}, + {'7', '\0'}, + {'8', '\0'}, + {'9', '\0'}}, + {{')', '\0'}, + {'!', '\0'}, + {'@', '\0'}, + {'#', '\0'}, + {'$', '\0'}, + {'%', '\0'}, + {'^', '\0'}, + {'&', '\0'}, + {'*', '\0'}, + {'(', '\0'}}, + {{'-', '\0'}, + {'_', '\0'}, + {'=', '\0'}, + {'+', '\0'}, + {'[', '\0'}, + {']', '\0'}, + {'{', '\0'}, + {'}', '\0'}, + {';', '\0'}, + {':', '\0'}}, + {{'\'', '\0'}, + {'"', '\0'}, + {'/', '\0'}, + {'\\', '\0'}, + {'|', '\0'}, + {'?', '\0'}, + {'.', '\0'}, + {',', '\0'}, + {'~', '\0'}, + {'`', '\0'}}, + }; + + const char* getShiftLabel() const; + + int getContentRowCount() const; + int getTotalRowCount() const; + int getBottomKeyCount() const; + SpecialKeyType getBottomSpecialKey(int index) const; + bool isBottomRow(int row) const; char getSelectedChar() const; - bool handleKeyPress(); // false if onComplete was triggered - int getRowLength(int row) const; + char getAlternativeChar() const; + bool handleKeyPress(); + bool insertChar(char c); }; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 65608a75..1170fc46 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -862,17 +862,68 @@ void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* } void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { - renderer.drawText(UI_12_FONT_ID, rect.x + 10, rect.y, "["); - renderer.drawText(UI_12_FONT_ID, rect.x + rect.width - 15, rect.y + rect.height, "]"); + (void)textWidth; + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int bracketHeight = lineHeight; + const int fieldLeft = rect.x + 10; + const int fieldRight = rect.x + rect.width - 15; + const int topY = rect.y; + const int bottomY = rect.y + rect.height + lineHeight; + const int tickLen = bracketHeight / 2; + + renderer.drawLine(fieldLeft, topY, fieldLeft, bottomY); + renderer.drawLine(fieldLeft, topY, fieldLeft + tickLen, topY); + renderer.drawLine(fieldLeft, bottomY, fieldLeft + tickLen, bottomY); + + renderer.drawLine(fieldRight, topY, fieldRight, bottomY); + renderer.drawLine(fieldRight, topY, fieldRight - tickLen, topY); + renderer.drawLine(fieldRight, bottomY, fieldRight - tickLen, bottomY); } -void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, - const bool isSelected) const { - const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label); - const int textX = rect.x + (rect.width - itemWidth) / 2; +void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, + const char* secondaryLabel, const KeyboardKeyType keyType) const { if (isSelected) { - renderer.drawText(UI_10_FONT_ID, textX - 6, rect.y, "["); - renderer.drawText(UI_10_FONT_ID, textX + itemWidth, rect.y, "]"); + renderer.fillRect(rect.x, rect.y, rect.width, rect.height, true); + } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || + keyType == KeyboardKeyType::Reveal || keyType == KeyboardKeyType::Space || + keyType == KeyboardKeyType::Del || keyType == KeyboardKeyType::Ok) { + renderer.drawRect(rect.x, rect.y, rect.width, rect.height); + } + + if (keyType == KeyboardKeyType::Space) { + const int lineHalfWidth = rect.width * 3 / 10; + const int centerX = rect.x + rect.width / 2; + const int lineY = rect.y + rect.height / 2 + 3; + renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !isSelected); + return; + } + + if (keyType == KeyboardKeyType::Del) { + const int centerX = rect.x + rect.width / 2; + const int centerY = rect.y + rect.height / 2; + const int arrowLen = rect.width / 4; + const int arrowHead = arrowLen / 2; + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !isSelected); + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, + !isSelected); + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, + !isSelected); + return; + } + + const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0'; + const int primaryOffset = 0; + const int fontId = (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || + keyType == KeyboardKeyType::Reveal || keyType == KeyboardKeyType::Ok) + ? UI_10_FONT_ID + : UI_12_FONT_ID; + const int itemWidth = renderer.getTextWidth(fontId, label); + const int textX = rect.x + (rect.width - itemWidth) / 2; + const int textY = rect.y + (rect.height - renderer.getLineHeight(fontId)) / 2 + primaryOffset; + renderer.drawText(fontId, textX, textY, label, !isSelected); + + if (hasSecondary) { + const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel); + renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 1, rect.y - 3, secondaryLabel, !isSelected); } - renderer.drawText(UI_10_FONT_ID, textX, rect.y, label); } diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index e5ddc5d6..68b5fab0 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -60,12 +60,16 @@ struct ThemeMetrics { int keyboardKeyWidth; int keyboardKeyHeight; int keyboardKeySpacing; + int keyboardBottomKeySpacing; bool keyboardBottomAligned; bool keyboardCenteredText; + int keyboardVerticalOffset; }; enum UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot, Weather }; +enum class KeyboardKeyType { Normal, Shift, Mode, Reveal, Space, Del, Ok }; + // Default theme implementation (Classic Theme) // Additional themes can inherit from this and override methods as needed @@ -98,8 +102,10 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .keyboardKeyWidth = 22, .keyboardKeyHeight = 30, .keyboardKeySpacing = 10, - .keyboardBottomAligned = false, - .keyboardCenteredText = false}; + .keyboardBottomKeySpacing = 0, + .keyboardBottomAligned = true, + .keyboardCenteredText = false, + .keyboardVerticalOffset = -10}; } class BaseTheme { @@ -142,7 +148,9 @@ class BaseTheme { const int textYOffset = 0) const; virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; - virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const; + virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, + const char* secondaryLabel = nullptr, + KeyboardKeyType keyType = KeyboardKeyType::Normal) const; virtual bool showsFileIcons() const { return false; } // Shared constants and helpers for battery drawing (used by all themes) diff --git a/src/components/themes/lyra/Lyra3CoversTheme.h b/src/components/themes/lyra/Lyra3CoversTheme.h index dde92001..3c17ec86 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.h +++ b/src/components/themes/lyra/Lyra3CoversTheme.h @@ -36,8 +36,10 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .keyboardKeyWidth = 31, .keyboardKeyHeight = 50, .keyboardKeySpacing = 0, + .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, - .keyboardCenteredText = true}; + .keyboardCenteredText = true, + .keyboardVerticalOffset = 0}; } class Lyra3CoversTheme : public LyraTheme { diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 7658356f..36fd800b 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -796,14 +796,52 @@ void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3); } -void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, - const bool isSelected) const { +void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, + const char* secondaryLabel, const KeyboardKeyType keyType) const { if (isSelected) { renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black); + } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || + keyType == KeyboardKeyType::Reveal || keyType == KeyboardKeyType::Del || + keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok) { + renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cornerRadius, true); } - const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, label); + const bool invert = isSelected; + + if (keyType == KeyboardKeyType::Space) { + const int lineHalfWidth = rect.width * 3 / 10; + const int centerX = rect.x + rect.width / 2; + const int lineY = rect.y + rect.height / 2 + 3; + renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert); + return; + } + + if (keyType == KeyboardKeyType::Del) { + const int centerX = rect.x + rect.width / 2; + const int centerY = rect.y + rect.height / 2; + const int arrowLen = rect.width / 4; + const int arrowHead = arrowLen / 2; + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert); + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, + !invert); + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, + !invert); + return; + } + + const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0'; + const int primaryOffset = 0; + const int fontId = (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || + keyType == KeyboardKeyType::Reveal || keyType == KeyboardKeyType::Ok) + ? UI_10_FONT_ID + : UI_12_FONT_ID; + const int textWidth = renderer.getTextWidth(fontId, label); const int textX = rect.x + (rect.width - textWidth) / 2; - const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; - renderer.drawText(UI_12_FONT_ID, textX, textY, label, !isSelected); + const int textY = rect.y + (rect.height - renderer.getLineHeight(fontId)) / 2 + primaryOffset; + renderer.drawText(fontId, textX, textY, label, !invert); + + if (hasSecondary) { + const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel); + renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 1, rect.y, secondaryLabel, !invert); + } } diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index c63e20d9..08f0ebaa 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -32,10 +32,12 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .statusBarHorizontalMargin = 5, .statusBarVerticalMargin = 19, .keyboardKeyWidth = 31, - .keyboardKeyHeight = 50, + .keyboardKeyHeight = 40, .keyboardKeySpacing = 0, + .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, - .keyboardCenteredText = true}; + .keyboardCenteredText = true, + .keyboardVerticalOffset = -12}; } class LyraTheme : public BaseTheme { @@ -67,7 +69,9 @@ class LyraTheme : public BaseTheme { Rect drawPopup(const GfxRenderer& renderer, const char* message) const override; void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const override; void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override; - void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override; + void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, + const char* secondaryLabel = nullptr, + KeyboardKeyType keyType = KeyboardKeyType::Normal) const override; bool showsFileIcons() const override { return true; } protected: