Implemented Files app (#33)

- Created Files app to browse PC and ESP32 files.
- Refactored toolbars so it's now a proper widget and allows for changing its properties from the app
- Toolbar now has extra action buttons
- Settings app now has a proper icon
- Minor cleanup in Desktop app
This commit is contained in:
Ken Van Hoeylandt 2024-02-06 23:18:34 +01:00 committed by GitHub
parent 93e4378a9e
commit 5880e841a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 689 additions and 117 deletions

View File

@ -28,6 +28,11 @@ Creating a touch-capable UI is [easy](https://docs.lvgl.io/8.3/get-started/quick
```c
static void app_show(TT_UNUSED App app, lv_obj_t* parent) {
// Default toolbar with app name and close button
lv_obj_t* toolbar = tt_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Label widget
lv_obj_t* label = lv_label_create(parent);
lv_label_set_text(label, "Hello, world!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

View File

@ -1,7 +1,11 @@
#include "hello_world.h"
#include "lvgl.h"
#include "ui/toolbar.h"
static void app_show(TT_UNUSED App app, lv_obj_t* parent) {
lv_obj_t* toolbar = tt_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent);
lv_label_set_text(label, "Hello, world!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

View File

@ -1,8 +1,11 @@
#include "hello_world.h"
#include "services/gui/gui.h"
#include "services/loader/loader.h"
#include "ui/toolbar.h"
static void app_show(TT_UNUSED App app, lv_obj_t* parent) {
lv_obj_t* toolbar = tt_toolbar_create_for_app(parent, app);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_t* label = lv_label_create(parent);
lv_label_set_text(label, "Hello, world!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

View File

@ -243,14 +243,14 @@
*-----------*/
/*1: Show CPU usage and FPS count*/
#define LV_USE_PERF_MONITOR 1
#define LV_USE_PERF_MONITOR 0
#if LV_USE_PERF_MONITOR
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
#endif
/*1: Show the used memory and the memory fragmentation
* Requires LV_MEM_CUSTOM = 0*/
#define LV_USE_MEM_MONITOR 1
#define LV_USE_MEM_MONITOR 0
#if LV_USE_MEM_MONITOR
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
#endif

View File

@ -12,6 +12,10 @@
- Support for displays with different DPI. Consider the layer-based system like on Android.
- Display orientation support for Display app
- If present, use LED to show boot status
- 2 wire speaker support
- tt_app_start() and similar functions as proxies for Loader app start/stop/etc.
- tt_app_set_result() for apps that need to return data to other apps (e.g. file selection)
- Make a statusbar service that apps can register icons to. Gui can observe its status changes?
# App Improvement Ideas
- Sort desktop apps by name.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,26 @@
#include "string_utils.h"
#include <string.h>
int tt_string_find_last_index(const char* text, size_t from_index, char find) {
for (size_t i = from_index; i >= 0; i--) {
if (text[i] == find) {
return (int)i;
}
}
return -1;
}
bool tt_string_get_path_parent(const char* path, char* output) {
int index = tt_string_find_last_index(path, strlen(path) - 1, '/');
if (index == -1) {
return false;
} else if (index == 0) {
output[0] = '/';
output[1] = 0x00;
return true;
} else {
memcpy(output, path, index);
output[index] = 0x00;
return true;
}
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <stdbool.h>
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Find the last occurrence of a character.
* @param[in] text the text to search in
* @param[in] from_index the index to search from (searching from right to left)
* @param[in] find the character to search for
* @return the index of the found character, or -1 if none found
*/
int tt_string_find_last_index(const char* text, size_t from_index, char find);
/**
* Given a filesystem path as input, try and get the parent path.
* @param[in] path input path
* @param[out] output an output buffer that is allocated to at least the size of "current"
* @return true when successful
*/
bool tt_string_get_path_parent(const char* path, char* output);
#ifdef __cplusplus
}
#endif

View File

@ -6,6 +6,7 @@
#include "services/wifi/wifi_credentials.h"
#include "ui/spacer.h"
#include "ui/style.h"
#include "ui/toolbar.h"
#include "wifi_connect.h"
#include "wifi_connect_bundle.h"
#include "wifi_connect_state.h"
@ -69,26 +70,29 @@ void wifi_connect_view_create(App app, void* wifi, lv_obj_t* parent) {
WifiConnectView* view = &wifi_connect->view;
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
tt_lv_obj_set_style_auto_padding(parent);
tt_toolbar_create_for_app(parent, app);
view->root = parent;
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_t* ssid_label = lv_label_create(parent);
lv_obj_t* ssid_label = lv_label_create(wrapper);
lv_label_set_text(ssid_label, "Network:");
view->ssid_textarea = lv_textarea_create(parent);
view->ssid_textarea = lv_textarea_create(wrapper);
lv_textarea_set_one_line(view->ssid_textarea, true);
tt_lv_spacer_create(parent, 1, 8);
tt_lv_spacer_create(wrapper, 1, 8);
lv_obj_t* password_label = lv_label_create(parent);
lv_obj_t* password_label = lv_label_create(wrapper);
lv_label_set_text(password_label, "Password:");
view->password_textarea = lv_textarea_create(parent);
view->password_textarea = lv_textarea_create(wrapper);
lv_textarea_set_one_line(view->password_textarea, true);
lv_textarea_set_password_mode(view->password_textarea, true);
tt_lv_spacer_create(parent, 1, 8);
tt_lv_spacer_create(wrapper, 1, 8);
wifi_connect_view_create_bottom_buttons(wifi, parent);
wifi_connect_view_create_bottom_buttons(wifi, wrapper);
gui_keyboard_add_textarea(view->ssid_textarea);
gui_keyboard_add_textarea(view->password_textarea);

View File

@ -9,7 +9,6 @@ extern "C" {
#endif
typedef struct {
lv_obj_t* root;
lv_obj_t* ssid_textarea;
lv_obj_t* password_textarea;
lv_obj_t* connect_button;

View File

@ -125,7 +125,7 @@ static void app_show(App app, lv_obj_t* parent) {
wifi_manage_lock(wifi);
wifi->view_enabled = true;
strcpy((char*)wifi->state.connect_ssid, "Connected"); // TODO update with proper SSID
wifi_manage_view_create(&wifi->view, &wifi->bindings, parent);
wifi_manage_view_create(app, &wifi->view, &wifi->bindings, parent);
wifi_manage_view_update(&wifi->view, &wifi->bindings, &wifi->state);
wifi_manage_unlock(wifi);

View File

@ -3,6 +3,7 @@
#include "log.h"
#include "services/wifi/wifi.h"
#include "ui/style.h"
#include "ui/toolbar.h"
#include "wifi_manage_state.h"
#define TAG "wifi_main_view"
@ -147,14 +148,19 @@ static void update_connected_ap(WifiManageView* view, WifiManageState* state, Wi
// region Main
void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent) {
void wifi_manage_view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent) {
view->root = parent;
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
tt_lv_obj_set_style_auto_padding(parent);
tt_toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
// Top row: enable/disable
lv_obj_t* switch_container = lv_obj_create(parent);
lv_obj_t* switch_container = lv_obj_create(wrapper);
lv_obj_set_width(switch_container, LV_PCT(100));
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
tt_lv_obj_set_style_no_padding(switch_container);
@ -168,7 +174,7 @@ void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings,
lv_obj_add_event_cb(view->enable_switch, on_enable_switch_changed, LV_EVENT_ALL, bindings);
lv_obj_set_align(view->enable_switch, LV_ALIGN_RIGHT_MID);
view->connected_ap_container = lv_obj_create(parent);
view->connected_ap_container = lv_obj_create(wrapper);
lv_obj_set_size(view->connected_ap_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_min_height(view->connected_ap_container, SPINNER_HEIGHT, 0);
tt_lv_obj_set_style_no_padding(view->connected_ap_container);
@ -185,7 +191,7 @@ void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings,
// Networks
lv_obj_t* networks_header = lv_obj_create(parent);
lv_obj_t* networks_header = lv_obj_create(wrapper);
lv_obj_set_size(networks_header, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_min_height(networks_header, SPINNER_HEIGHT, 0);
tt_lv_obj_set_style_no_padding(networks_header);
@ -201,7 +207,7 @@ void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings,
lv_obj_set_style_pad_bottom(view->scanning_spinner, 4, 0);
lv_obj_align_to(view->scanning_spinner, view->networks_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
view->networks_list = lv_obj_create(parent);
view->networks_list = lv_obj_create(wrapper);
lv_obj_set_flex_flow(view->networks_list, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(view->networks_list, LV_PCT(100));
lv_obj_set_height(view->networks_list, LV_SIZE_CONTENT);

View File

@ -1,5 +1,6 @@
#pragma once
#include "app.h"
#include "lvgl.h"
#include "wifi_manage_bindings.h"
#include "wifi_manage_state.h"
@ -18,7 +19,7 @@ typedef struct {
lv_obj_t* connected_ap_label;
} WifiManageView;
void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent);
void wifi_manage_view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent);
void wifi_manage_view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state);
#ifdef __cplusplus

View File

@ -24,6 +24,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
PUBLIC idf::driver
PUBLIC idf::spiffs
PUBLIC idf::nvs_flash
PUBLIC idf::newlib # for scandir() and related
)
else()
add_definitions(-D_Nullable=)

View File

@ -4,6 +4,10 @@
static AppFlags tt_app_get_flags_default(AppType type);
static const AppFlags DEFAULT_FLAGS = {
.show_statusbar = true
};
// region Alloc/free
App tt_app_alloc(const AppManifest* manifest, Bundle* _Nullable parameters) {
@ -41,19 +45,7 @@ static void tt_app_unlock(AppData* data) {
}
static AppFlags tt_app_get_flags_default(AppType type) {
static const AppFlags DEFAULT_DESKTOP_FLAGS = {
.show_toolbar = false,
.show_statusbar = true
};
static const AppFlags DEFAULT_APP_FLAGS = {
.show_toolbar = true,
.show_statusbar = true
};
return type == AppTypeDesktop
? DEFAULT_DESKTOP_FLAGS
: DEFAULT_APP_FLAGS;
return DEFAULT_FLAGS;
}
// endregion Internal

View File

@ -18,7 +18,6 @@ typedef enum {
typedef union {
struct {
bool show_statusbar : 1;
bool show_toolbar : 1;
};
unsigned char flags;
} AppFlags;

View File

@ -14,7 +14,8 @@ static void on_app_pressed(lv_event_t* e) {
static void create_app_widget(const AppManifest* manifest, void* parent) {
tt_check(parent);
lv_obj_t* list = (lv_obj_t*)parent;
lv_obj_t* btn = lv_list_add_btn(list, LV_SYMBOL_FILE, manifest->name);
const char* icon = manifest->icon ?: LV_SYMBOL_FILE;
lv_obj_t* btn = lv_list_add_btn(list, icon, manifest->name);
lv_obj_add_event_cb(btn, &on_app_pressed, LV_EVENT_CLICKED, (void*)manifest);
}

View File

@ -3,7 +3,7 @@
#include "preferences.h"
#include "tactility.h"
#include "ui/spacer.h"
#include "ui/style.h"
#include "ui/toolbar.h"
static bool backlight_duty_set = false;
static uint8_t backlight_duty = 255;
@ -25,19 +25,21 @@ static void slider_event_cb(lv_event_t* e) {
static void app_show(TT_UNUSED App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
tt_lv_obj_set_style_auto_padding(parent);
lv_obj_t* label = lv_label_create(parent);
tt_toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_t* label = lv_label_create(wrapper);
lv_label_set_text(label, "Brightness");
tt_lv_spacer_create(parent, 1, 2);
tt_lv_spacer_create(wrapper, 1, 2);
lv_obj_t* slider_container = lv_obj_create(parent);
lv_obj_set_size(slider_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_t* slider = lv_slider_create(slider_container);
lv_obj_set_width(slider, LV_PCT(90));
lv_obj_center(slider);
lv_obj_t* slider = lv_slider_create(wrapper);
lv_obj_set_width(slider, LV_PCT(100));
lv_slider_set_range(slider, 0, 255);
lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL);

View File

@ -2,6 +2,7 @@
#include "check.h"
#include "lvgl.h"
#include "services/loader/loader.h"
#include "ui/toolbar.h"
static void on_app_pressed(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
@ -19,9 +20,13 @@ static void create_app_widget(const AppManifest* manifest, void* parent) {
}
static void on_show(TT_UNUSED App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
tt_toolbar_create_for_app(parent, app);
lv_obj_t* list = lv_list_create(parent);
lv_obj_set_size(list, LV_PCT(100), LV_PCT(100));
lv_obj_center(list);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
tt_app_manifest_registry_for_each_of_type(AppTypeSettings, list, create_app_widget);
}
@ -29,7 +34,7 @@ static void on_show(TT_UNUSED App app, lv_obj_t* parent) {
const AppManifest settings_app = {
.id = "settings",
.name = "Settings",
.icon = NULL,
.icon = LV_SYMBOL_SETTINGS,
.type = AppTypeSystem,
.on_start = NULL,
.on_stop = NULL,

View File

@ -0,0 +1,67 @@
#include "file_utils.h"
#include "tactility_core.h"
#define TAG "file_utils"
#define SCANDIR_LIMIT 128
int tt_dirent_filter_dot_entries(const struct dirent* entry) {
return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0;
}
int tt_dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right) {
bool left_is_dir = (*left)->d_type == TT_DT_DIR;
bool right_is_dir = (*right)->d_type == TT_DT_DIR;
if (left_is_dir == right_is_dir) {
return strcmp((*left)->d_name, (*right)->d_name);
} else {
return (left_is_dir < right_is_dir) ? 1 : -1;
}
}
int tt_dirent_sort_alpha(const struct dirent** left, const struct dirent** right) {
return strcmp((*left)->d_name, (*right)->d_name);
}
int tt_scandir(
const char* path,
struct dirent*** output,
ScandirFilter _Nullable filter,
ScandirSort _Nullable sort
) {
DIR* dir = opendir(path);
if (dir == NULL) {
return -1;
}
*output = malloc(sizeof(void*) * SCANDIR_LIMIT);
struct dirent** dirent_array = *output;
int dirent_buffer_index = 0;
struct dirent* current_entry;
while ((current_entry = readdir(dir)) != NULL) {
TT_LOG_D(TAG, "debug: %s %d", current_entry->d_name, current_entry->d_type);
if (filter(current_entry) == 0) {
dirent_array[dirent_buffer_index] = malloc(sizeof(struct dirent));
memcpy(dirent_array[dirent_buffer_index], current_entry, sizeof(struct dirent));
dirent_buffer_index++;
if (dirent_buffer_index >= SCANDIR_LIMIT) {
TT_LOG_E(TAG, "directory has more than %d files", SCANDIR_LIMIT);
break;
}
}
}
if (dirent_buffer_index == 0) {
free(*output);
*output = NULL;
} else {
if (sort) {
qsort(dirent_array, dirent_buffer_index, sizeof(struct dirent*), (__compar_fn_t)sort);
}
}
closedir(dir);
return dirent_buffer_index;
};

View File

@ -0,0 +1,68 @@
#pragma once
#include <dirent.h>
/** File types for `dirent`'s `d_type`. */
enum {
TT_DT_UNKNOWN = 0,
#define TT_DT_UNKNOWN TT_DT_UNKNOWN
TT_DT_FIFO = 1,
#define TT_DT_FIFO TT_DT_FIFO
TT_DT_CHR = 2,
#define TT_DT_CHR TT_DT_CHR
TT_DT_DIR = 4,
#define TT_DT_DIR TT_DT_DIR
TT_DT_BLK = 6,
#define TT_DT_BLK TT_DT_BLK
TT_DT_REG = 8,
#define TT_DT_REG TT_DT_REG
TT_DT_LNK = 10,
#define TT_DT_LNK TT_DT_LNK
TT_DT_SOCK = 12,
#define TT_DT_SOCK TT_DT_SOCK
TT_DT_WHT = 14
#define TT_DT_WHT TT_DT_WHT
};
typedef int (*ScandirFilter)(const struct dirent*);
typedef int (*ScandirSort)(const struct dirent**, const struct dirent**);
#ifdef __cplusplus
extern "C" {
#endif
/**
* Alphabetic sorting function for tt_scandir()
* @param left left-hand side part for comparison
* @param right right-hand side part for comparison
* @return 0, -1 or 1
*/
int tt_dirent_sort_alpha(const struct dirent** left, const struct dirent** right);
int tt_dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right);
int tt_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] output a pointer to an array 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 tt_scandir(
const char* path,
struct dirent*** output,
ScandirFilter _Nullable filter,
ScandirSort _Nullable sort
);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,152 @@
#include "files_data.h"
#include "app.h"
#include "check.h"
#include "file_utils.h"
#include "lvgl.h"
#include "services/loader/loader.h"
#include "ui/toolbar.h"
#include <dirent.h>
#define TAG "files_app"
bool tt_string_ends_with(const char* base, const char* postfix) {
size_t postfix_len = strlen(postfix);
size_t base_len = strlen(base);
if (base_len < postfix_len) {
return false;
}
for (int i = (int)postfix_len - 1; i >= 0; i--) {
if (tolower(base[base_len - postfix_len + i]) != postfix[i]) {
return false;
}
}
return true;
}
static bool is_image_file(const char* filename) {
return tt_string_ends_with(filename, ".jpg") ||
tt_string_ends_with(filename, ".png") ||
tt_string_ends_with(filename, ".jpeg") ||
tt_string_ends_with(filename, ".svg") ||
tt_string_ends_with(filename, ".bmp");
}
// region Views
static void update_views(FilesData* data);
static void on_navigate_up_pressed(TT_UNUSED lv_event_t* event) {
FilesData* files_data = (FilesData*)event->user_data;
if (strcmp(files_data->current_path, "/") != 0) {
files_data_set_entries_navigate_up(files_data);
}
update_views(files_data);
}
static void on_exit_app_pressed(TT_UNUSED lv_event_t* event) {
loader_stop_app();
}
static void on_file_pressed(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lv_obj_t* button = e->current_target;
FilesData* files_data = lv_obj_get_user_data(button);
struct dirent* dir_entry = e->user_data;
TT_LOG_I(TAG, "clicked %s %d", dir_entry->d_name, dir_entry->d_type);
switch (dir_entry->d_type) {
case TT_DT_DIR:
files_data_set_entries_for_path(files_data, dir_entry->d_name);
update_views(files_data);
break;
case TT_DT_LNK:
TT_LOG_W(TAG, "opening links is not supported");
break;
case TT_DT_REG:
TT_LOG_W(TAG, "opening files is not supported");
break;
default:
TT_LOG_W(TAG, "file type %d is not supported", dir_entry->d_type);
break;
}
}
}
static void create_file_widget(FilesData* files_data, lv_obj_t* parent, struct dirent* dir_entry) {
tt_check(parent);
lv_obj_t* list = (lv_obj_t*)parent;
const char* symbol;
switch (dir_entry->d_type) {
case TT_DT_DIR:
symbol = LV_SYMBOL_DIRECTORY;
break;
case TT_DT_REG:
symbol = is_image_file(dir_entry->d_name) ? LV_SYMBOL_IMAGE : LV_SYMBOL_FILE;
break;
case TT_DT_LNK:
symbol = LV_SYMBOL_LOOP;
break;
default:
symbol = LV_SYMBOL_SETTINGS;
break;
}
lv_obj_t* button = lv_list_add_btn(list, symbol, dir_entry->d_name);
lv_obj_set_user_data(button, files_data);
lv_obj_add_event_cb(button, &on_file_pressed, LV_EVENT_CLICKED, (void*)dir_entry);
}
static void update_views(FilesData* data) {
lv_obj_clean(data->list);
for (int i = 0; i < data->dir_entries_count; ++i) {
create_file_widget(data, data->list, data->dir_entries[i]);
}
}
// endregion Views
// region Lifecycle
static void on_show(App app, lv_obj_t* parent) {
FilesData* data = tt_app_get_data(app);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = tt_toolbar_create(parent, "Files");
tt_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, &on_exit_app_pressed, NULL);
tt_toolbar_add_action(toolbar, LV_SYMBOL_UP, "Navigate up", &on_navigate_up_pressed, data);
data->list = lv_list_create(parent);
lv_obj_set_width(data->list, LV_PCT(100));
lv_obj_set_flex_grow(data->list, 1);
update_views(data);
}
static void on_start(App app) {
FilesData* data = files_data_alloc();
files_data_set_entries_root(data);
tt_app_set_data(app, data);
}
static void on_stop(App app) {
FilesData* data = tt_app_get_data(app);
files_data_free(data);
}
// endregion Lifecycle
const AppManifest files_app = {
.id = "files",
.name = "Files",
.icon = NULL,
.type = AppTypeSystem,
.on_start = &on_start,
.on_stop = &on_stop,
.on_show = &on_show,
.on_hide = NULL
};

View File

@ -0,0 +1,118 @@
#include "files_data.h"
#include "file_utils.h"
#include "tactility_core.h"
#include <string_utils.h>
#define TAG "files_app"
FilesData* files_data_alloc() {
FilesData* data = malloc(sizeof(FilesData));
*data = (FilesData) {
.current_path = {'/', 0x00},
.dir_entries = NULL,
.dir_entries_count = 0
};
return data;
}
void files_data_free(FilesData* data) {
files_data_free_entries(data);
free(data);
}
void files_data_free_entries(FilesData* data) {
for (int i = 0; i < data->dir_entries_count; ++i) {
free(data->dir_entries[i]);
}
free(data->dir_entries);
data->dir_entries = NULL;
data->dir_entries_count = 0;
}
void files_data_set_entries(FilesData* data, struct dirent** entries, int count) {
if (data->dir_entries != NULL) {
files_data_free_entries(data);
}
data->dir_entries = entries;
data->dir_entries_count = count;
}
void files_data_set_entries_navigate_up(FilesData* data) {
TT_LOG_I(TAG, "navigating upwards");
char new_absolute_path[MAX_PATH_LENGTH];
if (tt_string_get_path_parent(data->current_path, new_absolute_path)) {
if (strcmp(new_absolute_path, "/") == 0) {
files_data_set_entries_root(data);
} else {
strcpy(data->current_path, new_absolute_path);
data->dir_entries_count = tt_scandir(new_absolute_path, &(data->dir_entries), &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type);
TT_LOG_I(TAG, "%s has %u entries", new_absolute_path, data->dir_entries_count);
}
}
}
void files_data_set_entries_for_path(FilesData* data, const char* path) {
size_t current_path_length = strlen(data->current_path);
size_t added_path_length = strlen(path);
size_t total_path_length = current_path_length + added_path_length + 1; // two paths with `/`
if (total_path_length >= MAX_PATH_LENGTH) {
TT_LOG_E(TAG, "Path limit reached (%d chars)", MAX_PATH_LENGTH);
return;
}
char new_absolute_path[MAX_PATH_LENGTH];
memcpy(new_absolute_path, data->current_path, current_path_length);
// Postfix with "/" when the current path isn't "/"
if (current_path_length != 1) {
new_absolute_path[current_path_length] = '/';
strcpy(&new_absolute_path[current_path_length + 1], path);
} else {
strcpy(&new_absolute_path[current_path_length], path);
}
TT_LOG_I(TAG, "Navigating from %s to %s", data->current_path, new_absolute_path);
struct dirent** entries = NULL;
int count = tt_scandir(new_absolute_path, &entries, &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type);
if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", new_absolute_path, count);
files_data_set_entries(data, entries, count);
strcpy(data->current_path, new_absolute_path);
} else {
TT_LOG_E(TAG, "Failed to fetch entries for %s", new_absolute_path);
}
}
void files_data_set_entries_root(FilesData* data) {
data->current_path[0] = '/';
data->current_path[1] = 0x00;
#ifdef ESP_PLATFORM
int dir_entries_count = 3;
struct dirent** dir_entries = malloc(sizeof(struct dirent*) * 3);
dir_entries[0] = malloc(sizeof(struct dirent));
dir_entries[0]->d_type = 4;
strcpy(dir_entries[0]->d_name, "assets");
dir_entries[1] = malloc(sizeof(struct dirent));
dir_entries[1]->d_type = 4;
strcpy(dir_entries[1]->d_name, "config");
dir_entries[2] = malloc(sizeof(struct dirent));
dir_entries[2]->d_type = 4;
strcpy(dir_entries[2]->d_name, "sdcard");
files_data_set_entries(data, dir_entries, dir_entries_count);
TT_LOG_I(TAG, "test: %s", dir_entries[0]->d_name);
#else
struct dirent** dir_entries = NULL;
int count = tt_scandir(data->current_path, &dir_entries, &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type);
if (count >= 0) {
files_data_set_entries(data, dir_entries, count);
} else {
TT_LOG_E(TAG, "Failed to fetch root dir items");
}
#endif
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <dirent.h>
#include "lvgl.h"
#define MAX_PATH_LENGTH 256
typedef struct {
char current_path[MAX_PATH_LENGTH];
struct dirent** dir_entries;
int dir_entries_count;
lv_obj_t* list;
} FilesData;
#ifdef __cplusplus
extern "C" {
#endif
FilesData* files_data_alloc();
void files_data_free(FilesData* data);
void files_data_free_entries(FilesData* data);
void files_data_set_entries(FilesData* data, struct dirent** entries, int count);
void files_data_set_entries_for_path(FilesData* data, const char* path);
void files_data_set_entries_navigate_up(FilesData* data);
void files_data_set_entries_root(FilesData* data);
#ifdef __cplusplus
}
#endif

View File

@ -1,8 +1,17 @@
#include "app.h"
#include "lvgl.h"
#include "ui/toolbar.h"
static void app_show(TT_UNUSED App app, lv_obj_t* parent) {
lv_obj_t* heap_info = lv_label_create(parent);
static void app_show(App app, lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
tt_toolbar_create_for_app(parent, app);
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_t* heap_info = lv_label_create(wrapper);
lv_label_set_recolor(heap_info, true);
lv_obj_set_width(heap_info, 200);
lv_obj_set_style_text_align(heap_info, LV_TEXT_ALIGN_CENTER, 0);
@ -18,7 +27,7 @@ static void app_show(TT_UNUSED App app, lv_obj_t* parent) {
#endif
lv_obj_align(heap_info, LV_ALIGN_CENTER, 0, -20);
lv_obj_t* spi_info = lv_label_create(parent);
lv_obj_t* spi_info = lv_label_create(wrapper);
lv_label_set_recolor(spi_info, true);
lv_obj_set_width(spi_info, 200);
lv_obj_set_style_text_align(spi_info, LV_TEXT_ALIGN_CENTER, 0);

View File

@ -3,8 +3,6 @@
#include "gui_i.h"
#include "log.h"
#include "services/gui/widgets/statusbar.h"
#include "services/loader/loader.h"
#include "ui/spacer.h"
#include "ui/style.h"
#include "ui/toolbar.h"
@ -25,26 +23,6 @@ static lv_obj_t* create_app_views(Gui* gui, lv_obj_t* parent, App app) {
tt_lv_statusbar_create(vertical_container);
}
gui->toolbar = NULL;
if (flags.show_toolbar) {
const AppManifest* manifest = tt_app_get_manifest(app);
if (manifest != NULL) {
// TODO: Keep toolbar on app level so app can update it (app_set_toolbar() etc?)
Toolbar toolbar = {
.nav_action = &loader_stop_app,
.nav_icon = LV_SYMBOL_CLOSE,
.title = manifest->name
};
lv_obj_t* toolbar_widget = tt_lv_toolbar_create(vertical_container, &toolbar);
lv_obj_set_pos(toolbar_widget, 0, STATUSBAR_HEIGHT);
// Black area between toolbar and content below
lv_obj_t* spacer = tt_lv_spacer_create(vertical_container, 1, 2);
tt_lv_obj_set_style_bg_blacken(spacer);
gui->toolbar = toolbar_widget;
}
}
lv_obj_t* child_container = lv_obj_create(vertical_container);
lv_obj_set_width(child_container, LV_PCT(100));
lv_obj_set_flex_grow(child_container, 1);

View File

@ -25,7 +25,6 @@ struct Gui {
// App-specific
ViewPort* app_view_port;
lv_obj_t* _Nullable toolbar;
lv_obj_t* _Nullable keyboard;
lv_group_t* keyboard_group;
};

View File

@ -25,10 +25,6 @@ void gui_keyboard_show(lv_obj_t* textarea) {
if (gui->keyboard) {
lv_obj_clear_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
lv_keyboard_set_textarea(gui->keyboard, textarea);
if (gui->toolbar) {
lv_obj_add_flag(gui->toolbar, LV_OBJ_FLAG_HIDDEN);
}
}
gui_unlock();
@ -39,9 +35,6 @@ void gui_keyboard_hide() {
if (gui->keyboard) {
lv_obj_add_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN);
if (gui->toolbar) {
lv_obj_clear_flag(gui->toolbar, LV_OBJ_FLAG_HIDDEN);
}
}
gui_unlock();

View File

@ -26,12 +26,14 @@ static const ServiceManifest* const system_services[] = {
extern const AppManifest desktop_app;
extern const AppManifest display_app;
extern const AppManifest files_app;
extern const AppManifest settings_app;
extern const AppManifest system_info_app;
static const AppManifest* const system_apps[] = {
&desktop_app,
&display_app,
&files_app,
&settings_app,
&system_info_app
};

View File

@ -6,49 +6,105 @@
#include "lvgl.h"
#define TOOLBAR_HEIGHT 40
#define TOOLBAR_FONT_HEIGHT 18
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
static void on_nav_pressed(lv_event_t* event) {
NavAction action = (NavAction)event->user_data;
action();
static const lv_obj_class_t toolbar_class = {
.constructor_cb = &toolbar_constructor,
.destructor_cb = NULL,
.width_def = LV_PCT(100),
.height_def = TOOLBAR_HEIGHT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
.instance_size = sizeof(Toolbar),
.base_class = &lv_obj_class
};
static void stop_app(TT_UNUSED lv_event_t* event) {
loader_stop_app();
}
lv_obj_t* tt_lv_toolbar_create(lv_obj_t* parent, const Toolbar* toolbar) {
lv_obj_t* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_height(wrapper, TOOLBAR_HEIGHT);
tt_lv_obj_set_style_no_padding(wrapper);
lv_obj_center(wrapper);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(obj, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
LV_TRACE_OBJ_CREATE("finished");
}
lv_coord_t title_offset_x = (TOOLBAR_HEIGHT - TOOLBAR_FONT_HEIGHT - 8) / 4 * 3;
lv_coord_t title_offset_y = (TOOLBAR_HEIGHT - TOOLBAR_FONT_HEIGHT - 8) / 2;
lv_obj_t* tt_toolbar_create(lv_obj_t* parent, const char* title) {
LV_LOG_INFO("begin");
lv_obj_t* obj = lv_obj_class_create_obj(&toolbar_class, parent);
lv_obj_class_init_obj(obj);
lv_obj_t* close_button = lv_btn_create(wrapper);
lv_obj_set_size(close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
tt_lv_obj_set_style_no_padding(close_button);
lv_obj_add_event_cb(close_button, &on_nav_pressed, LV_EVENT_CLICKED, toolbar->nav_action);
lv_obj_t* close_button_image = lv_img_create(close_button);
lv_img_set_src(close_button_image, toolbar->nav_icon); // e.g. LV_SYMBOL_CLOSE
lv_obj_align(close_button_image, LV_ALIGN_CENTER, 0, 0);
Toolbar* toolbar = (Toolbar*)obj;
tt_lv_obj_set_style_no_padding(obj);
lv_obj_center(obj);
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
lv_coord_t title_offset_x = (TOOLBAR_HEIGHT - TOOLBAR_TITLE_FONT_HEIGHT - 8) / 4 * 3;
lv_coord_t title_offset_y = (TOOLBAR_HEIGHT - TOOLBAR_TITLE_FONT_HEIGHT - 8) / 2;
toolbar->close_button = lv_btn_create(obj);
lv_obj_set_size(toolbar->close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
tt_lv_obj_set_style_no_padding(toolbar->close_button);
toolbar->close_button_image = lv_img_create(toolbar->close_button);
lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0);
// Need spacer to avoid button press glitch animation
tt_lv_spacer_create(wrapper, title_offset_x, 1);
tt_lv_spacer_create(obj, title_offset_x, 1);
lv_obj_t* label_container = lv_obj_create(wrapper);
lv_obj_t* label_container = lv_obj_create(obj);
tt_lv_obj_set_style_no_padding(label_container);
lv_obj_set_style_border_width(label_container, 0, 0);
lv_obj_set_height(label_container, LV_PCT(100)); // 2% less due to 4px translate (it's not great, but it works)
lv_obj_set_flex_grow(label_container, 1);
lv_obj_t* title_label = lv_label_create(label_container);
lv_label_set_text(title_label, toolbar->title);
lv_obj_set_style_text_font(title_label, &lv_font_montserrat_18, 0); // TODO replace with size 18
lv_obj_set_height(title_label, TOOLBAR_FONT_HEIGHT);
toolbar->title_label = lv_label_create(label_container);
lv_obj_set_style_text_font(toolbar->title_label, &lv_font_montserrat_18, 0); // TODO replace with size 18
lv_obj_set_height(toolbar->title_label, TOOLBAR_TITLE_FONT_HEIGHT);
lv_label_set_text(toolbar->title_label, title);
lv_obj_set_pos(toolbar->title_label, 0, title_offset_y);
lv_obj_set_style_text_align(toolbar->title_label, LV_TEXT_ALIGN_LEFT, 0);
lv_obj_set_pos(title_label, 0, title_offset_y);
lv_obj_set_style_text_align(title_label, LV_TEXT_ALIGN_LEFT, 0);
toolbar->action_container = lv_obj_create(obj);
lv_obj_set_width(toolbar->action_container, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(toolbar->action_container, 0, 0);
lv_obj_set_style_border_width(toolbar->action_container, 0, 0);
return wrapper;
return obj;
}
lv_obj_t* tt_toolbar_create_for_app(lv_obj_t* parent, App app) {
const AppManifest* manifest = tt_app_get_manifest(app);
lv_obj_t* toolbar = tt_toolbar_create(parent, manifest->name);
tt_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, &stop_app, NULL);
return toolbar;
}
void tt_toolbar_set_title(lv_obj_t* obj, const char* title) {
Toolbar* toolbar = (Toolbar*)obj;
lv_label_set_text(toolbar->title_label, title);
}
void tt_toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) {
Toolbar* toolbar = (Toolbar*)obj;
lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_CLICKED, user_data);
lv_img_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE
}
uint8_t tt_toolbar_add_action(lv_obj_t* obj, const char* icon, const char* text, lv_event_cb_t callback, void* user_data) {
Toolbar* toolbar = (Toolbar*)obj;
uint8_t id = toolbar->action_count;
tt_check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
lv_obj_t* action_button = lv_btn_create(toolbar->action_container);
lv_obj_set_size(action_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4);
tt_lv_obj_set_style_no_padding(action_button);
lv_obj_add_event_cb(action_button, callback, LV_EVENT_CLICKED, user_data);
lv_obj_t* action_button_image = lv_img_create(action_button);
lv_img_set_src(action_button_image, icon);
lv_obj_align(action_button_image, LV_ALIGN_CENTER, 0, 0);
return id;
}

View File

@ -1,20 +1,40 @@
#pragma once
#include "lvgl.h"
#include "app.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef void(*NavAction)();
#define TOOLBAR_HEIGHT 40
#define TOOLBAR_ACTION_LIMIT 8
#define TOOLBAR_TITLE_FONT_HEIGHT 18
typedef void(*ToolbarActionCallback)(void* _Nullable context);
typedef struct {
const char* _Nullable title;
const char* _Nullable nav_icon; // LVGL compatible definition (e.g. local file or embedded icon from LVGL)
NavAction nav_action;
const char* icon;
const char* text;
ToolbarActionCallback callback;
void* _Nullable callback_context;
} ToolbarAction;
typedef struct {
lv_obj_t obj;
lv_obj_t* title_label;
lv_obj_t* close_button;
lv_obj_t* close_button_image;
lv_obj_t* action_container;
ToolbarAction* action_array[TOOLBAR_ACTION_LIMIT];
uint8_t action_count;
} Toolbar;
lv_obj_t* tt_lv_toolbar_create(lv_obj_t* parent, const Toolbar* toolbar);
lv_obj_t* tt_toolbar_create(lv_obj_t* parent, const char* title);
lv_obj_t* tt_toolbar_create_for_app(lv_obj_t* parent, App app);
void tt_toolbar_set_title(lv_obj_t* obj, const char* title);
void tt_toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data);
uint8_t tt_toolbar_add_action(lv_obj_t* obj, const char* icon, const char* text, lv_event_cb_t callback, void* user_data);
#ifdef __cplusplus
}