From c360850773052093b38ced6ef4096f4f8a68f957 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:56:43 +1000 Subject: [PATCH] T-Watch = contacts screen overlay enabling favouriting/deleting on long press. Favourites toaster popup confirmation 1-3 seconds after favourite enabled --- examples/companion_radio/main.cpp | 109 +++++++++++++----- .../companion_radio/ui-new/ContactsScreen.h | 66 +++++++++++ 2 files changed, 146 insertions(+), 29 deletions(-) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index e09033ee..216adb95 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1581,6 +1581,81 @@ static void lastHeardToggleContact() { if (ui_task.isOnContactsScreen()) { ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen(); if (cs) { +#if defined(MECK_TWATCH) + if (cs->isActionOverlayOpen()) { + int btn = cs->actionOverlayButtonAtVY(vy); + if (btn == 0) { + // Open -- route by contact type (mirrors the pre-overlay long-press open) + cs->closeActionOverlay(); + int idx = cs->getSelectedContactIdx(); + uint8_t ctype = cs->getSelectedContactType(); + if (idx >= 0 && ctype == ADV_TYPE_CHAT) { + if (ui_task.hasDMUnread(idx)) { + char cname[32]; + cs->getSelectedContactName(cname, sizeof(cname)); + ui_task.clearDMUnread(idx); + ui_task.gotoDMConversation(cname); + return 0; + } + ui_task.openTWatchKeyboard(1, idx); // TWKB_DM + return 0; + } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { + ui_task.gotoRepeaterAdmin(idx); + return 0; + } else if (idx >= 0 && ctype == ADV_TYPE_ROOM) { + ui_task.gotoRepeaterAdmin(idx); + return 0; + } else if (idx >= 0 && ui_task.hasDMUnread(idx)) { + char cname[32]; + cs->getSelectedContactName(cname, sizeof(cname)); + ui_task.clearDMUnread(idx); + ui_task.gotoDMConversation(cname); + return 0; + } + ui_task.forceRefresh(); + return 0; + } else if (btn == 1) { + // Favourite -- flip flags bit 0 on the selected contact and persist + int idx = cs->getSelectedContactIdx(); + if (idx >= 0) { + ContactInfo tmp; + if (the_mesh.getContactByIdx(idx, tmp)) { + ContactInfo* cp = the_mesh.lookupContactByPubKey(tmp.id.pub_key, PUB_KEY_SIZE); + if (cp) { + cp->flags ^= 0x01; + the_mesh.saveContacts(); + ui_task.showAlert((cp->flags & 0x01) ? "Favourited" : "Unfavourited", 1500); + } + } + } + cs->closeActionOverlay(); + cs->invalidateCache(); + ui_task.forceRefresh(); + return 0; + } else if (btn == 2) { + // Delete -- remove the selected contact and persist + int idx = cs->getSelectedContactIdx(); + if (idx >= 0) { + ContactInfo c; + if (the_mesh.getContactByIdx(idx, c)) { + if (the_mesh.removeContact(c)) { + the_mesh.saveContacts(); + ui_task.showAlert("Contact deleted", 1500); + } + } + } + cs->closeActionOverlay(); + cs->invalidateCache(); + ui_task.forceRefresh(); + return 0; + } else { + // Tap outside the buttons -- dismiss + cs->closeActionOverlay(); + ui_task.forceRefresh(); + return 0; + } + } +#endif int result = cs->selectRowAtVY(vy); if (result == 1) { ui_task.forceRefresh(); @@ -2044,35 +2119,11 @@ static void lastHeardToggleContact() { } return 0; #elif defined(MECK_TWATCH) - // Watch: long press = DM (tw-keyboard) / admin / room action. - // purpose ints match TWatchKeyboardScreen::Purpose: DM=1. - { - int idx = cs->getSelectedContactIdx(); - uint8_t ctype = cs->getSelectedContactType(); - if (idx >= 0 && ctype == ADV_TYPE_CHAT) { - if (ui_task.hasDMUnread(idx)) { - char cname[32]; - cs->getSelectedContactName(cname, sizeof(cname)); - ui_task.clearDMUnread(idx); - ui_task.gotoDMConversation(cname); - return 0; - } - ui_task.openTWatchKeyboard(1, idx); // TWKB_DM - return 0; - } else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) { - ui_task.gotoRepeaterAdmin(idx); - return 0; - } else if (idx >= 0 && ctype == ADV_TYPE_ROOM) { - ui_task.gotoRepeaterAdmin(idx); - return 0; - } else if (idx >= 0 && ui_task.hasDMUnread(idx)) { - char cname[32]; - cs->getSelectedContactName(cname, sizeof(cname)); - ui_task.clearDMUnread(idx); - ui_task.gotoDMConversation(cname); - return 0; - } - } + // Watch: long press raises the contact action overlay + // ([Open] [Favourite] [Delete]). The tap handler dispatches the choice; + // "Open" reproduces the type-aware DM/admin routing this branch used to do. + cs->openActionOverlay(); + ui_task.forceRefresh(); return 0; #else // T-Deck Pro: long press enters select mode diff --git a/examples/companion_radio/ui-new/ContactsScreen.h b/examples/companion_radio/ui-new/ContactsScreen.h index 8ddab3df..8efe8cbe 100644 --- a/examples/companion_radio/ui-new/ContactsScreen.h +++ b/examples/companion_radio/ui-new/ContactsScreen.h @@ -4,6 +4,10 @@ #include #include +#if defined(MECK_TWATCH) +#include // blue rounded-rect overlay primitives (watch LCD) +#endif + // Timestamps before this (Jan 1 2026 UTC) are treated as invalid/unsynced #define EPOCH_2026 1735689600UL @@ -53,6 +57,18 @@ private: bool _selectMode; uint8_t* _selectedBits; // Bitfield: 1 bit per MAX_CONTACTS raw index +#if defined(MECK_TWATCH) + // --- Action overlay state (watch) --- + // Long-press raises a small [Open] [Favourite] [Delete] overlay. Buttons are + // three full-width bands defined in the 128-space touch grid; render() maps + // them to the 120-space display via display.height(). + bool _actionOverlay = false; + static const int OVL_BTN_H = 26; + static const int OVL_BTN0_TOP = 21; // (128 - (3*26 + 2*4)) / 2 + static const int OVL_BTN1_TOP = 51; // 21 + 26 + 4 + static const int OVL_BTN2_TOP = 81; // 51 + 26 + 4 +#endif + // --- helpers --- static const char* filterLabel(FilterMode f) { @@ -180,6 +196,9 @@ public: void resetScroll() { _scrollPos = 0; _cacheValid = false; +#if defined(MECK_TWATCH) + _actionOverlay = false; // never carry a stale overlay across screen entry +#endif } FilterMode getFilter() const { return _filter; } @@ -288,6 +307,22 @@ public: return true; } +#if defined(MECK_TWATCH) + // --- Action overlay (watch) --- + void openActionOverlay() { _actionOverlay = true; } + void closeActionOverlay() { _actionOverlay = false; } + bool isActionOverlayOpen() const { return _actionOverlay; } + + // Hit-test a 128-space touch Y against the overlay buttons. + // Returns 0=Open, 1=Favourite, 2=Delete, -1=miss (tap outside dismisses). + int actionOverlayButtonAtVY(int vy) const { + if (vy >= OVL_BTN0_TOP && vy < OVL_BTN0_TOP + OVL_BTN_H) return 0; + if (vy >= OVL_BTN1_TOP && vy < OVL_BTN1_TOP + OVL_BTN_H) return 1; + if (vy >= OVL_BTN2_TOP && vy < OVL_BTN2_TOP + OVL_BTN_H) return 2; + return -1; + } +#endif + int render(DisplayDriver& display) override { if (!_cacheValid) rebuildCache(); @@ -500,6 +535,37 @@ public: } #endif +#if defined(MECK_TWATCH) + // Action overlay -- drawn last so it sits on top of the contact list. + if (_actionOverlay) { + const int H = display.height(); // 120 on the watch + const int W = display.width(); // 120 + int y0 = OVL_BTN0_TOP * H / 128; + int y1 = OVL_BTN1_TOP * H / 128; + int y2 = OVL_BTN2_TOP * H / 128; + int bh = OVL_BTN_H * H / 128; + int bx = 6; + int bw = W - 12; + + // Dark backdrop to hide the contact list behind the overlay + display.setColor(DisplayDriver::DARK); + display.fillRect(bx - 2, y0 - 4, bw + 4, (y2 + bh) - y0 + 8); + + // Blue rounded buttons with white centred labels, matching the watch's + // Notes/config style (0x231D = the home-tile blue, 0xFFFF = white). + LGFXDisplay* d = (LGFXDisplay*)&display; + d->setRawColor(0x231D); + d->drawRoundRect(bx, y0, bw, bh, 3); + d->drawRoundRect(bx, y1, bw, bh, 3); + d->drawRoundRect(bx, y2, bw, bh, 3); + + d->setRawColor(0xFFFF); + display.drawTextCentered(W / 2, y0 + (bh - 8) / 2, "Open"); + display.drawTextCentered(W / 2, y1 + (bh - 8) / 2, "Favourite"); + display.drawTextCentered(W / 2, y2 + (bh - 8) / 2, "Delete"); + } +#endif + return 5000; // e-ink: next render after 5s }