Просмотр исходного кода

C++ apps: templated scene controller (#517)

* C++ apps: templated scene controller
* templated app: fix type names
* templated app: text store component
* Applications: add "Templated Scene" application
* templated app: refractoring
* Gui module byte input: fix docs
* templated app: new byte input scene
* templated app: dialog ex view module
* templated app: popup view module
* templated app: dialog-ex view module, fix docs
* templated app: text input view module
* Gui module text input: fix docs
* Furi: duplicated include
* templated app: record holder (controller) class
* templated app: view modules can now be accessed via cast
* templated app: remove unused includes
* templated app: fix return code
SG 4 лет назад
Родитель
Сommit
0b14db4fb3
30 измененных файлов с 1242 добавлено и 9 удалено
  1. 5 0
      applications/applications.c
  2. 1 1
      applications/gui/modules/byte_input.h
  3. 16 6
      applications/gui/modules/text_input.h
  4. 35 0
      applications/scened-app-example/scene/scened-app-scene-byte-input.cpp
  5. 19 0
      applications/scened-app-example/scene/scened-app-scene-byte-input.h
  6. 47 0
      applications/scened-app-example/scene/scened-app-scene-start.cpp
  7. 13 0
      applications/scened-app-example/scene/scened-app-scene-start.h
  8. 10 0
      applications/scened-app-example/scened-app-launcher.cpp
  9. 20 0
      applications/scened-app-example/scened-app.cpp
  10. 47 0
      applications/scened-app-example/scened-app.h
  11. 0 1
      core/furi.h
  12. 9 0
      lib/app-scened-template/generic-scene.hpp
  13. 46 0
      lib/app-scened-template/record-controller.hpp
  14. 178 0
      lib/app-scened-template/scene-controller.hpp
  15. 18 0
      lib/app-scened-template/text-store.cpp
  16. 12 0
      lib/app-scened-template/text-store.h
  17. 121 0
      lib/app-scened-template/typeindex_no_rtti.hpp
  18. 163 0
      lib/app-scened-template/view-controller.hpp
  19. 32 0
      lib/app-scened-template/view-modules/byte-input-vm.cpp
  20. 37 0
      lib/app-scened-template/view-modules/byte-input-vm.h
  21. 61 0
      lib/app-scened-template/view-modules/dialog-ex-vm.cpp
  22. 73 0
      lib/app-scened-template/view-modules/dialog-ex-vm.h
  23. 10 0
      lib/app-scened-template/view-modules/generic-view-module.h
  24. 54 0
      lib/app-scened-template/view-modules/popup-vm.cpp
  25. 68 0
      lib/app-scened-template/view-modules/popup-vm.h
  26. 33 0
      lib/app-scened-template/view-modules/submenu-vm.cpp
  27. 43 0
      lib/app-scened-template/view-modules/submenu-vm.h
  28. 30 0
      lib/app-scened-template/view-modules/text-input-vm.cpp
  29. 35 0
      lib/app-scened-template/view-modules/text-input-vm.h
  30. 6 1
      lib/lib.mk

+ 5 - 0
applications/applications.c

@@ -39,6 +39,7 @@ int32_t app_accessor(void* p);
 int32_t internal_storage_task(void* p);
 int32_t internal_storage_task(void* p);
 int32_t app_archive(void* p);
 int32_t app_archive(void* p);
 int32_t notification_app(void* p);
 int32_t notification_app(void* p);
+int32_t scened_app(void* p);
 
 
 // On system start hooks declaration
 // On system start hooks declaration
 void irda_cli_init();
 void irda_cli_init();
@@ -293,6 +294,10 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = {
      .stack_size = 1024,
      .stack_size = 1024,
      .icon = A_Plugins_14},
      .icon = A_Plugins_14},
 #endif
 #endif
+
+#ifdef APP_SCENED
+    {.app = scened_app, .name = "Templated Scene", .stack_size = 1024, .icon = A_Plugins_14},
+#endif
 };
 };
 
 
 const size_t FLIPPER_DEBUG_APPS_COUNT = sizeof(FLIPPER_DEBUG_APPS) / sizeof(FlipperApplication);
 const size_t FLIPPER_DEBUG_APPS_COUNT = sizeof(FLIPPER_DEBUG_APPS) / sizeof(FlipperApplication);

+ 1 - 1
applications/gui/modules/byte_input.h

@@ -46,7 +46,7 @@ void byte_input_free(ByteInput* byte_input);
 View* byte_input_get_view(ByteInput* byte_input);
 View* byte_input_get_view(ByteInput* byte_input);
 
 
 /** 
 /** 
- * @brief Deinitialize and free byte input
+ * @brief Set byte input result callback
  * 
  * 
  * @param byte_input byte input instance
  * @param byte_input byte input instance
  * @param input_callback input callback fn
  * @param input_callback input callback fn

+ 16 - 6
applications/gui/modules/text_input.h

@@ -9,23 +9,31 @@ extern "C" {
 typedef struct TextInput TextInput;
 typedef struct TextInput TextInput;
 typedef void (*TextInputCallback)(void* context, char* text);
 typedef void (*TextInputCallback)(void* context, char* text);
 
 
-/* Allocate and initialize text input
- * This text input is used to enter string
+/** 
+ * @brief Allocate and initialize text input
+ *        This text input is used to enter string
+ * 
  */
  */
 TextInput* text_input_alloc();
 TextInput* text_input_alloc();
 
 
-/* Deinitialize and free text input
+/** 
+ * @brief Deinitialize and free text input
+ * 
  * @param text_input - Text input instance
  * @param text_input - Text input instance
  */
  */
 void text_input_free(TextInput* text_input);
 void text_input_free(TextInput* text_input);
 
 
