t5s3 - improved cardkb notes rendering; fix notes generic filename save type

This commit is contained in:
pelgraine
2026-03-20 08:05:23 +11:00
parent 3ae988c0bb
commit f81de07830
4 changed files with 153 additions and 81 deletions
+100 -80
View File
@@ -2025,90 +2025,118 @@ void loop() {
if (ui_task.isVKBActive()) {
// VKB is open — feed character into VKB text buffer
ui_task.feedCardKBChar(ckb);
} else if (ckb == 0x1B) {
// ESC → back (same as 'q' on T-Deck Pro)
ui_task.injectKey('q');
} else if (ui_task.isOnHomeScreen()) {
// Home screen: letter shortcuts open tiles, arrows cycle pages
switch (ckb) {
case 'm': ui_task.gotoChannelScreen(); break;
case 'c': ui_task.gotoContactsScreen(); break;
case 'e': ui_task.gotoTextReader(); break;
case 'n': ui_task.gotoNotesScreen(); break;
case 's': ui_task.gotoSettingsScreen(); break;
case 'f': ui_task.gotoDiscoveryScreen(); break;
case 'h': ui_task.gotoLastHeardScreen(); break;
// Home screen: ESC does nothing special, letter shortcuts open tiles
if (ckb == 0x1B) {
// ESC on home — no-op (already home)
} else {
switch (ckb) {
case 'm': ui_task.gotoChannelScreen(); break;
case 'c': ui_task.gotoContactsScreen(); break;
case 'e': ui_task.gotoTextReader(); break;
case 'n': ui_task.gotoNotesScreen(); break;
case 's': ui_task.gotoSettingsScreen(); break;
case 'f': ui_task.gotoDiscoveryScreen(); break;
case 'h': ui_task.gotoLastHeardScreen(); break;
#ifdef MECK_WEB_READER
case 'b': ui_task.gotoWebReader(); break;
case 'b': ui_task.gotoWebReader(); break;
#endif
#if HAS_GPS
case 'g': ui_task.gotoMapScreen(); break;
case 'g': ui_task.gotoMapScreen(); break;
#endif
default: ui_task.injectKey(ckb); break;
default: ui_task.injectKey(ckb); break;
}
}
} else {
// Non-home screens: handle Enter for compose, remap arrows to WASD.
// Screens respond to w/s (scroll up/down) and a/d (prev/next channel)
// but not to KEY_LEFT/KEY_RIGHT constants.
if (ckb == '\r') {
// Enter key — screen-specific compose or select
if (ui_task.isOnChannelScreen()) {
// Open VKB for channel message compose
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);
}
} else if (ui_task.isOnContactsScreen()) {
// DM compose for chat contacts, admin for repeaters
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);
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
ui_task.gotoRepeaterAdmin(idx);
}
}
} else if (ui_task.isOnRepeaterAdmin()) {
// Open VKB for password or CLI entry
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);
// Non-home screens: context-specific routing
bool handled = false;
// Notes editing/renaming: route ALL keys directly (no VKB).
// This gives: Enter=newline, arrows=cursor, printable=insert, ESC=save&exit
if (ui_task.isOnNotesScreen()) {
NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen();
if (notesScr && (notesScr->isEditing() || notesScr->isRenaming())) {
handled = true;
if (ckb == 0x1B) {
// ESC: save & exit editing, or cancel rename
if (notesScr->isEditing()) {
notesScr->triggerSaveAndExit();
} else {
ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137);
ui_task.injectKey('q');
}
} else if (notesScr->isEditing()) {
// Editing mode: arrows move cursor, everything else types directly
switch (ckb) {
case (char)0xF2: notesScr->moveCursorUp(); break;
case (char)0xF1: notesScr->moveCursorDown(); break;
case (char)0xF3: notesScr->moveCursorLeft(); break;
case (char)0xF4: notesScr->moveCursorRight(); break;
default: ui_task.injectKey(ckb); break;
}
}
} else if (ui_task.isOnNotesScreen()) {
// Open VKB for note editing
NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen();
if (notesScr && notesScr->isEditing()) {
ui_task.showVirtualKeyboard(VKB_NOTES, "Edit Note", "", 137);
} else {
ui_task.injectKey('\r'); // File list: select/open
// Renaming mode: all keys go directly to rename handler
ui_task.injectKey(ckb);
}
ui_task.forceRefresh();
}
}
if (!handled) {
// ESC → back (same as 'q' on T-Deck Pro) for all non-notes screens
if (ckb == 0x1B) {
ui_task.injectKey('q');
} else if (ckb == '\r') {
// Enter key — screen-specific compose or select
if (ui_task.isOnChannelScreen()) {
// Open VKB for channel message compose
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);
}
} else if (ui_task.isOnContactsScreen()) {
// DM compose for chat contacts, admin for repeaters
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);
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
ui_task.gotoRepeaterAdmin(idx);
}
}
} else if (ui_task.isOnRepeaterAdmin()) {
// Open VKB for password or CLI entry
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);
} else {
ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137);
}
}
} else {
// All other screens: pass Enter through for native handling
// (settings toggle, discovery add-contact, last heard, text reader, notes file list, etc.)
ui_task.injectKey('\r');
}
} else {
// All other screens: pass Enter through for native handling
// (settings toggle, discovery add-contact, last heard, text reader file select, etc.)
ui_task.injectKey('\r');
}
} else {
// Non-Enter keys: remap arrows to WASD, pass others through
switch (ckb) {
case (char)0xF2: ui_task.injectKey('w'); break; // Up → scroll up
case (char)0xF1: ui_task.injectKey('s'); break; // Down → scroll down
case (char)0xF3: ui_task.injectKey('a'); break; // Left → prev channel/category
case (char)0xF4: ui_task.injectKey('d'); break; // Right → next channel/category
default: ui_task.injectKey(ckb); break;
// Non-Enter keys: remap arrows to WASD, pass others through
switch (ckb) {
case (char)0xF2: ui_task.injectKey('w'); break; // Up → scroll up
case (char)0xF1: ui_task.injectKey('s'); break; // Down → scroll down
case (char)0xF3: ui_task.injectKey('a'); break; // Left → prev channel/category
case (char)0xF4: ui_task.injectKey('d'); break; // Right → next channel/category
default: ui_task.injectKey(ckb); break;
}
}
}
}
@@ -2843,14 +2871,6 @@ void handleKeyboardInput() {
case 'n':
// Open notes
Serial.println("Opening notes");
{
NotesScreen* notesScr2 = (NotesScreen*)ui_task.getNotesScreen();
if (notesScr2) {
uint32_t ts = rtc_clock.getCurrentTime();
int8_t utcOff = the_mesh.getNodePrefs()->utc_offset_hours;
notesScr2->setTimestamp(ts, utcOff);
}
}
ui_task.gotoNotesScreen();
break;
@@ -17,6 +17,7 @@
#include <Arduino.h>
#include <Wire.h>
#include "variant.h" // For I2C_SDA, I2C_SCL (bus recovery)
// I2C address (defined in variant.h, fallback here)
#ifndef CARDKB_I2C_ADDR
@@ -60,11 +61,31 @@ public:
// Poll for a keypress. Returns 0 if no key available.
// Returns raw ASCII for printable chars, or Meck KEY_* constants for nav keys.
// Throttled to avoid flooding I2C bus — polls at most every 50ms.
// On read failure, backs off 500ms and re-inits Wire to recover bus state.
char readKey() {
if (!_detected) return 0;
unsigned long now = millis();
if (now - _lastPoll < _pollInterval) return 0;
_lastPoll = now;
Wire.requestFrom((uint8_t)CARDKB_I2C_ADDR, (uint8_t)1);
if (!Wire.available()) return 0;
if (!Wire.available()) {
_errorCount++;
if (_errorCount >= 3) {
// I2C bus may be stuck — re-init to recover
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000);
_pollInterval = 500; // Back off for 500ms
_errorCount = 0;
Serial.println("[CardKB] I2C error recovery — bus re-init");
}
return 0;
}
_errorCount = 0;
_pollInterval = 50; // Normal polling rate
uint8_t raw = Wire.read();
if (raw == 0) return 0;
@@ -92,6 +113,9 @@ public:
private:
bool _detected;
unsigned long _lastPoll = 0;
unsigned long _pollInterval = 50; // ms between polls (increases on error)
uint8_t _errorCount = 0;
};
#endif // CARDKB_KEYBOARD_H
@@ -102,6 +102,10 @@ private:
uint32_t _rtcTime; // Unix timestamp (0 = unavailable)
int8_t _utcOffset; // UTC offset in hours
// Callback to get fresh RTC time (set by UITask at init)
typedef uint32_t (*TimeGetterFn)();
TimeGetterFn _getTimeFn = nullptr;
// ---- Helpers ----
String getFullPath(const String& filename) {
@@ -1077,6 +1081,10 @@ private:
// ---- Note Creation ----
void createNewNote() {
// Refresh timestamp at creation time for accurate filenames
if (_getTimeFn) {
_rtcTime = _getTimeFn();
}
_currentFile = generateFilename();
_buf[0] = '\0';
_bufLen = 0;
@@ -1176,6 +1184,8 @@ public:
_utcOffset = utcOffset;
}
void setTimeGetter(TimeGetterFn fn) { _getTimeFn = fn; }
void enter(DisplayDriver& display) {
initLayout(display);
scanFiles();
@@ -1463,6 +1463,15 @@ void UITask::loop() {
gotoHomeScreen(); // file list: go home
c = 0;
}
} else if (isOnNotesScreen()) {
NotesScreen* notes = (NotesScreen*)notes_screen;
if (notes && notes->isEditing()) {
notes->triggerSaveAndExit(); // save and return to file list
} else {
notes->exitNotes();
gotoHomeScreen();
}
c = 0;
} else {
gotoHomeScreen();
c = 0; // consumed
@@ -1616,6 +1625,11 @@ if (curr) curr->poll();
onVKBCancel();
}
} else {
// Default: allow full refresh. Override for notes editing (no flash while typing).
display.setForcePartial(false);
if (isOnNotesScreen() && ((NotesScreen*)notes_screen)->isEditing()) {
display.setForcePartial(true);
}
int delay_millis = curr->render(*_display);
// Check if settings screen needs VKB for WiFi password entry
@@ -2194,6 +2208,10 @@ void UITask::gotoNotesScreen() {
if (_display != NULL) {
notes->enter(*_display);
}
// Set fresh timestamp and wire up time getter for note creation
notes->setTimestamp(rtc_clock.getCurrentTime(),
_node_prefs ? _node_prefs->utc_offset_hours : 0);
notes->setTimeGetter([]() -> uint32_t { return rtc_clock.getCurrentTime(); });
setCurrScreen(notes_screen);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();