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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user