feat: properties menu in Files and delete confirmation for directories
This commit is contained in:
@@ -19,7 +19,7 @@ PROG_INC := ../../include
|
||||
JPEG_LIB := ../../lib/libjpeg
|
||||
LIBC_LIB := ../../lib/libc
|
||||
BEARSSL := ../../lib/bearssl
|
||||
LOADER_LIB := ../../bin/liblibloader.a
|
||||
LOADER_LIB := ../../lib/libloader/liblibloader.a
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
|
||||
@@ -139,10 +139,12 @@ void filemanager_delete_selected(FileManagerState* fm) {
|
||||
char fullpath[512];
|
||||
filemanager_build_fullpath(fullpath, 512,
|
||||
fm->current_path, fm->entry_names[fm->selected]);
|
||||
if (fm->is_dir[fm->selected])
|
||||
filemanager_delete_recursive(fullpath);
|
||||
else
|
||||
if (fm->is_dir[fm->selected]) {
|
||||
filemanager_show_directory_delete_confirmation(fm, fm->selected);
|
||||
return;
|
||||
} else {
|
||||
montauk::fdelete(fullpath);
|
||||
}
|
||||
filemanager_read_dir(fm);
|
||||
}
|
||||
|
||||
@@ -162,12 +164,14 @@ void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, i
|
||||
|| (filemanager_is_virtual_view(fm) && tt == FM_ENTRY_VIRTUAL_APP)) {
|
||||
// Drive, Home, Apps shortcut, or app entry: only Open
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||
} else {
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_OPEN;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_COPY;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_CUT;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_RENAME;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_DELETE;
|
||||
fm->ctx_items[fm->ctx_item_count++] = CTX_PROPERTIES;
|
||||
}
|
||||
} else {
|
||||
// Background click
|
||||
|
||||
@@ -50,6 +50,7 @@ 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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,6 +99,9 @@ struct FileManagerState {
|
||||
FileManagerVirtualViewKind virtual_view;
|
||||
FileManagerVirtualEntry virtual_entries[64];
|
||||
int virtual_entry_count;
|
||||
|
||||
// File Manager-owned dialogs
|
||||
void* delete_confirm_dialog;
|
||||
};
|
||||
|
||||
inline constexpr int FM_TOOLBAR_H = 32;
|
||||
@@ -131,6 +134,7 @@ inline constexpr int CTX_PASTE = 3;
|
||||
inline constexpr int CTX_RENAME = 4;
|
||||
inline constexpr int CTX_DELETE = 5;
|
||||
inline constexpr int CTX_NEW_FOLDER = 6;
|
||||
inline constexpr int CTX_PROPERTIES = 7;
|
||||
|
||||
inline bool filemanager_is_virtual_view(const FileManagerState* fm) {
|
||||
return fm && fm->virtual_view != FM_VIRTUAL_VIEW_NONE;
|
||||
@@ -210,6 +214,8 @@ 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_directory_delete_confirmation(FileManagerState* fm, int target_idx);
|
||||
|
||||
void filemanager_push_history(FileManagerState* fm);
|
||||
void filemanager_navigate(FileManagerState* fm, const char* name);
|
||||
|
||||
@@ -24,6 +24,7 @@ const char* ctx_label(int action) {
|
||||
case CTX_RENAME: return "Rename";
|
||||
case CTX_DELETE: return "Delete";
|
||||
case CTX_NEW_FOLDER: return "New Folder";
|
||||
case CTX_PROPERTIES: return "Properties";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
/*
|
||||
* 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];
|
||||
char path[512];
|
||||
SvgIcon icon;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static 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 "System Configuration";
|
||||
case FM_ENTRY_VIRTUAL_APP:
|
||||
if (fm->virtual_view == FM_VIRTUAL_VIEW_SYSTEM_CONFIGURATION)
|
||||
return "System Configuration 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, "System Configuration 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;
|
||||
}
|
||||
|
||||
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);
|
||||
bool deleted = filemanager_delete_recursive(st->path);
|
||||
if (owner) {
|
||||
if (owner->delete_confirm_dialog == st)
|
||||
owner->delete_confirm_dialog = nullptr;
|
||||
if (deleted || st->path[0] != '\0')
|
||||
filemanager_read_dir(owner);
|
||||
}
|
||||
|
||||
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];
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
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 = 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 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_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
|
||||
@@ -34,17 +34,21 @@ override CXXFLAGS := \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
BINDIR := ../../bin
|
||||
LIBDIR := ../../lib/libloader
|
||||
LEGACY_BINDIR := ../../bin
|
||||
|
||||
.PHONY: all clean
|
||||
.PHONY: all clean remove-legacy
|
||||
|
||||
all: $(BINDIR)/liblibloader.a
|
||||
all: remove-legacy $(LIBDIR)/liblibloader.a
|
||||
|
||||
$(BINDIR)/liblibloader.a: libloader.cpp
|
||||
@mkdir -p $(BINDIR)
|
||||
remove-legacy:
|
||||
rm -f $(LEGACY_BINDIR)/liblibloader.a
|
||||
|
||||
$(LIBDIR)/liblibloader.a: libloader.cpp
|
||||
@mkdir -p $(LIBDIR)
|
||||
$(CXX) $(CXXFLAGS) -c libloader.cpp -o libloader.o
|
||||
$(AR) rcs $(BINDIR)/liblibloader.a libloader.o
|
||||
$(AR) rcs $(LIBDIR)/liblibloader.a libloader.o
|
||||
rm -f libloader.o
|
||||
|
||||
clean:
|
||||
rm -f $(BINDIR)/liblibloader.a
|
||||
rm -f $(LIBDIR)/liblibloader.a $(LEGACY_BINDIR)/liblibloader.a
|
||||
|
||||
@@ -69,7 +69,7 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/apps/pdfviewer/pdfviewer.elf
|
||||
LOADER_LIB := $(BINDIR)/liblibloader.a
|
||||
LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/apps/spreadsheet/spreadsheet.elf
|
||||
LOADER_LIB := $(BINDIR)/liblibloader.a
|
||||
LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ SRCS := main.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/os/test_dialogs.elf
|
||||
LOADER_LIB := $(BINDIR)/liblibloader.a
|
||||
LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
|
||||
LIBC_LIB := $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
@@ -43,16 +43,16 @@ override LDFLAGS := \
|
||||
-T ../../link.ld
|
||||
|
||||
BINDIR := ../../bin/os
|
||||
LIBDIR := ../../bin
|
||||
LOADER_LIB := ../../lib/libloader/liblibloader.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(BINDIR)/test_dl.elf
|
||||
|
||||
$(BINDIR)/test_dl.elf: main.cpp Makefile $(LIBDIR)/liblibloader.a ../../lib/libc/liblibc.a
|
||||
$(BINDIR)/test_dl.elf: main.cpp Makefile $(LOADER_LIB) ../../lib/libc/liblibc.a
|
||||
@mkdir -p $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) -c main.cpp -o main.o
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) main.o $(LIBDIR)/liblibloader.a ../../lib/libc/liblibc.a -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) main.o $(LOADER_LIB) ../../lib/libc/liblibc.a -o $@
|
||||
rm -f main.o
|
||||
|
||||
clean:
|
||||
|
||||
@@ -69,7 +69,7 @@ OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
# ---- Target ----
|
||||
|
||||
TARGET := $(BINDIR)/apps/texteditor/texteditor.elf
|
||||
LOADER_LIB := $(BINDIR)/liblibloader.a
|
||||
LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ LDFLAGS := \
|
||||
|
||||
SRCS := main.cpp document.cpp render.cpp input.cpp print_export.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
LOADER_LIB := $(BINDIR)/liblibloader.a
|
||||
LOADER_LIB := $(LIBDIR)/libloader/liblibloader.a
|
||||
LIBS := $(LOADER_LIB) $(LIBDIR)/tls/libtls.a $(LIBDIR)/bearssl/libbearssl.a $(LIBDIR)/libjpegwrite/libjpegwrite.a $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
TARGET := $(BINDIR)/apps/wordprocessor/wordprocessor.elf
|
||||
|
||||
Reference in New Issue
Block a user