diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 3b001ead..c667b1a7 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -583,3 +583,18 @@ STR_BTN_ACT_PREV_SECTION: "Previous Section / Chapter" STR_BTN_ACT_EXIT_READER: "Exit Reader" STR_BTN_ACT_READER_MENU: "Reader Menu" STR_BTN_ACT_KOREADER_SYNC: "KOReader Sync" +STR_KB_HINT_MOVE_CURSOR: "Press LEFT or RIGHT to move cursor" +STR_KB_HINT_RETURN_CURSOR: "Press LEFT to return to cursor position" +STR_KB_HINT_HIDE_PASSWORD: "Hold RIGHT then press [***] to hide password" +STR_KB_HINT_SHOW_PASSWORD: "Hold RIGHT then press [abc] to show password" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Press [***] to hide password" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Press [abc] to show password" +STR_KB_HINT_EDIT_ENTRY: "Hold UP to edit entry" +STR_KB_TIPS: "Tips:" +STR_KB_HINT_RETURN_KEYBOARD: "Press DOWN to return to keyboard" +STR_KB_HINT_EXIT_URL_MODE: "Press ABC to exit URL mode" +STR_KB_HINT_CLEAR_TEXT: "Hold DEL to clear all text" +STR_KB_HINT_SECONDARY_CHAR: "Hold SELECT for secondary char" +STR_KB_HINT_UPPER_SECONDARY: "Hold SELECT for UPPERCASE or secondary char" +STR_KB_HINT_LOWER_SECONDARY: "Hold SELECT for lowercase or secondary char" +STR_KB_HINT_URL_SNIPPETS: "Press URL for snippets" diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 653d98e2..9d6a3285 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -1,31 +1,32 @@ #include "KeyboardEntryActivity.h" +#include #include +#include + #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" -const char* KeyboardEntryActivity::shiftLabel(int shiftState) { - return shiftState ? tr(STR_SHIFT_CAPS) : tr(STR_SHIFT); -} - void KeyboardEntryActivity::onEnter() { Activity::onEnter(); cursorPos = text.length(); symMode = false; urlMode = false; cursorMode = false; + togglePos = false; passwordVisible = false; shiftState = 0; selectedRow = 0; selectedCol = 0; - confirmHeld = false; - confirmLongHandled = false; - upHeld = false; - upLongHandled = false; - downHeld = false; - downLongHandled = false; + delPressCount = 0; + hintVisible = false; + hintShowTime = 0; + rightHeld = false; + rightLongHandled = false; + savedCursorPos = 0; + rightStartCursorPos = 0; requestUpdate(); } @@ -46,7 +47,7 @@ int KeyboardEntryActivity::getTotalRowCount() const { return getContentRowCount( bool KeyboardEntryActivity::isBottomRow(const int row) const { return row == getContentRowCount(); } char KeyboardEntryActivity::getSelectedChar() const { - const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; + const KeyDef(*layout)[COLS] = symMode ? symLayout : (inputType == InputType::Url ? urlLayout : abcLayout); if (selectedRow < 0 || selectedRow >= getContentRowCount()) return '\0'; if (selectedCol < 0 || selectedCol >= COLS) return '\0'; @@ -56,7 +57,8 @@ char KeyboardEntryActivity::getSelectedChar() const { } char KeyboardEntryActivity::getAlternativeChar() const { - if (symMode) return '\0'; + if (symMode || urlMode) return '\0'; + if (inputType == InputType::Url && selectedRow > 0) return '\0'; const KeyDef(*layout)[COLS] = abcLayout; @@ -92,15 +94,21 @@ void KeyboardEntryActivity::insertString(const std::string& str) { bool KeyboardEntryActivity::handleKeyPress() { if (isBottomRow(selectedRow)) { switch (static_cast(selectedCol)) { - case SpecShift: - if (urlMode) return true; + case SpecialKeyType::Shift: + delPressCount = 0; + hintVisible = false; + if (urlMode || inputType == InputType::Url) return true; if (symMode) return true; shiftState = (shiftState + 1) % 2; return true; - case SpecMode: { + case SpecialKeyType::Mode: { + delPressCount = 0; + hintVisible = false; if (urlMode) { urlMode = false; symMode = false; + selectedRow = getTotalRowCount() - 1; + selectedCol = static_cast(SpecialKeyType::Mode); requestUpdate(); return true; } @@ -114,26 +122,35 @@ bool KeyboardEntryActivity::handleKeyPress() { } return true; } - case SpecSpace: + case SpecialKeyType::Space: + delPressCount = 0; + hintVisible = false; if (inputType == InputType::Url) { urlMode = !urlMode; if (urlMode) { symMode = false; } selectedRow = getTotalRowCount() - 1; - selectedCol = SpecSpace; + selectedCol = static_cast(SpecialKeyType::Space); requestUpdate(); } else { return insertChar(' '); } return true; - case SpecDel: + case SpecialKeyType::Del: + delPressCount++; + if (delPressCount >= 2) { + hintVisible = true; + hintShowTime = millis(); + } if (cursorPos > 0 && !text.empty()) { text.erase(cursorPos - 1, 1); cursorPos--; } return true; - case SpecOk: + case SpecialKeyType::Ok: + delPressCount = 0; + hintVisible = false; onComplete(text); return false; default: @@ -142,6 +159,8 @@ bool KeyboardEntryActivity::handleKeyPress() { } if (urlMode) { + delPressCount = 0; + hintVisible = false; const int idx = selectedCol + selectedRow * 3; if (idx < URL_SNIPPET_COUNT) { insertString(urlSnippets[idx]); @@ -149,6 +168,9 @@ bool KeyboardEntryActivity::handleKeyPress() { return true; } + delPressCount = 0; + hintVisible = false; + return insertChar(getSelectedChar()); } @@ -174,6 +196,8 @@ void KeyboardEntryActivity::loop() { mappedInput.getHeldTime() > LONG_PRESS_MS) { cursorMode = true; upLongHandled = true; + hintVisible = true; + hintShowTime = millis(); requestUpdate(); } @@ -198,11 +222,10 @@ void KeyboardEntryActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { downHeld = true; if (cursorMode) { - if (cursorPos > text.length()) { - cursorPos = text.length(); - } + togglePos = false; passwordVisible = false; cursorMode = false; + hintVisible = false; downLongHandled = true; requestUpdate(); } else { @@ -229,41 +252,72 @@ void KeyboardEntryActivity::loop() { } buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { - if (cursorMode) { - if (cursorPos > 0) { - cursorPos--; - requestUpdate(); - } - return; - } + if (cursorMode) return; int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); requestUpdate(); }); - buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { + if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { if (cursorMode) { - if (cursorPos < text.length()) { - cursorPos++; + if (togglePos) { + cursorPos = savedCursorPos; + togglePos = false; requestUpdate(); - } else if (cursorPos == text.length() && inputType == InputType::Password) { - cursorPos = text.length() + 1; + } else if (cursorPos > 0) { + cursorPos--; requestUpdate(); } - return; } + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { + if (cursorMode && inputType == InputType::Password && !togglePos) { + rightHeld = true; + rightLongHandled = false; + rightStartCursorPos = cursorPos; + } + } + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { + if (cursorMode) return; int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); requestUpdate(); }); + if (rightHeld && !rightLongHandled && mappedInput.isPressed(MappedInputManager::Button::Right) && + mappedInput.getHeldTime() > LONG_PRESS_MS) { + if (cursorMode && inputType == InputType::Password && !togglePos) { + savedCursorPos = rightStartCursorPos; + togglePos = true; + rightLongHandled = true; + requestUpdate(); + } + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { + if (cursorMode && inputType == InputType::Password) { + rightHeld = false; + rightLongHandled = false; + } + if (cursorMode && !togglePos && cursorPos < text.length()) { + cursorPos++; + requestUpdate(); + } + if (cursorMode) return; + rightHeld = false; + rightLongHandled = false; + } + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { confirmHeld = true; confirmLongHandled = false; } if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && isBottomRow(selectedRow) && selectedCol == SpecDel) { + mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && isBottomRow(selectedRow) && + selectedCol == static_cast(SpecialKeyType::Del)) { text.clear(); cursorPos = 0; confirmLongHandled = true; @@ -271,7 +325,7 @@ void KeyboardEntryActivity::loop() { } if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() > LONG_PRESS_MS && !cursorMode && inputType == InputType::Text && !urlMode) { + mappedInput.getHeldTime() > LONG_PRESS_MS) { char alt = getAlternativeChar(); if (alt != '\0') { insertChar(alt); @@ -285,8 +339,7 @@ void KeyboardEntryActivity::loop() { if (handleKeyPress()) { requestUpdate(); } - } else if (confirmHeld && !confirmLongHandled && cursorMode && inputType == InputType::Password && - cursorPos > text.length()) { + } else if (confirmHeld && !confirmLongHandled && cursorMode && inputType == InputType::Password && togglePos) { passwordVisible = !passwordVisible; requestUpdate(); } @@ -297,24 +350,28 @@ void KeyboardEntryActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onCancel(); } + + if (hintVisible && !cursorMode && millis() - hintShowTime > 4000) { + hintVisible = false; + requestUpdate(); + } } void KeyboardEntryActivity::render(RenderLock&&) { renderer.clearScreen(); - const Rect contentRect = UITheme::getContentRect(renderer, true, true); + const Rect contentRect = UITheme::getContentRect(renderer, true, gpio.deviceIsX3()); const int pageWidth = contentRect.width; + const int pageHeight = contentRect.height; const int contentX = contentRect.x; - const int contentY = contentRect.y; const auto& metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{contentX, contentY + metrics.topPadding, pageWidth, metrics.headerHeight}, - title.c_str()); + GUI.drawHeader(renderer, Rect{contentX, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int inputStartY = contentY + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + + const int inputStartY = contentRect.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; - int inputHeight = lineHeight; + int inputHeight = 0; std::string displayText; if (inputType == InputType::Password && !passwordVisible) { @@ -322,7 +379,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { if (cursorMode) { revealPos = text.length(); // no reveal in displayText; block draws actual char directly } else { - revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : 0; + revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : std::string::npos; } displayText = text; for (size_t i = 0; i < displayText.length(); i++) { @@ -335,82 +392,33 @@ void KeyboardEntryActivity::render(RenderLock&&) { } const bool isPassword = (inputType == InputType::Password); - const int margin = metrics.contentSidePadding; - const int extraMargin = 10; - const int effectiveMargin = margin + extraMargin; - const int toggleGap = isPassword ? 8 : 0; + const int usableWidth = pageWidth; + const int effectiveMargin = (pageWidth - usableWidth * metrics.keyboardTextFieldWidthPercent / 100) / 2; + const int toggleGap = isPassword ? 4 : 0; const int toggleReserve = isPassword ? std::max(renderer.getTextWidth(UI_12_FONT_ID, "[abc]"), renderer.getTextWidth(UI_12_FONT_ID, "[***]")) + toggleGap : 0; - const int textAreaWidth = pageWidth - 2 * effectiveMargin - toggleReserve; + const int textAreaWidth = usableWidth - 2 * effectiveMargin - toggleReserve; const int maxLineWidth = textAreaWidth; const bool centerText = metrics.keyboardCenteredText; - const int keyHeight = metrics.keyboardKeyHeight; - const int bottomKeyHeight = metrics.keyboardBottomKeyHeight; - const int keySpacing = metrics.keyboardKeySpacing; - const int contentCols = getContentColCount(); - const int keyWidth = (pageWidth * 95 / 100 - (contentCols - 1) * keySpacing) / contentCols; - const int leftMargin = contentX + (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; - - const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; - const int keyboardStartY = metrics.keyboardBottomAligned - ? contentY + contentRect.height - metrics.verticalSpacing - - (keyHeight + keySpacing) * getContentRowCount() - bottomKeyHeight - - bottomRowGap + metrics.keyboardVerticalOffset - : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; - - const char* helpTexts[3]; - if (inputType == InputType::Url) { - helpTexts[0] = tr(STR_KEYBOARD_HELP_URL_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_URL_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_URL_LINE_3); - } else if (cursorMode) { - helpTexts[0] = tr(STR_KEYBOARD_HELP_CURSOR_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_CURSOR_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_CURSOR_LINE_3); - } else if (inputType == InputType::Password) { - helpTexts[0] = tr(STR_KEYBOARD_HELP_PASSWORD_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_PASSWORD_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_PASSWORD_LINE_3); - } else { - helpTexts[0] = tr(STR_KEYBOARD_HELP_DEFAULT_LINE_1); - helpTexts[1] = tr(STR_KEYBOARD_HELP_DEFAULT_LINE_2); - helpTexts[2] = tr(STR_KEYBOARD_HELP_DEFAULT_LINE_3); - } - - const int helpGap = keyboardStartY - (inputStartY + inputHeight); - const int helpLineHeight = renderer.getLineHeight(SMALL_FONT_ID); - const int maxHelpLines = std::max(0, helpGap / helpLineHeight); - const int helpLineCount = std::min(3, maxHelpLines); - const int helpBodyHeight = helpLineCount * helpLineHeight; - const int helpTop = inputStartY + inputHeight + std::max(0, (helpGap - helpBodyHeight) / 2); - const int helpLineWidth = pageWidth - 2 * metrics.contentSidePadding; - for (int i = 0; i < helpLineCount; ++i) { - const char* helpText = helpTexts[i]; - const std::string line = renderer.truncatedText(SMALL_FONT_ID, helpText, helpLineWidth, EpdFontFamily::ITALIC); - renderer.drawText(SMALL_FONT_ID, contentX + metrics.contentSidePadding, helpTop + i * helpLineHeight, line.c_str(), - true, EpdFontFamily::ITALIC); - } - - int cursorCharWidth; + int cursorCharWidth = 6; if (cursorPos < text.length()) { - cursorCharWidth = renderer.getTextWidth(UI_12_FONT_ID, text.substr(cursorPos, 1).c_str()); - } else { - cursorCharWidth = 6; + int w = renderer.getTextWidth(UI_12_FONT_ID, text.substr(cursorPos, 1).c_str()); + if (w > cursorCharWidth) cursorCharWidth = w; } int lineStartIdx = 0; int lineEndIdx = displayText.length(); int textWidth = 0; - int cursorPixelX = contentX + metrics.contentSidePadding; + int cursorPixelX = contentX + effectiveMargin; int cursorLineY = inputStartY; bool cursorDrawn = false; while (true) { std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); - textWidth = renderer.getTextWidth(UI_12_FONT_ID, lineText.c_str()); + textWidth = renderer.getTextAdvanceX(UI_12_FONT_ID, lineText.c_str(), EpdFontFamily::REGULAR); if (textWidth <= maxLineWidth) { const bool isLastLine = (lineEndIdx == static_cast(displayText.length())); bool isCursorLine = false; @@ -422,14 +430,20 @@ void KeyboardEntryActivity::render(RenderLock&&) { } else { beforeCursor = displayText.substr(lineStartIdx, cursorPos - lineStartIdx); } - int beforeWidth = renderer.getTextWidth(UI_12_FONT_ID, beforeCursor.c_str()); - if (centerText) { - cursorPixelX = contentX + effectiveMargin + (maxLineWidth - textWidth) / 2 + beforeWidth; - } else { - cursorPixelX = contentX + effectiveMargin + beforeWidth; + int beforeWidth = renderer.getTextAdvanceX(UI_12_FONT_ID, beforeCursor.c_str(), EpdFontFamily::REGULAR); + int kernOffset = 0; + if (cursorPos < displayText.length()) { + std::string beforeAndCursor = beforeCursor + displayText.substr(cursorPos, 1); + int beforeAndCursorWidth = + renderer.getTextAdvanceX(UI_12_FONT_ID, beforeAndCursor.c_str(), EpdFontFamily::REGULAR); + int charAdvance = + renderer.getTextAdvanceX(UI_12_FONT_ID, displayText.substr(cursorPos, 1).c_str(), EpdFontFamily::REGULAR); + kernOffset = beforeAndCursorWidth - beforeWidth - charAdvance; } - if (cursorPos == static_cast(lineEndIdx) && cursorPos == displayText.length()) { - cursorPixelX += 2; + if (centerText) { + cursorPixelX = contentX + effectiveMargin + (maxLineWidth - textWidth) / 2 + beforeWidth + kernOffset; + } else { + cursorPixelX = contentX + effectiveMargin + beforeWidth + kernOffset; } cursorLineY = inputStartY + inputHeight; cursorDrawn = true; @@ -438,7 +452,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { const int lineStartX = contentX + (centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin); - if (isCursorLine && cursorMode && isPassword && !passwordVisible) { + if (isCursorLine && cursorMode && isPassword && !passwordVisible && !togglePos) { // Draw text in 3 parts to avoid block cursor overflowing onto next char. // displayText uses '*' for all chars; actual char may be wider than '*'. // Part 1: chars before cursor position @@ -468,18 +482,18 @@ void KeyboardEntryActivity::render(RenderLock&&) { } const int fieldWidth = (inputHeight > 0) ? maxLineWidth : textWidth; - const int lineMargin = margin + extraMargin - 5; + const int lineMargin = effectiveMargin; GUI.drawTextField(renderer, Rect{contentX, inputStartY, pageWidth, inputHeight}, fieldWidth, cursorMode, lineMargin, pageWidth - 2 * lineMargin); - if (cursorMode && cursorPos <= displayText.length()) { + if (cursorMode && !togglePos && cursorPos <= displayText.length()) { static constexpr int blockPadding = 1; renderer.fillRect(cursorPixelX - blockPadding, cursorLineY, cursorCharWidth + blockPadding * 2, lineHeight, true); if (cursorPos < text.length()) { const char buf[2] = {text[cursorPos], '\0'}; renderer.drawText(UI_12_FONT_ID, cursorPixelX, cursorLineY, buf, false); } - } else if (!cursorMode && cursorPos <= displayText.length()) { + } else if (cursorPos <= displayText.length()) { static constexpr int serifW = 3; const int cX = cursorPixelX; const int cY = cursorLineY; @@ -496,7 +510,7 @@ void KeyboardEntryActivity::render(RenderLock&&) { const int toggleWidth = renderer.getTextWidth(UI_12_FONT_ID, toggleLabel); const int toggleX = contentX + pageWidth - effectiveMargin - toggleWidth; const int toggleY = inputStartY + inputHeight; - const bool toggleSelected = cursorMode && cursorPos > text.length(); + const bool toggleSelected = cursorMode && togglePos; if (toggleSelected) { renderer.fillRect(toggleX - 2, toggleY, toggleWidth + 5, lineHeight + 3, true); @@ -506,9 +520,101 @@ void KeyboardEntryActivity::render(RenderLock&&) { } } + if (hintVisible && !text.empty()) { + const int hintLh = renderer.getLineHeight(SMALL_FONT_ID); + const int underlineY = inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; + const int hintY = underlineY + 4; + if (cursorMode) { + int hintLineY = hintY; + if (inputType == InputType::Password && togglePos) { + renderer.drawCenteredText( + SMALL_FONT_ID, hintLineY, + passwordVisible ? tr(STR_KB_HINT_TOGGLE_HIDE_PASSWORD) : tr(STR_KB_HINT_TOGGLE_SHOW_PASSWORD), true); + hintLineY += hintLh; + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, tr(STR_KB_HINT_RETURN_CURSOR), true); + } else { + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, tr(STR_KB_HINT_MOVE_CURSOR), true); + hintLineY += hintLh; + if (inputType == InputType::Password) { + const char* passTip = passwordVisible ? tr(STR_KB_HINT_HIDE_PASSWORD) : tr(STR_KB_HINT_SHOW_PASSWORD); + renderer.drawCenteredText(SMALL_FONT_ID, hintLineY, passTip, true); + } + } + } else { + renderer.drawCenteredText(SMALL_FONT_ID, hintY, tr(STR_KB_HINT_EDIT_ENTRY), true); + } + } + + const int keyHeight = metrics.keyboardKeyHeight; + const int bottomKeyHeight = metrics.keyboardBottomKeyHeight; + const int keySpacing = metrics.keyboardKeySpacing; + const int contentCols = getContentColCount(); + const int keyboardWidth = pageWidth * metrics.keyboardWidthPercent / 100; + const int keyWidth = (keyboardWidth - (contentCols - 1) * keySpacing) / contentCols; + const int leftMargin = contentX + (pageWidth - (contentCols * keyWidth + (contentCols - 1) * keySpacing)) / 2; + + const int bottomRowGap = metrics.keyboardBottomKeySpacing > 0 ? 4 : 0; + const int keyboardStartY = metrics.keyboardBottomAligned + ? contentRect.y + pageHeight - metrics.verticalSpacing - + (keyHeight + keySpacing) * getContentRowCount() - bottomKeyHeight - + bottomRowGap + metrics.keyboardVerticalOffset + : inputStartY + inputHeight + lineHeight + metrics.verticalSpacing; + + const int tipsLh = renderer.getLineHeight(SMALL_FONT_ID); + const int underlineBottom = inputStartY + inputHeight + lineHeight + metrics.verticalSpacing + 4; + auto drawTip = [&](const char* tip, int y) { renderer.drawCenteredText(SMALL_FONT_ID, y, tip, true); }; + + int tipCount = 0; + if (cursorMode) { + tipCount = 1; + } else if (urlMode) { + tipCount = 1 + (!text.empty() ? 1 : 0); + } else if (symMode) { + tipCount = !text.empty() ? 1 : 0; + } else { + tipCount = 1 + (inputType == InputType::Url ? 1 : 0) + (!text.empty() ? 1 : 0); + } + + if (tipCount > 0) { + int y = (underlineBottom + keyboardStartY) / 2 - (tipCount + 1) * tipsLh / 2; + drawTip(tr(STR_KB_TIPS), y); + y += tipsLh; + if (cursorMode) { + drawTip(tr(STR_KB_HINT_RETURN_KEYBOARD), y); + } else if (urlMode) { + drawTip(tr(STR_KB_HINT_EXIT_URL_MODE), y); + y += tipsLh; + if (!text.empty()) { + drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); + } + } else if (symMode) { + if (!text.empty()) { + drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); + } + } else { + const char* altCharTip; + if (inputType == InputType::Url) { + altCharTip = tr(STR_KB_HINT_SECONDARY_CHAR); + } else if (shiftState > 0) { + altCharTip = tr(STR_KB_HINT_LOWER_SECONDARY); + } else { + altCharTip = tr(STR_KB_HINT_UPPER_SECONDARY); + } + drawTip(altCharTip, y); + y += tipsLh; + if (inputType == InputType::Url) { + drawTip(tr(STR_KB_HINT_URL_SNIPPETS), y); + y += tipsLh; + } + if (!text.empty()) { + drawTip(tr(STR_KB_HINT_CLEAR_TEXT), y); + } + } + } + const int bkSpacing = metrics.keyboardBottomKeySpacing; - const int contentTotalWidth = - COLS * ((pageWidth * 95 / 100 - (COLS - 1) * keySpacing) / COLS) + (COLS - 1) * keySpacing; + const int abcKeyWidth = (keyboardWidth - (COLS - 1) * keySpacing) / COLS; + const int contentTotalWidth = COLS * abcKeyWidth + (COLS - 1) * keySpacing; const int bottomKeyWidth = (contentTotalWidth - (BOTTOM_KEY_COUNT - 1) * bkSpacing) / BOTTOM_KEY_COUNT; const int bottomLeftMargin = contentX + (pageWidth - (BOTTOM_KEY_COUNT * bottomKeyWidth + (BOTTOM_KEY_COUNT - 1) * bkSpacing)) / 2; @@ -516,11 +622,12 @@ void KeyboardEntryActivity::render(RenderLock&&) { int urlLeftMargin = leftMargin; if (urlMode) { const int urlTotalWidth = 3 * keyWidth + 2 * keySpacing; - const int urlCenterX = bottomLeftMargin + SpecSpace * (bottomKeyWidth + bkSpacing) + bottomKeyWidth / 2; + const int urlCenterX = + bottomLeftMargin + static_cast(SpecialKeyType::Space) * (bottomKeyWidth + bkSpacing) + bottomKeyWidth / 2; urlLeftMargin = urlCenterX - urlTotalWidth / 2; } - const KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; + const KeyDef(*layout)[COLS] = symMode ? symLayout : (inputType == InputType::Url ? urlLayout : abcLayout); const int contentRows = getContentRowCount(); for (int row = 0; row < contentRows; row++) { @@ -566,7 +673,9 @@ void KeyboardEntryActivity::render(RenderLock&&) { const char* label; }; const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = { - {KeyboardKeyType::Shift, (symMode || urlMode) ? shiftLabel(0) : shiftLabel(shiftState)}, + {(symMode || urlMode || inputType == InputType::Url) ? KeyboardKeyType::Disabled : KeyboardKeyType::Shift, + (symMode || urlMode || inputType == InputType::Url) ? tr(STR_SHIFT) + : (shiftState == 0 ? tr(STR_SHIFT) : tr(STR_SHIFT_CAPS))}, {KeyboardKeyType::Mode, urlMode ? "abc" : (symMode ? "abc" : "#@!")}, {inputType == InputType::Url ? KeyboardKeyType::Mode : KeyboardKeyType::Space, inputType == InputType::Url ? "URL" : nullptr}, diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index f135429d..0a86febf 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -14,7 +14,7 @@ struct KeyDef { char secondary; }; -enum SpecialKeyType { SpecShift, SpecMode, SpecSpace, SpecDel, SpecOk }; +enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }; enum class InputType { Text, Password, Url }; @@ -51,18 +51,26 @@ class KeyboardEntryActivity : public Activity { bool confirmLongHandled = false; bool cursorMode = false; + bool togglePos = false; size_t cursorPos = 0; bool upHeld = false; bool upLongHandled = false; bool downHeld = false; bool downLongHandled = false; + bool rightHeld = false; + bool rightLongHandled = false; + size_t savedCursorPos = 0; + size_t rightStartCursorPos = 0; bool urlMode = false; - static const char* shiftLabel(int shiftState); static constexpr int URL_SNIPPET_COUNT = 9; static constexpr const char* const urlSnippets[URL_SNIPPET_COUNT] = { "https://", "www.", ".com", "http://", "192.168.", ".org", "/opds", ":8080", ".net"}; + int delPressCount = 0; + bool hintVisible = false; + unsigned long hintShowTime = 0; + void onComplete(std::string text); void onCancel(); @@ -75,8 +83,7 @@ class KeyboardEntryActivity : public Activity { static constexpr int BOTTOM_KEY_COUNT = 5; static constexpr KeyDef abcLayout[ABC_ROWS][COLS] = { - {{'0', ')'}, - {'1', '!'}, + {{'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, @@ -84,7 +91,8 @@ class KeyboardEntryActivity : public Activity { {'6', '^'}, {'7', '&'}, {'8', '*'}, - {'9', '('}}, + {'9', '('}, + {'0', ')'}}, {{'q', 'Q'}, {'w', 'W'}, {'e', 'E'}, @@ -117,9 +125,51 @@ class KeyboardEntryActivity : public Activity { {',', '<'}}, }; + static constexpr KeyDef urlLayout[ABC_ROWS][COLS] = { + {{'1', '!'}, + {'2', '@'}, + {'3', '#'}, + {'4', '$'}, + {'5', '%'}, + {'6', '^'}, + {'7', '&'}, + {'8', '*'}, + {'9', '('}, + {'0', ')'}}, + {{'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'}, + {{'1', '\0'}, {'2', '\0'}, {'3', '\0'}, {'4', '\0'}, @@ -127,9 +177,9 @@ class KeyboardEntryActivity : public Activity { {'6', '\0'}, {'7', '\0'}, {'8', '\0'}, - {'9', '\0'}}, - {{')', '\0'}, - {'!', '\0'}, + {'9', '\0'}, + {'0', '\0'}}, + {{'!', '\0'}, {'@', '\0'}, {'#', '\0'}, {'$', '\0'}, @@ -137,7 +187,8 @@ class KeyboardEntryActivity : public Activity { {'^', '\0'}, {'&', '\0'}, {'*', '\0'}, - {'(', '\0'}}, + {'(', '\0'}, + {')', '\0'}}, {{'-', '\0'}, {'_', '\0'}, {'=', '\0'}, diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 51d0212f..27d402ef 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -907,15 +907,18 @@ void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch if (isSelected) { if (inactiveSelection) { renderer.drawRect(rect.x, rect.y, rect.width, rect.height, 2, true); + } else if (keyType == KeyboardKeyType::Disabled) { + renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray); } else { renderer.fillRect(rect.x, rect.y, rect.width, rect.height, true); } } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || keyType == KeyboardKeyType::Del || - keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok) { + keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok || + keyType == KeyboardKeyType::Disabled) { renderer.drawRect(rect.x, rect.y, rect.width, rect.height); } - const bool invert = isSelected && !inactiveSelection; + const bool invert = isSelected && !inactiveSelection && keyType != KeyboardKeyType::Disabled; if (keyType == KeyboardKeyType::Space) { const int lineHalfWidth = rect.width * 3 / 10; diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index bb172499..b5c97031 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -65,11 +65,13 @@ struct ThemeMetrics { bool keyboardBottomAligned; bool keyboardCenteredText; int keyboardVerticalOffset; + int keyboardTextFieldWidthPercent; + int keyboardWidthPercent; }; enum UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot, Weather }; -enum class KeyboardKeyType { Normal, Shift, Mode, Reveal, Space, Del, Ok }; +enum class KeyboardKeyType { Normal, Shift, Mode, Reveal, Space, Del, Ok, Disabled }; // Default theme implementation (Classic Theme) // Additional themes can inherit from this and override methods as needed @@ -107,7 +109,9 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, .keyboardCenteredText = false, - .keyboardVerticalOffset = -13}; + .keyboardVerticalOffset = -13, + .keyboardTextFieldWidthPercent = 85, + .keyboardWidthPercent = 90}; } class BaseTheme { diff --git a/src/components/themes/lyra/Lyra3CoversTheme.h b/src/components/themes/lyra/Lyra3CoversTheme.h index b0f3d031..5d2bd974 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.h +++ b/src/components/themes/lyra/Lyra3CoversTheme.h @@ -40,7 +40,9 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, .keyboardCenteredText = false, - .keyboardVerticalOffset = -7}; + .keyboardVerticalOffset = -7, + .keyboardTextFieldWidthPercent = 85, + .keyboardWidthPercent = 90}; } class Lyra3CoversTheme : public LyraTheme { diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 73293497..49b8ac52 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -759,15 +759,18 @@ void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch if (isSelected) { if (inactiveSelection) { renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::LightGray); + } else if (keyType == KeyboardKeyType::Disabled) { + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::LightGray); } else { renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black); } } else if (keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode || keyType == KeyboardKeyType::Del || - keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok) { + keyType == KeyboardKeyType::Space || keyType == KeyboardKeyType::Ok || + keyType == KeyboardKeyType::Disabled) { renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cornerRadius, true); } - const bool invert = isSelected && !inactiveSelection; + const bool invert = isSelected && !inactiveSelection && keyType != KeyboardKeyType::Disabled; if (keyType == KeyboardKeyType::Space) { const int lineHalfWidth = rect.width * 3 / 10; diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index 4ad4b82d..31de32e2 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -38,7 +38,9 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, .keyboardCenteredText = false, - .keyboardVerticalOffset = -7}; + .keyboardVerticalOffset = -7, + .keyboardTextFieldWidthPercent = 85, + .keyboardWidthPercent = 90}; } class LyraTheme : public BaseTheme {