3314 lines
122 KiB
ObjectPascal
3314 lines
122 KiB
ObjectPascal
{
|
|
App->FileBrowser - GUI file browser / manager
|
|
|
|
Phases 2-7: window, VFS loading, navigation, detail list with
|
|
type column, breadcrumb bar, file operations (delete, rename,
|
|
create folder/file), right-click context menu.
|
|
|
|
@author(Aaron Hance <[email protected]>)
|
|
}
|
|
unit app.filebrowser;
|
|
|
|
interface
|
|
|
|
procedure init();
|
|
|
|
implementation
|
|
|
|
uses
|
|
driver.video.desktop,
|
|
driver.video.windows,
|
|
driver.video.lvgl,
|
|
driver.video,
|
|
driver.storage.vfs,
|
|
driver.storage.types,
|
|
driver.storage.filedispatch,
|
|
core.ds.hashmap,
|
|
memory.heap,
|
|
proc.mgr,
|
|
proc.types,
|
|
core.strings,
|
|
core.strings.helpers,
|
|
core.gfx.fileicons,
|
|
debug.tracer,
|
|
core.util,
|
|
arch.x86.util;
|
|
|
|
const
|
|
WIN_W = 800;
|
|
WIN_H = 600;
|
|
TOOLBAR_H = 36;
|
|
STATUSBAR_H = 24;
|
|
ENTRY_MAX = 256;
|
|
HIST_MAX = 32;
|
|
MAX_TABS = 8;
|
|
MAX_BOOKMARKS = 16;
|
|
PATH_MAX = 256;
|
|
|
|
{ LVGL FontAwesome symbol UTF-8 strings (from lv_symbol_def.h) }
|
|
SYM_LEFT = #$EF#$81#$93; { U+F053 }
|
|
SYM_RIGHT = #$EF#$81#$94; { U+F054 }
|
|
SYM_UP = #$EF#$81#$B7; { U+F077 }
|
|
SYM_REFRESH = #$EF#$80#$A1; { U+F021 }
|
|
SYM_PLUS = #$EF#$81#$A7; { U+F067 }
|
|
SYM_DIRECTORY = #$EF#$81#$BB; { U+F07B }
|
|
SYM_FILE = #$EF#$85#$9B; { U+F158 }
|
|
SYM_EDIT = #$EF#$8C#$84; { U+F304 }
|
|
SYM_TRASH = #$EF#$8B#$AD; { U+F2ED }
|
|
SYM_CLOSE = #$EF#$80#$8D; { U+F00D }
|
|
SYM_EYE_OPEN = #$EF#$81#$AE; { U+F06E }
|
|
SYM_IMAGE = #$EF#$80#$BE; { U+F03E }
|
|
SYM_LIST = #$EF#$80#$8B; { U+F00B }
|
|
SYM_HOME = #$EF#$80#$95; { U+F015 }
|
|
SYM_DRIVE = #$EF#$80#$9C; { U+F01C }
|
|
|
|
type
|
|
TViewMode = (vmDetail, vmCompact);
|
|
|
|
{ Heap-allocated name/size buffers (Lesson #11: no large stack arrays) }
|
|
TNameBuf = array[0..ENTRY_MAX - 1] of pchar;
|
|
PNameBuf = ^TNameBuf;
|
|
TSizeBuf = array[0..ENTRY_MAX - 1] of uint32;
|
|
PSizeBuf = ^TSizeBuf;
|
|
|
|
{ Collect context for hashmap forEach callback }
|
|
PFBCollectCtx = ^TFBCollectCtx;
|
|
TFBCollectCtx = record
|
|
dir_names : PNameBuf;
|
|
file_names : PNameBuf;
|
|
file_sizes : PSizeBuf;
|
|
dir_count : uint32;
|
|
file_count : uint32;
|
|
end;
|
|
|
|
{ Per-tab state snapshot }
|
|
PTabSnapshot = ^TTabSnapshot;
|
|
TTabSnapshot = record
|
|
cur_path : pchar;
|
|
history : array[0..HIST_MAX - 1] of pchar;
|
|
hist_len : uint32;
|
|
hist_pos : uint32;
|
|
sel_path : pchar;
|
|
sel_is_dir : boolean;
|
|
item_count : uint32;
|
|
filter_text: pchar;
|
|
end;
|
|
|
|
PFileBrowserState = ^TFileBrowserState;
|
|
TFileBrowserState = record
|
|
win_id : uint32;
|
|
pid : uint32;
|
|
{ Main widget pointers }
|
|
toolbar : Plv_obj;
|
|
tab_bar : Plv_obj;
|
|
content_area : Plv_obj;
|
|
status_bar : Plv_obj;
|
|
status_label : Plv_obj;
|
|
{ Navigation }
|
|
cur_path : pchar;
|
|
history : array[0..HIST_MAX - 1] of pchar;
|
|
hist_len : uint32;
|
|
hist_pos : uint32;
|
|
{ Nav buttons }
|
|
back_btn : Plv_obj;
|
|
fwd_btn : Plv_obj;
|
|
up_btn : Plv_obj;
|
|
{ Breadcrumb / path edit }
|
|
breadcrumb : Plv_obj;
|
|
path_ta : Plv_obj;
|
|
path_editing : boolean;
|
|
{ Selection }
|
|
sel_path : pchar; { full path of selected item (heap-alloc'd) }
|
|
sel_is_dir : boolean; { true if selected item is a directory }
|
|
sel_row : Plv_obj; { the highlighted row object }
|
|
{ File ops state }
|
|
op_err : TError; { result from async ops }
|
|
{ Context menu }
|
|
ctx_menu : Plv_obj; { popup panel, nil when hidden }
|
|
{ Tabs }
|
|
tabs : array[0..MAX_TABS - 1] of PTabSnapshot;
|
|
tab_btns : array[0..MAX_TABS - 1] of Plv_obj;
|
|
tab_count : uint32;
|
|
active_tab : uint32;
|
|
{ View state }
|
|
view_mode : TViewMode;
|
|
item_count : uint32;
|
|
{ Bookmarks }
|
|
body_row : Plv_obj;
|
|
sidebar : Plv_obj;
|
|
bookmarks : array[0..MAX_BOOKMARKS - 1] of pchar;
|
|
bm_count : uint32;
|
|
{ Filter }
|
|
filter_ta : Plv_obj;
|
|
filter_text : pchar;
|
|
{ Type-ahead }
|
|
ta_buf : array[0..31] of char;
|
|
ta_len : uint32;
|
|
ta_timer : Plv_timer;
|
|
{ Preview panel }
|
|
preview_panel : Plv_obj;
|
|
preview_visible: boolean;
|
|
{ Sorting }
|
|
sort_col : uint32; { 0=name, 1=size, 2=type }
|
|
sort_asc : boolean;
|
|
{ Hidden files }
|
|
show_hidden : boolean;
|
|
{ Directory watch }
|
|
watch_id : uint32;
|
|
watch_dirty : boolean;
|
|
watch_timer : Plv_timer;
|
|
{ Async refresh pipeline }
|
|
refresh_pending : uint32; { 1 = fb_entry should load data }
|
|
rd_dir_names : PNameBuf;
|
|
rd_file_names : PNameBuf;
|
|
rd_file_sizes : PSizeBuf;
|
|
rd_dir_count : uint32;
|
|
rd_file_count : uint32;
|
|
rd_ready : uint32; { 1 = data ready for UI build }
|
|
rd_failed : boolean; { true = VFS returned nil }
|
|
refresh_poll : Plv_timer; { 50ms poll timer }
|
|
{ Worker VFS operations (mkdir, rename) }
|
|
wop_pending : uint32; { 0=none, 1=mkdir, 2=rename }
|
|
wop_path1 : pchar; { path arg 1 }
|
|
wop_path2 : pchar; { path arg 2 (rename new-name) }
|
|
wop_done : uint32; { 1 = op finished }
|
|
wop_result : TError; { result from op }
|
|
wop_kind : uint32; { copy of pending kind for UI callback }
|
|
{ File dispatch (open file via handler) }
|
|
dispatch_pending : uint32; { 1 = fb_entry should dispatch }
|
|
dispatch_path : pchar; { abs path to dispatch }
|
|
dispatch_done : uint32; { 1 = dispatch finished }
|
|
dispatch_result : uint32; { PID returned by dispatch, 0 = no handler }
|
|
end;
|
|
|
|
var
|
|
g_win_id : uint32;
|
|
g_state : PFileBrowserState;
|
|
|
|
{ Forward declarations }
|
|
procedure fb_entry(ctx: PProcessContext); forward;
|
|
procedure launch; forward;
|
|
procedure onClose(wid: uint32); forward;
|
|
procedure onResize(wid: uint32; new_w, new_h: sint32); forward;
|
|
procedure freeState(state: PFileBrowserState); forward;
|
|
procedure do_refresh_load(state: PFileBrowserState); forward;
|
|
procedure do_refresh_build(state: PFileBrowserState); forward;
|
|
procedure schedule_refresh(state: PFileBrowserState); forward;
|
|
procedure fb_refresh_poll_cb(tmr: Plv_timer); cdecl; forward;
|
|
function fb_error_name(err : TError) : pchar; forward;
|
|
procedure fb_collect_cb(key: pchar; data: void; ud: void); forward;
|
|
procedure do_refresh(state: PFileBrowserState); forward;
|
|
procedure navigate_to(state: PFileBrowserState; newDir: pchar); forward;
|
|
procedure push_history(state: PFileBrowserState; path: pchar); forward;
|
|
procedure update_nav_btns(state: PFileBrowserState); forward;
|
|
procedure update_status_bar(state: PFileBrowserState); forward;
|
|
procedure free_children_userdata(container: Plv_obj); forward;
|
|
procedure fb_dir_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_file_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_back_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_fwd_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_up_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_refresh_cb(e: Plv_event); cdecl; forward;
|
|
procedure build_breadcrumbs(state: PFileBrowserState); forward;
|
|
procedure fb_crumb_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_crumb_area_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_path_ta_cb(e: Plv_event); cdecl; forward;
|
|
{ Phase 6 — file operations }
|
|
procedure fb_mbox_close_cb(e: Plv_event); cdecl; forward;
|
|
procedure select_item(state: PFileBrowserState; row: Plv_obj;
|
|
path: pchar; isDir: boolean); forward;
|
|
procedure clear_selection(state: PFileBrowserState); forward;
|
|
procedure fb_delete_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_delete_confirm_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_delete_done(error: TError; userdata: pointer); forward;
|
|
procedure fb_delete_done_timer(tmr: Plv_timer); cdecl; forward;
|
|
procedure fb_newfolder_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_newfolder_ok_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_rename_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_rename_ok_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_newfile_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_newfile_ok_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_newfile_opened(error: TError; userdata: pointer); forward;
|
|
procedure fb_newfile_written(error: TError; userdata: pointer); forward;
|
|
procedure fb_newfile_done_timer(tmr: Plv_timer); cdecl; forward;
|
|
{ Phase 7 — context menu }
|
|
procedure fb_row_long_press_cb(e: Plv_event); cdecl; forward;
|
|
procedure show_context_menu(state: PFileBrowserState; mx, my: sint32); forward;
|
|
procedure destroy_context_menu(state: PFileBrowserState); forward;
|
|
procedure fb_ctx_item_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_ctx_dismiss_timer(tmr: Plv_timer); cdecl; forward;
|
|
{ Phase 8 — tabs }
|
|
procedure save_tab(state: PFileBrowserState; idx: uint32); forward;
|
|
procedure restore_tab(state: PFileBrowserState; idx: uint32); forward;
|
|
procedure switch_tab(state: PFileBrowserState; idx: uint32); forward;
|
|
procedure add_tab(state: PFileBrowserState; path: pchar); forward;
|
|
procedure close_tab(state: PFileBrowserState; idx: uint32); forward;
|
|
procedure rebuild_tab_bar(state: PFileBrowserState); forward;
|
|
procedure fb_tab_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_tab_close_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_tab_new_cb(e: Plv_event); cdecl; forward;
|
|
procedure free_tab_snapshot(snap: PTabSnapshot); forward;
|
|
{ Phase 9 — bookmarks }
|
|
procedure rebuild_bookmarks(state: PFileBrowserState); forward;
|
|
procedure add_bookmark_entry(state: PFileBrowserState; path: pchar); forward;
|
|
procedure remove_bookmark_entry(state: PFileBrowserState; idx: uint32); forward;
|
|
procedure fb_bookmark_click_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_bookmark_add_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_bookmark_ctx_cb(e: Plv_event); cdecl; forward;
|
|
{ Phase 10 — quick filter }
|
|
procedure fb_filter_changed_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_filter_clear_cb(e: Plv_event); cdecl; forward;
|
|
{ Phase 11 — type-ahead jump }
|
|
procedure fb_typeahead_key_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_typeahead_reset_timer(tmr: Plv_timer); cdecl; forward;
|
|
{ Phase 12 — file preview panel }
|
|
procedure fb_preview_toggle_cb(e: Plv_event); cdecl; forward;
|
|
procedure update_preview(state: PFileBrowserState); forward;
|
|
{ Phase 13 — compact view }
|
|
procedure fb_viewmode_toggle_cb(e: Plv_event); cdecl; forward;
|
|
{ Phase 14 — column header sorting }
|
|
procedure fb_header_name_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_header_size_cb(e: Plv_event); cdecl; forward;
|
|
procedure fb_header_type_cb(e: Plv_event); cdecl; forward;
|
|
{ Phase 15 — hidden files toggle }
|
|
procedure fb_hidden_toggle_cb(e: Plv_event); cdecl; forward;
|
|
{ Phase 17 — directory watch live refresh }
|
|
procedure fb_watch_cb(event: TVFSWatchEvent; path: pchar; userdata: pointer); forward;
|
|
procedure fb_watch_poll_timer(tmr: Plv_timer); cdecl; forward;
|
|
{ ============================================================
|
|
free_tab_snapshot — release a TTabSnapshot and its contents
|
|
============================================================ }
|
|
procedure free_tab_snapshot(snap: PTabSnapshot);
|
|
var
|
|
i: uint32;
|
|
begin
|
|
if snap = nil then exit;
|
|
if snap^.cur_path <> nil then kfree(void(snap^.cur_path));
|
|
if snap^.sel_path <> nil then kfree(void(snap^.sel_path));
|
|
if snap^.hist_len > 0 then
|
|
for i := 0 to snap^.hist_len - 1 do
|
|
if snap^.history[i] <> nil then begin
|
|
kfree(void(snap^.history[i]));
|
|
snap^.history[i] := nil;
|
|
end;
|
|
if snap^.filter_text <> nil then kfree(void(snap^.filter_text));
|
|
kfree(void(snap));
|
|
end;
|
|
|
|
{ ============================================================
|
|
freeState — release all heap-allocated fields and the record
|
|
============================================================ }
|
|
procedure freeState(state: PFileBrowserState);
|
|
var
|
|
i: uint32;
|
|
begin
|
|
if state = nil then exit;
|
|
if state^.cur_path <> nil then kfree(void(state^.cur_path));
|
|
if state^.sel_path <> nil then kfree(void(state^.sel_path));
|
|
{ Free history entries }
|
|
if state^.hist_len > 0 then
|
|
for i := 0 to state^.hist_len - 1 do
|
|
if state^.history[i] <> nil then begin
|
|
kfree(void(state^.history[i]));
|
|
state^.history[i] := nil;
|
|
end;
|
|
{ Free tab snapshots }
|
|
if state^.tab_count > 0 then
|
|
for i := 0 to state^.tab_count - 1 do
|
|
if state^.tabs[i] <> nil then begin
|
|
free_tab_snapshot(state^.tabs[i]);
|
|
state^.tabs[i] := nil;
|
|
end;
|
|
{ Free bookmarks }
|
|
if state^.bm_count > 0 then
|
|
for i := 0 to state^.bm_count - 1 do
|
|
if state^.bookmarks[i] <> nil then begin
|
|
kfree(void(state^.bookmarks[i]));
|
|
state^.bookmarks[i] := nil;
|
|
end;
|
|
if state^.filter_text <> nil then kfree(void(state^.filter_text));
|
|
if state^.ta_timer <> nil then begin
|
|
lv_timer_delete(state^.ta_timer);
|
|
state^.ta_timer := nil;
|
|
end;
|
|
{ Unwatch directory }
|
|
if state^.watch_id <> 0 then begin
|
|
driver.storage.vfs.UnwatchDirectory(state^.watch_id);
|
|
state^.watch_id := 0;
|
|
end;
|
|
if state^.watch_timer <> nil then begin
|
|
lv_timer_delete(state^.watch_timer);
|
|
state^.watch_timer := nil;
|
|
end;
|
|
if state^.refresh_poll <> nil then begin
|
|
lv_timer_delete(state^.refresh_poll);
|
|
state^.refresh_poll := nil;
|
|
end;
|
|
{ Free rd_* buffers }
|
|
if state^.rd_dir_names <> nil then begin
|
|
if state^.rd_dir_count > 0 then
|
|
for i := 0 to state^.rd_dir_count - 1 do
|
|
if state^.rd_dir_names^[i] <> nil then
|
|
kfree(void(state^.rd_dir_names^[i]));
|
|
kfree(void(state^.rd_dir_names));
|
|
end;
|
|
if state^.rd_file_names <> nil then begin
|
|
if state^.rd_file_count > 0 then
|
|
for i := 0 to state^.rd_file_count - 1 do
|
|
if state^.rd_file_names^[i] <> nil then
|
|
kfree(void(state^.rd_file_names^[i]));
|
|
kfree(void(state^.rd_file_names));
|
|
end;
|
|
if state^.rd_file_sizes <> nil then
|
|
kfree(void(state^.rd_file_sizes));
|
|
{ Free worker-op paths }
|
|
if state^.wop_path1 <> nil then kfree(void(state^.wop_path1));
|
|
if state^.wop_path2 <> nil then kfree(void(state^.wop_path2));
|
|
{ Free dispatch path }
|
|
if state^.dispatch_path <> nil then kfree(void(state^.dispatch_path));
|
|
kfree(void(state));
|
|
end;
|
|
|
|
{ ============================================================
|
|
free_children_userdata — kfree stringCopy'd pchar in user_data
|
|
============================================================ }
|
|
procedure free_children_userdata(container: Plv_obj);
|
|
var
|
|
n : uint32;
|
|
i : uint32;
|
|
obj : Plv_obj;
|
|
ud : pchar;
|
|
begin
|
|
if container = nil then exit;
|
|
n := lv_obj_get_child_count(container);
|
|
if n = 0 then exit;
|
|
for i := 0 to n - 1 do begin
|
|
obj := lv_obj_get_child(container, sint32(i));
|
|
if obj = nil then continue;
|
|
ud := pchar(lv_obj_get_user_data(obj));
|
|
if ud <> nil then kfree(void(ud));
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
fb_entry — process entry point (keeps PS listing alive)
|
|
============================================================ }
|
|
procedure fb_entry(ctx: PProcessContext);
|
|
var
|
|
state : PFileBrowserState;
|
|
opKind : uint32;
|
|
err : TError;
|
|
dpath : pchar;
|
|
dpid : uint32;
|
|
begin
|
|
state := PFileBrowserState(ctx^.Local);
|
|
while (ctx^.State <> psFinished) and
|
|
(ctx^.PendingMsg <> smKill) and
|
|
(ctx^.PendingMsg <> smTerminate) do begin
|
|
{ Check if a refresh was requested (volatile read per Lesson #6) }
|
|
if puint32(@state^.refresh_pending)^ = 1 then begin
|
|
puint32(@state^.refresh_pending)^ := 0;
|
|
do_refresh_load(state);
|
|
puint32(@state^.rd_ready)^ := 1;
|
|
end;
|
|
{ Check for worker VFS operations (mkdir, rename) }
|
|
opKind := puint32(@state^.wop_pending)^;
|
|
if opKind <> 0 then begin
|
|
puint32(@state^.wop_pending)^ := 0;
|
|
state^.wop_kind := opKind;
|
|
err := eNone;
|
|
case opKind of
|
|
1: err := driver.storage.vfs.CreateDirectory(
|
|
state^.wop_path1, @err);
|
|
2: err := driver.storage.vfs.RenameFile(
|
|
state^.wop_path1, state^.wop_path2, @err);
|
|
end;
|
|
state^.wop_result := err;
|
|
puint32(@state^.wop_done)^ := 1;
|
|
end;
|
|
{ Check for file dispatch request }
|
|
if puint32(@state^.dispatch_pending)^ = 1 then begin
|
|
puint32(@state^.dispatch_pending)^ := 0;
|
|
dpath := state^.dispatch_path;
|
|
state^.dispatch_path := nil;
|
|
if dpath <> nil then begin
|
|
dpid := driver.storage.filedispatch.dispatch(
|
|
dpath, nil, nil, nil, nil);
|
|
state^.dispatch_result := dpid;
|
|
kfree(void(dpath));
|
|
end else
|
|
state^.dispatch_result := 0;
|
|
puint32(@state^.dispatch_done)^ := 1;
|
|
end;
|
|
proc.mgr.proc_yield;
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
onClose — called by the window manager when X is pressed
|
|
============================================================ }
|
|
procedure onClose(wid: uint32);
|
|
begin
|
|
debug.tracer.push_trace('filebrowser.onClose');
|
|
if g_state <> nil then begin
|
|
{ 1. Kill worker process so it stops touching state }
|
|
if g_state^.pid <> 0 then begin
|
|
proc.mgr.kill(g_state^.pid);
|
|
g_state^.pid := 0;
|
|
end;
|
|
{ 2. Delete LVGL timers early to prevent callbacks during teardown }
|
|
if g_state^.ta_timer <> nil then begin
|
|
lv_timer_delete(g_state^.ta_timer);
|
|
g_state^.ta_timer := nil;
|
|
end;
|
|
if g_state^.watch_timer <> nil then begin
|
|
lv_timer_delete(g_state^.watch_timer);
|
|
g_state^.watch_timer := nil;
|
|
end;
|
|
if g_state^.refresh_poll <> nil then begin
|
|
lv_timer_delete(g_state^.refresh_poll);
|
|
g_state^.refresh_poll := nil;
|
|
end;
|
|
{ 3. Clear selection — nil sel_row before deletion (Lesson #16) }
|
|
clear_selection(g_state);
|
|
{ 4. Destroy context menu (lives on lv_layer_top, not in frame) }
|
|
destroy_context_menu(g_state);
|
|
{ 5. Free user_data strings from LVGL children }
|
|
free_children_userdata(g_state^.breadcrumb);
|
|
free_children_userdata(g_state^.content_area);
|
|
if g_state^.sidebar <> nil then
|
|
free_children_userdata(g_state^.sidebar);
|
|
{ 6. Destroy window — deletes all LVGL widget objects }
|
|
driver.video.windows.destroyWindow(g_win_id);
|
|
g_win_id := 0;
|
|
{ 7. Now free remaining heap data and the state record }
|
|
freeState(g_state);
|
|
g_state := nil;
|
|
end else begin
|
|
driver.video.windows.destroyWindow(g_win_id);
|
|
g_win_id := 0;
|
|
end;
|
|
debug.tracer.pop_trace;
|
|
end;
|
|
|
|
{ ============================================================
|
|
onResize — called when the user resizes the window
|
|
============================================================ }
|
|
procedure onResize(wid: uint32; new_w, new_h: sint32);
|
|
begin
|
|
{ LVGL flex layout handles child resizing automatically. }
|
|
end;
|
|
|
|
{ ============================================================
|
|
History helpers
|
|
============================================================ }
|
|
procedure push_history(state: PFileBrowserState; path: pchar);
|
|
var
|
|
i: sint32;
|
|
begin
|
|
{ Trim forward entries }
|
|
if state^.hist_pos + 1 < state^.hist_len then
|
|
for i := sint32(state^.hist_pos) + 1 to sint32(state^.hist_len) - 1 do
|
|
if state^.history[i] <> nil then begin
|
|
kfree(void(state^.history[i]));
|
|
state^.history[i] := nil;
|
|
end;
|
|
state^.hist_len := state^.hist_pos + 1;
|
|
|
|
{ Drop oldest when full }
|
|
if state^.hist_len = HIST_MAX then begin
|
|
if state^.history[0] <> nil then kfree(void(state^.history[0]));
|
|
for i := 0 to HIST_MAX - 2 do
|
|
state^.history[i] := state^.history[i + 1];
|
|
state^.history[HIST_MAX - 1] := nil;
|
|
state^.hist_len := HIST_MAX - 1;
|
|
state^.hist_pos := HIST_MAX - 2;
|
|
end;
|
|
|
|
state^.history[state^.hist_len] := stringCopy(path);
|
|
state^.hist_len := state^.hist_len + 1;
|
|
state^.hist_pos := state^.hist_len - 1;
|
|
end;
|
|
|
|
procedure update_nav_btns(state: PFileBrowserState);
|
|
begin
|
|
if state^.back_btn <> nil then begin
|
|
if state^.hist_pos > 0 then begin
|
|
lv_obj_add_flag(state^.back_btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_opa(state^.back_btn, LV_OPA_COVER, 0);
|
|
end else begin
|
|
lv_obj_remove_flag(state^.back_btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_opa(state^.back_btn, 80, 0);
|
|
end;
|
|
end;
|
|
if state^.fwd_btn <> nil then begin
|
|
if state^.hist_pos + 1 < state^.hist_len then begin
|
|
lv_obj_add_flag(state^.fwd_btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_opa(state^.fwd_btn, LV_OPA_COVER, 0);
|
|
end else begin
|
|
lv_obj_remove_flag(state^.fwd_btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_opa(state^.fwd_btn, 80, 0);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
navigate_to — central navigation point
|
|
============================================================ }
|
|
procedure navigate_to(state: PFileBrowserState; newDir: pchar);
|
|
begin
|
|
{ Unwatch old directory }
|
|
if state^.watch_id <> 0 then begin
|
|
driver.storage.vfs.UnwatchDirectory(state^.watch_id);
|
|
state^.watch_id := 0;
|
|
end;
|
|
push_history(state, newDir);
|
|
if state^.cur_path <> nil then kfree(void(state^.cur_path));
|
|
state^.cur_path := stringCopy(newDir);
|
|
update_nav_btns(state);
|
|
build_breadcrumbs(state);
|
|
if state^.tab_bar <> nil then rebuild_tab_bar(state);
|
|
{ Watch new directory }
|
|
state^.watch_dirty := false;
|
|
state^.watch_id := driver.storage.vfs.WatchDirectory(newDir, @fb_watch_cb, state);
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
schedule_refresh — signal the fb_entry process to do VFS I/O
|
|
============================================================ }
|
|
procedure schedule_refresh(state: PFileBrowserState);
|
|
begin
|
|
{ Signal the worker process to load data (volatile write) }
|
|
puint32(@state^.refresh_pending)^ := 1;
|
|
end;
|
|
|
|
function fb_error_name(err : TError) : pchar;
|
|
begin
|
|
case err of
|
|
eNone: fb_error_name := 'eNone';
|
|
eUnknown: fb_error_name := 'eUnknown';
|
|
eNotSupported: fb_error_name := 'eNotSupported';
|
|
eOutOfMemory: fb_error_name := 'eOutOfMemory';
|
|
eInvalidArgument: fb_error_name := 'eInvalidArgument';
|
|
eFileInUse: fb_error_name := 'eFileInUse';
|
|
eFileDoesNotExist: fb_error_name := 'eFileDoesNotExist';
|
|
eInvalidFileName: fb_error_name := 'eInvalidFileName';
|
|
eInvalidFileExtension: fb_error_name := 'eInvalidFileExtension';
|
|
eFilenameTooLong: fb_error_name := 'eFilenameTooLong';
|
|
eDirectoryDoesNotExist: fb_error_name := 'eDirectoryDoesNotExist';
|
|
eDirectoryAlreadyExists: fb_error_name := 'eDirectoryAlreadyExists';
|
|
eDirectoryNotEmpty: fb_error_name := 'eDirectoryNotEmpty';
|
|
eDirectoryFull: fb_error_name := 'eDirectoryFull';
|
|
eNotADirectory: fb_error_name := 'eNotADirectory';
|
|
eWriteOnly: fb_error_name := 'eWriteOnly';
|
|
eReadOnly: fb_error_name := 'eReadOnly';
|
|
ePermissionDenied: fb_error_name := 'ePermissionDenied';
|
|
eInvalidPath: fb_error_name := 'eInvalidPath';
|
|
eTooManyOpenFiles: fb_error_name := 'eTooManyOpenFiles';
|
|
eInvalidHandle: fb_error_name := 'eInvalidHandle';
|
|
eFileNotLoaded: fb_error_name := 'eFileNotLoaded';
|
|
eAlreadyExists: fb_error_name := 'eAlreadyExists';
|
|
eDiskFull: fb_error_name := 'eDiskFull';
|
|
eIOError: fb_error_name := 'eIOError';
|
|
eIOTimeout: fb_error_name := 'eIOTimeout';
|
|
eIOCancelled: fb_error_name := 'eIOCancelled';
|
|
eDeviceNotReady: fb_error_name := 'eDeviceNotReady';
|
|
eDeviceRemoved: fb_error_name := 'eDeviceRemoved';
|
|
eDeviceNotFound: fb_error_name := 'eDeviceNotFound';
|
|
eQueueFull: fb_error_name := 'eQueueFull';
|
|
eNoFreeSlot: fb_error_name := 'eNoFreeSlot';
|
|
eCorruptFilesystem: fb_error_name := 'eCorruptFilesystem';
|
|
eBadSector: fb_error_name := 'eBadSector';
|
|
eAlreadyMounted: fb_error_name := 'eAlreadyMounted';
|
|
eNotMounted: fb_error_name := 'eNotMounted';
|
|
eUnsupportedFilesystem: fb_error_name := 'eUnsupportedFilesystem';
|
|
eInvalidPartitionTable: fb_error_name := 'eInvalidPartitionTable';
|
|
eVolumeNotFound: fb_error_name := 'eVolumeNotFound';
|
|
end;
|
|
end;
|
|
|
|
{ fb_refresh_poll_cb — 50ms LVGL poll timer; when rd_ready=1, build UI }
|
|
procedure fb_refresh_poll_cb(tmr: Plv_timer); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
ok_b : Plv_obj;
|
|
begin
|
|
state := PFileBrowserState(lv_timer_get_user_data(tmr));
|
|
if state = nil then exit;
|
|
if puint32(@state^.rd_ready)^ = 1 then begin
|
|
puint32(@state^.rd_ready)^ := 0;
|
|
do_refresh_build(state);
|
|
end;
|
|
{ Check for completed worker VFS operation }
|
|
if puint32(@state^.wop_done)^ = 1 then begin
|
|
puint32(@state^.wop_done)^ := 0;
|
|
if state^.wop_result <> eNone then begin
|
|
mbox := lv_msgbox_create(lv_layer_top);
|
|
lv_msgbox_add_title(mbox, 'Error');
|
|
case state^.wop_kind of
|
|
1: lv_msgbox_add_text(mbox, 'Could not create folder.');
|
|
2: lv_msgbox_add_text(mbox, 'Could not rename item.');
|
|
3: lv_msgbox_add_text(mbox, 'Could not create file.');
|
|
4: lv_msgbox_add_text(mbox, 'Could not delete item.');
|
|
else
|
|
lv_msgbox_add_text(mbox, 'Operation failed.');
|
|
end;
|
|
lv_msgbox_add_text(mbox, fb_error_name(state^.wop_result));
|
|
ok_b := lv_msgbox_add_footer_button(mbox, 'OK');
|
|
lv_obj_add_event_cb(ok_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
|
|
end;
|
|
{ Free op paths }
|
|
if state^.wop_path1 <> nil then begin
|
|
kfree(void(state^.wop_path1));
|
|
state^.wop_path1 := nil;
|
|
end;
|
|
if state^.wop_path2 <> nil then begin
|
|
kfree(void(state^.wop_path2));
|
|
state^.wop_path2 := nil;
|
|
end;
|
|
{ For rename/delete, clear selection }
|
|
if (state^.wop_kind = 2) or (state^.wop_kind = 4) then
|
|
clear_selection(state);
|
|
schedule_refresh(state);
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
fb_collect_cb — hashmap forEach: classify dirs/files
|
|
============================================================ }
|
|
procedure fb_collect_cb(key: pchar; data: void; ud: void);
|
|
var
|
|
ctx : PFBCollectCtx;
|
|
obj : PVFSObject;
|
|
begin
|
|
ctx := PFBCollectCtx(ud);
|
|
obj := PVFSObject(data);
|
|
if (ctx = nil) or (obj = nil) then exit;
|
|
{ Always hide '.' and '..' entries }
|
|
if stringEquals(key, '.') or stringEquals(key, '..') then exit;
|
|
case obj^.ObjectType of
|
|
otVDIRECTORY, otDRIVE, otDIRECTORY, otMOUNT, otSYMLINK:
|
|
if ctx^.dir_count < ENTRY_MAX then begin
|
|
ctx^.dir_names^[ctx^.dir_count] := key;
|
|
ctx^.dir_count := ctx^.dir_count + 1;
|
|
end;
|
|
otFILE, otVFILE:
|
|
if ctx^.file_count < ENTRY_MAX then begin
|
|
ctx^.file_names^[ctx^.file_count] := key;
|
|
ctx^.file_sizes^[ctx^.file_count] := obj^.FileSize;
|
|
ctx^.file_count := ctx^.file_count + 1;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
update_status_bar — show item count and current path
|
|
============================================================ }
|
|
procedure update_status_bar(state: PFileBrowserState);
|
|
var
|
|
s1, s2, s3, s4, s5 : pchar;
|
|
begin
|
|
if state^.status_label = nil then exit;
|
|
s1 := intToString(state^.item_count);
|
|
s2 := stringConcat(s1, ' items');
|
|
kfree(void(s1));
|
|
|
|
{ If something is selected, show its name }
|
|
if state^.sel_path <> nil then begin
|
|
s3 := stringConcat(s2, ' | Sel: ');
|
|
kfree(void(s2));
|
|
s4 := stringConcat(s3, state^.sel_path);
|
|
kfree(void(s3));
|
|
s5 := stringConcat(s4, ' | ');
|
|
kfree(void(s4));
|
|
end else begin
|
|
s5 := stringConcat(s2, ' | ');
|
|
kfree(void(s2));
|
|
end;
|
|
|
|
s3 := stringConcat(s5, state^.cur_path);
|
|
kfree(void(s5));
|
|
lv_label_set_text(state^.status_label, s3);
|
|
kfree(void(s3));
|
|
end;
|
|
|
|
{ ============================================================
|
|
do_refresh_load — VFS I/O + sort (runs in fb_entry process)
|
|
Stores results in state^.rd_* fields.
|
|
============================================================ }
|
|
procedure do_refresh_load(state: PFileBrowserState);
|
|
var
|
|
Map : PHashMap;
|
|
ctx : TFBCollectCtx;
|
|
dir_names : PNameBuf;
|
|
file_names : PNameBuf;
|
|
file_sizes : PSizeBuf;
|
|
dir_count : uint32;
|
|
file_count : uint32;
|
|
shown : uint32;
|
|
i : uint32;
|
|
extStr : pchar;
|
|
sep : pchar;
|
|
s1 : pchar;
|
|
begin
|
|
debug.tracer.push_trace('filebrowser.do_refresh_load');
|
|
|
|
{ Free any previous rd_* buffers }
|
|
if state^.rd_dir_names <> nil then begin
|
|
{ Free stringCopy'd entries }
|
|
if state^.rd_dir_count > 0 then
|
|
for i := 0 to state^.rd_dir_count - 1 do
|
|
if state^.rd_dir_names^[i] <> nil then
|
|
kfree(void(state^.rd_dir_names^[i]));
|
|
kfree(void(state^.rd_dir_names));
|
|
state^.rd_dir_names := nil;
|
|
end;
|
|
if state^.rd_file_names <> nil then begin
|
|
if state^.rd_file_count > 0 then
|
|
for i := 0 to state^.rd_file_count - 1 do
|
|
if state^.rd_file_names^[i] <> nil then
|
|
kfree(void(state^.rd_file_names^[i]));
|
|
kfree(void(state^.rd_file_names));
|
|
state^.rd_file_names := nil;
|
|
end;
|
|
if state^.rd_file_sizes <> nil then begin
|
|
kfree(void(state^.rd_file_sizes));
|
|
state^.rd_file_sizes := nil;
|
|
end;
|
|
state^.rd_dir_count := 0;
|
|
state^.rd_file_count := 0;
|
|
state^.rd_failed := false;
|
|
|
|
{ Heap-alloc name/size buffers (Lesson #11) }
|
|
dir_names := PNameBuf(kalloc(sizeof(TNameBuf)));
|
|
file_names := PNameBuf(kalloc(sizeof(TNameBuf)));
|
|
file_sizes := PSizeBuf(kalloc(sizeof(TSizeBuf)));
|
|
memset(uint32(dir_names), 0, sizeof(TNameBuf));
|
|
memset(uint32(file_names), 0, sizeof(TNameBuf));
|
|
memset(uint32(file_sizes), 0, sizeof(TSizeBuf));
|
|
|
|
{ Load directory from VFS — this may do disk I/O }
|
|
Map := driver.storage.vfs.GetDirectoryListingFrom(state^.cur_path, '/');
|
|
if Map = nil then begin
|
|
state^.rd_failed := true;
|
|
kfree(void(dir_names));
|
|
kfree(void(file_names));
|
|
kfree(void(file_sizes));
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
if Map^.Table = nil then begin
|
|
driver.storage.vfs.FreeDirectoryListing(Map);
|
|
state^.rd_failed := true;
|
|
kfree(void(dir_names));
|
|
kfree(void(file_names));
|
|
kfree(void(file_sizes));
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
{ Collect entries }
|
|
ctx.dir_names := dir_names;
|
|
ctx.file_names := file_names;
|
|
ctx.file_sizes := file_sizes;
|
|
ctx.dir_count := 0;
|
|
ctx.file_count := 0;
|
|
core.ds.hashmap.forEach(Map, @fb_collect_cb, void(@ctx));
|
|
dir_count := ctx.dir_count;
|
|
file_count := ctx.file_count;
|
|
|
|
{ Sort }
|
|
if dir_count > 0 then sortStringArray(@dir_names^[0], dir_count);
|
|
if file_count > 0 then begin
|
|
case state^.sort_col of
|
|
1: begin { Sort by size — bubble sort on sizes, move names along }
|
|
if file_count > 1 then
|
|
for i := 0 to file_count - 2 do
|
|
for shown := 0 to file_count - i - 2 do
|
|
if file_sizes^[shown] > file_sizes^[shown + 1] then begin
|
|
{ swap sizes }
|
|
dir_count := file_sizes^[shown];
|
|
file_sizes^[shown] := file_sizes^[shown + 1];
|
|
file_sizes^[shown + 1] := dir_count;
|
|
{ swap names }
|
|
s1 := file_names^[shown];
|
|
file_names^[shown] := file_names^[shown + 1];
|
|
file_names^[shown + 1] := s1;
|
|
end;
|
|
dir_count := ctx.dir_count; { restore dir_count }
|
|
end;
|
|
2: begin { Sort by extension — sort by extension string }
|
|
sortStringArrayWithData(@file_names^[0], @file_sizes^[0], file_count);
|
|
{ Re-sort by extension: bubble sort }
|
|
if file_count > 1 then
|
|
for i := 0 to file_count - 2 do
|
|
for shown := 0 to file_count - i - 2 do begin
|
|
extStr := getFileExtension(file_names^[shown]);
|
|
sep := getFileExtension(file_names^[shown + 1]);
|
|
if extStr = nil then extStr := '';
|
|
if sep = nil then sep := '';
|
|
if stringCompare(extStr, sep) > 0 then begin
|
|
{ swap names }
|
|
s1 := file_names^[shown];
|
|
file_names^[shown] := file_names^[shown + 1];
|
|
file_names^[shown + 1] := s1;
|
|
{ swap sizes }
|
|
dir_count := file_sizes^[shown];
|
|
file_sizes^[shown] := file_sizes^[shown + 1];
|
|
file_sizes^[shown + 1] := dir_count;
|
|
end;
|
|
end;
|
|
dir_count := ctx.dir_count;
|
|
end;
|
|
else { sort_col=0: default name sort }
|
|
sortStringArrayWithData(@file_names^[0], @file_sizes^[0], file_count);
|
|
end;
|
|
end;
|
|
|
|
{ Reverse if descending (dirs and files separately) }
|
|
if not state^.sort_asc then begin
|
|
if dir_count > 1 then
|
|
for i := 0 to (dir_count div 2) - 1 do begin
|
|
s1 := dir_names^[i];
|
|
dir_names^[i] := dir_names^[dir_count - 1 - i];
|
|
dir_names^[dir_count - 1 - i] := s1;
|
|
end;
|
|
if file_count > 1 then
|
|
for i := 0 to (file_count div 2) - 1 do begin
|
|
s1 := file_names^[i];
|
|
file_names^[i] := file_names^[file_count - 1 - i];
|
|
file_names^[file_count - 1 - i] := s1;
|
|
shown := file_sizes^[i];
|
|
file_sizes^[i] := file_sizes^[file_count - 1 - i];
|
|
file_sizes^[file_count - 1 - i] := shown;
|
|
end;
|
|
end;
|
|
|
|
{ Free the VFS snapshot — we only need name copies now }
|
|
{ But the names in dir_names/file_names point INTO the snapshot!
|
|
We must stringCopy them before freeing. }
|
|
if dir_count > 0 then
|
|
for i := 0 to dir_count - 1 do
|
|
dir_names^[i] := stringCopy(dir_names^[i]);
|
|
if file_count > 0 then
|
|
for i := 0 to file_count - 1 do
|
|
file_names^[i] := stringCopy(file_names^[i]);
|
|
|
|
driver.storage.vfs.FreeDirectoryListing(Map);
|
|
|
|
{ Store sorted results for UI build }
|
|
state^.rd_dir_names := dir_names;
|
|
state^.rd_file_names := file_names;
|
|
state^.rd_file_sizes := file_sizes;
|
|
state^.rd_dir_count := dir_count;
|
|
state^.rd_file_count := file_count;
|
|
|
|
debug.tracer.pop_trace;
|
|
end;
|
|
|
|
{ ============================================================
|
|
do_refresh_build — create LVGL widgets from rd_* data
|
|
(runs in LVGL poll timer — NO disk I/O!)
|
|
============================================================ }
|
|
procedure do_refresh_build(state: PFileBrowserState);
|
|
var
|
|
dir_names : PNameBuf;
|
|
file_names : PNameBuf;
|
|
file_sizes : PSizeBuf;
|
|
dir_count : uint32;
|
|
file_count : uint32;
|
|
shown : uint32;
|
|
has_filter : boolean;
|
|
i : uint32;
|
|
row : Plv_obj;
|
|
icon_lbl : Plv_obj;
|
|
name_lbl : Plv_obj;
|
|
size_lbl : Plv_obj;
|
|
type_lbl : Plv_obj;
|
|
szStr : pchar;
|
|
extStr : pchar;
|
|
s1, s2 : pchar;
|
|
ico_color : uint32;
|
|
begin
|
|
debug.tracer.push_trace('filebrowser.do_refresh_build');
|
|
if state^.content_area = nil then begin
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
{ Release old UI entries — clear_selection BEFORE lv_obj_clean,
|
|
otherwise we write styles to a freed LVGL object (sel_row) }
|
|
clear_selection(state);
|
|
free_children_userdata(state^.content_area);
|
|
lv_obj_clean(state^.content_area);
|
|
|
|
{ Handle failure case }
|
|
if state^.rd_failed then begin
|
|
name_lbl := lv_label_create(state^.content_area);
|
|
lv_label_set_text(name_lbl, 'Could not read directory');
|
|
lv_obj_set_style_text_color(name_lbl, lv_color_make(220, 60, 60), 0);
|
|
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
|
|
state^.item_count := 0;
|
|
update_status_bar(state);
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
dir_names := state^.rd_dir_names;
|
|
file_names := state^.rd_file_names;
|
|
file_sizes := state^.rd_file_sizes;
|
|
dir_count := state^.rd_dir_count;
|
|
file_count := state^.rd_file_count;
|
|
|
|
if (dir_names = nil) and (file_names = nil) then begin
|
|
state^.item_count := 0;
|
|
update_status_bar(state);
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
has_filter := (state^.filter_text <> nil) and (stringSize(state^.filter_text) > 0);
|
|
shown := 0;
|
|
|
|
{ Set layout based on view mode }
|
|
if state^.view_mode = vmCompact then begin
|
|
lv_obj_set_flex_flow(state^.content_area, LV_FLEX_FLOW_ROW_WRAP);
|
|
lv_obj_set_style_pad_row(state^.content_area, 4, 0);
|
|
lv_obj_set_style_pad_column(state^.content_area, 4, 0);
|
|
end else begin
|
|
lv_obj_set_flex_flow(state^.content_area, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_row(state^.content_area, 2, 0);
|
|
end;
|
|
|
|
{ --- Detail header row --- }
|
|
if state^.view_mode = vmDetail then begin
|
|
row := lv_obj_create(state^.content_area);
|
|
lv_obj_remove_style_all(row);
|
|
lv_obj_set_size(row, lv_pct(100), 26);
|
|
lv_obj_set_style_bg_color(row, lv_color_make(28, 31, 42), 0);
|
|
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_pad_left(row, 8, 0);
|
|
lv_obj_set_style_pad_right(row, 8, 0);
|
|
lv_obj_set_style_layout(row, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_border_color(row, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_border_side(row, LV_BORDER_SIDE_BOTTOM, 0);
|
|
lv_obj_set_style_border_width(row, 1, 0);
|
|
|
|
{ Icon column spacer }
|
|
icon_lbl := lv_label_create(row);
|
|
lv_label_set_text(icon_lbl, '');
|
|
lv_obj_set_size(icon_lbl, 36, LV_SIZE_CONTENT);
|
|
|
|
{ Name header }
|
|
name_lbl := lv_label_create(row);
|
|
if (state^.sort_col = 0) and state^.sort_asc then
|
|
lv_label_set_text(name_lbl, 'Name ^')
|
|
else if (state^.sort_col = 0) and (not state^.sort_asc) then
|
|
lv_label_set_text(name_lbl, 'Name v')
|
|
else
|
|
lv_label_set_text(name_lbl, 'Name');
|
|
lv_obj_set_style_text_color(name_lbl, lv_color_make(170, 178, 200), 0);
|
|
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_flex_grow(name_lbl, 1);
|
|
lv_obj_add_flag(name_lbl, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_add_event_cb(name_lbl, @fb_header_name_cb, LV_EVENT_CLICKED, state);
|
|
|
|
{ Size header }
|
|
size_lbl := lv_label_create(row);
|
|
if (state^.sort_col = 1) and state^.sort_asc then
|
|
lv_label_set_text(size_lbl, 'Size ^')
|
|
else if (state^.sort_col = 1) and (not state^.sort_asc) then
|
|
lv_label_set_text(size_lbl, 'Size v')
|
|
else
|
|
lv_label_set_text(size_lbl, 'Size');
|
|
lv_obj_set_style_text_color(size_lbl, lv_color_make(170, 178, 200), 0);
|
|
lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_size(size_lbl, 70, LV_SIZE_CONTENT);
|
|
lv_obj_add_flag(size_lbl, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_add_event_cb(size_lbl, @fb_header_size_cb, LV_EVENT_CLICKED, state);
|
|
|
|
{ Type header }
|
|
type_lbl := lv_label_create(row);
|
|
if (state^.sort_col = 2) and state^.sort_asc then
|
|
lv_label_set_text(type_lbl, 'Type ^')
|
|
else if (state^.sort_col = 2) and (not state^.sort_asc) then
|
|
lv_label_set_text(type_lbl, 'Type v')
|
|
else
|
|
lv_label_set_text(type_lbl, 'Type');
|
|
lv_obj_set_style_text_color(type_lbl, lv_color_make(170, 178, 200), 0);
|
|
lv_obj_set_style_text_font(type_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_size(type_lbl, 60, LV_SIZE_CONTENT);
|
|
lv_obj_add_flag(type_lbl, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_add_event_cb(type_lbl, @fb_header_type_cb, LV_EVENT_CLICKED, state);
|
|
end;
|
|
|
|
{ --- Build directory rows --- }
|
|
if (dir_names <> nil) and (dir_count > 0) then
|
|
for i := 0 to dir_count - 1 do begin
|
|
if dir_names^[i] = nil then continue;
|
|
if (not state^.show_hidden) and (dir_names^[i][0] = '.') then continue;
|
|
if has_filter then
|
|
if not stringContainsCI(dir_names^[i], state^.filter_text) then continue;
|
|
row := lv_obj_create(state^.content_area);
|
|
lv_obj_remove_style_all(row);
|
|
if state^.view_mode = vmCompact then begin
|
|
lv_obj_set_size(row, 120, 34);
|
|
end else begin
|
|
lv_obj_set_size(row, lv_pct(100), 34);
|
|
end;
|
|
lv_obj_set_style_bg_color(row, lv_color_make(38, 42, 56), 0);
|
|
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_bg_color(row, lv_color_make(50, 55, 72), LV_STATE_PRESSED);
|
|
lv_obj_set_style_radius(row, 4, 0);
|
|
lv_obj_set_style_pad_left(row, 8, 0);
|
|
lv_obj_set_style_pad_right(row, 8, 0);
|
|
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_layout(row, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
{ Folder icon — 32px PUA glyph }
|
|
icon_lbl := lv_label_create(row);
|
|
lv_label_set_text(icon_lbl, ICO_FOLDER);
|
|
lv_obj_set_style_text_color(icon_lbl, lv_color_make(255, 210, 60), 0);
|
|
lv_obj_set_style_text_font(icon_lbl, @asuro_icons_32, 0);
|
|
lv_obj_set_size(icon_lbl, 36, 32);
|
|
|
|
{ Folder name }
|
|
name_lbl := lv_label_create(row);
|
|
lv_label_set_text(name_lbl, dir_names^[i]);
|
|
lv_obj_set_style_text_color(name_lbl, lv_color_make(255, 210, 60), 0);
|
|
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_flex_grow(name_lbl, 1);
|
|
if state^.view_mode = vmCompact then
|
|
lv_label_set_long_mode(name_lbl, LV_LABEL_LONG_CLIP);
|
|
|
|
if state^.view_mode = vmDetail then begin
|
|
{ Size column — show DIR }
|
|
size_lbl := lv_label_create(row);
|
|
lv_label_set_text(size_lbl, 'DIR');
|
|
lv_obj_set_style_text_color(size_lbl, lv_color_make(140, 150, 170), 0);
|
|
lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_size(size_lbl, 70, LV_SIZE_CONTENT);
|
|
end;
|
|
|
|
{ Build full path in user_data for navigation }
|
|
if stringEquals(state^.cur_path, '/') then begin
|
|
s1 := stringConcat('/', dir_names^[i]);
|
|
end else begin
|
|
s1 := stringConcat(state^.cur_path, '/');
|
|
s2 := stringConcat(s1, dir_names^[i]);
|
|
kfree(void(s1));
|
|
s1 := s2;
|
|
end;
|
|
lv_obj_set_user_data(row, s1);
|
|
lv_obj_add_flag(row, LV_OBJ_FLAG_USER_1); { mark as directory row }
|
|
lv_obj_add_event_cb(row, @fb_dir_cb, LV_EVENT_CLICKED, g_state);
|
|
lv_obj_add_event_cb(row, @fb_row_long_press_cb, LV_EVENT_LONG_PRESSED, g_state);
|
|
shown := shown + 1;
|
|
end;
|
|
|
|
{ --- Build file rows --- }
|
|
if (file_names <> nil) and (file_count > 0) then
|
|
for i := 0 to file_count - 1 do begin
|
|
if file_names^[i] = nil then continue;
|
|
if (not state^.show_hidden) and (file_names^[i][0] = '.') then continue;
|
|
if has_filter then
|
|
if not stringContainsCI(file_names^[i], state^.filter_text) then continue;
|
|
row := lv_obj_create(state^.content_area);
|
|
lv_obj_remove_style_all(row);
|
|
if state^.view_mode = vmCompact then
|
|
lv_obj_set_size(row, 120, 34)
|
|
else
|
|
lv_obj_set_size(row, lv_pct(100), 34);
|
|
lv_obj_set_style_bg_color(row, lv_color_make(32, 35, 48), 0);
|
|
lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_bg_color(row, lv_color_make(50, 55, 72), LV_STATE_PRESSED);
|
|
lv_obj_set_style_radius(row, 4, 0);
|
|
lv_obj_set_style_pad_left(row, 8, 0);
|
|
lv_obj_set_style_pad_right(row, 8, 0);
|
|
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_layout(row, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
{ File icon — 32px PUA glyph, color by type }
|
|
ico_color := getFileTypeColor(file_names^[i]);
|
|
icon_lbl := lv_label_create(row);
|
|
lv_label_set_text(icon_lbl, getFileTypeIcon(file_names^[i]));
|
|
lv_obj_set_style_text_color(icon_lbl, lv_color_make(
|
|
(ico_color shr 16) and $FF,
|
|
(ico_color shr 8) and $FF,
|
|
ico_color and $FF), 0);
|
|
lv_obj_set_style_text_font(icon_lbl, @asuro_icons_32, 0);
|
|
lv_obj_set_size(icon_lbl, 36, 32);
|
|
|
|
{ File name }
|
|
name_lbl := lv_label_create(row);
|
|
lv_label_set_text(name_lbl, file_names^[i]);
|
|
lv_obj_set_style_text_color(name_lbl, lv_color_make(210, 218, 240), 0);
|
|
lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_flex_grow(name_lbl, 1);
|
|
if state^.view_mode = vmCompact then
|
|
lv_label_set_long_mode(name_lbl, LV_LABEL_LONG_CLIP);
|
|
|
|
if state^.view_mode = vmDetail then begin
|
|
{ Size label }
|
|
szStr := fmtFileSize(file_sizes^[i]);
|
|
size_lbl := lv_label_create(row);
|
|
lv_label_set_text(size_lbl, szStr);
|
|
kfree(void(szStr));
|
|
lv_obj_set_style_text_color(size_lbl, lv_color_make(140, 150, 170), 0);
|
|
lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_size(size_lbl, 70, LV_SIZE_CONTENT);
|
|
|
|
{ Type/extension label }
|
|
extStr := getFileExtension(file_names^[i]);
|
|
type_lbl := lv_label_create(row);
|
|
if extStr <> nil then
|
|
lv_label_set_text(type_lbl, extStr)
|
|
else
|
|
lv_label_set_text(type_lbl, '');
|
|
lv_obj_set_style_text_color(type_lbl, lv_color_make(120, 130, 150), 0);
|
|
lv_obj_set_style_text_font(type_lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_size(type_lbl, 60, LV_SIZE_CONTENT);
|
|
end;
|
|
|
|
{ Full path in user_data }
|
|
if stringEquals(state^.cur_path, '/') then begin
|
|
s1 := stringConcat('/', file_names^[i]);
|
|
end else begin
|
|
s1 := stringConcat(state^.cur_path, '/');
|
|
s2 := stringConcat(s1, file_names^[i]);
|
|
kfree(void(s1));
|
|
s1 := s2;
|
|
end;
|
|
lv_obj_set_user_data(row, s1);
|
|
lv_obj_add_event_cb(row, @fb_file_cb, LV_EVENT_CLICKED, g_state);
|
|
lv_obj_add_event_cb(row, @fb_row_long_press_cb, LV_EVENT_LONG_PRESSED, g_state);
|
|
shown := shown + 1;
|
|
end;
|
|
|
|
state^.item_count := shown;
|
|
update_status_bar(state);
|
|
debug.tracer.pop_trace;
|
|
end;
|
|
|
|
{ ============================================================
|
|
do_refresh — convenience: trigger load+build for callers that
|
|
run in the fb_entry process context (not used from LVGL)
|
|
============================================================ }
|
|
procedure do_refresh(state: PFileBrowserState);
|
|
begin
|
|
do_refresh_load(state);
|
|
do_refresh_build(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Row click callbacks
|
|
============================================================ }
|
|
procedure fb_dir_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
row : Plv_obj;
|
|
path : pchar;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
row := lv_event_get_target_obj(e);
|
|
path := pchar(lv_obj_get_user_data(row));
|
|
if path = nil then exit;
|
|
{ If already selected, navigate into it (double-click effect) }
|
|
if (state^.sel_path <> nil) and stringEquals(state^.sel_path, path) then begin
|
|
navigate_to(state, path);
|
|
exit;
|
|
end;
|
|
{ Otherwise select }
|
|
select_item(state, row, path, true);
|
|
end;
|
|
|
|
procedure fb_file_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
row : Plv_obj;
|
|
path : pchar;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
row := lv_event_get_target_obj(e);
|
|
path := pchar(lv_obj_get_user_data(row));
|
|
if path = nil then exit;
|
|
|
|
{ If clicking the already-selected file, treat as "open" via dispatch.
|
|
Schedule dispatch in the worker process (Lesson #10: no sync disk I/O
|
|
from LVGL callbacks). }
|
|
if (state^.sel_path <> nil) and stringEquals(path, state^.sel_path)
|
|
and (not state^.sel_is_dir) then begin
|
|
if puint32(@state^.dispatch_pending)^ = 0 then begin
|
|
state^.dispatch_path := stringCopy(path);
|
|
puint32(@state^.dispatch_done)^ := 0;
|
|
puint32(@state^.dispatch_pending)^ := 1;
|
|
end;
|
|
exit;
|
|
end;
|
|
|
|
select_item(state, row, path, false);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Navigation button callbacks
|
|
============================================================ }
|
|
procedure fb_back_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if (state = nil) or (state^.hist_pos = 0) then exit;
|
|
state^.hist_pos := state^.hist_pos - 1;
|
|
if state^.cur_path <> nil then kfree(void(state^.cur_path));
|
|
state^.cur_path := stringCopy(state^.history[state^.hist_pos]);
|
|
update_nav_btns(state);
|
|
build_breadcrumbs(state);
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
procedure fb_fwd_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if (state = nil) or (state^.hist_pos + 1 >= state^.hist_len) then exit;
|
|
state^.hist_pos := state^.hist_pos + 1;
|
|
if state^.cur_path <> nil then kfree(void(state^.cur_path));
|
|
state^.cur_path := stringCopy(state^.history[state^.hist_pos]);
|
|
update_nav_btns(state);
|
|
build_breadcrumbs(state);
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
procedure fb_up_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
cur : pchar;
|
|
len : uint32;
|
|
i : sint32;
|
|
parent : pchar;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
cur := state^.cur_path;
|
|
if cur = nil then exit;
|
|
len := stringSize(cur);
|
|
if (len <= 1) then exit; { already at root }
|
|
{ Find last '/' before end }
|
|
i := sint32(len) - 1;
|
|
{ Skip trailing slash }
|
|
if cur[i] = '/' then dec(i);
|
|
while (i > 0) and (cur[i] <> '/') do dec(i);
|
|
if i = 0 then
|
|
parent := stringCopy('/')
|
|
else begin
|
|
parent := pchar(kalloc(uint32(i) + 1));
|
|
memcpy(uint32(cur), uint32(parent), uint32(i));
|
|
parent[i] := char(0);
|
|
end;
|
|
navigate_to(state, parent);
|
|
kfree(void(parent));
|
|
end;
|
|
|
|
procedure fb_refresh_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state <> nil then
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
build_breadcrumbs — parse cur_path into clickable segments
|
|
============================================================ }
|
|
procedure build_breadcrumbs(state: PFileBrowserState);
|
|
var
|
|
bar : Plv_obj;
|
|
btn : Plv_obj;
|
|
lbl : Plv_obj;
|
|
sep : Plv_obj;
|
|
path : pchar;
|
|
len : uint32;
|
|
segStart : uint32;
|
|
segEnd : uint32;
|
|
prefix : pchar;
|
|
segBuf : pchar;
|
|
n : uint32;
|
|
begin
|
|
bar := state^.breadcrumb;
|
|
if bar = nil then exit;
|
|
|
|
{ Free old crumb user_data (path prefixes) }
|
|
n := lv_obj_get_child_count(bar);
|
|
if n > 0 then begin
|
|
free_children_userdata(bar);
|
|
end;
|
|
lv_obj_clean(bar);
|
|
|
|
{ Hide textarea if visible }
|
|
if state^.path_ta <> nil then begin
|
|
lv_obj_add_flag(state^.path_ta, LV_OBJ_FLAG_HIDDEN);
|
|
end;
|
|
lv_obj_remove_flag(bar, LV_OBJ_FLAG_HIDDEN);
|
|
state^.path_editing := false;
|
|
|
|
path := state^.cur_path;
|
|
if path = nil then exit;
|
|
len := stringSize(path);
|
|
|
|
{ Root "/" button }
|
|
btn := lv_button_create(bar);
|
|
lv_obj_remove_style_all(btn);
|
|
lv_obj_set_size(btn, LV_SIZE_CONTENT, 22);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(70, 78, 100), LV_STATE_PRESSED);
|
|
lv_obj_set_style_radius(btn, 3, 0);
|
|
lv_obj_set_style_pad_left(btn, 6, 0);
|
|
lv_obj_set_style_pad_right(btn, 6, 0);
|
|
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lbl := lv_label_create(btn);
|
|
lv_label_set_text(lbl, '/');
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(210, 215, 230), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_center(lbl);
|
|
lv_obj_set_user_data(btn, stringCopy('/'));
|
|
lv_obj_add_event_cb(btn, @fb_crumb_cb, LV_EVENT_CLICKED, g_state);
|
|
|
|
{ Parse remaining segments }
|
|
if len <= 1 then exit; { root only }
|
|
segStart := 1; { skip leading '/' }
|
|
while segStart < len do begin
|
|
{ Find end of segment }
|
|
segEnd := segStart;
|
|
while (segEnd < len) and (path[segEnd] <> '/') do
|
|
segEnd := segEnd + 1;
|
|
if segEnd = segStart then begin
|
|
segStart := segEnd + 1;
|
|
continue;
|
|
end;
|
|
|
|
{ Separator label }
|
|
sep := lv_label_create(bar);
|
|
lv_label_set_text(sep, '>');
|
|
lv_obj_set_style_text_color(sep, lv_color_make(100, 108, 130), 0);
|
|
lv_obj_set_style_text_font(sep, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_user_data(sep, nil);
|
|
|
|
{ Segment button }
|
|
btn := lv_button_create(bar);
|
|
lv_obj_remove_style_all(btn);
|
|
lv_obj_set_size(btn, LV_SIZE_CONTENT, 22);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(70, 78, 100), LV_STATE_PRESSED);
|
|
lv_obj_set_style_radius(btn, 3, 0);
|
|
lv_obj_set_style_pad_left(btn, 6, 0);
|
|
lv_obj_set_style_pad_right(btn, 6, 0);
|
|
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
|
|
|
{ Segment name label }
|
|
segBuf := pchar(kalloc(segEnd - segStart + 1));
|
|
memcpy(uint32(@path[segStart]), uint32(segBuf), segEnd - segStart);
|
|
segBuf[segEnd - segStart] := char(0);
|
|
lbl := lv_label_create(btn);
|
|
lv_label_set_text(lbl, segBuf);
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(210, 215, 230), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_center(lbl);
|
|
kfree(void(segBuf));
|
|
|
|
{ Build prefix path for this segment }
|
|
prefix := pchar(kalloc(segEnd + 1));
|
|
memcpy(uint32(path), uint32(prefix), segEnd);
|
|
prefix[segEnd] := char(0);
|
|
lv_obj_set_user_data(btn, prefix);
|
|
lv_obj_add_event_cb(btn, @fb_crumb_cb, LV_EVENT_CLICKED, g_state);
|
|
|
|
segStart := segEnd + 1;
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
fb_crumb_cb — breadcrumb segment clicked → navigate
|
|
============================================================ }
|
|
procedure fb_crumb_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
btn : Plv_obj;
|
|
path : pchar;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
btn := lv_event_get_target_obj(e);
|
|
path := pchar(lv_obj_get_user_data(btn));
|
|
if path = nil then exit;
|
|
navigate_to(state, path);
|
|
end;
|
|
|
|
{ ============================================================
|
|
fb_crumb_area_cb — click the breadcrumb area background
|
|
to switch to path text editing mode
|
|
============================================================ }
|
|
procedure fb_crumb_area_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
target : Plv_obj;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
{ Only activate on direct click on the breadcrumb bar itself }
|
|
target := lv_event_get_target_obj(e);
|
|
if target <> state^.breadcrumb then exit;
|
|
if state^.path_editing then exit;
|
|
|
|
state^.path_editing := true;
|
|
{ Hide breadcrumb buttons, show textarea }
|
|
lv_obj_add_flag(state^.breadcrumb, LV_OBJ_FLAG_HIDDEN);
|
|
if state^.path_ta <> nil then begin
|
|
lv_textarea_set_text(state^.path_ta, state^.cur_path);
|
|
lv_obj_remove_flag(state^.path_ta, LV_OBJ_FLAG_HIDDEN);
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
fb_path_ta_cb — Enter navigates, Escape reverts to breadcrumbs
|
|
============================================================ }
|
|
procedure fb_path_ta_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
key : uint32;
|
|
txt : pchar;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_KEY then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
key := lv_event_get_key(e);
|
|
if key = LV_KEY_ENTER then begin
|
|
txt := lv_textarea_get_text(state^.path_ta);
|
|
if txt <> nil then
|
|
navigate_to(state, txt);
|
|
end
|
|
else if key = LV_KEY_ESC then begin
|
|
{ Revert to breadcrumbs }
|
|
build_breadcrumbs(state);
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
Selection helpers
|
|
============================================================ }
|
|
procedure clear_selection(state: PFileBrowserState);
|
|
begin
|
|
if state^.sel_row <> nil then begin
|
|
{ Reset row background to its default colour }
|
|
if state^.sel_is_dir then
|
|
lv_obj_set_style_bg_color(state^.sel_row,
|
|
lv_color_make(38, 42, 56), 0)
|
|
else
|
|
lv_obj_set_style_bg_color(state^.sel_row,
|
|
lv_color_make(32, 35, 48), 0);
|
|
state^.sel_row := nil;
|
|
end;
|
|
if state^.sel_path <> nil then begin
|
|
kfree(void(state^.sel_path));
|
|
state^.sel_path := nil;
|
|
end;
|
|
end;
|
|
|
|
procedure select_item(state: PFileBrowserState; row: Plv_obj;
|
|
path: pchar; isDir: boolean);
|
|
begin
|
|
clear_selection(state);
|
|
state^.sel_row := row;
|
|
state^.sel_path := stringCopy(path);
|
|
state^.sel_is_dir := isDir;
|
|
{ Highlight with selection colour }
|
|
lv_obj_set_style_bg_color(row, lv_color_make(55, 75, 120), 0);
|
|
{ Update preview panel if visible }
|
|
if state^.preview_visible then
|
|
update_preview(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
fb_mbox_close_cb — generic: close the parent msgbox
|
|
============================================================ }
|
|
procedure fb_mbox_close_cb(e: Plv_event); cdecl;
|
|
var
|
|
mbox : Plv_obj;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
mbox := Plv_obj(lv_event_get_user_data(e));
|
|
if mbox <> nil then lv_msgbox_close_async(mbox);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 6a — Delete
|
|
============================================================ }
|
|
|
|
{ Callback from toolbar Delete button }
|
|
procedure fb_delete_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
yes_b : Plv_obj;
|
|
no_b : Plv_obj;
|
|
s1 : pchar;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if (state = nil) or (state^.sel_path = nil) then exit;
|
|
|
|
{ Confirmation msgbox }
|
|
mbox := lv_msgbox_create(lv_layer_top);
|
|
lv_msgbox_add_title(mbox, 'Delete');
|
|
s1 := stringConcat('Delete "', state^.sel_path);
|
|
lv_msgbox_add_text(mbox, s1);
|
|
kfree(void(s1));
|
|
lv_msgbox_add_text(mbox, '"?');
|
|
yes_b := lv_msgbox_add_footer_button(mbox, 'Delete');
|
|
no_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
|
|
lv_obj_add_event_cb(yes_b, @fb_delete_confirm_cb, LV_EVENT_CLICKED, state);
|
|
lv_obj_add_event_cb(no_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
|
|
end;
|
|
|
|
{ User confirmed deletion }
|
|
procedure fb_delete_confirm_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if (state = nil) or (state^.sel_path = nil) then exit;
|
|
|
|
{ Close the confirmation dialog }
|
|
mbox := lv_obj_get_parent(lv_event_get_target_obj(e));
|
|
if mbox <> nil then begin
|
|
mbox := lv_obj_get_parent(mbox); { footer → msgbox }
|
|
lv_msgbox_close_async(mbox);
|
|
end;
|
|
|
|
{ Start async delete }
|
|
state^.op_err := eNone;
|
|
if state^.sel_is_dir then
|
|
driver.storage.vfs.DeleteDirectoryAsync(
|
|
state^.sel_path, @state^.op_err,
|
|
@fb_delete_done, state)
|
|
else
|
|
driver.storage.vfs.DeleteFileAsync(
|
|
state^.sel_path, @state^.op_err,
|
|
@fb_delete_done, state);
|
|
end;
|
|
|
|
{ Async completion callback — runs outside the LVGL thread, so only set flags. }
|
|
procedure fb_delete_done(error: TError; userdata: pointer);
|
|
var
|
|
state: PFileBrowserState;
|
|
begin
|
|
state := PFileBrowserState(userdata);
|
|
if state = nil then exit;
|
|
state^.wop_kind := 4;
|
|
state^.wop_result := error;
|
|
puint32(@state^.wop_done)^ := 1;
|
|
end;
|
|
|
|
{ Deferred UI update after delete }
|
|
procedure fb_delete_done_timer(tmr: Plv_timer); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
ok_b : Plv_obj;
|
|
begin
|
|
state := PFileBrowserState(lv_timer_get_user_data(tmr));
|
|
lv_timer_delete(tmr);
|
|
if state = nil then exit;
|
|
if state^.op_err <> eNone then begin
|
|
{ Show error }
|
|
mbox := lv_msgbox_create(lv_layer_top);
|
|
lv_msgbox_add_title(mbox, 'Error');
|
|
lv_msgbox_add_text(mbox, 'Could not delete item.');
|
|
ok_b := lv_msgbox_add_footer_button(mbox, 'OK');
|
|
lv_obj_add_event_cb(ok_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
|
|
end;
|
|
clear_selection(state);
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 6b — Create Folder
|
|
============================================================ }
|
|
|
|
|
|
|
|
procedure fb_newfolder_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
cont : Plv_obj;
|
|
ta : Plv_obj;
|
|
ok_b : Plv_obj;
|
|
cancel_b : Plv_obj;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
|
|
mbox := lv_msgbox_create(lv_layer_top);
|
|
lv_msgbox_add_title(mbox, 'New Folder');
|
|
cont := lv_msgbox_get_content(mbox);
|
|
ta := lv_textarea_create(cont);
|
|
lv_textarea_set_one_line(ta, true);
|
|
lv_textarea_set_text(ta, '');
|
|
lv_obj_set_width(ta, lv_pct(100));
|
|
lv_obj_set_user_data(mbox, ta); { stash textarea ptr in msgbox }
|
|
ok_b := lv_msgbox_add_footer_button(mbox, 'Create');
|
|
cancel_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
|
|
lv_obj_add_event_cb(ok_b, @fb_newfolder_ok_cb, LV_EVENT_CLICKED, state);
|
|
lv_obj_set_user_data(ok_b, mbox); { stash msgbox in OK button }
|
|
lv_obj_add_event_cb(cancel_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
|
|
end;
|
|
|
|
procedure fb_newfolder_ok_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
ta : Plv_obj;
|
|
name : pchar;
|
|
fp : pchar;
|
|
sep : pchar;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
mbox := Plv_obj(lv_obj_get_user_data(lv_event_get_target_obj(e)));
|
|
if mbox = nil then exit;
|
|
ta := Plv_obj(lv_obj_get_user_data(mbox));
|
|
if ta = nil then exit;
|
|
name := lv_textarea_get_text(ta);
|
|
if (name = nil) or (stringSize(name) = 0) then exit;
|
|
|
|
{ Build full path }
|
|
if stringEquals(state^.cur_path, '/') then
|
|
fp := stringConcat('/', name)
|
|
else begin
|
|
sep := stringConcat(state^.cur_path, '/');
|
|
fp := stringConcat(sep, name);
|
|
kfree(void(sep));
|
|
end;
|
|
|
|
{ Close dialog }
|
|
lv_msgbox_close_async(mbox);
|
|
|
|
{ Signal worker process to create the directory (no disk I/O in LVGL) }
|
|
if state^.wop_path1 <> nil then kfree(void(state^.wop_path1));
|
|
state^.wop_path1 := fp;
|
|
state^.wop_path2 := nil;
|
|
state^.wop_result := eNone;
|
|
puint32(@state^.wop_pending)^ := 1; { 1 = mkdir }
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 6c — Rename
|
|
============================================================ }
|
|
|
|
|
|
|
|
procedure fb_rename_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
cont : Plv_obj;
|
|
ta : Plv_obj;
|
|
ok_b : Plv_obj;
|
|
cancel_b : Plv_obj;
|
|
basename : pchar;
|
|
len : uint32;
|
|
i : sint32;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if (state = nil) or (state^.sel_path = nil) then exit;
|
|
|
|
{ Extract basename from sel_path }
|
|
len := stringSize(state^.sel_path);
|
|
i := sint32(len) - 1;
|
|
while (i > 0) and (state^.sel_path[i] <> '/') do dec(i);
|
|
basename := @state^.sel_path[i + 1];
|
|
|
|
mbox := lv_msgbox_create(lv_layer_top);
|
|
lv_msgbox_add_title(mbox, 'Rename');
|
|
cont := lv_msgbox_get_content(mbox);
|
|
ta := lv_textarea_create(cont);
|
|
lv_textarea_set_one_line(ta, true);
|
|
lv_textarea_set_text(ta, basename);
|
|
lv_obj_set_width(ta, lv_pct(100));
|
|
lv_obj_set_user_data(mbox, ta);
|
|
ok_b := lv_msgbox_add_footer_button(mbox, 'Rename');
|
|
cancel_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
|
|
lv_obj_add_event_cb(ok_b, @fb_rename_ok_cb, LV_EVENT_CLICKED, state);
|
|
lv_obj_set_user_data(ok_b, mbox);
|
|
lv_obj_add_event_cb(cancel_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
|
|
end;
|
|
|
|
procedure fb_rename_ok_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
ta : Plv_obj;
|
|
name : pchar;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if (state = nil) or (state^.sel_path = nil) then exit;
|
|
mbox := Plv_obj(lv_obj_get_user_data(lv_event_get_target_obj(e)));
|
|
if mbox = nil then exit;
|
|
ta := Plv_obj(lv_obj_get_user_data(mbox));
|
|
if ta = nil then exit;
|
|
name := lv_textarea_get_text(ta);
|
|
if (name = nil) or (stringSize(name) = 0) then exit;
|
|
|
|
lv_msgbox_close_async(mbox);
|
|
|
|
{ Signal worker process to rename (no disk I/O in LVGL) }
|
|
if state^.wop_path1 <> nil then kfree(void(state^.wop_path1));
|
|
if state^.wop_path2 <> nil then kfree(void(state^.wop_path2));
|
|
state^.wop_path1 := stringCopy(state^.sel_path);
|
|
state^.wop_path2 := stringCopy(name);
|
|
state^.wop_result := eNone;
|
|
puint32(@state^.wop_pending)^ := 2; { 2 = rename }
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 6d — Create File
|
|
============================================================ }
|
|
|
|
type
|
|
PNewFileCtx = ^TNewFileCtx;
|
|
TNewFileCtx = record
|
|
state : PFileBrowserState;
|
|
handle : TFileHandle;
|
|
err : TError;
|
|
end;
|
|
|
|
procedure fb_newfile_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
cont : Plv_obj;
|
|
ta : Plv_obj;
|
|
ok_b : Plv_obj;
|
|
cancel_b : Plv_obj;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
|
|
mbox := lv_msgbox_create(lv_layer_top);
|
|
lv_msgbox_add_title(mbox, 'New File');
|
|
cont := lv_msgbox_get_content(mbox);
|
|
ta := lv_textarea_create(cont);
|
|
lv_textarea_set_one_line(ta, true);
|
|
lv_textarea_set_text(ta, '');
|
|
lv_obj_set_width(ta, lv_pct(100));
|
|
lv_obj_set_user_data(mbox, ta);
|
|
ok_b := lv_msgbox_add_footer_button(mbox, 'Create');
|
|
cancel_b := lv_msgbox_add_footer_button(mbox, 'Cancel');
|
|
lv_obj_add_event_cb(ok_b, @fb_newfile_ok_cb, LV_EVENT_CLICKED, state);
|
|
lv_obj_set_user_data(ok_b, mbox);
|
|
lv_obj_add_event_cb(cancel_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
|
|
end;
|
|
|
|
procedure fb_newfile_ok_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
mbox : Plv_obj;
|
|
ta : Plv_obj;
|
|
name : pchar;
|
|
fp : pchar;
|
|
sep : pchar;
|
|
nfctx : PNewFileCtx;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
mbox := Plv_obj(lv_obj_get_user_data(lv_event_get_target_obj(e)));
|
|
if mbox = nil then exit;
|
|
ta := Plv_obj(lv_obj_get_user_data(mbox));
|
|
if ta = nil then exit;
|
|
name := lv_textarea_get_text(ta);
|
|
if (name = nil) or (stringSize(name) = 0) then exit;
|
|
|
|
{ Build full path }
|
|
if stringEquals(state^.cur_path, '/') then
|
|
fp := stringConcat('/', name)
|
|
else begin
|
|
sep := stringConcat(state^.cur_path, '/');
|
|
fp := stringConcat(sep, name);
|
|
kfree(void(sep));
|
|
end;
|
|
|
|
lv_msgbox_close_async(mbox);
|
|
|
|
{ Use async open to create the file }
|
|
nfctx := PNewFileCtx(kalloc(sizeof(TNewFileCtx)));
|
|
nfctx^.state := state;
|
|
nfctx^.err := eNone;
|
|
nfctx^.handle := 0;
|
|
driver.storage.vfs.OpenFileAsync(fp, omCreate, nfctx^.handle,
|
|
@nfctx^.err, @fb_newfile_opened, nfctx);
|
|
kfree(void(fp));
|
|
end;
|
|
|
|
{ Async step 1: file descriptor opened — now write 0 bytes to create dir entry }
|
|
procedure fb_newfile_opened(error: TError; userdata: pointer);
|
|
var
|
|
nfctx : PNewFileCtx;
|
|
begin
|
|
nfctx := PNewFileCtx(userdata);
|
|
if nfctx = nil then exit;
|
|
nfctx^.err := error;
|
|
if (error = eNone) and (nfctx^.handle <> 0) then begin
|
|
{ WriteFileAsync needs a non-nil buffer; Length=0 so it is never read }
|
|
driver.storage.vfs.WriteFileAsync(nfctx^.handle, 0,
|
|
puint8(@nfctx^.err), 0, nil, @fb_newfile_written, nfctx);
|
|
end else begin
|
|
if nfctx^.handle <> 0 then
|
|
driver.storage.vfs.CloseFile(nfctx^.handle);
|
|
if nfctx^.state <> nil then begin
|
|
nfctx^.state^.wop_kind := 3;
|
|
nfctx^.state^.wop_result := nfctx^.err;
|
|
puint32(@nfctx^.state^.wop_done)^ := 1;
|
|
end;
|
|
kfree(void(nfctx));
|
|
end;
|
|
end;
|
|
|
|
{ Async step 2: write complete — close handle and defer UI work via poll flags }
|
|
procedure fb_newfile_written(error: TError; userdata: pointer);
|
|
var
|
|
nfctx : PNewFileCtx;
|
|
begin
|
|
nfctx := PNewFileCtx(userdata);
|
|
if nfctx = nil then exit;
|
|
if error <> eNone then nfctx^.err := error;
|
|
if nfctx^.handle <> 0 then
|
|
driver.storage.vfs.CloseFile(nfctx^.handle);
|
|
if nfctx^.state <> nil then begin
|
|
nfctx^.state^.wop_kind := 3;
|
|
nfctx^.state^.wop_result := nfctx^.err;
|
|
puint32(@nfctx^.state^.wop_done)^ := 1;
|
|
end;
|
|
kfree(void(nfctx));
|
|
end;
|
|
|
|
procedure fb_newfile_done_timer(tmr: Plv_timer); cdecl;
|
|
var
|
|
nfctx : PNewFileCtx;
|
|
mbox : Plv_obj;
|
|
ok_b : Plv_obj;
|
|
begin
|
|
nfctx := PNewFileCtx(lv_timer_get_user_data(tmr));
|
|
lv_timer_delete(tmr);
|
|
if nfctx = nil then exit;
|
|
|
|
if nfctx^.err <> eNone then begin
|
|
mbox := lv_msgbox_create(lv_layer_top);
|
|
lv_msgbox_add_title(mbox, 'Error');
|
|
lv_msgbox_add_text(mbox, 'Could not create file.');
|
|
ok_b := lv_msgbox_add_footer_button(mbox, 'OK');
|
|
lv_obj_add_event_cb(ok_b, @fb_mbox_close_cb, LV_EVENT_CLICKED, mbox);
|
|
end;
|
|
|
|
if nfctx^.state <> nil then
|
|
schedule_refresh(nfctx^.state);
|
|
kfree(void(nfctx));
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 8 — Tabs
|
|
============================================================ }
|
|
|
|
{ save_tab — snapshot current live state into tabs[idx] }
|
|
procedure save_tab(state: PFileBrowserState; idx: uint32);
|
|
var
|
|
snap : PTabSnapshot;
|
|
i : uint32;
|
|
begin
|
|
if idx >= MAX_TABS then exit;
|
|
{ Free old snapshot if any }
|
|
if state^.tabs[idx] <> nil then
|
|
free_tab_snapshot(state^.tabs[idx]);
|
|
snap := PTabSnapshot(kalloc(sizeof(TTabSnapshot)));
|
|
memset(uint32(snap), 0, sizeof(TTabSnapshot));
|
|
if state^.cur_path <> nil then
|
|
snap^.cur_path := stringCopy(state^.cur_path)
|
|
else
|
|
snap^.cur_path := stringCopy('/');
|
|
snap^.hist_len := state^.hist_len;
|
|
snap^.hist_pos := state^.hist_pos;
|
|
snap^.item_count := state^.item_count;
|
|
if snap^.hist_len > 0 then
|
|
for i := 0 to snap^.hist_len - 1 do
|
|
if state^.history[i] <> nil then
|
|
snap^.history[i] := stringCopy(state^.history[i])
|
|
else
|
|
snap^.history[i] := nil;
|
|
if state^.sel_path <> nil then
|
|
snap^.sel_path := stringCopy(state^.sel_path)
|
|
else
|
|
snap^.sel_path := nil;
|
|
snap^.sel_is_dir := state^.sel_is_dir;
|
|
if state^.filter_text <> nil then
|
|
snap^.filter_text := stringCopy(state^.filter_text)
|
|
else
|
|
snap^.filter_text := nil;
|
|
state^.tabs[idx] := snap;
|
|
end;
|
|
|
|
{ restore_tab — load tabs[idx] into the live state (clears old live state) }
|
|
procedure restore_tab(state: PFileBrowserState; idx: uint32);
|
|
var
|
|
snap : PTabSnapshot;
|
|
i : uint32;
|
|
begin
|
|
if idx >= MAX_TABS then exit;
|
|
snap := state^.tabs[idx];
|
|
if snap = nil then exit;
|
|
|
|
{ Free current live history }
|
|
if state^.hist_len > 0 then
|
|
for i := 0 to state^.hist_len - 1 do
|
|
if state^.history[i] <> nil then begin
|
|
kfree(void(state^.history[i]));
|
|
state^.history[i] := nil;
|
|
end;
|
|
if state^.cur_path <> nil then kfree(void(state^.cur_path));
|
|
if state^.sel_path <> nil then begin
|
|
kfree(void(state^.sel_path));
|
|
state^.sel_path := nil;
|
|
end;
|
|
state^.sel_row := nil;
|
|
|
|
{ Restore from snapshot }
|
|
if snap^.cur_path <> nil then
|
|
state^.cur_path := stringCopy(snap^.cur_path)
|
|
else
|
|
state^.cur_path := stringCopy('/');
|
|
state^.hist_len := snap^.hist_len;
|
|
state^.hist_pos := snap^.hist_pos;
|
|
state^.item_count := snap^.item_count;
|
|
if snap^.hist_len > 0 then
|
|
for i := 0 to snap^.hist_len - 1 do
|
|
if snap^.history[i] <> nil then
|
|
state^.history[i] := stringCopy(snap^.history[i])
|
|
else
|
|
state^.history[i] := nil;
|
|
if snap^.sel_path <> nil then
|
|
state^.sel_path := stringCopy(snap^.sel_path)
|
|
else
|
|
state^.sel_path := nil;
|
|
state^.sel_is_dir := snap^.sel_is_dir;
|
|
{ Restore filter text }
|
|
if state^.filter_text <> nil then begin
|
|
kfree(void(state^.filter_text));
|
|
state^.filter_text := nil;
|
|
end;
|
|
if snap^.filter_text <> nil then
|
|
state^.filter_text := stringCopy(snap^.filter_text);
|
|
{ Update filter textarea if present }
|
|
if state^.filter_ta <> nil then begin
|
|
if state^.filter_text <> nil then
|
|
lv_textarea_set_text(state^.filter_ta, state^.filter_text)
|
|
else
|
|
lv_textarea_set_text(state^.filter_ta, '');
|
|
end;
|
|
end;
|
|
|
|
{ switch_tab — save current, restore target, refresh UI }
|
|
procedure switch_tab(state: PFileBrowserState; idx: uint32);
|
|
begin
|
|
if idx >= state^.tab_count then exit;
|
|
if idx = state^.active_tab then exit;
|
|
{ Save current tab state }
|
|
save_tab(state, state^.active_tab);
|
|
{ Restore target tab }
|
|
restore_tab(state, idx);
|
|
state^.active_tab := idx;
|
|
update_nav_btns(state);
|
|
build_breadcrumbs(state);
|
|
rebuild_tab_bar(state);
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ add_tab — create a new tab with given path }
|
|
procedure add_tab(state: PFileBrowserState; path: pchar);
|
|
var
|
|
snap : PTabSnapshot;
|
|
begin
|
|
if state^.tab_count >= MAX_TABS then exit;
|
|
{ Save current tab first }
|
|
save_tab(state, state^.active_tab);
|
|
{ Create snapshot for new tab }
|
|
snap := PTabSnapshot(kalloc(sizeof(TTabSnapshot)));
|
|
memset(uint32(snap), 0, sizeof(TTabSnapshot));
|
|
snap^.cur_path := stringCopy(path);
|
|
snap^.hist_len := 0;
|
|
snap^.hist_pos := 0;
|
|
state^.tabs[state^.tab_count] := snap;
|
|
state^.active_tab := state^.tab_count;
|
|
state^.tab_count := state^.tab_count + 1;
|
|
{ Restore it into live state }
|
|
restore_tab(state, state^.active_tab);
|
|
{ Navigate (pushes history) }
|
|
navigate_to(state, path);
|
|
rebuild_tab_bar(state);
|
|
end;
|
|
|
|
{ close_tab — close tab at idx, switch to neighbour }
|
|
procedure close_tab(state: PFileBrowserState; idx: uint32);
|
|
var
|
|
i : uint32;
|
|
newIdx: uint32;
|
|
begin
|
|
if state^.tab_count <= 1 then exit; { must keep >=1 tab }
|
|
if idx >= state^.tab_count then exit;
|
|
|
|
{ If closing the active tab, save first so we don't lose data }
|
|
if idx = state^.active_tab then begin
|
|
{ Don't save — we're discarding it }
|
|
end else begin
|
|
save_tab(state, state^.active_tab);
|
|
end;
|
|
|
|
{ Free the snapshot }
|
|
if state^.tabs[idx] <> nil then begin
|
|
free_tab_snapshot(state^.tabs[idx]);
|
|
state^.tabs[idx] := nil;
|
|
end;
|
|
|
|
{ Shift tabs down }
|
|
if idx < state^.tab_count - 1 then
|
|
for i := idx to state^.tab_count - 2 do begin
|
|
state^.tabs[i] := state^.tabs[i + 1];
|
|
state^.tab_btns[i] := state^.tab_btns[i + 1];
|
|
end;
|
|
state^.tabs[state^.tab_count - 1] := nil;
|
|
state^.tab_btns[state^.tab_count - 1] := nil;
|
|
state^.tab_count := state^.tab_count - 1;
|
|
|
|
{ Determine new active index }
|
|
if idx = state^.active_tab then begin
|
|
if idx >= state^.tab_count then
|
|
newIdx := state^.tab_count - 1
|
|
else
|
|
newIdx := idx;
|
|
state^.active_tab := newIdx;
|
|
restore_tab(state, newIdx);
|
|
update_nav_btns(state);
|
|
build_breadcrumbs(state);
|
|
schedule_refresh(state);
|
|
end else begin
|
|
{ Active tab index may have shifted }
|
|
if state^.active_tab > idx then
|
|
state^.active_tab := state^.active_tab - 1;
|
|
restore_tab(state, state^.active_tab);
|
|
end;
|
|
rebuild_tab_bar(state);
|
|
end;
|
|
|
|
{ rebuild_tab_bar — recreate tab buttons to reflect current state }
|
|
procedure rebuild_tab_bar(state: PFileBrowserState);
|
|
var
|
|
bar : Plv_obj;
|
|
btn : Plv_obj;
|
|
lbl : Plv_obj;
|
|
close_b : Plv_obj;
|
|
plus_b : Plv_obj;
|
|
i : uint32;
|
|
snap : PTabSnapshot;
|
|
name : pchar;
|
|
len : uint32;
|
|
j : sint32;
|
|
begin
|
|
bar := state^.tab_bar;
|
|
if bar = nil then exit;
|
|
lv_obj_clean(bar);
|
|
|
|
if state^.tab_count > 0 then
|
|
for i := 0 to state^.tab_count - 1 do begin
|
|
{ For active tab, use live state; for others, snapshot }
|
|
if i = state^.active_tab then
|
|
name := state^.cur_path
|
|
else begin
|
|
snap := state^.tabs[i];
|
|
if (snap <> nil) and (snap^.cur_path <> nil) then
|
|
name := snap^.cur_path
|
|
else
|
|
name := '/';
|
|
end;
|
|
|
|
{ Extract last segment for display }
|
|
len := stringSize(name);
|
|
if len > 1 then begin
|
|
j := sint32(len) - 1;
|
|
if name[j] = '/' then dec(j);
|
|
while (j > 0) and (name[j] <> '/') do dec(j);
|
|
name := @name[j + 1];
|
|
end;
|
|
|
|
btn := lv_button_create(bar);
|
|
lv_obj_remove_style_all(btn);
|
|
lv_obj_set_size(btn, LV_SIZE_CONTENT, 22);
|
|
lv_obj_set_style_pad_left(btn, 8, 0);
|
|
lv_obj_set_style_pad_right(btn, 4, 0);
|
|
lv_obj_set_style_radius(btn, 4, 0);
|
|
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_layout(btn, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_column(btn, 4, 0);
|
|
|
|
if i = state^.active_tab then begin
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
|
end else begin
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(35, 38, 50), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
|
end;
|
|
|
|
lbl := lv_label_create(btn);
|
|
lv_label_set_text(lbl, name);
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(190, 198, 220), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
|
|
lv_obj_set_user_data(btn, pointer(i));
|
|
lv_obj_add_event_cb(btn, @fb_tab_cb, LV_EVENT_CLICKED, state);
|
|
state^.tab_btns[i] := btn;
|
|
|
|
{ Close button (x) on each tab }
|
|
if state^.tab_count > 1 then begin
|
|
close_b := lv_label_create(btn);
|
|
lv_label_set_text(close_b, 'x');
|
|
lv_obj_set_style_text_color(close_b, lv_color_make(140, 140, 160), 0);
|
|
lv_obj_set_style_text_font(close_b, @lv_font_montserrat_14, 0);
|
|
lv_obj_add_flag(close_b, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_user_data(close_b, pointer(i));
|
|
lv_obj_add_event_cb(close_b, @fb_tab_close_cb, LV_EVENT_CLICKED, state);
|
|
end;
|
|
end;
|
|
|
|
{ "+" button to add new tab }
|
|
if state^.tab_count < MAX_TABS then begin
|
|
plus_b := lv_button_create(bar);
|
|
lv_obj_remove_style_all(plus_b);
|
|
lv_obj_set_size(plus_b, 24, 22);
|
|
lv_obj_set_style_bg_color(plus_b, lv_color_make(35, 38, 50), 0);
|
|
lv_obj_set_style_bg_opa(plus_b, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_radius(plus_b, 4, 0);
|
|
lv_obj_add_flag(plus_b, LV_OBJ_FLAG_CLICKABLE);
|
|
lbl := lv_label_create(plus_b);
|
|
lv_label_set_text(lbl, '+');
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(140, 150, 170), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_center(lbl);
|
|
lv_obj_add_event_cb(plus_b, @fb_tab_new_cb, LV_EVENT_CLICKED, state);
|
|
end;
|
|
end;
|
|
|
|
{ Tab click — switch to this tab }
|
|
procedure fb_tab_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
btn : Plv_obj;
|
|
idx : uint32;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
btn := lv_event_get_target_obj(e);
|
|
idx := uint32(lv_obj_get_user_data(btn));
|
|
switch_tab(state, idx);
|
|
end;
|
|
|
|
{ Tab close (x) click }
|
|
procedure fb_tab_close_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
lbl : Plv_obj;
|
|
idx : uint32;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
lbl := lv_event_get_target_obj(e);
|
|
idx := uint32(lv_obj_get_user_data(lbl));
|
|
close_tab(state, idx);
|
|
end;
|
|
|
|
{ "+" button — new tab at current path }
|
|
procedure fb_tab_new_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
add_tab(state, state^.cur_path);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 10 — Quick filter
|
|
============================================================ }
|
|
|
|
{ fb_filter_changed_cb — re-render rows with current filter text }
|
|
procedure fb_filter_changed_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
txt : pchar;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_VALUE_CHANGED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
{ Update filter_text from textarea }
|
|
if state^.filter_text <> nil then begin
|
|
kfree(void(state^.filter_text));
|
|
state^.filter_text := nil;
|
|
end;
|
|
txt := lv_textarea_get_text(state^.filter_ta);
|
|
if (txt <> nil) and (stringSize(txt) > 0) then
|
|
state^.filter_text := stringCopy(txt);
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ fb_filter_clear_cb — clear filter text and refresh }
|
|
procedure fb_filter_clear_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
if state^.filter_text <> nil then begin
|
|
kfree(void(state^.filter_text));
|
|
state^.filter_text := nil;
|
|
end;
|
|
if state^.filter_ta <> nil then
|
|
lv_textarea_set_text(state^.filter_ta, '');
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 11 — Type-ahead jump-to
|
|
============================================================ }
|
|
|
|
{ fb_typeahead_reset_timer — clear type-ahead buffer after 1s inactivity }
|
|
procedure fb_typeahead_reset_timer(tmr: Plv_timer); cdecl;
|
|
var
|
|
state: PFileBrowserState;
|
|
begin
|
|
state := PFileBrowserState(lv_timer_get_user_data(tmr));
|
|
lv_timer_delete(tmr);
|
|
if state <> nil then begin
|
|
state^.ta_len := 0;
|
|
state^.ta_buf[0]:= #0;
|
|
state^.ta_timer := nil;
|
|
end;
|
|
end;
|
|
|
|
{ fb_typeahead_key_cb — handle key presses on content area for jump-to }
|
|
procedure fb_typeahead_key_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
key : uint32;
|
|
ch : char;
|
|
n, i : uint32;
|
|
child : Plv_obj;
|
|
firstLbl: Plv_obj;
|
|
lblText : pchar;
|
|
isDir : boolean;
|
|
ud : pchar;
|
|
baseName: pchar;
|
|
bLen : uint32;
|
|
j : sint32;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_KEY then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
key := lv_event_get_key(e);
|
|
|
|
{ Only handle printable ASCII }
|
|
if (key < 32) or (key > 126) then exit;
|
|
ch := char(key);
|
|
|
|
{ Append to buf (guard overflow) }
|
|
if state^.ta_len >= 31 then exit;
|
|
state^.ta_buf[state^.ta_len] := ch;
|
|
state^.ta_len := state^.ta_len + 1;
|
|
state^.ta_buf[state^.ta_len] := #0;
|
|
|
|
{ Reset/restart the 1-second timer }
|
|
if state^.ta_timer <> nil then
|
|
lv_timer_delete(state^.ta_timer);
|
|
state^.ta_timer := lv_timer_create(@fb_typeahead_reset_timer, 1000, state);
|
|
|
|
{ Search children of content_area for first match }
|
|
n := lv_obj_get_child_count(state^.content_area);
|
|
if n = 0 then exit;
|
|
for i := 0 to n - 1 do begin
|
|
child := lv_obj_get_child(state^.content_area, sint32(i));
|
|
if child = nil then continue;
|
|
ud := pchar(lv_obj_get_user_data(child));
|
|
if ud = nil then continue;
|
|
{ Extract basename from full path in user_data }
|
|
bLen := stringSize(ud);
|
|
if bLen = 0 then continue;
|
|
j := sint32(bLen) - 1;
|
|
if ud[j] = '/' then dec(j);
|
|
while (j > 0) and (ud[j] <> '/') do dec(j);
|
|
if ud[j] = '/' then inc(j);
|
|
baseName := @ud[j];
|
|
if stringStartsWithCI(baseName, @state^.ta_buf[0]) then begin
|
|
lv_obj_scroll_to_view(child, LV_ANIM_ON);
|
|
{ Dir rows have first label text starting with '[' }
|
|
isDir := false;
|
|
if lv_obj_get_child_count(child) > 0 then begin
|
|
firstLbl := lv_obj_get_child(child, 0);
|
|
if firstLbl <> nil then begin
|
|
lblText := lv_label_get_text(firstLbl);
|
|
if (lblText <> nil) and (lblText[0] = '[') then
|
|
isDir := true;
|
|
end;
|
|
end;
|
|
select_item(state, child, ud, isDir);
|
|
exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 13 — Compact view toggle
|
|
============================================================ }
|
|
procedure fb_viewmode_toggle_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
if state^.view_mode = vmDetail then
|
|
state^.view_mode := vmCompact
|
|
else
|
|
state^.view_mode := vmDetail;
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 14 — Column header sorting
|
|
============================================================ }
|
|
|
|
procedure fb_header_name_cb(e: Plv_event); cdecl;
|
|
var state: PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
if state^.sort_col = 0 then
|
|
state^.sort_asc := not state^.sort_asc
|
|
else begin
|
|
state^.sort_col := 0;
|
|
state^.sort_asc := true;
|
|
end;
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
procedure fb_header_size_cb(e: Plv_event); cdecl;
|
|
var state: PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
if state^.sort_col = 1 then
|
|
state^.sort_asc := not state^.sort_asc
|
|
else begin
|
|
state^.sort_col := 1;
|
|
state^.sort_asc := true;
|
|
end;
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
procedure fb_header_type_cb(e: Plv_event); cdecl;
|
|
var state: PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
if state^.sort_col = 2 then
|
|
state^.sort_asc := not state^.sort_asc
|
|
else begin
|
|
state^.sort_col := 2;
|
|
state^.sort_asc := true;
|
|
end;
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 15 — Hidden files toggle
|
|
============================================================ }
|
|
procedure fb_hidden_toggle_cb(e: Plv_event); cdecl;
|
|
var state: PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
state^.show_hidden := not state^.show_hidden;
|
|
schedule_refresh(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 17 — WatchDirectory live refresh
|
|
============================================================ }
|
|
|
|
{ fb_watch_cb — called from VFS context when directory changes.
|
|
MUST NOT call LVGL or disk I/O; just sets a flag. }
|
|
procedure fb_watch_cb(event: TVFSWatchEvent; path: pchar; userdata: pointer);
|
|
var state: PFileBrowserState;
|
|
begin
|
|
state := PFileBrowserState(userdata);
|
|
if state <> nil then
|
|
state^.watch_dirty := true;
|
|
end;
|
|
|
|
{ fb_watch_poll_timer — 500ms LVGL timer; checks dirty flag and refreshes }
|
|
procedure fb_watch_poll_timer(tmr: Plv_timer); cdecl;
|
|
var state: PFileBrowserState;
|
|
begin
|
|
state := PFileBrowserState(lv_timer_get_user_data(tmr));
|
|
if state = nil then exit;
|
|
if state^.watch_dirty then begin
|
|
state^.watch_dirty := false;
|
|
schedule_refresh(state);
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 12 — File preview panel
|
|
============================================================ }
|
|
|
|
{ update_preview — refresh the preview panel content }
|
|
procedure update_preview(state: PFileBrowserState);
|
|
var
|
|
panel : Plv_obj;
|
|
lbl : Plv_obj;
|
|
sep : Plv_obj;
|
|
baseName : pchar;
|
|
szStr : pchar;
|
|
extStr : pchar;
|
|
len : uint32;
|
|
j : sint32;
|
|
begin
|
|
panel := state^.preview_panel;
|
|
if panel = nil then exit;
|
|
lv_obj_clean(panel);
|
|
|
|
if not state^.preview_visible then exit;
|
|
|
|
if (state^.sel_path = nil) or (stringSize(state^.sel_path) = 0) then begin
|
|
lbl := lv_label_create(panel);
|
|
lv_label_set_text(lbl, 'No selection');
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(100, 110, 130), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
exit;
|
|
end;
|
|
|
|
{ Header }
|
|
lbl := lv_label_create(panel);
|
|
lv_label_set_text(lbl, 'Preview');
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(140, 150, 170), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
|
|
sep := lv_obj_create(panel);
|
|
lv_obj_remove_style_all(sep);
|
|
lv_obj_set_size(sep, lv_pct(100), 1);
|
|
lv_obj_set_style_bg_color(sep, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_bg_opa(sep, LV_OPA_COVER, 0);
|
|
|
|
{ Extract basename }
|
|
len := stringSize(state^.sel_path);
|
|
j := sint32(len) - 1;
|
|
if (j > 0) and (state^.sel_path[j] = '/') then dec(j);
|
|
while (j > 0) and (state^.sel_path[j] <> '/') do dec(j);
|
|
if state^.sel_path[j] = '/' then inc(j);
|
|
baseName := @state^.sel_path[j];
|
|
|
|
{ Name }
|
|
lbl := lv_label_create(panel);
|
|
lv_label_set_text(lbl, baseName);
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(220, 225, 240), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(lbl, lv_pct(100));
|
|
|
|
{ Type }
|
|
lbl := lv_label_create(panel);
|
|
if state^.sel_is_dir then
|
|
lv_label_set_text(lbl, 'Type: Directory')
|
|
else begin
|
|
extStr := getFileExtension(baseName);
|
|
if extStr <> nil then begin
|
|
szStr := stringConcat('Type: ', extStr);
|
|
lv_label_set_text(lbl, szStr);
|
|
kfree(void(szStr));
|
|
end else
|
|
lv_label_set_text(lbl, 'Type: File');
|
|
end;
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(160, 168, 190), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
|
|
{ Full path }
|
|
lbl := lv_label_create(panel);
|
|
lv_label_set_text(lbl, state^.sel_path);
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(120, 128, 150), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_label_set_long_mode(lbl, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(lbl, lv_pct(100));
|
|
|
|
{ For directories: show type indicator (no disk I/O per Lesson #10) }
|
|
if state^.sel_is_dir then begin
|
|
lbl := lv_label_create(panel);
|
|
lv_label_set_text(lbl, SYM_DIRECTORY + ' Folder');
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(160, 168, 190), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
end;
|
|
end;
|
|
|
|
{ fb_preview_toggle_cb — toggle preview panel visibility }
|
|
procedure fb_preview_toggle_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
state^.preview_visible := not state^.preview_visible;
|
|
if state^.preview_panel <> nil then begin
|
|
if state^.preview_visible then begin
|
|
lv_obj_remove_flag(state^.preview_panel, LV_OBJ_FLAG_HIDDEN);
|
|
update_preview(state);
|
|
end else
|
|
lv_obj_add_flag(state^.preview_panel, LV_OBJ_FLAG_HIDDEN);
|
|
end;
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 9 — Bookmarks sidebar
|
|
============================================================ }
|
|
|
|
{ add_bookmark_entry — add a path to the bookmarks list }
|
|
procedure add_bookmark_entry(state: PFileBrowserState; path: pchar);
|
|
var
|
|
i : uint32;
|
|
begin
|
|
if state^.bm_count >= MAX_BOOKMARKS then exit;
|
|
{ Check for duplicates }
|
|
if state^.bm_count > 0 then
|
|
for i := 0 to state^.bm_count - 1 do
|
|
if stringEquals(state^.bookmarks[i], path) then exit;
|
|
state^.bookmarks[state^.bm_count] := stringCopy(path);
|
|
state^.bm_count := state^.bm_count + 1;
|
|
rebuild_bookmarks(state);
|
|
end;
|
|
|
|
{ remove_bookmark_entry — remove bookmark at idx }
|
|
procedure remove_bookmark_entry(state: PFileBrowserState; idx: uint32);
|
|
var
|
|
i : uint32;
|
|
begin
|
|
if idx >= state^.bm_count then exit;
|
|
if state^.bookmarks[idx] <> nil then
|
|
kfree(void(state^.bookmarks[idx]));
|
|
{ Shift remainder down }
|
|
if idx < state^.bm_count - 1 then
|
|
for i := idx to state^.bm_count - 2 do
|
|
state^.bookmarks[i] := state^.bookmarks[i + 1];
|
|
state^.bookmarks[state^.bm_count - 1] := nil;
|
|
state^.bm_count := state^.bm_count - 1;
|
|
rebuild_bookmarks(state);
|
|
end;
|
|
|
|
{ fb_bookmark_click_cb — navigate to bookmarked path }
|
|
procedure fb_bookmark_click_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
btn : Plv_obj;
|
|
path : pchar;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
btn := lv_event_get_target_obj(e);
|
|
path := pchar(lv_obj_get_user_data(btn));
|
|
if path <> nil then
|
|
navigate_to(state, path);
|
|
end;
|
|
|
|
{ fb_bookmark_ctx_cb — right-click (long press) to remove bookmark }
|
|
procedure fb_bookmark_ctx_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
btn : Plv_obj;
|
|
path : pchar;
|
|
i : uint32;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_LONG_PRESSED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
btn := lv_event_get_target_obj(e);
|
|
path := pchar(lv_obj_get_user_data(btn));
|
|
if path = nil then exit;
|
|
{ Find matching bookmark index }
|
|
if state^.bm_count > 0 then
|
|
for i := 0 to state^.bm_count - 1 do
|
|
if stringEquals(state^.bookmarks[i], path) then begin
|
|
remove_bookmark_entry(state, i);
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
{ fb_bookmark_add_cb — "+" button adds current directory }
|
|
procedure fb_bookmark_add_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
add_bookmark_entry(state, state^.cur_path);
|
|
end;
|
|
|
|
{ rebuild_bookmarks — recreate sidebar bookmark list }
|
|
procedure rebuild_bookmarks(state: PFileBrowserState);
|
|
var
|
|
bar : Plv_obj;
|
|
btn : Plv_obj;
|
|
lbl : Plv_obj;
|
|
plus_b : Plv_obj;
|
|
sep : Plv_obj;
|
|
i : uint32;
|
|
name : pchar;
|
|
len : uint32;
|
|
j : sint32;
|
|
begin
|
|
bar := state^.sidebar;
|
|
if bar = nil then exit;
|
|
{ Free user_data (stringCopy'd paths) from old buttons }
|
|
free_children_userdata(bar);
|
|
lv_obj_clean(bar);
|
|
|
|
{ Header label }
|
|
lbl := lv_label_create(bar);
|
|
lv_label_set_text(lbl, 'Bookmarks');
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(140, 150, 170), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_pad_bottom(lbl, 4, 0);
|
|
|
|
{ Separator line }
|
|
sep := lv_obj_create(bar);
|
|
lv_obj_remove_style_all(sep);
|
|
lv_obj_set_size(sep, lv_pct(100), 1);
|
|
lv_obj_set_style_bg_color(sep, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_bg_opa(sep, LV_OPA_COVER, 0);
|
|
|
|
if state^.bm_count > 0 then
|
|
for i := 0 to state^.bm_count - 1 do begin
|
|
name := state^.bookmarks[i];
|
|
if name = nil then continue;
|
|
|
|
btn := lv_obj_create(bar);
|
|
lv_obj_remove_style_all(btn);
|
|
lv_obj_set_size(btn, lv_pct(100), 24);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(35, 38, 50), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_80, 0);
|
|
lv_obj_set_style_radius(btn, 4, 0);
|
|
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), uint32(LV_STATE_PRESSED));
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, uint32(LV_STATE_PRESSED));
|
|
lv_obj_set_style_pad_left(btn, 6, 0);
|
|
|
|
{ Extract last path segment for display }
|
|
len := stringSize(name);
|
|
if len <= 1 then
|
|
name := '/'
|
|
else begin
|
|
j := sint32(len) - 1;
|
|
if name[j] = '/' then dec(j);
|
|
while (j > 0) and (name[j] <> '/') do dec(j);
|
|
name := @state^.bookmarks[i][j + 1];
|
|
end;
|
|
|
|
lbl := lv_label_create(btn);
|
|
lv_label_set_text(lbl, name);
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(190, 198, 220), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
|
|
|
|
lv_obj_set_user_data(btn, void(stringCopy(state^.bookmarks[i])));
|
|
lv_obj_add_event_cb(btn, @fb_bookmark_click_cb, LV_EVENT_CLICKED, state);
|
|
lv_obj_add_event_cb(btn, @fb_bookmark_ctx_cb, LV_EVENT_LONG_PRESSED, state);
|
|
end;
|
|
|
|
{ "+" Add Bookmark button }
|
|
plus_b := lv_obj_create(bar);
|
|
lv_obj_remove_style_all(plus_b);
|
|
lv_obj_set_size(plus_b, lv_pct(100), 24);
|
|
lv_obj_set_style_bg_color(plus_b, lv_color_make(30, 33, 44), 0);
|
|
lv_obj_set_style_bg_opa(plus_b, LV_OPA_80, 0);
|
|
lv_obj_set_style_radius(plus_b, 4, 0);
|
|
lv_obj_add_flag(plus_b, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_bg_color(plus_b, lv_color_make(50, 55, 72), uint32(LV_STATE_PRESSED));
|
|
lv_obj_set_style_bg_opa(plus_b, LV_OPA_COVER, uint32(LV_STATE_PRESSED));
|
|
lv_obj_set_style_pad_left(plus_b, 6, 0);
|
|
lbl := lv_label_create(plus_b);
|
|
lv_label_set_text(lbl, '+ Add');
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(100, 110, 140), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
|
|
lv_obj_add_event_cb(plus_b, @fb_bookmark_add_cb, LV_EVENT_CLICKED, state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
Phase 7 — Context menu
|
|
============================================================ }
|
|
|
|
const
|
|
CTX_ACT_OPEN = 1;
|
|
CTX_ACT_RENAME = 2;
|
|
CTX_ACT_DELETE = 3;
|
|
CTX_ACT_NEWFOLD = 4;
|
|
CTX_ACT_NEWFILE = 5;
|
|
CTX_ACT_REFRESH = 6;
|
|
|
|
procedure destroy_context_menu(state: PFileBrowserState);
|
|
begin
|
|
if state^.ctx_menu <> nil then begin
|
|
lv_obj_delete(state^.ctx_menu);
|
|
state^.ctx_menu := nil;
|
|
end;
|
|
end;
|
|
|
|
{ Helper: add one menu row }
|
|
function ctx_add_item(parent: Plv_obj; text: pchar;
|
|
action: uint32; state: PFileBrowserState): Plv_obj;
|
|
var
|
|
btn : Plv_obj;
|
|
lbl : Plv_obj;
|
|
begin
|
|
btn := lv_button_create(parent);
|
|
lv_obj_remove_style_all(btn);
|
|
lv_obj_set_size(btn, lv_pct(100), 26);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(60, 68, 90), LV_STATE_PRESSED);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_STATE_PRESSED);
|
|
lv_obj_set_style_pad_left(btn, 10, 0);
|
|
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_layout(btn, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lbl := lv_label_create(btn);
|
|
lv_label_set_text(lbl, text);
|
|
lv_obj_set_style_text_color(lbl, lv_color_make(210, 215, 230), 0);
|
|
lv_obj_set_style_text_font(lbl, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_user_data(btn, pointer(action));
|
|
lv_obj_add_event_cb(btn, @fb_ctx_item_cb, LV_EVENT_CLICKED, state);
|
|
ctx_add_item := btn;
|
|
end;
|
|
|
|
procedure show_context_menu(state: PFileBrowserState; mx, my: sint32);
|
|
var
|
|
menu : Plv_obj;
|
|
begin
|
|
destroy_context_menu(state);
|
|
|
|
menu := lv_obj_create(lv_layer_top);
|
|
lv_obj_remove_style_all(menu);
|
|
lv_obj_set_pos(menu, mx, my);
|
|
lv_obj_set_size(menu, 140, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_bg_color(menu, lv_color_make(38, 42, 56), 0);
|
|
lv_obj_set_style_bg_opa(menu, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(menu, lv_color_make(70, 78, 100), 0);
|
|
lv_obj_set_style_border_width(menu, 1, 0);
|
|
lv_obj_set_style_radius(menu, 6, 0);
|
|
lv_obj_set_style_pad_all(menu, 4, 0);
|
|
lv_obj_set_style_pad_row(menu, 1, 0);
|
|
lv_obj_set_style_layout(menu, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(menu, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_remove_flag(menu, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_shadow_width(menu, 8, 0);
|
|
lv_obj_set_style_shadow_opa(menu, 120, 0);
|
|
lv_obj_set_style_shadow_color(menu, lv_color_make(0, 0, 0), 0);
|
|
|
|
{ Contextual items when an item is selected }
|
|
if state^.sel_path <> nil then begin
|
|
if state^.sel_is_dir then
|
|
ctx_add_item(menu, 'Open', CTX_ACT_OPEN, state);
|
|
ctx_add_item(menu, 'Rename', CTX_ACT_RENAME, state);
|
|
ctx_add_item(menu, 'Delete', CTX_ACT_DELETE, state);
|
|
end;
|
|
|
|
{ Always-visible items }
|
|
ctx_add_item(menu, 'New Folder', CTX_ACT_NEWFOLD, state);
|
|
ctx_add_item(menu, 'New File', CTX_ACT_NEWFILE, state);
|
|
ctx_add_item(menu, 'Refresh', CTX_ACT_REFRESH, state);
|
|
|
|
state^.ctx_menu := menu;
|
|
end;
|
|
|
|
procedure fb_ctx_item_cb(e: Plv_event); cdecl;
|
|
var
|
|
code : lv_event_code_t;
|
|
state : PFileBrowserState;
|
|
btn : Plv_obj;
|
|
action : uint32;
|
|
begin
|
|
code := lv_event_get_code(e);
|
|
if code <> LV_EVENT_CLICKED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
btn := lv_event_get_target_obj(e);
|
|
action := uint32(lv_obj_get_user_data(btn));
|
|
|
|
{ Close context menu first }
|
|
destroy_context_menu(state);
|
|
|
|
case action of
|
|
CTX_ACT_OPEN: begin
|
|
if (state^.sel_path <> nil) and state^.sel_is_dir then
|
|
navigate_to(state, state^.sel_path);
|
|
end;
|
|
CTX_ACT_RENAME: begin
|
|
if state^.sel_path <> nil then begin
|
|
{ Synthesize a fake LVGL event to reuse fb_rename_cb
|
|
— simpler to just inline the rename dialog creation }
|
|
fb_rename_cb(e);
|
|
end;
|
|
end;
|
|
CTX_ACT_DELETE: begin
|
|
if state^.sel_path <> nil then
|
|
fb_delete_cb(e);
|
|
end;
|
|
CTX_ACT_NEWFOLD: fb_newfolder_cb(e);
|
|
CTX_ACT_NEWFILE: fb_newfile_cb(e);
|
|
CTX_ACT_REFRESH: schedule_refresh(state);
|
|
end;
|
|
end;
|
|
|
|
{ LVGL long-press callback: select row and show context menu }
|
|
procedure fb_row_long_press_cb(e: Plv_event); cdecl;
|
|
var
|
|
state : PFileBrowserState;
|
|
row : Plv_obj;
|
|
path : pchar;
|
|
isDir : boolean;
|
|
coords : lv_area_t;
|
|
begin
|
|
if lv_event_get_code(e) <> LV_EVENT_LONG_PRESSED then exit;
|
|
state := PFileBrowserState(lv_event_get_user_data(e));
|
|
if state = nil then exit;
|
|
row := lv_event_get_target_obj(e);
|
|
if row = nil then exit;
|
|
|
|
path := pchar(lv_obj_get_user_data(row));
|
|
if path <> nil then begin
|
|
isDir := lv_obj_has_flag(row, LV_OBJ_FLAG_USER_1);
|
|
select_item(state, row, path, isDir);
|
|
end;
|
|
|
|
{ Position context menu at the row's screen coordinates }
|
|
lv_obj_get_coords(row, @coords);
|
|
show_context_menu(state, coords.x2, coords.y1);
|
|
end;
|
|
|
|
{ Timer to dismiss context menu when clicking elsewhere }
|
|
procedure fb_ctx_dismiss_timer(tmr: Plv_timer); cdecl;
|
|
var
|
|
state: PFileBrowserState;
|
|
begin
|
|
state := PFileBrowserState(lv_timer_get_user_data(tmr));
|
|
lv_timer_delete(tmr);
|
|
if state <> nil then
|
|
destroy_context_menu(state);
|
|
end;
|
|
|
|
{ ============================================================
|
|
makeToolBtn — compact toolbar button helper
|
|
============================================================ }
|
|
function makeToolBtn(parent: Plv_obj; text: pchar; w: sint32;
|
|
ud: pointer; cb: lv_event_cb_t): Plv_obj;
|
|
var
|
|
btn, lbl_obj : Plv_obj;
|
|
begin
|
|
btn := lv_button_create(parent);
|
|
lv_obj_remove_style_all(btn);
|
|
lv_obj_set_size(btn, w, 28);
|
|
lv_obj_set_style_bg_color(btn, lv_color_make(45, 50, 68), 0);
|
|
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_radius(btn, 4, 0);
|
|
lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_layout(btn, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(btn, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(btn, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lbl_obj := lv_label_create(btn);
|
|
lv_label_set_text(lbl_obj, text);
|
|
lv_obj_set_style_text_color(lbl_obj, lv_color_make(210, 215, 230), 0);
|
|
lv_obj_set_style_text_font(lbl_obj, @lv_font_montserrat_14, 0);
|
|
if cb <> nil then
|
|
lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, ud);
|
|
makeToolBtn := btn;
|
|
end;
|
|
|
|
{ ============================================================
|
|
launch — create the window and populate the initial UI
|
|
============================================================ }
|
|
procedure launch;
|
|
var
|
|
scr_w, scr_h : sint32;
|
|
wx, wy : sint32;
|
|
content : Plv_obj;
|
|
toolbar : Plv_obj;
|
|
statusbar : Plv_obj;
|
|
main_area : Plv_obj;
|
|
body_row : Plv_obj;
|
|
sidebar : Plv_obj;
|
|
sl : Plv_obj;
|
|
state : PFileBrowserState;
|
|
ctx : PProcessContext;
|
|
begin
|
|
debug.tracer.push_trace('filebrowser.launch');
|
|
|
|
{ Single instance guard }
|
|
if driver.video.windows.isWindowOpen(g_win_id) then begin
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
scr_w := sint32(driver.video.frontBufferWidth);
|
|
scr_h := sint32(driver.video.frontBufferHeight);
|
|
wx := (scr_w - WIN_W) div 2;
|
|
wy := (scr_h - WIN_H) div 2 - 30;
|
|
|
|
g_win_id := driver.video.windows.createWindow('Files', wx, wy, WIN_W, WIN_H,
|
|
@onClose, nil);
|
|
if g_win_id = 0 then begin
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
content := driver.video.windows.getWindowContent(g_win_id);
|
|
if content = nil then begin
|
|
driver.video.windows.destroyWindow(g_win_id);
|
|
g_win_id := 0;
|
|
debug.tracer.pop_trace; exit;
|
|
end;
|
|
|
|
{ Allocate and zero-initialise state }
|
|
state := PFileBrowserState(kalloc(sizeof(TFileBrowserState)));
|
|
memset(uint32(state), 0, sizeof(TFileBrowserState));
|
|
state^.win_id := g_win_id;
|
|
state^.cur_path := stringCopy('/');
|
|
state^.view_mode := vmDetail;
|
|
state^.sort_col := 0; { name }
|
|
state^.sort_asc := true;
|
|
state^.show_hidden := false;
|
|
g_state := state;
|
|
|
|
{ ---- Content area: flex column ---- }
|
|
lv_obj_set_style_layout(content, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_all(content, 0, 0);
|
|
lv_obj_set_style_pad_row(content, 0, 0);
|
|
lv_obj_remove_flag(content, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_bg_color(content, lv_color_make(28, 31, 40), 0);
|
|
lv_obj_set_style_bg_opa(content, LV_OPA_COVER, 0);
|
|
|
|
{ ---- Toolbar ---- }
|
|
toolbar := lv_obj_create(content);
|
|
lv_obj_remove_style_all(toolbar);
|
|
lv_obj_set_size(toolbar, lv_pct(100), TOOLBAR_H);
|
|
lv_obj_set_style_bg_color(toolbar, lv_color_make(28, 31, 40), 0);
|
|
lv_obj_set_style_bg_opa(toolbar, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(toolbar, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_border_side(toolbar, LV_BORDER_SIDE_BOTTOM, 0);
|
|
lv_obj_set_style_border_width(toolbar, 1, 0);
|
|
lv_obj_set_style_pad_all(toolbar, 4, 0);
|
|
lv_obj_set_style_pad_column(toolbar, 3, 0);
|
|
lv_obj_set_style_layout(toolbar, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(toolbar, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(toolbar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_remove_flag(toolbar, LV_OBJ_FLAG_SCROLLABLE);
|
|
state^.toolbar := toolbar;
|
|
|
|
{ Navigation buttons }
|
|
state^.back_btn := makeToolBtn(toolbar, SYM_LEFT, 28, state, @fb_back_cb);
|
|
state^.fwd_btn := makeToolBtn(toolbar, SYM_RIGHT, 28, state, @fb_fwd_cb);
|
|
state^.up_btn := makeToolBtn(toolbar, SYM_UP, 28, state, @fb_up_cb);
|
|
|
|
{ ---- Breadcrumb bar (fills remaining toolbar space) ---- }
|
|
state^.breadcrumb := lv_obj_create(toolbar);
|
|
lv_obj_remove_style_all(state^.breadcrumb);
|
|
lv_obj_set_height(state^.breadcrumb, 26);
|
|
lv_obj_set_flex_grow(state^.breadcrumb, 1);
|
|
lv_obj_set_style_bg_color(state^.breadcrumb, lv_color_make(38, 42, 56), 0);
|
|
lv_obj_set_style_bg_opa(state^.breadcrumb, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_radius(state^.breadcrumb, 4, 0);
|
|
lv_obj_set_style_pad_left(state^.breadcrumb, 4, 0);
|
|
lv_obj_set_style_pad_right(state^.breadcrumb, 4, 0);
|
|
lv_obj_set_style_pad_column(state^.breadcrumb, 2, 0);
|
|
lv_obj_set_style_layout(state^.breadcrumb, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(state^.breadcrumb, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(state^.breadcrumb, LV_FLEX_ALIGN_START,
|
|
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_remove_flag(state^.breadcrumb, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_flag(state^.breadcrumb, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_add_event_cb(state^.breadcrumb, @fb_crumb_area_cb,
|
|
LV_EVENT_CLICKED, state);
|
|
|
|
{ ---- Path text area (hidden by default, shown on breadcrumb click) ---- }
|
|
state^.path_ta := lv_textarea_create(toolbar);
|
|
lv_obj_set_height(state^.path_ta, 26);
|
|
lv_obj_set_flex_grow(state^.path_ta, 1);
|
|
lv_textarea_set_one_line(state^.path_ta, true);
|
|
lv_textarea_set_text(state^.path_ta, '/');
|
|
lv_obj_set_style_bg_color(state^.path_ta, lv_color_make(38, 42, 56), 0);
|
|
lv_obj_set_style_bg_opa(state^.path_ta, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_text_color(state^.path_ta, lv_color_make(210, 215, 230), 0);
|
|
lv_obj_set_style_text_font(state^.path_ta, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_border_color(state^.path_ta, lv_color_make(80, 120, 200), 0);
|
|
lv_obj_set_style_border_width(state^.path_ta, 1, 0);
|
|
lv_obj_set_style_radius(state^.path_ta, 4, 0);
|
|
lv_obj_set_style_pad_all(state^.path_ta, 4, 0);
|
|
lv_obj_add_flag(state^.path_ta, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_event_cb(state^.path_ta, @fb_path_ta_cb, LV_EVENT_KEY, state);
|
|
state^.path_editing := false;
|
|
|
|
makeToolBtn(toolbar, SYM_REFRESH, 28, state, @fb_refresh_cb);
|
|
{ File operation buttons }
|
|
makeToolBtn(toolbar, SYM_DIRECTORY, 32, state, @fb_newfolder_cb);
|
|
makeToolBtn(toolbar, SYM_FILE, 32, state, @fb_newfile_cb);
|
|
makeToolBtn(toolbar, SYM_EDIT, 32, state, @fb_rename_cb);
|
|
makeToolBtn(toolbar, SYM_TRASH, 32, state, @fb_delete_cb);
|
|
|
|
{ Filter input }
|
|
state^.filter_ta := lv_textarea_create(toolbar);
|
|
lv_textarea_set_one_line(state^.filter_ta, true);
|
|
lv_textarea_set_placeholder_text(state^.filter_ta, 'Filter...');
|
|
lv_obj_set_size(state^.filter_ta, 120, 26);
|
|
lv_obj_set_style_bg_color(state^.filter_ta, lv_color_make(38, 42, 56), 0);
|
|
lv_obj_set_style_bg_opa(state^.filter_ta, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_text_color(state^.filter_ta, lv_color_make(210, 215, 230), 0);
|
|
lv_obj_set_style_text_font(state^.filter_ta, @lv_font_montserrat_14, 0);
|
|
lv_obj_set_style_border_color(state^.filter_ta, lv_color_make(60, 65, 80), 0);
|
|
lv_obj_set_style_border_width(state^.filter_ta, 1, 0);
|
|
lv_obj_set_style_radius(state^.filter_ta, 4, 0);
|
|
lv_obj_set_style_pad_all(state^.filter_ta, 4, 0);
|
|
lv_obj_add_event_cb(state^.filter_ta, @fb_filter_changed_cb,
|
|
LV_EVENT_VALUE_CHANGED, state);
|
|
makeToolBtn(toolbar, SYM_CLOSE, 24, state, @fb_filter_clear_cb);
|
|
makeToolBtn(toolbar, SYM_IMAGE, 24, state, @fb_preview_toggle_cb);
|
|
makeToolBtn(toolbar, SYM_LIST, 24, state, @fb_viewmode_toggle_cb);
|
|
makeToolBtn(toolbar, SYM_EYE_OPEN, 28, state, @fb_hidden_toggle_cb);
|
|
|
|
{ Dim disabled nav buttons initially }
|
|
lv_obj_remove_flag(state^.back_btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_opa(state^.back_btn, 80, 0);
|
|
lv_obj_remove_flag(state^.fwd_btn, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_style_opa(state^.fwd_btn, 80, 0);
|
|
|
|
{ ---- Tab bar ---- }
|
|
state^.tab_bar := lv_obj_create(content);
|
|
lv_obj_remove_style_all(state^.tab_bar);
|
|
lv_obj_set_size(state^.tab_bar, lv_pct(100), 26);
|
|
lv_obj_set_style_bg_color(state^.tab_bar, lv_color_make(28, 31, 40), 0);
|
|
lv_obj_set_style_bg_opa(state^.tab_bar, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_layout(state^.tab_bar, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(state^.tab_bar, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(state^.tab_bar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_left(state^.tab_bar, 4, 0);
|
|
lv_obj_set_style_pad_column(state^.tab_bar, 2, 0);
|
|
lv_obj_remove_flag(state^.tab_bar, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_border_color(state^.tab_bar, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_border_side(state^.tab_bar, LV_BORDER_SIDE_BOTTOM, 0);
|
|
lv_obj_set_style_border_width(state^.tab_bar, 1, 0);
|
|
state^.tab_count := 1;
|
|
state^.active_tab := 0;
|
|
|
|
{ ---- Body row — sidebar + main content ---- }
|
|
body_row := lv_obj_create(content);
|
|
lv_obj_remove_style_all(body_row);
|
|
lv_obj_set_size(body_row, lv_pct(100), 0);
|
|
lv_obj_set_flex_grow(body_row, 1);
|
|
lv_obj_set_style_layout(body_row, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(body_row, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_style_pad_column(body_row, 0, 0);
|
|
lv_obj_remove_flag(body_row, LV_OBJ_FLAG_SCROLLABLE);
|
|
state^.body_row := body_row;
|
|
|
|
{ ---- Bookmark sidebar (150px) ---- }
|
|
sidebar := lv_obj_create(body_row);
|
|
lv_obj_remove_style_all(sidebar);
|
|
lv_obj_set_size(sidebar, 150, lv_pct(100));
|
|
lv_obj_set_style_bg_color(sidebar, lv_color_make(25, 27, 36), 0);
|
|
lv_obj_set_style_bg_opa(sidebar, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(sidebar, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_border_side(sidebar, LV_BORDER_SIDE_RIGHT, 0);
|
|
lv_obj_set_style_border_width(sidebar, 1, 0);
|
|
lv_obj_set_style_pad_all(sidebar, 6, 0);
|
|
lv_obj_set_style_pad_row(sidebar, 2, 0);
|
|
lv_obj_set_style_layout(sidebar, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(sidebar, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_add_flag(sidebar, LV_OBJ_FLAG_SCROLLABLE);
|
|
state^.sidebar := sidebar;
|
|
|
|
{ ---- Main content area — hosts directory listing rows ---- }
|
|
main_area := lv_obj_create(body_row);
|
|
lv_obj_remove_style_all(main_area);
|
|
lv_obj_set_size(main_area, 0, lv_pct(100));
|
|
lv_obj_set_flex_grow(main_area, 1);
|
|
lv_obj_set_style_bg_color(main_area, lv_color_make(20, 22, 32), 0);
|
|
lv_obj_set_style_bg_opa(main_area, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_pad_all(main_area, 4, 0);
|
|
lv_obj_set_style_layout(main_area, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(main_area, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_row(main_area, 2, 0);
|
|
lv_obj_add_flag(main_area, LV_OBJ_FLAG_SCROLLABLE);
|
|
state^.content_area := main_area;
|
|
|
|
{ Type-ahead keyboard handler on content area }
|
|
lv_obj_add_event_cb(main_area, @fb_typeahead_key_cb, LV_EVENT_KEY, state);
|
|
|
|
{ ---- Preview panel (right side, hidden by default) ---- }
|
|
state^.preview_panel := lv_obj_create(body_row);
|
|
lv_obj_remove_style_all(state^.preview_panel);
|
|
lv_obj_set_size(state^.preview_panel, 200, lv_pct(100));
|
|
lv_obj_set_style_bg_color(state^.preview_panel, lv_color_make(25, 27, 36), 0);
|
|
lv_obj_set_style_bg_opa(state^.preview_panel, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(state^.preview_panel, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_border_side(state^.preview_panel, LV_BORDER_SIDE_LEFT, 0);
|
|
lv_obj_set_style_border_width(state^.preview_panel, 1, 0);
|
|
lv_obj_set_style_pad_all(state^.preview_panel, 8, 0);
|
|
lv_obj_set_style_pad_row(state^.preview_panel, 4, 0);
|
|
lv_obj_set_style_layout(state^.preview_panel, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(state^.preview_panel, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_add_flag(state^.preview_panel, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_flag(state^.preview_panel, LV_OBJ_FLAG_HIDDEN);
|
|
state^.preview_visible := false;
|
|
|
|
{ ---- Status bar ---- }
|
|
statusbar := lv_obj_create(content);
|
|
lv_obj_remove_style_all(statusbar);
|
|
lv_obj_set_size(statusbar, lv_pct(100), STATUSBAR_H);
|
|
lv_obj_set_style_bg_color(statusbar, lv_color_make(28, 31, 40), 0);
|
|
lv_obj_set_style_bg_opa(statusbar, LV_OPA_COVER, 0);
|
|
lv_obj_set_style_border_color(statusbar, lv_color_make(48, 52, 68), 0);
|
|
lv_obj_set_style_border_side(statusbar, LV_BORDER_SIDE_TOP, 0);
|
|
lv_obj_set_style_border_width(statusbar, 1, 0);
|
|
lv_obj_set_style_layout(statusbar, LV_LAYOUT_FLEX, 0);
|
|
lv_obj_set_flex_flow(statusbar, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(statusbar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER,
|
|
LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_left(statusbar, 8, 0);
|
|
lv_obj_remove_flag(statusbar, LV_OBJ_FLAG_SCROLLABLE);
|
|
state^.status_bar := statusbar;
|
|
|
|
sl := lv_label_create(statusbar);
|
|
lv_label_set_text(sl, '/');
|
|
lv_obj_set_style_text_color(sl, lv_color_make(140, 150, 170), 0);
|
|
lv_obj_set_style_text_font(sl, @lv_font_montserrat_14, 0);
|
|
state^.status_label := sl;
|
|
|
|
{ Register resize callback }
|
|
driver.video.windows.setWindowResizeCallback(g_win_id, @onResize);
|
|
|
|
{ Create process so filebrowser appears in PS }
|
|
ctx := proc.mgr.create('Files', @fb_entry, void(state), 1);
|
|
if ctx <> nil then begin
|
|
state^.pid := ctx^.ProcessID;
|
|
driver.video.windows.setWindowOwner(g_win_id, ctx^.ProcessID);
|
|
end;
|
|
|
|
{ Create 50ms poll timer so LVGL can pick up async refresh data }
|
|
state^.refresh_poll := lv_timer_create(@fb_refresh_poll_cb, 50, state);
|
|
|
|
{ Load initial directory }
|
|
navigate_to(state, '/');
|
|
rebuild_tab_bar(state);
|
|
|
|
{ Create 500ms poll timer for directory watch notifications }
|
|
state^.watch_timer := lv_timer_create(@fb_watch_poll_timer, 500, state);
|
|
|
|
{ Default bookmarks }
|
|
add_bookmark_entry(state, '/');
|
|
add_bookmark_entry(state, '/sys');
|
|
add_bookmark_entry(state, '/boot');
|
|
|
|
|
|
|
|
debug.tracer.pop_trace;
|
|
end;
|
|
|
|
{ ============================================================
|
|
init — register with the desktop program launcher
|
|
============================================================ }
|
|
procedure init();
|
|
begin
|
|
debug.tracer.push_trace('filebrowser.init');
|
|
g_win_id := 0;
|
|
g_state := nil;
|
|
driver.video.desktop.registerProgram('Files', @launch);
|
|
debug.tracer.pop_trace;
|
|
end;
|
|
|
|
end.
|