Pager: Fn+V pastes the clipboard (no touch path to the edit menu)

On the T-LoRa Pager there was no way to paste: the in-RAM clipboard is
only consumed by the Cut/Copy/Paste edit menu, which opens solely from a
touch long-press on a textarea, and the Pager's encoder never reaches
LVGL's press events. Copy from a message bubble worked; nothing could
paste the result -- the 64-hex contact key and 32-hex channel secret
fields being the painful cases.

Physical Fn(Alt) held + V now pastes into the field being edited:

- PagerKeyboardState: V_POS; a held-Alt V press is resolved by matrix
  position before the symbol map, swallowed, and reported through
  alt_v_chord_pending_/consumeAltVChord() (same shape as Alt+Backspace
  and the Pro's Alt+B). discardAlt() clears it. !HAS_TDECK_PRO only.
- PagerKeyboard.h/.cpp: pagerKeyboardConsumeAltVChord().
- UITask: the Paste cell's logic is factored into txtPasteInto(ta)
  (selection replaced, pasteTextFor hex-key extraction kept) and reused
  by updatePagerAltVChord(), which uses the same "actually editing" test
  as the Alt+Shift Caps chord, hides any open edit menu/accent popup,
  and toasts "Pasted" / "Clipboard empty". Drained at the remote-mirror
  and screen-off discard sites like the other chords.
- TLORA_PAGER_SHORTCUTS.md: documented.
- test_pager_keyboard_state: chord fires once and swallows V;
  tap-Fn-then-V still returns '?'; discardAlt() drops a pending chord.

Trade-off: Fn+V is '?' on this layout, so only a HELD Fn chords. The
one-shot (tap Fn, then V) and locked (double-tap Fn) symbol layers still
type '?', the same compromise the Pro's physical Alt+B makes with '!'.

Tested on tlora_pager_sx1262_companion_radio_touch: Copy from a bubble
then Fn+V in the composer pastes; Fn+V in a key field pastes just the
key out of a copied message; empty clipboard toasts; tap-Fn-then-V
still types '?'.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VKtRiAcddFBAUDogBWmZk4
This commit is contained in:
CyrioandClaude Fable 5.1 committed 2026-10-09 02:34:07 -07:00
1 parent c230820945
commit e3baa774f5
6 files changed
+109 -5

No files matched your search

+2
View File
@@ -165,6 +165,7 @@ While any text field is focused, including a Wi-Fi or repeater password:
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — |
| **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
| **Fn (Alt)** (tap / double-tap) | Next key uses symbols / lock symbols until tapped again | Same |
| **Fn (Alt) held + V** | **Paste** the clipboard at the cursor (replaces a highlighted range). The clipboard is filled by **Copy** in a message bubble's action menu. Only a *held* Fn pastes — tap Fn then V still types `?` | — |
The **BOOT** button (top of the device) instantly wakes the screen from
idle-dim. It does *not* unlock a screen you've manually locked with the
@@ -264,6 +265,7 @@ that changes keyboard language.
| Fn + Shift (editing a field) | Toggle Caps Lock |
| Fn + Shift (not editing a field) | Nothing |
| Fn + Backspace (anywhere) | Jump to Home |
| Fn held + V (editing a field) | Paste the clipboard (tap-Fn-then-V still types `?`) |
| Fn + Space | Enter accent picker |
| @ + letters (composer) | Auto-focuses the mention list — no Fn+Space needed |
| Enter | Select / send / message action menu |
+5
View File
@@ -147,6 +147,11 @@ void pagerKeyboardToggleCaps() { s_state.toggleCaps(); }
bool pagerKeyboardConsumeAltBackspaceChord() {
return s_state.consumeAltBackspaceChord();
}
#if !defined(HAS_TDECK_PRO)
bool pagerKeyboardConsumeAltVChord() {
return s_state.consumeAltVChord();
}
#endif
#if defined(HAS_TDECK_PRO) && !defined(HAS_TDECK_MAX)
bool pagerKeyboardConsumeAltBChord() {
return s_state.consumeAltBChord();
+8
View File
@@ -86,6 +86,14 @@ void pagerKeyboardToggleCaps();
* reports held for this press, so it can't also fire the plain-Backspace
* hold-to-back/hold-to-unlock gestures. Consumes the pending flag on read. */
bool pagerKeyboardConsumeAltBackspaceChord();
#if !defined(HAS_TDECK_PRO)
/** One-shot: true exactly once after physical Alt(Fn)+V is chorded (V pressed
* while Fn is physically held). The V press is consumed rather than emitted
* as the symbol-layer '?'; tap-Fn-then-V and the locked symbol layer still
* type '?'. UITask pastes the in-RAM clipboard into the field being edited.
* Consumes the pending flag on read. */
bool pagerKeyboardConsumeAltVChord();
#endif
#if defined(HAS_TDECK_PRO) && !defined(HAS_TDECK_MAX)
/** One-shot: true after physical Alt+B is pressed on T-Deck Pro. The B press
* is consumed rather than emitted as the symbol-layer exclamation mark. */
+25
View File
@@ -24,6 +24,7 @@ class PagerKeyboardState {
static constexpr uint8_t SHIFT_POS = 2 * COLS + 8;
static constexpr uint8_t BACKSPACE_POS = 2 * COLS + 9;
static constexpr uint8_t SPACE_POS = 3 * COLS;
static constexpr uint8_t V_POS = 2 * COLS + 4; // physical Alt(Fn)+V = paste chord
#endif
uint8_t event(uint8_t code, bool pressed, uint32_t now_ms) {
@@ -126,6 +127,17 @@ class PagerKeyboardState {
return 0;
}
#endif
#if !defined(HAS_TDECK_PRO)
// Physical Alt(Fn)+V: paste chord, resolved by matrix position before the
// symbol map (same shape as the Pro's Alt+B). Only a HELD Fn chords; the
// one-shot (tap Fn, then V) and locked (double-tap Fn) symbol layers still
// type '?' from this key, so the question mark is not lost.
if (code == V_POS && alt_.held()) {
alt_.markHeldUsed();
alt_v_chord_pending_ = true;
return 0;
}
#endif
const uint8_t row = code / COLS;
const uint8_t col = code % COLS;
@@ -146,6 +158,9 @@ class PagerKeyboardState {
#if defined(HAS_TDECK_PRO) && !defined(HAS_TDECK_MAX)
alt_b_chord_pending_ = false;
#endif
#if !defined(HAS_TDECK_PRO)
alt_v_chord_pending_ = false;
#endif
}
bool backspaceHeld() const { return backspace_held_; }
bool spaceHeld() const { return space_held_; }
@@ -167,6 +182,13 @@ class PagerKeyboardState {
return pending;
}
#endif
#if !defined(HAS_TDECK_PRO)
bool consumeAltVChord() {
const bool pending = alt_v_chord_pending_;
alt_v_chord_pending_ = false;
return pending;
}
#endif
#if defined(HAS_TDECK_MAX)
bool consumeBothShiftChord() {
const bool pending = both_shift_chord_pending_;
@@ -187,6 +209,9 @@ class PagerKeyboardState {
bool alt_key_held_ = false;
bool alt_b_chord_pending_ = false;
#endif
#if !defined(HAS_TDECK_PRO)
bool alt_v_chord_pending_ = false;
#endif
#if defined(HAS_TDECK_MAX)
bool shift_l_held_ = false;
bool shift_r_held_ = false;
+45 -5
View File
@@ -11947,6 +11947,19 @@ static const char* pasteTextFor(lv_obj_t* ta, const char* clip, char* buf, size_
return PasteHexKey::extract(clip, want, buf, cap);
}
// Paste the clipboard into `ta` at the cursor, replacing any highlighted
// range. Shared by the edit menu's Paste cell and the Pager's Fn+V chord.
// Returns false when the clipboard is empty (nothing was changed).
static bool txtPasteInto(lv_obj_t* ta) {
if (!ta || !s_clipboard[0]) return false;
uint32_t s_cp = 0, e_cp = 0;
if (taHasSelection(ta, &s_cp, &e_cp)) taDeleteRange(ta, s_cp, e_cp);
char key[65];
lv_textarea_add_text(ta, pasteTextFor(ta, s_clipboard, key, sizeof key));
taClearSelection(ta);
return true;
}
static void txtMenuCellCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
intptr_t act = reinterpret_cast<intptr_t>(lv_event_get_user_data(e));
@@ -11987,11 +12000,7 @@ static void txtMenuCellCb(lv_event_t* e) {
else lv_textarea_set_text(ta, "");
}
} else if (act == TXT_PASTE) {
if (s_clipboard[0]) {
if (sel) taDeleteRange(ta, s_cp, e_cp);
char key[65];
lv_textarea_add_text(ta, pasteTextFor(ta, s_clipboard, key, sizeof key));
}
txtPasteInto(ta);
}
taClearSelection(ta);
txtMenuHide();
@@ -43095,6 +43104,28 @@ static void updateProAltBChord() {
if (g_lv.task) g_lv.task->noteUserInput();
}
#endif
#if !defined(HAS_TDECK_PRO)
// Physical Fn(Alt)+V: paste the in-RAM clipboard into the field being edited.
// The Pager has no touchscreen, so the long-press Cut/Copy/Paste menu
// (txtMenuShow) is unreachable there and the clipboard -- filled by a
// message bubble's Copy action -- had no consumer. Same "actually editing"
// test as the Alt+Shift Caps chord above; a deliberate no-op otherwise (the
// V press is already swallowed by the driver, so nothing leaks through).
// Only a HELD Fn chords: tap-Fn-then-V still types '?'.
static void updatePagerAltVChord() {
if (!pagerKeyboardConsumeAltVChord()) return;
lv_obj_t* ta_focused = lv_keyboard_get_textarea(g_lv.keyboard);
lv_obj_t* ta = (ta_focused && s_nav_group && lv_group_get_focused(s_nav_group) == ta_focused) ? ta_focused : nullptr;
if (!ta || !lv_obj_is_valid(ta)) return;
txtMenuHide();
accentBoxHide(); // a pasted word must not leave a stale accent popup for the last typed letter
const bool ok = txtPasteInto(ta);
if (g_lv.task) {
g_lv.task->showAlert(ok ? TR("Pasted") : TR("Clipboard empty"), ok ? 900 : 1500);
g_lv.task->noteUserInput();
}
}
#endif
static void updatePagerAltBackspaceChord() {
if (!pagerKeyboardConsumeAltBackspaceChord()) return;
// The accent/@-mention pickers live on lv_layer_top(), outside the tab
@@ -74276,6 +74307,9 @@ void UITask::loop() {
pagerKeyboardDiscardAlt();
pagerKeyboardConsumeAltShiftChord();
pagerKeyboardConsumeAltBackspaceChord();
#if !defined(HAS_TDECK_PRO)
pagerKeyboardConsumeAltVChord();
#endif
#if defined(HAS_TDECK_MAX)
pagerKeyboardConsumeBothShiftChord();
#endif
@@ -74301,6 +74335,9 @@ void UITask::loop() {
pagerKeyboardDiscardAlt();
pagerKeyboardConsumeAltShiftChord();
pagerKeyboardConsumeAltBackspaceChord();
#if !defined(HAS_TDECK_PRO)
pagerKeyboardConsumeAltVChord();
#endif
#if defined(HAS_TDECK_MAX)
pagerKeyboardConsumeBothShiftChord();
#endif
@@ -74339,6 +74376,9 @@ void UITask::loop() {
}
updatePagerAltShiftChord();
updatePagerAltBackspaceChord();
#if !defined(HAS_TDECK_PRO)
updatePagerAltVChord();
#endif
#if defined(HAS_TDECK_PRO) && !defined(HAS_TDECK_MAX)
updateProAltBChord();
#endif
+24
View File
@@ -63,6 +63,30 @@ int main() {
assert(keyboard.event(0, true, 740) == 'q');
keyboard.event(0, false, 750);
// Physical Fn+V = paste chord: V is swallowed, the chord is reported once,
// and nothing leaks into the next plain keypress.
keyboard.event(PagerKeyboardState::ALT_POS, true, 760);
assert(keyboard.event(PagerKeyboardState::V_POS, true, 770) == 0);
assert(keyboard.consumeAltVChord());
assert(!keyboard.consumeAltVChord());
keyboard.event(PagerKeyboardState::V_POS, false, 780);
keyboard.event(PagerKeyboardState::ALT_POS, false, 790); // used-while-held: no one-shot armed
assert(keyboard.event(0, true, 792) == 'q');
keyboard.event(0, false, 794);
// Tap Fn, then V (one-shot symbol layer) still types '?' -- not a chord.
keyboard.event(PagerKeyboardState::ALT_POS, true, 795);
keyboard.event(PagerKeyboardState::ALT_POS, false, 796);
assert(keyboard.event(PagerKeyboardState::V_POS, true, 797) == '?');
assert(!keyboard.consumeAltVChord());
keyboard.event(PagerKeyboardState::V_POS, false, 798);
// discardAlt() drops a pending chord too.
keyboard.event(PagerKeyboardState::ALT_POS, true, 799);
keyboard.event(PagerKeyboardState::V_POS, true, 799);
keyboard.discardAlt();
assert(!keyboard.consumeAltVChord());
keyboard.event(PagerKeyboardState::V_POS, false, 799);
keyboard.event(PagerKeyboardState::ALT_POS, false, 799);
keyboard.event(PagerKeyboardState::ALT_POS, true, 800);
keyboard.discardAlt();
keyboard.event(PagerKeyboardState::ALT_POS, false, 810);