/* * properties.cpp * MTK file properties dialog for the embedded desktop file manager * Copyright (c) 2026 Daniel Hammer */ #include "filemanager_internal.hpp" #include namespace filemanager { struct FilePropertiesDialogState { DesktopState* desktop; Color accent; int mouse_x; int mouse_y; char name[128]; char type[64]; char location[256]; char path[512]; char size[32]; char contains[32]; bool has_path; bool has_size; bool has_contains; bool owns_icon; bool fallback_is_dir; SvgIcon icon; }; struct FileDeleteConfirmDialogState { DesktopState* desktop; FileManagerState* owner; Color accent; int mouse_x; int mouse_y; 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 (dynamically allocated). count == 1 keeps the original // single-folder UX. char (*paths)[512]; int count; }; static void props_copy(char* dst, int dst_len, const char* src) { if (!dst || dst_len <= 0) return; if (!src) src = ""; montauk::strncpy(dst, src, dst_len - 1); dst[dst_len - 1] = '\0'; } static void props_fit_text(const char* src, char* out, int out_len, int max_w) { props_copy(out, out_len, src); if (out_len <= 4 || text_width(out) <= max_w) return; int ellipsis_w = text_width("..."); int len = montauk::slen(out); while (len > 0) { out[len] = '\0'; if (text_width(out) + ellipsis_w <= max_w) break; len--; } if (len > out_len - 4) len = out_len - 4; if (len < 0) len = 0; out[len] = '.'; out[len + 1] = '.'; out[len + 2] = '.'; out[len + 3] = '\0'; } static Rect props_close_button_rect(int content_w, int content_h) { return {content_w - 96, content_h - 42, 80, 28}; } static void props_close_self(DesktopState* ds, void* app_data) { if (!ds) return; for (int i = 0; i < ds->window_count; i++) { if (ds->windows[i].app_data == app_data) { desktop_close_window(ds, i); return; } } } void props_center_dialog(DesktopState* ds, const void* owner_app_data, int w, int h, int* out_x, int* out_y) { if (!out_x || !out_y) return; int wx = 180; int wy = 120; if (ds) { for (int i = 0; i < ds->window_count; i++) { if (ds->windows[i].app_data == owner_app_data) { wx = ds->windows[i].frame.x + (ds->windows[i].frame.w - w) / 2; wy = ds->windows[i].frame.y + (ds->windows[i].frame.h - h) / 2; break; } } if (wx < 8) wx = 8; if (wy < PANEL_HEIGHT + 8) wy = PANEL_HEIGHT + 8; if (wx + w > ds->screen_w) wx = ds->screen_w - w; if (wy + h > ds->screen_h) wy = ds->screen_h - h; } if (wx < 0) wx = 0; if (wy < 0) wy = 0; *out_x = wx; *out_y = wy; } static void props_drive_path(int drive, char* out, int out_len) { if (!out || out_len <= 0) return; if (drive < 0) { out[0] = '\0'; return; } if (drive < 10) { snprintf(out, out_len, "%d:/", drive); } else { snprintf(out, out_len, "%d:/", drive); } } static const char* props_type_label(const FileManagerState* fm, int idx) { if (!fm || idx < 0 || idx >= fm->entry_count) return "Item"; switch (fm->entry_types[idx]) { case FM_ENTRY_DIR: return "Folder"; case FM_ENTRY_EXEC: return "Executable"; case FM_ENTRY_DRIVE: return "Drive"; case FM_ENTRY_HOME: return "Home Folder"; case FM_ENTRY_APPS_ROOT: return "Apps Folder"; case FM_ENTRY_SPECIAL_DIR: return "Folder"; case FM_ENTRY_SYSTEM_CONFIGURATION_ROOT: return "Settings"; case FM_ENTRY_VIRTUAL_APP: if (fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION) return "Settings Applet"; return "Application"; case FM_ENTRY_FILE: default: return fm->is_dir[idx] ? "Folder" : "File"; } } static void props_build_location(const FileManagerState* fm, char* out, int out_len) { if (!fm) { props_copy(out, out_len, ""); return; } if (filemanager_is_computer_view(fm)) { props_copy(out, out_len, "Computer"); } else if (filemanager_is_virtual_view(fm)) { props_copy(out, out_len, filemanager_virtual_view_label(fm->virtual_view)); } else { props_copy(out, out_len, fm->current_path); } } static void props_build_path(const FileManagerState* fm, int idx, char* out, int out_len) { if (!fm || idx < 0 || idx >= fm->entry_count || !out || out_len <= 0) return; out[0] = '\0'; DesktopState* ds = fm->desktop; int type = fm->entry_types[idx]; if (!filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm)) { filemanager_build_fullpath(out, out_len, fm->current_path, fm->entry_names[idx]); return; } if (filemanager_is_computer_view(fm)) { if (type == FM_ENTRY_HOME && ds) { props_copy(out, out_len, ds->home_dir); } else if (type == FM_ENTRY_SPECIAL_DIR && ds) { props_copy(out, out_len, ds->home_dir); int plen = montauk::slen(out); if (plen > 0 && out[plen - 1] != '/') str_append(out, "/", out_len); str_append(out, fm->entry_names[idx], out_len); } else if (type == FM_ENTRY_DRIVE) { props_drive_path(fm->drive_indices[idx], out, out_len); } else if (type == FM_ENTRY_APPS_ROOT) { props_copy(out, out_len, "virtual://apps"); } else if (type == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT) { props_copy(out, out_len, "virtual://system-configuration"); } return; } if (type == FM_ENTRY_VIRTUAL_APP && idx < fm->virtual_entry_count) { const FileManagerVirtualEntry& entry = fm->virtual_entries[idx]; if (entry.kind == FM_VIRTUAL_ENTRY_EXTERNAL_APP && ds && entry.external_app_index >= 0 && entry.external_app_index < ds->external_app_count) { props_copy(out, out_len, ds->external_apps[entry.external_app_index].binary_path); } else if (entry.kind == FM_VIRTUAL_ENTRY_SYSTEM_CONFIGURATION_APPLET) { props_copy(out, out_len, "Settings applet"); } } } static void props_set_shared_icon(FilePropertiesDialogState* st, const SvgIcon* icon) { if (!st || !icon || !icon->pixels) return; st->icon = *icon; st->owns_icon = false; } static void props_load_owned_icon(FilePropertiesDialogState* st, const char* path) { if (!st || !path || !path[0]) return; SvgIcon icon = svg_load(path, 48, 48, colors::ICON_COLOR); if (!icon.pixels) return; st->icon = icon; st->owns_icon = true; } static void props_choose_icon(FilePropertiesDialogState* st, const FileManagerState* fm, int idx) { if (!st || !fm || idx < 0 || idx >= fm->entry_count) return; DesktopState* ds = fm->desktop; int type = fm->entry_types[idx]; st->fallback_is_dir = fm->is_dir[idx] || type == FM_ENTRY_DIR || type == FM_ENTRY_SPECIAL_DIR || type == FM_ENTRY_HOME || type == FM_ENTRY_APPS_ROOT || type == FM_ENTRY_DRIVE || type == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT; int sfi = -1; if (type == FM_ENTRY_SPECIAL_DIR) sfi = fm->drive_indices[idx]; else if (ds && type == FM_ENTRY_DIR) sfi = special_folder_index(fm->entry_names[idx]); if (sfi >= 0 && ds && ds->icon_special_folder_lg[sfi].pixels) { props_set_shared_icon(st, &ds->icon_special_folder_lg[sfi]); } else if (type == FM_ENTRY_VIRTUAL_APP && idx < fm->virtual_entry_count) { const FileManagerVirtualEntry& entry = fm->virtual_entries[idx]; if (entry.kind == FM_VIRTUAL_ENTRY_EXTERNAL_APP && ds && entry.external_app_index >= 0 && entry.external_app_index < ds->external_app_count) { props_load_owned_icon(st, ds->external_apps[entry.external_app_index].icon_path); } else if (entry.kind == FM_VIRTUAL_ENTRY_SYSTEM_CONFIGURATION_APPLET) { props_load_owned_icon(st, entry.applet.icon_path); } } else if (ds && type == FM_ENTRY_SYSTEM_CONFIGURATION_ROOT && ds->icon_system_configuration_lg.pixels) { props_set_shared_icon(st, &ds->icon_system_configuration_lg); } else if (ds && type == FM_ENTRY_APPS_ROOT && ds->icon_apps_lg.pixels) { props_set_shared_icon(st, &ds->icon_apps_lg); } else if (ds && type == FM_ENTRY_HOME && ds->icon_home_folder_lg.pixels) { props_set_shared_icon(st, &ds->icon_home_folder_lg); } else if (ds && type == FM_ENTRY_DRIVE && ds->icon_drive_lg.pixels) { props_set_shared_icon(st, &ds->icon_drive_lg); } else if (ds && type == FM_ENTRY_DIR && ds->icon_folder_lg.pixels) { props_set_shared_icon(st, &ds->icon_folder_lg); } else if (ds && type == FM_ENTRY_EXEC && ds->icon_exec_lg.pixels) { props_set_shared_icon(st, &ds->icon_exec_lg); } else if (ds && ds->icon_file_lg.pixels) { props_set_shared_icon(st, &ds->icon_file_lg); } } static bool props_entry_is_directory_like(const FileManagerState* fm, int idx) { if (!fm || idx < 0 || idx >= fm->entry_count) return false; int type = fm->entry_types[idx]; return fm->is_dir[idx] || type == FM_ENTRY_DIR || type == FM_ENTRY_HOME || type == FM_ENTRY_SPECIAL_DIR || type == FM_ENTRY_DRIVE; } static void props_populate(FilePropertiesDialogState* st, const FileManagerState* fm, int idx) { props_copy(st->name, sizeof(st->name), fm->entry_names[idx]); props_copy(st->type, sizeof(st->type), props_type_label(fm, idx)); props_build_location(fm, st->location, sizeof(st->location)); props_build_path(fm, idx, st->path, sizeof(st->path)); st->has_path = st->path[0] != '\0'; if (!props_entry_is_directory_like(fm, idx) && !filemanager_is_computer_view(fm) && !filemanager_is_virtual_view(fm)) { format_size(st->size, fm->entry_sizes[idx]); st->has_size = true; } if (props_entry_is_directory_like(fm, idx) && st->has_path) { const char* names[256]; int count = montauk::readdir(st->path, names, 256); if (count >= 0) { if (count >= 256) snprintf(st->contains, sizeof(st->contains), "256+ items"); else snprintf(st->contains, sizeof(st->contains), "%d item%s", count, count == 1 ? "" : "s"); st->has_contains = true; } } props_choose_icon(st, fm, idx); } static void props_draw_icon(Canvas& c, FilePropertiesDialogState* st, int x, int y) { if (st->icon.pixels) { c.icon(x + (48 - st->icon.width) / 2, y + (48 - st->icon.height) / 2, st->icon); return; } Color fallback = st->fallback_is_dir ? Color::from_rgb(0xFF, 0xBD, 0x2E) : Color::from_rgb(0x90, 0x90, 0x90); c.fill_rounded_rect(x, y, 48, 48, 4, fallback); } static int props_draw_row(Canvas& c, const mtk::Theme& theme, int x, int y, int label_w, const char* label, const char* value) { int fh = system_font_height(); c.text(x, y, label, theme.text_muted); char fit[512]; props_fit_text(value, fit, sizeof(fit), c.w - x - label_w - 16); c.text(x + label_w, y, fit, theme.text); return y + fh + 8; } static void props_on_draw(Window* win, Framebuffer& fb) { (void)fb; FilePropertiesDialogState* st = (FilePropertiesDialogState*)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(); props_draw_icon(c, st, pad, pad + 2); char title[128]; props_fit_text(st->name, title, sizeof(title), c.w - 88); c.text(80, pad + 4, title, theme.text); c.text(80, pad + 4 + fh + 4, st->type, 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, 82, "Name", st->name); y = props_draw_row(c, theme, pad, y, 82, "Type", st->type); y = props_draw_row(c, theme, pad, y, 82, "Location", st->location); if (st->has_path) y = props_draw_row(c, theme, pad, y, 82, "Path", st->path); if (st->has_size) y = props_draw_row(c, theme, pad, y, 82, "Size", st->size); if (st->has_contains) y = props_draw_row(c, theme, pad, y, 82, "Contains", st->contains); 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 props_on_mouse(Window* win, MouseEvent& ev) { FilePropertiesDialogState* st = (FilePropertiesDialogState*)win->app_data; if (!st) return; mtk::DesktopHost host(win); int mx = 0; int my = 0; if (!host.map_mouse(ev, &mx, &my)) return; st->mouse_x = mx; st->mouse_y = my; Rect close = props_close_button_rect(win->content_w, win->content_h); if (ev.left_pressed() && close.contains(mx, my)) props_close_self(st->desktop, st); } static void props_on_key(Window* win, const Montauk::KeyEvent& key) { FilePropertiesDialogState* st = (FilePropertiesDialogState*)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 props_on_close(Window* win) { FilePropertiesDialogState* st = (FilePropertiesDialogState*)win->app_data; if (!st) return; if (st->owns_icon) svg_free(st->icon); montauk::mfree(st); win->app_data = nullptr; } // Subtitle wording, shared between the width calculation at creation time and // the draw path so the two never drift out of sync. static const char* const kDeleteSubMulti = "This will delete all selected items and their contents."; static const char* const kDeleteSubSingle = "This will delete all of its contents."; // Pick a dialog width wide enough that the subtitle (drawn at x=80 with a 16px // right margin) fits without being clipped. Falls back to the original 380px // minimum and is capped so an absurd subtitle cannot create a huge window. static int delete_confirm_dialog_width(const char* sub) { int needed = 80 + text_width(sub) + 16; int w = 380; if (needed > w) w = needed; if (w > 560) w = 560; return w; } static void delete_confirm_button_rects(int content_w, int content_h, Rect* delete_btn, Rect* cancel_btn) { int btn_w = 92; int btn_h = 30; int gap = 8; int y = content_h - 44; int cancel_x = content_w - 16 - btn_w; int delete_x = cancel_x - gap - btn_w; if (delete_btn) *delete_btn = {delete_x, y, btn_w, btn_h}; if (cancel_btn) *cancel_btn = {cancel_x, y, btn_w, btn_h}; } static FileManagerState* delete_confirm_live_owner(FileDeleteConfirmDialogState* st) { if (!st || !st->desktop || !st->owner) return nullptr; for (int i = 0; i < st->desktop->window_count; i++) { Window& win = st->desktop->windows[i]; if (win.app_data == st->owner && win.on_draw == filemanager_on_draw) return st->owner; } return nullptr; } static void delete_confirm_close(FileDeleteConfirmDialogState* st) { if (!st) return; props_close_self(st->desktop, st); } static void delete_confirm_apply(FileDeleteConfirmDialogState* st) { if (!st) return; FileManagerState* owner = delete_confirm_live_owner(st); int n = st->count > 0 ? st->count : (st->path[0] ? 1 : 0); // Delete on a worker thread with a progress dialog. The job copies the path // list, so it stays valid after this confirm dialog is freed. bool started = false; if (owner && n > 0) { const char** arr = (const char**)montauk::malloc((uint64_t)n * sizeof(const char*)); if (arr) { for (int i = 0; i < n; i++) arr[i] = (st->count > 0) ? st->paths[i] : st->path; started = filemanager_start_delete_job(owner, arr, n); montauk::mfree(arr); } } if (!started) { // Fallback: delete synchronously (e.g. another operation is running). bool any = false; 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 && any) { filemanager_clear_multi_selection(owner); filemanager_read_dir(owner); } } if (owner && owner->delete_confirm_dialog == st) owner->delete_confirm_dialog = nullptr; delete_confirm_close(st); } static void delete_confirm_on_draw(Window* win, Framebuffer& fb) { (void)fb; FileDeleteConfirmDialogState* st = (FileDeleteConfirmDialogState*)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, 22 + (48 - st->icon.height) / 2, st->icon); } else { c.fill_rounded_rect(pad, 22, 48, 48, 4, Color::from_rgb(0xFF, 0xBD, 0x2E)); } 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); const char* sub = (st->count > 1) ? kDeleteSubMulti : kDeleteSubSingle; char sub_fit[160]; props_fit_text(sub, sub_fit, sizeof(sub_fit), c.w - 96); c.text(80, 22 + fh + 8, sub_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; delete_confirm_button_rects(c.w, c.h, &delete_btn, &cancel_btn); mtk::draw_button(c, delete_btn, "Delete", mtk::BUTTON_DANGER, mtk::widget_state(false, delete_btn.contains(st->mouse_x, st->mouse_y), true), theme); mtk::draw_button(c, cancel_btn, "Cancel", mtk::BUTTON_SECONDARY, mtk::widget_state(false, cancel_btn.contains(st->mouse_x, st->mouse_y), true), theme); } static void delete_confirm_on_mouse(Window* win, MouseEvent& ev) { FileDeleteConfirmDialogState* st = (FileDeleteConfirmDialogState*)win->app_data; if (!st) return; mtk::DesktopHost host(win); int mx = 0; int my = 0; if (!host.map_mouse(ev, &mx, &my)) return; Rect delete_btn; Rect cancel_btn; delete_confirm_button_rects(win->content_w, win->content_h, &delete_btn, &cancel_btn); bool old_delete = delete_btn.contains(st->mouse_x, st->mouse_y); bool old_cancel = cancel_btn.contains(st->mouse_x, st->mouse_y); bool new_delete = delete_btn.contains(mx, my); bool new_cancel = cancel_btn.contains(mx, my); st->mouse_x = mx; st->mouse_y = my; if (old_delete != new_delete || old_cancel != new_cancel) host.invalidate(); if (ev.left_pressed()) { if (new_delete) { delete_confirm_apply(st); } else if (new_cancel) { delete_confirm_close(st); } } } static void delete_confirm_on_key(Window* win, const Montauk::KeyEvent& key) { FileDeleteConfirmDialogState* st = (FileDeleteConfirmDialogState*)win->app_data; if (!st || !key.pressed) return; if (key.ascii == '\n' || key.ascii == '\r') { delete_confirm_apply(st); } else if (key.scancode == 0x01) { delete_confirm_close(st); } } static void delete_confirm_on_close(Window* win) { FileDeleteConfirmDialogState* st = (FileDeleteConfirmDialogState*)win->app_data; if (!st) return; FileManagerState* owner = delete_confirm_live_owner(st); if (owner && owner->delete_confirm_dialog == st) owner->delete_confirm_dialog = nullptr; if (st->paths) montauk::mfree(st->paths); montauk::mfree(st); win->app_data = nullptr; } void filemanager_show_directory_delete_confirmation(FileManagerState* fm, int target_idx) { if (!fm || !fm->desktop || target_idx < 0 || target_idx >= fm->entry_count) return; if (!fm->is_dir[target_idx] || filemanager_is_computer_view(fm) || filemanager_is_virtual_view(fm) || fm->entry_types[target_idx] == FM_ENTRY_DRIVE) 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; props_copy(st->name, sizeof(st->name), fm->entry_names[target_idx]); filemanager_build_fullpath(st->path, sizeof(st->path), fm->current_path, fm->entry_names[target_idx]); int sfi = -1; if (fm->entry_types[target_idx] == FM_ENTRY_SPECIAL_DIR) sfi = fm->drive_indices[target_idx]; else if (fm->entry_types[target_idx] == FM_ENTRY_DIR) sfi = special_folder_index(fm->entry_names[target_idx]); if (sfi >= 0 && ds->icon_special_folder_lg[sfi].pixels) { st->icon = ds->icon_special_folder_lg[sfi]; } else if (ds->icon_folder_lg.pixels) { st->icon = ds->icon_folder_lg; } int w = delete_confirm_dialog_width(kDeleteSubSingle); 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 Folder", 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; } 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; st->paths = (char(*)[512])montauk::malloc((uint64_t)count * 512); if (!st->paths) { montauk::mfree(st); return; } int out = 0; for (int i = 0; i < count; 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->paths); 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 = delete_confirm_dialog_width(out > 1 ? kDeleteSubMulti : kDeleteSubSingle); 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; uint64_t 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; DesktopState* ds = fm->desktop; FilePropertiesDialogState* st = (FilePropertiesDialogState*)montauk::malloc(sizeof(FilePropertiesDialogState)); if (!st) return; montauk::memset(st, 0, sizeof(FilePropertiesDialogState)); st->desktop = ds; st->accent = ds->settings.accent_color; props_populate(st, fm, target_idx); int w = 420; int h = 300; int wx = 0; int wy = 0; props_center_dialog(ds, fm, w, h, &wx, &wy); char title[64]; snprintf(title, sizeof(title), "%s Properties", st->name); int idx = desktop_create_window(ds, title, wx, wy, w, h); if (idx < 0) { if (st->owns_icon) svg_free(st->icon); montauk::mfree(st); return; } Window* win = &ds->windows[idx]; win->app_data = st; win->on_draw = props_on_draw; win->on_mouse = props_on_mouse; win->on_key = props_on_key; win->on_close = props_on_close; } } // namespace filemanager