feat: add Files app selection marquee, Properties dialog for multiple files

This commit is contained in:
2026-05-26 18:27:31 +02:00
parent c1d3d05c34
commit f2ca3956b0
8 changed files with 553 additions and 70 deletions
@@ -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;