mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-10 07:14:46 +02:00
t5s3 - improved cardkb notes rendering; fix notes generic filename save type
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user