rdefeo 1 год назад
Родитель
Сommit
a7c438633c

+ 10 - 10
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);

BIN
images/ArrowDown_8x4.png


BIN
images/ArrowUp_8x4.png


BIN
images/Directory_10px.png


BIN
images/IR_10px.png


BIN
images/NFC_10px.png


BIN
images/Playlist_10px.png


BIN
images/RFID_10px.png


BIN
images/Settings_10px.png


BIN
images/SubGHz_10px.png


+ 17 - 11
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);

+ 8 - 1
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;

+ 27 - 4
quac.c

@@ -5,6 +5,7 @@
 #include <gui/scene_manager.h>
 #include <gui/modules/button_menu.h>
 #include <gui/modules/dialog_ex.h>
+#include <gui/modules/variable_item_list.h>
 
 #include <storage/storage.h>
 #include <notification/notification_messages.h>
@@ -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 <quac_icons.h>
@@ -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);

+ 16 - 0
quac.h

@@ -4,9 +4,13 @@
 #include <gui/view_dispatcher.h>
 #include <gui/modules/button_menu.h>
 #include <gui/modules/dialog_ex.h>
+#include <gui/modules/variable_item_list.h>
+
 #include <storage/storage.h>
 #include <notification/notification_messages.h>
 
+#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();

+ 142 - 0
quac_settings.c

@@ -0,0 +1,142 @@
+#include "quac_settings.h"
+
+#include <flipper_format/flipper_format.h>
+
+// 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);
+}

+ 9 - 0
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);

+ 93 - 47
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 <lib/toolbox/path.h>
 
 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);
 }

+ 133 - 0
scenes/scene_settings.c

@@ -0,0 +1,133 @@
+#include <furi.h>
+
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/variable_item_list.h>
+#include <toolbox/value_index.h>
+
+#include "quac.h"
+#include "scenes.h"
+#include "scene_settings.h"
+#include "../actions/action.h"
+#include "../views/action_menu.h"
+#include "../quac_settings.h"
+
+#include <lib/toolbox/path.h>
+
+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);
+}

+ 10 - 0
scenes/scene_settings.h

@@ -0,0 +1,10 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// 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);

+ 7 - 3
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;

+ 7 - 5
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;

+ 611 - 0
views/action_menu.c

@@ -0,0 +1,611 @@
+#include "action_menu.h"
+
+#include <gui/canvas.h>
+#include <gui/elements.h>
+#include <input/input.h>
+
+#include <furi.h>
+
+#include <stdint.h>
+#include <m-array.h>
+
+#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);
+    }
+}

+ 117 - 0
views/action_menu.h

@@ -0,0 +1,117 @@
+#pragma once
+
+#include <stdint.h>
+#include <gui/view.h>
+
+#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