diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 961834dcc..98feabb6d 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -94,3 +94,11 @@ App( requires=["js_app"], sources=["modules/js_gpio.c"], ) + +App( + appid="js_textbox", + apptype=FlipperAppType.PLUGIN, + entry_point="js_textbox_ep", + requires=["js_app"], + sources=["modules/js_textbox.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/textbox.js b/applications/system/js_app/examples/apps/Scripts/textbox.js new file mode 100644 index 000000000..a260e8a21 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/textbox.js @@ -0,0 +1,24 @@ +let textbox = require("textbox"); + +// Set config before setting text +// Focus (start / end), Font (text / hex) +textbox.setConfig("end", "text"); + +let text = "Example dynamic updating textbox\n"; +textbox.setText(text); + +// Non-blocking, can keep updating text after, can close in JS or in GUI +textbox.show(); + +let i = 0; +while (textbox.isOpen() && i < 20) { + print("console", i++); + text += "textbox " + to_string(i) + "\n"; + textbox.setText(text); + delay(500); +} + +// If not closed by user (instead i < 20 is false above), close forcefully +if (textbox.isOpen()) { + textbox.close(); +} diff --git a/applications/system/js_app/modules/js_blebeacon.c b/applications/system/js_app/modules/js_blebeacon.c index 4b661a2cf..7dc212b6a 100644 --- a/applications/system/js_app/modules/js_blebeacon.c +++ b/applications/system/js_app/modules/js_blebeacon.c @@ -19,9 +19,9 @@ typedef struct { static JsBlebeaconInst* get_this_ctx(struct mjs* mjs) { mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsBlebeaconInst* storage = mjs_get_ptr(mjs, obj_inst); - furi_assert(storage); - return storage; + JsBlebeaconInst* blebeacon = mjs_get_ptr(mjs, obj_inst); + furi_assert(blebeacon); + return blebeacon; } static void ret_bad_args(struct mjs* mjs, const char* error) { diff --git a/applications/system/js_app/modules/js_keyboard.c b/applications/system/js_app/modules/js_keyboard.c index 6d7ce0e57..2bfb296e0 100644 --- a/applications/system/js_app/modules/js_keyboard.c +++ b/applications/system/js_app/modules/js_keyboard.c @@ -20,9 +20,9 @@ static void ret_bad_args(struct mjs* mjs, const char* error) { static JsKeyboardInst* get_this_ctx(struct mjs* mjs) { mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsKeyboardInst* storage = mjs_get_ptr(mjs, obj_inst); - furi_assert(storage); - return storage; + JsKeyboardInst* keyboard = mjs_get_ptr(mjs, obj_inst); + furi_assert(keyboard); + return keyboard; } static void keyboard_callback(void* context) { diff --git a/applications/system/js_app/modules/js_submenu.c b/applications/system/js_app/modules/js_submenu.c index 1d2f5db55..8c8d95fab 100644 --- a/applications/system/js_app/modules/js_submenu.c +++ b/applications/system/js_app/modules/js_submenu.c @@ -12,9 +12,9 @@ typedef struct { static JsSubmenuInst* get_this_ctx(struct mjs* mjs) { mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); - JsSubmenuInst* storage = mjs_get_ptr(mjs, obj_inst); - furi_assert(storage); - return storage; + JsSubmenuInst* submenu = mjs_get_ptr(mjs, obj_inst); + furi_assert(submenu); + return submenu; } static void ret_bad_args(struct mjs* mjs, const char* error) { diff --git a/applications/system/js_app/modules/js_textbox.c b/applications/system/js_app/modules/js_textbox.c new file mode 100644 index 000000000..199cd2a0e --- /dev/null +++ b/applications/system/js_app/modules/js_textbox.c @@ -0,0 +1,202 @@ +#include +#include +#include +#include "../js_modules.h" + +typedef struct { + TextBox* text_box; + ViewDispatcher* view_dispatcher; + FuriThread* thread; +} JsTextboxInst; + +static JsTextboxInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsTextboxInst* textbox = mjs_get_ptr(mjs, obj_inst); + furi_assert(textbox); + return textbox; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count(struct mjs* mjs, size_t count) { + size_t num_args = mjs_nargs(mjs); + if(num_args != count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_textbox_set_config(struct mjs* mjs) { + JsTextboxInst* textbox = get_this_ctx(mjs); + if(!check_arg_count(mjs, 2)) return; + + TextBoxFocus set_focus = TextBoxFocusStart; + mjs_val_t focus_arg = mjs_arg(mjs, 0); + const char* focus = mjs_get_string(mjs, &focus_arg, NULL); + if(!focus) { + ret_bad_args(mjs, "Focus must be a string"); + return; + } else { + if(!strncmp(focus, "start", strlen("start"))) { + set_focus = TextBoxFocusStart; + } else if(!strncmp(focus, "end", strlen("end"))) { + set_focus = TextBoxFocusEnd; + } else { + ret_bad_args(mjs, "Bad focus value"); + return; + } + } + + TextBoxFont set_font = TextBoxFontText; + mjs_val_t font_arg = mjs_arg(mjs, 1); + const char* font = mjs_get_string(mjs, &font_arg, NULL); + if(!font) { + ret_bad_args(mjs, "Font must be a string"); + return; + } else { + if(!strncmp(font, "text", strlen("text"))) { + set_font = TextBoxFontText; + } else if(!strncmp(font, "hex", strlen("hex"))) { + set_font = TextBoxFontHex; + } else { + ret_bad_args(mjs, "Bad font value"); + return; + } + } + + text_box_set_focus(textbox->text_box, set_focus); + text_box_set_font(textbox->text_box, set_font); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_textbox_set_text(struct mjs* mjs) { + JsTextboxInst* textbox = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) return; + + mjs_val_t text_arg = mjs_arg(mjs, 0); + const char* text = mjs_get_string(mjs, &text_arg, NULL); + if(!text) { + ret_bad_args(mjs, "Text must be a string"); + return; + } + + text_box_set_text(textbox->text_box, text); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_textbox_is_open(struct mjs* mjs) { + JsTextboxInst* textbox = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + mjs_return(mjs, mjs_mk_boolean(mjs, !!textbox->thread)); +} + +static void textbox_deinit(void* context) { + JsTextboxInst* textbox = context; + furi_thread_join(textbox->thread); + furi_thread_free(textbox->thread); + textbox->thread = NULL; + + view_dispatcher_remove_view(textbox->view_dispatcher, 0); + view_dispatcher_free(textbox->view_dispatcher); + textbox->view_dispatcher = NULL; + furi_record_close(RECORD_GUI); + + text_box_reset(textbox->text_box); +} + +static void textbox_callback(void* context, uint32_t arg) { + UNUSED(arg); + textbox_deinit(context); +} + +static bool textbox_exit(void* context) { + JsTextboxInst* textbox = context; + view_dispatcher_stop(textbox->view_dispatcher); + furi_timer_pending_callback(textbox_callback, textbox, 0); + return true; +} + +static int32_t textbox_thread(void* context) { + ViewDispatcher* view_dispatcher = context; + view_dispatcher_run(view_dispatcher); + return 0; +} + +static void js_textbox_show(struct mjs* mjs) { + JsTextboxInst* textbox = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + Gui* gui = furi_record_open(RECORD_GUI); + textbox->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(textbox->view_dispatcher); + view_dispatcher_add_view(textbox->view_dispatcher, 0, text_box_get_view(textbox->text_box)); + view_dispatcher_set_event_callback_context(textbox->view_dispatcher, textbox); + view_dispatcher_set_navigation_event_callback(textbox->view_dispatcher, textbox_exit); + view_dispatcher_attach_to_gui(textbox->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + view_dispatcher_switch_to_view(textbox->view_dispatcher, 0); + + textbox->thread = + furi_thread_alloc_ex("JsTextbox", 1024, textbox_thread, textbox->view_dispatcher); + furi_thread_start(textbox->thread); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_textbox_close(struct mjs* mjs) { + JsTextboxInst* textbox = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + if(textbox->thread) { + view_dispatcher_stop(textbox->view_dispatcher); + textbox_deinit(textbox); + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void* js_textbox_create(struct mjs* mjs, mjs_val_t* object) { + JsTextboxInst* textbox = malloc(sizeof(JsTextboxInst)); + mjs_val_t textbox_obj = mjs_mk_object(mjs); + mjs_set(mjs, textbox_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, textbox)); + mjs_set(mjs, textbox_obj, "setConfig", ~0, MJS_MK_FN(js_textbox_set_config)); + mjs_set(mjs, textbox_obj, "setText", ~0, MJS_MK_FN(js_textbox_set_text)); + mjs_set(mjs, textbox_obj, "isOpen", ~0, MJS_MK_FN(js_textbox_is_open)); + mjs_set(mjs, textbox_obj, "show", ~0, MJS_MK_FN(js_textbox_show)); + mjs_set(mjs, textbox_obj, "close", ~0, MJS_MK_FN(js_textbox_close)); + textbox->text_box = text_box_alloc(); + *object = textbox_obj; + return textbox; +} + +static void js_textbox_destroy(void* inst) { + JsTextboxInst* textbox = inst; + if(textbox->thread) { + view_dispatcher_stop(textbox->view_dispatcher); + textbox_deinit(textbox); + } + text_box_free(textbox->text_box); + free(textbox); +} + +static const JsModuleDescriptor js_textbox_desc = { + "textbox", + js_textbox_create, + js_textbox_destroy, +}; + +static const FlipperAppPluginDescriptor textbox_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_textbox_desc, +}; + +const FlipperAppPluginDescriptor* js_textbox_ep(void) { + return &textbox_plugin_descriptor; +}