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
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 264
|
#define MONTAUK_BUILD_NUMBER 265
|
||||||
@@ -62,7 +62,7 @@ inline const char* GetStatusExplanationText(StatusCodes statusCode) {
|
|||||||
nullptr, // 1
|
nullptr, // 1
|
||||||
nullptr, // 2
|
nullptr, // 2
|
||||||
nullptr, // 3
|
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, // 5
|
||||||
nullptr, // 6
|
nullptr, // 6
|
||||||
nullptr, // 7
|
nullptr, // 7
|
||||||
|
|||||||
@@ -73,18 +73,28 @@ void filemanager_finish_rename(FileManagerState* fm) {
|
|||||||
filemanager_build_fullpath(new_path, 512,
|
filemanager_build_fullpath(new_path, 512,
|
||||||
fm->current_path, fm->rename_buf);
|
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
|
// Fall back to copy + delete for drivers that don't support rename
|
||||||
if (fm->is_dir[fm->rename_idx]) {
|
if (fm->is_dir[fm->rename_idx]) {
|
||||||
if (filemanager_copy_dir_recursive(old_path, new_path))
|
status = filemanager_copy_dir_recursive(old_path, new_path);
|
||||||
filemanager_delete_recursive(old_path);
|
if (status == montauk::Success)
|
||||||
|
status = filemanager_delete_recursive(old_path);
|
||||||
} else {
|
} else {
|
||||||
if (filemanager_copy_file(old_path, new_path))
|
status = filemanager_copy_file(old_path, new_path);
|
||||||
montauk::fdelete(old_path);
|
if (status == montauk::Success)
|
||||||
|
status = filemanager_status_from_result(montauk::fdelete(old_path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filemanager_read_dir(fm);
|
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) {
|
void filemanager_cancel_rename(FileManagerState* fm) {
|
||||||
@@ -98,18 +108,25 @@ void filemanager_new_folder(FileManagerState* fm) {
|
|||||||
char name[64] = "New Folder";
|
char name[64] = "New Folder";
|
||||||
char path[512];
|
char path[512];
|
||||||
filemanager_build_fullpath(path, 512, fm->current_path, name);
|
filemanager_build_fullpath(path, 512, fm->current_path, name);
|
||||||
int attempt = 1;
|
// Try creating; if it fails (most likely the name is taken), append a
|
||||||
// Try creating; if it fails, append a number
|
// number. An access error fails the same way for every name, so stop.
|
||||||
if (montauk::fmkdir(path) != 0) {
|
montauk::StatusCodes status = filemanager_status_from_result(montauk::fmkdir(path));
|
||||||
for (attempt = 2; attempt <= 99; attempt++) {
|
for (int attempt = 2; status != montauk::Success &&
|
||||||
snprintf(name, 64, "New Folder %d", attempt);
|
!filemanager_status_is_access_denied(status) &&
|
||||||
filemanager_build_fullpath(path, 512, fm->current_path, name);
|
attempt <= 99; attempt++) {
|
||||||
if (montauk::fmkdir(path) == 0) break;
|
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);
|
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
|
// Select the new folder and start rename
|
||||||
for (int i = 0; i < fm->entry_count; i++) {
|
for (int i = 0; i < fm->entry_count; i++) {
|
||||||
if (montauk::streq(fm->entry_names[i], name)) {
|
if (montauk::streq(fm->entry_names[i], name)) {
|
||||||
@@ -182,9 +199,15 @@ void filemanager_delete_selected(FileManagerState* fm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
montauk::mfree(filtered);
|
montauk::mfree(filtered);
|
||||||
montauk::fdelete(fullpath);
|
montauk::StatusCodes status = filemanager_status_from_result(montauk::fdelete(fullpath));
|
||||||
filemanager_clear_multi_selection(fm);
|
filemanager_clear_multi_selection(fm);
|
||||||
filemanager_read_dir(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) {
|
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 "../apps_common.hpp"
|
||||||
#include <gui/mtk/widgets.hpp>
|
#include <gui/mtk/widgets.hpp>
|
||||||
|
#include <montauk/statuscodes.h>
|
||||||
|
|
||||||
namespace filemanager {
|
namespace filemanager {
|
||||||
|
|
||||||
@@ -230,9 +231,27 @@ void filemanager_read_virtual_view(FileManagerState* fm, FileManagerVirtualViewK
|
|||||||
void filemanager_read_apps(FileManagerState* fm);
|
void filemanager_read_apps(FileManagerState* fm);
|
||||||
void filemanager_read_system_configuration(FileManagerState* fm);
|
void filemanager_read_system_configuration(FileManagerState* fm);
|
||||||
|
|
||||||
bool filemanager_delete_recursive(const char* path);
|
// Map a raw syscall result (kernel StatusCodes are negative) to a userspace
|
||||||
bool filemanager_copy_file(const char* src, const char* dst);
|
// montauk::StatusCodes. Non-negative results (handles, counts) are Success.
|
||||||
bool filemanager_copy_dir_recursive(const char* src, const char* dst);
|
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_copy(FileManagerState* fm);
|
||||||
void filemanager_do_cut(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_directory_delete_confirmation(FileManagerState* fm, int target_idx);
|
||||||
void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
|
void filemanager_show_bulk_delete_confirmation(FileManagerState* fm,
|
||||||
const int* indices, int count);
|
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_push_history(FileManagerState* fm);
|
||||||
void filemanager_navigate(FileManagerState* fm, const char* name);
|
void filemanager_navigate(FileManagerState* fm, const char* name);
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ struct FileOpJob {
|
|||||||
volatile int finished; // worker -> UI
|
volatile int finished; // worker -> UI
|
||||||
volatile char current[128]; // basename of the item in progress (worker writes)
|
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 tid;
|
||||||
int last_drawn_done; // dialog redraw tracking
|
int last_drawn_done; // dialog redraw tracking
|
||||||
int mouse_x, mouse_y;
|
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;
|
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
|
// Worker thread
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -78,7 +120,7 @@ static int fileop_worker(void* arg) {
|
|||||||
fileop_set_current(job, base);
|
fileop_set_current(job, base);
|
||||||
|
|
||||||
if (job->kind == FILE_OP_DELETE) {
|
if (job->kind == FILE_OP_DELETE) {
|
||||||
filemanager_delete_recursive(src);
|
fileop_record_status(job, src, filemanager_delete_recursive(src));
|
||||||
job->files_done = i + 1;
|
job->files_done = i + 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -90,24 +132,31 @@ static int fileop_worker(void* arg) {
|
|||||||
|
|
||||||
bool is_dir = fileop_path_is_dir(src);
|
bool is_dir = fileop_path_is_dir(src);
|
||||||
if (is_dir && fileop_path_same_or_descendant(dst, 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;
|
job->files_done = i + 1;
|
||||||
continue; // refuse to copy a directory into itself
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
montauk::StatusCodes status;
|
||||||
if (job->kind == FILE_OP_MOVE) {
|
if (job->kind == FILE_OP_MOVE) {
|
||||||
// Try a rename first: an instant same-volume move with no copying.
|
// Try a rename first: an instant same-volume move with no copying.
|
||||||
if (montauk::frename(src, dst) == 0) { job->files_done = i + 1; continue; }
|
status = filemanager_status_from_result(montauk::frename(src, dst));
|
||||||
// Cross-volume (or driver without rename): fall back to copy + delete.
|
if (status != montauk::Success && !filemanager_status_is_access_denied(status)) {
|
||||||
bool ok = is_dir ? filemanager_copy_dir_recursive(src, dst)
|
// Cross-volume (or driver without rename): fall back to copy + delete.
|
||||||
: filemanager_copy_file(src, dst);
|
status = is_dir ? filemanager_copy_dir_recursive(src, dst)
|
||||||
if (ok) {
|
: filemanager_copy_file(src, dst);
|
||||||
if (is_dir) filemanager_delete_recursive(src);
|
if (status == montauk::Success) {
|
||||||
else montauk::fdelete(src);
|
status = is_dir
|
||||||
|
? filemanager_delete_recursive(src)
|
||||||
|
: filemanager_status_from_result(montauk::fdelete(src));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else { // FILE_OP_COPY
|
} else { // FILE_OP_COPY
|
||||||
if (is_dir) filemanager_copy_dir_recursive(src, dst);
|
status = is_dir ? filemanager_copy_dir_recursive(src, dst)
|
||||||
else filemanager_copy_file(src, dst);
|
: filemanager_copy_file(src, dst);
|
||||||
}
|
}
|
||||||
|
fileop_record_status(job, src, status);
|
||||||
job->files_done = i + 1;
|
job->files_done = i + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +327,10 @@ static void fileop_dialog_on_close(Window* win) {
|
|||||||
filemanager_read_dir(owner);
|
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);
|
if (job->paths) montauk::mfree(job->paths);
|
||||||
montauk::mfree(job);
|
montauk::mfree(job);
|
||||||
win->app_data = nullptr;
|
win->app_data = nullptr;
|
||||||
@@ -332,6 +385,7 @@ static bool fileop_launch(FileOpJob* job) {
|
|||||||
filemanager_clear_multi_selection(owner);
|
filemanager_clear_multi_selection(owner);
|
||||||
filemanager_read_dir(owner);
|
filemanager_read_dir(owner);
|
||||||
}
|
}
|
||||||
|
fileop_report_errors(job);
|
||||||
if (job->paths) montauk::mfree(job->paths);
|
if (job->paths) montauk::mfree(job->paths);
|
||||||
montauk::mfree(job);
|
montauk::mfree(job);
|
||||||
return true;
|
return true;
|
||||||
@@ -348,6 +402,7 @@ static bool fileop_launch(FileOpJob* job) {
|
|||||||
filemanager_clear_multi_selection(owner);
|
filemanager_clear_multi_selection(owner);
|
||||||
filemanager_read_dir(owner);
|
filemanager_read_dir(owner);
|
||||||
}
|
}
|
||||||
|
fileop_report_errors(job);
|
||||||
if (job->paths) montauk::mfree(job->paths);
|
if (job->paths) montauk::mfree(job->paths);
|
||||||
montauk::mfree(job);
|
montauk::mfree(job);
|
||||||
return true;
|
return true;
|
||||||
@@ -389,17 +444,30 @@ bool filemanager_start_paste_job(FileManagerState* fm) {
|
|||||||
if (sz <= FILEOP_INLINE_MAX_BYTES) {
|
if (sz <= FILEOP_INLINE_MAX_BYTES) {
|
||||||
char dst[512];
|
char dst[512];
|
||||||
filemanager_build_fullpath(dst, 512, fm->current_path, path_basename(src));
|
filemanager_build_fullpath(dst, 512, fm->current_path, path_basename(src));
|
||||||
|
montauk::StatusCodes status = montauk::Success;
|
||||||
if (!montauk::streq(dst, src)) {
|
if (!montauk::streq(dst, src)) {
|
||||||
if (is_cut) {
|
if (is_cut) {
|
||||||
if (montauk::frename(src, dst) != 0) {
|
status = filemanager_status_from_result(montauk::frename(src, dst));
|
||||||
if (filemanager_copy_file(src, dst)) montauk::fdelete(src);
|
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 {
|
} else {
|
||||||
filemanager_copy_file(src, dst);
|
status = filemanager_copy_file(src, dst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (is_cut) fm->clipboard_count = 0;
|
if (is_cut) fm->clipboard_count = 0;
|
||||||
filemanager_read_dir(fm);
|
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;
|
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
|
// (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
|
// 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
|
// 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]) {
|
static int filemanager_read_all_children(const char* dir, char (**out)[256]) {
|
||||||
*out = nullptr;
|
*out = nullptr;
|
||||||
|
|
||||||
@@ -459,7 +460,7 @@ static int filemanager_read_all_children(const char* dir, char (**out)[256]) {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int got = montauk::readdir_at(dir, names, BATCH, count);
|
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;
|
any = true;
|
||||||
if (got == 0) break;
|
if (got == 0) break;
|
||||||
|
|
||||||
@@ -494,17 +495,19 @@ static int filemanager_read_all_children(const char* dir, char (**out)[256]) {
|
|||||||
return count;
|
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
|
// 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
|
// 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
|
// first (the directory shrinks as we delete, so we cannot page while
|
||||||
// deleting), then delete each.
|
// deleting), then delete each.
|
||||||
char (*children)[256] = nullptr;
|
char (*children)[256] = nullptr;
|
||||||
int count = filemanager_read_all_children(path, &children);
|
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++) {
|
for (int i = 0; i < count; i++) {
|
||||||
char child[512];
|
char child[512];
|
||||||
montauk::strcpy(child, path);
|
montauk::strcpy(child, path);
|
||||||
@@ -517,30 +520,35 @@ bool filemanager_delete_recursive(const char* path) {
|
|||||||
int clen = montauk::slen(child);
|
int clen = montauk::slen(child);
|
||||||
if (clen > 0 && child[clen - 1] == '/') child[clen - 1] = '\0';
|
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);
|
if (children) montauk::mfree(children);
|
||||||
|
|
||||||
// Now the directory should be empty -- delete it
|
// 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) {
|
montauk::StatusCodes filemanager_copy_file(const char* src, const char* dst) {
|
||||||
if (!src || !dst || montauk::streq(src, dst)) return false;
|
if (!src || !dst || montauk::streq(src, dst)) return montauk::InvalidArgument;
|
||||||
|
|
||||||
int sfd = montauk::open(src);
|
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);
|
uint64_t size = montauk::getsize(sfd);
|
||||||
|
|
||||||
// Create destination
|
// Create destination
|
||||||
int dfd = montauk::fcreate(dst);
|
int dfd = montauk::fcreate(dst);
|
||||||
if (dfd < 0) {
|
if (dfd < 0) {
|
||||||
montauk::close(sfd);
|
montauk::close(sfd);
|
||||||
return false;
|
return filemanager_status_from_result(dfd);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = true;
|
montauk::StatusCodes status = montauk::Success;
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
static constexpr uint64_t COPY_CHUNK = 64 * 1024;
|
static constexpr uint64_t COPY_CHUNK = 64 * 1024;
|
||||||
uint64_t buf_size = size < COPY_CHUNK ? size : COPY_CHUNK;
|
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(sfd);
|
||||||
montauk::close(dfd);
|
montauk::close(dfd);
|
||||||
montauk::fdelete(dst);
|
montauk::fdelete(dst);
|
||||||
return false;
|
return montauk::OutOfMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t off = 0;
|
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;
|
if (todo64 > buf_size) todo64 = buf_size;
|
||||||
int todo = (int)todo64;
|
int todo = (int)todo64;
|
||||||
int got = montauk::read(sfd, buf, off, 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);
|
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;
|
off += todo64;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,23 +583,23 @@ bool filemanager_copy_file(const char* src, const char* dst) {
|
|||||||
|
|
||||||
montauk::close(sfd);
|
montauk::close(sfd);
|
||||||
montauk::close(dfd);
|
montauk::close(dfd);
|
||||||
if (!ok) {
|
if (status != montauk::Success) montauk::fdelete(dst);
|
||||||
montauk::fdelete(dst);
|
return status;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursive directory copy
|
// Recursive directory copy
|
||||||
bool filemanager_copy_dir_recursive(const char* src, const char* dst) {
|
montauk::StatusCodes filemanager_copy_dir_recursive(const char* src, const char* dst) {
|
||||||
if (!src || !dst || montauk::streq(src, dst)) return false;
|
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);
|
montauk::fmkdir(dst);
|
||||||
|
|
||||||
char (*children)[256] = nullptr;
|
char (*children)[256] = nullptr;
|
||||||
int count = filemanager_read_all_children(src, &children);
|
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++) {
|
for (int i = 0; i < count; i++) {
|
||||||
bool child_is_dir = false;
|
bool child_is_dir = false;
|
||||||
char basename[256];
|
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(src_child, 512, src, basename);
|
||||||
filemanager_build_fullpath(dst_child, 512, dst, basename);
|
filemanager_build_fullpath(dst_child, 512, dst, basename);
|
||||||
|
|
||||||
if (child_is_dir)
|
montauk::StatusCodes s = child_is_dir
|
||||||
filemanager_copy_dir_recursive(src_child, dst_child);
|
? filemanager_copy_dir_recursive(src_child, dst_child)
|
||||||
else
|
: filemanager_copy_file(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);
|
if (children) montauk::mfree(children);
|
||||||
|
|
||||||
return true;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace filemanager
|
} // namespace filemanager
|
||||||
@@ -490,15 +490,34 @@ static void delete_confirm_apply(FileDeleteConfirmDialogState* st) {
|
|||||||
if (!started) {
|
if (!started) {
|
||||||
// Fallback: delete synchronously (e.g. another operation is running).
|
// Fallback: delete synchronously (e.g. another operation is running).
|
||||||
bool any = false;
|
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++) {
|
for (int i = 0; i < n; i++) {
|
||||||
const char* p = (st->count > 0) ? st->paths[i] : st->path;
|
const char* p = (st->count > 0) ? st->paths[i] : st->path;
|
||||||
if (!p || !p[0]) continue;
|
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) {
|
if (owner && any) {
|
||||||
filemanager_clear_multi_selection(owner);
|
filemanager_clear_multi_selection(owner);
|
||||||
filemanager_read_dir(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)
|
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;
|
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 =====
|
// ===== Multi-item Properties dialog =====
|
||||||
|
|
||||||
struct MultiPropertiesDialogState {
|
struct MultiPropertiesDialogState {
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ ICONS=(
|
|||||||
"categories/scalable/utilities-log-viewer.svg"
|
"categories/scalable/utilities-log-viewer.svg"
|
||||||
"apps/scalable/sleep.svg"
|
"apps/scalable/sleep.svg"
|
||||||
"apps/scalable/kolourpaint.svg" # paint app
|
"apps/scalable/kolourpaint.svg" # paint app
|
||||||
|
"apps/scalable/dialog-error.svg" # Files error popup
|
||||||
"panel/sensors-temperature-symbolic.svg" # temperature panel icon
|
"panel/sensors-temperature-symbolic.svg" # temperature panel icon
|
||||||
"actions/symbolic/view-paged-symbolic.svg" # page view toggle (word processor)
|
"actions/symbolic/view-paged-symbolic.svg" # page view toggle (word processor)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user