Implemented LaunchId and FileSelection, updated Notes (#281)

- Implemented `LaunchId` to keep track of the apps that are started
- Implemented `FileSelection` app to select existing and/or new files.
- Moved some re-usable file functionality to `tt::file::`
- Renamed `Files` app to `FileBrowser`
- Updated `Notes` app to use new `FileSelection` functionality, and cleaned it up a bit.
- General code cleanliness improvements
This commit is contained in:
Ken Van Hoeylandt
2025-05-25 22:11:50 +02:00
committed by GitHub
parent 74eb830870
commit 2691dbb014
38 changed files with 971 additions and 508 deletions
+4 -4
View File
@@ -4,19 +4,19 @@
namespace tt::app {
void start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
service::loader::startApp(id, std::move(parameters));
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
return service::loader::startApp(id, std::move(parameters));
}
void stop() {
service::loader::stopApp();
}
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
std::shared_ptr<AppContext> _Nullable getCurrentAppContext() {
return service::loader::getCurrentAppContext();
}
std::shared_ptr<app::App> _Nullable getCurrentApp() {
std::shared_ptr<App> _Nullable getCurrentApp() {
return service::loader::getCurrentApp();
}
+2 -4
View File
@@ -36,8 +36,6 @@ static ElfManifest elfManifest;
class ElfApp : public App {
private:
const std::string filePath;
std::unique_ptr<uint8_t[]> elfFileData;
esp_elf_t elf;
@@ -143,9 +141,9 @@ public:
}
}
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> resultBundle) override {
void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultBundle) override {
if (manifest != nullptr && manifest->onResult != nullptr) {
manifest->onResult(&appContext, data, result, resultBundle.get());
manifest->onResult(&appContext, data, launchId, result, resultBundle.get());
}
}
};
@@ -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 <Tactility/Assets.h>
@@ -7,18 +7,18 @@
#include <memory>
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> view;
std::shared_ptr<State> state;
public:
FilesApp() {
FileBrowser() {
state = std::make_shared<State>();
view = std::make_unique<View>(state);
}
@@ -27,7 +27,7 @@ public:
view->init(parent);
}
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> 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<FilesApp>
.createApp = create<FileBrowser>
};
void start() {
@@ -1,6 +1,6 @@
#include "Tactility/app/files/State.h"
#include "Tactility/app/files/FileUtils.h"
#include "Tactility/app/filebrowser/State.h"
#include <Tactility/file/File.h>
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/Partitions.h>
@@ -11,9 +11,9 @@
#include <vector>
#include <dirent.h>
#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);
}
@@ -0,0 +1,34 @@
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityCore.h>
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
@@ -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 <Tactility/Tactility.h>
#include "Tactility/file/File.h"
#include <Tactility/StringUtils.h>
#include <cstring>
@@ -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<View*>(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<View*>(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<View*>(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<View*>(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<View*>(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<View*>(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<std::string> 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> 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> 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 {
-91
View File
@@ -1,91 +0,0 @@
#include "Tactility/app/files/FileUtils.h"
#include <Tactility/StringUtils.h>
#include <Tactility/TactilityCore.h>
#include <cstring>
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<dirent>& 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
@@ -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 <Tactility/Assets.h>
#include <Tactility/service/loader/Loader.h>
#include <memory>
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<int32_t>(Mode::ExistingOrNew);
bundle.optInt32("mode", mode);
return static_cast<Mode>(mode);
}
void setMode(Bundle& bundle, Mode mode) {
auto mode_int = static_cast<int32_t>(mode);
bundle.putInt32("mode", mode_int);
}
class FileSelection : public App {
std::unique_ptr<View> view;
std::shared_ptr<State> state;
public:
FileSelection() {
state = std::make_shared<State>();
view = std::make_unique<View>(state, [this](const std::string& path) {
auto bundle = std::make_unique<Bundle>();
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<FileSelection>
};
LaunchId startForExistingFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::Existing);
return service::loader::startApp(manifest.id, bundle);
}
LaunchId startForExistingOrNewFile() {
auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::ExistingOrNew);
return service::loader::startApp(manifest.id, bundle);
}
} // namespace
@@ -0,0 +1,118 @@
#include "Tactility/app/fileselection/State.h"
#include <Tactility/file/File.h>
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/Partitions.h>
#include <Tactility/kernel/Kernel.h>
#include <cstring>
#include <unistd.h>
#include <vector>
#include <dirent.h>
#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::sdcard::SdCardDevice>(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;
}
}
}
+215
View File
@@ -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 <Tactility/Tactility.h>
#include "Tactility/file/File.h"
#include <Tactility/StringUtils.h>
#include <cstring>
#include <unistd.h>
#include <Tactility/service/gui/Gui.h>
#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<View*>(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<View*>(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<View*>(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<View*>(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<dirent>& 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();
}
}
+2 -3
View File
@@ -91,10 +91,9 @@ public:
updateLogEntries();
}
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> 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;
+106 -227
View File
@@ -5,35 +5,30 @@
#include <Tactility/Assets.h>
#include <lvgl.h>
#include <dirent.h>
#include <fstream>
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <Tactility/lvgl/LvglSync.h>
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<NotesApp *>(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<NotesApp *>(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<NotesApp *>(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<void>(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<const char*>(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<NotesApp *>(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<std::string> 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<std::string>::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<NotesApp *>(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<bool>(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<NotesApp *>(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<Bundle> 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<NotesApp>
};
} // namespace tt::app::notes
@@ -88,7 +88,7 @@ public:
}
}
void onResult(AppContext& app, Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
if (result == Result::Ok && bundle != nullptr) {
auto name = timezone::getResultName(*bundle);
auto code = timezone::getResultCode(*bundle);
@@ -112,7 +112,7 @@ class WifiApSettings : public App {
}
}
void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED LaunchId launchId, TT_UNUSED Result result, std::unique_ptr<Bundle> bundle) override {
if (result == Result::Ok && bundle != nullptr) {
auto index = alertdialog::getResultIndex(*bundle);
if (index == 0) { // Yes