feat: add Files app selection marquee, Properties dialog for multiple files
This commit is contained in:
@@ -21,57 +21,65 @@ static bool path_is_same_or_descendant(const char* path, const char* root) {
|
||||
return path[root_len] == '\0' || path[root_len] == '/';
|
||||
}
|
||||
|
||||
void filemanager_do_copy(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
||||
fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||
static void filemanager_load_clipboard(FileManagerState* fm, bool is_cut) {
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
|
||||
filemanager_build_fullpath(fm->clipboard_path, 256,
|
||||
fm->current_path, fm->entry_names[fm->selected]);
|
||||
fm->clipboard_has_data = true;
|
||||
fm->clipboard_is_cut = false;
|
||||
int sel[64];
|
||||
int n = filemanager_collect_selection(fm, sel, 64);
|
||||
if (n <= 0) return;
|
||||
|
||||
int out = 0;
|
||||
for (int i = 0; i < n && out < 64; i++) {
|
||||
int idx = sel[i];
|
||||
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
|
||||
filemanager_build_fullpath(fm->clipboard_paths[out], 256,
|
||||
fm->current_path, fm->entry_names[idx]);
|
||||
out++;
|
||||
}
|
||||
if (out == 0) return;
|
||||
|
||||
fm->clipboard_count = out;
|
||||
fm->clipboard_is_cut = is_cut;
|
||||
}
|
||||
|
||||
void filemanager_do_copy(FileManagerState* fm) {
|
||||
filemanager_load_clipboard(fm, false);
|
||||
}
|
||||
|
||||
void filemanager_do_cut(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
||||
fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||
|
||||
filemanager_build_fullpath(fm->clipboard_path, 256,
|
||||
fm->current_path, fm->entry_names[fm->selected]);
|
||||
fm->clipboard_has_data = true;
|
||||
fm->clipboard_is_cut = true;
|
||||
filemanager_load_clipboard(fm, true);
|
||||
}
|
||||
|
||||
void filemanager_do_paste(FileManagerState* fm) {
|
||||
if (!fm->clipboard_has_data || filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
if (fm->clipboard_count <= 0 ||
|
||||
filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
|
||||
const char* basename = path_basename(fm->clipboard_path);
|
||||
char dst[512];
|
||||
filemanager_build_fullpath(dst, 512, fm->current_path, basename);
|
||||
if (montauk::streq(dst, fm->clipboard_path)) return;
|
||||
bool any_ok = false;
|
||||
bool clear_clipboard = fm->clipboard_is_cut;
|
||||
for (int i = 0; i < fm->clipboard_count; i++) {
|
||||
const char* src = fm->clipboard_paths[i];
|
||||
const char* basename = path_basename(src);
|
||||
char dst[512];
|
||||
filemanager_build_fullpath(dst, 512, fm->current_path, basename);
|
||||
if (montauk::streq(dst, src)) continue;
|
||||
|
||||
// Check if source is a directory by trying to readdir it
|
||||
const char* probe[1];
|
||||
int probe_count = montauk::readdir(fm->clipboard_path, probe, 1);
|
||||
bool src_is_dir = (probe_count >= 0);
|
||||
if (src_is_dir && path_is_same_or_descendant(dst, fm->clipboard_path)) return;
|
||||
const char* probe[1];
|
||||
int probe_count = montauk::readdir(src, probe, 1);
|
||||
bool src_is_dir = (probe_count >= 0);
|
||||
if (src_is_dir && path_is_same_or_descendant(dst, src)) continue;
|
||||
|
||||
bool ok;
|
||||
if (src_is_dir)
|
||||
ok = filemanager_copy_dir_recursive(fm->clipboard_path, dst);
|
||||
else
|
||||
ok = filemanager_copy_file(fm->clipboard_path, dst);
|
||||
bool ok = src_is_dir ? filemanager_copy_dir_recursive(src, dst)
|
||||
: filemanager_copy_file(src, dst);
|
||||
any_ok = any_ok || ok;
|
||||
|
||||
if (ok && fm->clipboard_is_cut) {
|
||||
if (src_is_dir)
|
||||
filemanager_delete_recursive(fm->clipboard_path);
|
||||
else
|
||||
montauk::fdelete(fm->clipboard_path);
|
||||
fm->clipboard_has_data = false;
|
||||
if (ok && fm->clipboard_is_cut) {
|
||||
if (src_is_dir) filemanager_delete_recursive(src);
|
||||
else montauk::fdelete(src);
|
||||
}
|
||||
}
|
||||
|
||||
filemanager_read_dir(fm);
|
||||
if (clear_clipboard) fm->clipboard_count = 0;
|
||||
if (any_ok) filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
void filemanager_start_rename(FileManagerState* fm) {
|
||||
@@ -147,19 +155,40 @@ void filemanager_new_folder(FileManagerState* fm) {
|
||||
}
|
||||
|
||||
void filemanager_delete_selected(FileManagerState* fm) {
|
||||
if (fm->selected < 0 || fm->selected >= fm->entry_count) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) ||
|
||||
fm->entry_types[fm->selected] == FM_ENTRY_DRIVE) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
|
||||
int sel[64];
|
||||
int n = filemanager_collect_selection(fm, sel, 64);
|
||||
if (n <= 0) return;
|
||||
|
||||
// Filter out drives.
|
||||
int filtered[64];
|
||||
int fn = 0;
|
||||
bool any_dir = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int idx = sel[i];
|
||||
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
|
||||
filtered[fn++] = idx;
|
||||
if (fm->is_dir[idx]) any_dir = true;
|
||||
}
|
||||
if (fn == 0) return;
|
||||
|
||||
// Anything with a directory or multi-item selection: confirm first.
|
||||
if (any_dir || fn > 1) {
|
||||
if (fn == 1 && any_dir) {
|
||||
filemanager_show_directory_delete_confirmation(fm, filtered[0]);
|
||||
} else {
|
||||
filemanager_show_bulk_delete_confirmation(fm, filtered, fn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Single plain file: delete immediately.
|
||||
char fullpath[512];
|
||||
filemanager_build_fullpath(fullpath, 512,
|
||||
fm->current_path, fm->entry_names[fm->selected]);
|
||||
if (fm->is_dir[fm->selected]) {
|
||||
filemanager_show_directory_delete_confirmation(fm, fm->selected);
|
||||
return;
|
||||
} else {
|
||||
montauk::fdelete(fullpath);
|
||||
}
|
||||
fm->current_path, fm->entry_names[filtered[0]]);
|
||||
montauk::fdelete(fullpath);
|
||||
filemanager_clear_multi_selection(fm);
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
@@ -197,7 +226,7 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
if (filemanager_is_computer_view(fm)) {
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_REFRESH;
|
||||
} else {
|
||||
if (fm->clipboard_has_data)
|
||||
if (fm->clipboard_count > 0)
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PASTE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_NEW_FOLDER;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,16 @@ 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: filemanager_show_properties(fm, target); break;
|
||||
case CTX_PROPERTIES: {
|
||||
if (fm->multi_count > 1) {
|
||||
int sel[64];
|
||||
int n = filemanager_collect_selection(fm, sel, 64);
|
||||
if (n > 0) filemanager_show_multi_properties(fm, sel, n);
|
||||
} else {
|
||||
filemanager_show_properties(fm, target);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CTX_REFRESH: filemanager_refresh(fm); break;
|
||||
}
|
||||
return;
|
||||
@@ -88,6 +97,8 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
else if (local_x >= 148 && local_x < 172) {
|
||||
fm->grid_view = !fm->grid_view;
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
fm->marquee_active = false;
|
||||
filemanager_clear_multi_selection(fm);
|
||||
}
|
||||
// Action buttons
|
||||
else if (local_x >= 188 && local_x < 212) filemanager_do_copy(fm);
|
||||
@@ -110,6 +121,7 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
|
||||
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) {
|
||||
@@ -122,7 +134,14 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
fm->last_click_time = now;
|
||||
}
|
||||
} else {
|
||||
// Background click in grid area: start marquee drag
|
||||
fm->selected = -1;
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->marquee_active = true;
|
||||
fm->marquee_start_x = local_x;
|
||||
fm->marquee_start_y = rel_y;
|
||||
fm->marquee_cur_x = local_x;
|
||||
fm->marquee_cur_y = rel_y;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -153,6 +172,61 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Marquee drag in grid view ----
|
||||
if (fm->marquee_active) {
|
||||
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H;
|
||||
int rel_y = local_y - list_y + fm->scrollbar.scroll_offset;
|
||||
int clamp_x = local_x;
|
||||
if (clamp_x < 0) clamp_x = 0;
|
||||
int max_x = cw - FM_SCROLLBAR_W - 1;
|
||||
if (clamp_x > max_x) clamp_x = max_x;
|
||||
if (rel_y < 0) rel_y = 0;
|
||||
|
||||
if (ev.left_held()) {
|
||||
fm->marquee_cur_x = clamp_x;
|
||||
fm->marquee_cur_y = rel_y;
|
||||
}
|
||||
|
||||
// Recompute hit-set every frame so icons highlight live during the drag.
|
||||
int cols = (cw - FM_SCROLLBAR_W) / FM_GRID_CELL_W;
|
||||
if (cols < 1) cols = 1;
|
||||
|
||||
int rx0 = fm->marquee_start_x < fm->marquee_cur_x ? fm->marquee_start_x : fm->marquee_cur_x;
|
||||
int ry0 = fm->marquee_start_y < fm->marquee_cur_y ? fm->marquee_start_y : fm->marquee_cur_y;
|
||||
int rx1 = fm->marquee_start_x > fm->marquee_cur_x ? fm->marquee_start_x : fm->marquee_cur_x;
|
||||
int ry1 = fm->marquee_start_y > fm->marquee_cur_y ? fm->marquee_start_y : fm->marquee_cur_y;
|
||||
|
||||
filemanager_clear_multi_selection(fm);
|
||||
int limit = fm->entry_count < 64 ? fm->entry_count : 64;
|
||||
for (int i = 0; i < limit; i++) {
|
||||
int col = i % cols;
|
||||
int row = i / cols;
|
||||
int cell_x = col * FM_GRID_CELL_W;
|
||||
int cell_y = filemanager_grid_row_y(fm, row, cols);
|
||||
int cell_h = filemanager_grid_row_height(fm, row, cols);
|
||||
int cx1 = cell_x + FM_GRID_CELL_W;
|
||||
int cy1 = cell_y + cell_h;
|
||||
|
||||
if (rx1 < cell_x || rx0 >= cx1) continue;
|
||||
if (ry1 < cell_y || ry0 >= cy1) continue;
|
||||
|
||||
fm->multi_selected[i] = true;
|
||||
fm->multi_count++;
|
||||
}
|
||||
fm->selected = -1;
|
||||
|
||||
if (!ev.left_held()) {
|
||||
// Release: collapse a single-item marquee to a normal selection.
|
||||
if (fm->multi_count == 1) {
|
||||
for (int i = 0; i < limit; i++) {
|
||||
if (fm->multi_selected[i]) { fm->selected = i; break; }
|
||||
}
|
||||
filemanager_clear_multi_selection(fm);
|
||||
}
|
||||
fm->marquee_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Right-click: open context menu ----
|
||||
if (ev.right_pressed()) {
|
||||
filemanager_cancel_rename(fm);
|
||||
@@ -170,7 +244,15 @@ void filemanager_on_mouse(Window* win, MouseEvent& ev) {
|
||||
int idx = filemanager_grid_index_at(fm, cols, local_x, rel_y);
|
||||
if (idx >= 0 && idx < fm->entry_count) {
|
||||
target_idx = idx;
|
||||
fm->selected = 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;
|
||||
}
|
||||
} else {
|
||||
filemanager_clear_multi_selection(fm);
|
||||
fm->selected = -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -70,11 +70,22 @@ struct FileManagerState {
|
||||
int drive_indices[64]; // drive number or special-folder index for Computer view entries
|
||||
int drive_kinds[64]; // kernel BlockDeviceKind for FM_ENTRY_DRIVE entries (4 = USB MSC)
|
||||
|
||||
// Clipboard
|
||||
char clipboard_path[256];
|
||||
bool clipboard_has_data;
|
||||
// Clipboard (supports multiple paths from a marquee selection)
|
||||
char clipboard_paths[64][256];
|
||||
int clipboard_count;
|
||||
bool clipboard_is_cut;
|
||||
|
||||
// Multi-selection (grid view marquee selection)
|
||||
bool multi_selected[64];
|
||||
int multi_count;
|
||||
|
||||
// Marquee drag state. Coordinates are stored in content space:
|
||||
// x is local_x (window content relative)
|
||||
// y is rel_y (local_y - list_y + scroll_offset), so anchor follows scroll
|
||||
bool marquee_active;
|
||||
int marquee_start_x, marquee_start_y;
|
||||
int marquee_cur_x, marquee_cur_y;
|
||||
|
||||
// Context menu
|
||||
bool ctx_open;
|
||||
int ctx_x, ctx_y;
|
||||
@@ -217,7 +228,11 @@ void filemanager_delete_selected(FileManagerState* fm);
|
||||
void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx);
|
||||
void filemanager_close_ctx_menu(FileManagerState* fm);
|
||||
void filemanager_show_properties(FileManagerState* fm, int target_idx);
|
||||
void filemanager_show_multi_properties(FileManagerState* fm,
|
||||
const int* indices, int count);
|
||||
void filemanager_show_directory_delete_confirmation(FileManagerState* fm, int target_idx);
|
||||
void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
|
||||
const int* indices, int count);
|
||||
|
||||
void filemanager_push_history(FileManagerState* fm);
|
||||
void filemanager_navigate(FileManagerState* fm, const char* name);
|
||||
@@ -239,6 +254,12 @@ int filemanager_grid_row_y(FileManagerState* fm, int row, int cols);
|
||||
int filemanager_grid_content_height(FileManagerState* fm, int cols);
|
||||
int filemanager_grid_index_at(FileManagerState* fm, int cols, int x, int rel_y);
|
||||
|
||||
// Selection helpers
|
||||
void filemanager_clear_multi_selection(FileManagerState* fm);
|
||||
// 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);
|
||||
|
||||
void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_color, Color btn_bg);
|
||||
void filemanager_on_draw(Window* win, Framebuffer& fb);
|
||||
void filemanager_on_mouse(Window* win, MouseEvent& ev);
|
||||
|
||||
@@ -14,6 +14,8 @@ static void filemanager_reset_list_state(FileManagerState* fm) {
|
||||
fm->scrollbar.scroll_offset = 0;
|
||||
fm->last_click_item = -1;
|
||||
fm->last_click_time = 0;
|
||||
fm->marquee_active = false;
|
||||
filemanager_clear_multi_selection(fm);
|
||||
}
|
||||
|
||||
static void filemanager_reset_virtual_entry(FileManagerVirtualEntry* entry) {
|
||||
|
||||
@@ -8,6 +8,26 @@
|
||||
|
||||
namespace filemanager {
|
||||
|
||||
void filemanager_clear_multi_selection(FileManagerState* fm) {
|
||||
if (!fm) return;
|
||||
for (int i = 0; i < 64; i++) fm->multi_selected[i] = false;
|
||||
fm->multi_count = 0;
|
||||
}
|
||||
|
||||
int filemanager_collect_selection(const FileManagerState* fm, int* out, int max) {
|
||||
if (!fm || !out || max <= 0) return 0;
|
||||
int n = 0;
|
||||
if (fm->multi_count > 0) {
|
||||
int limit = fm->entry_count < 64 ? fm->entry_count : 64;
|
||||
for (int i = 0; i < limit && n < max; i++) {
|
||||
if (fm->multi_selected[i]) out[n++] = i;
|
||||
}
|
||||
} else if (fm->selected >= 0 && fm->selected < fm->entry_count) {
|
||||
out[n++] = fm->selected;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int special_folder_index(const char* name) {
|
||||
for (int i = 0; i < SF_COUNT; i++) {
|
||||
if (montauk::streq(name, sf_names[i])) return i;
|
||||
|
||||
@@ -37,9 +37,13 @@ struct FileDeleteConfirmDialogState {
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
|
||||
char name[128];
|
||||
char path[512];
|
||||
char name[128]; // display name (single item) or summary ("3 items")
|
||||
char path[512]; // primary path (used for single-item subtitle)
|
||||
SvgIcon icon;
|
||||
|
||||
// Multi-item targets. count == 1 keeps the original single-folder UX.
|
||||
char paths[64][512];
|
||||
int count;
|
||||
};
|
||||
|
||||
static void props_copy(char* dst, int dst_len, const char* src) {
|
||||
@@ -437,12 +441,20 @@ static void delete_confirm_apply(FileDeleteConfirmDialogState* st) {
|
||||
if (!st) return;
|
||||
|
||||
FileManagerState* owner = delete_confirm_live_owner(st);
|
||||
bool deleted = filemanager_delete_recursive(st->path);
|
||||
bool any = false;
|
||||
int n = st->count > 0 ? st->count : (st->path[0] ? 1 : 0);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char* p = (st->count > 0) ? st->paths[i] : st->path;
|
||||
if (!p || !p[0]) continue;
|
||||
if (filemanager_delete_recursive(p)) any = true;
|
||||
}
|
||||
if (owner) {
|
||||
if (owner->delete_confirm_dialog == st)
|
||||
owner->delete_confirm_dialog = nullptr;
|
||||
if (deleted || st->path[0] != '\0')
|
||||
if (any) {
|
||||
filemanager_clear_multi_selection(owner);
|
||||
filemanager_read_dir(owner);
|
||||
}
|
||||
}
|
||||
|
||||
delete_confirm_close(st);
|
||||
@@ -468,16 +480,24 @@ static void delete_confirm_on_draw(Window* win, Framebuffer& fb) {
|
||||
}
|
||||
|
||||
char title[160];
|
||||
snprintf(title, sizeof(title), "Delete folder \"%s\"?", st->name);
|
||||
if (st->count > 1)
|
||||
snprintf(title, sizeof(title), "Delete %d items?", st->count);
|
||||
else
|
||||
snprintf(title, sizeof(title), "Delete folder \"%s\"?", st->name);
|
||||
char title_fit[160];
|
||||
props_fit_text(title, title_fit, sizeof(title_fit), c.w - 88);
|
||||
c.text(80, 22, title_fit, theme.text);
|
||||
|
||||
c.text(80, 22 + fh + 8, "This will delete all of its contents.", theme.text_muted);
|
||||
const char* sub = (st->count > 1)
|
||||
? "This will delete all selected items and their contents."
|
||||
: "This will delete all of its contents.";
|
||||
c.text(80, 22 + fh + 8, sub, theme.text_muted);
|
||||
|
||||
char path_fit[512];
|
||||
props_fit_text(st->path, path_fit, sizeof(path_fit), c.w - 96);
|
||||
c.text(80, 22 + fh * 2 + 16, path_fit, theme.text_muted);
|
||||
if (st->count <= 1) {
|
||||
char path_fit[512];
|
||||
props_fit_text(st->path, path_fit, sizeof(path_fit), c.w - 96);
|
||||
c.text(80, 22 + fh * 2 + 16, path_fit, theme.text_muted);
|
||||
}
|
||||
|
||||
Rect delete_btn;
|
||||
Rect cancel_btn;
|
||||
@@ -606,6 +626,255 @@ void filemanager_show_directory_delete_confirmation(FileManagerState* fm, int ta
|
||||
win->on_close = delete_confirm_on_close;
|
||||
}
|
||||
|
||||
void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
|
||||
const int* indices, int count) {
|
||||
if (!fm || !fm->desktop || !indices || count <= 0) return;
|
||||
if (filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm)) return;
|
||||
|
||||
DesktopState* ds = fm->desktop;
|
||||
if (fm->delete_confirm_dialog) {
|
||||
for (int i = 0; i < ds->window_count; i++) {
|
||||
if (ds->windows[i].app_data == fm->delete_confirm_dialog) {
|
||||
desktop_raise_window(ds, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fm->delete_confirm_dialog = nullptr;
|
||||
}
|
||||
|
||||
FileDeleteConfirmDialogState* st =
|
||||
(FileDeleteConfirmDialogState*)montauk::malloc(sizeof(FileDeleteConfirmDialogState));
|
||||
if (!st) return;
|
||||
montauk::memset(st, 0, sizeof(FileDeleteConfirmDialogState));
|
||||
st->desktop = ds;
|
||||
st->owner = fm;
|
||||
st->accent = ds->settings.accent_color;
|
||||
|
||||
int out = 0;
|
||||
for (int i = 0; i < count && out < 64; i++) {
|
||||
int idx = indices[i];
|
||||
if (idx < 0 || idx >= fm->entry_count) continue;
|
||||
if (fm->entry_types[idx] == FM_ENTRY_DRIVE) continue;
|
||||
filemanager_build_fullpath(st->paths[out], 512,
|
||||
fm->current_path, fm->entry_names[idx]);
|
||||
out++;
|
||||
}
|
||||
if (out == 0) {
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
st->count = out;
|
||||
|
||||
// For the single-item case, populate name/path so the dialog renders the
|
||||
// original "Delete folder X?" wording.
|
||||
if (out == 1) {
|
||||
int idx = indices[0];
|
||||
props_copy(st->name, sizeof(st->name), fm->entry_names[idx]);
|
||||
montauk::strncpy(st->path, st->paths[0], sizeof(st->path) - 1);
|
||||
} else {
|
||||
snprintf(st->name, sizeof(st->name), "%d items", out);
|
||||
}
|
||||
|
||||
if (ds->icon_folder_lg.pixels) st->icon = ds->icon_folder_lg;
|
||||
|
||||
int w = 380;
|
||||
int h = 180;
|
||||
int wx = 0;
|
||||
int wy = 0;
|
||||
props_center_dialog(ds, fm, w, h, &wx, &wy);
|
||||
|
||||
int idx = desktop_create_window(ds, "Delete Items", wx, wy, w, h);
|
||||
if (idx < 0) {
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
fm->delete_confirm_dialog = st;
|
||||
win->app_data = st;
|
||||
win->on_draw = delete_confirm_on_draw;
|
||||
win->on_mouse = delete_confirm_on_mouse;
|
||||
win->on_key = delete_confirm_on_key;
|
||||
win->on_close = delete_confirm_on_close;
|
||||
}
|
||||
|
||||
// ===== Multi-item Properties dialog =====
|
||||
|
||||
struct MultiPropertiesDialogState {
|
||||
DesktopState* desktop;
|
||||
Color accent;
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
|
||||
int total_count;
|
||||
int file_count;
|
||||
int folder_count;
|
||||
char title[64]; // "5 items"
|
||||
char breakdown[64]; // "3 files, 2 folders"
|
||||
char location[256];
|
||||
char size[64]; // sum of file sizes; "Unknown" when only folders
|
||||
bool has_size;
|
||||
SvgIcon icon; // shared, not owned
|
||||
};
|
||||
|
||||
static void multi_props_on_draw(Window* win, Framebuffer& fb) {
|
||||
(void)fb;
|
||||
MultiPropertiesDialogState* st = (MultiPropertiesDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
Canvas c(win);
|
||||
mtk::Theme theme = mtk::make_theme(st->accent);
|
||||
c.fill(theme.window_bg);
|
||||
|
||||
int pad = 16;
|
||||
int fh = system_font_height();
|
||||
|
||||
if (st->icon.pixels) {
|
||||
c.icon(pad + (48 - st->icon.width) / 2,
|
||||
pad + 2 + (48 - st->icon.height) / 2,
|
||||
st->icon);
|
||||
} else {
|
||||
c.fill_rounded_rect(pad, pad + 2, 48, 48, 4, Color::from_rgb(0x90, 0x90, 0x90));
|
||||
}
|
||||
|
||||
c.text(80, pad + 4, st->title, theme.text);
|
||||
c.text(80, pad + 4 + fh + 4, st->breakdown, theme.text_muted);
|
||||
|
||||
int y = pad + 68;
|
||||
mtk::draw_separator(c, pad, y, c.w - 2 * pad, theme);
|
||||
y += 14;
|
||||
|
||||
y = props_draw_row(c, theme, pad, y, 92, "Items", st->title);
|
||||
y = props_draw_row(c, theme, pad, y, 92, "Contents", st->breakdown);
|
||||
y = props_draw_row(c, theme, pad, y, 92, "Location", st->location);
|
||||
if (st->has_size)
|
||||
y = props_draw_row(c, theme, pad, y, 92, "Total size", st->size);
|
||||
|
||||
Rect close = props_close_button_rect(c.w, c.h);
|
||||
mtk::draw_button(c, close, "Close", mtk::BUTTON_PRIMARY,
|
||||
mtk::widget_state(false, close.contains(st->mouse_x, st->mouse_y), true),
|
||||
theme);
|
||||
}
|
||||
|
||||
static void multi_props_on_mouse(Window* win, MouseEvent& ev) {
|
||||
MultiPropertiesDialogState* st = (MultiPropertiesDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
|
||||
mtk::DesktopHost host(win);
|
||||
int mx = 0;
|
||||
int my = 0;
|
||||
if (!host.map_mouse(ev, &mx, &my)) return;
|
||||
|
||||
bool old_hover = props_close_button_rect(win->content_w, win->content_h)
|
||||
.contains(st->mouse_x, st->mouse_y);
|
||||
st->mouse_x = mx;
|
||||
st->mouse_y = my;
|
||||
Rect close = props_close_button_rect(win->content_w, win->content_h);
|
||||
bool new_hover = close.contains(mx, my);
|
||||
if (old_hover != new_hover) host.invalidate();
|
||||
|
||||
if (ev.left_pressed() && new_hover)
|
||||
props_close_self(st->desktop, st);
|
||||
}
|
||||
|
||||
static void multi_props_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
MultiPropertiesDialogState* st = (MultiPropertiesDialogState*)win->app_data;
|
||||
if (!st || !key.pressed) return;
|
||||
if (key.scancode == 0x01 || key.ascii == '\n' || key.ascii == '\r')
|
||||
props_close_self(st->desktop, st);
|
||||
}
|
||||
|
||||
static void multi_props_on_close(Window* win) {
|
||||
MultiPropertiesDialogState* st = (MultiPropertiesDialogState*)win->app_data;
|
||||
if (!st) return;
|
||||
montauk::mfree(st);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
|
||||
void filemanager_show_multi_properties(FileManagerState* fm,
|
||||
const int* indices, int count) {
|
||||
if (!fm || !fm->desktop || !indices || count <= 0) return;
|
||||
|
||||
DesktopState* ds = fm->desktop;
|
||||
MultiPropertiesDialogState* st =
|
||||
(MultiPropertiesDialogState*)montauk::malloc(sizeof(MultiPropertiesDialogState));
|
||||
if (!st) return;
|
||||
montauk::memset(st, 0, sizeof(MultiPropertiesDialogState));
|
||||
st->desktop = ds;
|
||||
st->accent = ds->settings.accent_color;
|
||||
|
||||
int total_size = 0;
|
||||
bool any_file = false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int idx = indices[i];
|
||||
if (idx < 0 || idx >= fm->entry_count) continue;
|
||||
st->total_count++;
|
||||
if (fm->is_dir[idx]) {
|
||||
st->folder_count++;
|
||||
} else {
|
||||
st->file_count++;
|
||||
total_size += fm->entry_sizes[idx];
|
||||
any_file = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (st->total_count == 0) {
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(st->title, sizeof(st->title), "%d item%s",
|
||||
st->total_count, st->total_count == 1 ? "" : "s");
|
||||
|
||||
if (st->file_count > 0 && st->folder_count > 0) {
|
||||
snprintf(st->breakdown, sizeof(st->breakdown), "%d file%s, %d folder%s",
|
||||
st->file_count, st->file_count == 1 ? "" : "s",
|
||||
st->folder_count, st->folder_count == 1 ? "" : "s");
|
||||
} else if (st->file_count > 0) {
|
||||
snprintf(st->breakdown, sizeof(st->breakdown), "%d file%s",
|
||||
st->file_count, st->file_count == 1 ? "" : "s");
|
||||
} else {
|
||||
snprintf(st->breakdown, sizeof(st->breakdown), "%d folder%s",
|
||||
st->folder_count, st->folder_count == 1 ? "" : "s");
|
||||
}
|
||||
|
||||
props_build_location(fm, st->location, sizeof(st->location));
|
||||
|
||||
if (any_file) {
|
||||
format_size(st->size, total_size);
|
||||
if (st->folder_count > 0)
|
||||
str_append(st->size, " (folders not counted)", sizeof(st->size));
|
||||
st->has_size = true;
|
||||
}
|
||||
|
||||
// Pick a representative icon: prefer folder if any folders, else file.
|
||||
if (st->folder_count > 0 && ds->icon_folder_lg.pixels)
|
||||
st->icon = ds->icon_folder_lg;
|
||||
else if (ds->icon_file_lg.pixels)
|
||||
st->icon = ds->icon_file_lg;
|
||||
|
||||
int w = 440;
|
||||
int h = 280;
|
||||
int wx = 0;
|
||||
int wy = 0;
|
||||
props_center_dialog(ds, fm, w, h, &wx, &wy);
|
||||
|
||||
char title[64];
|
||||
snprintf(title, sizeof(title), "%d Items Properties", st->total_count);
|
||||
int idx = desktop_create_window(ds, title, wx, wy, w, h);
|
||||
if (idx < 0) {
|
||||
montauk::mfree(st);
|
||||
return;
|
||||
}
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
win->app_data = st;
|
||||
win->on_draw = multi_props_on_draw;
|
||||
win->on_mouse = multi_props_on_mouse;
|
||||
win->on_key = multi_props_on_key;
|
||||
win->on_close = multi_props_on_close;
|
||||
}
|
||||
|
||||
void filemanager_show_properties(FileManagerState* fm, int target_idx) {
|
||||
if (!fm || target_idx < 0 || target_idx >= fm->entry_count || !fm->desktop)
|
||||
return;
|
||||
|
||||
@@ -206,10 +206,15 @@ void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_colo
|
||||
c.vline(180, 6, 20, colors::BORDER);
|
||||
|
||||
// Action buttons: Copy, Cut, Paste, Rename, New Folder, Delete
|
||||
bool has_sel = fm->selected >= 0 && fm->selected < fm->entry_count
|
||||
&& !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm)
|
||||
&& fm->entry_types[fm->selected] != FM_ENTRY_DRIVE;
|
||||
bool has_clip = fm->clipboard_has_data && !filemanager_is_computer_view(fm)
|
||||
bool single_sel = fm->selected >= 0 && fm->selected < fm->entry_count
|
||||
&& !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm)
|
||||
&& fm->entry_types[fm->selected] != FM_ENTRY_DRIVE;
|
||||
bool multi_sel = fm->multi_count > 0
|
||||
&& !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm);
|
||||
bool has_sel = single_sel || multi_sel;
|
||||
// Rename is single-target only.
|
||||
bool has_rename = single_sel && !multi_sel;
|
||||
bool has_clip = fm->clipboard_count > 0 && !filemanager_is_computer_view(fm)
|
||||
&& !filemanager_is_virtual_view(fm);
|
||||
Color dim_bg = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
||||
|
||||
@@ -219,7 +224,7 @@ void filemanager_draw_header(Canvas& c, FileManagerState* fm, Color toolbar_colo
|
||||
{ 188, ds ? &ds->icon_copy : nullptr, has_sel },
|
||||
{ 216, ds ? &ds->icon_cut : nullptr, has_sel },
|
||||
{ 244, ds ? &ds->icon_paste : nullptr, has_clip },
|
||||
{ 272, ds ? &ds->icon_rename : nullptr, has_sel },
|
||||
{ 272, ds ? &ds->icon_rename : nullptr, has_rename },
|
||||
{ 300, ds ? &ds->icon_folder_new : nullptr,
|
||||
!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm) },
|
||||
{ 328, ds ? &ds->icon_delete : nullptr, has_sel },
|
||||
@@ -306,8 +311,10 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
// Skip if entirely off-screen
|
||||
if (cell_y + row_h <= list_y || cell_y >= c.h) continue;
|
||||
|
||||
// Selection highlight
|
||||
if (i == fm->selected) {
|
||||
// Selection highlight (single or multi)
|
||||
bool cell_selected = (i == fm->selected) ||
|
||||
(i < 64 && fm->multi_selected[i]);
|
||||
if (cell_selected) {
|
||||
int sy = gui_max(cell_y, list_y);
|
||||
int sh = gui_min(cell_y + row_h, c.h) - sy;
|
||||
int sw = gui_min(FM_GRID_CELL_W, c.w - FM_SCROLLBAR_W - cell_x);
|
||||
@@ -398,6 +405,30 @@ void filemanager_on_draw(Window* win, Framebuffer& fb) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Marquee drawn on top of cells: a translucent accent wash so icons
|
||||
// and labels remain visible underneath, plus a darker outline.
|
||||
if (fm->marquee_active) {
|
||||
int rx0 = fm->marquee_start_x < fm->marquee_cur_x ? fm->marquee_start_x : fm->marquee_cur_x;
|
||||
int ry0 = fm->marquee_start_y < fm->marquee_cur_y ? fm->marquee_start_y : fm->marquee_cur_y;
|
||||
int rx1 = fm->marquee_start_x > fm->marquee_cur_x ? fm->marquee_start_x : fm->marquee_cur_x;
|
||||
int ry1 = fm->marquee_start_y > fm->marquee_cur_y ? fm->marquee_start_y : fm->marquee_cur_y;
|
||||
|
||||
int sy0 = list_y + ry0 - fm->scrollbar.scroll_offset;
|
||||
int sy1 = list_y + ry1 - fm->scrollbar.scroll_offset;
|
||||
int max_x = c.w - FM_SCROLLBAR_W;
|
||||
if (rx1 > max_x) rx1 = max_x;
|
||||
if (sy0 < list_y) sy0 = list_y;
|
||||
if (sy1 > c.h) sy1 = c.h;
|
||||
int mw = rx1 - rx0;
|
||||
int mh = sy1 - sy0;
|
||||
if (mw > 0 && mh > 0) {
|
||||
Color accent = ds ? ds->settings.accent_color : colors::ACCENT;
|
||||
Color wash = Color::from_rgba(accent.r, accent.g, accent.b, 80);
|
||||
c.fill_rect_alpha(rx0, sy0, mw, mh, wash);
|
||||
c.rect(rx0, sy0, mw, mh, gui::mtk::darken(accent, 48));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ---- List View ----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user