hid_ptt_menu.c 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. #include "hid_ptt_menu.h"
  2. #include "hid_ptt.h"
  3. #include <gui/elements.h>
  4. #include <m-array.h>
  5. #include "../hid.h"
  6. #include "../views.h"
  7. #define TAG "HidPushToTalkMenu"
  8. struct HidPushToTalkMenu {
  9. View* view;
  10. Hid* hid;
  11. };
  12. typedef struct {
  13. FuriString* label;
  14. uint32_t index;
  15. PushToTalkMenuItemCallback callback;
  16. void* callback_context;
  17. } PushToTalkMenuItem;
  18. // Menu item
  19. static void PushToTalkMenuItem_init(PushToTalkMenuItem* item) {
  20. item->label = furi_string_alloc();
  21. item->index = 0;
  22. }
  23. static void PushToTalkMenuItem_init_set(PushToTalkMenuItem* item, const PushToTalkMenuItem* src) {
  24. item->label = furi_string_alloc_set(src->label);
  25. item->index = src->index;
  26. }
  27. static void PushToTalkMenuItem_set(PushToTalkMenuItem* item, const PushToTalkMenuItem* src) {
  28. furi_string_set(item->label, src->label);
  29. item->index = src->index;
  30. }
  31. static void PushToTalkMenuItem_clear(PushToTalkMenuItem* item) {
  32. furi_string_free(item->label);
  33. }
  34. ARRAY_DEF(
  35. PushToTalkMenuItemArray,
  36. PushToTalkMenuItem,
  37. (INIT(API_2(PushToTalkMenuItem_init)),
  38. SET(API_6(PushToTalkMenuItem_set)),
  39. INIT_SET(API_6(PushToTalkMenuItem_init_set)),
  40. CLEAR(API_2(PushToTalkMenuItem_clear))))
  41. // Menu list (horisontal, 2d array)
  42. typedef struct {
  43. FuriString* label;
  44. uint32_t index;
  45. PushToTalkMenuItemArray_t items;
  46. } PushToTalkMenuList;
  47. typedef struct {
  48. size_t list_position;
  49. size_t position;
  50. size_t window_position;
  51. PushToTalkMenuList *lists;
  52. int lists_count;
  53. } HidPushToTalkMenuModel;
  54. static void hid_ptt_menu_draw_list(Canvas* canvas, void* context, const PushToTalkMenuItemArray_t items) {
  55. furi_assert(context);
  56. HidPushToTalkMenuModel* model = context;
  57. const uint8_t item_height = 16;
  58. uint8_t item_width = canvas_width(canvas) - 5;
  59. canvas_set_font(canvas, FontSecondary);
  60. size_t position = 0;
  61. PushToTalkMenuItemArray_it_t it;
  62. for(PushToTalkMenuItemArray_it(it, items); !PushToTalkMenuItemArray_end_p(it); PushToTalkMenuItemArray_next(it)) {
  63. const size_t item_position = position - model->window_position;
  64. const size_t items_on_screen = 3;
  65. uint8_t y_offset = 16;
  66. if(item_position < items_on_screen) {
  67. if(position == model->position) {
  68. canvas_set_color(canvas, ColorBlack);
  69. elements_slightly_rounded_box(
  70. canvas,
  71. 0,
  72. y_offset + (item_position * item_height) + 1,
  73. item_width,
  74. item_height - 2);
  75. canvas_set_color(canvas, ColorWhite);
  76. } else {
  77. canvas_set_color(canvas, ColorBlack);
  78. }
  79. FuriString* disp_str;
  80. disp_str = furi_string_alloc_set(PushToTalkMenuItemArray_cref(it)->label);
  81. elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
  82. canvas_draw_str(
  83. canvas,
  84. 6,
  85. y_offset + (item_position * item_height) + item_height - 4,
  86. furi_string_get_cstr(disp_str));
  87. furi_string_free(disp_str);
  88. }
  89. position++;
  90. }
  91. elements_scrollbar_pos(canvas, 128 , 17, 46, model->position, PushToTalkMenuItemArray_size(items));
  92. }
  93. PushToTalkMenuList * hid_ptt_menu_get_list_at_index(
  94. void* context,
  95. uint32_t index) {
  96. furi_assert(context);
  97. HidPushToTalkMenuModel* model = context;
  98. for (int i = 0; i < model->lists_count; i++) {
  99. PushToTalkMenuList* list = &model->lists[i];
  100. if(index == list->index) {
  101. return list;
  102. }
  103. }
  104. return NULL;
  105. }
  106. static void hid_ptt_menu_draw_callback(Canvas* canvas, void* context) {
  107. furi_assert(context);
  108. HidPushToTalkMenuModel* model = context;
  109. if (model->lists_count == 0){
  110. return;
  111. }
  112. uint8_t item_width = canvas_width(canvas) - 5;
  113. canvas_clear(canvas);
  114. canvas_set_font(canvas, FontPrimary);
  115. canvas_draw_str(canvas, 4, 11, "<");
  116. canvas_draw_str(canvas, 121, 11, ">");
  117. PushToTalkMenuList* list = &model->lists[model->list_position];
  118. FuriString* disp_str;
  119. disp_str = furi_string_alloc_set(list->label);
  120. elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
  121. uint8_t x_pos = (canvas_width(canvas) - canvas_string_width(canvas,furi_string_get_cstr(disp_str))) / 2;
  122. canvas_draw_str(canvas,x_pos,11,furi_string_get_cstr(disp_str));
  123. furi_string_free(disp_str);
  124. canvas_set_font(canvas, FontSecondary);
  125. hid_ptt_menu_draw_list(
  126. canvas,
  127. context,
  128. list->items
  129. );
  130. }
  131. void ptt_menu_add_list(
  132. HidPushToTalkMenu* hid_ptt_menu,
  133. const char* label,
  134. uint32_t index) {
  135. furi_assert(label);
  136. furi_assert(hid_ptt_menu);
  137. with_view_model(
  138. hid_ptt_menu->view, HidPushToTalkMenuModel * model,
  139. {
  140. if (model->lists_count == 0) {
  141. model->lists = (PushToTalkMenuList *)malloc(sizeof(PushToTalkMenuList));
  142. } else {
  143. model->lists = (PushToTalkMenuList *)realloc(model->lists, (model->lists_count + 1) * sizeof(PushToTalkMenuList));
  144. }
  145. if (model->lists == NULL) {
  146. FURI_LOG_E(TAG, "Memory reallocation failed (%i)", model->lists_count);
  147. return;
  148. }
  149. PushToTalkMenuList* list = &model->lists[model->lists_count];
  150. PushToTalkMenuItemArray_init(list->items);
  151. list->label = furi_string_alloc_set(label);
  152. list->index = index;
  153. model->lists_count += 1;
  154. },
  155. true);
  156. }
  157. void ptt_menu_add_item_to_list(
  158. HidPushToTalkMenu* hid_ptt_menu,
  159. uint32_t list_index,
  160. const char* label,
  161. uint32_t index,
  162. PushToTalkMenuItemCallback callback,
  163. void* callback_context) {
  164. PushToTalkMenuItem* item = NULL;
  165. furi_assert(label);
  166. furi_assert(hid_ptt_menu);
  167. UNUSED(list_index);
  168. with_view_model(
  169. hid_ptt_menu->view, HidPushToTalkMenuModel * model,
  170. {
  171. PushToTalkMenuList* list = hid_ptt_menu_get_list_at_index(model, list_index);
  172. if (list == NULL){
  173. FURI_LOG_E(TAG, "Adding item %s to unknown index %li", label, list_index);
  174. return;
  175. }
  176. item = PushToTalkMenuItemArray_push_new(list->items);
  177. furi_string_set_str(item->label, label);
  178. item->index = index;
  179. item->callback = callback;
  180. item->callback_context = callback_context;
  181. },
  182. true);
  183. }
  184. void ptt_menu_shift_list(HidPushToTalkMenu* hid_ptt_menu, int shift){
  185. size_t new_position = 0;
  186. uint32_t index = 0;
  187. with_view_model(
  188. hid_ptt_menu->view, HidPushToTalkMenuModel * model,
  189. {
  190. int new_list_position = (short) model->list_position + shift;
  191. if (new_list_position >= model->lists_count) {
  192. new_list_position = 0;
  193. } else if (new_list_position < 0) {
  194. new_list_position = model->lists_count - 1;
  195. }
  196. PushToTalkMenuList* list = &model->lists[model->list_position];
  197. PushToTalkMenuList* new_list = &model->lists[new_list_position];
  198. size_t new_window_position = model->window_position;
  199. const size_t items_size = PushToTalkMenuItemArray_size(new_list->items);
  200. size_t position = 0;
  201. // Find item index from current list
  202. PushToTalkMenuItemArray_it_t it;
  203. for(PushToTalkMenuItemArray_it(it, list->items); !PushToTalkMenuItemArray_end_p(it); PushToTalkMenuItemArray_next(it)) {
  204. if (position == model->position){
  205. index = PushToTalkMenuItemArray_cref(it)->index;
  206. break;
  207. }
  208. position++;
  209. }
  210. // Try to find item with the same index in a new list
  211. position = 0;
  212. bool item_exists_in_new_list = false;
  213. for(PushToTalkMenuItemArray_it(it, new_list->items); !PushToTalkMenuItemArray_end_p(it); PushToTalkMenuItemArray_next(it)) {
  214. if (PushToTalkMenuItemArray_cref(it)->index == index) {
  215. item_exists_in_new_list = true;
  216. new_position = position;
  217. break;
  218. }
  219. position++;
  220. }
  221. // This list item is not presented in a new list, let's try to keep position as is.
  222. // If it's out of range for the new list set it to the end
  223. if (!item_exists_in_new_list) {
  224. new_position = items_size - 1 < model->position ? items_size - 1 : model->position;
  225. }
  226. // Tune window position. As we have 3 items on screen, keep focus centered
  227. const size_t items_on_screen = 3;
  228. if (new_position >= items_size - 1) {
  229. if (items_size < items_on_screen + 1) {
  230. new_window_position = 0;
  231. } else {
  232. new_window_position = items_size - items_on_screen;
  233. }
  234. } else if (new_position < items_on_screen - 1) {
  235. new_window_position = 0;
  236. } else {
  237. new_window_position = new_position - 1;
  238. }
  239. model->list_position = new_list_position;
  240. model->position = new_position;
  241. model->window_position = new_window_position;
  242. },
  243. true);
  244. }
  245. void ptt_menu_process_up(HidPushToTalkMenu* hid_ptt_menu) {
  246. with_view_model(
  247. hid_ptt_menu->view, HidPushToTalkMenuModel * model,
  248. {
  249. PushToTalkMenuList* list = &model->lists[model->list_position];
  250. const size_t items_on_screen = 3;
  251. const size_t items_size = PushToTalkMenuItemArray_size(list->items);
  252. if(model->position > 0) {
  253. model->position--;
  254. if((model->position == model->window_position) && (model->window_position > 0)) {
  255. model->window_position--;
  256. }
  257. } else {
  258. model->position = items_size - 1;
  259. if(model->position > items_on_screen - 1) {
  260. model->window_position = model->position - (items_on_screen - 1);
  261. }
  262. }
  263. },
  264. true);
  265. }
  266. void ptt_menu_process_down(HidPushToTalkMenu* hid_ptt_menu) {
  267. with_view_model(
  268. hid_ptt_menu->view, HidPushToTalkMenuModel * model,
  269. {
  270. PushToTalkMenuList* list = &model->lists[model->list_position];
  271. const size_t items_on_screen = 3;
  272. const size_t items_size = PushToTalkMenuItemArray_size(list->items);
  273. if(model->position < items_size - 1) {
  274. model->position++;
  275. if((model->position - model->window_position > items_on_screen - 2) &&
  276. (model->window_position < items_size - items_on_screen)) {
  277. model->window_position++;
  278. }
  279. } else {
  280. model->position = 0;
  281. model->window_position = 0;
  282. }
  283. },
  284. true);
  285. }
  286. void ptt_menu_process_ok(HidPushToTalkMenu* hid_ptt_menu) {
  287. PushToTalkMenuList* list = NULL;
  288. PushToTalkMenuItem* item = NULL;
  289. with_view_model(
  290. hid_ptt_menu->view, HidPushToTalkMenuModel * model,
  291. {
  292. list = &model->lists[model->list_position];
  293. const size_t items_size = PushToTalkMenuItemArray_size(list->items);
  294. if(model->position < items_size) {
  295. item = PushToTalkMenuItemArray_get(list->items, model->position);
  296. }
  297. },
  298. true);
  299. if(item && list && item->callback) {
  300. item->callback(item->callback_context, list->index, list->label, item->index, item->label);
  301. }
  302. }
  303. static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) {
  304. furi_assert(context);
  305. HidPushToTalkMenu* hid_ptt_menu = context;
  306. bool consumed = false;
  307. if(event->type == InputTypeShort) {
  308. switch(event->key) {
  309. case InputKeyUp:
  310. consumed = true;
  311. ptt_menu_process_up(hid_ptt_menu);
  312. break;
  313. case InputKeyDown:
  314. consumed = true;
  315. ptt_menu_process_down(hid_ptt_menu);
  316. break;
  317. case InputKeyLeft:
  318. consumed = true;
  319. ptt_menu_shift_list(hid_ptt_menu, -1);
  320. break;
  321. case InputKeyRight:
  322. consumed = true;
  323. ptt_menu_shift_list(hid_ptt_menu, +1);
  324. break;
  325. case InputKeyOk:
  326. consumed = true;
  327. ptt_menu_process_ok(hid_ptt_menu);
  328. break;
  329. default:
  330. break;
  331. }
  332. } else if(event->type == InputTypeRepeat) {
  333. if(event->key == InputKeyUp) {
  334. consumed = true;
  335. ptt_menu_process_up(hid_ptt_menu);
  336. } else if(event->key == InputKeyDown) {
  337. consumed = true;
  338. ptt_menu_process_down(hid_ptt_menu);
  339. }
  340. }
  341. return consumed;
  342. }
  343. View* hid_ptt_menu_get_view(HidPushToTalkMenu* hid_ptt_menu) {
  344. furi_assert(hid_ptt_menu);
  345. return hid_ptt_menu->view;
  346. }
  347. HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
  348. HidPushToTalkMenu* hid_ptt_menu = malloc(sizeof(HidPushToTalkMenu));
  349. hid_ptt_menu->hid = hid;
  350. hid_ptt_menu->view = view_alloc();
  351. view_set_context(hid_ptt_menu->view, hid_ptt_menu);
  352. view_allocate_model(hid_ptt_menu->view, ViewModelTypeLocking, sizeof(HidPushToTalkMenuModel));
  353. view_set_draw_callback(hid_ptt_menu->view, hid_ptt_menu_draw_callback);
  354. view_set_input_callback(hid_ptt_menu->view, hid_ptt_menu_input_callback);
  355. with_view_model(
  356. hid_ptt_menu->view, HidPushToTalkMenuModel * model, {
  357. model->lists_count = 0;
  358. model->position = 0;
  359. model->window_position = 0;
  360. }, true);
  361. return hid_ptt_menu;
  362. }
  363. void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu) {
  364. furi_assert(hid_ptt_menu);
  365. with_view_model(
  366. hid_ptt_menu->view, HidPushToTalkMenuModel * model, {
  367. for (int i = 0; i < model->lists_count; i++) {
  368. PushToTalkMenuItemArray_clear(model->lists[i].items);
  369. furi_string_free(model->lists[i].label);
  370. }
  371. free(model->lists);
  372. }, true);
  373. view_free(hid_ptt_menu->view);
  374. free(hid_ptt_menu);
  375. }