diff --git a/actions/action_rfid.c b/actions/action_rfid.c index 2ec10286840..9c80ca8dac7 100644 --- a/actions/action_rfid.c +++ b/actions/action_rfid.c @@ -34,19 +34,18 @@ void action_rfid_tx(void* context, FuriString* action_path, FuriString* error) { size_t data_size = protocol_dict_get_max_data_size(dict); uint8_t* data = malloc(data_size); - FURI_LOG_I(TAG, "Max dict data size is %d", data_size); + // FURI_LOG_I(TAG, "Max dict data size is %d", data_size); bool successful_read = false; do { if(!flipper_format_file_open_existing(fff_data_file, furi_string_get_cstr(file_name))) { ACTION_SET_ERROR("RFID: Error opening %s", furi_string_get_cstr(file_name)); break; } - FURI_LOG_I(TAG, "Opened file"); if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) { ACTION_SET_ERROR("RFID: Missing or incorrect header"); break; } - FURI_LOG_I(TAG, "Read file headers"); + // FURI_LOG_I(TAG, "Read file headers"); // TODO: add better header checks here... if(!strcmp(furi_string_get_cstr(temp_str), "Flipper RFID key")) { } else { @@ -67,9 +66,9 @@ void action_rfid_tx(void* context, FuriString* action_path, FuriString* error) { // read and check data field size_t required_size = protocol_dict_get_data_size(dict, protocol); - FURI_LOG_I(TAG, "Protocol req data size is %d", required_size); + // FURI_LOG_I(TAG, "Protocol req data size is %d", required_size); if(!flipper_format_read_hex(fff_data_file, "Data", data, required_size)) { - FURI_LOG_E(TAG, "Error reading data"); + FURI_LOG_E(TAG, "RFID: Error reading data"); ACTION_SET_ERROR("RFID: Error reading data"); break; } @@ -86,7 +85,7 @@ void action_rfid_tx(void* context, FuriString* action_path, FuriString* error) { protocol_dict_set_data(dict, protocol, data, data_size); successful_read = true; - FURI_LOG_I(TAG, "protocol dict setup complete!"); + // FURI_LOG_I(TAG, "protocol dict setup complete!"); } while(false); if(successful_read) { @@ -95,14 +94,15 @@ void action_rfid_tx(void* context, FuriString* action_path, FuriString* error) { lfrfid_worker_start_thread(worker); lfrfid_worker_emulate_start(worker, protocol); - FURI_LOG_I(TAG, "Emulating RFID..."); - int16_t time_ms = 3000; - int16_t interval_ms = 200; + int16_t time_ms = app->settings.rfid_duration; + FURI_LOG_I( + TAG, "RFID: Emulating RFID (%s) for %d ms", furi_string_get_cstr(file_name), time_ms); + int16_t interval_ms = 100; while(time_ms > 0) { furi_delay_ms(interval_ms); time_ms -= interval_ms; } - FURI_LOG_I(TAG, "Emulation stopped"); + FURI_LOG_I(TAG, "RFID: Emulation stopped"); lfrfid_worker_stop(worker); lfrfid_worker_stop_thread(worker); diff --git a/images/ArrowDown_8x4.png b/images/ArrowDown_8x4.png new file mode 100644 index 00000000000..ed5d5c819f1 Binary files /dev/null and b/images/ArrowDown_8x4.png differ diff --git a/images/ArrowUp_8x4.png b/images/ArrowUp_8x4.png new file mode 100644 index 00000000000..89f13efba9d Binary files /dev/null and b/images/ArrowUp_8x4.png differ diff --git a/images/Directory_10px.png b/images/Directory_10px.png new file mode 100644 index 00000000000..a4cdf453e32 Binary files /dev/null and b/images/Directory_10px.png differ diff --git a/images/IR_10px.png b/images/IR_10px.png new file mode 100644 index 00000000000..22c986180a2 Binary files /dev/null and b/images/IR_10px.png differ diff --git a/images/NFC_10px.png b/images/NFC_10px.png new file mode 100644 index 00000000000..6bc027111a7 Binary files /dev/null and b/images/NFC_10px.png differ diff --git a/images/Playlist_10px.png b/images/Playlist_10px.png new file mode 100644 index 00000000000..f492b94d4e2 Binary files /dev/null and b/images/Playlist_10px.png differ diff --git a/images/RFID_10px.png b/images/RFID_10px.png new file mode 100644 index 00000000000..ce01284a2c1 Binary files /dev/null and b/images/RFID_10px.png differ diff --git a/images/Settings_10px.png b/images/Settings_10px.png new file mode 100644 index 00000000000..d7176c060d8 Binary files /dev/null and b/images/Settings_10px.png differ diff --git a/images/SubGHz_10px.png b/images/SubGHz_10px.png new file mode 100644 index 00000000000..5a25fdf4ef1 Binary files /dev/null and b/images/SubGHz_10px.png differ diff --git a/item.c b/item.c index 0eacda7f3cc..da2fabd1c9c 100644 --- a/item.c +++ b/item.c @@ -78,14 +78,7 @@ ItemsView* item_get_items_view_from_path(void* context, FuriString* input_path) furi_string_free(filename_tmp); furi_string_free(path); - // DEBUG: Now print our array in original order FileArray_it_t iter; - for(FileArray_it(iter, flist); !FileArray_end_p(iter); FileArray_next(iter)) { - const char* f = furi_string_get_cstr(*FileArray_cref(iter)); - FURI_LOG_I(TAG, "Found: %s", f); - } - - FURI_LOG_I(TAG, "Creating our ItemsArray"); ItemArray_init(iview->items); for(FileArray_it(iter, flist); !FileArray_end_p(iter); FileArray_next(iter)) { path = *FileArray_ref(iter); @@ -93,19 +86,32 @@ ItemsView* item_get_items_view_from_path(void* context, FuriString* input_path) Item* item = ItemArray_push_new(iview->items); - // Action files have extensions, so item->ext starts with '.' - ehhhh + // Action files have extensions, so item->ext starts with '.' item->ext[0] = 0; path_extract_extension(path, item->ext, MAX_EXT_LEN); - item->type = (item->ext[0] == '.') ? Item_Action : Item_Group; + // FURI_LOG_I(TAG, ". EXT = %s", item->ext); + if(item->ext[0] == '.') { + // TODO: hack alert - make a helper fn here, or something + if(item->ext[1] == 's') + item->type = Item_SubGhz; + else if(item->ext[1] == 'r') + item->type = Item_RFID; + else if(item->ext[1] == 'q') + item->type = Item_Playlist; + else if(item->ext[1] == 'i') + item->type = Item_IR; + } else { + item->type = Item_Group; + } item->name = furi_string_alloc(); path_extract_filename_no_ext(found_path, item->name); - FURI_LOG_I(TAG, "Basename: %s", furi_string_get_cstr(item->name)); + // FURI_LOG_I(TAG, "Basename: %s", furi_string_get_cstr(item->name)); item_prettify_name(item->name); item->path = furi_string_alloc(); furi_string_set(item->path, path); - FURI_LOG_I(TAG, "Path: %s", furi_string_get_cstr(item->path)); + // FURI_LOG_I(TAG, "Path: %s", furi_string_get_cstr(item->path)); } FileArray_clear(flist); diff --git a/item.h b/item.h index abed30c04c2..cc0c148316b 100644 --- a/item.h +++ b/item.h @@ -9,7 +9,14 @@ * on-screen as well as to perform that action. */ -typedef enum { Item_Action, Item_Group } ItemType; +typedef enum { + Item_SubGhz, + Item_RFID, + Item_IR, + Item_Playlist, + Item_Group, + Item_Settings +} ItemType; typedef struct Item { ItemType type; diff --git a/quac.c b/quac.c index cf2faa35589..04437d99fb7 100644 --- a/quac.c +++ b/quac.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -14,6 +15,7 @@ #include "scenes/scene_items.h" #include "quac.h" +#include "quac_settings.h" /* generated by fbt from .png files in images folder */ #include @@ -30,10 +32,18 @@ App* app_alloc() { // Create our UI elements app->btn_menu = button_menu_alloc(); view_dispatcher_add_view( - app->view_dispatcher, SR_ButtonMenu, button_menu_get_view(app->btn_menu)); + app->view_dispatcher, Q_ButtonMenu, button_menu_get_view(app->btn_menu)); + + app->action_menu = action_menu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, Q_ActionMenu, action_menu_get_view(app->action_menu)); + + app->vil_settings = variable_item_list_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, Q_Settings, variable_item_list_get_view(app->vil_settings)); app->dialog = dialog_ex_alloc(); - view_dispatcher_add_view(app->view_dispatcher, SR_Dialog, dialog_ex_get_view(app->dialog)); + view_dispatcher_add_view(app->view_dispatcher, Q_Dialog, dialog_ex_get_view(app->dialog)); // Storage app->storage = furi_record_open(RECORD_STORAGE); @@ -45,6 +55,16 @@ App* app_alloc() { app->depth = 0; app->selected_item = -1; + // Default settings + // TODO: Store settings in apps_data/quac/.quac.conf as a Flipper Format File! + // Create Settings Scene, save settings _on_exit + // Always use settings in _on_enter of other scenes + + app->settings.rfid_duration = 3000; + app->settings.layout = QUAC_APP_LANDSCAPE; + // app->settings.layout = QUAC_APP_PORTRAIT; + app->settings.show_icons = true; + app->items_view = item_get_items_view_from_path(app, NULL); return app; @@ -55,9 +75,11 @@ void app_free(App* app) { item_items_view_free(app->items_view); - view_dispatcher_remove_view(app->view_dispatcher, SR_ButtonMenu); + view_dispatcher_remove_view(app->view_dispatcher, Q_ButtonMenu); button_menu_free(app->btn_menu); + action_menu_free(app->action_menu); + scene_manager_free(app->scene_manager); view_dispatcher_free(app->view_dispatcher); @@ -73,10 +95,11 @@ int32_t quac_app(void* p) { FURI_LOG_I(TAG, "QUAC! QUAC!"); App* app = app_alloc(); + quac_load_settings(app); Gui* gui = furi_record_open(RECORD_GUI); view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); - scene_manager_next_scene(app->scene_manager, SR_Scene_Items); + scene_manager_next_scene(app->scene_manager, Q_Scene_Items); view_dispatcher_run(app->view_dispatcher); furi_record_close(RECORD_GUI); diff --git a/quac.h b/quac.h index 39bafdafe28..5c8b2f6e9b5 100644 --- a/quac.h +++ b/quac.h @@ -4,9 +4,13 @@ #include #include #include +#include + #include #include +#include "views/action_menu.h" + #include "item.h" #define QUAC_NAME "Quac!" @@ -17,11 +21,15 @@ // Full path to actions #define QUAC_DATA_PATH EXT_PATH(QUAC_PATH) +typedef enum { QUAC_APP_PORTRAIT, QUAC_APP_LANDSCAPE } QuacAppLayout; + typedef struct App { SceneManager* scene_manager; ViewDispatcher* view_dispatcher; ButtonMenu* btn_menu; DialogEx* dialog; + VariableItemList* vil_settings; + ActionMenu* action_menu; Storage* storage; NotificationApp* notifications; @@ -29,6 +37,14 @@ typedef struct App { int depth; ItemsView* items_view; int selected_item; + + struct { + QuacAppLayout layout; // Defaults to Portrait + bool show_icons; // Defaults to True + bool show_headers; // Defaults to True + uint32_t rfid_duration; // Defaults to 2500 ms + } settings; + } App; App* app_alloc(); diff --git a/quac_settings.c b/quac_settings.c new file mode 100644 index 00000000000..b23a3452e1f --- /dev/null +++ b/quac_settings.c @@ -0,0 +1,142 @@ +#include "quac_settings.h" + +#include + +// Quac Settings File Info +// TODO: Fix this path to use existing #defs for /ext, etc +#define QUAC_SETTINGS_FILENAME "/ext/apps_data/quac/.quac.conf" +#define QUAC_SETTINGS_FILE_TYPE "Quac Settings File" +#define QUAC_SETTINGS_FILE_VERSION 1 + +// Quac Settings Defaults +#define QUAC_SETTINGS_DEFAULT_RFID_DURATION 2500 +#define QUAC_SETTINGS_DEFAULT_LAYOUT QUAC_APP_LANDSCAPE // QUAC_APP_PORTRAIT +#define QUAC_SETTINGS_DEFAULT_SHOW_ICONS true +#define QUAC_SETTINGS_DEFAULT_SHOW_HEADERS true + +void quac_set_default_settings(App* app) { + app->settings.rfid_duration = QUAC_SETTINGS_DEFAULT_RFID_DURATION; + app->settings.layout = QUAC_SETTINGS_DEFAULT_LAYOUT; + app->settings.show_icons = QUAC_SETTINGS_DEFAULT_SHOW_ICONS; + app->settings.show_headers = QUAC_SETTINGS_DEFAULT_SHOW_HEADERS; +} + +void quac_load_settings(App* app) { + FlipperFormat* fff_settings = flipper_format_file_alloc(app->storage); + FuriString* temp_str; + temp_str = furi_string_alloc(); + uint32_t temp_data32 = 0; + + FURI_LOG_I(TAG, "SETTINGS: Reading settings file"); + bool successful = false; + do { + if(!flipper_format_file_open_existing(fff_settings, QUAC_SETTINGS_FILENAME)) { + FURI_LOG_I(TAG, "SETTINGS: File not found, loading defaults"); + break; + } + + if(!flipper_format_read_header(fff_settings, temp_str, &temp_data32)) { + FURI_LOG_E(TAG, "SETTINGS: Missing or incorrect header"); + break; + } + + if((!strcmp(furi_string_get_cstr(temp_str), QUAC_SETTINGS_FILE_TYPE)) && + (temp_data32 == QUAC_SETTINGS_FILE_VERSION)) { + } else { + FURI_LOG_E(TAG, "SETTINGS: Type or version mismatch"); + break; + } + + // Now read actual values we care about + if(!flipper_format_read_string(fff_settings, "Layout", temp_str)) { + FURI_LOG_E(TAG, "SETTINGS: Missing Layout"); + break; + } + if(!strcmp(furi_string_get_cstr(temp_str), "Landscape")) { + app->settings.layout = QUAC_APP_LANDSCAPE; + } else if(!strcmp(furi_string_get_cstr(temp_str), "Portrait")) { + app->settings.layout = QUAC_APP_PORTRAIT; + } else { + FURI_LOG_E(TAG, "SETTINGS: Invalid Layout"); + break; + } + + if(!flipper_format_read_uint32(fff_settings, "Show Icons", &temp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Missing 'Show Icons'"); + break; + } + app->settings.show_icons = (temp_data32 == 0) ? false : true; + + if(!flipper_format_read_uint32(fff_settings, "Show Headers", &temp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Missing 'Show Headers'"); + break; + } + app->settings.show_headers = (temp_data32 == 0) ? false : true; + + if(!flipper_format_read_uint32(fff_settings, "RFID Duration", &temp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Missing 'RFID Duration'"); + break; + } + app->settings.rfid_duration = temp_data32; + + successful = true; + } while(false); + + if(!successful) { + quac_set_default_settings(app); + } + + furi_string_free(temp_str); + flipper_format_free(fff_settings); +} + +void quac_save_settings(App* app) { + FlipperFormat* fff_settings = flipper_format_file_alloc(app->storage); + uint32_t temp_data32; + + bool successful = false; + do { + if(!flipper_format_file_open_always(fff_settings, QUAC_SETTINGS_FILENAME)) { + FURI_LOG_E(TAG, "SETTINGS: Unable to open file for save!!"); + break; + } + + if(!flipper_format_write_header_cstr( + fff_settings, QUAC_SETTINGS_FILE_TYPE, QUAC_SETTINGS_FILE_VERSION)) { + FURI_LOG_E(TAG, "SETTINGS: Failed writing file type and version"); + break; + } + // layout, icons, headers, duration + if(!flipper_format_write_string_cstr( + fff_settings, + "Layout", + app->settings.layout == QUAC_APP_LANDSCAPE ? "Landscape" : "Portrait")) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write Layout"); + break; + } + + temp_data32 = app->settings.show_icons ? 1 : 0; + if(!flipper_format_write_uint32(fff_settings, "Show Icons", &temp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Show Icons'"); + break; + } + temp_data32 = app->settings.show_headers ? 1 : 0; + if(!flipper_format_write_uint32(fff_settings, "Show Headers", &temp_data32, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Show Headers'"); + break; + } + if(!flipper_format_write_uint32( + fff_settings, "RFID Duration", &app->settings.rfid_duration, 1)) { + FURI_LOG_E(TAG, "SETTINGS: Failed to write 'RFID Duration'"); + break; + } + successful = true; + } while(false); + + if(!successful) { + FURI_LOG_E(TAG, "SETTINGS: Failed to save settings!!"); + } + + flipper_format_file_close(fff_settings); + flipper_format_free(fff_settings); +} \ No newline at end of file diff --git a/quac_settings.h b/quac_settings.h new file mode 100644 index 00000000000..7d4f3228dec --- /dev/null +++ b/quac_settings.h @@ -0,0 +1,9 @@ +#pragma once + +#include "quac.h" + +void quac_set_default_settings(App* app); + +void quac_load_settings(App* app); + +void quac_save_settings(App* app); diff --git a/scenes/scene_items.c b/scenes/scene_items.c index c74d121c756..e27d8bc76ac 100644 --- a/scenes/scene_items.c +++ b/scenes/scene_items.c @@ -11,6 +11,8 @@ #include "scenes.h" #include "scene_items.h" #include "../actions/action.h" +#include "../views/action_menu.h" + #include void scene_items_item_callback(void* context, int32_t index, InputType type) { @@ -27,35 +29,70 @@ void scene_items_item_callback(void* context, int32_t index, InputType type) { // For each scene, implement handler callbacks void scene_items_on_enter(void* context) { App* app = context; - ButtonMenu* menu = app->btn_menu; - button_menu_reset(menu); - DialogEx* dialog = app->dialog; - dialog_ex_reset(dialog); + + ActionMenu* menu = app->action_menu; + action_menu_reset(menu); + if(app->settings.layout == QUAC_APP_LANDSCAPE) + action_menu_set_layout(menu, ActionMenuLayoutLandscape); + else + action_menu_set_layout(menu, ActionMenuLayoutPortrait); + action_menu_set_show_icons(menu, app->settings.show_icons); + action_menu_set_show_headers(menu, app->settings.show_headers); ItemsView* items_view = app->items_view; FURI_LOG_I(TAG, "items on_enter: [%d] %s", app->depth, furi_string_get_cstr(items_view->path)); + furi_delay_ms(500); const char* header = furi_string_get_cstr(items_view->name); - button_menu_set_header(menu, header); + action_menu_set_header(menu, header); - if(ItemArray_size(items_view->items)) { + size_t item_view_size = ItemArray_size(items_view->items); + if(item_view_size > 0) { ItemArray_it_t iter; int32_t index = 0; for(ItemArray_it(iter, items_view->items); !ItemArray_end_p(iter); ItemArray_next(iter), ++index) { const char* label = furi_string_get_cstr(ItemArray_cref(iter)->name); - ButtonMenuItemType type = ItemArray_cref(iter)->type == Item_Action ? - ButtonMenuItemTypeCommon : - ButtonMenuItemTypeControl; - button_menu_add_item(menu, label, index, scene_items_item_callback, type, app); + ActionMenuItemType type; + // TODO: Fix this with an array/map + switch(ItemArray_cref(iter)->type) { + case Item_Group: + type = ActionMenuItemTypeGroup; + break; + case Item_Playlist: + type = ActionMenuItemTypePlaylist; + break; + case Item_SubGhz: + type = ActionMenuItemTypeSubGHz; + break; + case Item_RFID: + type = ActionMenuItemTypeRFID; + break; + case Item_IR: + type = ActionMenuItemTypeIR; + break; + default: + type = ActionMenuItemTypeGroup; // TODO: Does this ever get hit? + } + action_menu_add_item(menu, label, index, scene_items_item_callback, type, app); } } else { FURI_LOG_W(TAG, "No items for: %s", furi_string_get_cstr(items_view->path)); // TODO: Display Error popup? Empty folder? } - // ... - view_dispatcher_switch_to_view(app->view_dispatcher, SR_ButtonMenu); + // Always add the "Settings" item at the end of our list - but only at top level! + if(app->depth == 0) { + action_menu_add_item( + menu, + "Settings", + item_view_size, // last item! + scene_items_item_callback, + ActionMenuItemTypeSettings, + app); + } + + view_dispatcher_switch_to_view(app->view_dispatcher, Q_ActionMenu); } bool scene_items_on_event(void* context, SceneManagerEvent event) { App* app = context; @@ -66,39 +103,47 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) { case SceneManagerEventTypeCustom: if(event.event == Event_ButtonPressed) { consumed = true; + furi_delay_ms(100); FURI_LOG_I(TAG, "button pressed is %d", app->selected_item); - Item* item = ItemArray_get(app->items_view->items, app->selected_item); - if(item->type == Item_Group) { - app->depth++; - ItemsView* new_items = item_get_items_view_from_path(app, item->path); - item_items_view_free(app->items_view); - app->items_view = new_items; - scene_manager_next_scene(app->scene_manager, SR_Scene_Items); - } else { - FURI_LOG_I(TAG, "Initiating item action: %s", furi_string_get_cstr(item->name)); - - // LED goes blinky blinky - App* app = context; - notification_message(app->notifications, &sequence_blink_start_blue); - - // Prepare error string for action calls - FuriString* error; - error = furi_string_alloc(); - - action_tx(app, item, error); - - if(furi_string_size(error)) { - FURI_LOG_E(TAG, furi_string_get_cstr(error)); - // Change LED to Red and Vibrate! - notification_message(app->notifications, &sequence_error); - - // Display DialogEx popup or something? + if(app->selected_item < (int)ItemArray_size(app->items_view->items)) { + Item* item = ItemArray_get(app->items_view->items, app->selected_item); + if(item->type == Item_Group) { + app->depth++; + ItemsView* new_items = item_get_items_view_from_path(app, item->path); + item_items_view_free(app->items_view); + app->items_view = new_items; + scene_manager_next_scene(app->scene_manager, Q_Scene_Items); + } else { + FURI_LOG_I( + TAG, "Initiating item action: %s", furi_string_get_cstr(item->name)); + + // LED goes blinky blinky + App* app = context; + notification_message(app->notifications, &sequence_blink_start_blue); + + // Prepare error string for action calls + FuriString* error; + error = furi_string_alloc(); + + action_tx(app, item, error); + + if(furi_string_size(error)) { + FURI_LOG_E(TAG, furi_string_get_cstr(error)); + // Change LED to Red and Vibrate! + notification_message(app->notifications, &sequence_error); + + // Display DialogEx popup or something? + } + + furi_string_free(error); + + // Turn off LED light + notification_message(app->notifications, &sequence_blink_stop); } - - furi_string_free(error); - - // Turn off LED light - notification_message(app->notifications, &sequence_blink_stop); + } else { + FURI_LOG_I(TAG, "Selected Settings!"); + // TODO: Do we need to free this current items_view?? + scene_manager_next_scene(app->scene_manager, Q_Scene_Settings); } } break; @@ -122,17 +167,18 @@ bool scene_items_on_event(void* context, SceneManagerEvent event) { } break; default: + FURI_LOG_I(TAG, "Custom event not handled"); break; } + FURI_LOG_I(TAG, "Generic event not handled"); return consumed; } void scene_items_on_exit(void* context) { App* app = context; - ButtonMenu* menu = app->btn_menu; - button_menu_reset(menu); - DialogEx* dialog = app->dialog; - dialog_ex_reset(dialog); + + ActionMenu* menu = app->action_menu; + action_menu_reset(menu); FURI_LOG_I(TAG, "on_exit. depth = %d", app->depth); } \ No newline at end of file diff --git a/scenes/scene_settings.c b/scenes/scene_settings.c new file mode 100644 index 00000000000..e946c756515 --- /dev/null +++ b/scenes/scene_settings.c @@ -0,0 +1,133 @@ +#include + +#include +#include +#include +#include + +#include "quac.h" +#include "scenes.h" +#include "scene_settings.h" +#include "../actions/action.h" +#include "../views/action_menu.h" +#include "../quac_settings.h" + +#include + +static const char* const layout_text[2] = {"Vert", "Horiz"}; +static const uint32_t layout_value[2] = {QUAC_APP_PORTRAIT, QUAC_APP_LANDSCAPE}; + +static const char* const show_icons_text[2] = {"OFF", "ON"}; +static const uint32_t show_icons_value[2] = {false, true}; + +static const char* const show_headers_text[2] = {"OFF", "ON"}; +static const uint32_t show_headers_value[2] = {false, true}; + +#define V_RFID_DURATION_COUNT 8 +static const char* const rfid_duration_text[V_RFID_DURATION_COUNT] = { + "500 ms", + "1 sec", + "1.5 sec", + "2 sec", + "2.5 sec", + "3 sec", + "5 sec", + "10 sec", +}; +static const uint32_t rfid_duration_value[V_RFID_DURATION_COUNT] = { + 500, + 1000, + 1500, + 2000, + 2500, + 3000, + 5000, + 10000, +}; + +static void scene_settings_layout_changed(VariableItem* item) { + App* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, layout_text[index]); + app->settings.layout = layout_value[index]; +} + +static void scene_settings_show_icons_changed(VariableItem* item) { + App* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, show_icons_text[index]); + app->settings.show_icons = show_icons_value[index]; +} + +static void scene_settings_show_headers_changed(VariableItem* item) { + App* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, show_headers_text[index]); + app->settings.show_headers = show_headers_value[index]; +} + +static void scene_settings_rfid_duration_changed(VariableItem* item) { + App* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, rfid_duration_text[index]); + app->settings.rfid_duration = rfid_duration_value[index]; +} + +// For each scene, implement handler callbacks +void scene_settings_on_enter(void* context) { + App* app = context; + + FURI_LOG_I(TAG, "Settings _on_enter"); + VariableItemList* vil = app->vil_settings; + variable_item_list_reset(vil); + + VariableItem* item; + uint8_t value_index; + + FURI_LOG_I(TAG, "setting up Layout"); + item = variable_item_list_add(vil, "Layout", 2, scene_settings_layout_changed, app); + value_index = value_index_uint32(app->settings.layout, layout_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, layout_text[value_index]); + + FURI_LOG_I(TAG, "setting up Show Icons"); + item = variable_item_list_add(vil, "Show Icons", 2, scene_settings_show_icons_changed, app); + value_index = value_index_uint32(app->settings.show_icons, show_icons_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, show_icons_text[value_index]); + + FURI_LOG_I(TAG, "setting up Show Headers"); + item = + variable_item_list_add(vil, "Show Headers", 2, scene_settings_show_headers_changed, app); + value_index = value_index_uint32(app->settings.show_headers, show_headers_value, 2); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, show_headers_text[value_index]); + + FURI_LOG_I(TAG, "setting up RFID Duration"); + item = variable_item_list_add( + vil, "RFID Duration", V_RFID_DURATION_COUNT, scene_settings_rfid_duration_changed, app); + value_index = value_index_uint32( + app->settings.rfid_duration, rfid_duration_value, V_RFID_DURATION_COUNT); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, rfid_duration_text[value_index]); + + // TODO: Set Enter callback here - why?? All settings have custom callbacks + // variable_item_list_set_enter_callback(vil, my_cb, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, Q_Settings); +} +bool scene_settings_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + + return false; +} + +void scene_settings_on_exit(void* context) { + App* app = context; + VariableItemList* vil = app->vil_settings; + variable_item_list_reset(vil); + + quac_save_settings(app); +} \ No newline at end of file diff --git a/scenes/scene_settings.h b/scenes/scene_settings.h new file mode 100644 index 00000000000..88ef2c0e5ed --- /dev/null +++ b/scenes/scene_settings.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +// void scene_settings_item_callback(void* context, int32_t index, InputType type); + +// For each scene, implement handler callbacks +void scene_settings_on_enter(void* context); +bool scene_settings_on_event(void* context, SceneManagerEvent event); +void scene_settings_on_exit(void* context); diff --git a/scenes/scenes.c b/scenes/scenes.c index 1445fc4e5ed..13da3c259e3 100644 --- a/scenes/scenes.c +++ b/scenes/scenes.c @@ -3,19 +3,23 @@ #include "quac.h" #include "scenes.h" #include "scene_items.h" +#include "scene_settings.h" // define handler callbacks - order must match appScenes enum! -void (*const app_on_enter_handlers[])(void* context) = {scene_items_on_enter}; +void (*const app_on_enter_handlers[])(void* context) = { + scene_items_on_enter, + scene_settings_on_enter}; bool (*const app_on_event_handlers[])(void* context, SceneManagerEvent event) = { scene_items_on_event, + scene_settings_on_event, }; -void (*const app_on_exit_handlers[])(void* context) = {scene_items_on_exit}; +void (*const app_on_exit_handlers[])(void* context) = {scene_items_on_exit, scene_settings_on_exit}; const SceneManagerHandlers app_scene_handlers = { .on_enter_handlers = app_on_enter_handlers, .on_event_handlers = app_on_event_handlers, .on_exit_handlers = app_on_exit_handlers, - .scene_num = SR_Scene_count}; + .scene_num = Q_Scene_count}; bool app_scene_custom_callback(void* context, uint32_t custom_event_id) { App* app = context; diff --git a/scenes/scenes.h b/scenes/scenes.h index 4168485d0ca..cb4f2b84a08 100644 --- a/scenes/scenes.h +++ b/scenes/scenes.h @@ -1,12 +1,14 @@ #pragma once -typedef enum { SR_Scene_Items, SR_Scene_count } appScenes; +typedef enum { Q_Scene_Items, Q_Scene_Settings, Q_Scene_count } appScenes; typedef enum { - SR_ButtonMenu, // used on selected device, to show buttons/groups - SR_Dialog, // shows errors - SR_FileBrowser, // TODO: UNUSED! - SR_TextInput // TODO: UNUSED + Q_ButtonMenu, // used on selected device, to show buttons/groups + Q_Dialog, // shows errors + Q_ActionMenu, // new UI, + Q_Settings, // Variable Item List for settings + Q_FileBrowser, // TODO: UNUSED! + Q_TextInput // TODO: UNUSED } appView; typedef enum { Event_DeviceSelected, Event_ButtonPressed } AppCustomEvents; diff --git a/views/action_menu.c b/views/action_menu.c new file mode 100644 index 00000000000..bf3dc2de554 --- /dev/null +++ b/views/action_menu.c @@ -0,0 +1,611 @@ +#include "action_menu.h" + +#include +#include +#include + +#include + +#include +#include + +#include "quac_icons.h" + +#define ITEM_FIRST_OFFSET 17 +#define ITEM_NEXT_OFFSET 4 +#define ITEM_HEIGHT 14 +#define ITEM_WIDTH 64 +#define BUTTONS_PER_SCREEN 6 + +#define ITEMS_PER_SCREEN_LANDSCAPE 3 +#define ITEMS_PER_SCREEN_PORTRAIT 6 + +static const Icon* ActionMenuIcons[] = { + [ActionMenuItemTypeSubGHz] = &I_SubGHz_10px, + [ActionMenuItemTypeRFID] = &I_RFID_10px, + [ActionMenuItemTypeIR] = &I_IR_10px, + [ActionMenuItemTypePlaylist] = &I_Playlist_10px, + [ActionMenuItemTypeGroup] = &I_Directory_10px, + [ActionMenuItemTypeSettings] = &I_Settings_10px, +}; + +struct ActionMenuItem { + const char* label; + IconAnimation* icon; + uint32_t index; + ActionMenuItemCallback callback; + ActionMenuItemType type; + void* callback_context; +}; + +ARRAY_DEF(ActionMenuItemArray, ActionMenuItem, M_POD_OPLIST); +#define M_OPL_ActionMenuItemArray_t() ARRAY_OPLIST(ActionMenuItemArray, M_POD_OPLIST) + +struct ActionMenu { + View* view; + bool freeze_input; +}; + +typedef struct { + ActionMenuItemArray_t items; + size_t position; + size_t window_position; + FuriString* header; + ActionMenuLayout layout; + bool show_icons; + bool show_headers; +} ActionMenuModel; + +static void action_menu_draw_landscape(Canvas* canvas, ActionMenuModel* model) { + const uint8_t item_height = 16; + uint8_t item_width = canvas_width(canvas) - 5; // space for scrollbar + + const bool have_header = furi_string_size(model->header) && model->show_headers; + + canvas_clear(canvas); + if(have_header) { + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 4, 11, furi_string_get_cstr(model->header)); + } + canvas_set_font(canvas, FontSecondary); + + size_t position = 0; + const size_t items_on_screen = ITEMS_PER_SCREEN_LANDSCAPE + (have_header ? 0 : 1); + uint8_t y_offset = have_header ? 16 : 0; + const size_t x_txt_start = model->show_icons ? 18 : 4; + + ActionMenuItemArray_it_t it; + for(ActionMenuItemArray_it(it, model->items); !ActionMenuItemArray_end_p(it); + ActionMenuItemArray_next(it)) { + const size_t item_position = position - model->window_position; + + if(item_position < items_on_screen) { + if(position == model->position) { + canvas_set_color(canvas, ColorBlack); + elements_slightly_rounded_box( + canvas, + 0, + y_offset + (item_position * item_height) + 1, + item_width, + item_height - 2); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_set_color(canvas, ColorBlack); + } + + const ActionMenuItem* item = ActionMenuItemArray_cref(it); + if(model->show_icons) { + canvas_draw_icon( + canvas, + 4, + y_offset + (item_position * item_height) + 3, + ActionMenuIcons[item->type]); + } + + FuriString* disp_str; + disp_str = furi_string_alloc_set(item->label); + elements_string_fit_width(canvas, disp_str, item_width - (6 * 2)); + + canvas_draw_str( + canvas, + x_txt_start, // 6 + y_offset + (item_position * item_height) + item_height - 4, + furi_string_get_cstr(disp_str)); + furi_string_free(disp_str); + } + position++; + } + + elements_scrollbar(canvas, model->position, ActionMenuItemArray_size(model->items)); +} + +// static void action_menu_draw_portrait(Canvas* canvas, ActionMenuModel* model) { +// const bool have_header = furi_string_size(model->header) && model->show_headers; +// const size_t items_per_screen = have_header ? ITEMS_PER_SCREEN_PORTRAIT : +// ITEMS_PER_SCREEN_PORTRAIT + 1; +// const size_t active_screen = model->position / items_per_screen; +// const size_t items_size = ActionMenuItemArray_size(model->items); +// const size_t max_screen = items_size ? (items_size - 1) / items_per_screen : 0; + +// canvas_clear(canvas); + +// // Draw up/down arrows, as needed +// if(active_screen > 0) { +// canvas_draw_icon(canvas, 28, 1, &I_ArrowUp_8x4); +// } +// if(max_screen > active_screen) { +// canvas_draw_icon(canvas, 28, 123, &I_ArrowDown_8x4); +// } + +// if(have_header) { +// canvas_set_font(canvas, FontPrimary); +// elements_string_fit_width(canvas, model->header, ITEM_WIDTH - 6); +// canvas_draw_str_aligned( +// canvas, 32, 10, AlignCenter, AlignCenter, furi_string_get_cstr(model->header)); +// } +// canvas_set_font(canvas, FontSecondary); + +// size_t item_position = 0; +// size_t item_first_offset = have_header ? ITEM_FIRST_OFFSET : 6; +// size_t item_next_offset = have_header ? ITEM_NEXT_OFFSET : ITEM_NEXT_OFFSET - 1; +// ActionMenuItemArray_it_t it; +// for(ActionMenuItemArray_it(it, model->items); !ActionMenuItemArray_end_p(it); +// ActionMenuItemArray_next(it), ++item_position) { +// if(active_screen == (item_position / items_per_screen)) { +// uint8_t position_offset = item_position % items_per_screen; +// bool selected = item_position == model->position; + +// // draw the item +// uint8_t item_x = 0; +// uint8_t item_y = +// item_first_offset + (position_offset * (ITEM_HEIGHT + item_next_offset)); + +// canvas_set_color(canvas, ColorBlack); + +// if(selected) { +// // Same as elements_slightly_rounded_box with radius of 5 +// canvas_draw_rbox(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT, 1); +// canvas_set_color(canvas, ColorWhite); +// } else { +// canvas_draw_rframe(canvas, item_x, item_y, ITEM_WIDTH, ITEM_HEIGHT, 1); +// } + +// FuriString* disp_str; +// disp_str = furi_string_alloc_set(ActionMenuItemArray_cref(it)->label); +// elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6); + +// canvas_draw_str_aligned( +// canvas, +// item_x + (ITEM_WIDTH / 2), +// item_y + (ITEM_HEIGHT / 2), +// AlignCenter, +// AlignCenter, +// furi_string_get_cstr(disp_str)); +// furi_string_free(disp_str); +// } +// } +// } + +static void action_menu_draw_portrait(Canvas* canvas, ActionMenuModel* model) { + const bool have_header = furi_string_size(model->header) && model->show_headers; + const size_t items_per_screen = have_header ? ITEMS_PER_SCREEN_PORTRAIT : + ITEMS_PER_SCREEN_PORTRAIT + 1; + const size_t active_screen = model->position / items_per_screen; + const size_t items_size = ActionMenuItemArray_size(model->items); + const size_t max_screen = items_size ? (items_size - 1) / items_per_screen : 0; + + canvas_clear(canvas); + + // Draw up/down arrows, as needed + if(active_screen > 0) { + canvas_draw_icon(canvas, 28, 1, &I_ArrowUp_8x4); + } + if(max_screen > active_screen) { + canvas_draw_icon(canvas, 28, 123, &I_ArrowDown_8x4); + } + + if(have_header) { + canvas_set_font(canvas, FontPrimary); + elements_string_fit_width(canvas, model->header, ITEM_WIDTH - 6); + canvas_draw_str_aligned( + canvas, 32, 10, AlignCenter, AlignCenter, furi_string_get_cstr(model->header)); + } + canvas_set_font(canvas, FontSecondary); + + size_t item_position = 0; + const size_t x_txt_start = model->show_icons ? 16 : 4; + const size_t y_offset = have_header ? ITEM_FIRST_OFFSET : 6; + const size_t item_next_offset = have_header ? ITEM_NEXT_OFFSET : ITEM_NEXT_OFFSET - 1; + + ActionMenuItemArray_it_t it; + for(ActionMenuItemArray_it(it, model->items); !ActionMenuItemArray_end_p(it); + ActionMenuItemArray_next(it), ++item_position) { + if(active_screen == (item_position / items_per_screen)) { + uint8_t position_offset = item_position % items_per_screen; + bool selected = item_position == model->position; + + // draw the item + uint8_t item_y = y_offset + (position_offset * (ITEM_HEIGHT + item_next_offset)); + + canvas_set_color(canvas, ColorBlack); + + if(selected) { + // Same as elements_slightly_rounded_box with radius of 5 + canvas_draw_rbox(canvas, 0, item_y, ITEM_WIDTH, ITEM_HEIGHT, 1); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_draw_rframe(canvas, 0, item_y, ITEM_WIDTH, ITEM_HEIGHT, 1); + } + + const ActionMenuItem* item = ActionMenuItemArray_cref(it); + if(model->show_icons) { + canvas_draw_icon(canvas, 3, item_y + 2, ActionMenuIcons[item->type]); + } + + FuriString* disp_str; + disp_str = furi_string_alloc_set(item->label); + elements_string_fit_width(canvas, disp_str, ITEM_WIDTH - 6); + + canvas_draw_str( + canvas, + x_txt_start, + item_y + (ITEM_HEIGHT / 2) + 3, + furi_string_get_cstr(disp_str)); + furi_string_free(disp_str); + } + } +} + +static void action_menu_view_draw_callback(Canvas* canvas, void* context) { + furi_assert(canvas); + ActionMenuModel* model = (ActionMenuModel*)context; + + if(model->layout == ActionMenuLayoutLandscape) { + action_menu_draw_landscape(canvas, model); + } else { + action_menu_draw_portrait(canvas, model); + } +} + +static void action_menu_process_up(ActionMenu* action_menu) { + furi_assert(action_menu); + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + const size_t items_size = ActionMenuItemArray_size(model->items); + if(model->layout == ActionMenuLayoutPortrait) { + if(model->position > 0) { + model->position--; + } else { + model->position = items_size - 1; + } + } else { + const size_t items_on_screen = furi_string_empty(model->header) ? 4 : 3; + if(model->position > 0) { + model->position--; + if((model->position == model->window_position) && + (model->window_position > 0)) { + model->window_position--; + } + } else { + model->position = items_size - 1; + if(model->position > items_on_screen - 1) { + model->window_position = model->position - (items_on_screen - 1); + } + } + } + }, + true); +} + +// TODO: Up/Down keys are obeyed in the correct orientation! +static void action_menu_process_down(ActionMenu* action_menu) { + furi_assert(action_menu); + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + const size_t items_size = ActionMenuItemArray_size(model->items); + if(model->layout == ActionMenuLayoutPortrait) { + if(model->position < items_size - 1) { + model->position++; + } else { + model->position = 0; + } + } else { + const size_t items_on_screen = furi_string_empty(model->header) ? 4 : 3; + if(model->position < items_size - 1) { + model->position++; + if((model->position - model->window_position > items_on_screen - 2) && + (model->window_position < items_size - items_on_screen)) { + model->window_position++; + } + } else { + model->position = 0; + model->window_position = 0; + } + } + }, + true); +} + +static void action_menu_process_ok(ActionMenu* action_menu, InputType type) { + furi_assert(action_menu); + // UNUSED(type); + + FURI_LOG_I("AM", "OK pressed! %d: %s", type, input_get_type_name(type)); + ActionMenuItem* item = NULL; + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + if(model->position < (ActionMenuItemArray_size(model->items))) { + item = ActionMenuItemArray_get(model->items, model->position); + } + }, + false); + + // Landscape: Press, Short, Release + + if(item) { + if(type == InputTypeRelease && item->callback) + item->callback(item->callback_context, item->index, type); + } +} + +static bool action_menu_view_input_callback(InputEvent* event, void* context) { + furi_assert(event); + + ActionMenu* action_menu = context; + bool consumed = false; + + // Item selection + if(event->key == InputKeyOk) { + if((event->type == InputTypeRelease) || (event->type == InputTypePress)) { + consumed = true; + action_menu->freeze_input = (event->type == InputTypePress); + action_menu_process_ok(action_menu, event->type); + } else if(event->type == InputTypeShort) { + consumed = true; + action_menu_process_ok(action_menu, event->type); + } + } + + if(!action_menu->freeze_input && + ((event->type == InputTypeRepeat) || (event->type == InputTypeShort))) { + // FURI_LOG_I("AM", "Directional key: %d", event->key); + switch(event->key) { + case InputKeyUp: + consumed = true; + action_menu_process_up(action_menu); + break; + case InputKeyDown: + consumed = true; + action_menu_process_down(action_menu); + break; + case InputKeyRight: + FURI_LOG_W("AM", "InputKeyRight ignored"); + // consumed = true; + // action_menu_process_right(action_menu); + break; + case InputKeyLeft: + FURI_LOG_W("AM", "InputKeyLeft ignored"); + // consumed = true; + // action_menu_process_left(action_menu); + break; + default: + break; + } + } + + return consumed; +} + +View* action_menu_get_view(ActionMenu* action_menu) { + furi_assert(action_menu); + return action_menu->view; +} + +void action_menu_reset(ActionMenu* action_menu) { + furi_assert(action_menu); + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + // for + // M_EACH(item, model->items, ActionMenuItemArray_t) { + // icon_animation_stop(item->icon); + // icon_animation_free(item->icon); + // } + ActionMenuItemArray_reset(model->items); + model->position = 0; + model->window_position = 0; + furi_string_reset(model->header); + }, + true); +} + +void action_menu_set_layout(ActionMenu* action_menu, ActionMenuLayout layout) { + furi_assert(action_menu); + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + model->layout = layout; + if(model->layout == ActionMenuLayoutLandscape) { + view_set_orientation(action_menu->view, ViewOrientationHorizontal); + } else { + view_set_orientation(action_menu->view, ViewOrientationVertical); + } + }, + true); +} + +void action_menu_set_header(ActionMenu* action_menu, const char* header) { + furi_assert(action_menu); + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + if(header == NULL) { + furi_string_reset(model->header); + } else { + furi_string_set_str(model->header, header); + } + }, + true); +} + +void action_menu_set_show_icons(ActionMenu* action_menu, bool show_icons) { + with_view_model( + action_menu->view, ActionMenuModel * model, { model->show_icons = show_icons; }, true); +} + +void action_menu_set_show_headers(ActionMenu* action_menu, bool show_headers) { + with_view_model( + action_menu->view, ActionMenuModel * model, { model->show_headers = show_headers; }, true); +} + +ActionMenuItem* action_menu_add_item( + ActionMenu* action_menu, + const char* label, + int32_t index, + ActionMenuItemCallback callback, + ActionMenuItemType type, + void* callback_context) { + ActionMenuItem* item = NULL; + furi_assert(label); + furi_assert(action_menu); + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + item = ActionMenuItemArray_push_new(model->items); + item->label = label; + // item->icon = icon ? icon_animation_alloc(icon) : NULL; // or default icon? + // view_tie_icon_animation(action_menu->view, item->icon); + item->index = index; + item->type = type; + item->callback = callback; + item->callback_context = callback_context; + }, + true); + + return item; +} + +ActionMenu* action_menu_alloc(void) { + ActionMenu* action_menu = malloc(sizeof(ActionMenu)); + action_menu->view = view_alloc(); + view_set_orientation(action_menu->view, ViewOrientationHorizontal); + view_set_context(action_menu->view, action_menu); + view_allocate_model(action_menu->view, ViewModelTypeLocking, sizeof(ActionMenuModel)); + view_set_draw_callback(action_menu->view, action_menu_view_draw_callback); + view_set_input_callback(action_menu->view, action_menu_view_input_callback); + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + ActionMenuItemArray_init(model->items); + model->position = 0; + model->window_position = 0; + model->header = furi_string_alloc(); + model->layout = ActionMenuLayoutLandscape; // TODO: ehhhhhhhhhhhhhhhhhhh + model->show_icons = true; + model->show_headers = true; + }, + true); + + action_menu->freeze_input = false; + return action_menu; +} + +void action_menu_free(ActionMenu* action_menu) { + furi_assert(action_menu); + + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + // for + // M_EACH(item, model->items, ActionMenuItemArray_t) { + // icon_animation_stop(item->icon); + // icon_animation_free(item->icon); + // } + ActionMenuItemArray_clear(model->items); + furi_string_free(model->header); + }, + true); + view_free(action_menu->view); + free(action_menu); +} + +void action_menu_set_selected_item(ActionMenu* action_menu, uint32_t index) { + furi_assert(action_menu); + + ActionMenuModel* m = view_get_model(action_menu->view); + if(m->layout == ActionMenuLayoutPortrait) { + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + size_t item_position = 0; + ActionMenuItemArray_it_t it; + for(ActionMenuItemArray_it(it, model->items); !ActionMenuItemArray_end_p(it); + ActionMenuItemArray_next(it), ++item_position) { + if((uint32_t)ActionMenuItemArray_cref(it)->index == index) { + model->position = item_position; + break; + } + } + }, + true); + } else { + with_view_model( + action_menu->view, + ActionMenuModel * model, + { + size_t position = 0; + ActionMenuItemArray_it_t it; + for(ActionMenuItemArray_it(it, model->items); !ActionMenuItemArray_end_p(it); + ActionMenuItemArray_next(it)) { + if(index == ActionMenuItemArray_cref(it)->index) { + break; + } + position++; + } + const size_t items_size = ActionMenuItemArray_size(model->items); + + if(position >= items_size) { + position = 0; + } + + model->position = position; + model->window_position = position; + + if(model->window_position > 0) { + model->window_position -= 1; + } + + const size_t items_on_screen = furi_string_empty(model->header) ? 4 : 3; + + if(items_size <= items_on_screen) { + model->window_position = 0; + } else { + const size_t pos = items_size - items_on_screen; + if(model->window_position > pos) { + model->window_position = pos; + } + } + }, + true); + } +} \ No newline at end of file diff --git a/views/action_menu.h b/views/action_menu.h new file mode 100644 index 00000000000..60659303010 --- /dev/null +++ b/views/action_menu.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** ActionMenu anonymous structure */ +typedef struct ActionMenu ActionMenu; + +/** ActionMenuItem anonymous structure */ +typedef struct ActionMenuItem ActionMenuItem; + +/** Callback for any button menu actions */ +typedef void (*ActionMenuItemCallback)(void* context, int32_t index, InputType type); + +/** Type of button. Difference in drawing buttons. */ +typedef enum { + ActionMenuItemTypeSubGHz, + ActionMenuItemTypeRFID, + ActionMenuItemTypeIR, + ActionMenuItemTypePlaylist, + ActionMenuItemTypeGroup, + ActionMenuItemTypeSettings, +} ActionMenuItemType; + +typedef enum { + ActionMenuLayoutPortrait, + ActionMenuLayoutLandscape, +} ActionMenuLayout; + +/** Get button menu view + * + * @param action_menu ActionMenu instance + * + * @return View instance that can be used for embedding + */ +View* action_menu_get_view(ActionMenu* action_menu); + +/** Clean button menu + * + * @param action_menu ActionMenu instance + */ +void action_menu_reset(ActionMenu* action_menu); + +/** Set the layout + * + * @param layout Portrait or Landscape +*/ +void action_menu_set_layout(ActionMenu* menu, ActionMenuLayout layout); + +/** Show/Hide icons in UI + * + * @param show_icons Show or Hide icons +*/ +void action_menu_set_show_icons(ActionMenu* menu, bool show_icons); + +/** Show/Hide header labels in UI + * + * @param show_headers Show or Hide header labels +*/ +void action_menu_set_show_headers(ActionMenu* menu, bool show_headers); + +/** Add item to button menu instance + * + * @param action_menu ActionMenu instance + * @param label text inside new button + * @param icon IconAnimation instance + * @param index value to distinct between buttons inside + * ActionMenuItemCallback + * @param callback The callback + * @param type type of button to create. Differ by button + * drawing. Control buttons have no frames, and + * have more squared borders. + * @param callback_context The callback context + * + * @return pointer to just-created item + */ +ActionMenuItem* action_menu_add_item( + ActionMenu* action_menu, + const char* label, + int32_t index, + ActionMenuItemCallback callback, + ActionMenuItemType type, + void* callback_context); + +/** Allocate and initialize new instance of ActionMenu model + * + * @return just-created ActionMenu model + */ +ActionMenu* action_menu_alloc(void); + +/** Free ActionMenu element + * + * @param action_menu ActionMenu instance + */ +void action_menu_free(ActionMenu* action_menu); + +/** Set ActionMenu header on top of canvas + * + * @param action_menu ActionMenu instance + * @param header header on the top of button menu + */ +void action_menu_set_header(ActionMenu* action_menu, const char* header); + +/** Set selected item + * + * @param action_menu ActionMenu instance + * @param index index of ActionMenu to be selected + */ +void action_menu_set_selected_item(ActionMenu* action_menu, uint32_t index); + +#ifdef __cplusplus +} +#endif \ No newline at end of file