diff --git a/THINKNODE_M9_SHORTCUTS.md b/THINKNODE_M9_SHORTCUTS.md index 1d25933..874bb71 100644 --- a/THINKNODE_M9_SHORTCUTS.md +++ b/THINKNODE_M9_SHORTCUTS.md @@ -36,6 +36,9 @@ because that is the root. **HOME** is the shortcut past all of that: it goes straight to the root and clears the trail, so Back from there won't jump you back out again. +When **App drawer as home** is enabled, the drawer cog's **Lock home to drawer** +option makes HOME stay in or return to the drawer. The **Cmdr** tile still +opens the Commander screen when you need it. The only exception is deliberate: progress overlays (SD format, bulk delete) block all keys until the operation finishes — Back will not navigate out from diff --git a/src/helpers/esp32/TouchPrefsSchema.h b/src/helpers/esp32/TouchPrefsSchema.h index 44b3854..257d511 100644 --- a/src/helpers/esp32/TouchPrefsSchema.h +++ b/src/helpers/esp32/TouchPrefsSchema.h @@ -9,7 +9,7 @@ namespace TouchPrefsSchema { static constexpr uint16_t MAGIC = 0x5743; // 'WC' (WadaCfg) -static constexpr uint8_t CURRENT_VERSION = 59; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423); v59: telem_loc_exact (answer position requests from chosen contacts with the REAL fix instead of the advert displacement; 0 = keep it displaced) +static constexpr uint8_t CURRENT_VERSION = 60; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423); v59: telem_loc_exact (answer position requests from chosen contacts with the REAL fix instead of the advert displacement; 0 = keep it displaced); v60: home_key_keeps_drawer (#491; default OFF) static constexpr uint8_t BROKEN_MID_INSERT_VERSION = 44; // Persisted byte layout. New fields must be appended at the end: older blobs @@ -116,6 +116,7 @@ struct __attribute__((packed)) Config { // you already picked, not a broadcast, so applying the advert displacement to it is // arguably too blunt (honza_87628). OFF keeps the displacement, matching the advert. uint8_t telem_loc_exact; + uint8_t home_key_keeps_drawer; // v60: M9 Home key stays in/returns to the app drawer }; static constexpr size_t HEADER_SIZE = offsetof(Config, bright); @@ -128,10 +129,10 @@ static_assert(offsetof(Config, web_mirror) == offsetof(Config, rx_queue) + sizeo // rejection is silent: cfgFlush just returns false and every preference stops // persisting, with nothing in the log to say why. The struct grows most releases, // so guard the ceiling here rather than discover it as "settings do not save" on -// whichever board is using the file backend. 117 bytes today. +// whichever board is using the file backend. 121 bytes today. static_assert(sizeof(Config) <= 2048, "Config exceeds the SdNvsPrefs value cap; prefs would silently stop saving"); -static_assert(offsetof(Config, telem_loc_exact) + sizeof(Config::telem_loc_exact) == sizeof(Config), +static_assert(offsetof(Config, home_key_keeps_drawer) + sizeof(Config::home_key_keeps_drawer) == sizeof(Config), "new preference fields must remain trailing"); // lang_file must stay immediately before the tail, or a v48-era blob overlays // onto the wrong bytes. Checking both ends means the next person to append is @@ -169,6 +170,9 @@ static_assert(offsetof(Config, attaky_notify_dm_color) == static_assert(offsetof(Config, telem_loc_exact) == offsetof(Config, attaky_notify_dm_color) + sizeof(Config::attaky_notify_dm_color), "telem_loc_exact must follow attaky_notify_dm_color"); +static_assert(offsetof(Config, home_key_keeps_drawer) == + offsetof(Config, telem_loc_exact) + sizeof(Config::telem_loc_exact), + "home_key_keeps_drawer must follow telem_loc_exact"); // Overlay a persisted blob on caller-provided defaults. Beta 57 wrote v44 with // retry_echo inserted before web_mirror, shifting every later value. That blob diff --git a/src/helpers/esp32/TouchPrefsStore.cpp b/src/helpers/esp32/TouchPrefsStore.cpp index 7ade76b..3c4f6d7 100644 --- a/src/helpers/esp32/TouchPrefsStore.cpp +++ b/src/helpers/esp32/TouchPrefsStore.cpp @@ -149,6 +149,7 @@ static void cfgSetDefaults(TouchCfg& c) { c.attaky_notify_room_color = 0; // red c.attaky_notify_dm_color = 1; // green c.compact_chat = 0; // OFF: bubble chat layout (opt-in IRC-style dense rows) + c.home_key_keeps_drawer = 0; // OFF: preserve Home-key Commander/drawer toggle c.clock_floor = 0; // no persisted send-timestamp floor yet c.rx_queue = 1; // ON: buffered receive (test-channel default; opt-out toggle in Radio & Mesh) c.retry_echo = 0; // OFF: auto-retry is opt-in (toggle in Radio & Mesh) @@ -264,6 +265,7 @@ static void cfgLoadOrMigrate() { if (stored_version < 56) { s_cfg.theme_mode = 0; } // Night: unchanged appearance if (stored_version < 57) { s_cfg.gps_fuzz_m = 0; } // OFF: real position if (stored_version < 59) { s_cfg.telem_loc_exact = 0; } // OFF: answers stay displaced + if (stored_version < 60) { s_cfg.home_key_keeps_drawer = 0; } // preserve Home-key toggle if (stored_version < 58) { s_cfg.attaky_notify_enabled = 0; s_cfg.attaky_notify_room_color = 0; @@ -1398,6 +1400,16 @@ bool touchPrefsSetHomeIsDrawer(bool on) { return cfgFlush(); } +bool touchPrefsGetHomeKeyKeepsDrawer() { + if (!s_begun) touchPrefsBegin(); + return s_cfg.home_key_keeps_drawer != 0; +} +bool touchPrefsSetHomeKeyKeepsDrawer(bool on) { + if (!s_begun) touchPrefsBegin(); + s_cfg.home_key_keeps_drawer = on ? 1 : 0; + return cfgFlush(); +} + // Store ALL device data (identity, prefs, contacts, channels) on the SD card // under /meshcomod instead of internal SPIFFS. Read at boot (main.cpp) BEFORE // the data loads, so changing it needs a reboot. Key "use_sd" in the "touch" diff --git a/src/helpers/esp32/TouchPrefsStore.h b/src/helpers/esp32/TouchPrefsStore.h index 8e791cd..2f9fab6 100644 --- a/src/helpers/esp32/TouchPrefsStore.h +++ b/src/helpers/esp32/TouchPrefsStore.h @@ -289,6 +289,11 @@ bool touchPrefsSetNavDirKey(int idx, uint8_t ch); bool touchPrefsGetHomeIsDrawer(); bool touchPrefsSetHomeIsDrawer(bool on); +/* M9 Home-key behavior: when true and the app drawer is configured as Home, + * pressing Home stays in or returns to the drawer instead of toggling Commander. */ +bool touchPrefsGetHomeKeyKeepsDrawer(); +bool touchPrefsSetHomeKeyKeepsDrawer(bool on); + /** Hide the device/profile name in the status bar and move the clock to the * left where the name used to be. Default false (name shown). */ bool touchPrefsGetHideNodeName(); diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 401dbf6..e57ba08 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -41797,12 +41797,15 @@ static bool m9HandleNavKey(int key) { } if (getActiveTab() == HOME_TAB_INDEX) { const bool was_open = s_home_drawer_mode; // read BEFORE dismissing anything - // Stop on a key-blocker row (SD format / bulk delete progress) instead of - // spinning eight times closing nothing and then toggling the drawer out - // from under a running operation. - for (int i = 0; i < 8 && anyPopupOpen(); i++) { if (!hwKeyDismissTopPopup()) break; } - if (anyPopupOpen()) { s_nav_show = true; if (g_lv.task) g_lv.task->noteUserInput(); return true; } - setHomeDrawer(!was_open); // decide from the snapshot, not the now-mutated flag + const bool keep_drawer = s_home_is_drawer && touchPrefsGetHomeKeyKeepsDrawer(); + if (!(keep_drawer && was_open && s_appdrawer_root && !appDrawerCovered())) { + // Stop on a key-blocker row (SD format / bulk delete progress) instead of + // spinning eight times closing nothing and then toggling the drawer out + // from under a running operation. + for (int i = 0; i < 8 && anyPopupOpen(); i++) { if (!hwKeyDismissTopPopup()) break; } + if (anyPopupOpen()) { s_nav_show = true; if (g_lv.task) g_lv.task->noteUserInput(); return true; } + setHomeDrawer(keep_drawer ? true : !was_open); // optional sticky drawer; otherwise preserve toggle + } } else { s_m9_map_pan = false; // leaving the Map: pan must not outlive the tab (a stale flag ate the next Back press) if (!navGoToMainTab(HOME_TAB_INDEX)) { // a blocker popup refused: nothing moved, so keep the trail @@ -46026,7 +46029,21 @@ static void appHomeIsDrawerCb(lv_event_t* e) { #endif s_home_is_drawer = on; if (on) s_home_drawer_mode = true; // make the drawer the live Home view too (persists on return + reboot) +#if defined(HAS_THINKNODE_M9) + lv_obj_t* dependent_row = static_cast(lv_event_get_user_data(e)); + if (dependent_row && lv_obj_is_valid(dependent_row)) { + if (on) lv_obj_clear_flag(dependent_row, LV_OBJ_FLAG_HIDDEN); + else lv_obj_add_flag(dependent_row, LV_OBJ_FLAG_HIDDEN); + } +#endif } +#if defined(HAS_THINKNODE_M9) +static void appHomeKeyKeepsDrawerCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + touchPrefsSetHomeKeyKeepsDrawer( + lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED)); +} +#endif static void openAppGridSheet() { closeAppGridSheet(); lv_coord_t sw = lv_disp_get_hor_res(nullptr), sh = lv_disp_get_ver_res(nullptr); @@ -46056,6 +46073,9 @@ static void openAppGridSheet() { lv_obj_remove_style_all(card); { int card_h = hdr + 2 * btn_h + gap + home_row + gap + 2 * pad; +#if defined(HAS_THINKNODE_M9) + card_h += home_row + gap; +#endif if (card_h > card_h_max) card_h = card_h_max; lv_obj_set_size(card, card_w, card_h); } @@ -46064,7 +46084,13 @@ static void openAppGridSheet() { lv_obj_set_style_bg_opa(card, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_radius(card, 8, LV_PART_MAIN); lv_obj_set_style_pad_all(card, pad, LV_PART_MAIN); +#if defined(HAS_THINKNODE_M9) + lv_obj_set_scroll_dir(card, LV_DIR_VER); + lv_obj_set_scrollbar_mode(card, LV_SCROLLBAR_MODE_AUTO); + lv_obj_add_event_cb(card, scrollClampOnEndCb, LV_EVENT_SCROLL_END, nullptr); +#else lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE); +#endif lv_obj_t* ttl = lv_label_create(card); lv_label_set_text(ttl, TR("App icon size")); @@ -46109,7 +46135,29 @@ static void openAppGridSheet() { #if defined(ESP32) if (touchPrefsGetHomeIsDrawer()) lv_obj_add_state(sw, LV_STATE_CHECKED); #endif +#if defined(HAS_THINKNODE_M9) + const int ky = ty + home_row + gap; + lv_obj_t* key_row = lv_obj_create(card); + lv_obj_remove_style_all(key_row); + lv_obj_set_size(key_row, card_w - 2 * pad, home_row); + lv_obj_set_pos(key_row, 0, ky); + lv_obj_clear_flag(key_row, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE); + lv_obj_t* kl = lv_label_create(key_row); + lv_label_set_long_mode(kl, LV_LABEL_LONG_DOT); + lv_obj_set_width(kl, lv_pct(68)); + lv_label_set_text(kl, TR("Lock home to drawer")); + lv_obj_set_style_text_font(kl, &g_font_12, LV_PART_MAIN); + lv_obj_set_style_text_color(kl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); + lv_obj_align(kl, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_t* key_sw = lv_switch_create(key_row); + lv_obj_align(key_sw, LV_ALIGN_RIGHT_MID, 0, 0); + if (touchPrefsGetHomeKeyKeepsDrawer()) lv_obj_add_state(key_sw, LV_STATE_CHECKED); + if (!touchPrefsGetHomeIsDrawer()) lv_obj_add_flag(key_row, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_event_cb(key_sw, appHomeKeyKeepsDrawerCb, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_add_event_cb(sw, appHomeIsDrawerCb, LV_EVENT_VALUE_CHANGED, key_row); +#else lv_obj_add_event_cb(sw, appHomeIsDrawerCb, LV_EVENT_VALUE_CHANGED, nullptr); +#endif } } static void appDrawerSettingsCb(lv_event_t* e) { diff --git a/test/test_touch_prefs_schema.cpp b/test/test_touch_prefs_schema.cpp index 02ea60d..92d9cf8 100644 --- a/test/test_touch_prefs_schema.cpp +++ b/test/test_touch_prefs_schema.cpp @@ -30,6 +30,7 @@ static Config safeDefaults() { c.attaky_notify_enabled = 0; c.attaky_notify_room_color = 0; c.attaky_notify_dm_color = 1; + c.home_key_keeps_drawer = 0; return c; } @@ -125,7 +126,8 @@ int main() { + sizeof(Config::gps_fuzz_m) + sizeof(Config::attaky_notify_enabled) + sizeof(Config::attaky_notify_room_color) + sizeof(Config::attaky_notify_dm_color) - + sizeof(Config::telem_loc_exact) == sizeof(Config), + + sizeof(Config::telem_loc_exact) + + sizeof(Config::home_key_keeps_drawer) == sizeof(Config), "v53 is the current layout minus every byte appended since"); Config v53 = safeDefaults(); @@ -167,6 +169,7 @@ int main() { current.attaky_notify_enabled = 1; current.attaky_notify_room_color = 6; current.attaky_notify_dm_color = 4; + current.home_key_keeps_drawer = 1; migrated = safeDefaults(); assert(TouchPrefsSchema::overlayStored(migrated, ¤t, sizeof(current), &stored_version)); assert(stored_version == TouchPrefsSchema::CURRENT_VERSION); @@ -198,7 +201,8 @@ int main() { + sizeof(Config::attaky_notify_enabled) + sizeof(Config::attaky_notify_room_color) + sizeof(Config::attaky_notify_dm_color) - + sizeof(Config::telem_loc_exact) == sizeof(Config), + + sizeof(Config::telem_loc_exact) + + sizeof(Config::home_key_keeps_drawer) == sizeof(Config), "v55 is the current layout minus every byte appended since"); Config v55 = safeDefaults(); v55.ver = 55; @@ -220,7 +224,8 @@ int main() { static_assert(v57_size + sizeof(Config::attaky_notify_enabled) + sizeof(Config::attaky_notify_room_color) + sizeof(Config::attaky_notify_dm_color) - + sizeof(Config::telem_loc_exact) == sizeof(Config), + + sizeof(Config::telem_loc_exact) + + sizeof(Config::home_key_keeps_drawer) == sizeof(Config), "v57 is the current layout minus the Attaky notification fields"); Config v57 = safeDefaults(); v57.ver = 57; @@ -236,6 +241,22 @@ int main() { assert(migrated.attaky_notify_room_color == 0); assert(migrated.attaky_notify_dm_color == 1); + // #491 appends the M9 Home-key drawer behavior at v60. A v59 blob keeps + // every prior setting, while the new option defaults OFF to preserve the + // established Commander/drawer toggle until the user opts in. + constexpr size_t v59_size = offsetof(Config, home_key_keeps_drawer); + static_assert(v59_size + sizeof(Config::home_key_keeps_drawer) == sizeof(Config), + "v59 is the current layout minus the Home-key drawer option"); + Config v59 = safeDefaults(); + v59.ver = 59; + v59.telem_loc_exact = 1; + v59.home_key_keeps_drawer = 1; // outside the stored v59 extent + migrated = safeDefaults(); + assert(TouchPrefsSchema::overlayStored(migrated, &v59, v59_size, &stored_version)); + assert(stored_version == 59); + assert(migrated.telem_loc_exact == 1); + assert(migrated.home_key_keeps_drawer == 0); + Config invalid = safeDefaults(); uint8_t garbage[sizeof(Config)] = {}; assert(!TouchPrefsSchema::overlayStored(invalid, garbage, sizeof(garbage)));