mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-07 09:12:44 +02:00
t5s3 touch mapping fix; ui fixed for repeateradminscreen; highlighting fixed for notes and discovery screen; t5s3 initial virtual keyboard implementation
This commit is contained in:
@@ -505,26 +505,33 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
|
||||
// T5S3 touch mapping — must be after ui_task declaration
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// Home screen tile grid — physical touch zones (960×540)
|
||||
#define TILE_Y_START 150
|
||||
#define TILE_Y_MID 300
|
||||
#define TILE_Y_END 460
|
||||
#define TILE_COL1 325
|
||||
#define TILE_COL2 640
|
||||
|
||||
// Map a single tap based on current screen context
|
||||
static char mapTouchTap(int16_t x, int16_t y) {
|
||||
// --- Status bar tap (top ~80px) → go home from any non-home screen ---
|
||||
if (y < 80 && !ui_task.isOnHomeScreen()) {
|
||||
// Convert physical (960×540) to virtual (128×128) coordinates
|
||||
int vx = (int)(x / 7.5f);
|
||||
int vy = (int)(y / 4.21875f);
|
||||
|
||||
// --- Status bar tap (top ~18 virtual units) → go home from any non-home screen ---
|
||||
if (vy < 18 && !ui_task.isOnHomeScreen()) {
|
||||
ui_task.gotoHomeScreen();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Home screen FIRST page: tile taps
|
||||
// Home screen FIRST page: tile taps (virtual coordinate hit test)
|
||||
if (ui_task.isOnHomeScreen() && ui_task.isHomeShowingTiles()) {
|
||||
if (y >= TILE_Y_START && y < TILE_Y_END) {
|
||||
int col = (x < TILE_COL1) ? 0 : (x < TILE_COL2) ? 1 : 2;
|
||||
int row = (y < TILE_Y_MID) ? 0 : 1;
|
||||
const int tileW = 40, tileH = 32, gapX = 1, gapY = 2;
|
||||
const int gridW = tileW * 3 + gapX * 2;
|
||||
const int gridX = (128 - gridW) / 2; // =3
|
||||
int gridY = ui_task.getTileGridVY();
|
||||
|
||||
// Check if tap is within the tile grid area
|
||||
if (vx >= gridX && vx < gridX + gridW &&
|
||||
vy >= gridY && vy < gridY + 2 * (tileH + gapY)) {
|
||||
int col = (vx - gridX) / (tileW + gapX);
|
||||
if (col > 2) col = 2;
|
||||
int row = (vy - gridY) / (tileH + gapY);
|
||||
if (row > 1) row = 1;
|
||||
|
||||
if (row == 0 && col == 0) { ui_task.gotoChannelScreen(); return 0; }
|
||||
if (row == 0 && col == 1) { ui_task.gotoContactsScreen(); return 0; }
|
||||
if (row == 0 && col == 2) { ui_task.gotoSettingsScreen(); return 0; }
|
||||
@@ -550,6 +557,15 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
return KEY_ENTER; // file list: open selected
|
||||
}
|
||||
|
||||
// Notes editing: tap → open keyboard for typing
|
||||
if (ui_task.isOnNotesScreen()) {
|
||||
NotesScreen* notes = (NotesScreen*)ui_task.getNotesScreen();
|
||||
if (notes && notes->isEditing()) {
|
||||
ui_task.showVirtualKeyboard(VKB_NOTES, "Edit Note", "", 137);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// All other screens: tap = select
|
||||
return KEY_ENTER;
|
||||
}
|
||||
@@ -600,9 +616,10 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
|
||||
// Map a long press to a key
|
||||
static char mapTouchLongPress(int16_t x, int16_t y) {
|
||||
// Home screen: long press cycles pages
|
||||
// Home screen: long press = activate current page action
|
||||
// (BLE toggle, send advert, hibernate, GPS toggle, etc.)
|
||||
if (ui_task.isOnHomeScreen()) {
|
||||
return (char)KEY_NEXT;
|
||||
return (char)KEY_ENTER;
|
||||
}
|
||||
|
||||
// Reader reading: long press = close book
|
||||
@@ -614,23 +631,63 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
|
||||
return KEY_ENTER; // file list: open
|
||||
}
|
||||
|
||||
// Channel screen: long press → compose to current channel
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
uint8_t chIdx = ui_task.getChannelScreenViewIdx();
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(chIdx, ch)) {
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "To: %s", ch.name);
|
||||
ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Contacts screen: long press → DM for chat contacts, admin for repeaters
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
if (cs) {
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
uint8_t ctype = cs->getSelectedContactType();
|
||||
if (idx >= 0 && ctype == ADV_TYPE_CHAT) {
|
||||
char dname[32];
|
||||
cs->getSelectedContactName(dname, sizeof(dname));
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "DM: %s", dname);
|
||||
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx);
|
||||
return 0;
|
||||
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return KEY_ENTER;
|
||||
}
|
||||
|
||||
// Discovery screen: long press = rescan
|
||||
if (ui_task.isOnDiscoveryScreen()) {
|
||||
return 'f';
|
||||
}
|
||||
|
||||
// Contacts screen: long press = open repeater admin (if on a repeater contact)
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
if (cs) {
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
ContactInfo ci;
|
||||
if (the_mesh.getContactByIdx(idx, ci) && ci.type == ADV_TYPE_REPEATER) {
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
// Repeater admin: long press → open keyboard for password or CLI
|
||||
if (ui_task.isOnRepeaterAdmin()) {
|
||||
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen();
|
||||
if (admin) {
|
||||
RepeaterAdminScreen::AdminState astate = admin->getState();
|
||||
if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) {
|
||||
ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return KEY_ENTER; // non-repeater: normal select
|
||||
}
|
||||
|
||||
// Notes screen: long press in editor → save and exit
|
||||
if (ui_task.isOnNotesScreen()) {
|
||||
NotesScreen* notes = (NotesScreen*)ui_task.getNotesScreen();
|
||||
if (notes && notes->isEditing()) {
|
||||
notes->triggerSaveAndExit();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: enter/select (settings toggle, etc.)
|
||||
@@ -1396,9 +1453,10 @@ void loop() {
|
||||
// Long press = finger held > 500ms without moving → edit/enter
|
||||
// After processing an event, cooldown waits for finger lift before next event.
|
||||
// Touch is disabled while lock screen is active.
|
||||
// When virtual keyboard is active, taps route to keyboard.
|
||||
// ---------------------------------------------------------------------------
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
if (!ui_task.isLocked())
|
||||
if (!ui_task.isLocked() && !ui_task.isVKBActive())
|
||||
{
|
||||
int16_t tx, ty;
|
||||
bool gotPoint = readTouchLandscape(&tx, &ty);
|
||||
@@ -1484,6 +1542,33 @@ void loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual keyboard touch routing (separate state machine, simple tap-only)
|
||||
// vkbNeedLift: true after VKB opens, requires finger lift before accepting taps.
|
||||
// This prevents the long press that opened VKB from registering as a keystroke.
|
||||
{
|
||||
static bool vkbNeedLift = true;
|
||||
static unsigned long vkbLastTap = 0;
|
||||
|
||||
if (ui_task.isVKBActive()) {
|
||||
int16_t tx, ty;
|
||||
bool gotPt = readTouchLandscape(&tx, &ty);
|
||||
|
||||
if (!gotPt) {
|
||||
vkbNeedLift = false; // Finger lifted — now taps are allowed
|
||||
} else if (!vkbNeedLift && (millis() - vkbLastTap >= 300)) {
|
||||
int vx = (int)(tx / 7.5f);
|
||||
int vy = (int)(ty / 4.21875f);
|
||||
if (ui_task.getVKB().handleTap(vx, vy)) {
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
vkbLastTap = millis();
|
||||
vkbNeedLift = true; // Require lift before next tap
|
||||
}
|
||||
} else {
|
||||
vkbNeedLift = true; // Reset for next VKB open
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Poll touch input for phone dialer numpad
|
||||
|
||||
@@ -639,6 +639,10 @@ public:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setCursor(0, footerY);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back");
|
||||
const char* copyHint = "Tap:Dismiss";
|
||||
#else
|
||||
display.print("Q:Back");
|
||||
// Show scroll hint if path is scrollable
|
||||
if (msg && (msg->path_len & 63) > _pathHopsVisible && msg->path_len != 0xFF) {
|
||||
@@ -648,6 +652,7 @@ public:
|
||||
display.print(scrollHint);
|
||||
}
|
||||
const char* copyHint = "Ent:Copy";
|
||||
#endif
|
||||
display.setCursor(display.width() - display.getTextWidth(copyHint) - 2, footerY);
|
||||
display.print(copyHint);
|
||||
|
||||
@@ -664,9 +669,15 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.print("No messages yet");
|
||||
display.setCursor(0, 30);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Swipe: Switch channel");
|
||||
display.setCursor(0, 40);
|
||||
display.print("Long press: Compose");
|
||||
#else
|
||||
display.print("A/D: Switch channel");
|
||||
display.setCursor(0, 40);
|
||||
display.print("C: Compose message");
|
||||
#endif
|
||||
display.setTextSize(1); // Restore for footer
|
||||
} else {
|
||||
display.setTextSize(0); // Tiny font for message body
|
||||
@@ -956,8 +967,11 @@ public:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setTextSize(0);
|
||||
display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Select boot: home");
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Swipe:Ch/Scroll");
|
||||
const char* rtCh = "Hold:Compose";
|
||||
display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY);
|
||||
display.print(rtCh);
|
||||
#else
|
||||
// Left side: abbreviated controls
|
||||
if (_replySelectMode) {
|
||||
|
||||
@@ -219,7 +219,11 @@ public:
|
||||
display.setCursor(0, y);
|
||||
display.print("No contacts");
|
||||
display.setCursor(0, y + lineHeight);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Swipe to change filter");
|
||||
#else
|
||||
display.print("A/D: Change filter");
|
||||
#endif
|
||||
} else {
|
||||
// Center visible window around selected item (TextReaderScreen pattern)
|
||||
int maxVisible = (maxY - headerHeight) / lineHeight;
|
||||
@@ -302,8 +306,11 @@ public:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setTextSize(0);
|
||||
display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Select boot: home");
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Swipe:Filter");
|
||||
const char* right = "Hold:DM/Admin";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
#else
|
||||
// Left: Q:Bk
|
||||
display.setCursor(0, footerY);
|
||||
|
||||
@@ -100,7 +100,11 @@ public:
|
||||
// Highlight selected row
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -496,7 +496,11 @@ private:
|
||||
int rightX = display.width() - display.getTextWidth(tmp) - 2;
|
||||
|
||||
if (_selectedFile >= 1 && _selectedFile <= (int)_fileList.size()) {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const char* hint = "[Hold:Rename]";
|
||||
#else
|
||||
const char* hint = "[R:Rename]";
|
||||
#endif
|
||||
int hintX = rightX - display.getTextWidth(hint) - 4;
|
||||
display.setCursor(hintX, 0);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
@@ -511,7 +515,7 @@ private:
|
||||
|
||||
// File list with "+ New Note" at index 0
|
||||
display.setTextSize(0);
|
||||
int listLineH = 8;
|
||||
int listLineH = 9; // Match contacts/discovery for consistent selection highlight
|
||||
int startY = 14;
|
||||
int totalItems = 1 + (int)_fileList.size();
|
||||
int maxVisible = (display.height() - startY - _footerHeight) / listLineH;
|
||||
@@ -528,7 +532,11 @@ private:
|
||||
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), listLineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), listLineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -558,9 +566,13 @@ private:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setCursor(0, footerY);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Swipe:Nav");
|
||||
const char* right = "Tap:Open";
|
||||
#else
|
||||
display.print("Q:Back W/S:Nav");
|
||||
|
||||
const char* right = "Ent:Open";
|
||||
#endif
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
@@ -576,9 +588,13 @@ private:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Tap:Edit");
|
||||
const char* right = "Hold:Delete";
|
||||
#else
|
||||
display.print("Q:Bck Ent:Edit");
|
||||
|
||||
const char* right = "Sh+Del:Del";
|
||||
#endif
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
return;
|
||||
@@ -663,9 +679,15 @@ private:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
display.setCursor(0, footerY);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Swipe:Page");
|
||||
|
||||
const char* right = "Tap:Edit";
|
||||
#else
|
||||
display.print("Q:Bck Ent:Edit");
|
||||
|
||||
const char* right = "Sh+Del:Del";
|
||||
#endif
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
@@ -766,11 +788,25 @@ private:
|
||||
snprintf(status, sizeof(status), "Pg %d/%d", curPage, totalPg);
|
||||
display.print(status);
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const char* mid = "Tap:Type";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
#endif
|
||||
|
||||
const char* right;
|
||||
if (_bufLen == 0 || !_dirty) {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
right = "Back";
|
||||
#else
|
||||
right = "Q:Back";
|
||||
#endif
|
||||
} else {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
right = "Hold:Save";
|
||||
#else
|
||||
right = "Sh+Del:Save";
|
||||
#endif
|
||||
}
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
@@ -817,9 +853,13 @@ private:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Cancel");
|
||||
const char* right = "Tap:Confirm";
|
||||
#else
|
||||
display.print("Q:Cancel");
|
||||
|
||||
const char* right = "Ent:Confirm";
|
||||
#endif
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
@@ -852,9 +892,13 @@ private:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Cancel");
|
||||
const char* right = "Tap:Delete";
|
||||
#else
|
||||
display.print("Q:Cancel");
|
||||
|
||||
const char* right = "Ent:Delete";
|
||||
#endif
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
@@ -1124,6 +1168,8 @@ public:
|
||||
|
||||
void setSDReady(bool ready) { _sdReady = ready; }
|
||||
bool isSDReady() const { return _sdReady; }
|
||||
bool isDirty() const { return _dirty; }
|
||||
void triggerSaveAndExit() { saveAndExit(); }
|
||||
|
||||
void setTimestamp(uint32_t rtcTime, int8_t utcOffset) {
|
||||
_rtcTime = rtcTime;
|
||||
@@ -1145,7 +1191,6 @@ public:
|
||||
bool isInFileList() const { return _mode == FILE_LIST; }
|
||||
bool isRenaming() const { return _mode == RENAMING; }
|
||||
bool isConfirmingDelete() const { return _mode == CONFIRM_DELETE; }
|
||||
bool isDirty() const { return _dirty; }
|
||||
bool isEmpty() const { return _bufLen == 0; }
|
||||
|
||||
// ---- Cursor Navigation (called from main.cpp) ----
|
||||
|
||||
@@ -598,41 +598,77 @@ public:
|
||||
|
||||
switch (_state) {
|
||||
case STATE_PASSWORD_ENTRY:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Exit");
|
||||
renderFooterRight(display, footerY, "Hold:Type");
|
||||
#else
|
||||
display.print("Sh+Del:Exit");
|
||||
renderFooterRight(display, footerY, "Ent:Login");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case STATE_LOGGING_IN:
|
||||
case STATE_COMMAND_PENDING:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Cancel");
|
||||
#else
|
||||
display.print("Sh+Del:Cancel");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case STATE_CATEGORY_MENU:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Exit");
|
||||
renderFooterMidRight(display, footerY, "Back:Exit", "Tap:Open", "Swipe:Sel");
|
||||
#else
|
||||
display.print("Sh+Del:Exit");
|
||||
renderFooterMidRight(display, footerY, "Sh+Del:Exit", "Ent:Open", "W/S:Sel");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case STATE_COMMAND_MENU:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Back");
|
||||
renderFooterMidRight(display, footerY, "Back:Back", "Tap:Run", "Swipe:Sel");
|
||||
#else
|
||||
display.print("Sh+Del:Back");
|
||||
renderFooterMidRight(display, footerY, "Sh+Del:Back", "Ent:Run", "W/S:Sel");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case STATE_PARAM_ENTRY:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Cancel");
|
||||
renderFooterRight(display, footerY, "Tap:Send");
|
||||
#else
|
||||
display.print("Sh+Del:Cancel");
|
||||
renderFooterRight(display, footerY, "Ent:Send");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case STATE_CONFIRM:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:No");
|
||||
renderFooterRight(display, footerY, "Tap:Yes");
|
||||
#else
|
||||
display.print("Sh+Del:No");
|
||||
renderFooterRight(display, footerY, "Ent:Yes");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case STATE_RESPONSE_VIEW:
|
||||
case STATE_ERROR:
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Back:Back");
|
||||
if (_responseTotalLines > bodyHeight / 9) {
|
||||
renderFooterRight(display, footerY, "Swipe:Scroll");
|
||||
}
|
||||
#else
|
||||
display.print("Sh+Del:Back");
|
||||
if (_responseTotalLines > bodyHeight / 9) {
|
||||
renderFooterRight(display, footerY, "W/S:Scrll");
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1001,7 +1037,11 @@ private:
|
||||
if (_pendingCmd && (_pendingCmd->flags & CMDF_EXPECT_TIMEOUT)) {
|
||||
display.print("Timeout response is normal.");
|
||||
} else {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Tap=Yes Back=No");
|
||||
#else
|
||||
display.print("Enter=Yes Sh+Del=No");
|
||||
#endif
|
||||
}
|
||||
|
||||
display.setTextSize(1);
|
||||
|
||||
@@ -926,7 +926,7 @@ private:
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setTextSize(0);
|
||||
display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Open boot: home");
|
||||
display.drawTextCentered(display.width() / 2, footerY, "Swipe: Scroll Tap: Open Boot: home");
|
||||
#else
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Back W/S:Nav");
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#if UI_HAS_JOYSTICK
|
||||
#define PRESS_LABEL "press Enter"
|
||||
#elif defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#define PRESS_LABEL "hold boot btn"
|
||||
#define PRESS_LABEL "long press"
|
||||
#else
|
||||
#define PRESS_LABEL "long press"
|
||||
#endif
|
||||
@@ -378,6 +378,7 @@ public:
|
||||
const int gridW = tileW * 3 + gapX * 2;
|
||||
const int gridX = (display.width() - gridW) / 2;
|
||||
const int gridY = y + 2;
|
||||
_task->setTileGridVY(gridY); // Store for touch hit testing
|
||||
|
||||
for (int row = 0; row < 2; row++) {
|
||||
for (int col = 0; col < 3; col++) {
|
||||
@@ -1372,6 +1373,9 @@ void UITask::loop() {
|
||||
// Ignored while locked — long press required to unlock
|
||||
if (_locked) {
|
||||
c = 0;
|
||||
} else if (_vkbActive) {
|
||||
onVKBCancel();
|
||||
c = 0;
|
||||
} else if (curr == home) {
|
||||
c = checkDisplayOn(KEY_NEXT);
|
||||
} else {
|
||||
@@ -1501,6 +1505,33 @@ if (curr) curr->poll();
|
||||
if (_display != NULL && _display->isOn()) {
|
||||
if (millis() >= _next_refresh && curr) {
|
||||
_display->startFrame();
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
if (_vkbActive) {
|
||||
_vkb.render(*_display);
|
||||
_next_refresh = millis() + 500; // Moderate refresh for cursor blink
|
||||
// Check if keyboard was submitted or cancelled during render cycle
|
||||
if (_vkb.status() == VKB_SUBMITTED) {
|
||||
onVKBSubmit();
|
||||
} else if (_vkb.status() == VKB_CANCELLED) {
|
||||
onVKBCancel();
|
||||
}
|
||||
} else {
|
||||
int delay_millis = curr->render(*_display);
|
||||
if (millis() < _alert_expiry) {
|
||||
_display->setTextSize(1);
|
||||
int y = _display->height() / 3;
|
||||
int p = _display->height() / 32;
|
||||
_display->setColor(DisplayDriver::DARK);
|
||||
_display->fillRect(p, y, _display->width() - p*2, y);
|
||||
_display->setColor(DisplayDriver::LIGHT);
|
||||
_display->drawRect(p, y, _display->width() - p*2, y);
|
||||
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert);
|
||||
_next_refresh = _alert_expiry;
|
||||
} else {
|
||||
_next_refresh = millis() + delay_millis;
|
||||
}
|
||||
}
|
||||
#else
|
||||
int delay_millis = curr->render(*_display);
|
||||
if (millis() < _alert_expiry) { // render alert popup
|
||||
_display->setTextSize(1);
|
||||
@@ -1515,6 +1546,7 @@ if (curr) curr->poll();
|
||||
} else {
|
||||
_next_refresh = millis() + delay_millis;
|
||||
}
|
||||
#endif
|
||||
_display->endFrame();
|
||||
}
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
@@ -1575,7 +1607,10 @@ char UITask::handleLongPress(char c) {
|
||||
c = 0; // consume event
|
||||
}
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
else if (_locked) {
|
||||
else if (_vkbActive) {
|
||||
onVKBCancel(); // Long press while VKB → cancel
|
||||
c = 0;
|
||||
} else if (_locked) {
|
||||
unlockScreen();
|
||||
c = 0;
|
||||
} else {
|
||||
@@ -1645,6 +1680,115 @@ void UITask::unlockScreen() {
|
||||
_next_refresh = 0;
|
||||
Serial.println("[UI] Screen unlocked");
|
||||
}
|
||||
|
||||
void UITask::showVirtualKeyboard(VKBPurpose purpose, const char* label, const char* initial, int maxLen, int contextIdx) {
|
||||
_vkb.open(purpose, label, initial, maxLen, contextIdx);
|
||||
_vkbActive = true;
|
||||
_screenBeforeVKB = curr;
|
||||
_next_refresh = 0;
|
||||
display.invalidateFrameCRC(); // Force e-ink redraw (VKB may look same as last open)
|
||||
_auto_off = millis() + 120000; // 2min timeout while typing
|
||||
Serial.printf("[UI] VKB opened: %s\n", label);
|
||||
}
|
||||
|
||||
void UITask::onVKBSubmit() {
|
||||
_vkbActive = false;
|
||||
const char* text = _vkb.getText();
|
||||
VKBPurpose purpose = _vkb.purpose();
|
||||
int idx = _vkb.contextIdx();
|
||||
|
||||
Serial.printf("[UI] VKB submit: purpose=%d idx=%d text='%s'\n", purpose, idx, text);
|
||||
|
||||
switch (purpose) {
|
||||
case VKB_CHANNEL_MSG: {
|
||||
if (strlen(text) == 0) break;
|
||||
|
||||
ChannelDetails channel;
|
||||
if (the_mesh.getChannel(idx, channel)) {
|
||||
uint32_t timestamp = rtc_clock.getCurrentTime();
|
||||
int textLen = strlen(text);
|
||||
if (the_mesh.sendGroupMessage(timestamp, channel.channel,
|
||||
the_mesh.getNodePrefs()->node_name,
|
||||
text, textLen)) {
|
||||
addSentChannelMessage(idx, the_mesh.getNodePrefs()->node_name, text);
|
||||
the_mesh.queueSentChannelMessage(idx, timestamp,
|
||||
the_mesh.getNodePrefs()->node_name, text);
|
||||
showAlert("Sent!", 1500);
|
||||
} else {
|
||||
showAlert("Send failed!", 1500);
|
||||
}
|
||||
}
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
case VKB_DM: {
|
||||
if (strlen(text) == 0) break;
|
||||
|
||||
if (the_mesh.uiSendDirectMessage((uint32_t)idx, text)) {
|
||||
showAlert("DM sent!", 1500);
|
||||
} else {
|
||||
showAlert("DM failed!", 1500);
|
||||
}
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
case VKB_ADMIN_PASSWORD: {
|
||||
// Feed each character to the admin screen, then Enter
|
||||
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen();
|
||||
if (admin) {
|
||||
for (int i = 0; text[i]; i++) {
|
||||
admin->handleInput(text[i]);
|
||||
}
|
||||
admin->handleInput('\r');
|
||||
}
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
case VKB_ADMIN_CLI: {
|
||||
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen();
|
||||
if (admin) {
|
||||
for (int i = 0; text[i]; i++) {
|
||||
admin->handleInput(text[i]);
|
||||
}
|
||||
admin->handleInput('\r');
|
||||
}
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
case VKB_SETTINGS_NAME: {
|
||||
if (strlen(text) > 0) {
|
||||
strncpy(_node_prefs->node_name, text, sizeof(_node_prefs->node_name) - 1);
|
||||
_node_prefs->node_name[sizeof(_node_prefs->node_name) - 1] = '\0';
|
||||
the_mesh.savePrefs();
|
||||
showAlert("Name saved", 1000);
|
||||
}
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
case VKB_NOTES: {
|
||||
NotesScreen* notes = (NotesScreen*)getNotesScreen();
|
||||
if (notes && strlen(text) > 0) {
|
||||
for (int i = 0; text[i]; i++) {
|
||||
notes->handleInput(text[i]);
|
||||
}
|
||||
}
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_screenBeforeVKB = nullptr;
|
||||
_next_refresh = 0;
|
||||
display.invalidateFrameCRC();
|
||||
}
|
||||
|
||||
void UITask::onVKBCancel() {
|
||||
_vkbActive = false;
|
||||
if (_screenBeforeVKB) setCurrScreen(_screenBeforeVKB);
|
||||
_screenBeforeVKB = nullptr;
|
||||
_next_refresh = 0;
|
||||
display.invalidateFrameCRC();
|
||||
Serial.println("[UI] VKB cancelled");
|
||||
}
|
||||
#endif
|
||||
|
||||
bool UITask::getGPSState() {
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
#include "WebReaderScreen.h"
|
||||
#endif
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#include "VirtualKeyboard.h"
|
||||
#endif
|
||||
|
||||
// MapScreen.h included in UITask.cpp and main.cpp only (PNGdec headers
|
||||
// conflict with BLE if pulled into the global include chain)
|
||||
|
||||
@@ -86,10 +90,15 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* map_screen; // Map tile screen (GPS + SD card tiles)
|
||||
UIScreen* curr;
|
||||
bool _homeShowingTiles = false; // Set by HomeScreen render when tile grid is visible
|
||||
int _tileGridVY = 44; // Virtual Y of tile grid top (updated each render)
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
UIScreen* lock_screen; // Lock screen (big clock + battery + unread)
|
||||
UIScreen* _screenBeforeLock = nullptr;
|
||||
bool _locked = false;
|
||||
|
||||
VirtualKeyboard _vkb;
|
||||
bool _vkbActive = false;
|
||||
UIScreen* _screenBeforeVKB = nullptr;
|
||||
#endif
|
||||
|
||||
void userLedHandler();
|
||||
@@ -156,6 +165,8 @@ public:
|
||||
bool isOnHomeScreen() const { return curr == home; }
|
||||
bool isHomeShowingTiles() const { return _homeShowingTiles; }
|
||||
void setHomeShowingTiles(bool v) { _homeShowingTiles = v; }
|
||||
int getTileGridVY() const { return _tileGridVY; }
|
||||
void setTileGridVY(int vy) { _tileGridVY = vy; }
|
||||
bool isOnNotesScreen() const { return curr == notes_screen; }
|
||||
bool isOnSettingsScreen() const { return curr == settings_screen; }
|
||||
bool isOnAudiobookPlayer() const { return curr == audiobook_screen; }
|
||||
@@ -166,6 +177,11 @@ public:
|
||||
bool isLocked() const { return _locked; }
|
||||
void lockScreen();
|
||||
void unlockScreen();
|
||||
bool isVKBActive() const { return _vkbActive; }
|
||||
VirtualKeyboard& getVKB() { return _vkb; }
|
||||
void showVirtualKeyboard(VKBPurpose purpose, const char* label, const char* initial, int maxLen, int contextIdx = 0);
|
||||
void onVKBSubmit();
|
||||
void onVKBCancel();
|
||||
#endif
|
||||
#ifdef MECK_WEB_READER
|
||||
bool isOnWebReader() const { return curr == web_reader; }
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
#pragma once
|
||||
// =============================================================================
|
||||
// VirtualKeyboard — On-screen QWERTY keyboard for T5S3 (touch-only devices)
|
||||
//
|
||||
// Renders in virtual coordinate space (128×128). Touch hit testing converts
|
||||
// physical GT911 coords (960×540) to virtual coords.
|
||||
//
|
||||
// Usage:
|
||||
// keyboard.open("To: General", "", 137); // label, initial text, max len
|
||||
// keyboard.render(display); // in render loop
|
||||
// keyboard.handleTap(vx, vy); // on touch tap (virtual coords)
|
||||
// if (keyboard.status() == VKB_SUBMITTED) { ... keyboard.getText() ... }
|
||||
// =============================================================================
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
#ifndef VIRTUAL_KEYBOARD_H
|
||||
#define VIRTUAL_KEYBOARD_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
|
||||
enum VKBStatus { VKB_EDITING, VKB_SUBMITTED, VKB_CANCELLED };
|
||||
|
||||
// What the keyboard is being used for (dispatch on submit)
|
||||
enum VKBPurpose {
|
||||
VKB_CHANNEL_MSG, // Send to channel
|
||||
VKB_DM, // Direct message to contact
|
||||
VKB_ADMIN_PASSWORD, // Repeater admin login
|
||||
VKB_ADMIN_CLI, // Repeater admin CLI command
|
||||
VKB_NOTES, // Insert text into notes
|
||||
VKB_SETTINGS_NAME // Edit node name
|
||||
};
|
||||
|
||||
class VirtualKeyboard {
|
||||
public:
|
||||
static const int MAX_TEXT = 140;
|
||||
|
||||
VirtualKeyboard() : _status(VKB_CANCELLED), _purpose(VKB_CHANNEL_MSG),
|
||||
_contextIdx(0), _textLen(0), _shifted(false), _symbols(false),
|
||||
_rendered(false), _acceptTapsAfter(0) {
|
||||
_text[0] = '\0';
|
||||
_label[0] = '\0';
|
||||
}
|
||||
|
||||
void open(VKBPurpose purpose, const char* label, const char* initial, int maxLen, int contextIdx = 0) {
|
||||
_purpose = purpose;
|
||||
_contextIdx = contextIdx;
|
||||
_status = VKB_EDITING;
|
||||
_shifted = false;
|
||||
_symbols = false;
|
||||
_rendered = false; // Not yet drawn
|
||||
_acceptTapsAfter = 0; // Set after first render completes
|
||||
_maxLen = (maxLen > 0 && maxLen < MAX_TEXT) ? maxLen : MAX_TEXT;
|
||||
|
||||
strncpy(_label, label, sizeof(_label) - 1);
|
||||
_label[sizeof(_label) - 1] = '\0';
|
||||
|
||||
if (initial && initial[0]) {
|
||||
strncpy(_text, initial, _maxLen);
|
||||
_text[_maxLen] = '\0';
|
||||
_textLen = strlen(_text);
|
||||
} else {
|
||||
_text[0] = '\0';
|
||||
_textLen = 0;
|
||||
}
|
||||
}
|
||||
|
||||
VKBStatus status() const { return _status; }
|
||||
VKBPurpose purpose() const { return _purpose; }
|
||||
int contextIdx() const { return _contextIdx; }
|
||||
const char* getText() const { return _text; }
|
||||
int getTextLen() const { return _textLen; }
|
||||
bool isActive() const { return _status == VKB_EDITING; }
|
||||
|
||||
// --- Render keyboard + input field ---
|
||||
void render(DisplayDriver& display) {
|
||||
// Mark as rendered — touch cooldown starts from first handleTap after this
|
||||
_rendered = true;
|
||||
|
||||
// Header label (To: channel, DM: name, etc.)
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(2, 0);
|
||||
display.print(_label);
|
||||
|
||||
// Input text field
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 10, 128, 18); // Border
|
||||
|
||||
display.setCursor(2, 12);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Show text with cursor
|
||||
char dispBuf[MAX_TEXT + 2];
|
||||
snprintf(dispBuf, sizeof(dispBuf), "%s_", _text);
|
||||
display.print(dispBuf);
|
||||
|
||||
// Character count
|
||||
{
|
||||
char countBuf[12];
|
||||
snprintf(countBuf, sizeof(countBuf), "%d/%d", _textLen, _maxLen);
|
||||
int cw = display.getTextWidth(countBuf);
|
||||
display.setCursor(128 - cw - 2, 0);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.print(countBuf);
|
||||
}
|
||||
|
||||
// Separator
|
||||
display.drawRect(0, 30, 128, 1);
|
||||
|
||||
// --- Draw keyboard rows ---
|
||||
const char* const* layout = getLayout();
|
||||
|
||||
for (int row = 0; row < 3; row++) {
|
||||
int numKeys = strlen(layout[row]);
|
||||
int rowY = KEY_START_Y + row * (KEY_H + KEY_GAP);
|
||||
|
||||
// Calculate key width and starting X for this row
|
||||
int totalW = numKeys * KEY_W + (numKeys - 1) * KEY_GAP;
|
||||
int startX = (128 - totalW) / 2;
|
||||
|
||||
for (int k = 0; k < numKeys; k++) {
|
||||
int kx = startX + k * (KEY_W + KEY_GAP);
|
||||
char ch = layout[row][k];
|
||||
|
||||
// Draw key background (inverted for special keys)
|
||||
bool special = (ch == '<' || ch == '^' || ch == '~' || ch == '>' || ch == '\x01');
|
||||
if (special) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(kx, rowY + 1, KEY_W, KEY_H - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(kx, rowY + 1, KEY_W, KEY_H - 1);
|
||||
}
|
||||
|
||||
// Draw key label
|
||||
char keyLabel[2] = { ch, '\0' };
|
||||
// Remap special chars to display labels
|
||||
if (ch == '<') keyLabel[0] = '<'; // Backspace
|
||||
if (ch == '^') keyLabel[0] = '^'; // Shift
|
||||
if (ch == '>') keyLabel[0] = '>'; // Enter
|
||||
|
||||
if (ch == '~') {
|
||||
// Space key — don't draw individual label
|
||||
} else if (ch == '\x01') {
|
||||
// Symbol toggle in row — show "ab" hint
|
||||
int lx = kx + KEY_W / 2 - display.getTextWidth("ab") / 2;
|
||||
display.setCursor(lx, rowY + 2);
|
||||
display.print("ab");
|
||||
} else {
|
||||
int lx = kx + KEY_W / 2 - display.getTextWidth(keyLabel) / 2;
|
||||
display.setCursor(lx, rowY + 2);
|
||||
display.print(keyLabel);
|
||||
}
|
||||
|
||||
// Restore color
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw row 4 with variable-width keys
|
||||
int r4y = KEY_START_Y + 3 * (KEY_H + KEY_GAP);
|
||||
drawRow4(display, r4y);
|
||||
|
||||
// Shift/symbol indicator
|
||||
display.setTextSize(0);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
if (_shifted) {
|
||||
display.setCursor(2, 126);
|
||||
display.print("SHIFT");
|
||||
} else if (_symbols) {
|
||||
display.setCursor(2, 126);
|
||||
display.print("123");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Handle touch tap (virtual coordinates) ---
|
||||
// Returns true if the tap was consumed
|
||||
bool handleTap(int vx, int vy) {
|
||||
if (_status != VKB_EDITING) return false;
|
||||
|
||||
// Don't accept taps until keyboard has been rendered at least once
|
||||
if (!_rendered) return true;
|
||||
|
||||
// Start cooldown timer on first call after render — this runs AFTER
|
||||
// endFrame()'s blocking e-ink refresh, so the timer counts real
|
||||
// post-render time (not time spent refreshing the display)
|
||||
if (_acceptTapsAfter == 0) {
|
||||
_acceptTapsAfter = millis() + 500;
|
||||
return true; // consume this tap (residual from long press)
|
||||
}
|
||||
if (millis() < _acceptTapsAfter) return true; // still in cooldown
|
||||
|
||||
// Check keyboard rows 0-2
|
||||
const char* const* layout = getLayout();
|
||||
|
||||
for (int row = 0; row < 3; row++) {
|
||||
int numKeys = strlen(layout[row]);
|
||||
int rowY = KEY_START_Y + row * (KEY_H + KEY_GAP);
|
||||
if (vy < rowY || vy >= rowY + KEY_H) continue;
|
||||
|
||||
int totalW = numKeys * KEY_W + (numKeys - 1) * KEY_GAP;
|
||||
int startX = (128 - totalW) / 2;
|
||||
|
||||
for (int k = 0; k < numKeys; k++) {
|
||||
int kx = startX + k * (KEY_W + KEY_GAP);
|
||||
if (vx >= kx && vx < kx + KEY_W) {
|
||||
char ch = layout[row][k];
|
||||
processKey(ch);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true; // Tap was in row area but between keys — consume
|
||||
}
|
||||
|
||||
// Check row 4 (variable width keys)
|
||||
int r4y = KEY_START_Y + 3 * (KEY_H + KEY_GAP);
|
||||
if (vy >= r4y && vy < r4y + KEY_H) {
|
||||
return handleRow4Tap(vx);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Swipe up on keyboard = cancel
|
||||
void cancel() { _status = VKB_CANCELLED; }
|
||||
|
||||
private:
|
||||
VKBStatus _status;
|
||||
VKBPurpose _purpose;
|
||||
int _contextIdx;
|
||||
char _text[MAX_TEXT + 1];
|
||||
int _textLen;
|
||||
int _maxLen;
|
||||
char _label[40];
|
||||
bool _shifted;
|
||||
bool _symbols;
|
||||
bool _rendered;
|
||||
unsigned long _acceptTapsAfter;
|
||||
|
||||
// Layout constants (virtual coords)
|
||||
static const int KEY_W = 11;
|
||||
static const int KEY_H = 19;
|
||||
static const int KEY_GAP = 1;
|
||||
static const int KEY_START_Y = 34;
|
||||
|
||||
// Key layouts — rows 0-2 as char arrays
|
||||
// Special: ^ = shift, < = backspace, # = symbols, > = enter, ~ = space
|
||||
const char* const* getLayout() const {
|
||||
static const char* const lower[3] = { "qwertyuiop", "asdfghjkl", "^zxcvbnm<" };
|
||||
static const char* const upper[3] = { "QWERTYUIOP", "ASDFGHJKL", "^ZXCVBNM<" };
|
||||
static const char* const syms[3] = { "1234567890", "-/:;()@$&#", "\x01.,?!'\"_<" };
|
||||
return _symbols ? syms : (_shifted ? upper : lower);
|
||||
}
|
||||
|
||||
// Row 4: variable-width keys [#/ABC] [,] [SPACE] [.] [Enter]
|
||||
// Defined by physical zones, not the char-array approach
|
||||
struct R4Key { int x; int w; char ch; const char* label; };
|
||||
|
||||
void drawRow4(DisplayDriver& display, int y) {
|
||||
// # or ABC toggle: x=4, w=20
|
||||
// comma: x=26, w=11
|
||||
// space: x=39, w=50
|
||||
// period: x=91, w=11
|
||||
// enter: x=104, w=20
|
||||
const R4Key keys[] = {
|
||||
{ 4, 20, '\x01', _symbols ? "ABC" : "123" },
|
||||
{ 26, 11, ',', "," },
|
||||
{ 39, 50, '~', "space" },
|
||||
{ 91, 11, '.', "." },
|
||||
{ 104, 20, '>', "Send" }
|
||||
};
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
bool special = (keys[i].ch == '\x01' || keys[i].ch == '>');
|
||||
if (special) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(keys[i].x, y + 1, keys[i].w, KEY_H - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(keys[i].x, y + 1, keys[i].w, KEY_H - 1);
|
||||
}
|
||||
|
||||
// Center label in key
|
||||
display.setTextSize(0);
|
||||
int lw = display.getTextWidth(keys[i].label);
|
||||
int lx = keys[i].x + (keys[i].w - lw) / 2;
|
||||
display.setCursor(lx, y + 2);
|
||||
display.print(keys[i].label);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
bool handleRow4Tap(int vx) {
|
||||
const R4Key keys[] = {
|
||||
{ 4, 20, '\x01', nullptr },
|
||||
{ 26, 11, ',', nullptr },
|
||||
{ 39, 50, '~', nullptr },
|
||||
{ 91, 11, '.', nullptr },
|
||||
{ 104, 20, '>', nullptr }
|
||||
};
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (vx >= keys[i].x && vx < keys[i].x + keys[i].w) {
|
||||
processKey(keys[i].ch);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true; // Consume tap in row area
|
||||
}
|
||||
|
||||
void processKey(char ch) {
|
||||
if (ch == '^') {
|
||||
// Shift toggle
|
||||
_shifted = !_shifted;
|
||||
_symbols = false;
|
||||
} else if (ch == '\x01') {
|
||||
// Symbol/letter toggle
|
||||
_symbols = !_symbols;
|
||||
_shifted = false;
|
||||
} else if (ch == '<') {
|
||||
// Backspace
|
||||
if (_textLen > 0) {
|
||||
_textLen--;
|
||||
_text[_textLen] = '\0';
|
||||
}
|
||||
} else if (ch == '>') {
|
||||
// Enter/Send
|
||||
_status = VKB_SUBMITTED;
|
||||
} else if (ch == '~') {
|
||||
// Space
|
||||
if (_textLen < _maxLen) {
|
||||
_text[_textLen++] = ' ';
|
||||
_text[_textLen] = '\0';
|
||||
}
|
||||
} else {
|
||||
// Regular character
|
||||
if (_textLen < _maxLen) {
|
||||
_text[_textLen++] = ch;
|
||||
_text[_textLen] = '\0';
|
||||
// Auto-unshift after typing one character
|
||||
if (_shifted) _shifted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // VIRTUAL_KEYBOARD_H
|
||||
#endif // LilyGo_T5S3_EPaper_Pro
|
||||
Reference in New Issue
Block a user