feat: display status message on failed operations in Files app
This commit is contained in:
1 parent
5e4e8902c2
commit
2e3055f5f8
8 files changed
+421
-62
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user