From 4db0d9496de86f04a260c31df955092668a88944 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Fri, 4 Sep 2026 08:59:09 +0200 Subject: [PATCH] Fix for inputdialog --- .../Tactility/app/inputdialog/InputDialog.h | 30 ++++++++------ Tactility/Private/Tactility/app/files/View.h | 9 ++++- Tactility/Source/app/files/FilesApp.cpp | 5 ++- Tactility/Source/app/files/View.cpp | 26 ++++++++---- .../Source/app/inputdialog/InputDialog.cpp | 40 +++++++++++-------- 5 files changed, 71 insertions(+), 39 deletions(-) diff --git a/Tactility/Include/Tactility/app/inputdialog/InputDialog.h b/Tactility/Include/Tactility/app/inputdialog/InputDialog.h index 28fc089a4..ee42c7556 100644 --- a/Tactility/Include/Tactility/app/inputdialog/InputDialog.h +++ b/Tactility/Include/Tactility/app/inputdialog/InputDialog.h @@ -1,8 +1,12 @@ #pragma once +#include #include #include +#include +#include + /** * Show a dialog with a title, a message and a text field. */ @@ -10,19 +14,21 @@ namespace tt::app::inputdialog { /** * Show a dialog with the provided title, message and prefilled text, as a modal child of - * @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the result - * as an APP_EVENT_RESULT in its own event loop: 0 = OK (call getLastText() for the entered - * text), 1 = Cancelled or dismissed without a press. The caller is responsible for calling - * app_manager_stop() on the returned instance id once it has handled the result. + * @a callerAppInstanceId (see app_start_for_result_with_streams()). Result (0 = OK, 1 = + * Cancelled or dismissed without a press) is delivered back via APP_EVENT_RESULT once this app's + * thread exits. On result == 0, read the entered text with app_stream_read(&stream, ...) then + * app_stream_unsubscribe(&stream); on any other result, just app_stream_unsubscribe(&stream). + * The caller must call app_manager_stop() on the returned instance id once that event arrives, + * to fully reap this instance. + * @param[in,out] stream caller-owned storage bound to the started app's stdout; must stay valid + * until app_stream_unsubscribe() is called on it (see above). + * @param[in] buffer caller-owned backing storage for @a stream's ring buffer; must stay valid + * for the same duration as @a stream. + * @param[in] bufferCapacity size of @a buffer in bytes. + * @param[in] eventGroup the caller's own event group, reused for the stream's readiness bits + * (see app_stream_subscribe()). The caller isn't required to actually wait on them itself. * @return the new dialog's app instance id */ -uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled = ""); - -/** - * @return the text entered the last time any InputDialog instance was closed with OK. Only one - * dialog is expected to be open at a time - call this right after receiving its - * APP_EVENT_RESULT with result == 0. - */ -std::string getLastText(); +uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup); } diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/files/View.h index 948c8a9d7..51c34a41b 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/files/View.h @@ -2,6 +2,9 @@ #include "./State.h" +#include +#include + #include #include #include @@ -10,8 +13,12 @@ namespace tt::app::files { class View final { std::shared_ptr state; + TaskEventGroup* eventGroup = nullptr; uint32_t appInstanceId = 0; + AppStream inputDialogStream {}; + uint8_t inputDialogBuffer[256] {}; + size_t current_start_index = 0; size_t last_loaded_index = 0; const size_t MAX_BATCH = 50; @@ -39,7 +46,7 @@ class View final { public: - explicit View(const std::shared_ptr& state) : state(state) {} + View(const std::shared_ptr& state, TaskEventGroup* eventGroup) : state(state), eventGroup(eventGroup) {} void init(uint32_t appInstanceId, lv_obj_t* parent); void update(size_t start_index = 0); diff --git a/Tactility/Source/app/files/FilesApp.cpp b/Tactility/Source/app/files/FilesApp.cpp index c08d48423..16b5dd7e4 100644 --- a/Tactility/Source/app/files/FilesApp.cpp +++ b/Tactility/Source/app/files/FilesApp.cpp @@ -31,12 +31,13 @@ void createWidgets(lv_obj_t* parent, void* userData) { int32_t appMain(int argc, char* argv[]) { uint32_t appInstanceId = app_scheduler_current_app_id(); auto state = std::make_shared(); - View view(state); - CreateContext createContext { &view, appInstanceId }; TaskEventGroup event_group {}; task_event_group_construct(&event_group); + View view(state, &event_group); + CreateContext createContext { &view, appInstanceId }; + AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index fbf6d8871..c211a28d7 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -378,7 +379,7 @@ void View::onRenamePressed() { std::string entry_name = state->getSelectedChildEntry(); LOG_I(TAG, "Pending rename %s", entry_name.c_str()); state->setPendingAction(State::ActionRename); - inputdialog::start(appInstanceId, "Rename", "", entry_name); + inputdialog::start(appInstanceId, "Rename", "", entry_name, inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup); } void View::onDeletePressed() { @@ -393,13 +394,13 @@ void View::onDeletePressed() { void View::onNewFilePressed() { LOG_I(TAG, "Creating new file"); state->setPendingAction(State::ActionCreateFile); - inputdialog::start(appInstanceId, "New File", "Enter filename:", ""); + inputdialog::start(appInstanceId, "New File", "Enter filename:", "", inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup); } void View::onNewFolderPressed() { LOG_I(TAG, "Creating new folder"); state->setPendingAction(State::ActionCreateFolder); - inputdialog::start(appInstanceId, "New Folder", "Enter folder name:", ""); + inputdialog::start(appInstanceId, "New Folder", "Enter folder name:", "", inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup); } void View::addCommonFileActions() { @@ -597,10 +598,21 @@ void View::onResult(uint32_t launchId, int32_t result) { std::string filepath = state->getSelectedChildPath(); LOG_I(TAG, "Result for %s", filepath.c_str()); - // Text-entry result (rename/new file/new folder); empty for Cancel, or for a dialog that - // doesn't produce text (delete/paste confirmations) - those switch cases below only look at - // `result`, not this. - std::string resultText = (result == 0) ? inputdialog::getLastText() : std::string(); + // Text-entry result (rename/new file/new folder), read from the AppStream bound to that + // dialog's stdout. Empty for Cancel. Other pending actions (delete/paste confirmations) never + // bound this stream; their switch cases below only look at `result`, not `resultText`. + bool isTextEntryAction = state->getPendingAction() == State::ActionRename || + state->getPendingAction() == State::ActionCreateFile || + state->getPendingAction() == State::ActionCreateFolder; + std::string resultText; + if (isTextEntryAction) { + if (result == 0) { + char buffer[sizeof(inputDialogBuffer)]; + size_t length = app_stream_read(&inputDialogStream, buffer, sizeof(buffer)); + resultText = std::string(buffer, length); + } + app_stream_unsubscribe(&inputDialogStream); + } switch (state->getPendingAction()) { case State::ActionDelete: { diff --git a/Tactility/Source/app/inputdialog/InputDialog.cpp b/Tactility/Source/app/inputdialog/InputDialog.cpp index a6028a030..13ea866d1 100644 --- a/Tactility/Source/app/inputdialog/InputDialog.cpp +++ b/Tactility/Source/app/inputdialog/InputDialog.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include @@ -14,6 +15,8 @@ #include +#include + namespace tt::app::inputdialog { constexpr auto* TAG = "InputDialog"; @@ -31,7 +34,8 @@ struct Context { // The eventual appMain() return value - see AlertDialog.cpp's Context::result for why this // is a plain (non-atomic) field safely shared between the LVGL thread (writer, before // emitting APP_EVENT_CLOSE) and this dialog's own thread (reader, after waking from it). - int32_t result = 1; // Cancelled - safety-net default if closed without pressing a button + int32_t resultCode = 1; // Cancelled - safety-net default if closed without pressing a button + std::string resultText; }; struct ButtonContext { @@ -40,12 +44,6 @@ struct ButtonContext { lv_obj_t* textarea; }; -// The last text entered via OK. Static rather than per-instance: simple, and in practice only -// one InputDialog is ever open at a time. Written on the LVGL thread (onButtonPressed(), before -// emitting APP_EVENT_CLOSE); read by the parent via getLastText() after receiving that event - -// safe without a lock for the same reason Context::result is (see AlertDialog.cpp). -std::string lastText; - void onButtonDeleted(lv_event_t* e) { delete static_cast(lv_event_get_user_data(e)); } @@ -54,11 +52,11 @@ void onButtonPressed(lv_event_t* e) { auto* btnCtx = static_cast(lv_event_get_user_data(e)); if (btnCtx->textarea != nullptr) { LOG_I(TAG, "OK pressed"); - lastText = lv_textarea_get_text(btnCtx->textarea); - btnCtx->ctx->result = 0; + btnCtx->ctx->resultText = lv_textarea_get_text(btnCtx->textarea); + btnCtx->ctx->resultCode = 0; } else { LOG_I(TAG, "Cancel pressed"); - btnCtx->ctx->result = 1; + btnCtx->ctx->resultCode = 1; } app_event_emit_close(btnCtx->ctx->appInstanceId); } @@ -138,22 +136,30 @@ int32_t appMain(int argc, char* argv[]) { check(app_event_unsubscribe(&sub) == ERROR_NONE); task_event_group_destruct(&event_group); - return ctx.result; + if (ctx.resultCode == 0) { + // The caller captures this via an AppStream bound to our stdout (see start()); see + // AppStdioWrap.cpp for how printf() itself gets routed there on POSIX. + printf("%s", ctx.resultText.c_str()); + } + return ctx.resultCode; } } // namespace -uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled) { +uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled, AppStream& stream, void* buffer, size_t bufferCapacity, TaskEventGroup* eventGroup) { const char* argv[] = { title.c_str(), message.c_str(), prefilled.c_str() }; + AppStreamBinding binding = { + .producer_fd = STDOUT_FILENO, + .stream = &stream, + .buffer = buffer, + .buffer_capacity = bufferCapacity, + .event_group = eventGroup, + }; uint32_t instanceId = 0; - app_start_for_result(manifest.id, 3, argv, callerAppInstanceId, &instanceId); + app_start_for_result_with_streams(manifest.id, 3, argv, &binding, 1, callerAppInstanceId, &instanceId); return instanceId; } -std::string getLastText() { - return lastText; -} - extern const ::AppManifest manifest = { .id = "tactility.inputdialog", .name = "Input Dialog",