diff --git a/App/Source/Boards.h b/App/Source/Boards.h index c5e296ee..982b0cee 100644 --- a/App/Source/Boards.h +++ b/App/Source/Boards.h @@ -1,4 +1,5 @@ #pragma once +#include #ifdef ESP_PLATFORM #include diff --git a/Boards/Simulator/Source/Simulator.h b/Boards/Simulator/Source/Simulator.h index 340e6c2e..34da7869 100644 --- a/Boards/Simulator/Source/Simulator.h +++ b/Boards/Simulator/Source/Simulator.h @@ -1,7 +1,6 @@ #pragma once #include "Main.h" -#include namespace simulator { /** Set the function pointer of the real app_main() */ diff --git a/Tactility/Include/Tactility/app/App.h b/Tactility/Include/Tactility/app/App.h index 5135b485..e9808323 100644 --- a/Tactility/Include/Tactility/app/App.h +++ b/Tactility/Include/Tactility/app/App.h @@ -16,9 +16,9 @@ namespace tt::app { class AppContext; enum class Result; -class App { +typedef unsigned int LaunchId; -private: +class App { Mutex mutex; @@ -44,7 +44,7 @@ public: virtual void onDestroy(AppContext& appContext) {} virtual void onShow(AppContext& appContext, lv_obj_t* parent) {} virtual void onHide(AppContext& appContext) {} - virtual void onResult(AppContext& appContext, Result result, std::unique_ptr _Nullable resultData) {} + virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr _Nullable resultData) {} Mutex& getMutex() { return mutex; } @@ -83,15 +83,15 @@ std::shared_ptr create() { return std::shared_ptr(new T); } * @param[in] id application name or id * @param[in] parameters optional parameters to pass onto the application */ -void start(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); +LaunchId start(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); /** @brief Stop the currently showing app. Show the previous app if any app was still running. */ void stop(); /** @return the currently running app context (it is only ever null before the splash screen is shown) */ -std::shared_ptr _Nullable getCurrentAppContext(); +std::shared_ptr _Nullable getCurrentAppContext(); /** @return the currently running app (it is only ever null before the splash screen is shown) */ -std::shared_ptr _Nullable getCurrentApp(); +std::shared_ptr _Nullable getCurrentApp(); } diff --git a/Tactility/Include/Tactility/app/ElfApp.h b/Tactility/Include/Tactility/app/ElfApp.h index 9821f4eb..c086ab6a 100644 --- a/Tactility/Include/Tactility/app/ElfApp.h +++ b/Tactility/Include/Tactility/app/ElfApp.h @@ -12,7 +12,7 @@ typedef void (*OnCreate)(void* appContext, void* _Nullable data); typedef void (*OnDestroy)(void* appContext, void* _Nullable data); typedef void (*OnShow)(void* appContext, void* _Nullable data, lv_obj_t* parent); typedef void (*OnHide)(void* appContext, void* _Nullable data); -typedef void (*OnResult)(void* appContext, void* _Nullable data, Result result, Bundle* resultData); +typedef void (*OnResult)(void* appContext, void* _Nullable data, LaunchId launchId, Result result, Bundle* resultData); void setElfAppManifest( const char* name, diff --git a/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h b/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h new file mode 100644 index 00000000..86fcf424 --- /dev/null +++ b/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h @@ -0,0 +1,7 @@ +#pragma once + +namespace tt::app::filebrowser { + +void start(); + +} // namespace diff --git a/Tactility/Include/Tactility/app/files/Files.h b/Tactility/Include/Tactility/app/files/Files.h deleted file mode 100644 index 1d77e44a..00000000 --- a/Tactility/Include/Tactility/app/files/Files.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "app/files/View.h" -#include "app/files/State.h" -#include "app/AppManifest.h" - -#include -#include -#include - -namespace tt::app::files { - -void start(); - -} // namespace diff --git a/Tactility/Include/Tactility/app/fileselection/FileSelection.h b/Tactility/Include/Tactility/app/fileselection/FileSelection.h new file mode 100644 index 00000000..f2c08473 --- /dev/null +++ b/Tactility/Include/Tactility/app/fileselection/FileSelection.h @@ -0,0 +1,23 @@ +#pragma once + +namespace tt::app::fileselection { + +/** + * Show a file selection dialog that allows the user to select an existing file. + * This app returns the absolute file path as a result. + */ +LaunchId startForExistingFile(); + +/** + * Show a file selection dialog that allows the user to select a new or existing file. + * This app returns the absolute file path as a result. + */ +LaunchId startForExistingOrNewFile(); + +/** + * @param bundle the result bundle of an app + * @return the path from the bundle, or empty string if none is present + */ +std::string getResultPath(const Bundle& bundle); + +} // namespace diff --git a/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h b/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h index 7ca7bf10..8e753b22 100644 --- a/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h +++ b/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h @@ -53,7 +53,7 @@ std::shared_ptr _Nullable find(const std::string& path); * Always calls the function, but doesn't lock if the path is not an SD card path. */ template -inline ReturnType withSdCardLock(const std::string& path, std::function fn) { + ReturnType withSdCardLock(const std::string& path, std::function fn) { auto sdcard = find(path); if (sdcard != nullptr) { auto scoped_lockable = sdcard->getLock().asScopedLock(); diff --git a/Tactility/Include/Tactility/service/loader/Loader.h b/Tactility/Include/Tactility/service/loader/Loader.h index edd02d57..d5d3b651 100644 --- a/Tactility/Include/Tactility/service/loader/Loader.h +++ b/Tactility/Include/Tactility/service/loader/Loader.h @@ -4,7 +4,6 @@ #include #include -#include #include @@ -30,7 +29,7 @@ struct LoaderEvent { * @param[in] id application name or id * @param[in] parameters optional parameters to pass onto the application */ -void startApp(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); +app::LaunchId startApp(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); /** @brief Stop the currently showing app. Show the previous app if any app was still running. */ void stopApp(); diff --git a/Tactility/Private/Tactility/app/AppInstance.h b/Tactility/Private/Tactility/app/AppInstance.h index 3bd76558..f38cfb2b 100644 --- a/Tactility/Private/Tactility/app/AppInstance.h +++ b/Tactility/Private/Tactility/app/AppInstance.h @@ -25,22 +25,21 @@ enum class State { */ class AppInstance : public AppContext { -private: - Mutex mutex = Mutex(Mutex::Type::Normal); const std::shared_ptr manifest; State state = State::Initial; + LaunchId launchId; Flags flags = { .showStatusbar = true }; /** @brief Optional parameters to start the app with * When these are stored in the app struct, the struct takes ownership. * Do not mutate after app creation. */ - std::shared_ptr _Nullable parameters; + std::shared_ptr _Nullable parameters; std::shared_ptr app; - static std::shared_ptr createApp( - const std::shared_ptr& manifest + static std::shared_ptr createApp( + const std::shared_ptr& manifest ) { if (manifest->location.isInternal()) { assert(manifest->createApp != nullptr); @@ -50,7 +49,7 @@ private: TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps"); } #ifdef ESP_PLATFORM - return app::createElfApp(manifest); + return createElfApp(manifest); #else tt_crash("not supported"); #endif @@ -61,18 +60,23 @@ private: public: - explicit AppInstance(const std::shared_ptr& manifest) : + explicit AppInstance(const std::shared_ptr& manifest, LaunchId launchId) : manifest(manifest), + launchId(launchId), app(createApp(manifest)) {} - AppInstance(const std::shared_ptr& manifest, std::shared_ptr parameters) : + AppInstance(const std::shared_ptr& manifest, LaunchId launchId, std::shared_ptr parameters) : manifest(manifest), + launchId(launchId), parameters(std::move(parameters)), - app(createApp(manifest)) {} + app(createApp(manifest)) + {} ~AppInstance() override = default; + LaunchId getLaunchId() const { return launchId; } + void setState(State state); State getState() const; diff --git a/Tactility/Private/Tactility/app/files/State.h b/Tactility/Private/Tactility/app/filebrowser/State.h similarity index 97% rename from Tactility/Private/Tactility/app/files/State.h rename to Tactility/Private/Tactility/app/filebrowser/State.h index c29e1521..04308e3d 100644 --- a/Tactility/Private/Tactility/app/files/State.h +++ b/Tactility/Private/Tactility/app/filebrowser/State.h @@ -6,7 +6,7 @@ #include #include -namespace tt::app::files { +namespace tt::app::filebrowser { class State { diff --git a/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h new file mode 100644 index 00000000..0ae68fbb --- /dev/null +++ b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace tt::app::filebrowser { + +bool isSupportedExecutableFile(const std::string& filename); +bool isSupportedImageFile(const std::string& filename); +bool isSupportedTextFile(const std::string& filename); + +} // namespace diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/filebrowser/View.h similarity index 96% rename from Tactility/Private/Tactility/app/files/View.h rename to Tactility/Private/Tactility/app/filebrowser/View.h index 0c616c7c..c6f24026 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/filebrowser/View.h @@ -7,7 +7,7 @@ #include #include -namespace tt::app::files { +namespace tt::app::filebrowser { class View { std::shared_ptr state; diff --git a/Tactility/Private/Tactility/app/files/FileUtils.h b/Tactility/Private/Tactility/app/files/FileUtils.h deleted file mode 100644 index d05cba3c..00000000 --- a/Tactility/Private/Tactility/app/files/FileUtils.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace tt::app::files { - -/** File types for `dirent`'s `d_type`. */ -enum { - TT_DT_UNKNOWN = 0, -#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type - TT_DT_FIFO = 1, -#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO - TT_DT_CHR = 2, -#define TT_DT_CHR TT_DT_CHR // Character device - TT_DT_DIR = 4, -#define TT_DT_DIR TT_DT_DIR // Directory - TT_DT_BLK = 6, -#define TT_DT_BLK TT_DT_BLK // Block device - TT_DT_REG = 8, -#define TT_DT_REG TT_DT_REG // Regular file - TT_DT_LNK = 10, -#define TT_DT_LNK TT_DT_LNK // Symbolic link - TT_DT_SOCK = 12, -#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket - TT_DT_WHT = 14 -#define TT_DT_WHT TT_DT_WHT // Whiteout inodes -}; - - -std::string getChildPath(const std::string& basePath, const std::string& childPath); - -typedef int (*ScandirFilter)(const struct dirent*); - -typedef bool (*ScandirSort)(const struct dirent&, const struct dirent&); - -bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right); - -int dirent_filter_dot_entries(const struct dirent* entry); - -/** - * A scandir()-like implementation that works on ESP32. - * It does not return "." and ".." items but otherwise functions the same. - * It returns an allocated output array with allocated dirent instances. - * The caller is responsible for free-ing the memory of these. - * - * @param[in] path path the scan for files and directories - * @param[out] outList a pointer to vector of dirent - * @param[in] filter an optional filter to filter out specific items - * @param[in] sort an optional sorting function - * @return the amount of items that were stored in "output" or -1 when an error occurred - */ -int scandir( - const std::string& path, - std::vector& outList, - ScandirFilter _Nullable filter, - ScandirSort _Nullable sort -); - -bool isSupportedExecutableFile(const std::string& filename); -bool isSupportedImageFile(const std::string& filename); -bool isSupportedTextFile(const std::string& filename); - -} // namespace diff --git a/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h b/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h new file mode 100644 index 00000000..479edd12 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace tt::app::fileselection { + +enum class Mode { + Existing = 0, + ExistingOrNew = 1 +}; + +Mode getMode(const Bundle& bundle); + +} diff --git a/Tactility/Private/Tactility/app/fileselection/State.h b/Tactility/Private/Tactility/app/fileselection/State.h new file mode 100644 index 00000000..912f8466 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/State.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include + +namespace tt::app::fileselection { + +class State { + + Mutex mutex = Mutex(Mutex::Type::Recursive); + std::vector dir_entries; + std::string current_path; + std::string selected_child_entry; + +public: + + State(); + + void freeEntries() { + dir_entries.clear(); + } + + ~State() { + freeEntries(); + } + + bool setEntriesForChildPath(const std::string& child_path); + bool setEntriesForPath(const std::string& path); + + template &> Func> + void withEntries(Func&& onEntries) const { + mutex.withLock([&]() { + std::invoke(std::forward(onEntries), dir_entries); + }); + } + + bool getDirent(uint32_t index, dirent& dirent); + + void setSelectedChildEntry(const std::string& newFile) { + selected_child_entry = newFile; + } + + std::string getSelectedChildEntry() const { return selected_child_entry; } + std::string getCurrentPath() const { return current_path; } + std::string getCurrentPathWithTrailingSlash() const { + if (current_path.length() > 1) { + return current_path + "/"; + } else { + return current_path; + } + } + + std::string getSelectedChildPath() const; +}; + +} diff --git a/Tactility/Private/Tactility/app/fileselection/View.h b/Tactility/Private/Tactility/app/fileselection/View.h new file mode 100644 index 00000000..4f8d2085 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/View.h @@ -0,0 +1,44 @@ +#pragma once + +#include "./State.h" +#include "./FileSelectionPrivate.h" + +#include "Tactility/app/AppManifest.h" + +#include +#include + +namespace tt::app::fileselection { + +class View { + std::shared_ptr state; + + lv_obj_t* dir_entry_list = nullptr; + lv_obj_t* navigate_up_button = nullptr; + lv_obj_t* path_textarea = nullptr; + lv_obj_t* select_button = nullptr; + std::function on_file_selected; + + void onTapFile(const std::string&path, const std::string&filename); + static void onSelectButtonPressed(lv_event_t* event); + static void onPathTextChanged(lv_event_t* event); + void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry); + +public: + + explicit View(const std::shared_ptr& state, std::function onFileSelected) : + state(state), + on_file_selected(std::move(onFileSelected)) + {} + + void init(lv_obj_t* parent, Mode mode); + void update(); + + void onNavigateUpPressed(); + void onDirEntryPressed(uint32_t index); + void onFileSelected(const std::string& path) const { + on_file_selected(path); + } +}; + +} diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 6aa5d1d7..9a7ba056 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -37,7 +37,8 @@ namespace app { namespace chat { extern const AppManifest manifest; } namespace boot { extern const AppManifest manifest; } namespace display { extern const AppManifest manifest; } - namespace files { extern const AppManifest manifest; } + namespace filebrowser { extern const AppManifest manifest; } + namespace fileselection { extern const AppManifest manifest; } namespace gpio { extern const AppManifest manifest; } namespace gpssettings { extern const AppManifest manifest; } namespace i2cscanner { extern const AppManifest manifest; } @@ -78,7 +79,8 @@ static void registerSystemApps() { addApp(app::applist::manifest); addApp(app::calculator::manifest); addApp(app::display::manifest); - addApp(app::files::manifest); + addApp(app::filebrowser::manifest); + addApp(app::fileselection::manifest); addApp(app::gpio::manifest); addApp(app::gpssettings::manifest); addApp(app::i2cscanner::manifest); diff --git a/Tactility/Source/app/App.cpp b/Tactility/Source/app/App.cpp index 2c74ceb8..91ea12e3 100644 --- a/Tactility/Source/app/App.cpp +++ b/Tactility/Source/app/App.cpp @@ -4,19 +4,19 @@ namespace tt::app { -void start(const std::string& id, std::shared_ptr _Nullable parameters) { - service::loader::startApp(id, std::move(parameters)); +LaunchId start(const std::string& id, std::shared_ptr _Nullable parameters) { + return service::loader::startApp(id, std::move(parameters)); } void stop() { service::loader::stopApp(); } -std::shared_ptr _Nullable getCurrentAppContext() { +std::shared_ptr _Nullable getCurrentAppContext() { return service::loader::getCurrentAppContext(); } -std::shared_ptr _Nullable getCurrentApp() { +std::shared_ptr _Nullable getCurrentApp() { return service::loader::getCurrentApp(); } diff --git a/Tactility/Source/app/ElfApp.cpp b/Tactility/Source/app/ElfApp.cpp index 7f75e709..77217194 100644 --- a/Tactility/Source/app/ElfApp.cpp +++ b/Tactility/Source/app/ElfApp.cpp @@ -36,8 +36,6 @@ static ElfManifest elfManifest; class ElfApp : public App { -private: - const std::string filePath; std::unique_ptr elfFileData; esp_elf_t elf; @@ -143,9 +141,9 @@ public: } } - void onResult(AppContext& appContext, Result result, std::unique_ptr resultBundle) override { + void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr resultBundle) override { if (manifest != nullptr && manifest->onResult != nullptr) { - manifest->onResult(&appContext, data, result, resultBundle.get()); + manifest->onResult(&appContext, data, launchId, result, resultBundle.get()); } } }; diff --git a/Tactility/Source/app/files/Files.cpp b/Tactility/Source/app/filebrowser/FileBrowser.cpp similarity index 66% rename from Tactility/Source/app/files/Files.cpp rename to Tactility/Source/app/filebrowser/FileBrowser.cpp index 37b1ceb4..c39edcc2 100644 --- a/Tactility/Source/app/files/Files.cpp +++ b/Tactility/Source/app/filebrowser/FileBrowser.cpp @@ -1,5 +1,5 @@ -#include "Tactility/app/files/View.h" -#include "Tactility/app/files/State.h" +#include "Tactility/app/filebrowser/View.h" +#include "Tactility/app/filebrowser/State.h" #include "Tactility/app/AppContext.h" #include @@ -7,18 +7,18 @@ #include -namespace tt::app::files { +namespace tt::app::filebrowser { -#define TAG "files_app" +#define TAG "filebrowser_app" extern const AppManifest manifest; -class FilesApp : public App { +class FileBrowser : public App { std::unique_ptr view; std::shared_ptr state; public: - FilesApp() { + FileBrowser() { state = std::make_shared(); view = std::make_unique(state); } @@ -27,7 +27,7 @@ public: view->init(parent); } - void onResult(AppContext& appContext, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { view->onResult(result, std::move(bundle)); } }; @@ -37,7 +37,7 @@ extern const AppManifest manifest = { .name = "Files", .icon = TT_ASSETS_APP_ICON_FILES, .type = Type::Hidden, - .createApp = create + .createApp = create }; void start() { diff --git a/Tactility/Source/app/files/State.cpp b/Tactility/Source/app/filebrowser/State.cpp similarity index 84% rename from Tactility/Source/app/files/State.cpp rename to Tactility/Source/app/filebrowser/State.cpp index fae8c301..e2dbe18d 100644 --- a/Tactility/Source/app/files/State.cpp +++ b/Tactility/Source/app/filebrowser/State.cpp @@ -1,6 +1,6 @@ -#include "Tactility/app/files/State.h" -#include "Tactility/app/files/FileUtils.h" +#include "Tactility/app/filebrowser/State.h" +#include #include "Tactility/hal/sdcard/SdCardDevice.h" #include #include @@ -11,9 +11,9 @@ #include #include -#define TAG "files_app" +#define TAG "filebrowser_app" -namespace tt::app::files { +namespace tt::app::filebrowser { State::State() { if (kernel::getPlatform() == kernel::PlatformSimulator) { @@ -30,7 +30,7 @@ State::State() { } std::string State::getSelectedChildPath() const { - return getChildPath(current_path, selected_child_entry); + return file::getChildPath(current_path, selected_child_entry); } bool State::setEntriesForPath(const std::string& path) { @@ -52,12 +52,12 @@ bool State::setEntriesForPath(const std::string& path) { dir_entries.clear(); dir_entries.push_back(dirent{ .d_ino = 0, - .d_type = TT_DT_DIR, + .d_type = file::TT_DT_DIR, .d_name = SYSTEM_PARTITION_NAME }); dir_entries.push_back(dirent{ .d_ino = 1, - .d_type = TT_DT_DIR, + .d_type = file::TT_DT_DIR, .d_name = DATA_PARTITION_NAME }); @@ -68,7 +68,7 @@ bool State::setEntriesForPath(const std::string& path) { auto mount_name = sdcard->getMountPath().substr(1); auto dir_entry = dirent { .d_ino = 2, - .d_type = TT_DT_DIR, + .d_type = file::TT_DT_DIR, .d_name = { 0 } }; assert(mount_name.length() < sizeof(dirent::d_name)); @@ -83,7 +83,7 @@ bool State::setEntriesForPath(const std::string& path) { return true; } else { dir_entries.clear(); - int count = tt::app::files::scandir(path, dir_entries, &dirent_filter_dot_entries, dirent_sort_alpha_and_type); + int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType); if (count >= 0) { TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count); current_path = path; @@ -97,8 +97,8 @@ bool State::setEntriesForPath(const std::string& path) { } } -bool State::setEntriesForChildPath(const std::string& child_path) { - auto path = getChildPath(current_path, child_path); +bool State::setEntriesForChildPath(const std::string& childPath) { + auto path = file::getChildPath(current_path, childPath); TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str()); return setEntriesForPath(path); } diff --git a/Tactility/Source/app/filebrowser/SupportedFiles.cpp b/Tactility/Source/app/filebrowser/SupportedFiles.cpp new file mode 100644 index 00000000..3069ad08 --- /dev/null +++ b/Tactility/Source/app/filebrowser/SupportedFiles.cpp @@ -0,0 +1,34 @@ +#include +#include + +namespace tt::app::filebrowser { + +#define TAG "filebrowser_app" + +bool isSupportedExecutableFile(const std::string& filename) { +#ifdef ESP_PLATFORM + // Currently only the PNG library is built into Tactility + return filename.ends_with(".elf"); +#else + return false; +#endif +} + +bool isSupportedImageFile(const std::string& filename) { + // Currently only the PNG library is built into Tactility + return string::lowercase(filename).ends_with(".png"); +} + +bool isSupportedTextFile(const std::string& filename) { + std::string filename_lower = string::lowercase(filename); + return filename_lower.ends_with(".txt") || + filename_lower.ends_with(".ini") || + filename_lower.ends_with(".json") || + filename_lower.ends_with(".yaml") || + filename_lower.ends_with(".yml") || + filename_lower.ends_with(".lua") || + filename_lower.ends_with(".js") || + filename_lower.ends_with(".properties"); +} + +} // namespace tt::app::filebrowser diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/filebrowser/View.cpp similarity index 87% rename from Tactility/Source/app/files/View.cpp rename to Tactility/Source/app/filebrowser/View.cpp index f927bd4c..5da95b85 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/filebrowser/View.cpp @@ -1,5 +1,5 @@ -#include "Tactility/app/files/FileUtils.h" -#include "Tactility/app/files/View.h" +#include "Tactility/app/filebrowser/View.h" +#include "Tactility/app/filebrowser/SupportedFiles.h" #include "Tactility/app/alertdialog/AlertDialog.h" #include "Tactility/app/imageviewer/ImageViewer.h" @@ -10,6 +10,7 @@ #include "Tactility/lvgl/LvglSync.h" #include +#include "Tactility/file/File.h" #include #include @@ -19,43 +20,43 @@ #include "Tactility/service/loader/Loader.h" #endif -#define TAG "files_app" +#define TAG "filebrowser_app" -namespace tt::app::files { +namespace tt::app::filebrowser { // region Callbacks static void dirEntryListScrollBeginCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onDirEntryListScrollBegin(); } static void onDirEntryPressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); auto* button = lv_event_get_target_obj(event); auto index = lv_obj_get_index(button); view->onDirEntryPressed(index); } static void onDirEntryLongPressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); auto* button = lv_event_get_target_obj(event); auto index = lv_obj_get_index(button); view->onDirEntryLongPressed(index); } static void onRenamePressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onRenamePressed(); } static void onDeletePressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onDeletePressed(); } static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onNavigateUpPressed(); } @@ -86,18 +87,18 @@ void View::viewFile(const std::string& path, const std::string& filename) { if (isSupportedExecutableFile(filename)) { #ifdef ESP_PLATFORM - app::registerElfApp(processed_filepath); - auto app_id = app::getElfAppId(processed_filepath); + registerElfApp(processed_filepath); + auto app_id = getElfAppId(processed_filepath); service::loader::startApp(app_id); #endif } else if (isSupportedImageFile(filename)) { - app::imageviewer::start(processed_filepath); + imageviewer::start(processed_filepath); } else if (isSupportedTextFile(filename)) { if (kernel::getPlatform() == kernel::PlatformEsp) { - app::textviewer::start(processed_filepath); + textviewer::start(processed_filepath); } else { // Remove forward slash, because we need a relative path - app::textviewer::start(processed_filepath.substr(1)); + textviewer::start(processed_filepath.substr(1)); } } else { TT_LOG_W(TAG, "opening files of this type is not supported"); @@ -111,6 +112,7 @@ void View::onDirEntryPressed(uint32_t index) { if (state->getDirent(index, dir_entry)) { TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); state->setSelectedChildEntry(dir_entry.d_name); + using namespace tt::file; switch (dir_entry.d_type) { case TT_DT_DIR: case TT_DT_CHR: @@ -140,6 +142,7 @@ void View::onDirEntryLongPressed(int32_t index) { if (state->getDirent(index, dir_entry)) { TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); state->setSelectedChildEntry(dir_entry.d_name); + using namespace file; switch (dir_entry.d_type) { case TT_DT_DIR: case TT_DT_CHR: @@ -161,15 +164,14 @@ void View::onDirEntryLongPressed(int32_t index) { } -void View::createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry) { - tt_check(parent); - auto* list = (lv_obj_t*)parent; +void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { + tt_check(list); const char* symbol; - if (dir_entry.d_type == TT_DT_DIR || dir_entry.d_type == TT_DT_CHR) { + if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) { symbol = LV_SYMBOL_DIRECTORY; } else if (isSupportedImageFile(dir_entry.d_name)) { symbol = LV_SYMBOL_IMAGE; - } else if (dir_entry.d_type == TT_DT_LNK) { + } else if (dir_entry.d_type == file::TT_DT_LNK) { symbol = LV_SYMBOL_LOOP; } else { symbol = LV_SYMBOL_FILE; @@ -195,7 +197,7 @@ void View::onRenamePressed() { std::string entry_name = state->getSelectedChildEntry(); TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str()); state->setPendingAction(State::ActionRename); - app::inputdialog::start("Rename", "", entry_name); + inputdialog::start("Rename", "", entry_name); } void View::onDeletePressed() { @@ -204,7 +206,7 @@ void View::onDeletePressed() { state->setPendingAction(State::ActionDelete); std::string message = "Do you want to delete this?\n" + file_path; const std::vector choices = { "Yes", "No" }; - app::alertdialog::start("Are you sure?", message, choices); + alertdialog::start("Are you sure?", message, choices); } void View::showActionsForDirectory() { @@ -301,7 +303,7 @@ void View::onResult(Result result, std::unique_ptr bundle) { switch (state->getPendingAction()) { case State::ActionDelete: { if (alertdialog::getResultIndex(*bundle) == 0) { - int delete_count = (int)remove(filepath.c_str()); + int delete_count = remove(filepath.c_str()); if (delete_count > 0) { TT_LOG_I(TAG, "Deleted %d items", delete_count); } else { @@ -313,9 +315,9 @@ void View::onResult(Result result, std::unique_ptr bundle) { break; } case State::ActionRename: { - auto new_name = app::inputdialog::getResult(*bundle); + auto new_name = inputdialog::getResult(*bundle); if (!new_name.empty() && new_name != state->getSelectedChildEntry()) { - std::string rename_to = getChildPath(state->getCurrentPath(), new_name); + std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name); if (rename(filepath.c_str(), rename_to.c_str())) { TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); } else { diff --git a/Tactility/Source/app/files/FileUtils.cpp b/Tactility/Source/app/files/FileUtils.cpp deleted file mode 100644 index a649ea91..00000000 --- a/Tactility/Source/app/files/FileUtils.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "Tactility/app/files/FileUtils.h" - -#include -#include - -#include - -namespace tt::app::files { - -#define TAG "file_utils" - -std::string getChildPath(const std::string& basePath, const std::string& childPath) { - // Postfix with "/" when the current path isn't "/" - if (basePath.length() != 1) { - return basePath + "/" + childPath; - } else { - return "/" + childPath; - } -} - -int dirent_filter_dot_entries(const struct dirent* entry) { - return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; -} - -bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right) { - bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR; - bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR; - if (left_is_dir == right_is_dir) { - return strcmp(left.d_name, right.d_name) < 0; - } else { - return left_is_dir > right_is_dir; - } -} - -int scandir( - const std::string& path, - std::vector& outList, - ScandirFilter _Nullable filterMethod, - ScandirSort _Nullable sortMethod -) { - TT_LOG_I(TAG, "scandir start"); - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); - return -1; - } - - struct dirent* current_entry; - while ((current_entry = readdir(dir)) != nullptr) { - if (filterMethod(current_entry) == 0) { - outList.push_back(*current_entry); - } - } - - closedir(dir); - - if (sortMethod != nullptr) { - sort(outList.begin(), outList.end(), sortMethod); - } - - TT_LOG_I(TAG, "scandir finish"); - return (int)outList.size(); -}; - -bool isSupportedExecutableFile(const std::string& filename) { -#ifdef ESP_PLATFORM - // Currently only the PNG library is built into Tactility - return filename.ends_with(".elf"); -#else - return false; -#endif -} - -bool isSupportedImageFile(const std::string& filename) { - // Currently only the PNG library is built into Tactility - return string::lowercase(filename).ends_with(".png"); -} - -bool isSupportedTextFile(const std::string& filename) { - std::string filename_lower = string::lowercase(filename); - return filename_lower.ends_with(".txt") || - filename_lower.ends_with(".ini") || - filename_lower.ends_with(".json") || - filename_lower.ends_with(".yaml") || - filename_lower.ends_with(".yml") || - filename_lower.ends_with(".lua") || - filename_lower.ends_with(".js") || - filename_lower.ends_with(".properties"); -} - -} // namespace tt::app::files diff --git a/Tactility/Source/app/fileselection/FileSelection.cpp b/Tactility/Source/app/fileselection/FileSelection.cpp new file mode 100644 index 00000000..0784d001 --- /dev/null +++ b/Tactility/Source/app/fileselection/FileSelection.cpp @@ -0,0 +1,78 @@ +#include "Tactility/app/fileselection/FileSelectionPrivate.h" +#include "Tactility/app/fileselection/View.h" +#include "Tactility/app/fileselection/State.h" +#include "Tactility/app/AppContext.h" + +#include +#include + +#include + +namespace tt::app::fileselection { + +#define TAG "fileselection_app" + +extern const AppManifest manifest; + +std::string getResultPath(const Bundle& bundle) { + std::string result; + if (bundle.optString("path", result)) { + return result; + } else { + return ""; + } +} + +Mode getMode(const Bundle& bundle) { + int32_t mode = static_cast(Mode::ExistingOrNew); + bundle.optInt32("mode", mode); + return static_cast(mode); +} + +void setMode(Bundle& bundle, Mode mode) { + auto mode_int = static_cast(mode); + bundle.putInt32("mode", mode_int); +} + +class FileSelection : public App { + std::unique_ptr view; + std::shared_ptr state; + +public: + FileSelection() { + state = std::make_shared(); + view = std::make_unique(state, [this](const std::string& path) { + auto bundle = std::make_unique(); + bundle->putString("path", path); + setResult(Result::Ok, std::move(bundle)); + service::loader::stopApp(); + }); + } + + void onShow(AppContext& appContext, lv_obj_t* parent) override { + auto mode = getMode(*appContext.getParameters()); + view->init(parent, mode); + } +}; + +extern const AppManifest manifest = { + .id = "FileSelection", + .name = "File Selection", + .icon = TT_ASSETS_APP_ICON_FILES, + .type = Type::Hidden, + .createApp = create +}; + +LaunchId startForExistingFile() { + auto bundle = std::make_shared(); + setMode(*bundle, Mode::Existing); + return service::loader::startApp(manifest.id, bundle); +} + +LaunchId startForExistingOrNewFile() { + auto bundle = std::make_shared(); + setMode(*bundle, Mode::ExistingOrNew); + return service::loader::startApp(manifest.id, bundle); +} + +} // namespace diff --git a/Tactility/Source/app/fileselection/State.cpp b/Tactility/Source/app/fileselection/State.cpp new file mode 100644 index 00000000..c648b6d9 --- /dev/null +++ b/Tactility/Source/app/fileselection/State.cpp @@ -0,0 +1,118 @@ +#include "Tactility/app/fileselection/State.h" + +#include +#include "Tactility/hal/sdcard/SdCardDevice.h" +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "fileselection_app" + +namespace tt::app::fileselection { + +State::State() { + if (kernel::getPlatform() == kernel::PlatformSimulator) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) != nullptr) { + setEntriesForPath(cwd); + } else { + TT_LOG_E(TAG, "Failed to get current work directory files"); + setEntriesForPath("/"); + } + } else { + setEntriesForPath("/"); + } +} + +std::string State::getSelectedChildPath() const { + return file::getChildPath(current_path, selected_child_entry); +} + +bool State::setEntriesForPath(const std::string& path) { + auto lock = mutex.asScopedLock(); + if (!lock.lock(100)) { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "setEntriesForPath"); + return false; + } + + TT_LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str()); + + /** + * ESP32 does not have a root directory, so we have to create it manually. + * We'll add the NVS Flash partitions and the binding for the sdcard. + */ + bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/"); + if (show_custom_root) { + TT_LOG_I(TAG, "Setting custom root"); + dir_entries.clear(); + dir_entries.push_back(dirent{ + .d_ino = 0, + .d_type = file::TT_DT_DIR, + .d_name = SYSTEM_PARTITION_NAME + }); + dir_entries.push_back(dirent{ + .d_ino = 1, + .d_type = file::TT_DT_DIR, + .d_name = DATA_PARTITION_NAME + }); + + auto sdcards = tt::hal::findDevices(hal::Device::Type::SdCard); + for (auto& sdcard : sdcards) { + auto state = sdcard->getState(); + if (state == hal::sdcard::SdCardDevice::State::Mounted) { + auto mount_name = sdcard->getMountPath().substr(1); + auto dir_entry = dirent { + .d_ino = 2, + .d_type = file::TT_DT_DIR, + .d_name = { 0 } + }; + assert(mount_name.length() < sizeof(dirent::d_name)); + strcpy(dir_entry.d_name, mount_name.c_str()); + dir_entries.push_back(dir_entry); + } + } + + current_path = path; + selected_child_entry = ""; + return true; + } else { + dir_entries.clear(); + int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType); + if (count >= 0) { + TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count); + current_path = path; + selected_child_entry = ""; + return true; + } else { + TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str()); + return false; + } + } +} + +bool State::setEntriesForChildPath(const std::string& childPath) { + auto path = file::getChildPath(current_path, childPath); + TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str()); + return setEntriesForPath(path); +} + +bool State::getDirent(uint32_t index, dirent& dirent) { + auto lock = mutex.asScopedLock(); + if (!lock.lock(50 / portTICK_PERIOD_MS)) { + return false; + } + + if (index < dir_entries.size()) { + dirent = dir_entries[index]; + return true; + } else { + return false; + } +} + +} diff --git a/Tactility/Source/app/fileselection/View.cpp b/Tactility/Source/app/fileselection/View.cpp new file mode 100644 index 00000000..10513923 --- /dev/null +++ b/Tactility/Source/app/fileselection/View.cpp @@ -0,0 +1,215 @@ +#include "Tactility/app/fileselection/View.h" + +#include "Tactility/app/alertdialog/AlertDialog.h" +#include "Tactility/lvgl/Toolbar.h" +#include "Tactility/lvgl/LvglSync.h" + +#include +#include "Tactility/file/File.h" +#include + +#include +#include +#include + +#ifdef ESP_PLATFORM +#include "Tactility/service/loader/Loader.h" +#endif + +#define TAG "fileselection_app" + +namespace tt::app::fileselection { + +// region Callbacks + +static void onDirEntryPressedCallback(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + auto* button = lv_event_get_target_obj(event); + auto index = lv_obj_get_index(button); + view->onDirEntryPressed(index); +} + +static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->onNavigateUpPressed(); +} + +// endregion + +void View::onTapFile(const std::string& path, const std::string& filename) { + std::string file_path = path + "/" + filename; + + // For PC we need to make the path relative to the current work directory, + // because that's how LVGL maps its 'drive letter' to the file system. + std::string processed_filepath; + if (kernel::getPlatform() == kernel::PlatformSimulator) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) == nullptr) { + TT_LOG_E(TAG, "Failed to get current working directory"); + return; + } + if (!file_path.starts_with(cwd)) { + TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd); + return; + } + processed_filepath = file_path.substr(strlen(cwd)); + } else { + processed_filepath = file_path; + } + + TT_LOG_I(TAG, "Clicked %s", processed_filepath.c_str()); + + lv_textarea_set_text(path_textarea, processed_filepath.c_str()); +} + +void View::onDirEntryPressed(uint32_t index) { + dirent dir_entry; + if (state->getDirent(index, dir_entry)) { + TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); + state->setSelectedChildEntry(dir_entry.d_name); + using namespace tt::file; + switch (dir_entry.d_type) { + case TT_DT_DIR: + case TT_DT_CHR: + state->setEntriesForChildPath(dir_entry.d_name); + lv_textarea_set_text(path_textarea, state->getCurrentPathWithTrailingSlash().c_str()); + update(); + break; + case TT_DT_LNK: + TT_LOG_W(TAG, "opening links is not supported"); + break; + case TT_DT_REG: + onTapFile(state->getCurrentPath(), dir_entry.d_name); + break; + default: + // Assume it's a file + // TODO: Find a better way to identify a file + onTapFile(state->getCurrentPath(), dir_entry.d_name); + break; + } + } +} + +void View::onSelectButtonPressed(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + const char* path = lv_textarea_get_text(view->path_textarea); + if (path == nullptr || strlen(path) == 0) { + TT_LOG_W(TAG, "Select pressed, but not path found in textarea"); + return; + } + + view->onFileSelected(std::string(path)); +} + +static bool isSelectableFilePath(const char* path) { + if (path == nullptr) { + return false; + } + + auto len = strlen(path); + if (len == 0) { + return false; + } + + return path[len - 1] != '/'; +} + +void View::onPathTextChanged(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + const char* path = lv_textarea_get_text(view->path_textarea); + if (isSelectableFilePath(path)) { + lv_obj_remove_flag(view->select_button, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(view->select_button, LV_OBJ_FLAG_HIDDEN); + } +} + +void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { + tt_check(list); + const char* symbol; + if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) { + symbol = LV_SYMBOL_DIRECTORY; + } else { + symbol = LV_SYMBOL_FILE; + } + lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name); + lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this); +} + +void View::onNavigateUpPressed() { + if (state->getCurrentPath() != "/") { + TT_LOG_I(TAG, "Navigating upwards"); + std::string new_absolute_path; + if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) { + state->setEntriesForPath(new_absolute_path); + } + if (new_absolute_path.length() > 1) { + lv_textarea_set_text(path_textarea, (new_absolute_path + "/").c_str()); + } else { + lv_textarea_set_text(path_textarea, new_absolute_path.c_str()); + } + update(); + } +} + +void View::update() { + auto scoped_lockable = lvgl::getSyncLock()->asScopedLock(); + if (scoped_lockable.lock(lvgl::defaultLockTime)) { + lv_obj_clean(dir_entry_list); + + state->withEntries([this](const std::vector& entries) { + for (auto entry : entries) { + TT_LOG_D(TAG, "Entry: %s %d", entry.d_name, entry.d_type); + createDirEntryWidget(dir_entry_list, entry); + } + }); + + if (state->getCurrentPath() == "/") { + lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + } + } else { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl"); + } +} + +void View::init(lv_obj_t* parent, Mode mode) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + auto* toolbar = lvgl::toolbar_create(parent, "Select File"); + navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this); + + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_style_pad_all(wrapper, 0, 0); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + + dir_entry_list = lv_list_create(wrapper); + lv_obj_set_height(dir_entry_list, LV_PCT(100)); + lv_obj_set_flex_grow(dir_entry_list, 1); + + auto* bottom_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(bottom_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_size(bottom_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(bottom_wrapper, 0, 0); + lv_obj_set_style_pad_all(bottom_wrapper, 0, 0); + + path_textarea = lv_textarea_create(bottom_wrapper); + lv_textarea_set_one_line(path_textarea, true); + lv_obj_set_flex_grow(path_textarea, 1); + service::gui::keyboardAddTextArea(path_textarea); + lv_obj_add_event_cb(path_textarea, onPathTextChanged, LV_EVENT_VALUE_CHANGED, this); + + select_button = lv_button_create(bottom_wrapper); + auto* select_button_label = lv_label_create(select_button); + lv_label_set_text(select_button_label, "Select"); + lv_obj_add_event_cb(select_button, onSelectButtonPressed, LV_EVENT_SHORT_CLICKED, this); + lv_obj_add_flag(select_button, LV_OBJ_FLAG_HIDDEN); + + update(); +} + +} diff --git a/Tactility/Source/app/log/Log.cpp b/Tactility/Source/app/log/Log.cpp index fbd858a2..f2877e05 100644 --- a/Tactility/Source/app/log/Log.cpp +++ b/Tactility/Source/app/log/Log.cpp @@ -91,10 +91,9 @@ public: updateLogEntries(); } - void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { - auto resultIndex = selectiondialog::getResultIndex(*bundle); - switch (resultIndex) { + switch (selectiondialog::getResultIndex(*bundle)) { case 0: filterLevel = LogLevel::Verbose; break; diff --git a/Tactility/Source/app/notes/Notes.cpp b/Tactility/Source/app/notes/Notes.cpp index 301953cb..efb41d46 100644 --- a/Tactility/Source/app/notes/Notes.cpp +++ b/Tactility/Source/app/notes/Notes.cpp @@ -5,35 +5,30 @@ #include #include -#include -#include +#include +#include +#include namespace tt::app::notes { constexpr const char* TAG = "Notes"; class NotesApp : public App { + AppContext* appContext = nullptr; lv_obj_t* uiCurrentFileName; lv_obj_t* uiDropDownMenu; - lv_obj_t* uiFileList; - lv_obj_t* uiFileListCloseBtn; lv_obj_t* uiNoteText; - lv_obj_t* uiSaveDialog; - lv_obj_t* uiSaveDialogFileName; - lv_obj_t* uiSaveDialogSaveBtn; - lv_obj_t* uiSaveDialogCancelBtn; lv_obj_t* uiMessageBox; lv_obj_t* uiMessageBoxButtonOk; lv_obj_t* uiMessageBoxButtonNo; - char menuItem[32]; - uint8_t menuIdx = 0; - std::string fileContents; - std::string fileName; - std::string newFileName; std::string filePath; + std::string saveBuffer; + + LaunchId loadFileLaunchId = 0; + LaunchId saveFileLaunchId = 0; #pragma region Main_Events_Functions @@ -42,81 +37,56 @@ class NotesApp : public App { lv_obj_t* obj = lv_event_get_target_obj(e); if (code == LV_EVENT_CLICKED) { - if (obj == uiFileListCloseBtn) { - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - lv_obj_del(uiFileList); - } else if (obj == uiSaveDialogSaveBtn) { - newFileName = lv_textarea_get_text(uiSaveDialogFileName); - if (newFileName.length() == 0) { - uiMessageBoxShow(menuItem, "Filename is empty.", false); - } else { - std::string noteText = lv_textarea_get_text(uiNoteText); - filePath = appContext->getPaths()->getDataPath(newFileName); - - if (writeFile(filePath, noteText)) { - uiMessageBoxShow(menuItem, "File created successfully!", false); - lv_label_set_text(uiCurrentFileName, newFileName.c_str()); - } else { - uiMessageBoxShow(menuItem, "Something went wrong!\nFile creation failed.", false); - } - } - lv_obj_del(uiSaveDialog); - - } else if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) { + if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) { lv_obj_del(uiMessageBox); - } else if (obj == uiSaveDialogCancelBtn) { - lv_obj_del(uiSaveDialog); } } if (code == LV_EVENT_VALUE_CHANGED) { if (obj == uiDropDownMenu) { - lv_dropdown_get_selected_str(obj, menuItem, sizeof(menuItem)); - menuIdx = lv_dropdown_get_selected(obj); - std::string newContents = lv_textarea_get_text(uiNoteText); - if (menuIdx == 1) { //Save - //Normal Save? + switch (lv_dropdown_get_selected(obj)) { + case 0: // New + resetFileContent(); + break; + case 1: // Save + if (!filePath.empty()) { + lvgl::getSyncLock()->lock(); + saveBuffer = lv_textarea_get_text(uiNoteText); + lvgl::getSyncLock()->unlock(); + saveFile(filePath); + } + break; + case 2: // Save as... + lvgl::getSyncLock()->lock(); + saveBuffer = lv_textarea_get_text(uiNoteText); + lvgl::getSyncLock()->unlock(); + saveFileLaunchId = fileselection::startForExistingOrNewFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); + break; + case 3: // Load + loadFileLaunchId = fileselection::startForExistingFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); + break; } - if (menuIdx == 2) { //Save As... - uiSaveFileDialog(); - return; - } - - //Not working...more investigation needed. - //If note contents has changed in currently open file, save it. - - //bool newToSave = newContents != fileContents && newContents.length() != 0; - //if (newToSave) { - //uiMessageBoxShow(menuItem, "Do you want to save it?", true); - //} else { - menuAction(); - //} } else { - lv_obj_t* cont = lv_event_get_current_target_obj(e); + auto* cont = lv_event_get_current_target_obj(e); if (obj == cont) return; if (lv_obj_get_child(cont, 1)) { - uiSaveFileDialog(); + saveFileLaunchId = fileselection::startForExistingOrNewFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); } else { //Reset - lv_textarea_set_text(uiNoteText, ""); - fileName = ""; - lv_label_set_text(uiCurrentFileName, "Untitled"); + resetFileContent(); } lv_obj_delete(uiMessageBox); } } } - void menuAction() { - switch (menuIdx) { - case 0: //Reset - lv_textarea_set_text(uiNoteText, ""); - fileName = ""; - lv_label_set_text(uiCurrentFileName, "Untitled"); - break; - case 3: - uiOpenFileDialog(); - break; - } + void resetFileContent() { + lv_textarea_set_text(uiNoteText, ""); + filePath = ""; + saveBuffer = ""; + lv_label_set_text(uiCurrentFileName, "Untitled"); } void uiMessageBoxShow(std::string title, std::string message, bool isSelectable) { @@ -166,166 +136,49 @@ class NotesApp : public App { lv_obj_t* buttonLabelOk = lv_label_create(uiMessageBoxButtonOk); lv_obj_align(buttonLabelOk, LV_ALIGN_BOTTOM_MID, 0, 0); lv_label_set_text(buttonLabelOk, "Ok"); - lv_obj_add_event_cb(uiMessageBoxButtonOk, [](lv_event_t* e) { + lv_obj_add_event_cb(uiMessageBoxButtonOk, + [](lv_event_t* e) { auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); + self->appNotesEventCb(e); + }, + LV_EVENT_CLICKED, + this + ); + } + + if (!filePath.empty()) { + openFile(filePath); } } -#pragma endregion Main_Events_Functions - -#pragma region Save_Events_Functions - - void uiSaveFileDialog() { - uiSaveDialog = lv_obj_create(lv_scr_act()); - if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { //small screens - lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 80); - } else { //large screens - lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 230); - } - lv_obj_align(uiSaveDialog, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(uiSaveDialog, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t* uiSaveDialogTitle = lv_label_create(uiSaveDialog); - lv_label_set_text(uiSaveDialogTitle, menuItem); - lv_obj_set_size(uiSaveDialogTitle, lv_display_get_horizontal_resolution(nullptr) - 30, 30); - lv_obj_align(uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 0); - - uiSaveDialogFileName = lv_textarea_create(uiSaveDialog); - lv_obj_set_size(uiSaveDialogFileName, lv_display_get_horizontal_resolution(nullptr) - 30, 40); - lv_obj_align_to(uiSaveDialogFileName, uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 30); - lv_textarea_set_placeholder_text(uiSaveDialogFileName, "Enter file name..."); - lv_textarea_set_one_line(uiSaveDialogFileName, true); - lv_obj_add_state(uiSaveDialogFileName, LV_STATE_FOCUSED); - - //Both hardware and software keyboard not auto attaching here for some reason unless the textarea is touched to focus it... - tt::lvgl::keyboard_add_textarea(uiSaveDialogFileName); - - if (fileName != "" || fileName != "Untitled") { - lv_textarea_set_text(uiSaveDialogFileName, fileName.c_str()); - } else { - lv_textarea_set_placeholder_text(uiSaveDialogFileName, "filename?"); - } - - uiSaveDialogSaveBtn = lv_btn_create(uiSaveDialog); - lv_obj_add_event_cb(uiSaveDialogSaveBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - lv_obj_align(uiSaveDialogSaveBtn, LV_ALIGN_BOTTOM_LEFT, 0, 0); - lv_obj_t* btnLabel = lv_label_create(uiSaveDialogSaveBtn); - lv_label_set_text(btnLabel, "Save"); - lv_obj_center(btnLabel); - - uiSaveDialogCancelBtn = lv_btn_create(uiSaveDialog); - lv_obj_add_event_cb(uiSaveDialogCancelBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - lv_obj_align(uiSaveDialogCancelBtn, LV_ALIGN_BOTTOM_RIGHT, 0, 0); - lv_obj_t* btnLabel2 = lv_label_create(uiSaveDialogCancelBtn); - lv_label_set_text(btnLabel2, "Cancel"); - lv_obj_center(btnLabel2); - } - - bool writeFile(std::string path, std::string message) { - std::ofstream fileStream(path); - - if (!fileStream.is_open()) { - TT_LOG_E(TAG, "Failed to write file"); - return false; - } - if (fileStream.is_open()) { - fileStream << message; - TT_LOG_I(TAG, "File written successfully"); - fileStream.close(); - return true; - } - return true; - } - -#pragma endregion Save_Events_Functions - #pragma region Open_Events_Functions - void openFileEventCb(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t* obj = lv_event_get_target_obj(e); - - if (code == LV_EVENT_CLICKED) { - std::string selectedFile = lv_list_get_btn_text(uiFileList, obj); - fileName = selectedFile.substr(0, selectedFile.find(" (")); - std::string filePath = appContext->getPaths()->getDataPath(fileName); - fileContents = readFile(filePath.c_str()); - lv_textarea_set_text(uiNoteText, fileContents.c_str()); - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - lv_obj_del(uiFileList); - - lv_label_set_text(uiCurrentFileName, fileName.c_str()); - } + void openFile(const std::string& path) { + // We might be reading from the SD card, which could share a SPI bus with other devices (display) + hal::sdcard::withSdCardLock(path, [this, path]() { + auto data = file::readString(path); + if (data != nullptr) { + auto lock = lvgl::getSyncLock()->asScopedLock(); + lock.lock(); + lv_textarea_set_text(uiNoteText, reinterpret_cast(data.get())); + lv_label_set_text(uiCurrentFileName, path.c_str()); + filePath = path; + TT_LOG_I(TAG, "Loaded from %s", path.c_str()); + } + }); } - void uiOpenFileDialog() { - uiFileList = lv_list_create(lv_scr_act()); - lv_obj_set_size(uiFileList, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr)); - lv_obj_align(uiFileList, LV_ALIGN_TOP_MID, 0, 0); - lv_list_add_text(uiFileList, "Notes"); - - uiFileListCloseBtn = lv_btn_create(uiFileList); - lv_obj_set_size(uiFileListCloseBtn, 36, 36); - lv_obj_add_flag(uiFileListCloseBtn, LV_OBJ_FLAG_FLOATING); - lv_obj_align(uiFileListCloseBtn, LV_ALIGN_TOP_RIGHT, 10, 4); - lv_obj_add_event_cb(uiFileListCloseBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - - lv_obj_t* uiFileListCloseLabel = lv_label_create(uiFileListCloseBtn); - lv_label_set_text(uiFileListCloseLabel, LV_SYMBOL_CLOSE); - lv_obj_center(uiFileListCloseLabel); - - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - - //TODO: Move this to SD Card? - std::vector noteFileList; - const std::string& path = appContext->getPaths()->getDataDirectory(); - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); - return; - } - - struct dirent* current_entry; - while ((current_entry = readdir(dir)) != nullptr) { - noteFileList.push_back(current_entry->d_name); - } - closedir(dir); - - if (noteFileList.size() == 0) return; - - for (std::vector::iterator item = noteFileList.begin(); item != noteFileList.end(); ++item) { - lv_obj_t* btn = lv_list_add_btn(uiFileList, LV_SYMBOL_FILE, (*item).c_str()); - lv_obj_add_event_cb(btn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->openFileEventCb(e); }, LV_EVENT_CLICKED, this); - } - - lv_obj_move_foreground(uiFileListCloseBtn); - lv_obj_remove_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - } - - std::string readFile(std::string path) { - std::ifstream fileStream(path); - - if (!fileStream.is_open()) { - return ""; - } - - std::string temp = ""; - std::string file_contents; - while (std::getline(fileStream, temp)) { - file_contents += temp; - file_contents.push_back('\n'); - } - fileStream.close(); - return file_contents; + bool saveFile(const std::string& path) { + // We might be writing to SD card, which could share a SPI bus with other devices (display) + return hal::sdcard::withSdCardLock(path, [this, path]() { + if (file::writeString(path, saveBuffer.c_str())) { + TT_LOG_I(TAG, "Saved to %s", path.c_str()); + filePath = path; + return true; + } else { + return false; + } + }); } #pragma endregion Open_Events_Functions @@ -347,9 +200,14 @@ class NotesApp : public App { lv_obj_set_style_border_color(uiDropDownMenu, lv_color_hex(0xFAFAFA), LV_PART_MAIN); lv_obj_set_style_border_width(uiDropDownMenu, 1, LV_PART_MAIN); lv_obj_align(uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(uiDropDownMenu, [](lv_event_t* e) { + lv_obj_add_event_cb(uiDropDownMenu, + [](lv_event_t* e) { auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_VALUE_CHANGED, this); + self->appNotesEventCb(e); + }, + LV_EVENT_VALUE_CHANGED, + this + ); lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); @@ -387,11 +245,31 @@ class NotesApp : public App { lv_obj_align(uiCurrentFileName, LV_ALIGN_CENTER, 0, 0); //TODO: Move this to SD Card? - if (!tt::file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) { + if (!file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) { TT_LOG_E(TAG, "Failed to find or create path %s", context.getPaths()->getDataDirectory().c_str()); } - tt::lvgl::keyboard_add_textarea(uiNoteText); + lvgl::keyboard_add_textarea(uiNoteText); + } + + void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr resultData) override { + TT_LOG_I(TAG, "Result for launch id %d", launchId); + if (launchId == loadFileLaunchId) { + loadFileLaunchId = 0; + if (result == Result::Ok && resultData != nullptr) { + auto path = fileselection::getResultPath(*resultData); + openFile(path); + } + } else if (launchId == saveFileLaunchId) { + saveFileLaunchId = 0; + if (result == Result::Ok && resultData != nullptr) { + auto path = fileselection::getResultPath(*resultData); + // Must re-open file, because UI was cleared after opening other app + if (saveFile(path)) { + openFile(path); + } + } + } } }; @@ -401,4 +279,5 @@ extern const AppManifest manifest = { .icon = TT_ASSETS_APP_ICON_NOTES, .createApp = create }; + } // namespace tt::app::notes \ No newline at end of file diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index a56ef480..f9290374 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -88,7 +88,7 @@ public: } } - void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { auto name = timezone::getResultName(*bundle); auto code = timezone::getResultCode(*bundle); diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 8c08f91e..4b18cd01 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -112,7 +112,7 @@ class WifiApSettings : public App { } } - void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr bundle) override { + void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED LaunchId launchId, TT_UNUSED Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { auto index = alertdialog::getResultIndex(*bundle); if (index == 0) { // Yes diff --git a/Tactility/Source/lvgl/LabelUtils.cpp b/Tactility/Source/lvgl/LabelUtils.cpp index 78ffbb63..90d6a4e7 100644 --- a/Tactility/Source/lvgl/LabelUtils.cpp +++ b/Tactility/Source/lvgl/LabelUtils.cpp @@ -1,14 +1,18 @@ #include #include +#include namespace tt::lvgl { #define TAG "tt_lv_label" bool label_set_text_file(lv_obj_t* label, const char* filepath) { - auto text = file::readString(filepath); + auto text = hal::sdcard::withSdCardLock>(std::string(filepath), [filepath]() { + return file::readString(filepath); + }); + if (text != nullptr) { - lv_label_set_text(label, (const char*)text.get()); + lv_label_set_text(label, reinterpret_cast(text.get())); return true; } else { return false; diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index e8cd20bf..e7c343ab 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -47,17 +47,17 @@ static const char* appStateToString(app::State state) { class LoaderService final : public Service { -private: - std::shared_ptr pubsubExternal = std::make_shared(); Mutex mutex = Mutex(Mutex::Type::Recursive); std::stack> appStack; + app::LaunchId nextLaunchId = 0; + /** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods. * This includes full LVGL redraw via Gui::redraw() */ std::unique_ptr dispatcherThread = std::make_unique("loader_dispatcher", 6144); // Files app requires ~5k - void onStartAppMessage(const std::string& id, std::shared_ptr parameters); + void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr parameters); void onStopAppMessage(const std::string& id); void transitionAppToState(const std::shared_ptr& app, app::State state); @@ -75,7 +75,7 @@ public: }); } - void startApp(const std::string& id, std::shared_ptr parameters); + app::LaunchId startApp(const std::string& id, std::shared_ptr parameters); void stopApp(); std::shared_ptr _Nullable getCurrentAppContext(); @@ -86,8 +86,7 @@ std::shared_ptr _Nullable optScreenshotService() { return service::findServiceById(manifest.id); } -void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr parameters) { - +void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr parameters) { TT_LOG_I(TAG, "Start by id %s", id.c_str()); auto app_manifest = app::findAppById(id); @@ -103,7 +102,7 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr(app_manifest, parameters); + auto new_app = std::make_shared(app_manifest, launchId, parameters); new_app->mutableFlags().showStatusbar = (app_manifest->type != app::Type::Boot); @@ -123,7 +122,6 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptrgetLaunchId(); + transitionAppToState(app_to_stop, app::State::Hiding); transitionAppToState(app_to_stop, app::State::Stopped); @@ -196,12 +196,14 @@ void LoaderService::onStopAppMessage(const std::string& id) { if (result_bundle != nullptr) { instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, result, std::move(result_bundle) ); } else { instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, result, nullptr ); @@ -210,6 +212,7 @@ void LoaderService::onStopAppMessage(const std::string& id) { const Bundle empty_bundle; instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, app::Result::Cancelled, nullptr ); @@ -255,10 +258,12 @@ void LoaderService::transitionAppToState(const std::shared_ptr app->setState(state); } -void LoaderService::startApp(const std::string& id, std::shared_ptr parameters) { - dispatcherThread->dispatch([this, id, parameters]() { - onStartAppMessage(id, parameters); +app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr parameters) { + auto launch_id = nextLaunchId++; + dispatcherThread->dispatch([this, id, launch_id, parameters]() { + onStartAppMessage(id, launch_id, parameters); }); + return launch_id; } void LoaderService::stopApp() { @@ -278,11 +283,11 @@ std::shared_ptr _Nullable LoaderService::getCurrentAppContext() // region Public API -void startApp(const std::string& id, std::shared_ptr parameters) { +app::LaunchId startApp(const std::string& id, std::shared_ptr parameters) { TT_LOG_I(TAG, "Start app %s", id.c_str()); auto service = optScreenshotService(); assert(service); - service->startApp(id, std::move(parameters)); + return service->startApp(id, std::move(parameters)); } void stopApp() { diff --git a/TactilityC/Include/tt_app_manifest.h b/TactilityC/Include/tt_app_manifest.h index c41b90f7..0640b1d3 100644 --- a/TactilityC/Include/tt_app_manifest.h +++ b/TactilityC/Include/tt_app_manifest.h @@ -16,6 +16,8 @@ typedef enum { typedef void* AppHandle; +typedef unsigned int LaunchId; + /** Important: These function types must map to t::app types exactly */ typedef void* (*AppCreateData)(); typedef void (*AppDestroyData)(void* data); @@ -23,7 +25,7 @@ typedef void (*AppOnCreate)(AppHandle app, void* _Nullable data); typedef void (*AppOnDestroy)(AppHandle app, void* _Nullable data); typedef void (*AppOnShow)(AppHandle app, void* _Nullable data, lv_obj_t* parent); typedef void (*AppOnHide)(AppHandle app, void* _Nullable data); -typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, Result result, BundleHandle resultData); +typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, LaunchId launchId, Result result, BundleHandle resultData); typedef struct { /** The application's human-readable name */ diff --git a/TactilityC/Source/tt_app_manifest.cpp b/TactilityC/Source/tt_app_manifest.cpp index 4d77a2b8..d274cfd8 100644 --- a/TactilityC/Source/tt_app_manifest.cpp +++ b/TactilityC/Source/tt_app_manifest.cpp @@ -12,16 +12,16 @@ void tt_app_register( ) { #ifdef ESP_PLATFORM assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr)); - tt::app::setElfAppManifest( + setElfAppManifest( manifest->name, manifest->icon, - (tt::app::CreateData)manifest->createData, - (tt::app::DestroyData)manifest->destroyData, - (tt::app::OnCreate)manifest->onCreate, - (tt::app::OnDestroy)manifest->onDestroy, - (tt::app::OnShow)manifest->onShow, - (tt::app::OnHide)manifest->onHide, - (tt::app::OnResult)manifest->onResult + manifest->createData, + manifest->destroyData, + manifest->onCreate, + manifest->onDestroy, + manifest->onShow, + manifest->onHide, + reinterpret_cast(manifest->onResult) ); #else tt_crash("TactilityC is not intended for PC/Simulator"); diff --git a/TactilityCore/Include/Tactility/file/File.h b/TactilityCore/Include/Tactility/file/File.h index 5f7d5b38..2aa83f1b 100644 --- a/TactilityCore/Include/Tactility/file/File.h +++ b/TactilityCore/Include/Tactility/file/File.h @@ -3,10 +3,34 @@ #include "Tactility/TactilityCore.h" #include +#include #include +#include namespace tt::file { +/** File types for `dirent`'s `d_type`. */ +enum { + TT_DT_UNKNOWN = 0, +#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type + TT_DT_FIFO = 1, +#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO + TT_DT_CHR = 2, +#define TT_DT_CHR TT_DT_CHR // Character device + TT_DT_DIR = 4, +#define TT_DT_DIR TT_DT_DIR // Directory + TT_DT_BLK = 6, +#define TT_DT_BLK TT_DT_BLK // Block device + TT_DT_REG = 8, +#define TT_DT_REG TT_DT_REG // Regular file + TT_DT_LNK = 10, +#define TT_DT_LNK TT_DT_LNK // Symbolic link + TT_DT_SOCK = 12, +#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket + TT_DT_WHT = 14 +#define TT_DT_WHT TT_DT_WHT // Whiteout inodes +}; + #ifdef _WIN32 constexpr char SEPARATOR = '\\'; #else @@ -36,6 +60,13 @@ std::unique_ptr readBinary(const std::string& filepath, size_t& outSi */ std::unique_ptr readString(const std::string& filepath); +/** Write text to a file + * @param[in] path file path to write to + * @param[in] content file content to write + * @return true when operation is successful + */ +bool writeString(const std::string& filepath, const std::string& content); + /** Ensure a directory path exists. * @param[in] path the directory path to find, or to create recursively * @param[in] mode the mode to use when creating directories @@ -43,4 +74,41 @@ std::unique_ptr readString(const std::string& filepath); */ bool findOrCreateDirectory(std::string path, mode_t mode); +/** + * Concatenate a child path with a parent path, ensuring proper slash inbetween + * @param basePath an absolute path with or without trailing "/" + * @param childPath the name of the child path (e.g. subfolder or file) + * @return the concatenated path + */ +std::string getChildPath(const std::string& basePath, const std::string& childPath); + +typedef int (*ScandirFilter)(const dirent*); + +typedef bool (*ScandirSort)(const dirent&, const dirent&); + +/** Used for sorting by alphanumeric value and file type */ +bool direntSortAlphaAndType(const dirent& left, const dirent& right); + +/** A filter for filtering out "." and ".." */ +int direntFilterDotEntries(const dirent* entry); + +/** + * A scandir()-like implementation that works on ESP32. + * It does not return "." and ".." items but otherwise functions the same. + * It returns an allocated output array with allocated dirent instances. + * The caller is responsible for free-ing the memory of these. + * + * @param[in] path path the scan for files and directories + * @param[out] outList a pointer to vector of dirent + * @param[in] filter an optional filter to filter out specific items + * @param[in] sort an optional sorting function + * @return the amount of items that were stored in "output" or -1 when an error occurred + */ +int scandir( + const std::string& path, + std::vector& outList, + ScandirFilter _Nullable filter, + ScandirSort _Nullable sort +); + } diff --git a/TactilityCore/Source/file/File.cpp b/TactilityCore/Source/file/File.cpp index 464ec835..8ca16dbc 100644 --- a/TactilityCore/Source/file/File.cpp +++ b/TactilityCore/Source/file/File.cpp @@ -1,9 +1,66 @@ #include "Tactility/file/File.h" +#include +#include + namespace tt::file { #define TAG "file" +std::string getChildPath(const std::string& basePath, const std::string& childPath) { + // Postfix with "/" when the current path isn't "/" + if (basePath.length() != 1) { + return basePath + "/" + childPath; + } else { + return "/" + childPath; + } +} + +int direntFilterDotEntries(const dirent* entry) { + return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; +} + +bool direntSortAlphaAndType(const dirent& left, const dirent& right) { + bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR; + bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR; + if (left_is_dir == right_is_dir) { + return strcmp(left.d_name, right.d_name) < 0; + } else { + return left_is_dir > right_is_dir; + } +} + +int scandir( + const std::string& path, + std::vector& outList, + ScandirFilter _Nullable filterMethod, + ScandirSort _Nullable sortMethod +) { + TT_LOG_I(TAG, "scandir start"); + DIR* dir = opendir(path.c_str()); + if (dir == nullptr) { + TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); + return -1; + } + + dirent* current_entry; + while ((current_entry = readdir(dir)) != nullptr) { + if (filterMethod(current_entry) == 0) { + outList.push_back(*current_entry); + } + } + + closedir(dir); + + if (sortMethod != nullptr) { + std::ranges::sort(outList, sortMethod); + } + + TT_LOG_I(TAG, "scandir finish"); + return outList.size(); +} + + long getSize(FILE* file) { long original_offset = ftell(file); @@ -80,12 +137,25 @@ std::unique_ptr readBinary(const std::string& filepath, size_t& outSi std::unique_ptr readString(const std::string& filepath) { size_t size = 0; auto data = readBinaryInternal(filepath, size, 1); - if (data != nullptr) { - data.get()[size] = 0; // Append null terminator - return data; - } else { + if (data == nullptr) { return nullptr; } + + data.get()[size] = 0; // Append null terminator + return data; +} + +bool writeString(const std::string& filepath, const std::string& content) { + std::ofstream fileStream(filepath); + + if (!fileStream.is_open()) { + return false; + } + + fileStream << content; + fileStream.close(); + + return true; } static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) {