feat: add Files app selection marquee, Properties dialog for multiple files
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user