diff --git a/programs/include/gui/canvas.hpp b/programs/include/gui/canvas.hpp index f526098..873eb75 100644 --- a/programs/include/gui/canvas.hpp +++ b/programs/include/gui/canvas.hpp @@ -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(); diff --git a/programs/src/desktop/apps/filemanager/actions.cpp b/programs/src/desktop/apps/filemanager/actions.cpp index c6e80ec..35608e1 100644 --- a/programs/src/desktop/apps/filemanager/actions.cpp +++ b/programs/src/desktop/apps/filemanager/actions.cpp @@ -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; } diff --git a/programs/src/desktop/apps/filemanager/events.cpp b/programs/src/desktop/apps/filemanager/events.cpp index 7999b38..c3ec687 100644 --- a/programs/src/desktop/apps/filemanager/events.cpp +++ b/programs/src/desktop/apps/filemanager/events.cpp @@ -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 { diff --git a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp index 2350166..6790fff 100644 --- a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp +++ b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp @@ -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); diff --git a/programs/src/desktop/apps/filemanager/filesystem.cpp b/programs/src/desktop/apps/filemanager/filesystem.cpp index 3510011..4b31c7e 100644 --- a/programs/src/desktop/apps/filemanager/filesystem.cpp +++ b/programs/src/desktop/apps/filemanager/filesystem.cpp @@ -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) { diff --git a/programs/src/desktop/apps/filemanager/helpers.cpp b/programs/src/desktop/apps/filemanager/helpers.cpp index f3eca1d..ba01f7d 100644 --- a/programs/src/desktop/apps/filemanager/helpers.cpp +++ b/programs/src/desktop/apps/filemanager/helpers.cpp @@ -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; diff --git a/programs/src/desktop/apps/filemanager/properties.cpp b/programs/src/desktop/apps/filemanager/properties.cpp index 620c516..c82579e 100644 --- a/programs/src/desktop/apps/filemanager/properties.cpp +++ b/programs/src/desktop/apps/filemanager/properties.cpp @@ -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; diff --git a/programs/src/desktop/apps/filemanager/render.cpp b/programs/src/desktop/apps/filemanager/render.cpp index c0aebba..35edcf0 100644 --- a/programs/src/desktop/apps/filemanager/render.cpp +++ b/programs/src/desktop/apps/filemanager/render.cpp @@ -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 ----