From b05dad33863b84339e96b2fce38bbeff4193b3d9 Mon Sep 17 00:00:00 2001 From: Roman Belyakovsky Date: Tue, 28 Apr 2026 23:51:12 +0300 Subject: [PATCH] Adding helper info to the menu view --- .../system/hid_app/views/hid_ptt_menu.c | 197 ++++++++++++++---- 1 file changed, 160 insertions(+), 37 deletions(-) diff --git a/applications/system/hid_app/views/hid_ptt_menu.c b/applications/system/hid_app/views/hid_ptt_menu.c index 439ca4732..6c8b1fa65 100644 --- a/applications/system/hid_app/views/hid_ptt_menu.c +++ b/applications/system/hid_app/views/hid_ptt_menu.c @@ -2,16 +2,22 @@ #include "hid_ptt.h" #include #include +#include #include "../hid.h" #include "../views.h" +#include "hid_icons.h" + #define TAG "HidPushToTalkMenu" +#define PTT_MENU_HELP_HINT_DELAY_MS 5000U +#define PTT_MENU_HINT_TIMER_PERIOD_MS 150U struct HidPushToTalkMenu { View* view; Hid* hid; - PushToTalkMenuLongOkCallback long_ok_callback; - void* long_ok_callback_context; + PushToTalkMenuLongOkCallback long_ok_callback; + void* long_ok_callback_context; + FuriTimer* hint_timer; }; typedef struct { @@ -62,8 +68,71 @@ typedef struct { size_t window_position; PushToTalkMenuList* lists; int lists_count; + uint32_t last_interaction_tick; + size_t hint_list_position; + size_t hint_item_position; + bool hint_visible; + uint16_t hint_scroll_tick; } HidPushToTalkMenuModel; +static void hid_ptt_menu_mark_interaction(HidPushToTalkMenu* hid_ptt_menu) { + with_view_model( + hid_ptt_menu->view, + HidPushToTalkMenuModel * model, + { + model->last_interaction_tick = furi_get_tick(); + model->hint_list_position = model->list_position; + model->hint_item_position = model->position; + model->hint_visible = false; + model->hint_scroll_tick = 0; + }, + true); +} + +static void hid_ptt_menu_hint_timer_callback(void* context) { + furi_assert(context); + HidPushToTalkMenu* hid_ptt_menu = context; + with_view_model( + hid_ptt_menu->view, + HidPushToTalkMenuModel * model, + { + const uint32_t now = furi_get_tick(); + const bool selection_changed = + (model->list_position != model->hint_list_position) || + (model->position != model->hint_item_position); + + if(selection_changed) { + model->hint_list_position = model->list_position; + model->hint_item_position = model->position; + model->last_interaction_tick = now; + model->hint_visible = false; + model->hint_scroll_tick = 0; + } else if(!model->hint_visible) { + if((now - model->last_interaction_tick) >= PTT_MENU_HELP_HINT_DELAY_MS) { + model->hint_visible = true; + model->hint_scroll_tick = 0; + } + } else { + model->hint_scroll_tick++; + } + }, + true); +} + +static void hid_ptt_menu_enter_callback(void* context) { + furi_assert(context); + HidPushToTalkMenu* hid_ptt_menu = context; + hid_ptt_menu_mark_interaction(hid_ptt_menu); + furi_timer_start( + hid_ptt_menu->hint_timer, furi_ms_to_ticks(PTT_MENU_HINT_TIMER_PERIOD_MS)); +} + +static void hid_ptt_menu_exit_callback(void* context) { + furi_assert(context); + HidPushToTalkMenu* hid_ptt_menu = context; + furi_timer_stop(hid_ptt_menu->hint_timer); +} + static void hid_ptt_menu_draw_list(Canvas* canvas, void* context, const PushToTalkMenuItemArray_t items) { furi_assert(context); @@ -96,7 +165,12 @@ static void FuriString* disp_str; disp_str = furi_string_alloc_set(PushToTalkMenuItemArray_cref(it)->label); - elements_string_fit_width(canvas, disp_str, item_width - (6 * 2)); + size_t item_text_width = item_width - (6 * 2); + if((position == model->position) && model->hint_visible) { + // Reserve space for hint near selected item. + item_text_width = item_width / 2; + } + elements_string_fit_width(canvas, disp_str, item_text_width); canvas_draw_str( canvas, @@ -104,6 +178,38 @@ static void y_offset + (item_position * item_height) + item_height - 4, furi_string_get_cstr(disp_str)); + if((position == model->position) && model->hint_visible) { + const char* hint_prefix = "Long press"; + const char* hint_suffix = "for help"; + const int32_t text_y = y_offset + (item_position * item_height) + item_height - 4; + const int32_t icon_y = text_y - 8; + const size_t selected_text_w = canvas_string_width(canvas, furi_string_get_cstr(disp_str)); + const int32_t hint_start_x = 6 + selected_text_w + 3; + const int32_t row_right_x = item_width - 2; + + if(hint_start_x < row_right_x) { + const size_t prefix_w = canvas_string_width(canvas, hint_prefix); + const size_t icon_w = 9; + const size_t suffix_w = canvas_string_width(canvas, hint_suffix); + const size_t hint_w = prefix_w + 2 + icon_w + 2 + suffix_w; + const size_t available = row_right_x - hint_start_x; + + int32_t scroll_offset = 0; + if(hint_w > available) { + const size_t overflow = hint_w - available; + const size_t cycle = overflow * 2; + const size_t step = cycle ? (model->hint_scroll_tick % cycle) : 0; + scroll_offset = (step <= overflow) ? step : (cycle - step); + } + + const int32_t draw_x = hint_start_x - scroll_offset; + canvas_draw_str(canvas, draw_x, text_y, hint_prefix); + const int32_t icon_x = draw_x + prefix_w + 2; + canvas_draw_icon(canvas, icon_x, icon_y, &I_Ok_btn_9x9); + canvas_draw_str(canvas, icon_x + icon_w + 2, text_y, hint_suffix); + } + } + furi_string_free(disp_str); } @@ -340,38 +446,38 @@ void ptt_menu_process_ok(HidPushToTalkMenu* hid_ptt_menu) { } } - void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) { - PushToTalkMenuList* list = NULL; - PushToTalkMenuItem* item = NULL; - with_view_model( - hid_ptt_menu->view, - HidPushToTalkMenuModel * model, - { - list = &model->lists[model->list_position]; - const size_t items_size = PushToTalkMenuItemArray_size(list->items); - if(model->position < items_size) { - item = PushToTalkMenuItemArray_get(list->items, model->position); - } - }, - false); - if(item && list && hid_ptt_menu->long_ok_callback) { - hid_ptt_menu->long_ok_callback( - hid_ptt_menu->long_ok_callback_context, - list->index, - list->label, - item->index, - item->label); - } +void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) { + PushToTalkMenuList* list = NULL; + PushToTalkMenuItem* item = NULL; + with_view_model( + hid_ptt_menu->view, + HidPushToTalkMenuModel * model, + { + list = &model->lists[model->list_position]; + const size_t items_size = PushToTalkMenuItemArray_size(list->items); + if(model->position < items_size) { + item = PushToTalkMenuItemArray_get(list->items, model->position); + } + }, + false); + if(item && list && hid_ptt_menu->long_ok_callback) { + hid_ptt_menu->long_ok_callback( + hid_ptt_menu->long_ok_callback_context, + list->index, + list->label, + item->index, + item->label); } +} - void ptt_menu_set_long_ok_callback( - HidPushToTalkMenu* hid_ptt_menu, - PushToTalkMenuLongOkCallback callback, - void* callback_context) { - furi_assert(hid_ptt_menu); - hid_ptt_menu->long_ok_callback = callback; - hid_ptt_menu->long_ok_callback_context = callback_context; - } +void ptt_menu_set_long_ok_callback( + HidPushToTalkMenu* hid_ptt_menu, + PushToTalkMenuLongOkCallback callback, + void* callback_context) { + furi_assert(hid_ptt_menu); + hid_ptt_menu->long_ok_callback = callback; + hid_ptt_menu->long_ok_callback_context = callback_context; +} static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) { furi_assert(context); @@ -410,10 +516,15 @@ static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) { consumed = true; ptt_menu_process_down(hid_ptt_menu); } - } else if(event->type == InputTypeLong && event->key == InputKeyOk) { - consumed = true; - ptt_menu_process_long_ok(hid_ptt_menu); - } + } else if(event->type == InputTypeLong && event->key == InputKeyOk) { + consumed = true; + ptt_menu_process_long_ok(hid_ptt_menu); + } + + if(event->type != InputTypeRelease) { + hid_ptt_menu_mark_interaction(hid_ptt_menu); + } + return consumed; } @@ -430,6 +541,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) { view_allocate_model(hid_ptt_menu->view, ViewModelTypeLocking, sizeof(HidPushToTalkMenuModel)); view_set_draw_callback(hid_ptt_menu->view, hid_ptt_menu_draw_callback); view_set_input_callback(hid_ptt_menu->view, hid_ptt_menu_input_callback); + view_set_enter_callback(hid_ptt_menu->view, hid_ptt_menu_enter_callback); + view_set_exit_callback(hid_ptt_menu->view, hid_ptt_menu_exit_callback); + + hid_ptt_menu->hint_timer = + furi_timer_alloc(hid_ptt_menu_hint_timer_callback, FuriTimerTypePeriodic, hid_ptt_menu); with_view_model( hid_ptt_menu->view, @@ -438,6 +554,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) { model->lists_count = 0; model->position = 0; model->window_position = 0; + model->last_interaction_tick = furi_get_tick(); + model->hint_list_position = 0; + model->hint_item_position = 0; + model->hint_visible = false; + model->hint_scroll_tick = 0; }, true); return hid_ptt_menu; @@ -445,6 +566,8 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) { void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu) { furi_assert(hid_ptt_menu); + furi_timer_stop(hid_ptt_menu->hint_timer); + furi_timer_free(hid_ptt_menu->hint_timer); with_view_model( hid_ptt_menu->view, HidPushToTalkMenuModel * model,