From 2e3055f5f8b205330f267ca0fa6898d0956dc4e5 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Thu, 1 Oct 2026 08:20:49 +0200 Subject: [PATCH] feat: display status message on failed operations in Files app --- kernel/src/Api/BuildNo.hpp | 2 +- programs/include/montauk/statuscodes.h | 2 +- .../src/desktop/apps/filemanager/actions.cpp | 51 ++-- .../apps/filemanager/filemanager_internal.hpp | 32 ++- .../src/desktop/apps/filemanager/fileop.cpp | 96 ++++++-- .../desktop/apps/filemanager/filesystem.cpp | 70 +++--- .../desktop/apps/filemanager/properties.cpp | 229 +++++++++++++++++- scripts/copy_icons.sh | 1 + 8 files changed, 421 insertions(+), 62 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 4c64324..e37eda0 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 264 +#define MONTAUK_BUILD_NUMBER 265 diff --git a/programs/include/montauk/statuscodes.h b/programs/include/montauk/statuscodes.h index e0366d9..74a8f12 100644 --- a/programs/include/montauk/statuscodes.h +++ b/programs/include/montauk/statuscodes.h @@ -62,7 +62,7 @@ inline const char* GetStatusExplanationText(StatusCodes statusCode) { nullptr, // 1 nullptr, // 2 nullptr, // 3 - "The path is protected by the system and can't be modified.", // 4 + "The path is protected by the system and cannot be modified.", // 4 nullptr, // 5 nullptr, // 6 nullptr, // 7 diff --git a/programs/src/desktop/apps/filemanager/actions.cpp b/programs/src/desktop/apps/filemanager/actions.cpp index 0f8fad9..8399e52 100644 --- a/programs/src/desktop/apps/filemanager/actions.cpp +++ b/programs/src/desktop/apps/filemanager/actions.cpp @@ -73,18 +73,28 @@ void filemanager_finish_rename(FileManagerState* fm) { filemanager_build_fullpath(new_path, 512, fm->current_path, fm->rename_buf); - if (montauk::frename(old_path, new_path) != 0) { + montauk::StatusCodes status = + filemanager_status_from_result(montauk::frename(old_path, new_path)); + if (status != montauk::Success && !filemanager_status_is_access_denied(status)) { // Fall back to copy + delete for drivers that don't support rename if (fm->is_dir[fm->rename_idx]) { - if (filemanager_copy_dir_recursive(old_path, new_path)) - filemanager_delete_recursive(old_path); + status = filemanager_copy_dir_recursive(old_path, new_path); + if (status == montauk::Success) + status = filemanager_delete_recursive(old_path); } else { - if (filemanager_copy_file(old_path, new_path)) - montauk::fdelete(old_path); + status = filemanager_copy_file(old_path, new_path); + if (status == montauk::Success) + status = filemanager_status_from_result(montauk::fdelete(old_path)); } } filemanager_read_dir(fm); + + if (status != montauk::Success) { + char heading[192]; + snprintf(heading, sizeof(heading), "Couldn't rename \"%s\"", path_basename(old_path)); + filemanager_show_error(fm->desktop, fm, heading, old_path, status); + } } void filemanager_cancel_rename(FileManagerState* fm) { @@ -98,18 +108,25 @@ void filemanager_new_folder(FileManagerState* fm) { char name[64] = "New Folder"; char path[512]; filemanager_build_fullpath(path, 512, fm->current_path, name); - int attempt = 1; - // Try creating; if it fails, append a number - if (montauk::fmkdir(path) != 0) { - for (attempt = 2; attempt <= 99; attempt++) { - snprintf(name, 64, "New Folder %d", attempt); - filemanager_build_fullpath(path, 512, fm->current_path, name); - if (montauk::fmkdir(path) == 0) break; - } + // Try creating; if it fails (most likely the name is taken), append a + // number. An access error fails the same way for every name, so stop. + montauk::StatusCodes status = filemanager_status_from_result(montauk::fmkdir(path)); + for (int attempt = 2; status != montauk::Success && + !filemanager_status_is_access_denied(status) && + attempt <= 99; attempt++) { + snprintf(name, 64, "New Folder %d", attempt); + filemanager_build_fullpath(path, 512, fm->current_path, name); + status = filemanager_status_from_result(montauk::fmkdir(path)); } filemanager_read_dir(fm); + if (status != montauk::Success) { + filemanager_show_error(fm->desktop, fm, "Couldn't create a new folder", + fm->current_path, status); + return; + } + // Select the new folder and start rename for (int i = 0; i < fm->entry_count; i++) { if (montauk::streq(fm->entry_names[i], name)) { @@ -182,9 +199,15 @@ void filemanager_delete_selected(FileManagerState* fm) { } montauk::mfree(filtered); - montauk::fdelete(fullpath); + montauk::StatusCodes status = filemanager_status_from_result(montauk::fdelete(fullpath)); filemanager_clear_multi_selection(fm); filemanager_read_dir(fm); + + if (status != montauk::Success) { + char heading[192]; + snprintf(heading, sizeof(heading), "Couldn't delete \"%s\"", path_basename(fullpath)); + filemanager_show_error(fm->desktop, fm, heading, fullpath, status); + } } void filemanager_open_ctx_menu(FileManagerState* fm, int local_x, int local_y, int target_idx) { diff --git a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp index 43ddf72..17e9c30 100644 --- a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp +++ b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp @@ -8,6 +8,7 @@ #include "../apps_common.hpp" #include +#include namespace filemanager { @@ -230,9 +231,27 @@ void filemanager_read_virtual_view(FileManagerState* fm, FileManagerVirtualViewK void filemanager_read_apps(FileManagerState* fm); void filemanager_read_system_configuration(FileManagerState* fm); -bool filemanager_delete_recursive(const char* path); -bool filemanager_copy_file(const char* src, const char* dst); -bool filemanager_copy_dir_recursive(const char* src, const char* dst); +// Map a raw syscall result (kernel StatusCodes are negative) to a userspace +// montauk::StatusCodes. Non-negative results (handles, counts) are Success. +inline montauk::StatusCodes filemanager_status_from_result(int rc) { + if (rc >= 0) return montauk::Success; + int code = -rc; + if (code > montauk::OutOfMemory) return montauk::GenericFailure; + return (montauk::StatusCodes)code; +} + +// The path itself may not be modified. Falling back to copy + delete after a +// failed rename cannot help here and would leave a duplicate behind. +inline bool filemanager_status_is_access_denied(montauk::StatusCodes status) { + return status == montauk::SystemIntegrityError || + status == montauk::PermissionDenied || + status == montauk::CapabilityDenied; +} + +// These return the first failure encountered (Success when everything worked). +montauk::StatusCodes filemanager_delete_recursive(const char* path); +montauk::StatusCodes filemanager_copy_file(const char* src, const char* dst); +montauk::StatusCodes filemanager_copy_dir_recursive(const char* src, const char* dst); void filemanager_do_copy(FileManagerState* fm); void filemanager_do_cut(FileManagerState* fm); @@ -254,6 +273,13 @@ void filemanager_show_multi_properties(FileManagerState* fm, void filemanager_show_directory_delete_confirmation(FileManagerState* fm, int target_idx); void filemanager_show_bulk_delete_confirmation(FileManagerState* fm, const int* indices, int count); +// Graphical error popup for a failed file operation. `heading` says what +// failed, `path` (optional) names the item, and a non-Success `status` adds +// montauk::GetStatusExplanationText() as the cause. `owner_app_data` is only +// used to center the popup and may be null or stale. +void filemanager_show_error(DesktopState* ds, const void* owner_app_data, + const char* heading, const char* path, + montauk::StatusCodes status); void filemanager_push_history(FileManagerState* fm); void filemanager_navigate(FileManagerState* fm, const char* name); diff --git a/programs/src/desktop/apps/filemanager/fileop.cpp b/programs/src/desktop/apps/filemanager/fileop.cpp index 4d8a441..71b8848 100644 --- a/programs/src/desktop/apps/filemanager/fileop.cpp +++ b/programs/src/desktop/apps/filemanager/fileop.cpp @@ -33,6 +33,12 @@ struct FileOpJob { volatile int finished; // worker -> UI volatile char current[128]; // basename of the item in progress (worker writes) + // Failures, written by the worker and read on the UI thread only after + // the worker has finished (joined), so they need no synchronization. + int failed_count; + montauk::StatusCodes first_error; + char first_error_path[256]; + int tid; int last_drawn_done; // dialog redraw tracking int mouse_x, mouse_y; @@ -65,6 +71,42 @@ static bool fileop_path_is_dir(const char* path) { return montauk::readdir(path, probe, 1) >= 0; } +static const char* fileop_error_verb(int kind) { + switch (kind) { + case FILE_OP_COPY: return "copy"; + case FILE_OP_MOVE: return "move"; + case FILE_OP_DELETE: return "delete"; + default: return "process"; + } +} + +static void fileop_record_status(FileOpJob* job, const char* path, + montauk::StatusCodes status) { + if (status == montauk::Success) return; + if (job->failed_count++ == 0) { + job->first_error = status; + montauk::strncpy(job->first_error_path, path, sizeof(job->first_error_path) - 1); + job->first_error_path[sizeof(job->first_error_path) - 1] = '\0'; + } +} + +// Show an error popup for a finished job that had failures. UI thread only, +// after the worker has been joined. +static void fileop_report_errors(FileOpJob* job) { + if (!job || job->failed_count <= 0) return; + + char heading[192]; + if (job->failed_count == 1) { + snprintf(heading, sizeof(heading), "Couldn't %s \"%s\"", + fileop_error_verb(job->kind), path_basename(job->first_error_path)); + } else { + snprintf(heading, sizeof(heading), "Couldn't %s %d of %d items", + fileop_error_verb(job->kind), job->failed_count, job->count); + } + filemanager_show_error(job->desktop, job->owner, heading, + job->first_error_path, job->first_error); +} + // ============================================================================ // Worker thread // ============================================================================ @@ -78,7 +120,7 @@ static int fileop_worker(void* arg) { fileop_set_current(job, base); if (job->kind == FILE_OP_DELETE) { - filemanager_delete_recursive(src); + fileop_record_status(job, src, filemanager_delete_recursive(src)); job->files_done = i + 1; continue; } @@ -90,24 +132,31 @@ static int fileop_worker(void* arg) { bool is_dir = fileop_path_is_dir(src); if (is_dir && fileop_path_same_or_descendant(dst, src)) { + // Refuse to copy a directory into itself. + fileop_record_status(job, src, montauk::InvalidArgument); job->files_done = i + 1; - continue; // refuse to copy a directory into itself + continue; } + montauk::StatusCodes status; if (job->kind == FILE_OP_MOVE) { // Try a rename first: an instant same-volume move with no copying. - if (montauk::frename(src, dst) == 0) { job->files_done = i + 1; continue; } - // Cross-volume (or driver without rename): fall back to copy + delete. - bool ok = is_dir ? filemanager_copy_dir_recursive(src, dst) - : filemanager_copy_file(src, dst); - if (ok) { - if (is_dir) filemanager_delete_recursive(src); - else montauk::fdelete(src); + status = filemanager_status_from_result(montauk::frename(src, dst)); + if (status != montauk::Success && !filemanager_status_is_access_denied(status)) { + // Cross-volume (or driver without rename): fall back to copy + delete. + status = is_dir ? filemanager_copy_dir_recursive(src, dst) + : filemanager_copy_file(src, dst); + if (status == montauk::Success) { + status = is_dir + ? filemanager_delete_recursive(src) + : filemanager_status_from_result(montauk::fdelete(src)); + } } } else { // FILE_OP_COPY - if (is_dir) filemanager_copy_dir_recursive(src, dst); - else filemanager_copy_file(src, dst); + status = is_dir ? filemanager_copy_dir_recursive(src, dst) + : filemanager_copy_file(src, dst); } + fileop_record_status(job, src, status); job->files_done = i + 1; } @@ -278,6 +327,10 @@ static void fileop_dialog_on_close(Window* win) { filemanager_read_dir(owner); } + // Opening the popup here is safe: desktop_close_window() only shifts the + // window array after on_close returns, and the new window moves with it. + fileop_report_errors(job); + if (job->paths) montauk::mfree(job->paths); montauk::mfree(job); win->app_data = nullptr; @@ -332,6 +385,7 @@ static bool fileop_launch(FileOpJob* job) { filemanager_clear_multi_selection(owner); filemanager_read_dir(owner); } + fileop_report_errors(job); if (job->paths) montauk::mfree(job->paths); montauk::mfree(job); return true; @@ -348,6 +402,7 @@ static bool fileop_launch(FileOpJob* job) { filemanager_clear_multi_selection(owner); filemanager_read_dir(owner); } + fileop_report_errors(job); if (job->paths) montauk::mfree(job->paths); montauk::mfree(job); return true; @@ -389,17 +444,30 @@ bool filemanager_start_paste_job(FileManagerState* fm) { if (sz <= FILEOP_INLINE_MAX_BYTES) { char dst[512]; filemanager_build_fullpath(dst, 512, fm->current_path, path_basename(src)); + montauk::StatusCodes status = montauk::Success; if (!montauk::streq(dst, src)) { if (is_cut) { - if (montauk::frename(src, dst) != 0) { - if (filemanager_copy_file(src, dst)) montauk::fdelete(src); + status = filemanager_status_from_result(montauk::frename(src, dst)); + if (status != montauk::Success && + !filemanager_status_is_access_denied(status)) { + status = filemanager_copy_file(src, dst); + if (status == montauk::Success) + status = filemanager_status_from_result(montauk::fdelete(src)); } } else { - filemanager_copy_file(src, dst); + status = filemanager_copy_file(src, dst); } } if (is_cut) fm->clipboard_count = 0; filemanager_read_dir(fm); + + if (status != montauk::Success) { + char heading[192]; + snprintf(heading, sizeof(heading), "Couldn't %s \"%s\"", + fileop_error_verb(is_cut ? FILE_OP_MOVE : FILE_OP_COPY), + path_basename(src)); + filemanager_show_error(fm->desktop, fm, heading, src, status); + } return true; } } diff --git a/programs/src/desktop/apps/filemanager/filesystem.cpp b/programs/src/desktop/apps/filemanager/filesystem.cpp index ae31e0e..a819b74 100644 --- a/programs/src/desktop/apps/filemanager/filesystem.cpp +++ b/programs/src/desktop/apps/filemanager/filesystem.cpp @@ -427,7 +427,8 @@ void filemanager_read_system_configuration(FileManagerState* fm) { // (each up to 255 chars, directory entries keep their trailing '/'). Pages the // directory fully so there is no fixed entry cap. Returns the child count and // stores the malloc'd buffer in *out (caller frees with montauk::mfree); the -// buffer is null when the count is 0. Returns -1 if the path is not readable. +// buffer is null when the count is 0. Returns the (negative) readdir status if +// the path is not readable. static int filemanager_read_all_children(const char* dir, char (**out)[256]) { *out = nullptr; @@ -459,7 +460,7 @@ static int filemanager_read_all_children(const char* dir, char (**out)[256]) { for (;;) { int got = montauk::readdir_at(dir, names, BATCH, count); - if (got < 0) { if (!any) return -1; break; } + if (got < 0) { if (!any) return got; break; } any = true; if (got == 0) break; @@ -494,17 +495,19 @@ static int filemanager_read_all_children(const char* dir, char (**out)[256]) { return count; } -bool filemanager_delete_recursive(const char* path) { +montauk::StatusCodes filemanager_delete_recursive(const char* path) { // Try deleting as a file (or empty directory) first - if (montauk::fdelete(path) == 0) return true; + montauk::StatusCodes status = filemanager_status_from_result(montauk::fdelete(path)); + if (status == montauk::Success) return status; // If that failed, it may be a non-empty directory -- snapshot all children // first (the directory shrinks as we delete, so we cannot page while // deleting), then delete each. char (*children)[256] = nullptr; int count = filemanager_read_all_children(path, &children); - if (count < 0) return false; + if (count < 0) return status; + montauk::StatusCodes child_status = montauk::Success; for (int i = 0; i < count; i++) { char child[512]; montauk::strcpy(child, path); @@ -517,30 +520,35 @@ bool filemanager_delete_recursive(const char* path) { int clen = montauk::slen(child); if (clen > 0 && child[clen - 1] == '/') child[clen - 1] = '\0'; - filemanager_delete_recursive(child); + montauk::StatusCodes s = filemanager_delete_recursive(child); + if (s != montauk::Success && child_status == montauk::Success) child_status = s; } if (children) montauk::mfree(children); // Now the directory should be empty -- delete it - return montauk::fdelete(path) == 0; + status = filemanager_status_from_result(montauk::fdelete(path)); + // A child that could not be deleted keeps the directory non-empty; its + // status explains the failure better than the directory's own. + if (status != montauk::Success && child_status != montauk::Success) return child_status; + return status; } -bool filemanager_copy_file(const char* src, const char* dst) { - if (!src || !dst || montauk::streq(src, dst)) return false; +montauk::StatusCodes filemanager_copy_file(const char* src, const char* dst) { + if (!src || !dst || montauk::streq(src, dst)) return montauk::InvalidArgument; int sfd = montauk::open(src); - if (sfd < 0) return false; + if (sfd < 0) return filemanager_status_from_result(sfd); uint64_t size = montauk::getsize(sfd); // Create destination int dfd = montauk::fcreate(dst); if (dfd < 0) { montauk::close(sfd); - return false; + return filemanager_status_from_result(dfd); } - bool ok = true; + montauk::StatusCodes status = montauk::Success; if (size > 0) { static constexpr uint64_t COPY_CHUNK = 64 * 1024; uint64_t buf_size = size < COPY_CHUNK ? size : COPY_CHUNK; @@ -549,7 +557,7 @@ bool filemanager_copy_file(const char* src, const char* dst) { montauk::close(sfd); montauk::close(dfd); montauk::fdelete(dst); - return false; + return montauk::OutOfMemory; } uint64_t off = 0; @@ -558,9 +566,15 @@ bool filemanager_copy_file(const char* src, const char* dst) { if (todo64 > buf_size) todo64 = buf_size; int todo = (int)todo64; int got = montauk::read(sfd, buf, off, todo64); - if (got != todo) { ok = false; break; } + if (got != todo) { + status = got < 0 ? filemanager_status_from_result(got) : montauk::IoFailure; + break; + } int wrote = montauk::fwrite(dfd, buf, off, todo64); - if (wrote != todo) { ok = false; break; } + if (wrote != todo) { + status = wrote < 0 ? filemanager_status_from_result(wrote) : montauk::IoFailure; + break; + } off += todo64; } @@ -569,23 +583,23 @@ bool filemanager_copy_file(const char* src, const char* dst) { montauk::close(sfd); montauk::close(dfd); - if (!ok) { - montauk::fdelete(dst); - return false; - } - return true; + if (status != montauk::Success) montauk::fdelete(dst); + return status; } // Recursive directory copy -bool filemanager_copy_dir_recursive(const char* src, const char* dst) { - if (!src || !dst || montauk::streq(src, dst)) return false; +montauk::StatusCodes filemanager_copy_dir_recursive(const char* src, const char* dst) { + if (!src || !dst || montauk::streq(src, dst)) return montauk::InvalidArgument; + // Ignore the result: the destination may already exist (merge). If it + // truly could not be created, the child copies fail and report why. montauk::fmkdir(dst); char (*children)[256] = nullptr; int count = filemanager_read_all_children(src, &children); - if (count < 0) return false; + if (count < 0) return filemanager_status_from_result(count); + montauk::StatusCodes status = montauk::Success; for (int i = 0; i < count; i++) { bool child_is_dir = false; char basename[256]; @@ -601,15 +615,15 @@ bool filemanager_copy_dir_recursive(const char* src, const char* dst) { filemanager_build_fullpath(src_child, 512, src, basename); filemanager_build_fullpath(dst_child, 512, dst, basename); - if (child_is_dir) - filemanager_copy_dir_recursive(src_child, dst_child); - else - filemanager_copy_file(src_child, dst_child); + montauk::StatusCodes s = child_is_dir + ? filemanager_copy_dir_recursive(src_child, dst_child) + : filemanager_copy_file(src_child, dst_child); + if (s != montauk::Success && status == montauk::Success) status = s; } if (children) montauk::mfree(children); - return true; + return status; } } // namespace filemanager diff --git a/programs/src/desktop/apps/filemanager/properties.cpp b/programs/src/desktop/apps/filemanager/properties.cpp index 3bc5cc7..092dc48 100644 --- a/programs/src/desktop/apps/filemanager/properties.cpp +++ b/programs/src/desktop/apps/filemanager/properties.cpp @@ -490,15 +490,34 @@ static void delete_confirm_apply(FileDeleteConfirmDialogState* st) { if (!started) { // Fallback: delete synchronously (e.g. another operation is running). bool any = false; + int failed = 0; + const char* first_failed_path = nullptr; + montauk::StatusCodes first_error = montauk::Success; 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; + montauk::StatusCodes status = filemanager_delete_recursive(p); + if (status == montauk::Success) { + any = true; + } else if (failed++ == 0) { + first_failed_path = p; + first_error = status; + } } if (owner && any) { filemanager_clear_multi_selection(owner); filemanager_read_dir(owner); } + if (failed > 0) { + char heading[192]; + if (failed == 1) + snprintf(heading, sizeof(heading), "Couldn't delete \"%s\"", + path_basename(first_failed_path)); + else + snprintf(heading, sizeof(heading), "Couldn't delete %d of %d items", failed, n); + filemanager_show_error(st->desktop, st->owner, heading, + first_failed_path, first_error); + } } if (owner && owner->delete_confirm_dialog == st) @@ -753,6 +772,214 @@ void filemanager_show_bulk_delete_confirmation(FileManagerState* fm, win->on_close = delete_confirm_on_close; } +// ===== File operation error dialog ===== + +struct FileErrorDialogState { + DesktopState* desktop; + Color accent; + int mouse_x; + int mouse_y; + + char heading[192]; // what failed, e.g. "Couldn't delete "foo"" + char path[256]; // the item involved (optional) + char cause[4][160]; // wrapped GetStatusExplanationText() lines + int cause_lines; + SvgIcon icon; // owned +}; + +static constexpr int kErrorDialogW = 440; +static constexpr int kErrorTextX = 80; +static constexpr int kErrorTop = 22; + +// Greedy word wrap of `src` into at most `max_lines` lines no wider than +// `max_w`. A word that cannot fit on a line of its own, and any text left over +// for the last line, is ellipsized. +static int error_wrap_text(const char* src, char (*lines)[160], int max_lines, int max_w) { + int count = 0; + int pos = 0; + while (src[pos] && count < max_lines) { + while (src[pos] == ' ') pos++; + if (!src[pos]) break; + + if (count == max_lines - 1) { + props_fit_text(src + pos, lines[count++], 160, max_w); + break; + } + + // Extend the line one word at a time while it still fits. + char buf[160]; + int best = -1; + int end = pos; + for (;;) { + while (src[end] && src[end] != ' ') end++; + int n = end - pos; + if (n > 159) break; + montauk::memcpy(buf, src + pos, n); + buf[n] = '\0'; + if (text_width(buf) > max_w) break; + best = end; + if (!src[end]) break; + end++; + } + + if (best < 0) { + // The first word alone is too wide: truncate it and skip past it. + int word_end = pos; + while (src[word_end] && src[word_end] != ' ') word_end++; + int n = word_end - pos; + if (n > 159) n = 159; + montauk::memcpy(buf, src + pos, n); + buf[n] = '\0'; + props_fit_text(buf, lines[count++], 160, max_w); + pos = word_end; + } else { + int n = best - pos; + montauk::memcpy(lines[count], src + pos, n); + lines[count][n] = '\0'; + count++; + pos = best; + } + } + return count; +} + +// Top-down text layout shared by the draw path and the window sizing. +static int error_path_y(int fh) { return kErrorTop + fh + 8; } +static int error_cause_y(const FileErrorDialogState* st, int fh) { + return st->path[0] ? error_path_y(fh) + fh + 12 : error_path_y(fh); +} + +static Rect error_ok_button_rect(int content_w, int content_h) { + return {content_w - 96, content_h - 42, 80, 28}; +} + +static void error_on_draw(Window* win, Framebuffer& fb) { + (void)fb; + FileErrorDialogState* st = (FileErrorDialogState*)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(); + int text_w = c.w - kErrorTextX - pad; + + if (st->icon.pixels) { + c.icon(pad + (48 - st->icon.width) / 2, + kErrorTop + (48 - st->icon.height) / 2, + st->icon); + } else { + c.fill_rounded_rect(pad, kErrorTop, 48, 48, 24, theme.danger); + c.text(pad + (48 - text_width("!")) / 2, kErrorTop + (48 - fh) / 2, "!", + theme.danger_fg); + } + + char fit[192]; + props_fit_text(st->heading, fit, sizeof(fit), text_w); + c.text(kErrorTextX, kErrorTop, fit, theme.text); + + if (st->path[0]) { + props_fit_text(st->path, fit, sizeof(fit), text_w); + c.text(kErrorTextX, error_path_y(fh), fit, theme.text_muted); + } + + int y = error_cause_y(st, fh); + for (int i = 0; i < st->cause_lines; i++) { + c.text(kErrorTextX, y, st->cause[i], theme.text); + y += fh + 6; + } + + Rect ok = error_ok_button_rect(c.w, c.h); + mtk::draw_button(c, ok, "OK", mtk::BUTTON_PRIMARY, + mtk::widget_state(false, ok.contains(st->mouse_x, st->mouse_y), true), + theme); +} + +static void error_on_mouse(Window* win, MouseEvent& ev) { + FileErrorDialogState* st = (FileErrorDialogState*)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 ok = error_ok_button_rect(win->content_w, win->content_h); + bool old_hover = ok.contains(st->mouse_x, st->mouse_y); + bool new_hover = ok.contains(mx, my); + st->mouse_x = mx; + st->mouse_y = my; + if (old_hover != new_hover) host.invalidate(); + + if (ev.left_pressed() && new_hover) + props_close_self(st->desktop, st); +} + +static void error_on_key(Window* win, const montauk::abi::KeyEvent& key) { + FileErrorDialogState* st = (FileErrorDialogState*)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 error_on_close(Window* win) { + FileErrorDialogState* st = (FileErrorDialogState*)win->app_data; + if (!st) return; + if (st->icon.pixels) svg_free(st->icon); + montauk::mfree(st); + win->app_data = nullptr; +} + +void filemanager_show_error(DesktopState* ds, const void* owner_app_data, + const char* heading, const char* path, + montauk::StatusCodes status) { + if (!ds) return; + + FileErrorDialogState* st = + (FileErrorDialogState*)montauk::malloc(sizeof(FileErrorDialogState)); + if (!st) return; + montauk::memset(st, 0, sizeof(FileErrorDialogState)); + st->desktop = ds; + st->accent = ds->settings.accent_color; + props_copy(st->heading, sizeof(st->heading), heading ? heading : "The operation failed"); + props_copy(st->path, sizeof(st->path), path); + + int content_w = kErrorDialogW - 2 * gui::BORDER_WIDTH; + if (status != montauk::Success) { + st->cause_lines = error_wrap_text(montauk::GetStatusExplanationText(status), + st->cause, 4, content_w - kErrorTextX - 16); + } + + st->icon = svg_load("0:/os/icons/dialog-error.svg", 48, 48, colors::ICON_COLOR); + + int fh = system_font_height(); + int text_bottom = error_cause_y(st, fh) + st->cause_lines * (fh + 6); + int body_bottom = kErrorTop + 48; + if (text_bottom > body_bottom) body_bottom = text_bottom; + int content_h = body_bottom + 18 /*gap*/ + 28 /*button*/ + 14 /*bottom margin*/; + int w = kErrorDialogW; + int h = content_h + gui::TITLEBAR_HEIGHT + gui::BORDER_WIDTH; + int wx = 0; + int wy = 0; + props_center_dialog(ds, owner_app_data, w, h, &wx, &wy); + + int idx = desktop_create_window(ds, "Error", wx, wy, w, h); + if (idx < 0) { + if (st->icon.pixels) svg_free(st->icon); + montauk::mfree(st); + return; + } + + Window* win = &ds->windows[idx]; + win->app_data = st; + win->on_draw = error_on_draw; + win->on_mouse = error_on_mouse; + win->on_key = error_on_key; + win->on_close = error_on_close; +} + // ===== Multi-item Properties dialog ===== struct MultiPropertiesDialogState { diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index 4ec8a74..b47ab9e 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -161,6 +161,7 @@ ICONS=( "categories/scalable/utilities-log-viewer.svg" "apps/scalable/sleep.svg" "apps/scalable/kolourpaint.svg" # paint app + "apps/scalable/dialog-error.svg" # Files error popup "panel/sensors-temperature-symbolic.svg" # temperature panel icon "actions/symbolic/view-paged-symbolic.svg" # page view toggle (word processor) )