diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 0c3c7005..b366ff04 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -44,6 +44,18 @@ STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network" STR_OPEN_URL_HINT: "Open this URL in your browser" STR_OR_HTTP_PREFIX: "or http://" STR_SCAN_QR_HINT: "or scan QR code with your phone:" +STR_KEYBOARD_HELP_URL_LINE_1: "URL mode: Space toggles URL snippets." +STR_KEYBOARD_HELP_URL_LINE_2: "Use a 3×3 grid for protocols, hosts, and domains." +STR_KEYBOARD_HELP_URL_LINE_3: "Press ABC to exit URL mode." +STR_KEYBOARD_HELP_CURSOR_LINE_1: "Cursor mode: long-press Up, short Down to exit." +STR_KEYBOARD_HELP_CURSOR_LINE_2: "Left/Right moves the cursor; Confirm toggles [abc]/[***]." +STR_KEYBOARD_HELP_CURSOR_LINE_3: "Block cursor highlights the selected character." +STR_KEYBOARD_HELP_PASSWORD_LINE_1: "Password mode hides text with * and reveals one char." +STR_KEYBOARD_HELP_PASSWORD_LINE_2: "Use cursor mode past end to toggle [abc]/[***]." +STR_KEYBOARD_HELP_PASSWORD_LINE_3: "Long-press Del to clear all text." +STR_KEYBOARD_HELP_DEFAULT_LINE_1: "Long-press Up for cursor mode; Left/Right moves cursor." +STR_KEYBOARD_HELP_DEFAULT_LINE_2: "Hold Confirm for alternate character input in ABC mode." +STR_KEYBOARD_HELP_DEFAULT_LINE_3: "Shift toggles case; Space inserts a normal space." STR_RSSI: "RSSI" STR_NO_SIGNAL: "No signal" STR_SIGNAL_QUALITY_POOR: "Poor" diff --git a/src/activities/home/GlobalBookmarksActivity.cpp b/src/activities/home/GlobalBookmarksActivity.cpp index d40482a9..09912068 100644 --- a/src/activities/home/GlobalBookmarksActivity.cpp +++ b/src/activities/home/GlobalBookmarksActivity.cpp @@ -183,7 +183,7 @@ void GlobalBookmarksActivity::renameSelected() { entry.bookmarks[bookmarkIndex].name.empty() ? getRowTitle(selectorIndex) : entry.bookmarks[bookmarkIndex].name; startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_RENAME), initial, - BookmarkStore::MAX_NAME_LENGTH, false), + BookmarkStore::MAX_NAME_LENGTH, InputType::Text), [this, bookIndex, bookmarkIndex](const ActivityResult& result) { if (!result.isCancelled) { const auto& kr = std::get(result.data); diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index d4f15315..66eed43f 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -220,20 +220,18 @@ void WifiSelectionActivity::selectNetwork(const int index) { // Show password entry state = WifiSelectionState::PASSWORD_ENTRY; // Don't allow screen updates while changing activity - startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), - "", // No initial text - 64, // Max password length - false // Show password by default (hard keyboard to use) - ), - [this](const ActivityResult& result) { - if (result.isCancelled) { - state = WifiSelectionState::NETWORK_LIST; - } else { - enteredPassword = std::get(result.data).text; - // state will be updated in next loop iteration - } - }); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), + "", // No initial text + 64, // Max password length + InputType::Password), + [this](const ActivityResult& result) { + if (result.isCancelled) { + state = WifiSelectionState::NETWORK_LIST; + } else { + enteredPassword = std::get(result.data).text; + // state will be updated in next loop iteration + } + }); } else { // Connect directly for open networks attemptConnection(); diff --git a/src/activities/reader/StarredPagesActivity.cpp b/src/activities/reader/StarredPagesActivity.cpp index 2a9c6123..6735a15b 100644 --- a/src/activities/reader/StarredPagesActivity.cpp +++ b/src/activities/reader/StarredPagesActivity.cpp @@ -49,7 +49,7 @@ void StarredPagesActivity::startRename() { const std::string initial = all[renamingIndex].name.empty() ? getDefaultLabel(renamingIndex) : all[renamingIndex].name; startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_RENAME), initial, - BookmarkStore::MAX_NAME_LENGTH, false), + BookmarkStore::MAX_NAME_LENGTH, InputType::Text), [this, renamingIndex](const ActivityResult& result) { if (!result.isCancelled) { const auto& kr = std::get(result.data); diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index ec635861..24fe2b83 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -52,7 +52,7 @@ void CalibreSettingsActivity::handleSelection() { if (selectedIndex == 0) { // OPDS Server URL startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), - SETTINGS.opdsServerUrl, 127, false), + SETTINGS.opdsServerUrl, 127, InputType::Url), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -64,7 +64,7 @@ void CalibreSettingsActivity::handleSelection() { } else if (selectedIndex == 1) { // Username startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_USERNAME), - SETTINGS.opdsUsername, 63, false), + SETTINGS.opdsUsername, 63, InputType::Text), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -76,7 +76,7 @@ void CalibreSettingsActivity::handleSelection() { } else if (selectedIndex == 2) { // Password startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_PASSWORD), - SETTINGS.opdsPassword, 63, false), + SETTINGS.opdsPassword, 63, InputType::Password), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index de554e32..49f8ab61 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -63,7 +63,7 @@ void KOReaderSettingsActivity::onActionSelected(int index) { if (item.nameId == StrId::STR_USERNAME) { startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_USERNAME), - KOREADER_STORE.getUsername(), 64, false), + KOREADER_STORE.getUsername(), 64, InputType::Text), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -72,28 +72,30 @@ void KOReaderSettingsActivity::onActionSelected(int index) { } }); } else if (item.nameId == StrId::STR_PASSWORD) { - startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), - KOREADER_STORE.getPassword(), 64, true), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& kb = std::get(result.data); - KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), kb.text); - KOREADER_STORE.saveToFile(); - } - }); - } else if (item.nameId == StrId::STR_SYNC_SERVER_URL) { - const std::string currentUrl = KOREADER_STORE.getServerUrl(); - const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 128, false), + std::make_unique(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), + KOREADER_STORE.getPassword(), 64, InputType::Password), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); - const std::string urlToSave = (kb.text == "https://" || kb.text == "http://") ? "" : kb.text; - KOREADER_STORE.setServerUrl(urlToSave); + KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), kb.text); KOREADER_STORE.saveToFile(); } }); + } else if (item.nameId == StrId::STR_SYNC_SERVER_URL) { + const std::string currentUrl = KOREADER_STORE.getServerUrl(); + const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), + prefillUrl, 128, InputType::Url), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + const std::string urlToSave = + (kb.text == "https://" || kb.text == "http://") ? "" : kb.text; + KOREADER_STORE.setServerUrl(urlToSave); + KOREADER_STORE.saveToFile(); + } + }); } else if (item.nameId == StrId::STR_AUTHENTICATE) { if (!KOREADER_STORE.hasCredentials()) return; startActivityForResult( diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 62613ff4..653d98e2 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -2,32 +2,49 @@ #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; + passwordVisible = false; + shiftState = 0; + selectedRow = 0; + selectedCol = 0; + confirmHeld = false; + confirmLongHandled = false; + upHeld = false; + upLongHandled = false; + downHeld = false; + downLongHandled = false; requestUpdate(); } void KeyboardEntryActivity::onExit() { Activity::onExit(); } -int KeyboardEntryActivity::getContentRowCount() const { return ABC_ROWS; } +int KeyboardEntryActivity::getContentRowCount() const { + if (urlMode) return 3; + return ABC_ROWS; +} + +int KeyboardEntryActivity::getContentColCount() const { + if (urlMode) return 3; + return COLS; +} 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 KeyDef(*layout)[COLS] = symMode ? symLayout : abcLayout; @@ -35,8 +52,7 @@ char KeyboardEntryActivity::getSelectedChar() const { if (selectedCol < 0 || selectedCol >= COLS) return '\0'; const KeyDef& key = layout[selectedRow][selectedCol]; - const bool useSecondary = !symMode && shiftState > 0 && key.secondary != '\0'; - return useSecondary ? key.secondary : key.primary; + return (shiftState > 0 && key.secondary != '\0') ? key.secondary : key.primary; } char KeyboardEntryActivity::getAlternativeChar() const { @@ -57,72 +73,64 @@ char KeyboardEntryActivity::getAlternativeChar() const { bool KeyboardEntryActivity::insertChar(char c) { if (c == '\0') return true; if (maxLength != 0 && text.length() >= maxLength) return true; + if (cursorPos > text.length()) cursorPos = text.length(); - text += c; + text.insert(cursorPos, 1, c); + cursorPos++; return true; } -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; - } - } +void KeyboardEntryActivity::insertString(const std::string& str) { + if (str.empty()) return; + if (maxLength != 0 && text.length() + str.length() > maxLength) return; + if (cursorPos > text.length()) cursorPos = text.length(); - switch (index) { - case 0: - return SpecShift; - case 1: - return SpecMode; - case 2: - return SpecSpace; - case 3: - return SpecDel; - case 4: - default: - return SpecOk; - } + text.insert(cursorPos, str); + cursorPos += str.length(); } bool KeyboardEntryActivity::handleKeyPress() { if (isBottomRow(selectedRow)) { - switch (getBottomSpecialKey(selectedCol)) { + switch (static_cast(selectedCol)) { case SpecShift: + if (urlMode) return true; if (symMode) return true; shiftState = (shiftState + 1) % 2; return true; case SpecMode: { + if (urlMode) { + urlMode = false; + symMode = false; + requestUpdate(); + return true; + } symMode = !symMode; int maxRow = getTotalRowCount() - 1; if (selectedRow > maxRow) selectedRow = maxRow; if (isBottomRow(selectedRow)) { - int bottomCount = getBottomKeyCount(); - if (selectedCol >= bottomCount) selectedCol = bottomCount - 1; + if (selectedCol >= BOTTOM_KEY_COUNT) selectedCol = BOTTOM_KEY_COUNT - 1; } else { - if (selectedCol >= COLS) selectedCol = COLS - 1; + if (selectedCol >= getContentColCount()) selectedCol = getContentColCount() - 1; } return true; } - case SpecReveal: - passwordVisible = !passwordVisible; - return true; case SpecSpace: - return insertChar(' '); + if (inputType == InputType::Url) { + urlMode = !urlMode; + if (urlMode) { + symMode = false; + } + selectedRow = getTotalRowCount() - 1; + selectedCol = SpecSpace; + requestUpdate(); + } else { + return insertChar(' '); + } + return true; case SpecDel: - if (!text.empty()) { - text.pop_back(); + if (cursorPos > 0 && !text.empty()) { + text.erase(cursorPos - 1, 1); + cursorPos--; } return true; case SpecOk: @@ -133,46 +141,118 @@ bool KeyboardEntryActivity::handleKeyPress() { } } + if (urlMode) { + const int idx = selectedCol + selectedRow * 3; + if (idx < URL_SNIPPET_COUNT) { + insertString(urlSnippets[idx]); + } + return true; + } + return insertChar(getSelectedChar()); } +void KeyboardEntryActivity::mapColContentBottom(int& col, bool goingUp) const { + if (urlMode) { + col = goingUp ? col - 1 : col + 1; + if (col < 0) col = 0; + if (col >= 3) col = 2; + } else { + col = goingUp ? col * 2 : col / 2; + } +} + 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(); - }); + if (!cursorMode && mappedInput.wasPressed(MappedInputManager::Button::Up)) { + upHeld = true; + upLongHandled = false; + } - 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; + if (upHeld && !upLongHandled && mappedInput.isPressed(MappedInputManager::Button::Up) && + mappedInput.getHeldTime() > LONG_PRESS_MS) { + cursorMode = true; + upLongHandled = true; requestUpdate(); - }); + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { + if (upHeld && !upLongHandled && !cursorMode) { + bool wasBottom = isBottomRow(selectedRow); + const int contentCols = getContentColCount(); + selectedRow = ButtonNavigator::previousIndex(selectedRow, totalRows); + if (wasBottom && !isBottomRow(selectedRow)) { + mapColContentBottom(selectedCol, true); + } else if (!wasBottom && isBottomRow(selectedRow)) { + mapColContentBottom(selectedCol, false); + } + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : contentCols - 1; + if (selectedCol > maxCol) selectedCol = maxCol; + requestUpdate(); + } + upHeld = false; + upLongHandled = false; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { + downHeld = true; + if (cursorMode) { + if (cursorPos > text.length()) { + cursorPos = text.length(); + } + passwordVisible = false; + cursorMode = false; + downLongHandled = true; + requestUpdate(); + } else { + downLongHandled = false; + } + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { + if (downHeld && !downLongHandled && !cursorMode) { + bool wasBottom = isBottomRow(selectedRow); + const int contentCols = getContentColCount(); + selectedRow = ButtonNavigator::nextIndex(selectedRow, totalRows); + if (wasBottom && !isBottomRow(selectedRow)) { + mapColContentBottom(selectedCol, true); + } else if (!wasBottom && isBottomRow(selectedRow)) { + mapColContentBottom(selectedCol, false); + } + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : contentCols - 1; + if (selectedCol > maxCol) selectedCol = maxCol; + requestUpdate(); + } + downHeld = false; + downLongHandled = false; + } buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { - int maxCol = isBottomRow(selectedRow) ? getBottomKeyCount() - 1 : COLS - 1; + if (cursorMode) { + if (cursorPos > 0) { + cursorPos--; + requestUpdate(); + } + return; + } + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); requestUpdate(); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { - int maxCol = isBottomRow(selectedRow) ? getBottomKeyCount() - 1 : COLS - 1; + if (cursorMode) { + if (cursorPos < text.length()) { + cursorPos++; + requestUpdate(); + } else if (cursorPos == text.length() && inputType == InputType::Password) { + cursorPos = text.length() + 1; + requestUpdate(); + } + return; + } + int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 1 : getContentColCount() - 1; selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); requestUpdate(); }); @@ -183,7 +263,15 @@ void KeyboardEntryActivity::loop() { } if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() > LONG_PRESS_MS) { + mappedInput.getHeldTime() > DEL_LONG_PRESS_MS && isBottomRow(selectedRow) && selectedCol == SpecDel) { + text.clear(); + cursorPos = 0; + confirmLongHandled = true; + requestUpdate(); + } + + if (confirmHeld && !confirmLongHandled && mappedInput.isPressed(MappedInputManager::Button::Confirm) && + mappedInput.getHeldTime() > LONG_PRESS_MS && !cursorMode && inputType == InputType::Text && !urlMode) { char alt = getAlternativeChar(); if (alt != '\0') { insertChar(alt); @@ -193,10 +281,14 @@ void KeyboardEntryActivity::loop() { } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (confirmHeld && !confirmLongHandled) { + if (confirmHeld && !confirmLongHandled && !cursorMode) { if (handleKeyPress()) { requestUpdate(); } + } else if (confirmHeld && !confirmLongHandled && cursorMode && inputType == InputType::Password && + cursorPos > text.length()) { + passwordVisible = !passwordVisible; + requestUpdate(); } confirmHeld = false; confirmLongHandled = false; @@ -210,41 +302,158 @@ 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); + const int pageWidth = contentRect.width; + const int contentX = contentRect.x; + const int contentY = contentRect.y; + const auto& metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight}, + GUI.drawHeader(renderer, Rect{contentX, contentY + metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int inputStartY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + + const int inputStartY = contentY + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4 + metrics.keyboardVerticalOffset; - int inputHeight = 0; + int inputHeight = lineHeight; std::string displayText; - if (isPassword && !passwordVisible) { - displayText = std::string(text.length(), '*'); + if (inputType == InputType::Password && !passwordVisible) { + size_t revealPos; + if (cursorMode) { + revealPos = text.length(); // no reveal in displayText; block draws actual char directly + } else { + revealPos = (text.length() > 0 && cursorPos > 0) ? cursorPos - 1 : 0; + } + displayText = text; + for (size_t i = 0; i < displayText.length(); i++) { + if (i != revealPos) { + displayText[i] = '*'; + } + } } else { displayText = text; } - displayText += "_"; + 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 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 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; + if (cursorPos < text.length()) { + cursorCharWidth = renderer.getTextWidth(UI_12_FONT_ID, text.substr(cursorPos, 1).c_str()); + } else { + cursorCharWidth = 6; + } int lineStartIdx = 0; int lineEndIdx = displayText.length(); int textWidth = 0; + int cursorPixelX = contentX + metrics.contentSidePadding; + 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()); - if (textWidth <= contentRect.width - 2 * metrics.contentSidePadding) { - if (metrics.keyboardCenteredText) { - const int centeredX = contentRect.x + (contentRect.width - textWidth) / 2; - renderer.drawText(UI_12_FONT_ID, centeredX, inputStartY + inputHeight, lineText.c_str()); + if (textWidth <= maxLineWidth) { + const bool isLastLine = (lineEndIdx == static_cast(displayText.length())); + bool isCursorLine = false; + if (!cursorDrawn && cursorPos >= lineStartIdx && + (isLastLine ? cursorPos <= lineEndIdx : cursorPos < lineEndIdx)) { + std::string beforeCursor; + if (isPassword && !passwordVisible && cursorMode) { + beforeCursor = std::string(cursorPos - lineStartIdx, '*'); + } 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; + } + if (cursorPos == static_cast(lineEndIdx) && cursorPos == displayText.length()) { + cursorPixelX += 2; + } + cursorLineY = inputStartY + inputHeight; + cursorDrawn = true; + isCursorLine = true; + } + + const int lineStartX = + contentX + (centerText ? effectiveMargin + (maxLineWidth - textWidth) / 2 : effectiveMargin); + if (isCursorLine && cursorMode && isPassword && !passwordVisible) { + // 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 + const std::string part1 = displayText.substr(lineStartIdx, cursorPos - lineStartIdx); + renderer.drawText(UI_12_FONT_ID, lineStartX, inputStartY + inputHeight, part1.c_str()); + // Part 2: skip cursor slot (block + actual char drawn later) + // Part 3: chars after cursor position (skip char under cursor), starting at cursorPixelX + cursorCharWidth + const int afterStart = static_cast(cursorPos) + (cursorPos < text.length() ? 1 : 0); + const int afterEnd = lineEndIdx; + if (afterStart < afterEnd) { + const std::string part3 = displayText.substr(afterStart, afterEnd - afterStart); + renderer.drawText(UI_12_FONT_ID, cursorPixelX + cursorCharWidth, inputStartY + inputHeight, part3.c_str()); + } } else { - renderer.drawText(UI_12_FONT_ID, contentRect.x + metrics.contentSidePadding, inputStartY + inputHeight, - lineText.c_str()); + renderer.drawText(UI_12_FONT_ID, lineStartX, inputStartY + inputHeight, lineText.c_str()); } if (lineEndIdx == displayText.length()) { break; @@ -258,75 +467,159 @@ void KeyboardEntryActivity::render(RenderLock&&) { } } - GUI.drawTextField(renderer, Rect{contentRect.x, inputStartY, contentRect.width, inputHeight}, textWidth); + const int fieldWidth = (inputHeight > 0) ? maxLineWidth : textWidth; + const int lineMargin = margin + extraMargin - 5; + GUI.drawTextField(renderer, Rect{contentX, inputStartY, pageWidth, inputHeight}, fieldWidth, cursorMode, lineMargin, + pageWidth - 2 * lineMargin); - 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; + if (cursorMode && 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()) { + static constexpr int serifW = 3; + const int cX = cursorPixelX; + const int cY = cursorLineY; + const int cBottom = cursorLineY + lineHeight - 1; + renderer.fillRect(cX, cY, 2, lineHeight, true); + renderer.drawLine(cX - serifW, cY, cX - 1, cY, 2, true); + renderer.drawLine(cX + 1, cY, cX + serifW, cY, 2, true); + renderer.drawLine(cX - serifW, cBottom, cX - 1, cBottom, 2, true); + renderer.drawLine(cX + 1, cBottom, cX + serifW, cBottom, 2, true); + } + + if (isPassword) { + const char* toggleLabel = passwordVisible ? "[***]" : "[abc]"; + 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(); + + if (toggleSelected) { + renderer.fillRect(toggleX - 2, toggleY, toggleWidth + 5, lineHeight + 3, true); + renderer.drawText(UI_12_FONT_ID, toggleX, toggleY, toggleLabel, false); + } else { + renderer.drawText(UI_12_FONT_ID, toggleX, toggleY, toggleLabel, true); + } + } + + const int bkSpacing = metrics.keyboardBottomKeySpacing; + const int contentTotalWidth = + COLS * ((pageWidth * 95 / 100 - (COLS - 1) * keySpacing) / COLS) + (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; + + int urlLeftMargin = leftMargin; + if (urlMode) { + const int urlTotalWidth = 3 * keyWidth + 2 * keySpacing; + const int urlCenterX = bottomLeftMargin + SpecSpace * (bottomKeyWidth + bkSpacing) + bottomKeyWidth / 2; + urlLeftMargin = urlCenterX - urlTotalWidth / 2; + } - 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(); for (int row = 0; row < contentRows; row++) { const int rowY = keyboardStartY + row * (keyHeight + keySpacing); + const int rowLeftMargin = urlMode ? urlLeftMargin : leftMargin; - for (int col = 0; col < COLS; col++) { - const KeyDef& key = layout[row][col]; - const int keyX = leftMargin + col * (keyWidth + keySpacing); + for (int col = 0; col < contentCols; col++) { + const int keyX = rowLeftMargin + col * (keyWidth + keySpacing); const bool isSelected = row == selectedRow && col == selectedCol; + const bool activeKeySelected = isSelected && !cursorMode; - char primaryChar = key.primary; - char secondaryChar = key.secondary; + if (urlMode) { + const int snippetIdx = col + row * 3; + if (snippetIdx < URL_SNIPPET_COUNT) { + GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, urlSnippets[snippetIdx], + activeKeySelected, nullptr); + } + } else { + const KeyDef& key = layout[row][col]; - if (!symMode && shiftState > 0 && key.secondary != '\0') { - primaryChar = key.secondary; - secondaryChar = key.primary; + char primaryChar = key.primary; + char secondaryChar = key.secondary; + + 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, activeKeySelected, + showSecondary ? secondaryBuf : nullptr); } - - 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); } } - 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; }; + const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = { + {KeyboardKeyType::Shift, (symMode || urlMode) ? shiftLabel(0) : shiftLabel(shiftState)}, + {KeyboardKeyType::Mode, urlMode ? "abc" : (symMode ? "abc" : "#@!")}, + {inputType == InputType::Url ? KeyboardKeyType::Mode : KeyboardKeyType::Space, + inputType == InputType::Url ? "URL" : nullptr}, + {KeyboardKeyType::Del, nullptr}, + {KeyboardKeyType::Ok, tr(STR_OK_BUTTON)}, + }; - 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); + for (int i = 0; i < BOTTOM_KEY_COUNT; i++) { + const int keyX = bottomLeftMargin + 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 bool activeKeySelected = isSelected && !cursorMode; + GUI.drawKeyboardKey(renderer, Rect{keyX, bottomRowY, bottomKeyWidth, bottomKeyHeight}, bottomKeys[i].label, + activeKeySelected, nullptr, bottomKeys[i].themeType); + } + + if (cursorMode) { + int selKeyX, selKeyY, selKeyW, selKeyH; + if (isBottomRow(selectedRow)) { + selKeyX = bottomLeftMargin + selectedCol * (bottomKeyWidth + bkSpacing); + selKeyY = bottomRowY; + selKeyW = bottomKeyWidth; + selKeyH = bottomKeyHeight; + } else { + const int rowLM = urlMode ? urlLeftMargin : leftMargin; + selKeyX = rowLM + selectedCol * (keyWidth + keySpacing); + selKeyY = keyboardStartY + selectedRow * (keyHeight + keySpacing); + selKeyW = keyWidth; + selKeyH = keyHeight; + } + if (isBottomRow(selectedRow)) { + GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, bottomKeys[selectedCol].label, true, + nullptr, bottomKeys[selectedCol].themeType, true); + } else if (urlMode) { + const int idx = selectedCol + selectedRow * 3; + if (idx < URL_SNIPPET_COUNT) { + GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, urlSnippets[idx], true, nullptr, + KeyboardKeyType::Normal, true); + } + } else { + const KeyDef& selKey = layout[selectedRow][selectedCol]; + char selPrimary = selKey.primary; + char selSecondary = selKey.secondary; + if (!symMode && shiftState > 0 && selKey.secondary != '\0') { + selPrimary = selKey.secondary; + selSecondary = selKey.primary; + } + const char selPrimaryBuf[2] = {selPrimary, '\0'}; + const char selSecondaryBuf[2] = {selSecondary, '\0'}; + const bool selShowSecondary = !symMode && selectedRow == 0 && selSecondary != '\0'; + GUI.drawKeyboardKey(renderer, Rect{selKeyX, selKeyY, selKeyW, selKeyH}, selPrimaryBuf, true, + selShowSecondary ? selSecondaryBuf : nullptr, KeyboardKeyType::Normal, true); + } } const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index c5e29765..f135429d 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -14,18 +14,20 @@ struct KeyDef { char secondary; }; -enum SpecialKeyType { SpecShift, SpecMode, SpecReveal, SpecSpace, SpecDel, SpecOk }; +enum SpecialKeyType { SpecShift, SpecMode, SpecSpace, SpecDel, SpecOk }; + +enum class InputType { Text, Password, Url }; class KeyboardEntryActivity : public Activity { public: explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string title = "Enter Text", std::string initialText = "", - const size_t maxLength = 0, const bool isPassword = false) + const size_t maxLength = 0, InputType inputType = InputType::Text) : Activity("KeyboardEntry", renderer, mappedInput), title(std::move(title)), text(std::move(initialText)), maxLength(maxLength), - isPassword(isPassword) {} + inputType(inputType) {} void onEnter() override; void onExit() override; @@ -36,7 +38,7 @@ class KeyboardEntryActivity : public Activity { std::string title; std::string text; size_t maxLength; - bool isPassword; + InputType inputType; bool passwordVisible = false; ButtonNavigator buttonNavigator; @@ -48,14 +50,29 @@ class KeyboardEntryActivity : public Activity { bool confirmHeld = false; bool confirmLongHandled = false; + bool cursorMode = false; + size_t cursorPos = 0; + bool upHeld = false; + bool upLongHandled = false; + bool downHeld = false; + bool downLongHandled = false; + + 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"}; + void onComplete(std::string text); void onCancel(); static constexpr uint16_t LONG_PRESS_MS = 500; + static constexpr uint16_t DEL_LONG_PRESS_MS = 1500; static constexpr int COLS = 10; static constexpr int ABC_ROWS = 4; static constexpr int SYM_ROWS = 4; + static constexpr int BOTTOM_KEY_COUNT = 5; static constexpr KeyDef abcLayout[ABC_ROWS][COLS] = { {{'0', ')'}, @@ -143,15 +160,16 @@ class KeyboardEntryActivity : public Activity { {'`', '\0'}}, }; - const char* getShiftLabel() const; + static const char* const shiftString[2]; int getContentRowCount() const; + int getContentColCount() const; int getTotalRowCount() const; - int getBottomKeyCount() const; - SpecialKeyType getBottomSpecialKey(int index) const; bool isBottomRow(int row) const; char getSelectedChar() const; char getAlternativeChar() const; bool handleKeyPress(); bool insertChar(char c); + void insertString(const std::string& str); + void mapColContentBottom(int& col, bool goingUp) const; }; diff --git a/src/activities/weather/WeatherSettingsActivity.cpp b/src/activities/weather/WeatherSettingsActivity.cpp index 7550d969..8f3fdbd4 100644 --- a/src/activities/weather/WeatherSettingsActivity.cpp +++ b/src/activities/weather/WeatherSettingsActivity.cpp @@ -114,71 +114,71 @@ void WeatherSettingsActivity::handleSelection() { } void WeatherSettingsActivity::launchCitySearch() { - startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_WEATHER_SEARCH_CITY), "", 64, false), - [this](const ActivityResult& result) { - if (result.isCancelled) return; + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_WEATHER_SEARCH_CITY), "", + 64, InputType::Text), + [this](const ActivityResult& result) { + if (result.isCancelled) return; - const auto& kb = std::get(result.data); - if (kb.text.empty()) return; + const auto& kb = std::get(result.data); + if (kb.text.empty()) return; - // Need WiFi for geocoding - if (WiFi.status() != WL_CONNECTED || WiFi.localIP() == IPAddress(0, 0, 0, 0)) { - startActivityForResult(std::make_unique(renderer, mappedInput), - [this, query = kb.text](const ActivityResult& wifiResult) { - if (wifiResult.isCancelled) return; - searchResults = WeatherClient::searchCity(query); - showingSearchResults = !searchResults.empty(); - selectedIndex = 0; - requestUpdate(); - }); - } else { - searchResults = WeatherClient::searchCity(kb.text); - showingSearchResults = !searchResults.empty(); - selectedIndex = 0; - requestUpdate(); - } - }); + // Need WiFi for geocoding + if (WiFi.status() != WL_CONNECTED || WiFi.localIP() == IPAddress(0, 0, 0, 0)) { + startActivityForResult(std::make_unique(renderer, mappedInput), + [this, query = kb.text](const ActivityResult& wifiResult) { + if (wifiResult.isCancelled) return; + searchResults = WeatherClient::searchCity(query); + showingSearchResults = !searchResults.empty(); + selectedIndex = 0; + requestUpdate(); + }); + } else { + searchResults = WeatherClient::searchCity(kb.text); + showingSearchResults = !searchResults.empty(); + selectedIndex = 0; + requestUpdate(); + } + }); } void WeatherSettingsActivity::launchLatitudeEntry() { std::string current = std::to_string(WEATHER_SETTINGS.getLatitude()); - startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_WEATHER_LATITUDE), current, 12, false), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& kb = std::get(result.data); - char* end = nullptr; - const float lat = strtof(kb.text.c_str(), &end); - if (end != kb.text.c_str() && *end == '\0' && lat >= -90.0f && lat <= 90.0f) { - if (lat != WEATHER_SETTINGS.getLatitude()) { - WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), ""); - } - WEATHER_SETTINGS.saveToFile(); - requestUpdate(); - } - } - }); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_WEATHER_LATITUDE), + current, 12, InputType::Text), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + char* end = nullptr; + const float lat = strtof(kb.text.c_str(), &end); + if (end != kb.text.c_str() && *end == '\0' && lat >= -90.0f && lat <= 90.0f) { + if (lat != WEATHER_SETTINGS.getLatitude()) { + WEATHER_SETTINGS.setLocation(lat, WEATHER_SETTINGS.getLongitude(), ""); + } + WEATHER_SETTINGS.saveToFile(); + requestUpdate(); + } + } + }); } void WeatherSettingsActivity::launchLongitudeEntry() { std::string current = std::to_string(WEATHER_SETTINGS.getLongitude()); - startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_WEATHER_LONGITUDE), current, 12, false), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& kb = std::get(result.data); - char* end = nullptr; - const float lon = strtof(kb.text.c_str(), &end); - if (end != kb.text.c_str() && *end == '\0' && lon >= -180.0f && lon <= 180.0f) { - if (lon != WEATHER_SETTINGS.getLongitude()) { - WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, ""); - } - WEATHER_SETTINGS.saveToFile(); - requestUpdate(); - } - } - }); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_WEATHER_LONGITUDE), + current, 12, InputType::Text), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + char* end = nullptr; + const float lon = strtof(kb.text.c_str(), &end); + if (end != kb.text.c_str() && *end == '\0' && lon >= -180.0f && lon <= 180.0f) { + if (lon != WEATHER_SETTINGS.getLongitude()) { + WEATHER_SETTINGS.setLocation(WEATHER_SETTINGS.getLatitude(), lon, ""); + } + WEATHER_SETTINGS.saveToFile(); + requestUpdate(); + } + } + }); } void WeatherSettingsActivity::toggleTempUnit() { diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 4edfb5b5..51d0212f 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -878,40 +878,50 @@ void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* renderer.drawCenteredText(SMALL_FONT_ID, rect.y, truncatedLabel.c_str()); } -void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { +void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode, + int contentStartX, int contentWidth) const { (void)textWidth; + (void)contentStartX; + (void)contentWidth; const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); const int bracketHeight = lineHeight; - const int fieldLeft = rect.x + 10; + const int fieldLeft = rect.x + 15; const int fieldRight = rect.x + rect.width - 15; - const int topY = rect.y; - const int bottomY = rect.y + rect.height + lineHeight; + const int topY = rect.y - 7; + const int bottomY = rect.y + rect.height + lineHeight + 7; const int tickLen = bracketHeight / 2; + const int thickness = cursorMode ? 3 : 1; - renderer.drawLine(fieldLeft, topY, fieldLeft, bottomY); - renderer.drawLine(fieldLeft, topY, fieldLeft + tickLen, topY); - renderer.drawLine(fieldLeft, bottomY, fieldLeft + tickLen, bottomY); + renderer.fillRect(fieldLeft, topY, thickness, bottomY - topY + 1, true); + renderer.drawLine(fieldLeft, topY, fieldLeft + tickLen, topY, thickness, true); + renderer.drawLine(fieldLeft, bottomY, fieldLeft + tickLen, bottomY, thickness, true); - renderer.drawLine(fieldRight, topY, fieldRight, bottomY); - renderer.drawLine(fieldRight, topY, fieldRight - tickLen, topY); - renderer.drawLine(fieldRight, bottomY, fieldRight - tickLen, bottomY); + renderer.fillRect(fieldRight - thickness + 1, topY, thickness, bottomY - topY + 1, true); + renderer.drawLine(fieldRight, topY, fieldRight - tickLen, topY, thickness, true); + renderer.drawLine(fieldRight, bottomY, fieldRight - tickLen, bottomY, thickness, true); } void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, - const char* secondaryLabel, const KeyboardKeyType keyType) const { + const char* secondaryLabel, const KeyboardKeyType keyType, + const bool inactiveSelection) const { if (isSelected) { - 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) { + if (inactiveSelection) { + renderer.drawRect(rect.x, rect.y, rect.width, rect.height, 2, true); + } 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) { renderer.drawRect(rect.x, rect.y, rect.width, rect.height); } + const bool invert = isSelected && !inactiveSelection; + 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); + renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert); return; } @@ -920,27 +930,24 @@ void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch 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, centerY, 3, !invert); renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, - !isSelected); + !invert); renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, - !isSelected); + !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 itemWidth = renderer.getTextWidth(fontId, label); + const int itemWidth = renderer.getTextWidth(UI_12_FONT_ID, 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); + const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2 + primaryOffset; 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(SMALL_FONT_ID, rect.x + rect.width - secWidth - 1, rect.y, secondaryLabel, !invert); } + + renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert); } diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 920912a7..bb172499 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -60,6 +60,7 @@ struct ThemeMetrics { int keyboardKeyWidth; int keyboardKeyHeight; int keyboardKeySpacing; + int keyboardBottomKeyHeight; int keyboardBottomKeySpacing; bool keyboardBottomAligned; bool keyboardCenteredText; @@ -100,12 +101,13 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .statusBarHorizontalMargin = 5, .statusBarVerticalMargin = 19, .keyboardKeyWidth = 22, - .keyboardKeyHeight = 30, - .keyboardKeySpacing = 10, - .keyboardBottomKeySpacing = 0, + .keyboardKeyHeight = 40, + .keyboardKeySpacing = 0, + .keyboardBottomKeyHeight = 35, + .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, .keyboardCenteredText = false, - .keyboardVerticalOffset = -10}; + .keyboardVerticalOffset = -13}; } class BaseTheme { @@ -147,10 +149,11 @@ class BaseTheme { const int pageCount, std::string title, const int paddingBottom = 0, const int textYOffset = 0, const bool isStarred = false) 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 drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, + int contentStartX = 0, int contentWidth = 0) const; virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, - const char* secondaryLabel = nullptr, - KeyboardKeyType keyType = KeyboardKeyType::Normal) const; + const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal, + bool inactiveSelection = false) 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 3c17ec86..b0f3d031 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.h +++ b/src/components/themes/lyra/Lyra3CoversTheme.h @@ -34,12 +34,13 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .statusBarHorizontalMargin = 5, .statusBarVerticalMargin = 19, .keyboardKeyWidth = 31, - .keyboardKeyHeight = 50, + .keyboardKeyHeight = 40, .keyboardKeySpacing = 0, + .keyboardBottomKeyHeight = 35, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, - .keyboardCenteredText = true, - .keyboardVerticalOffset = 0}; + .keyboardCenteredText = false, + .keyboardVerticalOffset = -7}; } class Lyra3CoversTheme : public LyraTheme { diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 36fd800b..d51b633e 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -790,23 +790,34 @@ void LyraTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou renderer.displayBuffer(HalDisplay::FAST_REFRESH); } -void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { +void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode, + int contentStartX, int contentWidth) const { int lineY = rect.y + rect.height + renderer.getLineHeight(UI_12_FONT_ID) + LyraMetrics::values.verticalSpacing; - int lineW = textWidth + hPaddingInSelection * 2; - renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3); + const int thickness = cursorMode ? 3 : 2; + if (contentWidth > 0) { + renderer.drawLine(rect.x + contentStartX, lineY, rect.x + contentStartX + contentWidth, lineY, thickness, true); + } else { + int lineW = textWidth + hPaddingInSelection * 2; + renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, thickness, + true); + } } void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, - const char* secondaryLabel, const KeyboardKeyType keyType) const { + const char* secondaryLabel, const KeyboardKeyType keyType, + const bool inactiveSelection) 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 || + if (inactiveSelection) { + 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) { renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cornerRadius, true); } - const bool invert = isSelected; + const bool invert = isSelected && !inactiveSelection; 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 08f0ebaa..4ad4b82d 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -34,10 +34,11 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .keyboardKeyWidth = 31, .keyboardKeyHeight = 40, .keyboardKeySpacing = 0, + .keyboardBottomKeyHeight = 35, .keyboardBottomKeySpacing = 5, .keyboardBottomAligned = true, - .keyboardCenteredText = true, - .keyboardVerticalOffset = -12}; + .keyboardCenteredText = false, + .keyboardVerticalOffset = -7}; } class LyraTheme : public BaseTheme { @@ -68,10 +69,11 @@ class LyraTheme : public BaseTheme { void drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const; 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 drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, + int contentStartX = 0, int contentWidth = 0) 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; + const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal, + bool inactiveSelection = false) const override; bool showsFileIcons() const override { return true; } protected: