From 6b500ec7cba15b2e2903f95f2d86441136715b3e Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Wed, 6 May 2026 17:55:57 +0200 Subject: [PATCH] feat: properties menu in Files and delete confirmation for directories --- .gitignore | 1 + programs/src/desktop/Makefile | 2 +- .../src/desktop/apps/filemanager/actions.cpp | 10 +- .../src/desktop/apps/filemanager/events.cpp | 1 + .../apps/filemanager/filemanager_internal.hpp | 6 + .../src/desktop/apps/filemanager/helpers.cpp | 1 + .../desktop/apps/filemanager/properties.cpp | 645 ++++++++++++++++++ programs/src/libloader/Makefile | 18 +- programs/src/pdfviewer/Makefile | 2 +- programs/src/spreadsheet/Makefile | 2 +- programs/src/test_dialogs/Makefile | 2 +- programs/src/test_dl/Makefile | 6 +- programs/src/texteditor/Makefile | 2 +- programs/src/wordprocessor/Makefile | 2 +- 14 files changed, 681 insertions(+), 19 deletions(-) create mode 100644 programs/src/desktop/apps/filemanager/properties.cpp diff --git a/.gitignore b/.gitignore index b233bfa..4187e92 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ programs/obj/ programs/src/*/obj/ programs/libs/*/obj/ programs/lib/libc/liblibc.a +programs/lib/libloader/liblibloader.a programs/gui/icons/ CLAUDE.md programs/lib/bearssl diff --git a/programs/src/desktop/Makefile b/programs/src/desktop/Makefile index c39ec57..bd95ceb 100644 --- a/programs/src/desktop/Makefile +++ b/programs/src/desktop/Makefile @@ -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 diff --git a/programs/src/desktop/apps/filemanager/actions.cpp b/programs/src/desktop/apps/filemanager/actions.cpp index f10f5a4..6a1d5c6 100644 --- a/programs/src/desktop/apps/filemanager/actions.cpp +++ b/programs/src/desktop/apps/filemanager/actions.cpp @@ -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 diff --git a/programs/src/desktop/apps/filemanager/events.cpp b/programs/src/desktop/apps/filemanager/events.cpp index 451a2e6..95f6ae8 100644 --- a/programs/src/desktop/apps/filemanager/events.cpp +++ b/programs/src/desktop/apps/filemanager/events.cpp @@ -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; } diff --git a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp index b2d6aa4..233c8a7 100644 --- a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp +++ b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp @@ -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); diff --git a/programs/src/desktop/apps/filemanager/helpers.cpp b/programs/src/desktop/apps/filemanager/helpers.cpp index b28992a..b3a6121 100644 --- a/programs/src/desktop/apps/filemanager/helpers.cpp +++ b/programs/src/desktop/apps/filemanager/helpers.cpp @@ -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 "?"; } } diff --git a/programs/src/desktop/apps/filemanager/properties.cpp b/programs/src/desktop/apps/filemanager/properties.cpp new file mode 100644 index 0000000..462a117 --- /dev/null +++ b/programs/src/desktop/apps/filemanager/properties.cpp @@ -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 + +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 diff --git a/programs/src/libloader/Makefile b/programs/src/libloader/Makefile index 8a91f1f..defcd5d 100644 --- a/programs/src/libloader/Makefile +++ b/programs/src/libloader/Makefile @@ -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 diff --git a/programs/src/pdfviewer/Makefile b/programs/src/pdfviewer/Makefile index 65ecd3c..33ded1a 100644 --- a/programs/src/pdfviewer/Makefile +++ b/programs/src/pdfviewer/Makefile @@ -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 diff --git a/programs/src/spreadsheet/Makefile b/programs/src/spreadsheet/Makefile index 2563db0..ae10f8f 100644 --- a/programs/src/spreadsheet/Makefile +++ b/programs/src/spreadsheet/Makefile @@ -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 diff --git a/programs/src/test_dialogs/Makefile b/programs/src/test_dialogs/Makefile index e16f693..94c6572 100644 --- a/programs/src/test_dialogs/Makefile +++ b/programs/src/test_dialogs/Makefile @@ -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 diff --git a/programs/src/test_dl/Makefile b/programs/src/test_dl/Makefile index 6bbf85f..e0106ac 100644 --- a/programs/src/test_dl/Makefile +++ b/programs/src/test_dl/Makefile @@ -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: diff --git a/programs/src/texteditor/Makefile b/programs/src/texteditor/Makefile index 26ad8d4..8976ae7 100644 --- a/programs/src/texteditor/Makefile +++ b/programs/src/texteditor/Makefile @@ -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 diff --git a/programs/src/wordprocessor/Makefile b/programs/src/wordprocessor/Makefile index 6776382..d5f2781 100644 --- a/programs/src/wordprocessor/Makefile +++ b/programs/src/wordprocessor/Makefile @@ -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