mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Compare commits
2 Commits
13d77bc547
...
d64eac64f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d64eac64f0 | ||
|
|
7413888cdf |
@ -21,25 +21,38 @@ check e668
|
||||
circle ef4a
|
||||
close e5cd
|
||||
cloud f15c
|
||||
content_copy e14d
|
||||
content_cut e14e
|
||||
content_paste e14f
|
||||
create_new_folder e2cc
|
||||
delete e92e
|
||||
deployed_code f720
|
||||
description e873
|
||||
devices e326
|
||||
display_settings eb97
|
||||
download f090
|
||||
draft e66d
|
||||
edit f097
|
||||
edit_note e745
|
||||
eject e8fb
|
||||
electric_bolt ec1c
|
||||
folder e2c7
|
||||
folder_open e2c8
|
||||
forum e8af
|
||||
gamepad e30f
|
||||
hard_disk f3da
|
||||
help e8fd
|
||||
hub e9f4
|
||||
image e3f4
|
||||
info e88e
|
||||
keyboard_alt f028
|
||||
keyboard_arrow_up e316
|
||||
language ea07
|
||||
lightbulb e90f
|
||||
lightstrip fff75
|
||||
link e250
|
||||
lists e9b9
|
||||
location_off e0c7
|
||||
location_on f1db
|
||||
logo_dev ead6
|
||||
mail e159
|
||||
@ -54,11 +67,16 @@ network_wifi_2_bar ebd6
|
||||
network_wifi_3_bar ebe1
|
||||
note_add e89c
|
||||
palette e40a
|
||||
phone_android f2db
|
||||
play_arrow e037
|
||||
power_settings_new f8c7
|
||||
refresh e5d5
|
||||
save e161
|
||||
save_as eb60
|
||||
sd_card e623
|
||||
sd_card_alert f057
|
||||
search ef7a
|
||||
select_window_2 f4c8
|
||||
settings e8b8
|
||||
signal_wifi_0_bar f0b0
|
||||
signal_wifi_4_bar f065
|
||||
|
||||
Binary file not shown.
@ -48,8 +48,8 @@ void tab5_create_devices_v2(Device* i2c0) {
|
||||
.ldo_voltage_mv = 2500,
|
||||
.dsi_bus_id = 0,
|
||||
.num_data_lanes = 2,
|
||||
.lane_bit_rate_mbps = 965, // ST7123 lane bitrate per M5Stack BSP
|
||||
.dpi_clock_freq_mhz = 70,
|
||||
.lane_bit_rate_mbps = 1040, // Deviates from M5Stack BSP to fix flickering
|
||||
.dpi_clock_freq_mhz = 80,
|
||||
.hsync_pulse_width = 2,
|
||||
.hsync_back_porch = 40,
|
||||
.hsync_front_porch = 40,
|
||||
|
||||
@ -49,8 +49,8 @@ void tab5_create_devices_v3(Device* i2c0) {
|
||||
.ldo_voltage_mv = 2500,
|
||||
.dsi_bus_id = 0,
|
||||
.num_data_lanes = 2,
|
||||
.lane_bit_rate_mbps = 965, // ST7121 lane bitrate per M5Stack BSP (same as ST7123)
|
||||
.dpi_clock_freq_mhz = 70,
|
||||
.lane_bit_rate_mbps = 1040, // Deviates from M5Stack BSP to fix flickering
|
||||
.dpi_clock_freq_mhz = 80,
|
||||
.hsync_pulse_width = 2,
|
||||
.hsync_back_porch = 40,
|
||||
.hsync_front_porch = 40,
|
||||
|
||||
@ -182,6 +182,22 @@ shared_symbol_code_point_names = [
|
||||
"camera",
|
||||
"lightstrip",
|
||||
"phone_android", # Display settings: orientation
|
||||
# Files app
|
||||
"draft",
|
||||
"description",
|
||||
"link",
|
||||
"sd_card",
|
||||
"content_copy",
|
||||
"content_cut",
|
||||
"content_paste",
|
||||
"edit",
|
||||
"play_arrow",
|
||||
"eject",
|
||||
"create_new_folder",
|
||||
# Notes app
|
||||
"save",
|
||||
"save_as",
|
||||
"folder_open",
|
||||
]
|
||||
|
||||
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
|
||||
|
||||
@ -9,7 +9,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/** Changes whenever the icon TTF subset, its codepoints file or the icon name lists change */
|
||||
#define LVGL_ICON_FONT_VERSION 0xC28E1A92u
|
||||
#define LVGL_ICON_FONT_VERSION 0xD5578751u
|
||||
|
||||
// Icon names per icon font, resolved through the .codepoints file next to the TTF
|
||||
extern const char* const lvgl_icon_shared_names[];
|
||||
|
||||
@ -55,3 +55,17 @@
|
||||
#define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF"
|
||||
#define LVGL_ICON_SHARED_LIGHTSTRIP "\xF3\xBF\xBD\xB5"
|
||||
#define LVGL_ICON_SHARED_PHONE_ANDROID "\xEF\x8B\x9B"
|
||||
#define LVGL_ICON_SHARED_DRAFT "\xEE\x99\xAD"
|
||||
#define LVGL_ICON_SHARED_DESCRIPTION "\xEE\xA1\xB3"
|
||||
#define LVGL_ICON_SHARED_LINK "\xEE\x89\x90"
|
||||
#define LVGL_ICON_SHARED_SD_CARD "\xEE\x98\xA3"
|
||||
#define LVGL_ICON_SHARED_CONTENT_COPY "\xEE\x85\x8D"
|
||||
#define LVGL_ICON_SHARED_CONTENT_CUT "\xEE\x85\x8E"
|
||||
#define LVGL_ICON_SHARED_CONTENT_PASTE "\xEE\x85\x8F"
|
||||
#define LVGL_ICON_SHARED_EDIT "\xEF\x82\x97"
|
||||
#define LVGL_ICON_SHARED_PLAY_ARROW "\xEE\x80\xB7"
|
||||
#define LVGL_ICON_SHARED_EJECT "\xEE\xA3\xBB"
|
||||
#define LVGL_ICON_SHARED_CREATE_NEW_FOLDER "\xEE\x8B\x8C"
|
||||
#define LVGL_ICON_SHARED_SAVE "\xEE\x85\xA1"
|
||||
#define LVGL_ICON_SHARED_SAVE_AS "\xEE\xAD\xA0"
|
||||
#define LVGL_ICON_SHARED_FOLDER_OPEN "\xEE\x8B\x88"
|
||||
|
||||
@ -57,6 +57,20 @@ const char* const lvgl_icon_shared_names[] = {
|
||||
"camera",
|
||||
"lightstrip",
|
||||
"phone_android",
|
||||
"draft",
|
||||
"description",
|
||||
"link",
|
||||
"sd_card",
|
||||
"content_copy",
|
||||
"content_cut",
|
||||
"content_paste",
|
||||
"edit",
|
||||
"play_arrow",
|
||||
"eject",
|
||||
"create_new_folder",
|
||||
"save",
|
||||
"save_as",
|
||||
"folder_open",
|
||||
};
|
||||
|
||||
const size_t lvgl_icon_shared_name_count = sizeof(lvgl_icon_shared_names) / sizeof(lvgl_icon_shared_names[0]);
|
||||
|
||||
@ -377,6 +377,7 @@ typedef struct {
|
||||
lv_style_t text_button_pressed;
|
||||
#endif
|
||||
lv_style_t icon_button;
|
||||
lv_style_t icon_button_selected;
|
||||
lv_style_t icon_button_filled;
|
||||
lv_style_t icon_button_filled_pressed;
|
||||
lv_style_t icon_button_tonal;
|
||||
@ -874,6 +875,11 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_radius(&theme->styles.icon_button, BUTTON_SHAPE);
|
||||
lv_style_set_pad_all(&theme->styles.icon_button, BUTTON_PAD_VER);
|
||||
|
||||
style_init_reset(&theme->styles.icon_button_selected);
|
||||
lv_style_set_bg_color(&theme->styles.icon_button_selected, LIST_ITEM_FOCUSED_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.icon_button_selected, LV_OPA_COVER);
|
||||
lv_style_set_text_color(&theme->styles.icon_button_selected, LIST_ITEM_FOCUSED_TEXT_COLOR);
|
||||
|
||||
style_init_reset(&theme->styles.icon_button_filled);
|
||||
lv_style_set_bg_color(&theme->styles.icon_button_filled, ICON_BUTTON_FILLED_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.icon_button_filled, LV_OPA_COVER);
|
||||
@ -1734,6 +1740,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.transition_exit, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.state_pressed, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.transition_enter, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.icon_button_selected, LV_STATE_CHECKED);
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring_inset, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.edit_ring_inset, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||
|
||||
@ -1347,6 +1347,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
/* icon_button */
|
||||
lv_obj_add_style(obj, &theme->styles.icon_button, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_CHECKED);
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring_inset, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.edit_ring_inset, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||
|
||||
@ -690,6 +690,13 @@ icon_button {
|
||||
padAll: @button-pad-ver;
|
||||
}
|
||||
|
||||
// A selected tile, e.g. the file that the Files app shows actions for
|
||||
icon_button_selected {
|
||||
bgColor: @list-item-focused-color;
|
||||
bgOpa: cover;
|
||||
textColor: @list-item-focused-text-color;
|
||||
}
|
||||
|
||||
icon_button_filled {
|
||||
bgColor: @icon-button-filled-color;
|
||||
bgOpa: cover;
|
||||
@ -1348,6 +1355,7 @@ apply(icon_button) {
|
||||
transition_exit: default;
|
||||
state_pressed: state.pressed;
|
||||
transition_enter: state.pressed;
|
||||
icon_button_selected: state.checked;
|
||||
focus_ring_inset: state.focusKey;
|
||||
edit_ring_inset: state.edited;
|
||||
disabled: state.disabled;
|
||||
|
||||
@ -1003,6 +1003,7 @@ apply(toolbar) {
|
||||
apply(icon_button) {
|
||||
icon_button: default;
|
||||
inverted: state.pressed;
|
||||
inverted: state.checked;
|
||||
focus_ring_inset: state.focusKey;
|
||||
edit_ring_inset: state.edited;
|
||||
disabled: state.disabled;
|
||||
|
||||
@ -1,43 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
struct AppGridItem {
|
||||
/** Owned copy, so a tile never references the app ledger directly */
|
||||
::AppManifest manifest;
|
||||
/** Shared icon font glyph */
|
||||
struct TileGridItem {
|
||||
/** Identifies the item across rebuilds, e.g. to keep it selected */
|
||||
std::string id;
|
||||
std::string name;
|
||||
/** Shared icon font glyph, or nullptr to resolve it with Callbacks::resolveIcon when the tile is shown */
|
||||
const char* icon;
|
||||
/** Rendered with an accent colour when not selected */
|
||||
/** Marked with a badge */
|
||||
bool highlighted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Paged, non-scrolling grid of app tiles (icon above name) with "<" and ">" page buttons.
|
||||
* Paged, non-scrolling grid of tiles (icon above name) with "<" and ">" page buttons.
|
||||
* Must outlive the widgets it creates.
|
||||
*/
|
||||
class AppGrid final {
|
||||
class TileGrid final {
|
||||
|
||||
public:
|
||||
|
||||
struct Callbacks {
|
||||
/** Returns the items in display order. Called on every rebuild. */
|
||||
std::vector<AppGridItem> (*collect)(void* userData);
|
||||
void (*onClicked)(const ::AppManifest& manifest, void* userData);
|
||||
std::vector<TileGridItem> (*collect)(void* userData);
|
||||
void (*onClicked)(const TileGridItem& item, void* userData);
|
||||
/** Optional */
|
||||
void (*onLongPressed)(const ::AppManifest& manifest, void* userData);
|
||||
void (*onLongPressed)(const TileGridItem& item, void* userData);
|
||||
/** Optional */
|
||||
void (*onKey)(const ::AppManifest& manifest, uint32_t key, void* userData);
|
||||
void (*onKey)(const TileGridItem& item, uint32_t key, void* userData);
|
||||
/** Optional: returns the icon of an item without one. Only called for the items on the shown page. */
|
||||
const char* (*resolveIcon)(const TileGridItem& item, void* userData);
|
||||
/** Optional: called after the user went to another page */
|
||||
void (*onPageChanged)(void* userData);
|
||||
void* userData;
|
||||
};
|
||||
|
||||
explicit AppGrid(const Callbacks& callbacks) : callbacks(callbacks) {}
|
||||
explicit TileGrid(const Callbacks& callbacks) : callbacks(callbacks) {}
|
||||
|
||||
enum class IconColor {
|
||||
/** The theme's icon colour */
|
||||
@ -57,6 +61,11 @@ public:
|
||||
/** Swiping left or right over the grid goes to the next or previous page. Call before creating the widgets. */
|
||||
void setSwipeNavigation(bool enabled) { swipeNavigation = enabled; }
|
||||
|
||||
/**
|
||||
* Creates the grid and populates it. Call setPageButtons() right after it to add page buttons.
|
||||
* @param[in] parent the container for the grid, with a flex column layout
|
||||
*/
|
||||
void createWidgets(lv_obj_t* parent);
|
||||
|
||||
/**
|
||||
* Creates the grid and populates it, with a bar below it: buttons on the left (see addBarButton()),
|
||||
@ -66,6 +75,12 @@ public:
|
||||
*/
|
||||
void createWidgetsWithBottomBar(lv_obj_t* parent);
|
||||
|
||||
/**
|
||||
* Makes the buttons go to the previous and next page. They are hidden when there's only one page, and wrap around.
|
||||
* Call right after createWidgets().
|
||||
*/
|
||||
void setPageButtons(lv_obj_t* previous, lv_obj_t* next);
|
||||
|
||||
/**
|
||||
* Adds an icon button to the left side of the bottom bar. Call right after createWidgetsWithBottomBar().
|
||||
* @param[in] icon a shared icon (see lvgl/icons/shared.h)
|
||||
@ -75,14 +90,17 @@ public:
|
||||
|
||||
/**
|
||||
* Rebuilds the grid asynchronously, so it is safe to call from a tile's own event.
|
||||
* @param[in] keepSelection when true, the selected app stays selected and its page is shown
|
||||
* @param[in] keepSelection when true, the selected item stays selected and its page is shown
|
||||
*/
|
||||
void requestRepopulate(bool keepSelection);
|
||||
|
||||
/** @return the tile of the item on the shown page, or nullptr */
|
||||
lv_obj_t* findTile(const std::string& id) const;
|
||||
|
||||
private:
|
||||
|
||||
Callbacks callbacks;
|
||||
std::vector<AppGridItem> items;
|
||||
std::vector<TileGridItem> items;
|
||||
uint32_t page = 0;
|
||||
uint32_t pageCount = 1;
|
||||
IconColor iconColor = IconColor::Default;
|
||||
@ -99,6 +117,7 @@ private:
|
||||
lv_obj_t* barSpacer = nullptr;
|
||||
|
||||
void createGrid(lv_obj_t* parent);
|
||||
void finishCreate(lv_obj_t* parent);
|
||||
void goToPreviousPage();
|
||||
void goToNextPage();
|
||||
void populate();
|
||||
@ -61,8 +61,6 @@ public:
|
||||
});
|
||||
}
|
||||
|
||||
bool getDirent(uint32_t index, dirent& dirent);
|
||||
|
||||
void setSelectedChildEntry(const std::string& newFile) {
|
||||
selected_child_entry = newFile;
|
||||
action = ActionNone;
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
#include "./State.h"
|
||||
|
||||
#include <Tactility/app/TileGrid.h>
|
||||
|
||||
#include <app/stream.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
|
||||
@ -19,43 +21,51 @@ class View final {
|
||||
AppStream inputDialogStream {};
|
||||
uint8_t inputDialogBuffer[256] {};
|
||||
|
||||
size_t current_start_index = 0;
|
||||
size_t last_loaded_index = 0;
|
||||
const size_t MAX_BATCH = 50;
|
||||
TileGrid grid;
|
||||
|
||||
lv_obj_t* dir_entry_list = nullptr;
|
||||
lv_obj_t* action_list = nullptr;
|
||||
lv_obj_t* overlay = nullptr;
|
||||
lv_obj_t* navigate_up_button = nullptr;
|
||||
lv_obj_t* new_file_button = nullptr;
|
||||
lv_obj_t* new_folder_button = nullptr;
|
||||
lv_obj_t* paste_button = nullptr;
|
||||
lv_obj_t* overflow_button = nullptr;
|
||||
|
||||
std::string installAppPath = { 0 };
|
||||
uint32_t installDialogId = 0;
|
||||
|
||||
void showActions();
|
||||
void showActionsForDirectory();
|
||||
void showActionsForFile();
|
||||
void showActionsForMountPoint();
|
||||
static std::vector<TileGridItem> collectItems(void* userData);
|
||||
static const char* resolveIcon(const TileGridItem& item, void* userData);
|
||||
static void onTileClicked(const TileGridItem& item, void* userData);
|
||||
static void onTileLongPressed(const TileGridItem& item, void* userData);
|
||||
static void onTileKey(const TileGridItem& item, uint32_t key, void* userData);
|
||||
static void onPageChanged(void* userData);
|
||||
|
||||
lv_obj_t* addOverlayButton(const char* icon, lv_event_cb_t callback);
|
||||
void showOverlay(lv_obj_t* anchorTile);
|
||||
void hideOverlay();
|
||||
void showActionsForDirectory(lv_obj_t* tile);
|
||||
void showActionsForFile(lv_obj_t* tile);
|
||||
void showActionsForMountPoint(lv_obj_t* tile);
|
||||
void addCommonFileActions();
|
||||
|
||||
void viewFile(const std::string&path, const std::string&filename);
|
||||
void runFile(const std::string& file_path);
|
||||
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
|
||||
void onNavigate();
|
||||
|
||||
public:
|
||||
|
||||
View(const std::shared_ptr<State>& state, TaskEventGroup* eventGroup) : state(state), eventGroup(eventGroup) {}
|
||||
View(const std::shared_ptr<State>& state, TaskEventGroup* eventGroup);
|
||||
|
||||
void init(uint32_t appInstanceId, lv_obj_t* parent);
|
||||
void update(size_t start_index = 0);
|
||||
/**
|
||||
* Shows the current entries of the state.
|
||||
* @param[in] firstPage true after navigating to another directory, false to stay on the current page
|
||||
*/
|
||||
void update(bool firstPage);
|
||||
|
||||
void onBackPressed();
|
||||
void onNavigateUpPressed();
|
||||
void onDirEntryPressed(uint32_t index);
|
||||
void onDirEntryLongPressed(int32_t index);
|
||||
void onDirEntryKeyPressed(int32_t index, uint32_t key);
|
||||
void onOverflowPressed();
|
||||
void onDirEntryPressed(const std::string& name);
|
||||
void onDirEntryLongPressed(const std::string& name);
|
||||
void onDirEntryKeyPressed(const std::string& name, uint32_t key);
|
||||
void onRenamePressed();
|
||||
void onDeletePressed();
|
||||
void onNewFilePressed();
|
||||
@ -65,13 +75,12 @@ public:
|
||||
void onPastePressed();
|
||||
void onEjectPressed();
|
||||
void onRunPressed();
|
||||
void onDirEntryListScrollBegin();
|
||||
void onOverlayKey(uint32_t key);
|
||||
void onResult(uint32_t launchId, int32_t result);
|
||||
void deinit();
|
||||
|
||||
private:
|
||||
|
||||
bool resolveDirentFromListIndex(int32_t list_index, dirent& out_entry);
|
||||
bool findDirent(const std::string& name, dirent& out_entry);
|
||||
void doPaste(const std::string& src, bool is_cut, const std::string& dst);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
#include <Tactility/app/TileGrid.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
@ -12,13 +12,13 @@ struct LauncherMode {
|
||||
/** Represents this mode on the button that switches to it */
|
||||
const char* buttonIcon;
|
||||
/** The tile icon colour for themes that aren't monochrome */
|
||||
AppGrid::IconColor iconColor;
|
||||
TileGrid::IconColor iconColor;
|
||||
/** Returns the items in display order */
|
||||
std::vector<AppGridItem> (*collect)();
|
||||
std::vector<TileGridItem> (*collect)();
|
||||
/** Optional */
|
||||
void (*onLongPressed)(AppGrid& grid, const ::AppManifest& manifest);
|
||||
void (*onLongPressed)(TileGrid& grid, const TileGridItem& item);
|
||||
/** Optional */
|
||||
void (*onKey)(AppGrid& grid, const ::AppManifest& manifest, uint32_t key);
|
||||
void (*onKey)(TileGrid& grid, const TileGridItem& item, uint32_t key);
|
||||
};
|
||||
|
||||
extern const LauncherMode APPS_MODE;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
#include <Tactility/app/TileGrid.h>
|
||||
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/fonts.h>
|
||||
@ -51,8 +51,8 @@ PageLayout computePageLayout(lv_obj_t* grid) {
|
||||
};
|
||||
}
|
||||
|
||||
const AppGridItem* getTileItem(lv_event_t* e) {
|
||||
return static_cast<const AppGridItem*>(lv_obj_get_user_data(lv_event_get_target_obj(e)));
|
||||
const TileGridItem* getTileItem(lv_event_t* e) {
|
||||
return static_cast<const TileGridItem*>(lv_obj_get_user_data(lv_event_get_target_obj(e)));
|
||||
}
|
||||
|
||||
|
||||
@ -109,27 +109,27 @@ void onBottomBarDeleted(lv_event_t* event) {
|
||||
|
||||
} // namespace
|
||||
|
||||
void AppGrid::onDeferredNextPage(void* userData) {
|
||||
static_cast<AppGrid*>(userData)->goToNextPage();
|
||||
void TileGrid::onDeferredNextPage(void* userData) {
|
||||
static_cast<TileGrid*>(userData)->goToNextPage();
|
||||
}
|
||||
|
||||
void AppGrid::onDeferredPreviousPage(void* userData) {
|
||||
static_cast<AppGrid*>(userData)->goToPreviousPage();
|
||||
void TileGrid::onDeferredPreviousPage(void* userData) {
|
||||
static_cast<TileGrid*>(userData)->goToPreviousPage();
|
||||
}
|
||||
|
||||
void AppGrid::onDeferredRepopulate(void* userData) {
|
||||
static_cast<AppGrid*>(userData)->populate();
|
||||
void TileGrid::onDeferredRepopulate(void* userData) {
|
||||
static_cast<TileGrid*>(userData)->populate();
|
||||
}
|
||||
|
||||
// A layout pass can resize the grid temporarily (e.g. to its content height, before the parent's flex layout grows it again)
|
||||
void AppGrid::onDeferredResize(void* userData) {
|
||||
auto* self = static_cast<AppGrid*>(userData);
|
||||
void TileGrid::onDeferredResize(void* userData) {
|
||||
auto* self = static_cast<TileGrid*>(userData);
|
||||
if (lv_obj_get_width(self->grid) != self->populatedWidth || lv_obj_get_height(self->grid) != self->populatedHeight) {
|
||||
self->populate();
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::onGridDeleted(lv_event_t* e) {
|
||||
void TileGrid::onGridDeleted(lv_event_t* e) {
|
||||
// Cancels a still-pending repopulate scheduled right before the grid itself was deleted (e.g. the app closing)
|
||||
lv_async_call_cancel(onDeferredRepopulate, lv_event_get_user_data(e));
|
||||
lv_async_call_cancel(onDeferredResize, lv_event_get_user_data(e));
|
||||
@ -138,36 +138,42 @@ void AppGrid::onGridDeleted(lv_event_t* e) {
|
||||
}
|
||||
|
||||
// Deferred: page size depends on the grid size, which is final only after the layout pass that triggered this.
|
||||
void AppGrid::onGridSizeChanged(lv_event_t* e) {
|
||||
void TileGrid::onGridSizeChanged(lv_event_t* e) {
|
||||
// One pending check is enough, also when the size changes several times before it runs
|
||||
lv_async_call_cancel(onDeferredResize, lv_event_get_user_data(e));
|
||||
lv_async_call(onDeferredResize, lv_event_get_user_data(e));
|
||||
}
|
||||
|
||||
// Changing the page doesn't keep the selected app selected: that would return to the selected app's page
|
||||
void AppGrid::goToPreviousPage() {
|
||||
void TileGrid::goToPreviousPage() {
|
||||
page = page > 0 ? page - 1 : pageCount - 1;
|
||||
keepSelectionOnRepopulate = false;
|
||||
populate();
|
||||
if (callbacks.onPageChanged != nullptr) {
|
||||
callbacks.onPageChanged(callbacks.userData);
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::goToNextPage() {
|
||||
void TileGrid::goToNextPage() {
|
||||
page = page + 1 < pageCount ? page + 1 : 0;
|
||||
keepSelectionOnRepopulate = false;
|
||||
populate();
|
||||
if (callbacks.onPageChanged != nullptr) {
|
||||
callbacks.onPageChanged(callbacks.userData);
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::onPrevPressed(lv_event_t* e) {
|
||||
static_cast<AppGrid*>(lv_event_get_user_data(e))->goToPreviousPage();
|
||||
void TileGrid::onPrevPressed(lv_event_t* e) {
|
||||
static_cast<TileGrid*>(lv_event_get_user_data(e))->goToPreviousPage();
|
||||
}
|
||||
|
||||
void AppGrid::onNextPressed(lv_event_t* e) {
|
||||
static_cast<AppGrid*>(lv_event_get_user_data(e))->goToNextPage();
|
||||
void TileGrid::onNextPressed(lv_event_t* e) {
|
||||
static_cast<TileGrid*>(lv_event_get_user_data(e))->goToNextPage();
|
||||
}
|
||||
|
||||
// Swipes that start on a tile reach the grid too, as tiles pass their gestures on to the grid
|
||||
void AppGrid::onGridGesture(lv_event_t* e) {
|
||||
auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
void TileGrid::onGridGesture(lv_event_t* e) {
|
||||
auto* self = static_cast<TileGrid*>(lv_event_get_user_data(e));
|
||||
lv_indev_t* indev = lv_indev_active();
|
||||
if (indev == nullptr || self->pageCount <= 1) {
|
||||
return;
|
||||
@ -182,38 +188,38 @@ void AppGrid::onGridGesture(lv_event_t* e) {
|
||||
lv_async_call(direction == LV_DIR_LEFT ? onDeferredNextPage : onDeferredPreviousPage, self);
|
||||
}
|
||||
|
||||
void AppGrid::onTileClicked(lv_event_t* e) {
|
||||
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
self->callbacks.onClicked(getTileItem(e)->manifest, self->callbacks.userData);
|
||||
void TileGrid::onTileClicked(lv_event_t* e) {
|
||||
const auto* self = static_cast<TileGrid*>(lv_event_get_user_data(e));
|
||||
self->callbacks.onClicked(*getTileItem(e), self->callbacks.userData);
|
||||
}
|
||||
|
||||
void AppGrid::onTileLongPressed(lv_event_t* e) {
|
||||
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
self->callbacks.onLongPressed(getTileItem(e)->manifest, self->callbacks.userData);
|
||||
void TileGrid::onTileLongPressed(lv_event_t* e) {
|
||||
const auto* self = static_cast<TileGrid*>(lv_event_get_user_data(e));
|
||||
self->callbacks.onLongPressed(*getTileItem(e), self->callbacks.userData);
|
||||
}
|
||||
|
||||
void AppGrid::onTileKey(lv_event_t* e) {
|
||||
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
self->callbacks.onKey(getTileItem(e)->manifest, lv_event_get_key(e), self->callbacks.userData);
|
||||
void TileGrid::onTileKey(lv_event_t* e) {
|
||||
const auto* self = static_cast<TileGrid*>(lv_event_get_user_data(e));
|
||||
self->callbacks.onKey(*getTileItem(e), lv_event_get_key(e), self->callbacks.userData);
|
||||
}
|
||||
|
||||
void AppGrid::requestRepopulate(bool keepSelection) {
|
||||
void TileGrid::requestRepopulate(bool keepSelection) {
|
||||
keepSelectionOnRepopulate = keepSelection;
|
||||
lv_async_call(onDeferredRepopulate, this);
|
||||
}
|
||||
|
||||
void AppGrid::populate() {
|
||||
void TileGrid::populate() {
|
||||
// Captured before lv_obj_clean() deletes the currently focused tile below: creating
|
||||
// replacement tiles doesn't restore the focus, so the app id is remembered here and
|
||||
// replacement tiles doesn't restore the focus, so the item id is remembered here and
|
||||
// re-focused on its new tile once rebuilt.
|
||||
std::string focusedAppId;
|
||||
std::string focusedId;
|
||||
const bool keepSelection = keepSelectionOnRepopulate;
|
||||
lv_group_t* group = lv_group_get_default();
|
||||
if (group != nullptr && keepSelection) {
|
||||
// Grid navigation focuses the grid in the group, and a tile within the grid
|
||||
lv_obj_t* focused = lv_group_get_focused(group) == grid ? lvgl_grid_navigation_get_focused(grid) : nullptr;
|
||||
if (focused != nullptr && lv_obj_get_parent(focused) == grid) {
|
||||
focusedAppId = static_cast<const AppGridItem*>(lv_obj_get_user_data(focused))->manifest.id;
|
||||
focusedId = static_cast<const TileGridItem*>(lv_obj_get_user_data(focused))->id;
|
||||
}
|
||||
}
|
||||
keepSelectionOnRepopulate = true;
|
||||
@ -231,10 +237,10 @@ void AppGrid::populate() {
|
||||
|
||||
const uint32_t item_count = items.size();
|
||||
const uint32_t page_count = std::max<uint32_t>(1, (item_count + layout.pageSize - 1) / layout.pageSize);
|
||||
// Follows the focused app onto its new page, e.g. after a reorder moved it.
|
||||
if (!focusedAppId.empty()) {
|
||||
const auto it = std::ranges::find_if(items, [&](const AppGridItem& item) {
|
||||
return focusedAppId == item.manifest.id;
|
||||
// Follows the focused item onto its new page, e.g. after a reorder moved it.
|
||||
if (!focusedId.empty()) {
|
||||
const auto it = std::ranges::find_if(items, [&](const TileGridItem& item) {
|
||||
return focusedId == item.id;
|
||||
});
|
||||
if (it != items.end()) {
|
||||
page = static_cast<uint32_t>(it - items.begin()) / layout.pageSize;
|
||||
@ -243,11 +249,15 @@ void AppGrid::populate() {
|
||||
page = std::min(page, page_count - 1);
|
||||
pageCount = page_count;
|
||||
|
||||
lv_obj_set_hidden(pageIndicator, page_count <= 1);
|
||||
lv_obj_set_hidden(prevButton, page_count <= 1);
|
||||
lv_obj_set_hidden(nextButton, page_count <= 1);
|
||||
lvgl_page_indicator_set_page_count(pageIndicator, page_count);
|
||||
lvgl_page_indicator_set_page(pageIndicator, page);
|
||||
if (pageIndicator != nullptr) {
|
||||
lv_obj_set_hidden(pageIndicator, page_count <= 1);
|
||||
lvgl_page_indicator_set_page_count(pageIndicator, page_count);
|
||||
lvgl_page_indicator_set_page(pageIndicator, page);
|
||||
}
|
||||
if (prevButton != nullptr) {
|
||||
lv_obj_set_hidden(prevButton, page_count <= 1);
|
||||
lv_obj_set_hidden(nextButton, page_count <= 1);
|
||||
}
|
||||
|
||||
lv_obj_t* focusedTile = nullptr;
|
||||
const uint32_t first = page * layout.pageSize;
|
||||
@ -262,13 +272,17 @@ void AppGrid::populate() {
|
||||
lv_obj_set_style_pad_row(tile, layout.pad / 2, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(tile, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(tile, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_user_data(tile, const_cast<AppGridItem*>(&item));
|
||||
lv_obj_set_user_data(tile, const_cast<TileGridItem*>(&item));
|
||||
|
||||
lv_obj_t* icon = lv_label_create(tile);
|
||||
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_large_font(), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_text_align(icon, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT);
|
||||
lv_obj_set_size(icon, layout.iconSize, layout.iconSize);
|
||||
lv_label_set_text(icon, item.icon);
|
||||
const char* icon_text = item.icon;
|
||||
if (icon_text == nullptr && callbacks.resolveIcon != nullptr) {
|
||||
icon_text = callbacks.resolveIcon(item, callbacks.userData);
|
||||
}
|
||||
lv_label_set_text(icon, icon_text != nullptr ? icon_text : "");
|
||||
if (iconColor == IconColor::Primary) {
|
||||
lv_obj_set_style_text_color(icon, lv_theme_get_color_primary(icon), LV_STATE_DEFAULT);
|
||||
} else if (iconColor == IconColor::Secondary) {
|
||||
@ -281,7 +295,7 @@ void AppGrid::populate() {
|
||||
// Dots mode needs a fixed height, otherwise the label wraps instead of truncating
|
||||
lv_obj_set_size(label, LV_PCT(100), lv_font_get_line_height(lvgl_get_text_font(FONT_SIZE_SMALL)));
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_DOTS);
|
||||
lv_label_set_text(label, item.manifest.name);
|
||||
lv_label_set_text(label, item.name.c_str());
|
||||
|
||||
// A badge marks favourites, as it doesn't look like the focus indicator
|
||||
if (item.highlighted) {
|
||||
@ -297,7 +311,7 @@ void AppGrid::populate() {
|
||||
lv_obj_add_event_cb(tile, onTileKey, LV_EVENT_KEY, this);
|
||||
}
|
||||
|
||||
if (!focusedAppId.empty() && focusedAppId == item.manifest.id) {
|
||||
if (!focusedId.empty() && focusedId == item.id) {
|
||||
focusedTile = tile;
|
||||
}
|
||||
}
|
||||
@ -312,7 +326,7 @@ void AppGrid::populate() {
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::createGrid(lv_obj_t* parent) {
|
||||
void TileGrid::createGrid(lv_obj_t* parent) {
|
||||
grid = lv_obj_create(parent);
|
||||
lv_obj_set_width(grid, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(grid, 1);
|
||||
@ -333,8 +347,8 @@ void AppGrid::createGrid(lv_obj_t* parent) {
|
||||
}
|
||||
|
||||
// Keys enter the bar on the previous page button, or on the first button when there's only one page
|
||||
void AppGrid::onBottomBarFocused(lv_event_t* e) {
|
||||
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
void TileGrid::onBottomBarFocused(lv_event_t* e) {
|
||||
const auto* self = static_cast<TileGrid*>(lv_event_get_user_data(e));
|
||||
lv_obj_t* current = lvgl_grid_navigation_get_focused(self->bottomBar);
|
||||
lv_obj_t* target = lv_obj_is_hidden(self->prevButton) ? lv_obj_get_child(self->bottomBar, 0) : self->prevButton;
|
||||
if (current == nullptr || target == current || target == self->barSpacer) {
|
||||
@ -347,7 +361,7 @@ void AppGrid::onBottomBarFocused(lv_event_t* e) {
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
void TileGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
bottomBar = lv_obj_create(parent);
|
||||
lv_obj_set_size(bottomBar, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(bottomBar, 0, LV_STATE_DEFAULT);
|
||||
@ -380,6 +394,21 @@ void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
lvgl_grid_navigation_add(bottomBar);
|
||||
lv_obj_add_event_cb(bottomBar, onBottomBarFocused, LV_EVENT_FOCUSED, this);
|
||||
|
||||
finishCreate(parent);
|
||||
}
|
||||
|
||||
void TileGrid::createWidgets(lv_obj_t* parent) {
|
||||
// Widgets of a previous build are deleted by now
|
||||
prevButton = nullptr;
|
||||
nextButton = nullptr;
|
||||
pageIndicator = nullptr;
|
||||
bottomBar = nullptr;
|
||||
barSpacer = nullptr;
|
||||
createGrid(parent);
|
||||
finishCreate(parent);
|
||||
}
|
||||
|
||||
void TileGrid::finishCreate(lv_obj_t* parent) {
|
||||
// Resolves the grid's flex_grow height, which the page size is derived from.
|
||||
lv_obj_update_layout(parent);
|
||||
lv_obj_add_event_cb(grid, onGridSizeChanged, LV_EVENT_SIZE_CHANGED, this);
|
||||
@ -392,7 +421,27 @@ void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* AppGrid::addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData) {
|
||||
void TileGrid::setPageButtons(lv_obj_t* previous, lv_obj_t* next) {
|
||||
prevButton = previous;
|
||||
nextButton = next;
|
||||
lv_obj_add_event_cb(prevButton, onPrevPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_add_event_cb(nextButton, onNextPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_set_hidden(prevButton, pageCount <= 1);
|
||||
lv_obj_set_hidden(nextButton, pageCount <= 1);
|
||||
}
|
||||
|
||||
lv_obj_t* TileGrid::findTile(const std::string& id) const {
|
||||
const uint32_t count = lv_obj_get_child_count(grid);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
lv_obj_t* tile = lv_obj_get_child(grid, static_cast<int32_t>(i));
|
||||
if (static_cast<const TileGridItem*>(lv_obj_get_user_data(tile))->id == id) {
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
lv_obj_t* TileGrid::addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData) {
|
||||
auto* button = lvgl_icon_button_create(bottomBar);
|
||||
lv_obj_move_to_index(button, lv_obj_get_index(barSpacer));
|
||||
auto* label = lv_label_create(button);
|
||||
@ -64,7 +64,6 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
view.deinit();
|
||||
window_manager_remove(window);
|
||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&event_group);
|
||||
|
||||
@ -58,18 +58,4 @@ bool State::setEntriesForChildPath(const std::string& childPath) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,8 +4,13 @@
|
||||
#include <app/start.h>
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <Tactility/app/files/SupportedFiles.h>
|
||||
@ -22,6 +27,7 @@
|
||||
#include <tactility/drivers/usb_host_msc.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
@ -36,36 +42,11 @@ constexpr auto* TAG = "Files";
|
||||
|
||||
// region Callbacks
|
||||
|
||||
static void dirEntryListScrollBeginCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<files::View*>(lv_event_get_user_data(event));
|
||||
view->onDirEntryListScrollBegin();
|
||||
}
|
||||
|
||||
static void onBackPressedCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<files::View*>(lv_event_get_user_data(event));
|
||||
view->onBackPressed();
|
||||
}
|
||||
|
||||
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 onDirEntryLongPressedCallback(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->onDirEntryLongPressed(index);
|
||||
}
|
||||
|
||||
static void onDirEntryKeyCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
auto* button = lv_event_get_target_obj(event);
|
||||
view->onDirEntryKeyPressed(lv_obj_get_index(button), lv_event_get_key(event));
|
||||
}
|
||||
|
||||
static void onRenamePressedCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
view->onRenamePressed();
|
||||
@ -81,6 +62,15 @@ static void onNavigateUpPressedCallback(lv_event_t* event) {
|
||||
view->onNavigateUpPressed();
|
||||
}
|
||||
|
||||
static void onOverflowPressedCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
view->onOverflowPressed();
|
||||
}
|
||||
|
||||
static void onPageButtonPressedCallback(lv_event_t*) {
|
||||
// TileGrid::setPageButtons() makes the page buttons change the page
|
||||
}
|
||||
|
||||
static void onNewFilePressedCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
view->onNewFilePressed();
|
||||
@ -116,6 +106,11 @@ static void onRunPressedCallback(lv_event_t* event) {
|
||||
view->onRunPressed();
|
||||
}
|
||||
|
||||
static void onOverlayKeyCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
view->onOverlayKey(lv_event_get_key(event));
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region File helpers
|
||||
@ -125,6 +120,33 @@ static bool isExecutablePath(const std::string& path) {
|
||||
return app_is_executable(location);
|
||||
}
|
||||
|
||||
static bool isUsbMountPoint(const char* name) {
|
||||
return strncmp(name, "usb", 3) == 0 && isdigit((unsigned char)name[3]);
|
||||
}
|
||||
|
||||
// Returns nullptr for the files that View::resolveIcon() checks, as that reads the file
|
||||
static const char* getIcon(const dirent& entry, bool isRoot) {
|
||||
if (entry.d_type == file::TT_DT_DIR || entry.d_type == file::TT_DT_CHR) {
|
||||
if (isRoot && strncmp(entry.d_name, "sd", 2) == 0) {
|
||||
return LVGL_ICON_SHARED_SD_CARD;
|
||||
} else if (isRoot && isUsbMountPoint(entry.d_name)) {
|
||||
return LVGL_ICON_SHARED_USB;
|
||||
} else {
|
||||
return LVGL_ICON_SHARED_FOLDER;
|
||||
}
|
||||
} else if (entry.d_type == file::TT_DT_LNK) {
|
||||
return LVGL_ICON_SHARED_LINK;
|
||||
} else if (isSupportedImageFile(entry.d_name)) {
|
||||
return LVGL_ICON_SHARED_IMAGE;
|
||||
} else if (isSupportedTextFile(entry.d_name)) {
|
||||
return LVGL_ICON_SHARED_DESCRIPTION;
|
||||
} else if (isSupportedAppFile(entry.d_name)) {
|
||||
return LVGL_ICON_SHARED_DEPLOYED_CODE;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static bool copyFileContents(const std::string& src, const std::string& dst) {
|
||||
FILE* in = fopen(src.c_str(), "rb");
|
||||
if (in == nullptr) {
|
||||
@ -233,23 +255,75 @@ void View::runFile(const std::string& file_path) {
|
||||
}
|
||||
}
|
||||
|
||||
bool View::resolveDirentFromListIndex(int32_t list_index, dirent& out_entry) {
|
||||
const bool is_root = (state->getCurrentPath() == "/");
|
||||
const bool has_back = (!is_root && current_start_index > 0);
|
||||
View::View(const std::shared_ptr<State>& state, TaskEventGroup* eventGroup) :
|
||||
state(state),
|
||||
eventGroup(eventGroup),
|
||||
grid(TileGrid::Callbacks {
|
||||
.collect = collectItems,
|
||||
.onClicked = onTileClicked,
|
||||
.onLongPressed = onTileLongPressed,
|
||||
.onKey = onTileKey,
|
||||
.resolveIcon = resolveIcon,
|
||||
.onPageChanged = onPageChanged,
|
||||
.userData = this
|
||||
}) {}
|
||||
|
||||
if (has_back && list_index == 0) {
|
||||
return false; // Back button
|
||||
}
|
||||
|
||||
const size_t adjusted_index =
|
||||
current_start_index + static_cast<size_t>(list_index) - (has_back ? 1 : 0);
|
||||
|
||||
return state->getDirent(static_cast<uint32_t>(adjusted_index), out_entry);
|
||||
std::vector<TileGridItem> View::collectItems(void* userData) {
|
||||
auto* view = static_cast<View*>(userData);
|
||||
const bool is_root = (view->state->getCurrentPath() == "/");
|
||||
std::vector<TileGridItem> items;
|
||||
view->state->withEntries([&](const std::vector<dirent>& entries) {
|
||||
items.reserve(entries.size());
|
||||
for (const auto& entry : entries) {
|
||||
items.push_back({ entry.d_name, entry.d_name, getIcon(entry, is_root), false });
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
void View::onDirEntryPressed(uint32_t index) {
|
||||
const char* View::resolveIcon(const TileGridItem& item, void* userData) {
|
||||
auto* view = static_cast<View*>(userData);
|
||||
const std::string path = file::getChildPath(view->state->getCurrentPath(), item.id);
|
||||
return isExecutablePath(path) ? LVGL_ICON_SHARED_PLAY_ARROW : LVGL_ICON_SHARED_DRAFT;
|
||||
}
|
||||
|
||||
void View::onTileClicked(const TileGridItem& item, void* userData) {
|
||||
static_cast<View*>(userData)->onDirEntryPressed(item.id);
|
||||
}
|
||||
|
||||
void View::onTileLongPressed(const TileGridItem& item, void* userData) {
|
||||
static_cast<View*>(userData)->onDirEntryLongPressed(item.id);
|
||||
}
|
||||
|
||||
void View::onTileKey(const TileGridItem& item, uint32_t key, void* userData) {
|
||||
static_cast<View*>(userData)->onDirEntryKeyPressed(item.id, key);
|
||||
}
|
||||
|
||||
void View::onPageChanged(void* userData) {
|
||||
static_cast<View*>(userData)->hideOverlay();
|
||||
}
|
||||
|
||||
bool View::findDirent(const std::string& name, dirent& out_entry) {
|
||||
bool found = false;
|
||||
state->withEntries([&](const std::vector<dirent>& entries) {
|
||||
const auto it = std::ranges::find_if(entries, [&](const dirent& entry) { return name == entry.d_name; });
|
||||
if (it != entries.end()) {
|
||||
out_entry = *it;
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
void View::onDirEntryPressed(const std::string& name) {
|
||||
// A tap only closes the overlay, so it can't open an entry by accident
|
||||
if (!lv_obj_is_hidden(overlay)) {
|
||||
hideOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
dirent dir_entry;
|
||||
if (!resolveDirentFromListIndex(static_cast<int32_t>(index), dir_entry)) {
|
||||
if (!findDirent(name, dir_entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -262,7 +336,7 @@ void View::onDirEntryPressed(uint32_t index) {
|
||||
case TT_DT_CHR:
|
||||
state->setEntriesForChildPath(dir_entry.d_name);
|
||||
onNavigate();
|
||||
update();
|
||||
update(true);
|
||||
break;
|
||||
|
||||
case TT_DT_LNK:
|
||||
@ -276,21 +350,24 @@ void View::onDirEntryPressed(uint32_t index) {
|
||||
}
|
||||
}
|
||||
|
||||
void View::onDirEntryLongPressed(int32_t index) {
|
||||
void View::onDirEntryLongPressed(const std::string& name) {
|
||||
// Deselects the tile of an overlay that is still open
|
||||
hideOverlay();
|
||||
|
||||
dirent dir_entry;
|
||||
if (!resolveDirentFromListIndex(index, dir_entry)) {
|
||||
if (!findDirent(name, dir_entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Long-pressed %s %d", dir_entry.d_name, (int)dir_entry.d_type);
|
||||
state->setSelectedChildEntry(dir_entry.d_name);
|
||||
lv_obj_t* tile = grid.findTile(name);
|
||||
|
||||
if (state->getCurrentPath() == "/") {
|
||||
// At root, only USB mount points support actions (eject).
|
||||
// Other root-level entries intentionally have no context actions.
|
||||
const char* name = dir_entry.d_name;
|
||||
if (strncmp(name, "usb", 3) == 0 && isdigit((unsigned char)name[3])) {
|
||||
showActionsForMountPoint();
|
||||
if (isUsbMountPoint(dir_entry.d_name)) {
|
||||
showActionsForMountPoint(tile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -299,7 +376,7 @@ void View::onDirEntryLongPressed(int32_t index) {
|
||||
switch (dir_entry.d_type) {
|
||||
case TT_DT_DIR:
|
||||
case TT_DT_CHR:
|
||||
showActionsForDirectory();
|
||||
showActionsForDirectory(tile);
|
||||
break;
|
||||
|
||||
case TT_DT_LNK:
|
||||
@ -307,63 +384,17 @@ void View::onDirEntryLongPressed(int32_t index) {
|
||||
break;
|
||||
|
||||
default:
|
||||
showActionsForFile();
|
||||
showActionsForFile(tile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
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 if (isSupportedImageFile(dir_entry.d_name)) {
|
||||
symbol = LV_SYMBOL_IMAGE;
|
||||
} else if (dir_entry.d_type == file::TT_DT_LNK) {
|
||||
symbol = LV_SYMBOL_LOOP;
|
||||
} else if (isExecutablePath(file::getChildPath(state->getCurrentPath(), dir_entry.d_name))) {
|
||||
symbol = LV_SYMBOL_PLAY;
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
void View::onDirEntryKeyPressed(const std::string& name, uint32_t key) {
|
||||
if (key == ' ') {
|
||||
onDirEntryLongPressed(name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get file size for regular files
|
||||
std::string label_text = dir_entry.d_name;
|
||||
if (dir_entry.d_type == file::TT_DT_REG) {
|
||||
std::string file_path = file::getChildPath(state->getCurrentPath(), dir_entry.d_name);
|
||||
struct stat st;
|
||||
if (stat(file_path.c_str(), &st) == 0) {
|
||||
// Format file size in human-readable format
|
||||
const char* size_suffix;
|
||||
double size;
|
||||
if (st.st_size < 1024) {
|
||||
size = st.st_size;
|
||||
size_suffix = " B";
|
||||
} else if (st.st_size < 1024 * 1024) {
|
||||
size = st.st_size / 1024.0;
|
||||
size_suffix = " KB";
|
||||
} else {
|
||||
size = st.st_size / (1024.0 * 1024.0);
|
||||
size_suffix = " MB";
|
||||
}
|
||||
|
||||
char size_str[32];
|
||||
if (st.st_size < 1024) {
|
||||
snprintf(size_str, sizeof(size_str), " (%d%s)", (int)size, size_suffix);
|
||||
} else {
|
||||
snprintf(size_str, sizeof(size_str), " (%.1f%s)", size, size_suffix);
|
||||
}
|
||||
label_text += size_str;
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* button = lvgl_list_add_button(list, symbol, label_text.c_str());
|
||||
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this);
|
||||
lv_obj_add_event_cb(button, &onDirEntryKeyCallback, LV_EVENT_KEY, this);
|
||||
}
|
||||
|
||||
void View::onDirEntryKeyPressed(int32_t index, uint32_t key) {
|
||||
// Keyboards report their delete key as either delete or backspace (e.g. Cardputer's "del" key)
|
||||
const bool is_delete = key == LV_KEY_DEL || key == LV_KEY_BACKSPACE;
|
||||
const bool is_rename = key == 'r' || key == 'R';
|
||||
@ -372,7 +403,7 @@ void View::onDirEntryKeyPressed(int32_t index, uint32_t key) {
|
||||
}
|
||||
|
||||
dirent dir_entry;
|
||||
if (!resolveDirentFromListIndex(index, dir_entry)) {
|
||||
if (!findDirent(name, dir_entry)) {
|
||||
return;
|
||||
}
|
||||
// Same rules as the long-press actions: root entries are mount points, links have no actions
|
||||
@ -399,11 +430,27 @@ void View::onNavigateUpPressed() {
|
||||
state->setEntriesForPath(new_absolute_path);
|
||||
}
|
||||
onNavigate();
|
||||
update();
|
||||
update(true);
|
||||
}
|
||||
}
|
||||
|
||||
void View::onOverflowPressed() {
|
||||
if (!lv_obj_is_hidden(overlay)) {
|
||||
hideOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_clean(overlay);
|
||||
addOverlayButton(LVGL_ICON_SHARED_NOTE_ADD, onNewFilePressedCallback);
|
||||
addOverlayButton(LVGL_ICON_SHARED_CREATE_NEW_FOLDER, onNewFolderPressedCallback);
|
||||
if (state->hasClipboard()) {
|
||||
addOverlayButton(LVGL_ICON_SHARED_CONTENT_PASTE, onPastePressedCallback);
|
||||
}
|
||||
showOverlay(nullptr);
|
||||
}
|
||||
|
||||
void View::onRenamePressed() {
|
||||
onNavigate();
|
||||
std::string entry_name = state->getSelectedChildEntry();
|
||||
LOG_I(TAG, "Pending rename %s", entry_name.c_str());
|
||||
state->setPendingAction(State::ActionRename);
|
||||
@ -411,6 +458,7 @@ void View::onRenamePressed() {
|
||||
}
|
||||
|
||||
void View::onDeletePressed() {
|
||||
onNavigate();
|
||||
std::string file_path = state->getSelectedChildPath();
|
||||
LOG_I(TAG, "Pending delete %s", file_path.c_str());
|
||||
state->setPendingAction(State::ActionDelete);
|
||||
@ -420,55 +468,112 @@ void View::onDeletePressed() {
|
||||
}
|
||||
|
||||
void View::onNewFilePressed() {
|
||||
onNavigate();
|
||||
LOG_I(TAG, "Creating new file");
|
||||
state->setPendingAction(State::ActionCreateFile);
|
||||
inputdialog::start(appInstanceId, "New File", "Enter filename:", "", inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup);
|
||||
}
|
||||
|
||||
void View::onNewFolderPressed() {
|
||||
onNavigate();
|
||||
LOG_I(TAG, "Creating new folder");
|
||||
state->setPendingAction(State::ActionCreateFolder);
|
||||
inputdialog::start(appInstanceId, "New Folder", "Enter folder name:", "", inputDialogStream, inputDialogBuffer, sizeof(inputDialogBuffer), eventGroup);
|
||||
}
|
||||
|
||||
lv_obj_t* View::addOverlayButton(const char* icon, lv_event_cb_t callback) {
|
||||
auto* button = lvgl_icon_button_create(overlay);
|
||||
auto* label = lv_label_create(button);
|
||||
lv_obj_set_style_text_font(label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(label, icon);
|
||||
lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, this);
|
||||
return button;
|
||||
}
|
||||
|
||||
void View::addCommonFileActions() {
|
||||
auto* copy_button = lvgl_list_add_button(action_list, LV_SYMBOL_COPY, "Copy");
|
||||
lv_obj_add_event_cb(copy_button, onCopyPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* cut_button = lvgl_list_add_button(action_list, LV_SYMBOL_CUT, "Cut");
|
||||
lv_obj_add_event_cb(cut_button, onCutPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* rename_button = lvgl_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename");
|
||||
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* delete_button = lvgl_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete");
|
||||
lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
addOverlayButton(LVGL_ICON_SHARED_CONTENT_COPY, onCopyPressedCallback);
|
||||
addOverlayButton(LVGL_ICON_SHARED_CONTENT_CUT, onCutPressedCallback);
|
||||
if (state->hasClipboard()) {
|
||||
addOverlayButton(LVGL_ICON_SHARED_CONTENT_PASTE, onPastePressedCallback);
|
||||
}
|
||||
addOverlayButton(LVGL_ICON_SHARED_EDIT, onRenamePressedCallback);
|
||||
addOverlayButton(LVGL_ICON_SHARED_DELETE, onDeletePressedCallback);
|
||||
}
|
||||
|
||||
void View::showActions() {
|
||||
lv_obj_clean(action_list);
|
||||
addCommonFileActions();
|
||||
lv_obj_set_hidden(action_list, false);
|
||||
}
|
||||
|
||||
void View::showActionsForDirectory() { showActions(); }
|
||||
|
||||
void View::showActionsForFile() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
if (isExecutablePath(state->getSelectedChildPath())) {
|
||||
auto* run_button = lvgl_list_add_button(action_list, LV_SYMBOL_PLAY, "Run");
|
||||
lv_obj_add_event_cb(run_button, onRunPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
// The overlay covers the bottom of the grid, or its top when it would cover the anchor tile
|
||||
void View::showOverlay(lv_obj_t* anchorTile) {
|
||||
lv_obj_set_hidden(overlay, false);
|
||||
lv_obj_align(overlay, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_update_layout(overlay);
|
||||
if (anchorTile != nullptr) {
|
||||
lv_obj_add_state(anchorTile, LV_STATE_CHECKED);
|
||||
lv_area_t tile_area;
|
||||
lv_area_t overlay_area;
|
||||
lv_obj_get_coords(anchorTile, &tile_area);
|
||||
lv_obj_get_coords(overlay, &overlay_area);
|
||||
if (tile_area.y2 > overlay_area.y1) {
|
||||
lv_obj_align_to(overlay, lv_obj_get_parent(anchorTile), LV_ALIGN_TOP_MID, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
addCommonFileActions();
|
||||
lv_obj_set_hidden(action_list, false);
|
||||
lv_group_t* group = lv_group_get_default();
|
||||
if (group != nullptr && lv_obj_get_child_count(overlay) > 0) {
|
||||
lv_group_focus_obj(overlay);
|
||||
lv_gridnav_set_focused(overlay, lv_obj_get_child(overlay, 0), LV_ANIM_OFF);
|
||||
// Touch shows no selection, until a key moves the focus
|
||||
lv_indev_t* indev = lv_indev_active();
|
||||
if (indev != nullptr && lv_indev_get_type(indev) == LV_INDEV_TYPE_POINTER) {
|
||||
lvgl_focus_hide_key_selection(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void View::showActionsForMountPoint() {
|
||||
lv_obj_clean(action_list);
|
||||
void View::hideOverlay() {
|
||||
if (overlay == nullptr || lv_obj_is_hidden(overlay)) {
|
||||
return;
|
||||
}
|
||||
lv_obj_set_hidden(overlay, true);
|
||||
|
||||
auto* eject_button = lvgl_list_add_button(action_list, LV_SYMBOL_EJECT, "Eject");
|
||||
lv_obj_add_event_cb(eject_button, onEjectPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_t* tile = grid.findTile(state->getSelectedChildEntry());
|
||||
if (tile != nullptr) {
|
||||
lv_obj_remove_state(tile, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
lv_obj_set_hidden(action_list, false);
|
||||
// Keys continue on the tile that the overlay was for
|
||||
lv_group_t* group = lv_group_get_default();
|
||||
if (group != nullptr && lv_group_get_focused(group) == overlay) {
|
||||
if (tile != nullptr) {
|
||||
lv_group_focus_obj(lv_obj_get_parent(tile));
|
||||
lv_gridnav_set_focused(lv_obj_get_parent(tile), tile, LV_ANIM_OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void View::onOverlayKey(uint32_t key) {
|
||||
if (key == LV_KEY_ESC) {
|
||||
hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void View::showActionsForDirectory(lv_obj_t* tile) {
|
||||
lv_obj_clean(overlay);
|
||||
addCommonFileActions();
|
||||
showOverlay(tile);
|
||||
}
|
||||
|
||||
void View::showActionsForFile(lv_obj_t* tile) {
|
||||
lv_obj_clean(overlay);
|
||||
if (isExecutablePath(state->getSelectedChildPath())) {
|
||||
addOverlayButton(LVGL_ICON_SHARED_PLAY_ARROW, onRunPressedCallback);
|
||||
}
|
||||
addCommonFileActions();
|
||||
showOverlay(tile);
|
||||
}
|
||||
|
||||
void View::showActionsForMountPoint(lv_obj_t* tile) {
|
||||
lv_obj_clean(overlay);
|
||||
addOverlayButton(LVGL_ICON_SHARED_EJECT, onEjectPressedCallback);
|
||||
showOverlay(tile);
|
||||
}
|
||||
|
||||
void View::onRunPressed() {
|
||||
@ -493,76 +598,23 @@ void View::onEjectPressed() {
|
||||
|
||||
onNavigate();
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
update(false);
|
||||
}
|
||||
|
||||
void View::update(size_t start_index) {
|
||||
const bool is_root = (state->getCurrentPath() == "/");
|
||||
|
||||
void View::update(bool firstPage) {
|
||||
if (!lvgl_try_lock(500 / portTICK_PERIOD_MS)) {
|
||||
LOG_E(TAG, "Mutex acquisition timeout (%s)", "lvgl");
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_clean(dir_entry_list);
|
||||
const bool is_root = (state->getCurrentPath() == "/");
|
||||
lv_obj_set_hidden(navigate_up_button, is_root);
|
||||
lv_obj_set_hidden(overflow_button, is_root);
|
||||
|
||||
current_start_index = start_index;
|
||||
|
||||
state->withEntries([this, is_root](const std::vector<dirent>& entries) {
|
||||
size_t total_entries = entries.size();
|
||||
if (current_start_index >= total_entries) {
|
||||
current_start_index = (total_entries > MAX_BATCH)
|
||||
? (total_entries - MAX_BATCH)
|
||||
: 0;
|
||||
}
|
||||
size_t count = 0;
|
||||
|
||||
if (!is_root && current_start_index > 0) {
|
||||
auto* back_btn = lvgl_list_add_button(dir_entry_list, LV_SYMBOL_LEFT, "Back");
|
||||
lv_obj_add_event_cb(back_btn, [](lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
size_t new_index = (view->current_start_index >= view->MAX_BATCH) ?
|
||||
view->current_start_index - view->MAX_BATCH : 0;
|
||||
view->update(new_index); }, LV_EVENT_SHORT_CLICKED, this);
|
||||
}
|
||||
|
||||
for (size_t i = current_start_index; i < total_entries; ++i) {
|
||||
auto entry = entries[i];
|
||||
|
||||
createDirEntryWidget(dir_entry_list, entry);
|
||||
count++;
|
||||
|
||||
if (count >= MAX_BATCH) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
last_loaded_index = std::min(current_start_index + count, total_entries);
|
||||
|
||||
if (!is_root && last_loaded_index < total_entries) {
|
||||
if (total_entries > current_start_index &&
|
||||
(total_entries - current_start_index) > MAX_BATCH) {
|
||||
auto* next_btn = lvgl_list_add_button(dir_entry_list, LV_SYMBOL_RIGHT, "Next");
|
||||
lv_obj_add_event_cb(next_btn, [](lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
view->update(view->last_loaded_index); }, LV_EVENT_SHORT_CLICKED, this);
|
||||
}
|
||||
} else {
|
||||
last_loaded_index = total_entries;
|
||||
}
|
||||
});
|
||||
|
||||
if (is_root) {
|
||||
lv_obj_set_hidden(navigate_up_button, true);
|
||||
} else {
|
||||
lv_obj_set_hidden(navigate_up_button, false);
|
||||
}
|
||||
|
||||
if (state->hasClipboard() && !is_root) {
|
||||
lv_obj_set_hidden(paste_button, false);
|
||||
} else {
|
||||
lv_obj_set_hidden(paste_button, true);
|
||||
if (firstPage) {
|
||||
grid.showFirstPage();
|
||||
}
|
||||
grid.requestRepopulate(!firstPage);
|
||||
|
||||
lvgl_unlock();
|
||||
}
|
||||
@ -573,48 +625,38 @@ void View::init(uint32_t appInstanceId, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
const bool is_root = (state->getCurrentPath() == "/");
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "Files");
|
||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressedCallback, this);
|
||||
navigate_up_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
|
||||
new_file_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_FILE, &onNewFilePressedCallback, this);
|
||||
new_folder_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_DIRECTORY, &onNewFolderPressedCallback, this);
|
||||
paste_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_PASTE, &onPastePressedCallback, this);
|
||||
lv_obj_set_hidden(paste_button, true);
|
||||
lv_obj_set_hidden(navigate_up_button, is_root);
|
||||
auto* previous_button = lvgl_toolbar_add_text_button_action(toolbar, "<", onPageButtonPressedCallback, this);
|
||||
auto* next_button = lvgl_toolbar_add_text_button_action(toolbar, ">", onPageButtonPressedCallback, this);
|
||||
overflow_button = lvgl_toolbar_add_text_button_action(toolbar, LVGL_ICON_SHARED_MORE_VERT, onOverflowPressedCallback, this);
|
||||
lv_obj_set_style_text_font(overflow_button, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_obj_set_hidden(overflow_button, is_root);
|
||||
|
||||
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);
|
||||
grid.setSwipeNavigation(true);
|
||||
grid.createWidgets(parent);
|
||||
grid.setPageButtons(previous_button, next_button);
|
||||
|
||||
dir_entry_list = lvgl_list_create(wrapper);
|
||||
lv_obj_set_height(dir_entry_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(dir_entry_list, 1);
|
||||
|
||||
lv_obj_add_event_cb(dir_entry_list, dirEntryListScrollBeginCallback, LV_EVENT_SCROLL_BEGIN, this);
|
||||
|
||||
action_list = lvgl_list_create(wrapper);
|
||||
lv_obj_set_height(action_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(action_list, 1);
|
||||
lv_obj_set_hidden(action_list, true);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void View::onDirEntryListScrollBegin() {
|
||||
if (lvgl_try_lock(500 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_set_hidden(action_list, true);
|
||||
lvgl_unlock();
|
||||
}
|
||||
// Floating, so showing it doesn't change the grid's size and page layout
|
||||
overlay = lvgl_card_create(parent);
|
||||
lv_obj_set_floating(overlay, true);
|
||||
lv_obj_set_size(overlay, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(overlay, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(overlay, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(overlay, false);
|
||||
lvgl_obj_add_edge_padding(overlay);
|
||||
// The arrow keys move between the overlay's buttons as one row
|
||||
lvgl_grid_navigation_add(overlay);
|
||||
lv_obj_add_event_cb(overlay, onOverlayKeyCallback, LV_EVENT_KEY, this);
|
||||
lv_obj_set_hidden(overlay, true);
|
||||
}
|
||||
|
||||
void View::onNavigate() {
|
||||
if (lvgl_try_lock(500 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_set_hidden(action_list, true);
|
||||
lvgl_unlock();
|
||||
}
|
||||
hideOverlay();
|
||||
}
|
||||
|
||||
void View::onResult(uint32_t launchId, int32_t result) {
|
||||
@ -656,7 +698,7 @@ void View::onResult(uint32_t launchId, int32_t result) {
|
||||
}
|
||||
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
update(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -678,7 +720,7 @@ void View::onResult(uint32_t launchId, int32_t result) {
|
||||
}
|
||||
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
update(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -705,7 +747,7 @@ void View::onResult(uint32_t launchId, int32_t result) {
|
||||
}
|
||||
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
update(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -727,7 +769,7 @@ void View::onResult(uint32_t launchId, int32_t result) {
|
||||
}
|
||||
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
update(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -790,7 +832,7 @@ void View::onCopyPressed() {
|
||||
state->setClipboard(path, false);
|
||||
LOG_I(TAG, "Copied to clipboard: %s", path.c_str());
|
||||
onNavigate();
|
||||
update();
|
||||
update(false);
|
||||
}
|
||||
|
||||
void View::onCutPressed() {
|
||||
@ -798,10 +840,11 @@ void View::onCutPressed() {
|
||||
state->setClipboard(path, true);
|
||||
LOG_I(TAG, "Cut to clipboard: %s", path.c_str());
|
||||
onNavigate();
|
||||
update();
|
||||
update(false);
|
||||
}
|
||||
|
||||
void View::onPastePressed() {
|
||||
onNavigate();
|
||||
auto clipboard = state->getClipboard();
|
||||
if (!clipboard.has_value()) return;
|
||||
|
||||
@ -873,11 +916,7 @@ void View::doPaste(const std::string& src, bool is_cut, const std::string& dst)
|
||||
}
|
||||
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
}
|
||||
|
||||
void View::deinit() {
|
||||
lv_obj_remove_event_cb(dir_entry_list, dirEntryListScrollBeginCallback);
|
||||
update(false);
|
||||
}
|
||||
|
||||
} // namespace tt::app::files
|
||||
|
||||
@ -76,7 +76,7 @@ void collectIncompatibleAppIds(const AppPackage* package, void* context) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AppGridItem> collectItems() {
|
||||
std::vector<TileGridItem> collectItems() {
|
||||
const std::vector<std::string> favouriteIds = favourites.load();
|
||||
|
||||
std::vector<::AppManifest> collected;
|
||||
@ -87,34 +87,34 @@ std::vector<AppGridItem> collectItems() {
|
||||
return !isShown(manifest) || std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end();
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
std::vector<TileGridItem> items;
|
||||
items.reserve(collected.size());
|
||||
for (const auto& manifest : collected) {
|
||||
items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
|
||||
items.push_back({ manifest.id, manifest.name, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
|
||||
}
|
||||
std::ranges::sort(items, [](const AppGridItem& a, const AppGridItem& b) {
|
||||
std::ranges::sort(items, [](const TileGridItem& a, const TileGridItem& b) {
|
||||
if (a.highlighted != b.highlighted) {
|
||||
return a.highlighted;
|
||||
}
|
||||
return strcmp(a.manifest.name, b.manifest.name) < 0;
|
||||
return a.name < b.name;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
void toggleFavourite(AppGrid& grid, const char* appId, bool keepSelection) {
|
||||
void toggleFavourite(TileGrid& grid, const char* appId, bool keepSelection) {
|
||||
if (!favourites.toggle(appId)) {
|
||||
LOG_E(TAG, "Failed to save favourites");
|
||||
}
|
||||
grid.requestRepopulate(keepSelection);
|
||||
}
|
||||
|
||||
void onLongPressed(AppGrid& grid, const ::AppManifest& manifest) {
|
||||
toggleFavourite(grid, manifest.id, false);
|
||||
void onLongPressed(TileGrid& grid, const TileGridItem& item) {
|
||||
toggleFavourite(grid, item.id.c_str(), false);
|
||||
}
|
||||
|
||||
void onKey(AppGrid& grid, const ::AppManifest& manifest, uint32_t key) {
|
||||
void onKey(TileGrid& grid, const TileGridItem& item, uint32_t key) {
|
||||
if (key == 'f' || key == 'F') {
|
||||
toggleFavourite(grid, manifest.id, true);
|
||||
toggleFavourite(grid, item.id.c_str(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ void onKey(AppGrid& grid, const ::AppManifest& manifest, uint32_t key) {
|
||||
|
||||
const LauncherMode APPS_MODE = {
|
||||
.buttonIcon = LVGL_ICON_SHARED_APPS,
|
||||
.iconColor = AppGrid::IconColor::Primary,
|
||||
.iconColor = TileGrid::IconColor::Primary,
|
||||
.collect = collectItems,
|
||||
.onLongPressed = onLongPressed,
|
||||
.onKey = onKey
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
#include <Tactility/app/TileGrid.h>
|
||||
#include <Tactility/app/launcher/LauncherMode.h>
|
||||
#include <Tactility/app/setup/Setup.h>
|
||||
#include <Tactility/settings/BootSettings.h>
|
||||
@ -30,10 +30,10 @@ constexpr auto* TAG = "Launcher";
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<AppGridItem> collectItems(void* userData);
|
||||
void onAppClicked(const ::AppManifest& manifest, void* userData);
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData);
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData);
|
||||
std::vector<TileGridItem> collectItems(void* userData);
|
||||
void onAppClicked(const TileGridItem& item, void* userData);
|
||||
void onAppLongPressed(const TileGridItem& item, void* userData);
|
||||
void onAppKey(const TileGridItem& item, uint32_t key, void* userData);
|
||||
|
||||
enum class Mode {
|
||||
Apps,
|
||||
@ -51,16 +51,18 @@ Mode getOtherMode(Mode mode) {
|
||||
struct Context {
|
||||
Mode mode = Mode::Apps;
|
||||
lv_obj_t* modeButtonIcon = nullptr;
|
||||
AppGrid grid { AppGrid::Callbacks {
|
||||
TileGrid grid { TileGrid::Callbacks {
|
||||
.collect = collectItems,
|
||||
.onClicked = onAppClicked,
|
||||
.onLongPressed = onAppLongPressed,
|
||||
.onKey = onAppKey,
|
||||
.resolveIcon = nullptr,
|
||||
.onPageChanged = nullptr,
|
||||
.userData = this
|
||||
} };
|
||||
};
|
||||
|
||||
std::vector<AppGridItem> collectItems(void* userData) {
|
||||
std::vector<TileGridItem> collectItems(void* userData) {
|
||||
return getLauncherMode(static_cast<Context*>(userData)->mode).collect();
|
||||
}
|
||||
|
||||
@ -72,23 +74,23 @@ void startApp(const char* appId) {
|
||||
}
|
||||
}
|
||||
|
||||
void onAppClicked(const ::AppManifest& manifest, void*) {
|
||||
startApp(manifest.id);
|
||||
void onAppClicked(const TileGridItem& item, void*) {
|
||||
startApp(item.id.c_str());
|
||||
}
|
||||
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData) {
|
||||
void onAppLongPressed(const TileGridItem& item, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
const LauncherMode& mode = getLauncherMode(ctx->mode);
|
||||
if (mode.onLongPressed != nullptr) {
|
||||
mode.onLongPressed(ctx->grid, manifest);
|
||||
mode.onLongPressed(ctx->grid, item);
|
||||
}
|
||||
}
|
||||
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData) {
|
||||
void onAppKey(const TileGridItem& item, uint32_t key, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
const LauncherMode& mode = getLauncherMode(ctx->mode);
|
||||
if (mode.onKey != nullptr) {
|
||||
mode.onKey(ctx->grid, manifest, key);
|
||||
mode.onKey(ctx->grid, item, key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -49,17 +49,17 @@ void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
manifests->push_back(*manifest);
|
||||
}
|
||||
|
||||
std::vector<AppGridItem> collectItems() {
|
||||
std::vector<TileGridItem> collectItems() {
|
||||
std::vector<::AppManifest> collected;
|
||||
app_manager_for_each_manifest(collectManifest, &collected);
|
||||
std::ranges::sort(collected, [](const ::AppManifest& a, const ::AppManifest& b) {
|
||||
return strcmp(a.name, b.name) < 0;
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
std::vector<TileGridItem> items;
|
||||
for (const auto& manifest : collected) {
|
||||
if (manifest.category == APP_CATEGORY_SETTINGS && (manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
|
||||
items.push_back({ manifest, appIcon(&manifest), false });
|
||||
items.push_back({ manifest.id, manifest.name, appIcon(&manifest), false });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
@ -69,7 +69,7 @@ std::vector<AppGridItem> collectItems() {
|
||||
|
||||
const LauncherMode SETTINGS_MODE = {
|
||||
.buttonIcon = LVGL_ICON_SHARED_SETTINGS,
|
||||
.iconColor = AppGrid::IconColor::Secondary,
|
||||
.iconColor = TileGrid::IconColor::Secondary,
|
||||
.collect = collectItems,
|
||||
.onLongPressed = nullptr,
|
||||
.onKey = nullptr
|
||||
|
||||
@ -15,7 +15,13 @@
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/log.h>
|
||||
@ -33,7 +39,9 @@ struct Context {
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
|
||||
lv_obj_t* uiCurrentFileName = nullptr;
|
||||
lv_obj_t* uiDropDownMenu = nullptr;
|
||||
lv_obj_t* uiToolbar = nullptr;
|
||||
lv_obj_t* uiOverflowButton = nullptr;
|
||||
lv_obj_t* uiOverlay = nullptr;
|
||||
lv_obj_t* uiNoteText = nullptr;
|
||||
|
||||
std::string filePath;
|
||||
@ -77,50 +85,100 @@ bool saveFile(Context* ctx, const std::string& path) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void appNotesEventCb(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
lv_event_code_t code = lv_event_get_code(e);
|
||||
lv_obj_t* obj = lv_event_get_target_obj(e);
|
||||
void hideOverlay(Context* ctx) {
|
||||
if (lv_obj_is_hidden(ctx->uiOverlay)) {
|
||||
return;
|
||||
}
|
||||
lv_obj_set_hidden(ctx->uiOverlay, true);
|
||||
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
if (obj == ctx->uiDropDownMenu) {
|
||||
switch (lv_dropdown_get_selected(obj)) {
|
||||
case 0: // New
|
||||
resetFileContent(ctx);
|
||||
break;
|
||||
case 1: // Save
|
||||
if (!ctx->filePath.empty()) {
|
||||
lvgl_lock();
|
||||
ctx->saveBuffer = lv_textarea_get_text(ctx->uiNoteText);
|
||||
lvgl_unlock();
|
||||
saveFile(ctx, ctx->filePath);
|
||||
}
|
||||
break;
|
||||
case 2: // Save as...
|
||||
lvgl_lock();
|
||||
ctx->saveBuffer = lv_textarea_get_text(ctx->uiNoteText);
|
||||
lvgl_unlock();
|
||||
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId, ctx->saveResultStream, ctx->saveResultBuffer, sizeof(ctx->saveResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->saveFileLaunchId);
|
||||
break;
|
||||
case 3: // Load
|
||||
ctx->loadFileLaunchId = fileselection::startForExistingFile(ctx->appInstanceId, ctx->loadResultStream, ctx->loadResultBuffer, sizeof(ctx->loadResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->loadFileLaunchId);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
auto* cont = lv_event_get_current_target_obj(e);
|
||||
if (obj == cont) return;
|
||||
if (lv_obj_get_child(cont, 1)) {
|
||||
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId, ctx->saveResultStream, ctx->saveResultBuffer, sizeof(ctx->saveResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->saveFileLaunchId);
|
||||
} else { //Reset
|
||||
resetFileContent(ctx);
|
||||
}
|
||||
// Keys continue on the button that opened the overlay
|
||||
lv_group_t* group = lv_group_get_default();
|
||||
if (group != nullptr && lv_group_get_focused(group) == ctx->uiOverlay) {
|
||||
lv_group_focus_obj(ctx->uiToolbar);
|
||||
lv_gridnav_set_focused(ctx->uiToolbar, ctx->uiOverflowButton, LV_ANIM_OFF);
|
||||
}
|
||||
}
|
||||
|
||||
void onNewPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
hideOverlay(ctx);
|
||||
resetFileContent(ctx);
|
||||
}
|
||||
|
||||
void onSavePressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
hideOverlay(ctx);
|
||||
ctx->saveBuffer = lv_textarea_get_text(ctx->uiNoteText);
|
||||
saveFile(ctx, ctx->filePath);
|
||||
}
|
||||
|
||||
void onSaveAsPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
hideOverlay(ctx);
|
||||
ctx->saveBuffer = lv_textarea_get_text(ctx->uiNoteText);
|
||||
ctx->saveFileLaunchId = fileselection::startForExistingOrNewFile(ctx->appInstanceId, ctx->saveResultStream, ctx->saveResultBuffer, sizeof(ctx->saveResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->saveFileLaunchId);
|
||||
}
|
||||
|
||||
void onOpenPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
hideOverlay(ctx);
|
||||
ctx->loadFileLaunchId = fileselection::startForExistingFile(ctx->appInstanceId, ctx->loadResultStream, ctx->loadResultBuffer, sizeof(ctx->loadResultBuffer), ctx->eventGroup);
|
||||
LOG_I(TAG, "launched with id %u", ctx->loadFileLaunchId);
|
||||
}
|
||||
|
||||
void addOverlayButton(Context* ctx, const char* icon, lv_event_cb_t callback) {
|
||||
auto* button = lvgl_icon_button_create(ctx->uiOverlay);
|
||||
auto* label = lv_label_create(button);
|
||||
lv_obj_set_style_text_font(label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(label, icon);
|
||||
lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
|
||||
void showOverlay(Context* ctx) {
|
||||
lv_obj_clean(ctx->uiOverlay);
|
||||
addOverlayButton(ctx, LVGL_ICON_SHARED_NOTE_ADD, onNewPressed);
|
||||
// Saving needs a file, which "save as" picks
|
||||
if (!ctx->filePath.empty()) {
|
||||
addOverlayButton(ctx, LVGL_ICON_SHARED_SAVE, onSavePressed);
|
||||
}
|
||||
addOverlayButton(ctx, LVGL_ICON_SHARED_SAVE_AS, onSaveAsPressed);
|
||||
addOverlayButton(ctx, LVGL_ICON_SHARED_FOLDER_OPEN, onOpenPressed);
|
||||
|
||||
lv_obj_set_hidden(ctx->uiOverlay, false);
|
||||
lv_obj_align_to(ctx->uiOverlay, ctx->uiToolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
|
||||
|
||||
lv_group_t* group = lv_group_get_default();
|
||||
if (group != nullptr) {
|
||||
lv_group_focus_obj(ctx->uiOverlay);
|
||||
lv_gridnav_set_focused(ctx->uiOverlay, lv_obj_get_child(ctx->uiOverlay, 0), LV_ANIM_OFF);
|
||||
// Touch shows no selection, until a key moves the focus
|
||||
lv_indev_t* indev = lv_indev_active();
|
||||
if (indev != nullptr && lv_indev_get_type(indev) == LV_INDEV_TYPE_POINTER) {
|
||||
lvgl_focus_hide_key_selection(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onOverflowPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
if (lv_obj_is_hidden(ctx->uiOverlay)) {
|
||||
showOverlay(ctx);
|
||||
} else {
|
||||
hideOverlay(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
void onOverlayKey(lv_event_t* e) {
|
||||
if (lv_event_get_key(e) == LV_KEY_ESC) {
|
||||
hideOverlay(static_cast<Context*>(lv_event_get_user_data(e)));
|
||||
}
|
||||
}
|
||||
|
||||
void onNoteTextPressed(lv_event_t* e) {
|
||||
hideOverlay(static_cast<Context*>(lv_event_get_user_data(e)));
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
@ -128,16 +186,10 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_t* toolbar = lvgl_toolbar_create(parent, "Notes");
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
|
||||
ctx->uiDropDownMenu = lv_dropdown_create(toolbar);
|
||||
lv_dropdown_set_options(ctx->uiDropDownMenu, LV_SYMBOL_FILE " New File\n" LV_SYMBOL_SAVE " Save\n" LV_SYMBOL_SAVE " Save As...\n" LV_SYMBOL_DIRECTORY " Open File");
|
||||
lv_dropdown_set_text(ctx->uiDropDownMenu, "Menu");
|
||||
lv_dropdown_set_symbol(ctx->uiDropDownMenu, LV_SYMBOL_DOWN);
|
||||
lv_dropdown_set_selected_highlight(ctx->uiDropDownMenu, false);
|
||||
lv_obj_align(ctx->uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->uiDropDownMenu, appNotesEventCb, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
ctx->uiToolbar = lvgl_toolbar_create(parent, "Notes");
|
||||
lv_obj_align(ctx->uiToolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
ctx->uiOverflowButton = lvgl_toolbar_add_text_button_action(ctx->uiToolbar, LVGL_ICON_SHARED_MORE_VERT, onOverflowPressed, ctx);
|
||||
lv_obj_set_style_text_font(ctx->uiOverflowButton, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
@ -158,17 +210,18 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_bg_color(ctx->uiNoteText, lv_color_hex(0x262626), LV_PART_MAIN);
|
||||
}
|
||||
lv_textarea_set_placeholder_text(ctx->uiNoteText, "Notes...");
|
||||
lv_obj_add_event_cb(ctx->uiNoteText, onNoteTextPressed, LV_EVENT_PRESSED, ctx);
|
||||
|
||||
lv_obj_t* footer = lv_obj_create(wrapper);
|
||||
lv_obj_set_flex_flow(footer, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(footer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
// Shows the screen background
|
||||
lv_obj_set_style_bg_opa(footer, LV_OPA_TRANSP, LV_PART_MAIN);
|
||||
if (lv_display_get_color_format(lv_obj_get_display(parent)) == LV_COLOR_FORMAT_L8) {
|
||||
lv_obj_set_style_bg_color(footer, lv_color_hex(0xEEEEEE), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(footer, 1, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_color(footer, lv_theme_get_color_secondary(footer), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_side(footer, LV_BORDER_SIDE_TOP, LV_PART_MAIN);
|
||||
} else {
|
||||
lv_obj_set_style_bg_color(footer, lv_color_hex(0x262626), LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(footer, 0, LV_PART_MAIN);
|
||||
}
|
||||
lv_obj_set_width(footer, LV_PCT(100));
|
||||
@ -183,6 +236,19 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_label_set_text(ctx->uiCurrentFileName, "Untitled");
|
||||
lv_obj_align(ctx->uiCurrentFileName, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
// Floating over the text area, below the toolbar
|
||||
ctx->uiOverlay = lvgl_card_create(parent);
|
||||
lv_obj_set_floating(ctx->uiOverlay, true);
|
||||
lv_obj_set_size(ctx->uiOverlay, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(ctx->uiOverlay, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(ctx->uiOverlay, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(ctx->uiOverlay, false);
|
||||
lvgl_obj_add_edge_padding(ctx->uiOverlay);
|
||||
// The arrow keys move between the overlay's buttons as one row
|
||||
lvgl_grid_navigation_add(ctx->uiOverlay);
|
||||
lv_obj_add_event_cb(ctx->uiOverlay, onOverlayKey, LV_EVENT_KEY, ctx);
|
||||
lv_obj_set_hidden(ctx->uiOverlay, true);
|
||||
|
||||
if (!ctx->filePath.empty()) {
|
||||
openFile(ctx, ctx->filePath);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user