Files
MontaukOS/programs/src/desktop/apps/filemanager/properties.cpp
T

958 lines
32 KiB
C++

/*
* properties.cpp
* MTK file properties dialog for the embedded desktop file manager
* Copyright (c) 2026 Daniel Hammer
*/
#include "filemanager_internal.hpp"
#include <gui/mtk/hosts.hpp>
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