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