feat: expand Files features; add file associations
This commit is contained in:
1 parent
23cc56b933
commit
e047c1e0c0
22 files changed
+1359
-238
No files matched your search
@@ -49,6 +49,9 @@ void filemanager_do_paste(FileManagerState* fm) {
|
||||
|
||||
void filemanager_start_rename(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
// Rename is single-target; with several entries selected, `selected` is
|
||||
// only the focus and renaming it would be a surprise.
|
||||
if (fm->multi_count > 1) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
||||
fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||
|
||||
@@ -228,10 +231,14 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||
} else {
|
||||
bool several = fm->multi_count > 1;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
if (!several && !fm->is_dir[target_idx])
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN_WITH;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_COPY;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_CUT;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_RENAME;
|
||||
if (!several)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_RENAME;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||
}
|
||||
@@ -247,6 +254,13 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
if (fm->clipboard_count > 0)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW_FOLDER;
|
||||
if (fm->entry_count > 0)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SELECT_ALL;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_NAME;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_SIZE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_TYPE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_SORT_MODIFIED;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_REFRESH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ static void open_filemanager_internal(DesktopState* ds, const char* initial_path
|
||||
FileManagerState* fm = (FileManagerState*)montauk::malloc(sizeof(FileManagerState));
|
||||
montauk::memset(fm, 0, sizeof(FileManagerState));
|
||||
fm->selected = -1;
|
||||
fm->sel_anchor = -1;
|
||||
fm->last_click_item = -1;
|
||||
fm->history_pos = -1;
|
||||
fm->history_count = 0;
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* associations.cpp
|
||||
* File type associations for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
#include <montauk/config.h>
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
// Per-user overrides live in <home>/config/associations.toml as
|
||||
// [open_with]
|
||||
// jpg = "paint"
|
||||
// mapping an extension to the app id of the user's chosen default.
|
||||
static constexpr const char* ASSOC_CONFIG_NAME = "associations";
|
||||
|
||||
static bool assoc_item_launchable(const DesktopState* ds, const DesktopItem& item) {
|
||||
if (item.launch_kind != DESKTOP_ITEM_LAUNCH_EXECUTABLE || !item.binary_path[0]) return false;
|
||||
return !(item.admin_only && !ds->is_admin);
|
||||
}
|
||||
|
||||
// file_types is stored as " ext1 ext2 ", so a whole-word match is " ext ".
|
||||
static bool assoc_item_claims(const DesktopItem& item, const char* ext) {
|
||||
if (!ext[0] || !item.file_types[0]) return false;
|
||||
int elen = montauk::slen(ext);
|
||||
for (const char* p = item.file_types; *p; p++) {
|
||||
if (*p != ' ') continue;
|
||||
int k = 0;
|
||||
while (k < elen && p[1 + k] == ext[k]) k++;
|
||||
if (k == elen && p[1 + elen] == ' ') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The config writer emits bare keys, so only extensions made of bare-key
|
||||
// characters can be remembered.
|
||||
static bool assoc_ext_storable(const char* ext) {
|
||||
if (!ext || !ext[0] || montauk::slen(ext) > 15) return false;
|
||||
for (const char* p = ext; *p; p++) {
|
||||
char c = *p;
|
||||
bool ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-';
|
||||
if (!ok) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void assoc_config_key(char* out, int out_max, const char* ext) {
|
||||
snprintf(out, out_max, "open_with.%s", ext);
|
||||
}
|
||||
|
||||
static int assoc_find_item(const DesktopState* ds, const char* id) {
|
||||
for (int i = 0; i < ds->desktop_item_count; i++) {
|
||||
const DesktopItem& item = ds->desktop_items[i];
|
||||
if (montauk::streq(item.id, id) && assoc_item_launchable(ds, item)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int filemanager_assoc_default(DesktopState* ds, const char* ext) {
|
||||
if (!ds || !ext || !ext[0]) return -1;
|
||||
|
||||
if (ds->current_user[0] && assoc_ext_storable(ext)) {
|
||||
auto doc = montauk::config::load_user(ds->current_user, ASSOC_CONFIG_NAME);
|
||||
char key[48];
|
||||
assoc_config_key(key, sizeof(key), ext);
|
||||
const char* id = doc.get_string(key, "");
|
||||
int idx = id[0] ? assoc_find_item(ds, id) : -1;
|
||||
doc.destroy();
|
||||
if (idx >= 0) return idx;
|
||||
}
|
||||
|
||||
// Several apps may claim an extension; pick by name so the choice does
|
||||
// not depend on directory enumeration order.
|
||||
int best = -1;
|
||||
for (int i = 0; i < ds->desktop_item_count; i++) {
|
||||
const DesktopItem& item = ds->desktop_items[i];
|
||||
if (!assoc_item_launchable(ds, item) || !assoc_item_claims(item, ext)) continue;
|
||||
if (best < 0 || str_compare_ci(item.name, ds->desktop_items[best].name) < 0) best = i;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
int filemanager_assoc_candidates(DesktopState* ds, const char* ext,
|
||||
int* out, int max, int* recommended) {
|
||||
if (recommended) *recommended = 0;
|
||||
if (!ds || !out || max <= 0 || ds->desktop_item_count <= 0) return 0;
|
||||
|
||||
int* sorted = (int*)montauk::malloc((uint64_t)ds->desktop_item_count * sizeof(int));
|
||||
if (!sorted) return 0;
|
||||
int n = 0;
|
||||
for (int i = 0; i < ds->desktop_item_count; i++) {
|
||||
if (assoc_item_launchable(ds, ds->desktop_items[i])) sorted[n++] = i;
|
||||
}
|
||||
for (int i = 1; i < n; i++) {
|
||||
int tmp = sorted[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && str_compare_ci(ds->desktop_items[tmp].name,
|
||||
ds->desktop_items[sorted[j]].name) < 0) {
|
||||
sorted[j + 1] = sorted[j];
|
||||
j--;
|
||||
}
|
||||
sorted[j + 1] = tmp;
|
||||
}
|
||||
|
||||
// Apps that declare the type lead; any other regular app follows (an app
|
||||
// that declares nothing may still open the file). Hidden and settings
|
||||
// items only appear when they claim the type.
|
||||
int count = 0;
|
||||
for (int i = 0; i < n && count < max; i++) {
|
||||
if (assoc_item_claims(ds->desktop_items[sorted[i]], ext)) out[count++] = sorted[i];
|
||||
}
|
||||
if (recommended) *recommended = count;
|
||||
for (int i = 0; i < n && count < max; i++) {
|
||||
const DesktopItem& item = ds->desktop_items[sorted[i]];
|
||||
if (item.section == DESKTOP_ITEM_SECTION_APPS && !assoc_item_claims(item, ext))
|
||||
out[count++] = sorted[i];
|
||||
}
|
||||
|
||||
montauk::mfree(sorted);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool filemanager_assoc_set_default(DesktopState* ds, const char* ext, int item_index) {
|
||||
if (!ds || !ds->current_user[0] || !assoc_ext_storable(ext)) return false;
|
||||
if (item_index < 0 || item_index >= ds->desktop_item_count) return false;
|
||||
|
||||
auto doc = montauk::config::load_user(ds->current_user, ASSOC_CONFIG_NAME);
|
||||
char key[48];
|
||||
assoc_config_key(key, sizeof(key), ext);
|
||||
montauk::config::set_string(&doc, key, ds->desktop_items[item_index].id);
|
||||
int rc = montauk::config::save_user(ds->current_user, ASSOC_CONFIG_NAME, &doc);
|
||||
doc.destroy();
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
bool filemanager_file_looks_textual(const char* path) {
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) return false;
|
||||
uint64_t size = montauk::getsize(fd);
|
||||
if (size == 0) {
|
||||
montauk::close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t buf[1024];
|
||||
uint64_t want = size < sizeof(buf) ? size : sizeof(buf);
|
||||
int got = montauk::read(fd, buf, 0, want);
|
||||
montauk::close(fd);
|
||||
if (got <= 0) return false;
|
||||
|
||||
// NUL or C0 control bytes other than tab/newline/CR/form feed/escape mean
|
||||
// binary data. Bytes >= 0x80 pass: UTF-8 and the 1252 fallback are text.
|
||||
for (int i = 0; i < got; i++) {
|
||||
uint8_t b = buf[i];
|
||||
if (b >= 0x20 || b == '\t' || b == '\n' || b == '\r' || b == 0x0C || b == 0x1B) continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool filemanager_launch_with(DesktopState* ds, int item_index, const char* path) {
|
||||
if (!ds || item_index < 0 || item_index >= ds->desktop_item_count) return false;
|
||||
const DesktopItem& item = ds->desktop_items[item_index];
|
||||
if (!assoc_item_launchable(ds, item)) return false;
|
||||
return desktop_spawn_app(ds, item.binary_path, path) >= 0;
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -8,6 +8,51 @@
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
static void filemanager_show_selection_properties(FileManagerState* fm, int target) {
|
||||
if (fm->multi_count > 1 && fm->entry_count > 0) {
|
||||
int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
|
||||
if (sel) {
|
||||
int n = filemanager_collect_selection(fm, sel, fm->entry_count);
|
||||
if (n > 0) filemanager_show_multi_properties(fm, sel, n);
|
||||
montauk::mfree(sel);
|
||||
}
|
||||
} else {
|
||||
filemanager_show_properties(fm, target);
|
||||
}
|
||||
}
|
||||
|
||||
static bool filemanager_is_listing(const FileManagerState* fm) {
|
||||
return !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
|
||||
}
|
||||
|
||||
// Left click on an entry. Ctrl toggles it, Shift selects the range from the
|
||||
// anchor (Ctrl+Shift adds that range); a plain click selects it alone, and a
|
||||
// second plain click within 400 ms opens it.
|
||||
static void filemanager_click_entry(FileManagerState* fm, int idx) {
|
||||
DesktopState* ds = fm->desktop;
|
||||
bool ctrl = ds && ds->kbd_ctrl_down;
|
||||
bool shift = ds && ds->kbd_shift_down;
|
||||
|
||||
if (ctrl || shift) {
|
||||
if (shift) filemanager_select_range(fm, idx, ctrl);
|
||||
else filemanager_toggle_selection(fm, idx);
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (fm->last_click_item == idx && (now - fm->last_click_time) < 400) {
|
||||
filemanager_open_entry(fm, idx);
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
filemanager_select_single(fm, idx);
|
||||
fm->last_click_item = idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||
if (!fm) return;
|
||||
@@ -26,13 +71,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
// ---- Context menu interaction ----
|
||||
if (fm->ctx_open) {
|
||||
mtk::ContextMenuState state = filemanager_context_state(fm);
|
||||
mtk::ContextMenuItem items[8];
|
||||
filemanager_build_context_items(fm, items, 8);
|
||||
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
||||
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
||||
int action = mtk::context_menu_handle_mouse(state, items,
|
||||
fm->ctx_item_count,
|
||||
local_x, local_y,
|
||||
ev.buttons, ev.prev_buttons,
|
||||
cw, win->content_h);
|
||||
cw, win->content_h,
|
||||
FM_CTX_MENU_W);
|
||||
filemanager_store_context_state(fm, state);
|
||||
|
||||
if (action >= 0) {
|
||||
@@ -50,20 +96,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
case CTX_RENAME: filemanager_start_rename(fm); break;
|
||||
case CTX_DELETE: filemanager_delete_selected(fm); break;
|
||||
case CTX_NEW_FOLDER: filemanager_new_folder(fm); break;
|
||||
case CTX_PROPERTIES: {
|
||||
if (fm->multi_count > 1 && fm->entry_count > 0) {
|
||||
int* sel = (int*)montauk::malloc((uint64_t)fm->entry_count * sizeof(int));
|
||||
if (sel) {
|
||||
int n = filemanager_collect_selection(fm, sel, fm->entry_count);
|
||||
if (n > 0) filemanager_show_multi_properties(fm, sel, n);
|
||||
montauk::mfree(sel);
|
||||
}
|
||||
} else {
|
||||
filemanager_show_properties(fm, target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CTX_PROPERTIES: filemanager_show_selection_properties(fm, target); break;
|
||||
case CTX_REFRESH: filemanager_refresh(fm); break;
|
||||
case CTX_OPEN_WITH: filemanager_show_open_with(fm, target); break;
|
||||
case CTX_SELECT_ALL: filemanager_select_all(fm); break;
|
||||
case CTX_SORT_NAME: filemanager_set_sort(fm, FM_SORT_NAME); break;
|
||||
case CTX_SORT_SIZE: filemanager_set_sort(fm, FM_SORT_SIZE); break;
|
||||
case CTX_SORT_TYPE: filemanager_set_sort(fm, FM_SORT_TYPE); break;
|
||||
case CTX_SORT_MODIFIED: filemanager_set_sort(fm, FM_SORT_MODIFIED); break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -123,23 +163,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
int clicked_idx = filemanager_grid_index_at(fm, cols, local_x, rel_y);
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
filemanager_clear_multi_selection(fm);
|
||||
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
filemanager_open_entry(fm, clicked_idx);
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
filemanager_click_entry(fm, clicked_idx);
|
||||
} else {
|
||||
// Background click in grid area: start marquee drag
|
||||
fm->selected = -1;
|
||||
filemanager_clear_multi_selection(fm);
|
||||
filemanager_select_single(fm, -1);
|
||||
fm->marquee_active = true;
|
||||
fm->marquee_start_x = local_x;
|
||||
fm->marquee_start_y = rel_y;
|
||||
@@ -149,30 +176,30 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
}
|
||||
} else {
|
||||
// List view clicks
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
int list_y = header_y + FM_HEADER_H;
|
||||
if (local_y >= header_y && local_y < list_y && filemanager_is_listing(fm)) {
|
||||
// Column header click: sort by that column (again = reverse).
|
||||
FileManagerListColumns cols = filemanager_list_columns(cw);
|
||||
if (cols.type_x >= 0 && local_x >= cols.type_x)
|
||||
filemanager_set_sort(fm, FM_SORT_TYPE);
|
||||
else if (cols.size_x >= 0 && local_x >= cols.size_x)
|
||||
filemanager_set_sort(fm, FM_SORT_SIZE);
|
||||
else if (cols.modified_x >= 0 && local_x >= cols.modified_x - 4)
|
||||
filemanager_set_sort(fm, FM_SORT_MODIFIED);
|
||||
else
|
||||
filemanager_set_sort(fm, FM_SORT_NAME);
|
||||
return;
|
||||
}
|
||||
if (local_y >= list_y && local_x < cw - FM_SCROLLBAR_W) {
|
||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
||||
int clicked_idx = rel_y / FM_ITEM_H;
|
||||
|
||||
if (clicked_idx >= 0 && clicked_idx < fm->entry_count) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
filemanager_clear_multi_selection(fm);
|
||||
|
||||
// Double-click detection
|
||||
if (fm->last_click_item == clicked_idx &&
|
||||
(now - fm->last_click_time) < 400) {
|
||||
filemanager_open_entry(fm, clicked_idx);
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
} else {
|
||||
fm->selected = clicked_idx;
|
||||
fm->last_click_item = clicked_idx;
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
filemanager_click_entry(fm, clicked_idx);
|
||||
} else {
|
||||
// Background click in list area: start marquee drag
|
||||
fm->selected = -1;
|
||||
filemanager_clear_multi_selection(fm);
|
||||
filemanager_select_single(fm, -1);
|
||||
fm->marquee_active = true;
|
||||
fm->marquee_start_x = local_x;
|
||||
fm->marquee_start_y = rel_y;
|
||||
@@ -271,13 +298,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
target_idx = idx;
|
||||
// Preserve multi-selection if right-clicking inside it,
|
||||
// otherwise replace it with a single selection.
|
||||
if (!(fm->multi_count > 0 && fm->multi_selected[idx])) {
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->selected = idx;
|
||||
}
|
||||
if (!filemanager_is_entry_selected(fm, idx))
|
||||
filemanager_select_single(fm, idx);
|
||||
} else {
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->selected = -1;
|
||||
filemanager_select_single(fm, -1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -289,13 +313,10 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
target_idx = idx;
|
||||
// Preserve multi-selection if right-clicking inside it,
|
||||
// otherwise replace it with a single selection.
|
||||
if (!(fm->multi_count > 0 && fm->multi_selected[idx])) {
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->selected = idx;
|
||||
}
|
||||
if (!filemanager_is_entry_selected(fm, idx))
|
||||
filemanager_select_single(fm, idx);
|
||||
} else {
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->selected = -1;
|
||||
filemanager_select_single(fm, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,27 +340,103 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the selected row inside the visible list area so arrow-key navigation
|
||||
static int filemanager_grid_cols(Window* win) {
|
||||
int cols = (win->content_rect().w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
return cols < 1 ? 1 : cols;
|
||||
}
|
||||
|
||||
// Keep the focused entry inside the visible area so keyboard navigation
|
||||
// scrolls the view along with the selection.
|
||||
static void filemanager_scroll_to_selected(Window* win, FileManagerState* fm) {
|
||||
if (fm->grid_view) return;
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
int list_y = fm->grid_view ? FM_TOOLBAR_H + FM_PATHBAR_H
|
||||
: FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
|
||||
int list_h = win->content_h - list_y;
|
||||
if (list_h <= 0) return;
|
||||
|
||||
int top = fm->selected * FM_ITEM_H;
|
||||
int top, item_h, content_h;
|
||||
if (fm->grid_view) {
|
||||
int cols = filemanager_grid_cols(win);
|
||||
int row = fm->selected / cols;
|
||||
top = filemanager_grid_row_y(fm, row, cols);
|
||||
item_h = filemanager_grid_row_height(fm, row, cols);
|
||||
content_h = filemanager_grid_content_height(fm, cols);
|
||||
} else {
|
||||
top = fm->selected * FM_ITEM_H;
|
||||
item_h = FM_ITEM_H;
|
||||
content_h = fm->entry_count * FM_ITEM_H;
|
||||
}
|
||||
|
||||
int off = fm->scrollbar.scroll_offset;
|
||||
if (top < off) off = top;
|
||||
else if (top + FM_ITEM_H > off + list_h) off = top + FM_ITEM_H - list_h;
|
||||
else if (top + item_h > off + list_h) off = top + item_h - list_h;
|
||||
|
||||
int max_scroll = fm->entry_count * FM_ITEM_H - list_h;
|
||||
int max_scroll = content_h - list_h;
|
||||
if (off > max_scroll) off = max_scroll;
|
||||
if (off < 0) off = 0;
|
||||
fm->scrollbar.scroll_offset = off;
|
||||
}
|
||||
|
||||
// Move keyboard focus to target (clamped). With extend (Shift held) the
|
||||
// selection grows from the anchor instead of moving with the focus.
|
||||
static void filemanager_move_focus(Window* win, FileManagerState* fm, int target, bool extend) {
|
||||
if (fm->entry_count <= 0) return;
|
||||
if (target < 0) target = 0;
|
||||
if (target >= fm->entry_count) target = fm->entry_count - 1;
|
||||
if (extend) filemanager_select_range(fm, target, false);
|
||||
else filemanager_select_single(fm, target);
|
||||
filemanager_scroll_to_selected(win, fm);
|
||||
}
|
||||
|
||||
static bool filemanager_prefix_match_ci(const char* name, const char* prefix, int len) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
char a = name[i];
|
||||
char b = prefix[i];
|
||||
if (!a) return false;
|
||||
if (a >= 'A' && a <= 'Z') a += 32;
|
||||
if (b >= 'A' && b <= 'Z') b += 32;
|
||||
if (a != b) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Type-to-jump. Keys typed within a second of each other build up a prefix;
|
||||
// repeating a single character instead cycles through the names starting
|
||||
// with it.
|
||||
static void filemanager_typeahead(Window* win, FileManagerState* fm,
|
||||
const montauk::abi::KeyEvent& key) {
|
||||
char ch[4];
|
||||
int n = montauk::utf8::key_utf8(key, ch);
|
||||
if (n <= 0 || fm->entry_count <= 0) return;
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - fm->typeahead_time > 1000) fm->typeahead_len = 0;
|
||||
fm->typeahead_time = now;
|
||||
if (fm->typeahead_len == 0 && ch[0] == ' ') return;
|
||||
if (fm->typeahead_len + n >= (int)sizeof(fm->typeahead)) return;
|
||||
montauk::memcpy(fm->typeahead + fm->typeahead_len, ch, n);
|
||||
fm->typeahead_len += n;
|
||||
fm->typeahead[fm->typeahead_len] = '\0';
|
||||
|
||||
bool cycle = fm->typeahead_len % n == 0;
|
||||
for (int off = n; cycle && off < fm->typeahead_len; off += n) {
|
||||
for (int k = 0; k < n; k++) {
|
||||
if (fm->typeahead[off + k] != ch[k]) { cycle = false; break; }
|
||||
}
|
||||
}
|
||||
int prefix_len = cycle ? n : fm->typeahead_len;
|
||||
|
||||
int start = fm->selected < 0 ? 0 : fm->selected + (cycle ? 1 : 0);
|
||||
for (int k = 0; k < fm->entry_count; k++) {
|
||||
int i = (start + k) % fm->entry_count;
|
||||
if (filemanager_prefix_match_ci(fm->entry_names[i], fm->typeahead, prefix_len)) {
|
||||
filemanager_move_focus(win, fm, i, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
FileManagerState* fm = (FileManagerState*)win->app_data;
|
||||
if (!fm || !key.pressed) return;
|
||||
@@ -411,61 +508,71 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
}
|
||||
|
||||
// ---- Keyboard shortcuts ----
|
||||
if (key.ctrl) {
|
||||
if (key.ascii == 'c' || key.ascii == 'C') {
|
||||
filemanager_do_copy(fm);
|
||||
if (key.ctrl && !key.alt) {
|
||||
char ch = key.ascii;
|
||||
if (ch >= 'A' && ch <= 'Z') ch += 32;
|
||||
switch (ch) {
|
||||
case 'c': filemanager_do_copy(fm); return;
|
||||
case 'x': filemanager_do_cut(fm); return;
|
||||
case 'v': filemanager_do_paste(fm); return;
|
||||
case 'a':
|
||||
if (filemanager_is_listing(fm)) filemanager_select_all(fm);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'x' || key.ascii == 'X') {
|
||||
filemanager_do_cut(fm);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'v' || key.ascii == 'V') {
|
||||
filemanager_do_paste(fm);
|
||||
case 'l': filemanager_start_pathbar(fm); return;
|
||||
case 'n':
|
||||
if (key.shift) filemanager_new_folder(fm);
|
||||
return;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// F2 = rename
|
||||
// F2 = rename, F5 = refresh
|
||||
if (key.scancode == 0x3C) {
|
||||
filemanager_start_rename(fm);
|
||||
return;
|
||||
}
|
||||
if (key.scancode == 0x3F) {
|
||||
filemanager_refresh(fm);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t nav = montauk::nav_scancode(key);
|
||||
int cur = fm->selected;
|
||||
bool have_focus = cur >= 0 && cur < fm->entry_count;
|
||||
int cols = fm->grid_view ? filemanager_grid_cols(win) : 1;
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
filemanager_go_up(fm);
|
||||
} else if (montauk::nav_scancode(key) == 0x48) {
|
||||
// Up arrow
|
||||
if (fm->grid_view) {
|
||||
Rect cr_r = win->content_rect();
|
||||
int cols = (cr_r.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected >= cols) fm->selected -= cols;
|
||||
} else {
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
}
|
||||
filemanager_scroll_to_selected(win, fm);
|
||||
} else if (montauk::nav_scancode(key) == 0x50) {
|
||||
// Down arrow
|
||||
if (fm->grid_view) {
|
||||
Rect cr_r = win->content_rect();
|
||||
int cols = (cr_r.w - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
if (fm->selected + cols < fm->entry_count) fm->selected += cols;
|
||||
} else {
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
}
|
||||
filemanager_scroll_to_selected(win, fm);
|
||||
} else if (montauk::nav_scancode(key) == 0x4B && !key.alt && fm->grid_view) {
|
||||
} else if (key.scancode == 0x01) {
|
||||
// Escape: drop the selection
|
||||
filemanager_select_single(fm, -1);
|
||||
} else if (nav == 0x48 && !key.alt) {
|
||||
// Up arrow (one row up in the grid)
|
||||
int target = !have_focus ? 0 : (cur >= cols ? cur - cols : cur);
|
||||
filemanager_move_focus(win, fm, target, key.shift);
|
||||
} else if (nav == 0x50 && !key.alt) {
|
||||
// Down arrow (one row down in the grid)
|
||||
int target = !have_focus ? 0 : (cur + cols < fm->entry_count ? cur + cols : cur);
|
||||
filemanager_move_focus(win, fm, target, key.shift);
|
||||
} else if (nav == 0x4B && !key.alt && fm->grid_view) {
|
||||
// Left arrow (grid view only)
|
||||
if (fm->selected > 0) fm->selected--;
|
||||
} else if (montauk::nav_scancode(key) == 0x4D && !key.alt && fm->grid_view) {
|
||||
filemanager_move_focus(win, fm, have_focus ? cur - 1 : 0, key.shift);
|
||||
} else if (nav == 0x4D && !key.alt && fm->grid_view) {
|
||||
// Right arrow (grid view only)
|
||||
if (fm->selected < fm->entry_count - 1) fm->selected++;
|
||||
filemanager_move_focus(win, fm, have_focus ? cur + 1 : 0, key.shift);
|
||||
} else if (nav == 0x47) {
|
||||
// Home
|
||||
filemanager_move_focus(win, fm, 0, key.shift);
|
||||
} else if (nav == 0x4F) {
|
||||
// End
|
||||
filemanager_move_focus(win, fm, fm->entry_count - 1, key.shift);
|
||||
} else if ((key.ascii == '\n' || key.ascii == '\r') && key.alt) {
|
||||
// Alt+Enter: properties
|
||||
if (have_focus) filemanager_show_selection_properties(fm, cur);
|
||||
} else if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
if (fm->selected >= 0 && fm->selected < fm->entry_count)
|
||||
filemanager_open_entry(fm, fm->selected);
|
||||
} else if (montauk::nav_scancode(key) == 0x53) {
|
||||
if (have_focus && filemanager_is_entry_selected(fm, cur))
|
||||
filemanager_open_entry(fm, cur);
|
||||
} else if (nav == 0x53) {
|
||||
// Delete key
|
||||
filemanager_delete_selected(fm);
|
||||
} else if (key.alt && montauk::nav_scancode(key) == 0x4B) {
|
||||
@@ -474,6 +581,8 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
} else if (key.alt && montauk::nav_scancode(key) == 0x4D) {
|
||||
// Alt+Right: go forward
|
||||
filemanager_go_forward(fm);
|
||||
} else if (!key.ctrl && !key.alt) {
|
||||
filemanager_typeahead(win, fm, key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,12 +48,21 @@ struct FileManagerVirtualEntry {
|
||||
// misbehaving driver and bounds memory. Far above any realistic directory.
|
||||
inline constexpr int FM_ENTRY_HARD_CAP = 8192;
|
||||
|
||||
inline constexpr int FM_CTX_MAX_ITEMS = 10;
|
||||
|
||||
// Single-file deletes at or below this size are freed inline; larger files go
|
||||
// to the background worker + progress dialog. Freeing a big file rewrites the
|
||||
// filesystem allocation bitmaps and can take seconds on real disks, which would
|
||||
// otherwise stall the desktop (Files runs inside desktop.elf).
|
||||
inline constexpr uint64_t FILEMANAGER_DELETE_INLINE_MAX_BYTES = 4ull * 1024 * 1024;
|
||||
|
||||
enum FileManagerSortKey : uint8_t {
|
||||
FM_SORT_NAME = 0,
|
||||
FM_SORT_SIZE,
|
||||
FM_SORT_TYPE,
|
||||
FM_SORT_MODIFIED,
|
||||
};
|
||||
|
||||
struct FileManagerState {
|
||||
char current_path[256];
|
||||
FileManagerLocation history[16];
|
||||
@@ -67,7 +76,10 @@ struct FileManagerState {
|
||||
int64_t* entry_mtimes; // last-modified time, UTC unix seconds (0 = unknown)
|
||||
int entry_count;
|
||||
int entry_cap; // allocated capacity of the per-entry arrays
|
||||
// Focused entry. With no multi-selection it is also the selection; while
|
||||
// multi_count > 0 only multi_selected marks what is selected.
|
||||
int selected;
|
||||
int sel_anchor; // fixed end of a Shift range selection (-1 = none)
|
||||
int scroll_offset;
|
||||
bool* is_dir;
|
||||
int last_click_item;
|
||||
@@ -84,7 +96,7 @@ struct FileManagerState {
|
||||
int clipboard_count;
|
||||
bool clipboard_is_cut;
|
||||
|
||||
// Multi-selection (marquee selection), sized with the entry arrays.
|
||||
// Multi-selection (marquee, Ctrl/Shift-click), sized with the entry arrays.
|
||||
bool* multi_selected;
|
||||
int multi_count;
|
||||
|
||||
@@ -99,7 +111,7 @@ struct FileManagerState {
|
||||
bool ctx_open;
|
||||
int ctx_x, ctx_y;
|
||||
int ctx_target_idx; // -1 = background
|
||||
int ctx_items[8];
|
||||
int ctx_items[FM_CTX_MAX_ITEMS];
|
||||
int ctx_item_count;
|
||||
int ctx_hover;
|
||||
|
||||
@@ -121,11 +133,22 @@ struct FileManagerState {
|
||||
FileManagerVirtualEntry* virtual_entries;
|
||||
int virtual_entry_count;
|
||||
|
||||
// Sort order of real directory listings (folders always come first).
|
||||
FileManagerSortKey sort_key;
|
||||
bool sort_desc;
|
||||
|
||||
// Type-to-jump: characters typed in quick succession select the first
|
||||
// entry whose name starts with them.
|
||||
char typeahead[32];
|
||||
int typeahead_len;
|
||||
uint64_t typeahead_time;
|
||||
|
||||
// Background file operation (copy/move/delete) + its progress dialog.
|
||||
void* active_fileop; // FileOpJob*, non-null while an operation runs
|
||||
|
||||
// File Manager-owned dialogs
|
||||
void* delete_confirm_dialog;
|
||||
void* open_with_dialog;
|
||||
};
|
||||
|
||||
inline constexpr int FM_TOOLBAR_H = 32;
|
||||
@@ -138,6 +161,37 @@ inline constexpr int FM_GRID_CELL_H = 80;
|
||||
inline constexpr int FM_GRID_CELL_H_WRAPPED = 92;
|
||||
inline constexpr int FM_GRID_ICON = 48;
|
||||
inline constexpr int FM_GRID_PAD = 4;
|
||||
inline constexpr int FM_CTX_MENU_W = 184;
|
||||
|
||||
// List view column layout. Columns are dropped right to left as the window
|
||||
// narrows (Modified first, then Type, then Size) so the name keeps room.
|
||||
struct FileManagerListColumns {
|
||||
int name_x;
|
||||
int name_right; // right edge available to the name text
|
||||
int modified_x; // -1 = hidden
|
||||
int size_x; // -1 = hidden
|
||||
int type_x; // -1 = hidden
|
||||
};
|
||||
|
||||
inline FileManagerListColumns filemanager_list_columns(int content_w) {
|
||||
static constexpr int MIN_NAME_W = 160;
|
||||
static constexpr int TYPE_W = 96;
|
||||
static constexpr int SIZE_W = 76;
|
||||
static constexpr int MODIFIED_W = 132;
|
||||
|
||||
FileManagerListColumns cols = {8, 0, -1, -1, -1};
|
||||
int right = content_w - FM_SCROLLBAR_W;
|
||||
if (right - SIZE_W >= MIN_NAME_W) { right -= SIZE_W; cols.size_x = right; }
|
||||
if (right - TYPE_W >= MIN_NAME_W) {
|
||||
// Type sits right of Size; shift Size left to make room.
|
||||
cols.type_x = content_w - FM_SCROLLBAR_W - TYPE_W;
|
||||
if (cols.size_x >= 0) cols.size_x -= TYPE_W;
|
||||
right -= TYPE_W;
|
||||
}
|
||||
if (right - MODIFIED_W >= MIN_NAME_W) { right -= MODIFIED_W; cols.modified_x = right; }
|
||||
cols.name_right = right - 8;
|
||||
return cols;
|
||||
}
|
||||
|
||||
// Special user folders: name, icon filename, index into DesktopState arrays
|
||||
inline constexpr int SF_COUNT = DesktopState::SPECIAL_FOLDER_COUNT;
|
||||
@@ -160,6 +214,12 @@ inline constexpr int CTX_DELETE = 5;
|
||||
inline constexpr int CTX_NEW_FOLDER = 6;
|
||||
inline constexpr int CTX_PROPERTIES = 7;
|
||||
inline constexpr int CTX_REFRESH = 8;
|
||||
inline constexpr int CTX_OPEN_WITH = 9;
|
||||
inline constexpr int CTX_SELECT_ALL = 10;
|
||||
inline constexpr int CTX_SORT_NAME = 11;
|
||||
inline constexpr int CTX_SORT_SIZE = 12;
|
||||
inline constexpr int CTX_SORT_TYPE = 13;
|
||||
inline constexpr int CTX_SORT_MODIFIED = 14;
|
||||
|
||||
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
||||
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
||||
@@ -179,6 +239,9 @@ inline const char* filemanager_virtual_view_label(FileManagerVirtualViewKind vie
|
||||
|
||||
int special_folder_index(const char* name);
|
||||
const char* ctx_label(int action);
|
||||
const char* ctx_shortcut(const FileManagerState* fm, int action);
|
||||
// Menu items of different groups are split by a separator line.
|
||||
int ctx_group(int action);
|
||||
|
||||
inline mtk::ContextMenuState filemanager_context_state(const FileManagerState* fm) {
|
||||
if (!fm) return {};
|
||||
@@ -199,19 +262,20 @@ inline void filemanager_build_context_items(const FileManagerState* fm,
|
||||
int max_items) {
|
||||
if (!fm || !items || max_items <= 0) return;
|
||||
int count = fm->ctx_item_count < max_items ? fm->ctx_item_count : max_items;
|
||||
for (int i = 0; i < count; i++)
|
||||
items[i] = {ctx_label(fm->ctx_items[i]), fm->ctx_items[i], true};
|
||||
for (int i = 0; i < count; i++) {
|
||||
int action = fm->ctx_items[i];
|
||||
items[i] = {ctx_label(action), action, true, ctx_shortcut(fm, action),
|
||||
i + 1 < count && ctx_group(action) != ctx_group(fm->ctx_items[i + 1])};
|
||||
}
|
||||
}
|
||||
|
||||
bool str_ends_with(const char* s, const char* suffix);
|
||||
bool is_image_file(const char* name);
|
||||
bool is_font_file(const char* name);
|
||||
bool is_pdf_file(const char* name);
|
||||
bool is_spreadsheet_file(const char* name);
|
||||
bool is_wordprocessor_file(const char* name);
|
||||
bool is_audio_file(const char* name);
|
||||
bool is_video_file(const char* name);
|
||||
int detect_file_type(const char* name, bool is_dir);
|
||||
// Lowercased extension after the last '.', without the dot ("" if none).
|
||||
// Leading-dot names (".profile") count as having no extension.
|
||||
void file_extension(const char* name, char* out, int out_max);
|
||||
// Human-readable type for the list view's Type column ("PDF file", "Folder").
|
||||
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max);
|
||||
|
||||
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name);
|
||||
const char* path_basename(const char* path);
|
||||
@@ -226,6 +290,11 @@ void filemanager_free_arrays(FileManagerState* fm);
|
||||
|
||||
void filemanager_read_drives(FileManagerState* fm);
|
||||
void filemanager_read_dir(FileManagerState* fm);
|
||||
// Re-sort a real directory listing by sort_key/sort_desc, keeping the
|
||||
// selection on the same entries. No-op for Computer and virtual views.
|
||||
void filemanager_sort_entries(FileManagerState* fm);
|
||||
// Pick a sort column: a new key sorts ascending, the current key flips order.
|
||||
void filemanager_set_sort(FileManagerState* fm, FileManagerSortKey key);
|
||||
void filemanager_free_app_icons(FileManagerState* fm);
|
||||
void filemanager_read_virtual_view(FileManagerState* fm, FileManagerVirtualViewKind view);
|
||||
void filemanager_read_apps(FileManagerState* fm);
|
||||
@@ -293,6 +362,26 @@ void filemanager_commit_pathbar(FileManagerState* fm);
|
||||
|
||||
void filemanager_open_entry(FileManagerState* fm, int idx);
|
||||
|
||||
// ---- File associations (associations.cpp, openwith.cpp) ----
|
||||
// Default app for a file: the user's "Always use" choice from
|
||||
// <home>/config/associations.toml, else the first app (by name) whose manifest
|
||||
// lists the extension in files.types. Returns a desktop_items index or -1.
|
||||
int filemanager_assoc_default(DesktopState* ds, const char* ext);
|
||||
// Apps whose manifest claims ext come first ("recommended"); every other
|
||||
// launchable app follows. Returns the total count, *recommended gets how many
|
||||
// of those lead the list. Indices refer to ds->desktop_items.
|
||||
int filemanager_assoc_candidates(DesktopState* ds, const char* ext,
|
||||
int* out, int max, int* recommended);
|
||||
// Remember item as the user's default app for ext. Returns false if ext cannot
|
||||
// be stored as a config key or the save failed.
|
||||
bool filemanager_assoc_set_default(DesktopState* ds, const char* ext, int item_index);
|
||||
// True if the file looks like text (no NULs / stray control bytes in its
|
||||
// first KiB). Empty files count as text.
|
||||
bool filemanager_file_looks_textual(const char* path);
|
||||
// Launch a desktop item with a file path argument.
|
||||
bool filemanager_launch_with(DesktopState* ds, int item_index, const char* path);
|
||||
void filemanager_show_open_with(FileManagerState* fm, int target_idx);
|
||||
|
||||
// ---- Async file operations (copy/move/delete on a worker thread) ----
|
||||
enum FileOpKind : int {
|
||||
FILE_OP_COPY = 0,
|
||||
@@ -313,6 +402,15 @@ int filemanager_grid_index_at(FileManagerState* fm, int cols, int x, int rel_y);
|
||||
|
||||
// Selection helpers
|
||||
void filemanager_clear_multi_selection(FileManagerState* fm);
|
||||
// Replace the selection with a single entry (or none for idx < 0).
|
||||
void filemanager_select_single(FileManagerState* fm, int idx);
|
||||
// Ctrl-click: add idx to or remove it from the selection.
|
||||
void filemanager_toggle_selection(FileManagerState* fm, int idx);
|
||||
// Shift-click / Shift+arrow: select the range from sel_anchor to idx. With
|
||||
// additive set (Ctrl+Shift) the range is added to the existing selection.
|
||||
void filemanager_select_range(FileManagerState* fm, int idx, bool additive);
|
||||
void filemanager_select_all(FileManagerState* fm);
|
||||
bool filemanager_is_entry_selected(const FileManagerState* fm, int idx);
|
||||
// Fill out with selected entry indices. Returns count. Uses multi_selected if
|
||||
// non-empty, otherwise falls back to fm->selected (0 or 1 results).
|
||||
int filemanager_collect_selection(const FileManagerState* fm, int* out, int max);
|
||||
|
||||
@@ -100,6 +100,8 @@ void filemanager_free_arrays(FileManagerState* fm) {
|
||||
|
||||
static void filemanager_reset_list_state(FileManagerState* fm) {
|
||||
fm->selected = -1;
|
||||
fm->sel_anchor = -1;
|
||||
fm->typeahead_len = 0;
|
||||
fm->scroll_offset = 0;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
fm->last_click_item = -1;
|
||||
@@ -344,42 +346,113 @@ void filemanager_read_dir(FileManagerState* fm) {
|
||||
if (fm->entry_count >= FM_ENTRY_HARD_CAP) break;
|
||||
}
|
||||
|
||||
// Sort: directories first, then alphabetical (case-insensitive)
|
||||
for (int i = 1; i < fm->entry_count; i++) {
|
||||
char tmp_name[128];
|
||||
int tmp_type = fm->entry_types[i];
|
||||
uint64_t tmp_size = fm->entry_sizes[i];
|
||||
int64_t tmp_mtime = fm->entry_mtimes[i];
|
||||
bool tmp_isdir = fm->is_dir[i];
|
||||
montauk::strcpy(tmp_name, fm->entry_names[i]);
|
||||
filemanager_reset_list_state(fm);
|
||||
filemanager_sort_entries(fm);
|
||||
}
|
||||
|
||||
int j = i - 1;
|
||||
while (j >= 0) {
|
||||
bool swap = false;
|
||||
if (tmp_isdir && !fm->is_dir[j]) {
|
||||
swap = true;
|
||||
} else if (tmp_isdir == fm->is_dir[j]) {
|
||||
if (str_compare_ci(tmp_name, fm->entry_names[j]) < 0) {
|
||||
swap = true;
|
||||
}
|
||||
}
|
||||
if (!swap) break;
|
||||
// Folders always lead; within each group the active column decides, with the
|
||||
// name as tie-breaker so equal sizes/types/times still list predictably.
|
||||
static int filemanager_compare_entries(const FileManagerState* fm, int a, int b) {
|
||||
if (fm->is_dir[a] != fm->is_dir[b]) return fm->is_dir[a] ? -1 : 1;
|
||||
|
||||
montauk::strcpy(fm->entry_names[j + 1], fm->entry_names[j]);
|
||||
fm->entry_types[j + 1] = fm->entry_types[j];
|
||||
fm->entry_sizes[j + 1] = fm->entry_sizes[j];
|
||||
fm->entry_mtimes[j + 1] = fm->entry_mtimes[j];
|
||||
fm->is_dir[j + 1] = fm->is_dir[j];
|
||||
j--;
|
||||
}
|
||||
montauk::strcpy(fm->entry_names[j + 1], tmp_name);
|
||||
fm->entry_types[j + 1] = tmp_type;
|
||||
fm->entry_sizes[j + 1] = tmp_size;
|
||||
fm->entry_mtimes[j + 1] = tmp_mtime;
|
||||
fm->is_dir[j + 1] = tmp_isdir;
|
||||
int c = 0;
|
||||
switch (fm->sort_key) {
|
||||
case FM_SORT_SIZE:
|
||||
if (fm->entry_sizes[a] != fm->entry_sizes[b])
|
||||
c = fm->entry_sizes[a] < fm->entry_sizes[b] ? -1 : 1;
|
||||
break;
|
||||
case FM_SORT_MODIFIED:
|
||||
if (fm->entry_mtimes[a] != fm->entry_mtimes[b])
|
||||
c = fm->entry_mtimes[a] < fm->entry_mtimes[b] ? -1 : 1;
|
||||
break;
|
||||
case FM_SORT_TYPE: {
|
||||
char ea[16], eb[16];
|
||||
file_extension(fm->entry_names[a], ea, sizeof(ea));
|
||||
file_extension(fm->entry_names[b], eb, sizeof(eb));
|
||||
c = str_compare_ci(ea, eb);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (c == 0) c = str_compare_ci(fm->entry_names[a], fm->entry_names[b]);
|
||||
return fm->sort_desc ? -c : c;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void filemanager_permute(T* arr, const int* perm, int n, void* scratch) {
|
||||
T* tmp = (T*)scratch;
|
||||
for (int j = 0; j < n; j++) tmp[j] = arr[perm[j]];
|
||||
montauk::memcpy(arr, tmp, (uint64_t)n * sizeof(T));
|
||||
}
|
||||
|
||||
static int filemanager_remap_index(int idx, const int* inv, int n) {
|
||||
return (idx >= 0 && idx < n) ? inv[idx] : idx;
|
||||
}
|
||||
|
||||
void filemanager_sort_entries(FileManagerState* fm) {
|
||||
if (!fm || filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
int n = fm->entry_count;
|
||||
if (n < 2) return;
|
||||
|
||||
// Sort an index permutation (stable bottom-up merge sort), then apply it
|
||||
// to every parallel array once instead of swapping 128-byte names around.
|
||||
int* perm = (int*)montauk::malloc((uint64_t)n * sizeof(int));
|
||||
int* tmp = (int*)montauk::malloc((uint64_t)n * sizeof(int));
|
||||
void* scratch = montauk::malloc((uint64_t)n * 128);
|
||||
if (!perm || !tmp || !scratch) {
|
||||
montauk::mfree(perm);
|
||||
montauk::mfree(tmp);
|
||||
montauk::mfree(scratch);
|
||||
return;
|
||||
}
|
||||
|
||||
filemanager_reset_list_state(fm);
|
||||
for (int i = 0; i < n; i++) perm[i] = i;
|
||||
for (int width = 1; width < n; width *= 2) {
|
||||
for (int lo = 0; lo < n; lo += 2 * width) {
|
||||
int mid = lo + width < n ? lo + width : n;
|
||||
int hi = lo + 2 * width < n ? lo + 2 * width : n;
|
||||
int i = lo, j = mid, k = lo;
|
||||
while (i < mid && j < hi)
|
||||
tmp[k++] = filemanager_compare_entries(fm, perm[j], perm[i]) < 0 ? perm[j++] : perm[i++];
|
||||
while (i < mid) tmp[k++] = perm[i++];
|
||||
while (j < hi) tmp[k++] = perm[j++];
|
||||
}
|
||||
int* swap = perm; perm = tmp; tmp = swap;
|
||||
}
|
||||
|
||||
auto* names = (char(*)[128])scratch;
|
||||
for (int j = 0; j < n; j++) montauk::memcpy(names[j], fm->entry_names[perm[j]], 128);
|
||||
montauk::memcpy(fm->entry_names, names, (uint64_t)n * 128);
|
||||
filemanager_permute(fm->entry_types, perm, n, scratch);
|
||||
filemanager_permute(fm->entry_sizes, perm, n, scratch);
|
||||
filemanager_permute(fm->entry_mtimes, perm, n, scratch);
|
||||
filemanager_permute(fm->is_dir, perm, n, scratch);
|
||||
filemanager_permute(fm->multi_selected, perm, n, scratch);
|
||||
|
||||
// Keep focus, anchor and click tracking on the same entries.
|
||||
int* inv = tmp;
|
||||
for (int j = 0; j < n; j++) inv[perm[j]] = j;
|
||||
fm->selected = filemanager_remap_index(fm->selected, inv, n);
|
||||
fm->sel_anchor = filemanager_remap_index(fm->sel_anchor, inv, n);
|
||||
fm->last_click_item = filemanager_remap_index(fm->last_click_item, inv, n);
|
||||
fm->rename_idx = filemanager_remap_index(fm->rename_idx, inv, n);
|
||||
|
||||
montauk::mfree(perm);
|
||||
montauk::mfree(tmp);
|
||||
montauk::mfree(scratch);
|
||||
}
|
||||
|
||||
void filemanager_set_sort(FileManagerState* fm, FileManagerSortKey key) {
|
||||
if (!fm) return;
|
||||
if (fm->sort_key == key) {
|
||||
fm->sort_desc = !fm->sort_desc;
|
||||
} else {
|
||||
fm->sort_key = key;
|
||||
// Largest and newest first is what one usually wants from these.
|
||||
fm->sort_desc = key == FM_SORT_SIZE || key == FM_SORT_MODIFIED;
|
||||
}
|
||||
filemanager_sort_entries(fm);
|
||||
}
|
||||
|
||||
void filemanager_free_app_icons(FileManagerState* fm) {
|
||||
|
||||
@@ -15,6 +15,91 @@ void filemanager_clear_multi_selection(FileManagerState* fm) {
|
||||
fm->multi_count = 0;
|
||||
}
|
||||
|
||||
bool filemanager_is_entry_selected(const FileManagerState* fm, int idx) {
|
||||
if (!fm || idx < 0 || idx >= fm->entry_count) return false;
|
||||
if (fm->multi_count > 0) return fm->multi_selected[idx];
|
||||
return idx == fm->selected;
|
||||
}
|
||||
|
||||
void filemanager_select_single(FileManagerState* fm, int idx) {
|
||||
if (!fm) return;
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->selected = (idx >= 0 && idx < fm->entry_count) ? idx : -1;
|
||||
fm->sel_anchor = fm->selected;
|
||||
}
|
||||
|
||||
// A multi-selection of one entry collapses back to the plain single-selection
|
||||
// form, which the rename/toolbar logic treats as "one item selected".
|
||||
static void filemanager_normalize_selection(FileManagerState* fm) {
|
||||
if (fm->multi_count != 1) return;
|
||||
for (int i = 0; i < fm->entry_count; i++) {
|
||||
if (fm->multi_selected[i]) {
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a plain single selection into the multi form before extending it.
|
||||
static void filemanager_seed_multi_selection(FileManagerState* fm) {
|
||||
if (fm->multi_count > 0) return;
|
||||
if (fm->selected >= 0 && fm->selected < fm->entry_count) {
|
||||
fm->multi_selected[fm->selected] = true;
|
||||
fm->multi_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void filemanager_toggle_selection(FileManagerState* fm, int idx) {
|
||||
if (!fm || idx < 0 || idx >= fm->entry_count) return;
|
||||
filemanager_seed_multi_selection(fm);
|
||||
|
||||
if (fm->multi_selected[idx]) {
|
||||
fm->multi_selected[idx] = false;
|
||||
fm->multi_count--;
|
||||
} else {
|
||||
fm->multi_selected[idx] = true;
|
||||
fm->multi_count++;
|
||||
}
|
||||
fm->selected = fm->multi_count > 0 ? idx : -1;
|
||||
fm->sel_anchor = idx;
|
||||
filemanager_normalize_selection(fm);
|
||||
}
|
||||
|
||||
void filemanager_select_range(FileManagerState* fm, int idx, bool additive) {
|
||||
if (!fm || idx < 0 || idx >= fm->entry_count) return;
|
||||
|
||||
int anchor = fm->sel_anchor;
|
||||
if (anchor < 0 || anchor >= fm->entry_count)
|
||||
anchor = (fm->selected >= 0 && fm->selected < fm->entry_count) ? fm->selected : idx;
|
||||
|
||||
if (additive) filemanager_seed_multi_selection(fm);
|
||||
else filemanager_clear_multi_selection(fm);
|
||||
|
||||
int lo = anchor < idx ? anchor : idx;
|
||||
int hi = anchor < idx ? idx : anchor;
|
||||
for (int i = lo; i <= hi; i++) {
|
||||
if (!fm->multi_selected[i]) {
|
||||
fm->multi_selected[i] = true;
|
||||
fm->multi_count++;
|
||||
}
|
||||
}
|
||||
fm->selected = idx;
|
||||
fm->sel_anchor = anchor;
|
||||
filemanager_normalize_selection(fm);
|
||||
}
|
||||
|
||||
void filemanager_select_all(FileManagerState* fm) {
|
||||
if (!fm || fm->entry_count <= 0) return;
|
||||
if (fm->entry_count == 1) {
|
||||
filemanager_select_single(fm, 0);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < fm->entry_count; i++) fm->multi_selected[i] = true;
|
||||
fm->multi_count = fm->entry_count;
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) fm->selected = 0;
|
||||
}
|
||||
|
||||
int filemanager_collect_selection(const FileManagerState* fm, int* out, int max) {
|
||||
if (!fm || !out || max <= 0) return 0;
|
||||
int n = 0;
|
||||
@@ -46,10 +131,64 @@ const char* ctx_label(int action) {
|
||||
case CTX_NEW_FOLDER: return "New Folder";
|
||||
case CTX_PROPERTIES: return "Properties";
|
||||
case CTX_REFRESH: return "Refresh";
|
||||
case CTX_OPEN_WITH: return "Open With...";
|
||||
case CTX_SELECT_ALL: return "Select All";
|
||||
case CTX_SORT_NAME: return "Sort by Name";
|
||||
case CTX_SORT_SIZE: return "Sort by Size";
|
||||
case CTX_SORT_TYPE: return "Sort by Type";
|
||||
case CTX_SORT_MODIFIED: return "Sort by Modified";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const char* ctx_shortcut(const FileManagerState* fm, int action) {
|
||||
// The active sort column shows its direction in the shortcut slot.
|
||||
int sort_action = -1;
|
||||
if (fm) {
|
||||
switch (fm->sort_key) {
|
||||
case FM_SORT_NAME: sort_action = CTX_SORT_NAME; break;
|
||||
case FM_SORT_SIZE: sort_action = CTX_SORT_SIZE; break;
|
||||
case FM_SORT_TYPE: sort_action = CTX_SORT_TYPE; break;
|
||||
case FM_SORT_MODIFIED: sort_action = CTX_SORT_MODIFIED; break;
|
||||
}
|
||||
}
|
||||
if (action == sort_action) return fm->sort_desc ? "Desc" : "Asc";
|
||||
|
||||
switch (action) {
|
||||
case CTX_OPEN: return "Enter";
|
||||
case CTX_COPY: return "Ctrl+C";
|
||||
case CTX_CUT: return "Ctrl+X";
|
||||
case CTX_PASTE: return "Ctrl+V";
|
||||
case CTX_RENAME: return "F2";
|
||||
case CTX_DELETE: return "Del";
|
||||
case CTX_NEW_FOLDER: return "Ctrl+Shift+N";
|
||||
case CTX_PROPERTIES: return "Alt+Enter";
|
||||
case CTX_REFRESH: return "F5";
|
||||
case CTX_SELECT_ALL: return "Ctrl+A";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int ctx_group(int action) {
|
||||
switch (action) {
|
||||
case CTX_OPEN:
|
||||
case CTX_OPEN_WITH: return 0;
|
||||
case CTX_COPY:
|
||||
case CTX_CUT:
|
||||
case CTX_PASTE: return 1;
|
||||
case CTX_RENAME:
|
||||
case CTX_DELETE:
|
||||
case CTX_NEW_FOLDER: return 2;
|
||||
case CTX_SELECT_ALL: return 3;
|
||||
case CTX_SORT_NAME:
|
||||
case CTX_SORT_SIZE:
|
||||
case CTX_SORT_TYPE:
|
||||
case CTX_SORT_MODIFIED: return 4;
|
||||
case CTX_REFRESH: return 5;
|
||||
default: return 6; // Properties
|
||||
}
|
||||
}
|
||||
|
||||
bool str_ends_with(const char* s, const char* suffix) {
|
||||
int slen = montauk::slen(s);
|
||||
int suflen = montauk::slen(suffix);
|
||||
@@ -64,42 +203,64 @@ bool str_ends_with(const char* s, const char* suffix) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_image_file(const char* name) {
|
||||
return str_ends_with(name, ".jpg")
|
||||
|| str_ends_with(name, ".jpeg")
|
||||
|| str_ends_with(name, ".svg");
|
||||
}
|
||||
|
||||
bool is_font_file(const char* name) {
|
||||
return str_ends_with(name, ".ttf");
|
||||
}
|
||||
|
||||
bool is_pdf_file(const char* name) {
|
||||
return str_ends_with(name, ".pdf");
|
||||
}
|
||||
|
||||
bool is_spreadsheet_file(const char* name) {
|
||||
return str_ends_with(name, ".mss");
|
||||
}
|
||||
|
||||
bool is_wordprocessor_file(const char* name) {
|
||||
return str_ends_with(name, ".mwp");
|
||||
}
|
||||
|
||||
bool is_audio_file(const char* name) {
|
||||
return str_ends_with(name, ".mp3") || str_ends_with(name, ".wav");
|
||||
}
|
||||
|
||||
bool is_video_file(const char* name) {
|
||||
return str_ends_with(name, ".avi");
|
||||
}
|
||||
|
||||
int detect_file_type(const char* name, bool is_dir) {
|
||||
if (is_dir) return 1;
|
||||
if (str_ends_with(name, ".elf")) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void file_extension(const char* name, char* out, int out_max) {
|
||||
if (!out || out_max <= 0) return;
|
||||
out[0] = '\0';
|
||||
if (!name) return;
|
||||
|
||||
const char* dot = nullptr;
|
||||
for (const char* p = name; *p; p++) {
|
||||
if (*p == '.') dot = p;
|
||||
}
|
||||
if (!dot || dot == name || !dot[1]) return;
|
||||
|
||||
int n = 0;
|
||||
for (const char* p = dot + 1; *p && n < out_max - 1; p++) {
|
||||
char ch = *p;
|
||||
out[n++] = (ch >= 'A' && ch <= 'Z') ? ch + 32 : ch;
|
||||
}
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
void filemanager_type_label(const FileManagerState* fm, int idx, char* out, int out_max) {
|
||||
if (!out || out_max <= 0) return;
|
||||
const char* fixed = nullptr;
|
||||
switch (fm->entry_types[idx]) {
|
||||
case FM_ENTRY_VIRTUAL_ITEM:
|
||||
fixed = fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION ? "Setting" : "App";
|
||||
break;
|
||||
case FM_ENTRY_APPS_ROOT: fixed = "Apps"; break;
|
||||
case FM_ENTRY_SYSTEM_CONFIGURATION_ROOT: fixed = "Settings"; break;
|
||||
case FM_ENTRY_HOME: fixed = "Home"; break;
|
||||
case FM_ENTRY_DRIVE: fixed = "Drive"; break;
|
||||
case FM_ENTRY_SPECIAL_DIR:
|
||||
case FM_ENTRY_DIR: fixed = "Folder"; break;
|
||||
case FM_ENTRY_EXEC: fixed = "Program"; break;
|
||||
default: break;
|
||||
}
|
||||
if (fixed) {
|
||||
montauk::strncpy(out, fixed, out_max);
|
||||
return;
|
||||
}
|
||||
|
||||
char ext[16];
|
||||
file_extension(fm->entry_names[idx], ext, 9);
|
||||
if (!ext[0]) {
|
||||
montauk::strncpy(out, "File", out_max);
|
||||
return;
|
||||
}
|
||||
for (int k = 0; ext[k]; k++) {
|
||||
if (ext[k] >= 'a' && ext[k] <= 'z') ext[k] -= 32;
|
||||
}
|
||||
snprintf(out, out_max, "%s file", ext);
|
||||
}
|
||||
|
||||
void filemanager_build_fullpath(char* out, int out_max, const char* dir, const char* name) {
|
||||
montauk::strcpy(out, dir);
|
||||
int plen = montauk::slen(out);
|
||||
|
||||
@@ -281,28 +281,28 @@ void filemanager_open_entry(FileManagerState* fm, int idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open file with appropriate app
|
||||
// Open the file with its default app (see associations.cpp).
|
||||
DesktopState* ds = fm->desktop;
|
||||
const char* name = fm->entry_names[idx];
|
||||
char fullpath[512];
|
||||
filemanager_build_fullpath(fullpath, 512, fm->current_path, fm->entry_names[idx]);
|
||||
if (is_image_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/imageviewer/imageviewer.elf", fullpath);
|
||||
} else if (is_font_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/fontpreview/fontpreview.elf", fullpath);
|
||||
} else if (is_pdf_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/pdfviewer/pdfviewer.elf", fullpath);
|
||||
} else if (is_spreadsheet_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/spreadsheet/spreadsheet.elf", fullpath);
|
||||
} else if (is_wordprocessor_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/wordprocessor/wordprocessor.elf", fullpath);
|
||||
} else if (is_audio_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/music/music.elf", fullpath);
|
||||
} else if (is_video_file(fm->entry_names[idx])) {
|
||||
montauk::spawn("0:/apps/video/video.elf", fullpath);
|
||||
} else if (str_ends_with(fm->entry_names[idx], ".elf")) {
|
||||
montauk::spawn(fullpath);
|
||||
} else {
|
||||
montauk::spawn("0:/apps/texteditor/texteditor.elf", fullpath);
|
||||
filemanager_build_fullpath(fullpath, 512, fm->current_path, name);
|
||||
if (str_ends_with(name, ".elf")) {
|
||||
desktop_spawn_app(ds, fullpath);
|
||||
return;
|
||||
}
|
||||
|
||||
char ext[16];
|
||||
file_extension(name, ext, sizeof(ext));
|
||||
int app = filemanager_assoc_default(ds, ext);
|
||||
if (app >= 0 && filemanager_launch_with(ds, app, fullpath)) return;
|
||||
|
||||
// Nothing claims this type: text still goes straight to the Text Editor
|
||||
// (READMEs, Makefiles, odd extensions); anything else asks.
|
||||
if (filemanager_file_looks_textual(fullpath)) {
|
||||
desktop_spawn_app(ds, "0:/apps/texteditor/texteditor.elf", fullpath);
|
||||
return;
|
||||
}
|
||||
filemanager_show_open_with(fm, idx);
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -0,0 +1,411 @@
|
||||
/*
|
||||
* openwith.cpp
|
||||
* "Open With" app chooser for the embedded desktop file manager
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "filemanager_internal.hpp"
|
||||
#include <gui/mtk/hosts.hpp>
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
struct OpenWithDialogState {
|
||||
DesktopState* desktop;
|
||||
FileManagerState* owner;
|
||||
Color accent;
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
|
||||
char name[128];
|
||||
char path[512];
|
||||
char ext[16];
|
||||
bool remember; // "Always use this app" checkbox
|
||||
|
||||
// Candidate apps as desktop_items indices; the first `recommended` of
|
||||
// them declare the file type. The catalog is only built at desktop
|
||||
// startup, so the indices stay valid while the dialog is open.
|
||||
int* items;
|
||||
int item_count;
|
||||
int recommended;
|
||||
int selected; // position in items, -1 = none
|
||||
int scroll_row; // first visible list row
|
||||
int last_click_row;
|
||||
uint64_t last_click_time;
|
||||
};
|
||||
|
||||
static constexpr int OW_W = 400;
|
||||
static constexpr int OW_H = 400;
|
||||
static constexpr int OW_PAD = 16;
|
||||
static constexpr int OW_ROW_H = 28;
|
||||
|
||||
// ---- Row model: optional "Recommended" / "Other apps" captions + items ----
|
||||
|
||||
static bool ow_has_captions(const OpenWithDialogState* st) {
|
||||
return st->recommended > 0;
|
||||
}
|
||||
|
||||
static int ow_row_count(const OpenWithDialogState* st) {
|
||||
if (!ow_has_captions(st)) return st->item_count;
|
||||
int captions = 1 + (st->item_count > st->recommended ? 1 : 0);
|
||||
return st->item_count + captions;
|
||||
}
|
||||
|
||||
// Item position for a list row, or -1 for a caption row.
|
||||
static int ow_row_item(const OpenWithDialogState* st, int row) {
|
||||
if (!ow_has_captions(st)) return row;
|
||||
if (row == 0) return -1;
|
||||
if (row <= st->recommended) return row - 1;
|
||||
if (row == st->recommended + 1) return -1;
|
||||
return row - 2;
|
||||
}
|
||||
|
||||
static int ow_item_row(const OpenWithDialogState* st, int pos) {
|
||||
if (!ow_has_captions(st)) return pos;
|
||||
return pos < st->recommended ? pos + 1 : pos + 2;
|
||||
}
|
||||
|
||||
// ---- Layout ----
|
||||
|
||||
static Rect ow_list_rect(int content_w, int content_h) {
|
||||
int fh = system_font_height();
|
||||
int y = OW_PAD + fh + 12;
|
||||
int h = content_h - y - 92;
|
||||
h -= h % OW_ROW_H;
|
||||
return {OW_PAD, y, content_w - OW_PAD * 2, h};
|
||||
}
|
||||
|
||||
static int ow_visible_rows(const Rect& list) {
|
||||
return list.h / OW_ROW_H;
|
||||
}
|
||||
|
||||
static Rect ow_checkbox_rect(int content_w, int content_h) {
|
||||
Rect list = ow_list_rect(content_w, content_h);
|
||||
return {OW_PAD, list.y + list.h + 10, content_w - OW_PAD * 2, 22};
|
||||
}
|
||||
|
||||
static void ow_button_rects(int content_w, int content_h, Rect* open_btn, Rect* cancel_btn) {
|
||||
int btn_w = 92;
|
||||
int btn_h = 30;
|
||||
int y = content_h - 44;
|
||||
int cancel_x = content_w - OW_PAD - btn_w;
|
||||
if (open_btn) *open_btn = {cancel_x - 8 - btn_w, y, btn_w, btn_h};
|
||||
if (cancel_btn) *cancel_btn = {cancel_x, y, btn_w, btn_h};
|
||||
}
|
||||
|
||||
static void ow_scroll_to_selected(OpenWithDialogState* st, int content_w, int content_h) {
|
||||
if (st->selected < 0) return;
|
||||
int visible = ow_visible_rows(ow_list_rect(content_w, content_h));
|
||||
if (visible <= 0) return;
|
||||
int row = ow_item_row(st, st->selected);
|
||||
// Keep the "Recommended" caption visible when the first item is selected.
|
||||
if (row == 1 && ow_has_captions(st)) row = 0;
|
||||
if (row < st->scroll_row) st->scroll_row = row;
|
||||
else if (row >= st->scroll_row + visible) st->scroll_row = row - visible + 1;
|
||||
}
|
||||
|
||||
static void ow_clamp_scroll(OpenWithDialogState* st, int content_w, int content_h) {
|
||||
int visible = ow_visible_rows(ow_list_rect(content_w, content_h));
|
||||
int max_scroll = ow_row_count(st) - visible;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (st->scroll_row > max_scroll) st->scroll_row = max_scroll;
|
||||
if (st->scroll_row < 0) st->scroll_row = 0;
|
||||
}
|
||||
|
||||
// ---- Actions ----
|
||||
|
||||
static void ow_close(OpenWithDialogState* st) {
|
||||
if (!st || !st->desktop) return;
|
||||
for (int i = 0; i < st->desktop->window_count; i++) {
|
||||
if (st->desktop->windows[i].app_data == st) {
|
||||
desktop_close_window(st->desktop, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ow_apply(OpenWithDialogState* st) {
|
||||
if (!st || st->selected < 0 || st->selected >= st->item_count) return;
|
||||
DesktopState* ds = st->desktop;
|
||||
int item = st->items[st->selected];
|
||||
|
||||
// Copy what the error paths need: closing frees st.
|
||||
char name[128];
|
||||
char path[512];
|
||||
char ext[16];
|
||||
montauk::strncpy(name, st->name, sizeof(name));
|
||||
montauk::strncpy(path, st->path, sizeof(path));
|
||||
montauk::strncpy(ext, st->ext, sizeof(ext));
|
||||
bool remember = st->remember && ext[0];
|
||||
FileManagerState* owner = st->owner;
|
||||
ow_close(st);
|
||||
|
||||
if (remember && !filemanager_assoc_set_default(ds, ext, item)) {
|
||||
char heading[96];
|
||||
snprintf(heading, sizeof(heading), "Couldn't save the default app for .%s files", ext);
|
||||
filemanager_show_error(ds, owner, heading, nullptr, montauk::Success);
|
||||
}
|
||||
if (!filemanager_launch_with(ds, item, path)) {
|
||||
char heading[192];
|
||||
snprintf(heading, sizeof(heading), "Couldn't open \"%s\" with %s",
|
||||
name, ds->desktop_items[item].name);
|
||||
filemanager_show_error(ds, owner, heading, path, montauk::Success);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Window callbacks ----
|
||||
|
||||
static void ow_on_draw(Window* win, Framebuffer& fb) {
|
||||
(void)fb;
|
||||
OpenWithDialogState* st = (OpenWithDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
mtk::Theme theme = mtk::make_theme(st->accent);
|
||||
c.fill(theme.window_bg);
|
||||
int fh = system_font_height();
|
||||
|
||||
char title[192];
|
||||
snprintf(title, sizeof(title), "Open \"%s\" with:", st->name);
|
||||
char title_fit[192];
|
||||
montauk::strncpy(title_fit, title, sizeof(title_fit));
|
||||
if (text_width(title_fit) > c.w - OW_PAD * 2) {
|
||||
// Shorten the file name rather than the sentence around it.
|
||||
int len = montauk::slen(st->name);
|
||||
char short_name[128];
|
||||
while (len > 1) {
|
||||
len = montauk::utf8::prev_boundary(st->name, len);
|
||||
montauk::strncpy(short_name, st->name, len + 1);
|
||||
snprintf(title_fit, sizeof(title_fit), "Open \"%s...\" with:", short_name);
|
||||
if (text_width(title_fit) <= c.w - OW_PAD * 2) break;
|
||||
}
|
||||
}
|
||||
c.text(OW_PAD, OW_PAD, title_fit, theme.text);
|
||||
|
||||
// List
|
||||
Rect list = ow_list_rect(c.w, c.h);
|
||||
c.fill_rect(list.x, list.y, list.w, list.h, colors::WHITE);
|
||||
c.rect(list.x - 1, list.y - 1, list.w + 2, list.h + 2, theme.border);
|
||||
|
||||
int rows = ow_row_count(st);
|
||||
int visible = ow_visible_rows(list);
|
||||
bool need_scrollbar = rows > visible;
|
||||
int row_w = list.w - (need_scrollbar ? 8 : 0);
|
||||
|
||||
if (st->item_count == 0) {
|
||||
c.text(list.x + 12, list.y + 10, "No apps available.", theme.text_muted);
|
||||
}
|
||||
|
||||
for (int v = 0; v < visible; v++) {
|
||||
int row = st->scroll_row + v;
|
||||
if (row >= rows) break;
|
||||
int ry = list.y + v * OW_ROW_H;
|
||||
int pos = ow_row_item(st, row);
|
||||
|
||||
if (pos < 0) {
|
||||
const char* caption = row == 0 ? "Recommended" : "Other apps";
|
||||
c.text(list.x + 10, ry + OW_ROW_H - fh - 4, caption, theme.text_muted);
|
||||
continue;
|
||||
}
|
||||
|
||||
Rect r = {list.x + 2, ry + 1, row_w - 4, OW_ROW_H - 2};
|
||||
bool hovered = r.contains(st->mouse_x, st->mouse_y);
|
||||
if (pos == st->selected) {
|
||||
c.fill_rect(r.x, r.y, r.w, r.h, gui::mtk::accent_hover_tint(st->accent));
|
||||
} else if (hovered) {
|
||||
c.fill_rect(r.x, r.y, r.w, r.h, theme.surface_alt);
|
||||
}
|
||||
|
||||
const DesktopItem& item = st->desktop->desktop_items[st->items[pos]];
|
||||
int ix = list.x + 10;
|
||||
if (item.icon.pixels) {
|
||||
c.icon(ix + (20 - item.icon.width) / 2, ry + (OW_ROW_H - item.icon.height) / 2,
|
||||
item.icon);
|
||||
}
|
||||
c.text(ix + 28, ry + (OW_ROW_H - fh) / 2, item.name, theme.text);
|
||||
}
|
||||
|
||||
if (need_scrollbar) {
|
||||
int track_h = list.h;
|
||||
int thumb_h = track_h * visible / rows;
|
||||
if (thumb_h < 16) thumb_h = 16;
|
||||
int max_scroll = rows - visible;
|
||||
int thumb_y = list.y + (track_h - thumb_h) * st->scroll_row / (max_scroll > 0 ? max_scroll : 1);
|
||||
c.fill_rect(list.x + list.w - 6, thumb_y, 4, thumb_h, theme.border);
|
||||
}
|
||||
|
||||
// "Always use" checkbox, only when the file has an extension to key on.
|
||||
if (st->ext[0]) {
|
||||
Rect cb = ow_checkbox_rect(c.w, c.h);
|
||||
char label[96];
|
||||
snprintf(label, sizeof(label), "Always use this app for .%s files", st->ext);
|
||||
mtk::draw_checkbox(c, cb, label, mtk::check_state(st->remember), theme, true,
|
||||
cb.contains(st->mouse_x, st->mouse_y));
|
||||
}
|
||||
|
||||
Rect open_btn, cancel_btn;
|
||||
ow_button_rects(c.w, c.h, &open_btn, &cancel_btn);
|
||||
bool can_open = st->selected >= 0;
|
||||
mtk::draw_button(c, open_btn, "Open", mtk::BUTTON_PRIMARY,
|
||||
mtk::widget_state(false, can_open && open_btn.contains(st->mouse_x, st->mouse_y),
|
||||
can_open),
|
||||
theme);
|
||||
mtk::draw_button(c, cancel_btn, "Cancel", mtk::BUTTON_SECONDARY,
|
||||
mtk::widget_state(false, cancel_btn.contains(st->mouse_x, st->mouse_y), true),
|
||||
theme);
|
||||
}
|
||||
|
||||
static void ow_on_mouse(Window* win, MouseEvent& ev) {
|
||||
OpenWithDialogState* st = (OpenWithDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
mtk::DesktopHost host(win);
|
||||
int mx = 0;
|
||||
int my = 0;
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
if (mx != st->mouse_x || my != st->mouse_y) host.invalidate();
|
||||
st->mouse_x = mx;
|
||||
st->mouse_y = my;
|
||||
|
||||
Rect list = ow_list_rect(win->content_w, win->content_h);
|
||||
|
||||
if (ev.scroll != 0 && list.contains(mx, my)) {
|
||||
st->scroll_row -= ev.scroll;
|
||||
ow_clamp_scroll(st, win->content_w, win->content_h);
|
||||
host.invalidate();
|
||||
}
|
||||
|
||||
if (!ev.left_pressed()) return;
|
||||
|
||||
if (list.contains(mx, my)) {
|
||||
int row = st->scroll_row + (my - list.y) / OW_ROW_H;
|
||||
int pos = row < ow_row_count(st) ? ow_row_item(st, row) : -1;
|
||||
if (pos < 0) return;
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (st->last_click_row == row && now - st->last_click_time < 400) {
|
||||
st->selected = pos;
|
||||
ow_apply(st);
|
||||
return;
|
||||
}
|
||||
st->selected = pos;
|
||||
st->last_click_row = row;
|
||||
st->last_click_time = now;
|
||||
host.invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (st->ext[0] && ow_checkbox_rect(win->content_w, win->content_h).contains(mx, my)) {
|
||||
st->remember = !st->remember;
|
||||
host.invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
Rect open_btn, cancel_btn;
|
||||
ow_button_rects(win->content_w, win->content_h, &open_btn, &cancel_btn);
|
||||
if (open_btn.contains(mx, my)) ow_apply(st);
|
||||
else if (cancel_btn.contains(mx, my)) ow_close(st);
|
||||
}
|
||||
|
||||
static void ow_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
OpenWithDialogState* st = (OpenWithDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
|
||||
uint8_t nav = montauk::nav_scancode(key);
|
||||
if (key.ascii == '\n' || key.ascii == '\r') {
|
||||
ow_apply(st);
|
||||
} else if (key.scancode == 0x01) {
|
||||
ow_close(st);
|
||||
} else if ((nav == 0x48 || nav == 0x50) && st->item_count > 0) {
|
||||
if (st->selected < 0) st->selected = 0;
|
||||
else if (nav == 0x48 && st->selected > 0) st->selected--;
|
||||
else if (nav == 0x50 && st->selected < st->item_count - 1) st->selected++;
|
||||
ow_scroll_to_selected(st, win->content_w, win->content_h);
|
||||
} else if (key.ascii == ' ' && st->ext[0]) {
|
||||
st->remember = !st->remember;
|
||||
}
|
||||
}
|
||||
|
||||
static void ow_on_close(Window* win) {
|
||||
OpenWithDialogState* st = (OpenWithDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
// The owner may have closed first; only clear its pointer if it is still
|
||||
// a live Files window that points at us.
|
||||
DesktopState* ds = st->desktop;
|
||||
for (int i = 0; ds && i < ds->window_count; i++) {
|
||||
Window& w = ds->windows[i];
|
||||
if (w.app_data == st->owner && w.on_draw == filemanager_on_draw) {
|
||||
if (st->owner->open_with_dialog == st) st->owner->open_with_dialog = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
montauk::mfree(st->items);
|
||||
montauk::mfree(st);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
|
||||
void filemanager_show_open_with(FileManagerState* fm, int target_idx) {
|
||||
if (!fm || !fm->desktop || target_idx < 0 || target_idx >= fm->entry_count) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
if (fm->is_dir[target_idx]) return;
|
||||
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (fm->open_with_dialog) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == fm->open_with_dialog) {
|
||||
desktop_raise_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fm->open_with_dialog = nullptr;
|
||||
}
|
||||
|
||||
auto* st = (OpenWithDialogState*)montauk::malloc(sizeof(OpenWithDialogState));
|
||||
if (!st) return;
|
||||
montauk::memset(st, 0, sizeof(OpenWithDialogState));
|
||||
st->desktop = ds;
|
||||
st->owner = fm;
|
||||
st->accent = ds->settings.accent_color;
|
||||
st->last_click_row = -1;
|
||||
montauk::strncpy(st->name, fm->entry_names[target_idx], sizeof(st->name));
|
||||
filemanager_build_fullpath(st->path, sizeof(st->path),
|
||||
fm->current_path, fm->entry_names[target_idx]);
|
||||
file_extension(st->name, st->ext, sizeof(st->ext));
|
||||
|
||||
int cap = ds->desktop_item_count > 0 ? ds->desktop_item_count : 1;
|
||||
st->items = (int*)montauk::malloc((uint64_t)cap * sizeof(int));
|
||||
if (!st->items) {
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
st->item_count = filemanager_assoc_candidates(ds, st->ext, st->items, cap, &st->recommended);
|
||||
|
||||
// Preselect the current default app, else the first candidate.
|
||||
st->selected = st->item_count > 0 ? 0 : -1;
|
||||
int current = filemanager_assoc_default(ds, st->ext);
|
||||
for (int i = 0; i < st->item_count; i++) {
|
||||
if (st->items[i] == current) { st->selected = i; break; }
|
||||
}
|
||||
|
||||
int wx = 0;
|
||||
int wy = 0;
|
||||
props_center_dialog(ds, fm, OW_W, OW_H, &wx, &wy);
|
||||
int idx = desktop_create_window(ds, "Open With", wx, wy, OW_W, OW_H);
|
||||
if (idx < 0) {
|
||||
montauk::mfree(st->items);
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
fm->open_with_dialog = st;
|
||||
win->app_data = st;
|
||||
win->on_draw = ow_on_draw;
|
||||
win->on_mouse = ow_on_mouse;
|
||||
win->on_key = ow_on_key;
|
||||
win->on_close = ow_on_close;
|
||||
ow_scroll_to_selected(st, win->content_w, win->content_h);
|
||||
}
|
||||
|
||||
} // namespace filemanager
|
||||
@@ -135,6 +135,21 @@ static void draw_marquee(Canvas& c, FileManagerState* fm, int list_y) {
|
||||
}
|
||||
}
|
||||
|
||||
// Column title, plus a small arrow when it is the active sort column.
|
||||
static void draw_column_header(Canvas& c, int x, int header_y, const char* label,
|
||||
bool active, bool desc, Color color) {
|
||||
c.text(x, header_y + 2, label, active ? colors::TEXT_COLOR : color);
|
||||
if (!active) return;
|
||||
|
||||
int ax = x + text_width(label) + 6;
|
||||
int ay = header_y + FM_HEADER_H / 2 - 2;
|
||||
for (int r = 0; r < 4; r++) {
|
||||
// Ascending points up, descending points down.
|
||||
int row = desc ? 3 - r : r;
|
||||
c.hline(ax + 3 - r, ay + row, r * 2 + 1, colors::TEXT_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
int filemanager_grid_row_height(FileManagerState* fm, int row, int cols) {
|
||||
if (!fm || cols <= 0 || row < 0) return FM_GRID_CELL_H;
|
||||
|
||||
@@ -342,8 +357,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
if (cell_y + row_h <= list_y || cell_y >= c.h) continue;
|
||||
|
||||
// Selection highlight (single or multi)
|
||||
bool cell_selected = (i == fm->selected) ||
|
||||
(fm->multi_selected && fm->multi_selected[i]);
|
||||
bool cell_selected = filemanager_is_entry_selected(fm, i);
|
||||
if (cell_selected) {
|
||||
int sy = gui_max(cell_y, list_y);
|
||||
int sh = gui_min(cell_y + row_h, c.h) - sy;
|
||||
@@ -446,23 +460,32 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
int header_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
c.fill_rect(0, header_y, c.w, FM_HEADER_H, Color::from_rgb(0xF8, 0xF8, 0xF8));
|
||||
|
||||
int name_col_x = 8;
|
||||
int size_col_x = c.w - FM_SCROLLBAR_W - 120;
|
||||
int type_col_x = c.w - FM_SCROLLBAR_W - 60;
|
||||
FileManagerListColumns cols = filemanager_list_columns(c.w);
|
||||
// Sorting only applies to real directories; Computer and the virtual
|
||||
// views keep their curated order and show no sort arrow.
|
||||
bool sortable = !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
|
||||
|
||||
c.text(name_col_x, header_y + 2, "Name", dim);
|
||||
if (size_col_x > 100)
|
||||
c.text(size_col_x, header_y + 2, "Size", dim);
|
||||
if (type_col_x > 160)
|
||||
c.text(type_col_x, header_y + 2, "Type", dim);
|
||||
draw_column_header(c, cols.name_x, header_y, "Name",
|
||||
sortable && fm->sort_key == FM_SORT_NAME, fm->sort_desc, dim);
|
||||
if (cols.modified_x >= 0)
|
||||
draw_column_header(c, cols.modified_x, header_y, "Modified",
|
||||
sortable && fm->sort_key == FM_SORT_MODIFIED, fm->sort_desc, dim);
|
||||
if (cols.size_x >= 0)
|
||||
draw_column_header(c, cols.size_x, header_y, "Size",
|
||||
sortable && fm->sort_key == FM_SORT_SIZE, fm->sort_desc, dim);
|
||||
if (cols.type_x >= 0)
|
||||
draw_column_header(c, cols.type_x, header_y, "Type",
|
||||
sortable && fm->sort_key == FM_SORT_TYPE, fm->sort_desc, dim);
|
||||
|
||||
// Header separator
|
||||
c.hline(0, header_y + FM_HEADER_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// Column separator lines
|
||||
if (size_col_x > 100) {
|
||||
c.vline(size_col_x - 4, header_y, c.h - header_y, colors::BORDER);
|
||||
}
|
||||
int first_col_x = cols.modified_x >= 0 ? cols.modified_x
|
||||
: cols.size_x >= 0 ? cols.size_x : cols.type_x;
|
||||
if (first_col_x >= 0)
|
||||
c.vline(first_col_x - 4, header_y, c.h - header_y, colors::BORDER);
|
||||
int tz = montauk::gettz();
|
||||
|
||||
// ---- File entries ----
|
||||
int list_y = header_y + FM_HEADER_H;
|
||||
@@ -482,8 +505,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
if (iy + FM_ITEM_H <= list_y || iy >= c.h) continue;
|
||||
|
||||
// Highlight selected (single or marquee multi-selection)
|
||||
bool row_selected = (i == fm->selected) ||
|
||||
(fm->multi_selected && fm->multi_selected[i]);
|
||||
bool row_selected = filemanager_is_entry_selected(fm, i);
|
||||
if (row_selected) {
|
||||
int sy = gui_max(iy, list_y);
|
||||
int sh = gui_min(iy + FM_ITEM_H, c.h) - sy;
|
||||
@@ -540,7 +562,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
|
||||
if (fm->rename_active && fm->rename_idx == i && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
// Rename textbox inline
|
||||
int rw = (size_col_x > 100 ? size_col_x - 8 : c.w - FM_SCROLLBAR_W - 8) - tx;
|
||||
int rw = cols.name_right - tx;
|
||||
c.fill_rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::WHITE);
|
||||
c.rect(tx - 2, ty - 1, rw, fm_sfh + 2, colors::ACCENT);
|
||||
c.text(tx, ty, fm->rename_buf, colors::TEXT_COLOR);
|
||||
@@ -556,8 +578,7 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
if (ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
// Truncate name to fit the name column width
|
||||
const char* name = fm->entry_names[i];
|
||||
int name_max_w = size_col_x - tx - 8;
|
||||
if (name_max_w < 20) name_max_w = c.w - FM_SCROLLBAR_W - tx - 8;
|
||||
int name_max_w = cols.name_right - tx;
|
||||
char label[64];
|
||||
montauk::utf8::copy(label, 64, name);
|
||||
|
||||
@@ -581,28 +602,30 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
}
|
||||
}
|
||||
|
||||
bool row_text_visible = ty >= list_y && ty + fm_sfh <= c.h;
|
||||
|
||||
// Modified
|
||||
if (cols.modified_x >= 0 && row_text_visible && fm->entry_mtimes[i] > 0) {
|
||||
char date_str[24];
|
||||
format_datetime(date_str, sizeof(date_str), fm->entry_mtimes[i], tz);
|
||||
c.text(cols.modified_x, ty, date_str, dim);
|
||||
}
|
||||
|
||||
// Size
|
||||
if (size_col_x > 100 && !fm->is_dir[i] && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
if (cols.size_x >= 0 && !fm->is_dir[i] && row_text_visible) {
|
||||
char size_str[16];
|
||||
format_size(size_str, fm->entry_sizes[i]);
|
||||
c.text(size_col_x, ty, size_str, dim);
|
||||
c.text(cols.size_x, ty, size_str, dim);
|
||||
}
|
||||
|
||||
// Type
|
||||
if (type_col_x > 160 && ty >= list_y && ty + fm_sfh <= c.h) {
|
||||
const char* type_str = "File";
|
||||
if (fm->entry_types[i] == FM_ENTRY_VIRTUAL_ITEM) {
|
||||
type_str = fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION
|
||||
? "Setting"
|
||||
: "App";
|
||||
}
|
||||
else if (fm->entry_types[i] == FM_ENTRY_APPS_ROOT) type_str = "Apps";
|
||||
else if (fm->entry_types[i] == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT) type_str = "Settings";
|
||||
else if (fm->entry_types[i] == FM_ENTRY_HOME) type_str = "Home";
|
||||
else if (fm->entry_types[i] == FM_ENTRY_DRIVE) type_str = "Drive";
|
||||
else if (fm->entry_types[i] == FM_ENTRY_DIR) type_str = "Dir";
|
||||
else if (fm->entry_types[i] == FM_ENTRY_EXEC) type_str = "Exec";
|
||||
c.text(type_col_x, ty, type_str, dim);
|
||||
if (cols.type_x >= 0 && row_text_visible) {
|
||||
char type_str[24];
|
||||
filemanager_type_label(fm, i, type_str, sizeof(type_str));
|
||||
char type_fit[24];
|
||||
fit_label_with_ellipsis(type_fit, sizeof(type_fit), type_str,
|
||||
c.w - FM_SCROLLBAR_W - cols.type_x - 4);
|
||||
c.text(cols.type_x, ty, type_fit, dim);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,10 +660,10 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
theme.border = colors::BORDER;
|
||||
theme.text = colors::TEXT_COLOR;
|
||||
|
||||
mtk::ContextMenuItem items[8];
|
||||
filemanager_build_context_items(fm, items, 8);
|
||||
mtk::ContextMenuItem items[FM_CTX_MAX_ITEMS];
|
||||
filemanager_build_context_items(fm, items, FM_CTX_MAX_ITEMS);
|
||||
mtk::draw_context_menu(c, filemanager_context_state(fm), items,
|
||||
fm->ctx_item_count, theme);
|
||||
fm->ctx_item_count, theme, FM_CTX_MENU_W);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -887,6 +887,8 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
||||
montauk::abi::KeyEvent translated_key = key;
|
||||
montauk::keyboard::translate(ds->keyboard, &translated_key);
|
||||
const montauk::abi::KeyEvent& routed_key = translated_key;
|
||||
ds->kbd_shift_down = routed_key.shift;
|
||||
ds->kbd_ctrl_down = routed_key.ctrl;
|
||||
|
||||
if (ds->screen_locked) {
|
||||
handle_lock_keyboard(ds, routed_key);
|
||||
|
||||
@@ -181,6 +181,27 @@ void desktop_scan_apps(DesktopState* ds) {
|
||||
snprintf(item->binary_path, sizeof(item->binary_path),
|
||||
"0:/apps/%s/%s", dirname, binary);
|
||||
|
||||
// File types the app opens, used by Files for default apps and
|
||||
// "Open With". Stored lowercased as " ext1 ext2 " for whole-word
|
||||
// matching; entries that would overflow the buffer are dropped.
|
||||
if (auto* types = doc.get_array("files.types")) {
|
||||
int len = 0;
|
||||
item->file_types[len++] = ' ';
|
||||
for (int t = 0; t < types->array.count; t++) {
|
||||
auto* v = types->array.items[t];
|
||||
if (v->type != montauk::toml::Type::String || !v->str[0]) continue;
|
||||
const char* ext = v->str[0] == '.' ? v->str + 1 : v->str;
|
||||
int elen = montauk::slen(ext);
|
||||
if (elen == 0 || len + elen + 2 > (int)sizeof(item->file_types)) continue;
|
||||
for (int k = 0; k < elen; k++) {
|
||||
char ch = ext[k];
|
||||
item->file_types[len++] = (ch >= 'A' && ch <= 'Z') ? ch + 32 : ch;
|
||||
}
|
||||
item->file_types[len++] = ' ';
|
||||
}
|
||||
item->file_types[len < 2 ? 0 : len] = '\0';
|
||||
}
|
||||
|
||||
// Load icon from app directory
|
||||
item->icon = {};
|
||||
item->icon_path[0] = '\0';
|
||||
|
||||
Reference in new issue
Block a user