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
+29
View File
@@ -71,6 +71,35 @@ struct Canvas {
}
}
void fill_rect_alpha(int x, int y, int rw, int rh, Color c) {
if (c.a == 0) return;
if (c.a == 255) { fill_rect(x, y, rw, rh, c); return; }
int x0 = gui_max(x, 0), y0 = gui_max(y, 0);
int x1 = gui_min(x + rw, w), y1 = gui_min(y + rh, h);
if (x0 >= x1 || y0 >= y1) return;
uint32_t a = c.a;
uint32_t inv_a = 255 - a;
uint32_t src_r = a * c.r;
uint32_t src_g = a * c.g;
uint32_t src_b = a * c.b;
for (int dy = y0; dy < y1; dy++) {
uint32_t* row = pixels + dy * w;
for (int dx = x0; dx < x1; dx++) {
uint32_t p = row[dx];
uint32_t dr = (p >> 16) & 0xFF;
uint32_t dg = (p >> 8) & 0xFF;
uint32_t db = p & 0xFF;
uint32_t nr = (src_r + dr * inv_a) / 255;
uint32_t ng = (src_g + dg * inv_a) / 255;
uint32_t nb = (src_b + db * inv_a) / 255;
row[dx] = (0xFFu << 24) | (nr << 16) | (ng << 8) | nb;
}
}
}
void fill_rounded_rect(int x, int y, int rw, int rh, int radius, Color c) {
if (radius <= 0) { fill_rect(x, y, rw, rh, c); return; }
uint32_t px = c.to_pixel();
@@ -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);
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, fm->clipboard_path)) return;
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);
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, fm->clipboard_path)) return;
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 (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 {
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,8 +244,16 @@ 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;
// 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 {
int list_y = FM_TOOLBAR_H + FM_PATHBAR_H + FM_HEADER_H;
@@ -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,13 +441,21 @@ 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];
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);
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
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 has_clip = fm->clipboard_has_data && !filemanager_is_computer_view(fm)
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 ----