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
+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