{ Prog->FilePicker - OS-level reusable file picker dialog Features: - Two-pane layout: bookmarks sidebar + sorted file list - Navigation history: Back / Forward / Up buttons - Sorted display: directories first then files, both alphabetical - File size shown inline per entry - Extension filter (caller-supplied; nil = all files) - Inline New Folder creation (no sub-modal) - Save-mode overwrite guard: two-click confirmation - Status bar: "X dirs, Y files" Usage: filepicker.show_open(title, start_dir, filter, callback, userdata) filepicker.show_save(title, start_dir, initial_name, filter, callback, userdata) The callback receives a kalloc'd path string the caller must kfree, or nil when the user cancels. Design: - A full-screen semi-transparent backdrop (child of lv_screen_active) absorbs clicks, preventing interaction with the app behind. - A centered panel is a child of the backdrop so a single lv_obj_delete(backdrop) tears down everything. - Entry row user_data holds stringCopy'd entry names freed in free_list_items before any list repopulation or close. - Bookmark button user_data holds stringCopy'd paths freed in free_bkmk_items before close. @author(Aaron Hance ) } unit app.filepicker; interface const FP_FILTER_ALL = nil; { pass as filter param to show all file core.types } type TPickerCallback = procedure(path: pchar; userdata: pointer); cdecl; procedure show_open(title : pchar; start_dir : pchar; filter : pchar; cb : TPickerCallback; userdata : pointer); procedure show_save(title : pchar; start_dir : pchar; initial_name : pchar; filter : pchar; cb : TPickerCallback; userdata : pointer); implementation uses core.ds.hashmap, memory.heap, driver.video.lvgl, driver.storage.types, core.strings, core.strings.helpers, io.syslog, debug.tracer, core.util, arch.x86.util, driver.storage.vfs; const PANEL_W = 580; PANEL_H = 480; BKMK_W = 120; { bookmarks sidebar fixed width } HIST_MAX = 16; { navigation history depth cap } ENTRY_MAX = 256; { max entries collected per pass in do_refresh } type { Flat name buffer — heap-alloc'd in do_refresh to avoid stack bloat } TNameBuf = array[0..ENTRY_MAX - 1] of pchar; PNameBuf = ^TNameBuf; TSizeBuf = array[0..ENTRY_MAX - 1] of uint32; PSizeBuf = ^TSizeBuf; { Per-call context passed to the forEach collect callback } PFPCollectCtx = ^TFPCollectCtx; TFPCollectCtx = record dir_names : PNameBuf; file_names : PNameBuf; file_sizes : PSizeBuf; dir_count : uint32; file_count : uint32; filter : pchar; end; PPicker = ^TPicker; TPicker = record { Root UI object — deleting this tears down everything } backdrop : Plv_obj; { Navigation row } path_bar : Plv_obj; back_btn : Plv_obj; fwd_btn : Plv_obj; { Content area } bkmk_panel : Plv_obj; file_list : Plv_obj; { flex-column scrollable container } { Info strip } status_label : Plv_obj; { Save mode } fname_bar : Plv_obj; { nil in open mode } { Bottom rows } action_row : Plv_obj; newfolder_row : Plv_obj; newfolder_bar : Plv_obj; { State } cur_dir : pchar; cb : TPickerCallback; userdata : pointer; is_save : boolean; pending_refresh : boolean; confirm_pending : boolean; { save: waiting for 2nd-click overwrite } filter : pchar; { nil=all; else extension e.g. '.txt' } { Navigation history: history[0..hist_len-1] are kalloc'd pchars } history : array[0..HIST_MAX - 1] of pchar; hist_len : sint32; hist_pos : sint32; { index in history of current location } end; { ============================================================ Forward declarations ============================================================ } procedure picker_close(p: PPicker; sel_path: pchar); forward; procedure do_refresh(p: PPicker); forward; procedure fp_collect_cb(key: pchar; data: void; ud: void); forward; procedure schedule_refresh(p: PPicker); forward; procedure navigate_to(p: PPicker; newDir: pchar); forward; procedure fp_refresh_timer_cb(tmr: Plv_timer); cdecl; forward; procedure fp_dir_cb(e: Plv_event); cdecl; forward; procedure fp_file_cb(e: Plv_event); cdecl; forward; procedure fp_confirm_cb(e: Plv_event); cdecl; forward; procedure fp_cancel_cb(e: Plv_event); cdecl; forward; procedure fp_up_cb(e: Plv_event); cdecl; forward; procedure fp_back_cb(e: Plv_event); cdecl; forward; procedure fp_fwd_cb(e: Plv_event); cdecl; forward; procedure fp_bkmk_cb(e: Plv_event); cdecl; forward; procedure fp_newfolder_btn_cb(e: Plv_event); cdecl; forward; procedure fp_newfolder_create_cb(e: Plv_event); cdecl; forward; procedure fp_newfolder_cancel_cb(e: Plv_event); cdecl; forward; { ============================================================ free_list_items / free_bkmk_items kfree the stringCopy'd core.strings stored in child 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; { ============================================================ History helpers ============================================================ } { Push a new path. Discards any forward history beyond hist_pos. Caps at HIST_MAX by dropping the oldest entry. } procedure push_history(p: PPicker; path: pchar); var i: sint32; begin { Trim forward entries } for i := p^.hist_pos + 1 to p^.hist_len - 1 do if p^.history[i] <> nil then begin kfree(void(p^.history[i])); p^.history[i] := nil; end; p^.hist_len := p^.hist_pos + 1; { Drop oldest when full } if p^.hist_len = HIST_MAX then begin if p^.history[0] <> nil then kfree(void(p^.history[0])); for i := 0 to HIST_MAX - 2 do p^.history[i] := p^.history[i + 1]; p^.history[HIST_MAX - 1] := nil; p^.hist_len := HIST_MAX - 1; p^.hist_pos := HIST_MAX - 2; end; p^.history[p^.hist_len] := stringCopy(path); p^.hist_len := p^.hist_len + 1; p^.hist_pos := p^.hist_len - 1; end; { Sync back/fwd button enabled state with history position } procedure update_nav_btns(p: PPicker); begin if p^.back_btn <> nil then begin if p^.hist_pos > 0 then begin lv_obj_add_flag(p^.back_btn, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_opa(p^.back_btn, LV_OPA_COVER, 0); end else begin lv_obj_remove_flag(p^.back_btn, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_opa(p^.back_btn, 80, 0); end; end; if p^.fwd_btn <> nil then begin if p^.hist_pos < p^.hist_len - 1 then begin lv_obj_add_flag(p^.fwd_btn, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_opa(p^.fwd_btn, LV_OPA_COVER, 0); end else begin lv_obj_remove_flag(p^.fwd_btn, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_opa(p^.fwd_btn, 80, 0); end; end; end; { ============================================================ navigate_to Central navigation point — updates cur_dir, pushes history, resets confirm state, schedules refresh. ============================================================ } procedure navigate_to(p: PPicker; newDir: pchar); begin push_history(p, newDir); if p^.cur_dir <> nil then kfree(void(p^.cur_dir)); p^.cur_dir := stringCopy(newDir); p^.confirm_pending := false; update_nav_btns(p); schedule_refresh(p); end; { ============================================================ picker_close Free all state, destroy UI, fire callback. ============================================================ } procedure picker_close(p: PPicker; sel_path: pchar); var cb : TPickerCallback; userdata : pointer; backdrop : Plv_obj; i : sint32; begin cb := p^.cb; userdata := p^.userdata; backdrop := p^.backdrop; free_children_userdata(p^.file_list); free_children_userdata(p^.bkmk_panel); if p^.cur_dir <> nil then kfree(void(p^.cur_dir)); for i := 0 to p^.hist_len - 1 do if p^.history[i] <> nil then kfree(void(p^.history[i])); kfree(void(p)); lv_obj_delete(backdrop); if cb <> nil then cb(sel_path, userdata); end; { ============================================================ fp_collect_cb core.ds.hashmap.forEach callback: classifies each VFS entry as dir or file and appends to the appropriate name array in the context. ============================================================ } procedure fp_collect_cb(key: pchar; data: void; ud: void); var ctx : PFPCollectCtx; obj : PVFSObject; begin ctx := PFPCollectCtx(ud); obj := PVFSObject(data); if (ctx = nil) or (obj = nil) 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 stringEndsWith(key, ctx^.filter) then 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; { ============================================================ do_refresh Repopulate file_list for p^.cur_dir. Dirs collected and sorted first, then files (filtered + sorted). Each row is a flex-row button: [name, flex_grow] [size/type, 70px]. ============================================================ } procedure do_refresh(p: PPicker); var Map : PHashMap; ctx : TFPCollectCtx; dir_names : PNameBuf; { heap-alloc'd to avoid blowing the core.version stack } file_names : PNameBuf; file_sizes : PSizeBuf; dir_count : uint32; file_count : uint32; i : uint32; row, name_lbl, size_lbl : Plv_obj; sep : pchar; copy : pchar; szStr : pchar; s1, s2, s3 : pchar; begin debug.tracer.push_trace('filepicker.do_refresh'); io.syslog.logln('FPCIK', 'do_refresh: enter'); if p^.file_list = nil then begin io.syslog.logln('FPCIK', 'do_refresh: file_list nil - exit'); debug.tracer.pop_trace; exit; end; io.syslog.logln('FPCIK', 'do_refresh: kalloc name bufs'); 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)); { Sync path bar } if p^.path_bar <> nil then lv_textarea_set_text(p^.path_bar, p^.cur_dir); { Release old entries } free_children_userdata(p^.file_list); lv_obj_clean(p^.file_list); io.syslog.logln('FPCIK', 'do_refresh: calling GetDirectoryListingFrom'); io.syslog.logln('FPCIK', p^.cur_dir); Map := driver.storage.vfs.GetDirectoryListingFrom(p^.cur_dir, '/'); io.syslog.logln('FPCIK', 'do_refresh: GetDirectoryListingFrom returned'); if Map = nil then begin io.syslog.logln('FPCIK', 'do_refresh: map nil - bad dir'); if p^.status_label <> nil then lv_label_set_text(p^.status_label, 'Could not read directory'); kfree(void(dir_names)); kfree(void(file_names)); kfree(void(file_sizes)); debug.tracer.pop_trace; exit; end; if Map^.Table = nil then begin io.syslog.logln('FPCIK', 'do_refresh: map table nil - corrupt map'); driver.storage.vfs.FreeDirectoryListing(Map); kfree(void(dir_names)); kfree(void(file_names)); kfree(void(file_sizes)); debug.tracer.pop_trace; exit; end; io.syslog.logln('FPCIK', 'do_refresh: map ok, collecting entries'); ctx.dir_names := dir_names; ctx.file_names := file_names; ctx.file_sizes := file_sizes; ctx.dir_count := 0; ctx.file_count := 0; ctx.filter := p^.filter; core.ds.hashmap.forEach(Map, @fp_collect_cb, void(@ctx)); dir_count := ctx.dir_count; file_count := ctx.file_count; io.syslog.logln('FPCIK', 'do_refresh: collection done, sorting'); { --- Sort both collections alphabetically --- } if dir_count > 0 then sortStringArray(@dir_names^[0], dir_count); if file_count > 0 then sortStringArrayWithData(@file_names^[0], @file_sizes^[0], file_count); { --- Build directory rows --- } if dir_count > 0 then for i := 0 to dir_count - 1 do begin { Full path only needed for user_data (nav); no size call needed } row := lv_obj_create(p^.file_list); lv_obj_remove_style_all(row); lv_obj_set_size(row, lv_pct(100), 30); 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_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED); 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_add_flag(row, LV_OBJ_FLAG_CLICKABLE); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); { Name label with folder indicator } name_lbl := lv_label_create(row); sep := stringConcat('[/] ', dir_names^[i]); lv_label_set_text(name_lbl, sep); kfree(void(sep)); lv_obj_set_flex_grow(name_lbl, 1); lv_obj_set_style_text_color(name_lbl, lv_color_make(200, 215, 240), 0); lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0); { Type indicator } size_lbl := lv_label_create(row); lv_label_set_text(size_lbl, ''); lv_obj_set_width(size_lbl, 70); lv_obj_set_style_text_color(size_lbl, lv_color_make(100, 160, 255), 0); lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0); lv_obj_set_style_text_align(size_lbl, LV_TEXT_ALIGN_RIGHT, 0); copy := stringCopy(dir_names^[i]); lv_obj_set_user_data(row, copy); lv_obj_add_event_cb(row, @fp_dir_cb, LV_EVENT_CLICKED, p); end; { --- Build file rows --- } if file_count > 0 then for i := 0 to file_count - 1 do begin row := lv_obj_create(p^.file_list); lv_obj_remove_style_all(row); lv_obj_set_size(row, lv_pct(100), 30); lv_obj_set_style_bg_color(row, lv_color_make(28, 31, 44), 0); lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(row, lv_color_make(42, 46, 62), LV_STATE_PRESSED); lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED); 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_add_flag(row, LV_OBJ_FLAG_CLICKABLE); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); name_lbl := lv_label_create(row); lv_label_set_text(name_lbl, file_names^[i]); lv_obj_set_flex_grow(name_lbl, 1); lv_obj_set_style_text_color(name_lbl, lv_color_make(210, 215, 230), 0); lv_obj_set_style_text_font(name_lbl, @lv_font_montserrat_14, 0); { File size column: use metadata from directory listing } size_lbl := lv_label_create(row); szStr := fmtFileSize(file_sizes^[i]); lv_label_set_text(size_lbl, szStr); kfree(void(szStr)); lv_obj_set_width(size_lbl, 70); lv_obj_set_style_text_color(size_lbl, lv_color_make(100, 110, 130), 0); lv_obj_set_style_text_font(size_lbl, @lv_font_montserrat_14, 0); lv_obj_set_style_text_align(size_lbl, LV_TEXT_ALIGN_RIGHT, 0); copy := stringCopy(file_names^[i]); lv_obj_set_user_data(row, copy); lv_obj_add_event_cb(row, @fp_file_cb, LV_EVENT_CLICKED, p); end; io.syslog.logln('FPCIK', 'do_refresh: building UI rows done, freeing map'); driver.storage.vfs.FreeDirectoryListing(Map); kfree(void(dir_names)); kfree(void(file_names)); kfree(void(file_sizes)); { Update status label: "X dirs, Y files" } if p^.status_label <> nil then begin s1 := intToString(dir_count); s2 := stringConcat(s1, ' dirs, '); kfree(void(s1)); s1 := intToString(file_count); s3 := stringConcat(s2, s1); kfree(void(s1)); kfree(void(s2)); s1 := stringConcat(s3, ' files'); kfree(void(s3)); lv_label_set_text(p^.status_label, s1); lv_obj_set_style_text_color(p^.status_label, lv_color_make(140, 150, 170), 0); kfree(void(s1)); end; io.syslog.logln('FPCIK', 'do_refresh: exit'); debug.tracer.pop_trace; end; { ============================================================ schedule_refresh / fp_refresh_timer_cb Defers do_refresh via a 1 ms LVGL one-shot timer so VFS I/O runs outside the event-dispatch stack. ============================================================ } procedure fp_refresh_timer_cb(tmr: Plv_timer); cdecl; var p: PPicker; begin io.syslog.logln('FPCIK', 'fp_refresh_timer_cb: fired'); p := PPicker(lv_timer_get_user_data(tmr)); lv_timer_delete(tmr); if p = nil then begin io.syslog.logln('FPCIK', 'fp_refresh_timer_cb: p nil'); exit; end; p^.pending_refresh := false; do_refresh(p); end; procedure schedule_refresh(p: PPicker); var tmr: Plv_timer; begin if p^.pending_refresh then exit; p^.pending_refresh := true; tmr := lv_timer_create(@fp_refresh_timer_cb, 1, p); lv_timer_set_repeat_count(tmr, 1); end; { ============================================================ Navigation event callbacks ============================================================ } procedure fp_dir_cb(e: Plv_event); cdecl; var p : PPicker; target : Plv_obj; name : pchar; sep, np: pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); target := lv_event_get_current_target_obj(e); name := pchar(lv_obj_get_user_data(target)); if (p = nil) or (name = nil) then exit; if p^.pending_refresh then exit; if stringEquals(p^.cur_dir, '/') then np := stringConcat('/', name) else begin sep := stringConcat(p^.cur_dir, '/'); np := stringConcat(sep, name); kfree(void(sep)); end; navigate_to(p, np); kfree(void(np)); end; procedure fp_file_cb(e: Plv_event); cdecl; var p : PPicker; target : Plv_obj; name : pchar; sep, fp: pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); target := lv_event_get_current_target_obj(e); name := pchar(lv_obj_get_user_data(target)); if (p = nil) or (name = nil) then exit; if p^.pending_refresh then exit; if stringEquals(p^.cur_dir, '/') then fp := stringConcat('/', name) else begin sep := stringConcat(p^.cur_dir, '/'); fp := stringConcat(sep, name); kfree(void(sep)); end; if p^.is_save then begin { Save mode: populate fname_bar for review; reset overwrite guard } if p^.fname_bar <> nil then lv_textarea_set_text(p^.fname_bar, name); p^.confirm_pending := false; if p^.status_label <> nil then begin lv_label_set_text(p^.status_label, ''); end; kfree(void(fp)); end else begin { Open mode: auto-confirm } picker_close(p, fp); { fp now owned by callback — do NOT kfree } end; end; procedure fp_up_cb(e: Plv_event); cdecl; var p : PPicker; dir : pchar; len, i : uint32; lastSep : sint32; newDir : pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; if p^.pending_refresh then exit; if stringEquals(p^.cur_dir, '/') then exit; dir := p^.cur_dir; len := stringSize(dir); lastSep := -1; for i := 0 to len - 1 do if dir[i] = '/' then lastSep := sint32(i); if lastSep <= 0 then newDir := stringCopy('/') else newDir := stringSub(dir, 0, uint32(lastSep)); navigate_to(p, newDir); kfree(void(newDir)); end; procedure fp_back_cb(e: Plv_event); cdecl; var p : PPicker; newDir : pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; if p^.pending_refresh then exit; if p^.hist_pos <= 0 then exit; p^.hist_pos := p^.hist_pos - 1; newDir := p^.history[p^.hist_pos]; if p^.cur_dir <> nil then kfree(void(p^.cur_dir)); p^.cur_dir := stringCopy(newDir); p^.confirm_pending := false; update_nav_btns(p); schedule_refresh(p); end; procedure fp_fwd_cb(e: Plv_event); cdecl; var p : PPicker; newDir : pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; if p^.pending_refresh then exit; if p^.hist_pos >= p^.hist_len - 1 then exit; p^.hist_pos := p^.hist_pos + 1; newDir := p^.history[p^.hist_pos]; if p^.cur_dir <> nil then kfree(void(p^.cur_dir)); p^.cur_dir := stringCopy(newDir); p^.confirm_pending := false; update_nav_btns(p); schedule_refresh(p); end; procedure fp_bkmk_cb(e: Plv_event); cdecl; var p : PPicker; target : Plv_obj; path : pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); target := lv_event_get_current_target_obj(e); path := pchar(lv_obj_get_user_data(target)); if (p = nil) or (path = nil) then exit; if p^.pending_refresh then exit; navigate_to(p, path); end; { ============================================================ Confirm / Cancel callbacks ============================================================ } procedure fp_confirm_cb(e: Plv_event); cdecl; var p : PPicker; path : pchar; name : pchar; sep : pchar; s1 : pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; if p^.pending_refresh then exit; if p^.is_save then begin if p^.fname_bar <> nil then name := lv_textarea_get_text(p^.fname_bar) else name := lv_textarea_get_text(p^.path_bar); if (name = nil) or (stringSize(name) = 0) then exit; if name[0] = '/' then path := stringCopy(name) else if stringEquals(p^.cur_dir, '/') then path := stringConcat('/', name) else begin sep := stringConcat(p^.cur_dir, '/'); path := stringConcat(sep, name); kfree(void(sep)); end; { Overwrite guard: warn on first click, confirm on second } if (not p^.confirm_pending) and (driver.storage.vfs.PathValid(path) = pvFile) then begin p^.confirm_pending := true; if p^.status_label <> nil then begin s1 := stringConcat(name, ' exists — click Save again to overwrite'); lv_label_set_text(p^.status_label, s1); lv_obj_set_style_text_color(p^.status_label, lv_color_make(220, 160, 50), 0); kfree(void(s1)); end; kfree(void(path)); exit; end; picker_close(p, path); { path now owned by callback } end else begin { Open mode: accept whatever is in the path_bar } name := lv_textarea_get_text(p^.path_bar); if (name = nil) or (stringSize(name) = 0) then exit; path := stringCopy(name); picker_close(p, path); end; end; procedure fp_cancel_cb(e: Plv_event); cdecl; var p: PPicker; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; if p^.pending_refresh then exit; picker_close(p, nil); end; { ============================================================ New Folder callbacks ============================================================ } procedure fp_newfolder_btn_cb(e: Plv_event); cdecl; var p: PPicker; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; lv_textarea_set_text(p^.newfolder_bar, ''); lv_obj_add_flag(p^.action_row, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(p^.newfolder_row, LV_OBJ_FLAG_HIDDEN); end; procedure fp_newfolder_create_cb(e: Plv_event); cdecl; var p : PPicker; fname : pchar; sep, fp : pchar; err : TError; s1 : pchar; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; fname := lv_textarea_get_text(p^.newfolder_bar); if (fname = nil) or (stringSize(fname) = 0) then exit; if stringEquals(p^.cur_dir, '/') then fp := stringConcat('/', fname) else begin sep := stringConcat(p^.cur_dir, '/'); fp := stringConcat(sep, fname); kfree(void(sep)); end; err := driver.storage.vfs.newVirtualDirectory(fp); kfree(void(fp)); { Restore action row regardless of outcome } lv_obj_remove_flag(p^.action_row, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(p^.newfolder_row, LV_OBJ_FLAG_HIDDEN); if err = eNone then begin { Refresh to show the new folder } schedule_refresh(p); end else begin if p^.status_label <> nil then begin s1 := stringConcat('Could not create folder: ', fname); lv_label_set_text(p^.status_label, s1); lv_obj_set_style_text_color(p^.status_label, lv_color_make(200, 80, 80), 0); kfree(void(s1)); end; end; end; procedure fp_newfolder_cancel_cb(e: Plv_event); cdecl; var p: PPicker; begin if lv_event_get_code(e) <> LV_EVENT_CLICKED then exit; p := PPicker(lv_event_get_user_data(e)); if p = nil then exit; lv_obj_remove_flag(p^.action_row, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(p^.newfolder_row, LV_OBJ_FLAG_HIDDEN); end; { ============================================================ make_btn — small helper for action-row buttons Returns the created button object. ============================================================ } function make_btn(parent: Plv_obj; text: pchar; w: sint32; br, bg, bb: uint8; cb: lv_event_cb_t; ud: pointer): Plv_obj; var btn, lbl: Plv_obj; begin btn := lv_button_create(parent); lv_obj_remove_style_all(btn); lv_obj_set_size(btn, w, 30); lv_obj_set_style_bg_color(btn, lv_color_make(br, bg, bb), 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 := lv_label_create(btn); lv_label_set_text(lbl, text); 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_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, ud); make_btn := btn; end; { ============================================================ add_bookmark — helper to add one entry to the bookmarks panel ============================================================ } procedure add_bookmark(parent: Plv_obj; name: pchar; path: pchar; p: PPicker); var btn, lbl : Plv_obj; begin btn := lv_button_create(parent); lv_obj_remove_style_all(btn); lv_obj_set_size(btn, lv_pct(100), 30); lv_obj_set_style_bg_color(btn, lv_color_make(38, 42, 56), 0); lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0); lv_obj_set_style_bg_color(btn, lv_color_make(50, 55, 72), 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_set_style_radius(btn, 0, 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, name); 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, stringCopy(path)); lv_obj_add_event_cb(btn, @fp_bkmk_cb, LV_EVENT_CLICKED, p); end; { ============================================================ make_picker — allocate state, build full picker UI ============================================================ } procedure make_picker(title : pchar; start_dir : pchar; is_save : boolean; initial_name : pchar; filter : pchar; cb : TPickerCallback; userdata : pointer); var p : PPicker; scr : Plv_obj; backdrop : Plv_obj; panel : Plv_obj; title_lbl : Plv_obj; nav_row : Plv_obj; body_row : Plv_obj; bkmk_sep : Plv_obj; bkmk_hdr : Plv_obj; bkmk_panel : Plv_obj; file_list : Plv_obj; status_row : Plv_obj; fname_bar : Plv_obj; action_row : Plv_obj; newfolder_row : Plv_obj; newfolder_bar : Plv_obj; spacer : Plv_obj; conf_text : pchar; back_btn : Plv_obj; fwd_btn : Plv_obj; begin debug.tracer.push_trace('filepicker.make_picker'); io.syslog.logln('FPCIK', 'make_picker: enter'); p := PPicker(kalloc(sizeof(TPicker))); memset(uint32(p), 0, sizeof(TPicker)); p^.cb := cb; p^.userdata := userdata; p^.is_save := is_save; p^.filter := filter; p^.hist_pos := -1; p^.hist_len := 0; if (start_dir <> nil) and (stringSize(start_dir) > 0) then p^.cur_dir := stringCopy(start_dir) else p^.cur_dir := stringCopy('/disk'); { Seed history with start_dir so Back is immediately available after the first navigation } push_history(p, p^.cur_dir); scr := lv_screen_active; { ====== Full-screen backdrop ====== } backdrop := lv_obj_create(scr); lv_obj_remove_style_all(backdrop); lv_obj_set_size(backdrop, lv_pct(100), lv_pct(100)); lv_obj_set_pos(backdrop, 0, 0); lv_obj_set_style_bg_color(backdrop, lv_color_make(0, 0, 0), 0); lv_obj_set_style_bg_opa(backdrop, 140, 0); lv_obj_set_style_pad_all(backdrop, 0, 0); lv_obj_remove_flag(backdrop, LV_OBJ_FLAG_SCROLLABLE); lv_obj_move_foreground(backdrop); p^.backdrop := backdrop; { ====== Centered panel ====== } panel := lv_obj_create(backdrop); lv_obj_remove_style_all(panel); lv_obj_set_size(panel, PANEL_W, PANEL_H); lv_obj_align(panel, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_bg_color(panel, lv_color_make(22, 25, 36), 0); lv_obj_set_style_bg_opa(panel, LV_OPA_COVER, 0); lv_obj_set_style_border_color(panel, lv_color_make(48, 52, 68), 0); lv_obj_set_style_border_width(panel, 1, 0); lv_obj_set_style_radius(panel, 6, 0); lv_obj_set_style_pad_all(panel, 10, 0); lv_obj_set_style_pad_row(panel, 6, 0); lv_obj_set_style_layout(panel, LV_LAYOUT_FLEX, 0); lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); { ---- Title ---- } title_lbl := lv_label_create(panel); lv_label_set_text(title_lbl, title); lv_obj_set_style_text_color(title_lbl, lv_color_make(100, 160, 255), 0); lv_obj_set_style_text_font(title_lbl, @lv_font_montserrat_14, 0); { ---- Navigation row: [<] [^] [>] [path_bar...............] ---- } nav_row := lv_obj_create(panel); lv_obj_remove_style_all(nav_row); lv_obj_set_size(nav_row, lv_pct(100), 36); lv_obj_set_style_layout(nav_row, LV_LAYOUT_FLEX, 0); lv_obj_set_flex_flow(nav_row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(nav_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(nav_row, 4, 0); lv_obj_remove_flag(nav_row, LV_OBJ_FLAG_SCROLLABLE); back_btn := make_btn(nav_row, '<', 30, 45, 50, 68, @fp_back_cb, p); make_btn(nav_row, '^', 30, 45, 50, 68, @fp_up_cb, p); fwd_btn := make_btn(nav_row, '>', 30, 45, 50, 68, @fp_fwd_cb, p); p^.back_btn := back_btn; p^.fwd_btn := fwd_btn; { Both start disabled — no back/fwd history yet } lv_obj_remove_flag(back_btn, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_opa(back_btn, 80, 0); lv_obj_remove_flag(fwd_btn, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_opa(fwd_btn, 80, 0); { Path bar — 4th flex child of nav_row, takes remaining width } p^.path_bar := lv_textarea_create(nav_row); lv_obj_set_size(p^.path_bar, 0, 30); lv_obj_set_flex_grow(p^.path_bar, 1); lv_textarea_set_one_line(p^.path_bar, true); lv_textarea_set_text(p^.path_bar, p^.cur_dir); lv_textarea_set_placeholder_text(p^.path_bar, '/disk'); lv_obj_set_style_bg_color(p^.path_bar, lv_color_make(35, 38, 55), LV_PART_MAIN); lv_obj_set_style_bg_opa(p^.path_bar, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_text_color(p^.path_bar, lv_color_make(200, 210, 240), LV_PART_MAIN); lv_obj_set_style_text_font(p^.path_bar, @lv_font_montserrat_14, LV_PART_MAIN); lv_obj_set_style_border_color(p^.path_bar, lv_color_make(55, 70, 110), LV_PART_MAIN); lv_obj_set_style_border_width(p^.path_bar, 1, LV_PART_MAIN); lv_obj_set_style_radius(p^.path_bar, 4, LV_PART_MAIN); lv_group_add_obj(lvgl_get_kb_group, p^.path_bar); { ---- Body row: [bookmarks 120px] | [file list, flex_grow] ---- } body_row := lv_obj_create(panel); 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_flex_align(body_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); lv_obj_set_style_pad_column(body_row, 0, 0); lv_obj_remove_flag(body_row, LV_OBJ_FLAG_SCROLLABLE); { Bookmarks sidebar } bkmk_panel := lv_obj_create(body_row); lv_obj_remove_style_all(bkmk_panel); lv_obj_set_size(bkmk_panel, BKMK_W, lv_pct(100)); lv_obj_set_style_bg_color(bkmk_panel, lv_color_make(28, 31, 40), 0); lv_obj_set_style_bg_opa(bkmk_panel, LV_OPA_COVER, 0); lv_obj_set_style_border_color(bkmk_panel, lv_color_make(48, 52, 68), 0); lv_obj_set_style_border_side(bkmk_panel, LV_BORDER_SIDE_RIGHT, 0); lv_obj_set_style_border_width(bkmk_panel, 1, 0); lv_obj_set_style_layout(bkmk_panel, LV_LAYOUT_FLEX, 0); lv_obj_set_flex_flow(bkmk_panel, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(bkmk_panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); lv_obj_set_style_pad_all(bkmk_panel, 0, 0); lv_obj_set_style_pad_row(bkmk_panel, 0, 0); lv_obj_remove_flag(bkmk_panel, LV_OBJ_FLAG_SCROLLABLE); p^.bkmk_panel := bkmk_panel; { Bookmarks header label } bkmk_hdr := lv_label_create(bkmk_panel); lv_label_set_text(bkmk_hdr, 'Locations'); lv_obj_set_style_text_color(bkmk_hdr, lv_color_make(140, 150, 170), 0); lv_obj_set_style_text_font(bkmk_hdr, @lv_font_montserrat_14, 0); lv_obj_set_style_pad_top(bkmk_hdr, 6, 0); lv_obj_set_style_pad_left(bkmk_hdr, 10, 0); lv_obj_set_style_pad_bottom(bkmk_hdr, 4, 0); bkmk_sep := lv_obj_create(bkmk_panel); lv_obj_remove_style_all(bkmk_sep); lv_obj_set_size(bkmk_sep, lv_pct(100), 1); lv_obj_set_style_bg_color(bkmk_sep, lv_color_make(55, 60, 78), 0); lv_obj_set_style_bg_opa(bkmk_sep, LV_OPA_COVER, 0); add_bookmark(bkmk_panel, 'Root', '/', p); add_bookmark(bkmk_panel, 'Disk', '/disk', p); add_bookmark(bkmk_panel, 'Boot', '/boot', p); { File list — takes remaining width, scrollable } file_list := lv_obj_create(body_row); lv_obj_remove_style_all(file_list); lv_obj_set_size(file_list, 0, lv_pct(100)); lv_obj_set_flex_grow(file_list, 1); lv_obj_set_style_bg_color(file_list, lv_color_make(28, 31, 44), 0); lv_obj_set_style_bg_opa(file_list, LV_OPA_COVER, 0); lv_obj_set_style_layout(file_list, LV_LAYOUT_FLEX, 0); lv_obj_set_flex_flow(file_list, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(file_list, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); lv_obj_set_style_pad_all(file_list, 0, 0); lv_obj_set_style_pad_row(file_list, 1, 0); lv_obj_set_scroll_dir(file_list, LV_DIR_VER); p^.file_list := file_list; { ---- Status strip ---- } status_row := lv_obj_create(panel); lv_obj_remove_style_all(status_row); lv_obj_set_size(status_row, lv_pct(100), 20); lv_obj_set_style_pad_left(status_row, 4, 0); lv_obj_remove_flag(status_row, LV_OBJ_FLAG_SCROLLABLE); p^.status_label := lv_label_create(status_row); lv_label_set_text(p^.status_label, ''); lv_obj_set_style_text_color(p^.status_label, lv_color_make(140, 150, 170), 0); lv_obj_set_style_text_font(p^.status_label, @lv_font_montserrat_14, 0); { ---- Filename bar (save mode only) ---- } fname_bar := nil; if is_save then begin fname_bar := lv_textarea_create(panel); lv_obj_set_size(fname_bar, lv_pct(100), 34); lv_textarea_set_one_line(fname_bar, true); if (initial_name <> nil) and (stringSize(initial_name) > 0) then lv_textarea_set_text(fname_bar, initial_name) else lv_textarea_set_text(fname_bar, ''); lv_textarea_set_placeholder_text(fname_bar, 'filename.txt'); lv_obj_set_style_bg_color(fname_bar, lv_color_make(28, 35, 28), LV_PART_MAIN); lv_obj_set_style_bg_opa(fname_bar, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_text_color(fname_bar, lv_color_make(190, 235, 190), LV_PART_MAIN); lv_obj_set_style_text_font(fname_bar, @lv_font_montserrat_14, LV_PART_MAIN); lv_obj_set_style_border_color(fname_bar, lv_color_make(55, 130, 55), LV_PART_MAIN); lv_obj_set_style_border_width(fname_bar, 1, LV_PART_MAIN); lv_obj_set_style_radius(fname_bar, 4, LV_PART_MAIN); lv_group_add_obj(lvgl_get_kb_group, fname_bar); lv_group_focus_obj(fname_bar); end; p^.fname_bar := fname_bar; { ---- Action row: [New Folder] [Cancel] [Open/Save] ---- } action_row := lv_obj_create(panel); lv_obj_remove_style_all(action_row); lv_obj_set_size(action_row, lv_pct(100), 36); lv_obj_set_style_layout(action_row, LV_LAYOUT_FLEX, 0); lv_obj_set_flex_flow(action_row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(action_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(action_row, 6, 0); lv_obj_remove_flag(action_row, LV_OBJ_FLAG_SCROLLABLE); p^.action_row := action_row; make_btn(action_row, '+ Folder', 80, 45, 50, 68, @fp_newfolder_btn_cb, p); spacer := lv_obj_create(action_row); lv_obj_remove_style_all(spacer); lv_obj_set_flex_grow(spacer, 1); lv_obj_set_size(spacer, 0, 30); make_btn(action_row, 'Cancel', 72, 60, 40, 40, @fp_cancel_cb, p); if is_save then conf_text := 'Save' else conf_text := 'Open'; make_btn(action_row, conf_text, 72, 55, 90, 190, @fp_confirm_cb, p); { ---- New Folder row (hidden until '+ Folder' clicked) ---- } newfolder_row := lv_obj_create(panel); lv_obj_remove_style_all(newfolder_row); lv_obj_set_size(newfolder_row, lv_pct(100), 36); lv_obj_set_style_layout(newfolder_row, LV_LAYOUT_FLEX, 0); lv_obj_set_flex_flow(newfolder_row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(newfolder_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(newfolder_row, 6, 0); lv_obj_remove_flag(newfolder_row, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(newfolder_row, LV_OBJ_FLAG_HIDDEN); p^.newfolder_row := newfolder_row; newfolder_bar := lv_textarea_create(newfolder_row); lv_obj_set_size(newfolder_bar, 0, 30); lv_obj_set_flex_grow(newfolder_bar, 1); lv_textarea_set_one_line(newfolder_bar, true); lv_textarea_set_text(newfolder_bar, ''); lv_textarea_set_placeholder_text(newfolder_bar, 'New folder name'); lv_obj_set_style_bg_color(newfolder_bar, lv_color_make(35, 38, 55), LV_PART_MAIN); lv_obj_set_style_bg_opa(newfolder_bar, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_text_color(newfolder_bar, lv_color_make(200, 210, 240), LV_PART_MAIN); lv_obj_set_style_text_font(newfolder_bar, @lv_font_montserrat_14, LV_PART_MAIN); lv_obj_set_style_border_color(newfolder_bar, lv_color_make(55, 70, 110), LV_PART_MAIN); lv_obj_set_style_border_width(newfolder_bar, 1, LV_PART_MAIN); lv_obj_set_style_radius(newfolder_bar, 4, LV_PART_MAIN); lv_group_add_obj(lvgl_get_kb_group, newfolder_bar); p^.newfolder_bar := newfolder_bar; make_btn(newfolder_row, 'Create', 72, 55, 90, 190, @fp_newfolder_create_cb, p); make_btn(newfolder_row, 'x', 30, 60, 40, 40, @fp_newfolder_cancel_cb, p); { Kick off first directory listing } io.syslog.logln('FPCIK', 'make_picker: scheduling initial refresh'); schedule_refresh(p); io.syslog.logln('FPCIK', 'make_picker: exit'); debug.tracer.pop_trace; end; { ============================================================ Public API ============================================================ } procedure show_open(title : pchar; start_dir : pchar; filter : pchar; cb : TPickerCallback; userdata : pointer); begin io.syslog.logln('FPCIK', 'show_open: enter'); make_picker(title, start_dir, false, nil, filter, cb, userdata); end; procedure show_save(title : pchar; start_dir : pchar; initial_name : pchar; filter : pchar; cb : TPickerCallback; userdata : pointer); begin io.syslog.logln('FPCIK', 'show_save: enter'); make_picker(title, start_dir, true, initial_name, filter, cb, userdata); end; end.