refactor: redesign on-screen keyboard with 10-col layout, symbol mode, long-press, visual improvements
This commit is contained in:
@@ -6,184 +6,154 @@
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
// Keyboard layouts - lowercase
|
||||
const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = {
|
||||
"`1234567890-=", "qwertyuiop[]\\", "asdfghjkl;'", "zxcvbnm,./",
|
||||
"^ _____<OK" // ^ = shift, _ = space, < = backspace, OK = done
|
||||
};
|
||||
|
||||
// Keyboard layouts - uppercase/symbols
|
||||
const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"",
|
||||
"ZXCVBNM<>?", "SPECIAL ROW"};
|
||||
|
||||
// Shift state strings
|
||||
const char* const KeyboardEntryActivity::shiftString[3] = {"shift", "SHIFT", "LOCK"};
|
||||
const char* const KeyboardEntryActivity::shiftString[2] = {"shift", "SHIFT"};
|
||||
|
||||
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 symMode ? SYM_ROWS : 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; }
|
||||
|
||||
bool KeyboardEntryActivity::isBottomRow(const int row) const { return row == getContentRowCount(); }
|
||||
|
||||
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];
|
||||
return (shiftState > 0 && key.secondary != '\0') ? 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);
|
||||
bool KeyboardEntryActivity::handleKeyPress() {
|
||||
if (isBottomRow(selectedRow)) {
|
||||
switch (static_cast<SpecialKeyType>(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)) {
|
||||
if (selectedCol >= BOTTOM_KEY_COUNT) selectedCol = BOTTOM_KEY_COUNT - 1;
|
||||
} else {
|
||||
if (selectedCol >= COLS) selectedCol = COLS - 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case SpecSpace:
|
||||
return insertChar(' ');
|
||||
case SpecDel:
|
||||
if (!text.empty()) {
|
||||
text.pop_back();
|
||||
}
|
||||
return true;
|
||||
case SpecOk:
|
||||
onComplete(text);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const int maxCol = getRowLength(selectedRow) - 1;
|
||||
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 * 2;
|
||||
} else if (!wasBottom && isBottomRow(selectedRow)) {
|
||||
selectedCol = selectedCol / 2;
|
||||
}
|
||||
int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 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 * 2;
|
||||
} else if (!wasBottom && isBottomRow(selectedRow)) {
|
||||
selectedCol = selectedCol / 2;
|
||||
}
|
||||
int maxCol = isBottomRow(selectedRow) ? BOTTOM_KEY_COUNT - 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) ? BOTTOM_KEY_COUNT - 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) ? BOTTOM_KEY_COUNT - 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();
|
||||
}
|
||||
// If handleKeyPress returns false, it means onComplete was triggered, no update needed
|
||||
confirmLongHandled = true;
|
||||
}
|
||||
|
||||
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,16 +162,17 @@ 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;
|
||||
@@ -211,10 +182,8 @@ void KeyboardEntryActivity::render(RenderLock&&) {
|
||||
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 +212,71 @@ 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;
|
||||
}
|
||||
|
||||
char primaryBuf[2] = {primaryChar, '\0'};
|
||||
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 bottomRowY = keyboardStartY + contentRows * (keyHeight + keySpacing);
|
||||
const int bottomKeyWidth = 2 * keyWidth + keySpacing;
|
||||
const int bottomSelectedRow = isBottomRow(selectedRow);
|
||||
|
||||
struct BottomKeyInfo {
|
||||
SpecialKeyType type;
|
||||
KeyboardKeyType themeType;
|
||||
const char* label;
|
||||
};
|
||||
const BottomKeyInfo bottomKeys[BOTTOM_KEY_COUNT] = {
|
||||
{SpecShift, KeyboardKeyType::Shift, symMode ? shiftString[0] : shiftString[shiftState]},
|
||||
{SpecMode, KeyboardKeyType::Mode, symMode ? "abc" : "#@!"},
|
||||
{SpecSpace, KeyboardKeyType::Space, nullptr},
|
||||
{SpecDel, KeyboardKeyType::Del, nullptr},
|
||||
{SpecOk, KeyboardKeyType::Ok, tr(STR_OK_BUTTON)},
|
||||
};
|
||||
|
||||
for (int i = 0; i < BOTTOM_KEY_COUNT; i++) {
|
||||
const int keyX = leftMargin + (2 * i) * (keyWidth + keySpacing);
|
||||
const bool isSelected = bottomSelectedRow && 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();
|
||||
|
||||
Reference in New Issue
Block a user