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
@@ -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();
}
}