mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-20 12:25:06 +00:00
293 lines
11 KiB
C++
293 lines
11 KiB
C++
#include "SimulatorProfile.h"
|
|
|
|
#include <app/event.h>
|
|
#include <app/manifest.h>
|
|
#include <app/scheduler.h>
|
|
#include <app/start.h>
|
|
|
|
#include <lvgl_window_manager/window_manager.h>
|
|
|
|
#include <tactility/check.h>
|
|
#include <tactility/concurrent/task_event_group.h>
|
|
|
|
#include <atomic>
|
|
|
|
#include <lvgl/fonts.h>
|
|
#include <lvgl/grid_navigation.h>
|
|
#include <lvgl/lvgl.h>
|
|
#include <lvgl/widgets/card.h>
|
|
#include <lvgl/widgets/chip.h>
|
|
#include <lvgl/widgets/icon_button.h>
|
|
#include <lvgl/widgets/text_button.h>
|
|
#include <lvgl/widgets/toolbar.h>
|
|
|
|
namespace simulator {
|
|
|
|
namespace {
|
|
|
|
constexpr auto* SELECTION_DIALOG_ID = "tactility.selectiondialog";
|
|
constexpr const char* ORIENTATION_LABELS[] = { "Default", "Landscape", "Portrait" };
|
|
constexpr int ORIENTATION_COUNT = 3;
|
|
|
|
struct Context {
|
|
uint32_t appInstanceId = 0;
|
|
TaskEventGroup eventGroup {};
|
|
uint32_t selectProfileBit = 0;
|
|
uint32_t rebootBit = 0;
|
|
uint32_t orientationBit = 0;
|
|
/** Set by the pills on the LVGL task, taken by the app's task. -1 when there is none. */
|
|
std::atomic<int> requestedOrientation { -1 };
|
|
AppInstanceId dialogId = 0;
|
|
/** Last read from the profile file, which is not on the LVGL thread */
|
|
size_t selectedIndex = 0;
|
|
OrientationOverride selectedOrientation = OrientationOverride::Default;
|
|
bool saveFailed = false;
|
|
|
|
lv_obj_t* profileLabel = nullptr;
|
|
lv_obj_t* rebootButton = nullptr;
|
|
lv_obj_t* orientationChips = nullptr;
|
|
lv_obj_t* errorLabel = nullptr;
|
|
};
|
|
|
|
void onBackPressed(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
app_event_emit_close(ctx->appInstanceId);
|
|
}
|
|
|
|
void onSelectProfilePressed(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
task_event_group_signal(&ctx->eventGroup, ctx->selectProfileBit);
|
|
}
|
|
|
|
void onOrientationPressed(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
ctx->requestedOrientation.store(static_cast<int>(lv_obj_get_index(lv_event_get_target_obj(event))));
|
|
task_event_group_signal(&ctx->eventGroup, ctx->orientationBit);
|
|
}
|
|
|
|
void onRebootPressed(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
task_event_group_signal(&ctx->eventGroup, ctx->rebootBit);
|
|
}
|
|
|
|
lv_obj_t* createRow(lv_obj_t* parent) {
|
|
auto* row = lv_obj_create(parent);
|
|
lv_obj_remove_style_all(row);
|
|
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_scrollable(row, false);
|
|
return row;
|
|
}
|
|
|
|
void updateWidgets(Context* ctx) {
|
|
if (ctx->profileLabel == nullptr) {
|
|
return;
|
|
}
|
|
lv_label_set_text(ctx->profileLabel, getProfile(ctx->selectedIndex).name);
|
|
const bool unchanged = ctx->selectedIndex == getActiveProfileIndex() && ctx->selectedOrientation == getActiveOrientation();
|
|
lv_obj_set_state(ctx->rebootButton, LV_STATE_DISABLED, unchanged);
|
|
for (int i = 0; i < ORIENTATION_COUNT; i++) {
|
|
lv_obj_set_state(lv_obj_get_child(ctx->orientationChips, i), LV_STATE_CHECKED, i == static_cast<int>(ctx->selectedOrientation));
|
|
}
|
|
lv_obj_set_hidden(ctx->errorLabel, !ctx->saveFailed);
|
|
}
|
|
|
|
void createWidgets(lv_obj_t* parent, void* userData) {
|
|
auto* ctx = static_cast<Context*>(userData);
|
|
|
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
|
|
|
auto* toolbar = lvgl_toolbar_create(parent, "Simulator");
|
|
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
|
|
|
auto* wrapper = lv_obj_create(parent);
|
|
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_width(wrapper, LV_PCT(100));
|
|
lv_obj_set_flex_grow(wrapper, 1);
|
|
|
|
auto* card = lvgl_card_create(wrapper);
|
|
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
|
|
|
auto* profile_row = createRow(card);
|
|
auto* profile_column = lv_obj_create(profile_row);
|
|
lv_obj_remove_style_all(profile_column);
|
|
lv_obj_set_height(profile_column, LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_grow(profile_column, 1);
|
|
lv_obj_set_flex_flow(profile_column, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_scrollable(profile_column, false);
|
|
|
|
auto* title_label = lv_label_create(profile_column);
|
|
lv_label_set_text(title_label, "Profile");
|
|
|
|
ctx->profileLabel = lv_label_create(profile_column);
|
|
lv_obj_set_style_text_font(ctx->profileLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
|
|
|
auto* list_button = lvgl_icon_button_create(profile_row);
|
|
auto* list_icon = lv_label_create(list_button);
|
|
lv_label_set_text(list_icon, LV_SYMBOL_LIST);
|
|
lv_obj_add_event_cb(list_button, onSelectProfilePressed, LV_EVENT_SHORT_CLICKED, ctx);
|
|
|
|
auto* reboot_row = createRow(card);
|
|
auto* reboot_label = lv_label_create(reboot_row);
|
|
lv_label_set_text(reboot_label, "Reboot to apply");
|
|
lv_obj_set_flex_grow(reboot_label, 1);
|
|
|
|
ctx->rebootButton = lvgl_text_button_create(reboot_row);
|
|
auto* reboot_button_label = lv_label_create(ctx->rebootButton);
|
|
lv_label_set_text(reboot_button_label, "Reboot");
|
|
lv_obj_add_event_cb(ctx->rebootButton, onRebootPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
|
|
|
ctx->errorLabel = lv_label_create(card);
|
|
lv_label_set_text(ctx->errorLabel, "Failed to save the profile");
|
|
lv_obj_set_style_text_color(ctx->errorLabel, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
|
|
lv_obj_set_style_text_font(ctx->errorLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
|
|
|
auto* orientation_title = lv_label_create(wrapper);
|
|
lv_label_set_text(orientation_title, "Orientation");
|
|
|
|
auto* orientation_card = lvgl_card_create(wrapper);
|
|
lv_obj_set_size(orientation_card, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(orientation_card, LV_FLEX_FLOW_COLUMN);
|
|
|
|
// The pills wrap onto the next row when they don't fit
|
|
ctx->orientationChips = lv_obj_create(orientation_card);
|
|
lv_obj_set_size(ctx->orientationChips, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(ctx->orientationChips, LV_FLEX_FLOW_ROW_WRAP);
|
|
lv_obj_set_style_bg_opa(ctx->orientationChips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_border_width(ctx->orientationChips, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_pad_all(ctx->orientationChips, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_scrollable(ctx->orientationChips, false);
|
|
for (const auto* label : ORIENTATION_LABELS) {
|
|
auto* chip = lvgl_chip_create(ctx->orientationChips);
|
|
lv_obj_add_event_cb(chip, onOrientationPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
|
lv_label_set_text(lv_label_create(chip), label);
|
|
}
|
|
// Only the container is in the input group: left and right move between the pills
|
|
lvgl_grid_navigation_add(ctx->orientationChips);
|
|
|
|
updateWidgets(ctx);
|
|
}
|
|
|
|
void destroyWidgets(void* userData) {
|
|
auto* ctx = static_cast<Context*>(userData);
|
|
ctx->profileLabel = nullptr;
|
|
ctx->rebootButton = nullptr;
|
|
ctx->orientationChips = nullptr;
|
|
ctx->errorLabel = nullptr;
|
|
}
|
|
|
|
void startProfileDialog(Context* ctx) {
|
|
if (ctx->dialogId != 0) {
|
|
return;
|
|
}
|
|
const char* argv[PROFILE_COUNT + 1] = { "Profile" };
|
|
for (size_t i = 0; i < PROFILE_COUNT; i++) {
|
|
argv[i + 1] = getProfile(i).name;
|
|
}
|
|
AppStartContext start_context;
|
|
if (app_start_context_from_id(SELECTION_DIALOG_ID, &start_context) != ERROR_NONE) {
|
|
return;
|
|
}
|
|
app_start_context_set_arguments_ext(&start_context, PROFILE_COUNT + 1, argv);
|
|
app_start_context_set_parent(&start_context, ctx->appInstanceId);
|
|
app_start_with_context(&start_context, &ctx->dialogId);
|
|
}
|
|
|
|
/** A failed save keeps the selection that the next start loads */
|
|
void saveAndShow(Context* ctx, size_t profileIndex, OrientationOverride orientation) {
|
|
ctx->saveFailed = !saveSelection(profileIndex, orientation);
|
|
if (!ctx->saveFailed) {
|
|
ctx->selectedIndex = profileIndex;
|
|
ctx->selectedOrientation = orientation;
|
|
}
|
|
lvgl_lock();
|
|
updateWidgets(ctx);
|
|
lvgl_unlock();
|
|
}
|
|
|
|
// Other results than an index mean that the dialog was dismissed
|
|
void onDialogResult(Context* ctx, int32_t selected) {
|
|
ctx->dialogId = 0;
|
|
if (selected < 0 || selected >= static_cast<int32_t>(PROFILE_COUNT)) {
|
|
return;
|
|
}
|
|
// A new profile starts with its own orientation
|
|
saveAndShow(ctx, selected, OrientationOverride::Default);
|
|
}
|
|
|
|
void onOrientationRequest(Context* ctx, int requested) {
|
|
if (requested < 0 || requested >= ORIENTATION_COUNT) {
|
|
return;
|
|
}
|
|
saveAndShow(ctx, ctx->selectedIndex, static_cast<OrientationOverride>(requested));
|
|
}
|
|
|
|
int32_t appMain(int argc, char* argv[]) {
|
|
Context ctx;
|
|
ctx.appInstanceId = app_scheduler_current_app_id();
|
|
ctx.selectedIndex = loadSelectedProfileIndex();
|
|
ctx.selectedOrientation = loadSelectedOrientation();
|
|
|
|
task_event_group_construct(&ctx.eventGroup);
|
|
check(task_event_group_claim_bit(&ctx.eventGroup, &ctx.selectProfileBit) == ERROR_NONE);
|
|
check(task_event_group_claim_bit(&ctx.eventGroup, &ctx.rebootBit) == ERROR_NONE);
|
|
check(task_event_group_claim_bit(&ctx.eventGroup, &ctx.orientationBit) == ERROR_NONE);
|
|
|
|
AppEventSubscription sub {};
|
|
check(app_event_subscribe(&sub, &ctx.eventGroup) == ERROR_NONE);
|
|
|
|
WindowId window = window_manager_create_ext(ctx.appInstanceId, createWidgets, destroyWidgets, &ctx);
|
|
|
|
bool shouldClose = false;
|
|
while (!shouldClose) {
|
|
uint32_t flags = 0;
|
|
task_event_group_wait_any(&ctx.eventGroup, &flags, portMAX_DELAY);
|
|
|
|
if (flags & ctx.selectProfileBit) {
|
|
startProfileDialog(&ctx);
|
|
}
|
|
if (flags & ctx.orientationBit) {
|
|
onOrientationRequest(&ctx, ctx.requestedOrientation.exchange(-1));
|
|
}
|
|
if ((flags & ctx.rebootBit) && (ctx.selectedIndex != getActiveProfileIndex() || ctx.selectedOrientation != getActiveOrientation())) {
|
|
reboot();
|
|
}
|
|
|
|
AppEvent event {};
|
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
|
if (event.type == APP_EVENT_CLOSE) {
|
|
shouldClose = true;
|
|
break;
|
|
}
|
|
if (event.type == APP_EVENT_RESULT) {
|
|
if (event.result.launch_id == ctx.dialogId) {
|
|
onDialogResult(&ctx, event.result.result);
|
|
}
|
|
app_manager_stop(event.result.launch_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
window_manager_remove(window);
|
|
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
|
task_event_group_destruct(&ctx.eventGroup);
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern const ::AppManifest profileAppManifest = {
|
|
.id = "tactility.simulator",
|
|
.name = "Simulator",
|
|
.category = APP_CATEGORY_SETTINGS,
|
|
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
|
.flags = 0,
|
|
.stack = {}
|
|
};
|
|
|
|
}
|