diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 8ef50f61..35c75925 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -1,6 +1,5 @@ # TODOs - AppContext's onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched -- Create AlertDialog app like on Android with title, message and optional buttons. Update WifiApSettings with it. - Loader: Use Timer instead of Thread, and move API to `tt::app::` - Gpio: Use Timer instead of Thread - I2cScannerThread: Use Timer instead of Thread @@ -36,7 +35,6 @@ # App Ideas - System logger -- Add FreeRTOS task manager functionality to System Info app - BlueTooth keyboard app - Chip 8 emulator - BadUSB (in December 2024, TinyUSB has a bug where uninstalling and re-installing the driver fails) diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index d1f69763..b5234a52 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -1,4 +1,3 @@ -#include #include "Tactility.h" #include "app/ManifestRegistry.h" @@ -36,6 +35,7 @@ static const std::vector system_services = { // region Default apps namespace app { + namespace alertdialog { extern const AppManifest manifest; } namespace boot { extern const AppManifest manifest; } namespace desktop { extern const AppManifest manifest; } namespace files { extern const AppManifest manifest; } @@ -63,6 +63,7 @@ extern const app::AppManifest screenshot_app; #endif static const std::vector system_apps = { + &app::alertdialog::manifest, &app::boot::manifest, &app::desktop::manifest, &app::display::manifest, diff --git a/Tactility/Source/app/alertdialog/AlertDialog.cpp b/Tactility/Source/app/alertdialog/AlertDialog.cpp new file mode 100644 index 00000000..16778f12 --- /dev/null +++ b/Tactility/Source/app/alertdialog/AlertDialog.cpp @@ -0,0 +1,125 @@ +#include "AlertDialog.h" +#include "lvgl.h" +#include "lvgl/Toolbar.h" +#include "service/loader/Loader.h" +#include +#include + +namespace tt::app::alertdialog { + +#define PARAMETER_BUNDLE_KEY_TITLE "title" +#define PARAMETER_BUNDLE_KEY_MESSAGE "message" +#define PARAMETER_BUNDLE_KEY_BUTTON_LABELS "buttonLabels" +#define RESULT_BUNDLE_KEY_INDEX "index" + +#define PARAMETER_ITEM_CONCATENATION_TOKEN ";;" +#define DEFAULT_TITLE "Select..." + +#define TAG "selection_dialog" + +extern const AppManifest manifest; + +void start(std::string title, std::string message, const std::vector& buttonLabels) { + std::string items_joined = string::join(buttonLabels, PARAMETER_ITEM_CONCATENATION_TOKEN); + auto bundle = std::make_shared(); + bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title); + bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); + bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined); + service::loader::startApp(manifest.id, false, bundle); +} + +int32_t getResultIndex(const Bundle& bundle) { + int32_t index = -1; + bundle.optInt32(RESULT_BUNDLE_KEY_INDEX, index); + return index; +} + +void setResultIndex(std::shared_ptr bundle, int32_t index) { + bundle->putInt32(RESULT_BUNDLE_KEY_INDEX, index); +} + +static std::string getTitleParameter(std::shared_ptr bundle) { + std::string result; + if (bundle->optString(PARAMETER_BUNDLE_KEY_TITLE, result)) { + return result; + } else { + return DEFAULT_TITLE; + } +} + +static void onButtonClicked(lv_event_t* e) { + lv_event_code_t code = lv_event_get_code(e); + if (code == LV_EVENT_CLICKED) { + size_t index = (size_t)(e->user_data); + TT_LOG_I(TAG, "Selected item at index %d", index); + tt::app::AppContext* app = service::loader::getCurrentApp(); + auto bundle = std::make_shared(); + setResultIndex(bundle, (int32_t)index); + app->setResult(app::ResultOk, bundle); + service::loader::stopApp(); + } +} + +static void createButton(lv_obj_t* parent, const std::string& text, size_t index) { + lv_obj_t* button = lv_button_create(parent); + lv_obj_t* button_label = lv_label_create(button); + lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); + lv_label_set_text(button_label, text.c_str()); + lv_obj_add_event_cb(button, &onButtonClicked, LV_EVENT_CLICKED, (void*)index); +} + +static void onShow(AppContext& app, lv_obj_t* parent) { + auto parameters = app.getParameters(); + tt_check(parameters != nullptr, "Parameters missing"); + + std::string title = getTitleParameter(app.getParameters()); + lv_obj_t* toolbar = lvgl::toolbar_create(parent, title); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t* message_label = lv_label_create(parent); + lv_obj_align(message_label, LV_ALIGN_CENTER, 0, 0); + + std::string message; + if (parameters->optString(PARAMETER_BUNDLE_KEY_MESSAGE, message)) { + lv_label_set_text(message_label, message.c_str()); + lv_label_set_long_mode(message_label, LV_LABEL_LONG_WRAP); + } + + lv_obj_t* button_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(button_wrapper, 0, 0); + lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(button_wrapper, 0, 0); + lv_obj_align(button_wrapper, LV_ALIGN_BOTTOM_MID, 0, -4); + + std::string items_concatenated; + if (parameters->optString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_concatenated)) { + std::vector labels = string::split(items_concatenated, PARAMETER_ITEM_CONCATENATION_TOKEN); + if (labels.empty() || labels.front().empty()) { + TT_LOG_E(TAG, "No items provided"); + app.setResult(ResultError); + service::loader::stopApp(); + } else if (labels.size() == 1) { + auto result_bundle = std::make_shared(); + setResultIndex(result_bundle, 0); + app.setResult(ResultOk, result_bundle); + service::loader::stopApp(); + TT_LOG_W(TAG, "Auto-selecting single item"); + } else { + size_t index = 0; + for (const auto& label: labels) { + createButton(button_wrapper, label, index++); + } + } + } +} + +extern const AppManifest manifest = { + .id = "AlertDialog", + .name = "Alert Dialog", + .type = TypeHidden, + .onShow = onShow +}; + +} diff --git a/Tactility/Source/app/alertdialog/AlertDialog.h b/Tactility/Source/app/alertdialog/AlertDialog.h new file mode 100644 index 00000000..057f84c7 --- /dev/null +++ b/Tactility/Source/app/alertdialog/AlertDialog.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include "Bundle.h" + +/** + * Start the app by its ID and provide: + * - a title + * - a text + * - 0, 1 or more buttons + */ +namespace tt::app::alertdialog { + + void start(std::string title, std::string message, const std::vector& buttonLabels); + + /** + * Get the index of the button that the user selected. + * + * @return a value greater than 0 when a selection was done, or -1 when the app was closed clicking one of the selection buttons. + */ + int32_t getResultIndex(const Bundle& bundle); +} diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp index e06851df..94ee4e6b 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp @@ -109,15 +109,3 @@ extern const AppManifest manifest = { }; } -extern "C" { - -extern void tt_app_selectiondialog_start2(const char* title, int argc, const char* argv[]) { - std::vector list; - for (int i = 0; i < argc; i++) { - const char* item = argv[i]; - list.push_back(item); - } - tt::app::selectiondialog::start(title, list); -} - -} diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.h b/Tactility/Source/app/selectiondialog/SelectionDialog.h index d81ddc36..2cd82048 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.h +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.h @@ -16,7 +16,10 @@ namespace tt::app::selectiondialog { void start(std::string title, const std::vector& items); - /** App result data */ - + /** + * Get the index of the item that the user selected. + * + * @return a value greater than 0 when a selection was done, or -1 when the app was closed without selecting an item. + */ int32_t getResultIndex(const Bundle& bundle); } diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 9bf597a5..1da662d4 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -1,7 +1,7 @@ #include "WifiApSettings.h" #include "TactilityCore.h" #include "app/AppContext.h" -#include "app/selectiondialog/SelectionDialog.h" +#include "app/alertdialog/AlertDialog.h" #include "lvgl.h" #include "lvgl/Style.h" #include "lvgl/Toolbar.h" @@ -30,12 +30,12 @@ void start(const std::string& ssid) { service::loader::startApp(manifest.id, false, bundle); } -static void onPressForget(lv_event_t* event) { +static void onPressForget(TT_UNUSED lv_event_t* event) { std::vector choices = { "Yes", "No" }; - selectiondialog::start("Are you sure?", choices); + alertdialog::start("Confirmation", "Forget the Wi-Fi access point?", choices); } static void onToggleAutoConnect(lv_event_t* event) { @@ -118,8 +118,8 @@ static void onShow(AppContext& app, lv_obj_t* parent) { } } -void onResult(AppContext& app, Result result, const Bundle& bundle) { - auto index = selectiondialog::getResultIndex(bundle); +void onResult(TT_UNUSED AppContext& app, TT_UNUSED Result result, const Bundle& bundle) { + auto index = alertdialog::getResultIndex(bundle); if (index == 0) {// Yes auto* app = optWifiApSettingsApp(); if (app == nullptr) {