-/* Get text input view
+/**
+ * @brief Get text input view
+ * 
  * @param text_input - Text input instance
  * @param text_input - Text input instance
  * @return View instance that can be used for embedding
  * @return View instance that can be used for embedding
  */
  */
 View* text_input_get_view(TextInput* text_input);
 View* text_input_get_view(TextInput* text_input);
 
 
-/* Deinitialize and free text input
+/**
+ * @brief Set text input result callback
+ * 
  * @param text_input - Text input instance
  * @param text_input - Text input instance
  * @param callback - callback fn
  * @param callback - callback fn
  * @param callback_context - callback context
  * @param callback_context - callback context
@@ -39,7 +47,9 @@ void text_input_set_result_callback(
     char* text,
     char* text,
     uint8_t max_text_length);
     uint8_t max_text_length);
 
 
-/* Set text input header text
+/** 
+ * @brief Set text input header text
+ * 
  * @param text input - Text input instance
  * @param text input - Text input instance
  * @param text - text to be shown
  * @param text - text to be shown
  */
  */

+ 35 - 0
applications/scened-app-example/scene/scened-app-scene-byte-input.cpp

@@ -0,0 +1,35 @@
+#include "scened-app-scene-byte-input.h"
+
+void ScenedAppSceneByteInput::on_enter(ScenedApp* app, bool need_restore) {
+    ByteInputVM* byte_input = app->view_controller;
+    auto callback = cbc::obtain_connector(this, &ScenedAppSceneByteInput::result_callback);
+
+    byte_input->set_result_callback(callback, NULL, app, data, 4);
+    byte_input->set_header_text("Enter the key");
+
+    app->view_controller.switch_to<ByteInputVM>();
+}
+
+bool ScenedAppSceneByteInput::on_event(ScenedApp* app, ScenedApp::Event* event) {
+    bool consumed = false;
+
+    if(event->type == ScenedApp::EventType::ByteEditResult) {
+        app->scene_controller.switch_to_previous_scene();
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void ScenedAppSceneByteInput::on_exit(ScenedApp* app) {
+    app->view_controller.get<ByteInputVM>()->clean();
+}
+
+void ScenedAppSceneByteInput::result_callback(void* context, uint8_t* bytes, uint8_t bytes_count) {
+    ScenedApp* app = static_cast<ScenedApp*>(context);
+    ScenedApp::Event event;
+
+    event.type = ScenedApp::EventType::ByteEditResult;
+
+    app->view_controller.send_event(&event);
+}

+ 19 - 0
applications/scened-app-example/scene/scened-app-scene-byte-input.h

@@ -0,0 +1,19 @@
+#pragma once
+#include "../scened-app.h"
+
+class ScenedAppSceneByteInput : public GenericScene<ScenedApp> {
+public:
+    void on_enter(ScenedApp* app, bool need_restore) final;
+    bool on_event(ScenedApp* app, ScenedApp::Event* event) final;
+    void on_exit(ScenedApp* app) final;
+
+private:
+    void result_callback(void* context, uint8_t* bytes, uint8_t bytes_count);
+
+    uint8_t data[4] = {
+        0x01,
+        0xA2,
+        0xF4,
+        0xD3,
+    };
+};

+ 47 - 0
applications/scened-app-example/scene/scened-app-scene-start.cpp

@@ -0,0 +1,47 @@
+#include "scened-app-scene-start.h"
+
+typedef enum {
+    SubmenuByteInput,
+} SubmenuIndex;
+
+void ScenedAppSceneStart::on_enter(ScenedApp* app, bool need_restore) {
+    auto submenu = app->view_controller.get<SubmenuVM>();
+    auto callback = cbc::obtain_connector(this, &ScenedAppSceneStart::submenu_callback);
+
+    submenu->add_item("Byte Input", SubmenuByteInput, callback, app);
+
+    if(need_restore) {
+        submenu->set_selected_item(submenu_item_selected);
+    }
+    app->view_controller.switch_to<SubmenuVM>();
+}
+
+bool ScenedAppSceneStart::on_event(ScenedApp* app, ScenedApp::Event* event) {
+    bool consumed = false;
+
+    if(event->type == ScenedApp::EventType::MenuSelected) {
+        submenu_item_selected = event->payload.menu_index;
+        switch(event->payload.menu_index) {
+        case SubmenuByteInput:
+            app->scene_controller.switch_to_next_scene(ScenedApp::SceneType::ByteInputScene);
+            break;
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void ScenedAppSceneStart::on_exit(ScenedApp* app) {
+    app->view_controller.get<SubmenuVM>()->clean();
+}
+
+void ScenedAppSceneStart::submenu_callback(void* context, uint32_t index) {
+    ScenedApp* app = static_cast<ScenedApp*>(context);
+    ScenedApp::Event event;
+
+    event.type = ScenedApp::EventType::MenuSelected;
+    event.payload.menu_index = index;
+
+    app->view_controller.send_event(&event);
+}

+ 13 - 0
applications/scened-app-example/scene/scened-app-scene-start.h

@@ -0,0 +1,13 @@
+#pragma once
+#include "../scened-app.h"
+
+class ScenedAppSceneStart : public GenericScene<ScenedApp> {
+public:
+    void on_enter(ScenedApp* app, bool need_restore) final;
+    bool on_event(ScenedApp* app, ScenedApp::Event* event) final;
+    void on_exit(ScenedApp* app) final;
+
+private:
+    void submenu_callback(void* context, uint32_t index);
+    uint32_t submenu_item_selected = 0;
+};

+ 10 - 0
applications/scened-app-example/scened-app-launcher.cpp

@@ -0,0 +1,10 @@
+#include "scened-app.h"
+
+// app enter function
+extern "C" int32_t scened_app(void* p) {
+    ScenedApp* app = new ScenedApp();
+    app->run();
+    delete app;
+
+    return 0;
+}

+ 20 - 0
applications/scened-app-example/scened-app.cpp

@@ -0,0 +1,20 @@
+#include "scened-app.h"
+#include "scene/scened-app-scene-start.h"
+#include "scene/scened-app-scene-byte-input.h"
+
+ScenedApp::ScenedApp()
+    : scene_controller{this}
+    , text_store{128}
+    , notification{"notification"} {
+}
+
+ScenedApp::~ScenedApp() {
+}
+
+void ScenedApp::run() {
+    scene_controller.add_scene(SceneType::Start, new ScenedAppSceneStart());
+    scene_controller.add_scene(SceneType::ByteInputScene, new ScenedAppSceneByteInput());
+
+    notification_message(notification, &sequence_blink_green_10);
+    scene_controller.process(100);
+}

+ 47 - 0
applications/scened-app-example/scened-app.h

@@ -0,0 +1,47 @@
+#pragma once
+#include <furi.h>
+#include <api-hal.h>
+
+#include <generic-scene.hpp>
+#include <scene-controller.hpp>
+#include <view-controller.hpp>
+#include <record-controller.hpp>
+#include <text-store.h>
+
+#include <view-modules/submenu-vm.h>
+#include <view-modules/byte-input-vm.h>
+
+#include <notification/notification-messages.h>
+
+class ScenedApp {
+public:
+    enum class EventType : uint8_t {
+        GENERIC_EVENT_ENUM_VALUES,
+        MenuSelected,
+        ByteEditResult,
+    };
+
+    enum class SceneType : uint8_t {
+        GENERIC_SCENE_ENUM_VALUES,
+        ByteInputScene,
+    };
+
+    class Event {
+    public:
+        union {
+            int32_t menu_index;
+        } payload;
+
+        EventType type;
+    };
+
+    SceneController<GenericScene<ScenedApp>, ScenedApp> scene_controller;
+    TextStore text_store;
+    ViewController<ScenedApp, SubmenuVM, ByteInputVM> view_controller;
+    RecordController<NotificationApp> notification;
+
+    ~ScenedApp();
+    ScenedApp();
+
+    void run();
+};

+ 0 - 1
core/furi.h

@@ -11,7 +11,6 @@
 #include <furi/thread.h>
 #include <furi/thread.h>
 #include <furi/valuemutex.h>
 #include <furi/valuemutex.h>
 #include <furi/log.h>
 #include <furi/log.h>
-#include <furi/common_defines.h>
 
 
 #include <api-hal-gpio.h>
 #include <api-hal-gpio.h>
 #include <api-hal/api-interrupt-mgr.h>
 #include <api-hal/api-interrupt-mgr.h>

+ 9 - 0
lib/app-scened-template/generic-scene.hpp

@@ -0,0 +1,9 @@
+template <typename TApp> class GenericScene {
+public:
+    virtual void on_enter(TApp* app, bool need_restore) = 0;
+    virtual bool on_event(TApp* app, typename TApp::Event* event) = 0;
+    virtual void on_exit(TApp* app) = 0;
+    virtual ~GenericScene(){};
+
+private:
+};

+ 46 - 0
lib/app-scened-template/record-controller.hpp

@@ -0,0 +1,46 @@
+#pragma once
+#include <furi/record.h>
+
+/**
+ * @brief Class for opening, casting, holding and closing records
+ * 
+ * @tparam TRecordClass record class
+ */
+template <typename TRecordClass> class RecordController {
+public:
+    /**
+     * @brief Construct a new Record Controller object for record with record name
+     * 
+     * @param record_name record name
+     */
+    RecordController(const char* record_name) {
+        name = record_name;
+        value = static_cast<TRecordClass*>(furi_record_open(name));
+    };
+
+    ~RecordController() {
+        furi_record_close(name);
+    }
+
+    /**
+     * @brief Record getter
+     * 
+     * @return TRecordClass* record value
+     */
+    TRecordClass* get() {
+        return value;
+    }
+
+    /**
+     * @brief Record getter (by cast)
+     * 
+     * @return TRecordClass* record value
+     */
+    operator TRecordClass*() const {
+        return value;
+    }
+
+private:
+    const char* name;
+    TRecordClass* value;
+};

+ 178 - 0
lib/app-scened-template/scene-controller.hpp

@@ -0,0 +1,178 @@
+#include <map>
+#include <forward_list>
+#include <initializer_list>
+
+#define GENERIC_SCENE_ENUM_VALUES Uninitalized, Exit, Start
+#define GENERIC_EVENT_ENUM_VALUES Tick, Back
+
+/**
+ * @brief Controller for scene navigation in application
+ * 
+ * @tparam TScene generic scene class
+ * @tparam TApp application class
+ */
+template <typename TScene, typename TApp> class SceneController {
+public:
+    /**
+     * @brief Add scene to scene container
+     * 
+     * @param scene_index scene index
+     * @param scene_pointer scene object pointer
+     */
+    void add_scene(typename TApp::SceneType scene_index, TScene* scene_pointer) {
+        furi_check(scenes.count(scene_index) == 0);
+        scenes[scene_index] = scene_pointer;
+    }
+
+    /**
+     * @brief Switch to next scene and store current scene in previous scenes list
+     * 
+     * @param scene_index next scene index
+     * @param need_restore true, if we want the scene to restore its parameters
+     */
+    void switch_to_next_scene(typename TApp::SceneType scene_index, bool need_restore = false) {
+        previous_scenes_list.push_front(current_scene_index);
+        switch_to_scene(scene_index, need_restore);
+    }
+
+    /**
+     * @brief Switch to next scene without ability to return to current scene
+     * 
+     * @param scene_index next scene index
+     * @param need_restore true, if we want the scene to restore its parameters
+     */
+    void switch_to_scene(typename TApp::SceneType scene_index, bool need_restore = false) {
+        if(scene_index != TApp::SceneType::Exit) {
+            scenes[current_scene_index]->on_exit(app);
+            current_scene_index = scene_index;
+            scenes[current_scene_index]->on_enter(app, need_restore);
+        }
+    }
+
+    /**
+     * @brief Search the scene in the list of previous scenes and switch to it
+     * 
+     * @param scene_index_list list of scene indexes to which you want to switch
+     */
+    void search_and_switch_to_previous_scene(
+        const std::initializer_list<typename TApp::SceneType>& scene_index_list) {
+        auto previous_scene_index = TApp::SceneType::Start;
+        bool scene_found = false;
+
+        while(!scene_found) {
+            previous_scene_index = get_previous_scene_index();
+            for(const auto& element : scene_index_list) {
+                if(previous_scene_index == element) {
+                    scene_found = true;
+                    break;
+                }
+            }
+        }
+
+        switch_to_scene(previous_scene_index, true);
+    }
+
+    /**
+     * @brief Start application main cycle
+     * 
+     * @param tick_length_ms tick event length in milliseconds
+     */
+    void process(uint32_t tick_length_ms = 100) {
+        typename TApp::Event event;
+        bool consumed;
+        bool exit = false;
+
+        current_scene_index = TApp::SceneType::Start;
+        scenes[current_scene_index]->on_enter(app, false);
+
+        while(!exit) {
+            app->view_controller.receive_event(&event);
+
+            consumed = scenes[current_scene_index]->on_event(app, &event);
+
+            if(!consumed) {
+                if(event.type == TApp::EventType::Back) {
+                    exit = switch_to_previous_scene();
+                }
+            }
+        };
+
+        scenes[current_scene_index]->on_exit(app);
+    }
+
+    /**
+     * @brief Switch to previous scene
+     * 
+     * @param count how many steps back
+     * @return true if app need to exit
+     */
+    bool switch_to_previous_scene(uint8_t count = 1) {
+        auto previous_scene_index = TApp::SceneType::Start;
+
+        for(uint8_t i = 0; i < count; i++) previous_scene_index = get_previous_scene_index();
+
+        if(previous_scene_index == TApp::SceneType::Exit) return true;
+
+        switch_to_scene(previous_scene_index, true);
+        return false;
+    }
+
+    /**
+     * @brief Construct a new Scene Controller object
+     * 
+     * @param app_pointer pointer to application class
+     */
+    SceneController(TApp* app_pointer) {
+        app = app_pointer;
+        current_scene_index = TApp::SceneType::Uninitalized;
+    }
+
+    /**
+     * @brief Destroy the Scene Controller object
+     * 
+     */
+    ~SceneController() {
+        for(auto& it : scenes) delete it.second;
+    }
+
+private:
+    /**
+     * @brief Scenes pointers container
+     * 
+     */
+    std::map<typename TApp::SceneType, TScene*> scenes;
+
+    /**
+     * @brief List of indexes of previous scenes
+     * 
+     */
+    std::forward_list<typename TApp::SceneType> previous_scenes_list;
+
+    /**
+     * @brief Current scene index holder
+     * 
+     */
+    typename TApp::SceneType current_scene_index;
+
+    /**
+     * @brief Application pointer holder
+     * 
+     */
+    TApp* app;
+
+    /**
+     * @brief Get the previous scene index
+     * 
+     * @return previous scene index
+     */
+    typename TApp::SceneType get_previous_scene_index() {
+        auto scene_index = TApp::SceneType::Exit;
+
+        if(!previous_scenes_list.empty()) {
+            scene_index = previous_scenes_list.front();
+            previous_scenes_list.pop_front();
+        }
+
+        return scene_index;
+    }
+};

+ 18 - 0
lib/app-scened-template/text-store.cpp

@@ -0,0 +1,18 @@
+#include "text-store.h"
+#include <furi.h>
+
+TextStore::TextStore(uint8_t _text_size)
+    : text_size(_text_size) {
+    text = static_cast<char*>(malloc(text_size + 1));
+}
+
+TextStore::~TextStore() {
+    free(text);
+}
+
+void TextStore::set_text_store(const char* _text...) {
+    va_list args;
+    va_start(args, _text);
+    vsnprintf(text, text_size, _text, args);
+    va_end(args);
+}

+ 12 - 0
lib/app-scened-template/text-store.h

@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+class TextStore {
+public:
+    TextStore(uint8_t text_size);
+    ~TextStore();
+
+    void set_text_store(const char* text...);
+    const uint8_t text_size;
+    char* text;
+};

+ 121 - 0
lib/app-scened-template/typeindex_no_rtti.hpp

@@ -0,0 +1,121 @@
+/*
+ * type_index without RTTI
+ *
+ * Copyright frickiericker 2016.
+ * Distributed under the Boost Software License, Version 1.0.
+ *
+ * Permission is hereby granted, free of charge, to any person or organization
+ * obtaining a copy of the software and accompanying documentation covered by
+ * this license (the "Software") to use, reproduce, display, distribute,
+ * execute, and transmit the Software, and to prepare derivative works of the
+ * Software, and to permit third-parties to whom the Software is furnished to
+ * do so, all subject to the following:
+ *
+ * The copyright notices in the Software and this entire statement, including
+ * the above license grant, this restriction and the following disclaimer,
+ * must be included in all copies of the Software, in whole or in part, and
+ * all derivative works of the Software, unless such copies or derivative
+ * works are solely in the form of machine-executable object code generated by
+ * a source language processor.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+ * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include <functional>
+
+namespace ext {
+/**
+ * Dummy type for tag-dispatching.
+ */
+template <typename T> struct tag_type {};
+
+/**
+ * A value of tag_type<T>.
+ */
+template <typename T> constexpr tag_type<T> tag{};
+
+/**
+ * A type_index implementation without RTTI.
+ */
+struct type_index {
+    /**
+     * Creates a type_index object for the specified type.
+     */
+    template <typename T> type_index(tag_type<T>) noexcept : hash_code_{index<T>} {
+    }
+
+    /**
+     * Returns the hash code.
+     */
+    std::size_t hash_code() const noexcept {
+        return hash_code_;
+    }
+
+private:
+    /**
+     * Unique integral index associated to template type argument.
+     */
+    template <typename T> static std::size_t const index;
+
+    /**
+     * Global counter for generating index values.
+     */
+    static std::size_t& counter() noexcept {
+        static std::size_t counter_;
+        return counter_;
+    }
+
+private:
+    std::size_t hash_code_;
+};
+
+template <typename> std::size_t const type_index::index = type_index::counter()++;
+
+/**
+ * Creates a type_index object for the specified type.
+ *
+ * Equivalent to `ext::type_index{ext::tag<T>}`.
+ */
+template <typename T> type_index make_type_index() noexcept {
+    return tag<T>;
+}
+
+inline bool operator==(type_index const& a, type_index const& b) noexcept {
+    return a.hash_code() == b.hash_code();
+}
+
+inline bool operator!=(type_index const& a, type_index const& b) noexcept {
+    return !(a == b);
+}
+
+inline bool operator<(type_index const& a, type_index const& b) noexcept {
+    return a.hash_code() < b.hash_code();
+}
+
+inline bool operator<=(type_index const& a, type_index const& b) noexcept {
+    return a.hash_code() <= b.hash_code();
+}
+
+inline bool operator>(type_index const& a, type_index const& b) noexcept {
+    return !(a <= b);
+}
+
+inline bool operator>=(type_index const& a, type_index const& b) noexcept {
+    return !(a < b);
+}
+}
+
+template <> struct std::hash<ext::type_index> {
+    using argument_type = ext::type_index;
+    using result_type = std::size_t;
+
+    result_type operator()(argument_type const& t) const noexcept {
+        return t.hash_code();
+    }
+};

+ 163 - 0
lib/app-scened-template/view-controller.hpp

@@ -0,0 +1,163 @@
+#pragma once
+#include "view-modules/generic-view-module.h"
+#include <map>
+#include <furi/check.h>
+#include <gui/view_dispatcher.h>
+#include <callback-connector.h>
+#include "typeindex_no_rtti.hpp"
+
+/**
+ * @brief Controller for switching application views and handling inputs and events
+ * 
+ * @tparam TApp application class
+ * @tparam TViewModules variadic list of ViewModules
+ */
+template <typename TApp, typename... TViewModules> class ViewController {
+public:
+    ViewController() {
+        event_queue = osMessageQueueNew(10, sizeof(typename TApp::Event), NULL);
+
+        view_dispatcher = view_dispatcher_alloc();
+        previous_view_callback_pointer = cbc::obtain_connector(
+            this, &ViewController<TApp, TViewModules...>::previous_view_callback);
+
+        [](...) {
+        }((this->add_view(ext::make_type_index<TViewModules>().hash_code(), new TViewModules()),
+           0)...);
+
+        gui = static_cast<Gui*>(furi_record_open("gui"));
+        view_dispatcher_attach_to_gui(view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    };
+
+    ~ViewController() {
+        for(auto& it : holder) {
+            view_dispatcher_remove_view(view_dispatcher, static_cast<uint32_t>(it.first));
+            delete it.second;
+        }
+
+        view_dispatcher_free(view_dispatcher);
+        osMessageQueueDelete(event_queue);
+    }
+
+    /**
+     * @brief Get ViewModule pointer
+     * 
+     * @tparam T Concrete ViewModule class
+     * @return T* ViewModule pointer
+     */
+    template <typename T> T* get() {
+        uint32_t view_index = ext::make_type_index<T>().hash_code();
+        furi_check(holder.count(view_index) != 0);
+        return static_cast<T*>(holder[view_index]);
+    }
+
+    /**
+     * @brief Get ViewModule pointer by cast
+     * 
+     * @tparam T Concrete ViewModule class
+     * @return T* ViewModule pointer
+     */
+    template <typename T> operator T*() {
+        uint32_t view_index = ext::make_type_index<T>().hash_code();
+        furi_check(holder.count(view_index) != 0);
+        return static_cast<T*>(holder[view_index]);
+    }
+
+    /**
+     * @brief Switch view to ViewModule
+     * 
+     * @tparam T Concrete ViewModule class
+     * @return T* ViewModule pointer
+     */
+    template <typename T> void switch_to() {
+        uint32_t view_index = ext::make_type_index<T>().hash_code();
+        furi_check(holder.count(view_index) != 0);
+        view_dispatcher_switch_to_view(view_dispatcher, view_index);
+    }
+
+    /**
+     * @brief Receive event from app event queue
+     * 
+     * @param event event pointer
+     */
+    void receive_event(typename TApp::Event* event) {
+        if(osMessageQueueGet(event_queue, event, NULL, 100) != osOK) {
+            event->type = TApp::EventType::Tick;
+        }
+    }
+
+    /**
+     * @brief Send event to app event queue
+     * 
+     * @param event event pointer
+     */
+    void send_event(typename TApp::Event* event) {
+        osStatus_t result = osMessageQueuePut(event_queue, event, 0, osWaitForever);
+        furi_check(result == osOK);
+    }
+
+private:
+    /**
+     * @brief ViewModulesHolder
+     * 
+     */
+    std::map<size_t, GenericViewModule*> holder;
+
+    /**
+     * @brief App event queue
+     * 
+     */
+    osMessageQueueId_t event_queue;
+
+    /**
+     * @brief Main ViewDispatcher pointer
+     * 
+     */
+    ViewDispatcher* view_dispatcher;
+
+    /**
+     * @brief Gui record pointer
+     * 
+     */
+    Gui* gui;
+
+    /**
+     * @brief Previous view callback fn pointer
+     * 
+     */
+    ViewNavigationCallback previous_view_callback_pointer;
+
+    /**
+     * @brief Previous view callback fn
+     * 
+     * @param context not used
+     * @return uint32_t VIEW_IGNORE
+     */
+    uint32_t previous_view_callback(void* context) {
+        (void)context;
+
+        typename TApp::Event event;
+        event.type = TApp::EventType::Back;
+
+        if(event_queue != NULL) {
+            send_event(&event);
+        }
+
+        return VIEW_IGNORE;
+    }
+
+    /**
+     * @brief Add ViewModule to holder
+     * 
+     * @param view_index view index in holder
+     * @param view_module view module pointer
+     */
+    void add_view(size_t view_index, GenericViewModule* view_module) {
+        furi_check(holder.count(view_index) == 0);
+        holder[view_index] = view_module;
+
+        View* view = view_module->get_view();
+        view_dispatcher_add_view(view_dispatcher, static_cast<uint32_t>(view_index), view);
+        view_set_previous_callback(view, previous_view_callback_pointer);
+    }
+};

+ 32 - 0
lib/app-scened-template/view-modules/byte-input-vm.cpp

@@ -0,0 +1,32 @@
+#include "byte-input-vm.h"
+
+ByteInputVM::ByteInputVM() {
+    byte_input = byte_input_alloc();
+}
+
+ByteInputVM::~ByteInputVM() {
+    byte_input_free(byte_input);
+}
+
+View* ByteInputVM::get_view() {
+    return byte_input_get_view(byte_input);
+}
+
+void ByteInputVM::clean() {
+    byte_input_set_header_text(byte_input, "");
+    byte_input_set_result_callback(byte_input, NULL, NULL, NULL, NULL, 0);
+}
+
+void ByteInputVM::set_result_callback(
+    ByteInputCallback input_callback,
+    ByteChangedCallback changed_callback,
+    void* callback_context,
+    uint8_t* bytes,
+    uint8_t bytes_count) {
+    byte_input_set_result_callback(
+        byte_input, input_callback, changed_callback, callback_context, bytes, bytes_count);
+}
+
+void ByteInputVM::set_header_text(const char* text) {
+    byte_input_set_header_text(byte_input, text);
+}

+ 37 - 0
lib/app-scened-template/view-modules/byte-input-vm.h

@@ -0,0 +1,37 @@
+#pragma once
+#include "generic-view-module.h"
+#include <gui/modules/byte_input.h>
+
+class ByteInputVM : public GenericViewModule {
+public:
+    ByteInputVM();
+    ~ByteInputVM() final;
+    View* get_view() final;
+    void clean() final;
+
+    /** 
+     * @brief Set byte input result callback
+     * 
+     * @param input_callback input callback fn
+     * @param changed_callback changed callback fn
+     * @param callback_context callback context
+     * @param bytes buffer to use
+     * @param bytes_count buffer length
+     */
+    void set_result_callback(
+        ByteInputCallback input_callback,
+        ByteChangedCallback changed_callback,
+        void* callback_context,
+        uint8_t* bytes,
+        uint8_t bytes_count);
+
+    /**
+     * @brief Set byte input header text
+     * 
+     * @param text text to be shown
+     */
+    void set_header_text(const char* text);
+
+private:
+    ByteInput* byte_input;
+};

+ 61 - 0
lib/app-scened-template/view-modules/dialog-ex-vm.cpp

@@ -0,0 +1,61 @@
+#include "dialog-ex-vm.h"
+
+DialogExVM::DialogExVM() {
+    dialog_ex = dialog_ex_alloc();
+}
+
+DialogExVM::~DialogExVM() {
+    dialog_ex_free(dialog_ex);
+}
+
+View* DialogExVM::get_view() {
+    return dialog_ex_get_view(dialog_ex);
+}
+
+void DialogExVM::clean() {
+    set_result_callback(NULL);
+    set_context(NULL);
+    set_header(NULL, 0, 0, AlignLeft, AlignBottom);
+    set_text(NULL, 0, 0, AlignLeft, AlignBottom);
+    set_icon(-1, -1, I_ButtonCenter_7x7);
+    set_left_button_text(NULL);
+    set_center_button_text(NULL);
+    set_right_button_text(NULL);
+}
+
+void DialogExVM::set_result_callback(DialogExResultCallback callback) {
+    dialog_ex_set_result_callback(dialog_ex, callback);
+}
+
+void DialogExVM::set_context(void* context) {
+    dialog_ex_set_context(dialog_ex, context);
+}
+
+void DialogExVM::set_header(
+    const char* text,
+    uint8_t x,
+    uint8_t y,
+    Align horizontal,
+    Align vertical) {
+    dialog_ex_set_header(dialog_ex, text, x, y, horizontal, vertical);
+}
+
+void DialogExVM::set_text(const char* text, uint8_t x, uint8_t y, Align horizontal, Align vertical) {
+    dialog_ex_set_text(dialog_ex, text, x, y, horizontal, vertical);
+}
+
+void DialogExVM::set_icon(int8_t x, int8_t y, IconName name) {
+    dialog_ex_set_icon(dialog_ex, x, y, name);
+}
+
+void DialogExVM::set_left_button_text(const char* text) {
+    dialog_ex_set_left_button_text(dialog_ex, text);
+}
+
+void DialogExVM::set_center_button_text(const char* text) {
+    dialog_ex_set_center_button_text(dialog_ex, text);
+}
+
+void DialogExVM::set_right_button_text(const char* text) {
+    dialog_ex_set_right_button_text(dialog_ex, text);
+}

+ 73 - 0
lib/app-scened-template/view-modules/dialog-ex-vm.h

@@ -0,0 +1,73 @@
+#pragma once
+#include "generic-view-module.h"
+#include <gui/modules/dialog_ex.h>
+
+class DialogExVM : public GenericViewModule {
+public:
+    DialogExVM();
+    ~DialogExVM() final;
+    View* get_view() final;
+    void clean() final;
+
+    /** 
+     * Set dialog result callback
+     * @param callback - result callback function
+     */
+    void set_result_callback(DialogExResultCallback callback);
+
+    /** 
+     * Set dialog context
+     * @param context - context pointer, will be passed to result callback
+     */
+    void set_context(void* context);
+
+    /** 
+     * Set dialog header text
+     * If text is null, dialog header will not be rendered
+     * @param text - text to be shown, can be multiline
+     * @param x, y - text position
+     * @param horizontal, vertical - text aligment
+     */
+    void set_header(const char* text, uint8_t x, uint8_t y, Align horizontal, Align vertical);
+
+    /** 
+     * Set dialog text
+     * If text is null, dialog text will not be rendered
+     * @param text - text to be shown, can be multiline
+     * @param x, y - text position
+     * @param horizontal, vertical - text aligment
+     */
+    void set_text(const char* text, uint8_t x, uint8_t y, Align horizontal, Align vertical);
+
+    /** 
+     * Set dialog icon
+     * If x or y is negative, dialog icon will not be rendered
+     * @param x, y - icon position
+     * @param name - icon to be shown
+     */
+    void set_icon(int8_t x, int8_t y, IconName name);
+
+    /**
+     * Set left button text
+     * If text is null, left button will not be rendered and processed
+     * @param text - text to be shown
+     */
+    void set_left_button_text(const char* text);
+
+    /** 
+     * Set center button text
+     * If text is null, center button will not be rendered and processed
+     * @param text - text to be shown
+     */
+    void set_center_button_text(const char* text);
+
+    /**
+     * Set right button text
+     * If text is null, right button will not be rendered and processed
+     * @param text - text to be shown
+     */
+    void set_right_button_text(const char* text);
+
+private:
+    DialogEx* dialog_ex;
+};

+ 10 - 0
lib/app-scened-template/view-modules/generic-view-module.h

@@ -0,0 +1,10 @@
+#pragma once
+#include <gui/view.h>
+
+class GenericViewModule {
+public:
+    GenericViewModule(){};
+    virtual ~GenericViewModule(){};
+    virtual View* get_view() = 0;
+    virtual void clean() = 0;
+};

+ 54 - 0
lib/app-scened-template/view-modules/popup-vm.cpp

@@ -0,0 +1,54 @@
+#include "popup-vm.h"
+PopupVM::PopupVM() {
+    popup = popup_alloc();
+}
+
+PopupVM::~PopupVM() {
+    popup_free(popup);
+}
+
+View* PopupVM::get_view() {
+    return popup_get_view(popup);
+}
+
+void PopupVM::clean() {
+    set_callback(NULL);
+    set_context(NULL);
+    set_header(NULL, 0, 0, AlignLeft, AlignBottom);
+    set_text(NULL, 0, 0, AlignLeft, AlignBottom);
+    set_icon(-1, -1, I_ButtonCenter_7x7);
+    disable_timeout();
+    set_timeout(1000);
+}
+
+void PopupVM::set_callback(PopupCallback callback) {
+    popup_set_callback(popup, callback);
+}
+
+void PopupVM::set_context(void* context) {
+    popup_set_context(popup, context);
+}
+
+void PopupVM::set_header(const char* text, uint8_t x, uint8_t y, Align horizontal, Align vertical) {
+    popup_set_header(popup, text, x, y, horizontal, vertical);
+}
+
+void PopupVM::set_text(const char* text, uint8_t x, uint8_t y, Align horizontal, Align vertical) {
+    popup_set_text(popup, text, x, y, horizontal, vertical);
+}
+
+void PopupVM::set_icon(int8_t x, int8_t y, IconName name) {
+    popup_set_icon(popup, x, y, name);
+}
+
+void PopupVM::set_timeout(uint32_t timeout_in_ms) {
+    popup_set_timeout(popup, timeout_in_ms);
+}
+
+void PopupVM::enable_timeout() {
+    popup_enable_timeout(popup);
+}
+
+void PopupVM::disable_timeout() {
+    popup_enable_timeout(popup);
+}

+ 68 - 0
lib/app-scened-template/view-modules/popup-vm.h

@@ -0,0 +1,68 @@
+#pragma once
+#include "generic-view-module.h"
+#include <gui/modules/popup.h>
+
+class PopupVM : public GenericViewModule {
+public:
+    PopupVM();
+    ~PopupVM() final;
+    View* get_view() final;
+    void clean() final;
+
+    /** 
+     * Set popup header text
+     * @param text - text to be shown
+     */
+    void set_callback(PopupCallback callback);
+
+    /** 
+     * Set popup context
+     * @param context - context pointer, will be passed to result callback
+     */
+    void set_context(void* context);
+
+    /** 
+     * Set popup header text
+     * If text is null, popup header will not be rendered
+     * @param text - text to be shown, can be multiline
+     * @param x, y - text position
+     * @param horizontal, vertical - text aligment
+     */
+    void set_header(const char* text, uint8_t x, uint8_t y, Align horizontal, Align vertical);
+
+    /** 
+     * Set popup text
+     * If text is null, popup text will not be rendered
+     * @param text - text to be shown, can be multiline
+     * @param x, y - text position
+     * @param horizontal, vertical - text aligment
+     */
+    void set_text(const char* text, uint8_t x, uint8_t y, Align horizontal, Align vertical);
+
+    /** 
+     * Set popup icon
+     * If icon position is negative, popup icon will not be rendered
+     * @param x, y - icon position
+     * @param name - icon to be shown
+     */
+    void set_icon(int8_t x, int8_t y, IconName name);
+
+    /** 
+     * Set popup timeout
+     * @param timeout_in_ms - popup timeout value in milliseconds
+     */
+    void set_timeout(uint32_t timeout_in_ms);
+
+    /** 
+     * Enable popup timeout
+     */
+    void enable_timeout();
+
+    /** 
+     * Disable popup timeout
+     */
+    void disable_timeout();
+
+private:
+    Popup* popup;
+};

+ 33 - 0
lib/app-scened-template/view-modules/submenu-vm.cpp

@@ -0,0 +1,33 @@
+#include "submenu-vm.h"
+
+SubmenuVM::SubmenuVM() {
+    submenu = submenu_alloc();
+}
+
+SubmenuVM::~SubmenuVM() {
+    submenu_free(submenu);
+}
+
+View* SubmenuVM::get_view() {
+    return submenu_get_view(submenu);
+}
+
+void SubmenuVM::clean() {
+    submenu_clean(submenu);
+}
+
+SubmenuItem* SubmenuVM::add_item(
+    const char* label,
+    uint32_t index,
+    SubmenuItemCallback callback,
+    void* callback_context) {
+    return submenu_add_item(submenu, label, index, callback, callback_context);
+}
+
+void SubmenuVM::set_selected_item(uint32_t index) {
+    submenu_set_selected_item(submenu, index);
+}
+
+void SubmenuVM::set_header(const char* header) {
+    submenu_set_header(submenu, header);
+}

+ 43 - 0
lib/app-scened-template/view-modules/submenu-vm.h

@@ -0,0 +1,43 @@
+#pragma once
+#include "generic-view-module.h"
+#include <gui/modules/submenu.h>
+
+class SubmenuVM : public GenericViewModule {
+public:
+    SubmenuVM();
+    ~SubmenuVM() final;
+    View* get_view() final;
+    void clean() final;
+
+    /**
+     * @brief Add item to submenu
+     * 
+     * @param label - menu item label
+     * @param index - menu item index, used for callback, may be the same with other items
+     * @param callback - menu item callback
+     * @param callback_context - menu item callback context
+     * @return SubmenuItem instance that can be used to modify or delete that item
+     */
+    SubmenuItem* add_item(
+        const char* label,
+        uint32_t index,
+        SubmenuItemCallback callback,
+        void* callback_context);
+
+    /**
+     * @brief Set submenu item selector
+     * 
+     * @param index index of the item to be selected
+     */
+    void set_selected_item(uint32_t index);
+
+    /**
+     * @brief Set optional header for submenu
+     * 
+     * @param header header to set
+     */
+    void set_header(const char* header);
+
+private:
+    Submenu* submenu;
+};

+ 30 - 0
lib/app-scened-template/view-modules/text-input-vm.cpp

@@ -0,0 +1,30 @@
+#include "text-input-vm.h"
+
+TextInputVM::TextInputVM() {
+    text_input = text_input_alloc();
+}
+
+TextInputVM::~TextInputVM() {
+    text_input_free(text_input);
+}
+
+View* TextInputVM::get_view() {
+    return text_input_get_view(text_input);
+}
+
+void TextInputVM::clean() {
+    set_result_callback(NULL, NULL, NULL, 0);
+    set_header_text("");
+}
+
+void TextInputVM::set_result_callback(
+    TextInputCallback callback,
+    void* callback_context,
+    char* text,
+    uint8_t max_text_length) {
+    text_input_set_result_callback(text_input, callback, callback_context, text, max_text_length);
+}
+
+void TextInputVM::set_header_text(const char* text) {
+    text_input_set_header_text(text_input, text);
+}

+ 35 - 0
lib/app-scened-template/view-modules/text-input-vm.h

@@ -0,0 +1,35 @@
+#pragma once
+#include "generic-view-module.h"
+#include <gui/modules/text_input.h>
+
+class TextInputVM : public GenericViewModule {
+public:
+    TextInputVM();
+    ~TextInputVM() final;
+    View* get_view() final;
+    void clean() final;
+
+    /**
+     * @brief Set text input result callback
+     * 
+     * @param callback - callback fn
+     * @param callback_context - callback context
+     * @param text - text buffer to use
+     * @param max_text_length - text buffer length
+     */
+    void set_result_callback(
+        TextInputCallback callback,
+        void* callback_context,
+        char* text,
+        uint8_t max_text_length);
+
+    /** 
+     * @brief Set text input header text
+     * 
+     * @param text - text to be shown
+     */
+    void set_header_text(const char* text);
+
+private:
+    TextInput* text_input;
+};

+ 6 - 1
lib/lib.mk

@@ -94,4 +94,9 @@ C_SOURCES		+= $(wildcard $(LIB_DIR)/irda/*/*.c)
 
 
 #args lib
 #args lib
 CFLAGS			+= -I$(LIB_DIR)/args
 CFLAGS			+= -I$(LIB_DIR)/args
-C_SOURCES		+= $(wildcard $(LIB_DIR)/args/*.c)
+C_SOURCES		+= $(wildcard $(LIB_DIR)/args/*.c)
+
+#scened app template lib
+CFLAGS			+= -I$(LIB_DIR)/app-scened-template
+CPP_SOURCES		+= $(wildcard $(LIB_DIR)/app-scened-template/*.cpp)
+CPP_SOURCES		+= $(wildcard $(LIB_DIR)/app-scened-template/*/*.cpp)