feat: display status message on failed operations in Files app

This commit is contained in:
daniel committed 2026-10-01 08:20:49 +02:00
1 parent 5e4e8902c2
commit 2e3055f5f8
8 files changed
+421 -62

No files matched your search

+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 264
#define MONTAUK_BUILD_NUMBER 265
+1 -1
View File
@@ -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
@@ -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) {
@@ -8,6 +8,7 @@
#include "../apps_common.hpp"
#include <gui/mtk/widgets.hpp>
#include <montauk/statuscodes.h>
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);
@@ -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;
}
}
@@ -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
@@ -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 {
+1
View File
@@ -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)
)