diff --git a/apps/file_manager/commands.cpp b/apps/file_manager/commands.cpp index 93670dc..ce8dd96 100644 --- a/apps/file_manager/commands.cpp +++ b/apps/file_manager/commands.cpp @@ -164,7 +164,60 @@ void CommandCopy(Instance *instance, EsElement *, EsCommand *) { CommandCopyOrCut(instance, ES_FLAGS_DEFAULT); } -EsError CommandPasteFile(String source, String destinationBase, void **copyBuffer, bool move, String *_destination) { +struct PasteOperation { + String source, destination; +}; + +struct PasteTask { + // Input: + String destinationBase; + bool move; + char *pathList; + size_t pathListBytes; + + // State: + bool progressByData; + EsFileOffset totalDataToProcess, totalDataProcessed; + size_t sourceItemCount, sourceItemsProcessed; + EsUserTask *userTask; + EsFileOffset lastBytesCopied, cumulativeSecondBytesCopied; + EsFileOffsetDifference bytesPerSecond; + double cumulativeSecondTimeStampMs; +}; + +bool CommandPasteCopyCallback(EsFileOffset bytesCopied, EsFileOffset totalBytes, EsGeneric data) { + (void) totalBytes; + + PasteTask *task = (PasteTask *) data.p; + EsFileOffset delta = bytesCopied - task->lastBytesCopied; + task->totalDataProcessed += delta; + task->lastBytesCopied = bytesCopied; + + if (task->progressByData) { + double timeStampMs = EsTimeStampMs(); + + if (!task->cumulativeSecondTimeStampMs) { + task->cumulativeSecondTimeStampMs = timeStampMs; + task->bytesPerSecond = -1; + } else if (timeStampMs - task->cumulativeSecondTimeStampMs > 1000.0) { + // TODO Test that this calculation is correct. + task->bytesPerSecond = task->cumulativeSecondBytesCopied / (timeStampMs - task->cumulativeSecondTimeStampMs); + task->cumulativeSecondTimeStampMs = timeStampMs; + task->cumulativeSecondBytesCopied = 0; + } + + task->cumulativeSecondBytesCopied += delta; + EsUserTaskSetProgress(task->userTask, (double) task->totalDataProcessed / task->totalDataToProcess, task->bytesPerSecond); + } + + return EsUserTaskIsRunning(task->userTask); +} + +EsError CommandPasteFile(String source, String destinationBase, void **copyBuffer, PasteTask *task, String *_destination) { + if (!EsUserTaskIsRunning(task->userTask)) { + return ES_ERROR_CANCELLED; + } + if (PathHasPrefix(destinationBase, source)) { return ES_ERROR_TARGET_WITHIN_SOURCE; } @@ -174,7 +227,7 @@ EsError CommandPasteFile(String source, String destinationBase, void **copyBuffe EsError error; if (StringEquals(PathGetParent(source), destinationBase)) { - if (move) { + if (task->move) { // Move with the source and destination folders identical; meaningless. error = ES_SUCCESS; goto done; @@ -193,17 +246,19 @@ EsError CommandPasteFile(String source, String destinationBase, void **copyBuffe } } - EsPrint("%z %s -> %s...\n", move ? "Moving" : "Copying", STRFMT(source), STRFMT(destination)); + EsPrint("%z %s -> %s...\n", task->move ? "Moving" : "Copying", STRFMT(source), STRFMT(destination)); - if (move) { + if (task->move) { error = EsPathMove(STRING(source), STRING(destination), ES_FLAGS_DEFAULT); if (error == ES_ERROR_VOLUME_MISMATCH) { // TODO Delete the files after all copies complete successfully. - error = EsFileCopy(STRING(source), STRING(destination), copyBuffer); + goto copy; } } else { - error = EsFileCopy(STRING(source), STRING(destination), copyBuffer); + copy:; + task->lastBytesCopied = 0; + error = EsFileCopy(STRING(source), STRING(destination), copyBuffer, CommandPasteCopyCallback, task); } if (error == ES_ERROR_INCORRECT_NODE_TYPE) { @@ -219,7 +274,7 @@ EsError CommandPasteFile(String source, String destinationBase, void **copyBuffe for (intptr_t i = 0; i < childCount && error == ES_SUCCESS; i++) { String childSourcePath = StringAllocateAndFormat("%s%z%s", STRFMT(source), PathHasTrailingSlash(source) ? "" : "/", buffer[i].nameBytes, buffer[i].name); - error = CommandPasteFile(childSourcePath, destination, copyBuffer, move, nullptr); + error = CommandPasteFile(childSourcePath, destination, copyBuffer, task, nullptr); StringDestroy(&childSourcePath); } } @@ -230,7 +285,7 @@ EsError CommandPasteFile(String source, String destinationBase, void **copyBuffe if (error == ES_SUCCESS) { EsMessageMutexAcquire(); - if (move) FolderFileUpdatedAtPath(source, nullptr); + if (task->move) FolderFileUpdatedAtPath(source, nullptr); FolderFileUpdatedAtPath(destination, nullptr); EsMessageMutexRelease(); } @@ -246,46 +301,75 @@ EsError CommandPasteFile(String source, String destinationBase, void **copyBuffe return error; } -struct PasteOperation { - String source, destination; -}; - -struct PasteTask { - // Input: - String destinationBase; - bool move; - char *pathList; - size_t pathListBytes; -}; - -void CommandPasteTask(EsGeneric _task) { - // TODO Background task. - // TODO Reporting errors properly. Ask to retry or cancel. - // TODO If the destination file already exists, ask to replace, skip, rename or cancel. +void CommandPasteTask(EsUserTask *userTask, EsGeneric _task) { + // TODO Reporting errors properly. Ask to retry or skip. + // TODO If the destination file already exists, ask to rename or skip (as replace is destructive, it should be an advanced option). // TODO Other namespace handlers. // TODO Undo. PasteTask *task = (PasteTask *) _task.p; Array pasteOperations = {}; EsError error = ES_SUCCESS; + task->userTask = userTask; void *copyBuffer = nullptr; - const char *position = task->pathList; - while (task->pathListBytes) { - const char *newline = (const char *) EsCRTmemchr(position, '\n', task->pathListBytes); + const char *position = task->pathList; + size_t remainingBytes = task->pathListBytes; + + while (remainingBytes && EsUserTaskIsRunning(task->userTask)) { + const char *newline = (const char *) EsCRTmemchr(position, '\n', remainingBytes); + if (!newline) break; + + String source = StringFromLiteralWithSize(position, newline - position); + + if (!task->move || !StringEquals(PathGetDrive(source), PathGetDrive(task->destinationBase))) { + // Files are actually being copied, so report progress by the amount of data copied, + // rather than the amount of files processed. + task->progressByData = true; + } + + EsDirectoryChild information; + + if (EsPathQueryInformation(STRING(source), &information)) { + if (information.fileSize == -1) { + // TODO Support progress on volumes that don't report total directory sizes. + } else { + task->totalDataToProcess += information.fileSize; + } + + task->sourceItemCount++; + } else { + // We will probably error on this file, so ignore it. + } + + position += source.bytes + 1; + remainingBytes -= source.bytes + 1; + } + + position = task->pathList; + remainingBytes = task->pathListBytes; + + while (remainingBytes && EsUserTaskIsRunning(task->userTask)) { + const char *newline = (const char *) EsCRTmemchr(position, '\n', remainingBytes); if (!newline) break; String source = StringFromLiteralWithSize(position, newline - position); String destination; - error = CommandPasteFile(source, task->destinationBase, ©Buffer, task->move, &destination); + error = CommandPasteFile(source, task->destinationBase, ©Buffer, task, &destination); if (error != ES_SUCCESS) break; PasteOperation operation = { .source = StringDuplicate(source), .destination = destination }; pasteOperations.Add(operation); position += source.bytes + 1; - task->pathListBytes -= source.bytes + 1; + remainingBytes -= source.bytes + 1; + + task->sourceItemsProcessed++; + + if (!task->progressByData) { + EsUserTaskSetProgress(userTask, (double) task->sourceItemsProcessed / task->sourceItemCount, -1); + } } EsMessageMutexAcquire(); @@ -357,7 +441,15 @@ void CommandPaste(Instance *instance, EsElement *, EsCommand *) { task->destinationBase = StringDuplicate(instance->folder->path); instance->issuedPasteTask = task; - if (ES_SUCCESS != EsUserTaskStart(CommandPasteTask, task)) { + EsError error; + + if (task->move) { + error = EsUserTaskStart(CommandPasteTask, task, INTERFACE_STRING(FileManagerMoveTask), ES_ICON_FOLDER_MOVE); + } else { + error = EsUserTaskStart(CommandPasteTask, task, INTERFACE_STRING(FileManagerCopyTask), ES_ICON_FOLDER_COPY); + } + + if (error != ES_SUCCESS) { EsPoint point = EsListViewGetAnnouncementPointForSelection(instance->list); EsAnnouncementShow(instance->window, ES_FLAGS_DEFAULT, point.x, point.y, INTERFACE_STRING(CommonAnnouncementPasteErrorOther)); EsHeapFree(task->pathList); diff --git a/apps/file_manager/ui.cpp b/apps/file_manager/ui.cpp index a14deb1..0620b31 100644 --- a/apps/file_manager/ui.cpp +++ b/apps/file_manager/ui.cpp @@ -662,7 +662,7 @@ void ListItemCreated(EsElement *element, uintptr_t index, bool fromFolderRename) thumbnail->generatingTasksInProgress++; - String path = StringAllocateAndFormat("%s%s", STRFMT(instance->path), STRFMT(entry->GetName())); + String path = StringAllocateAndFormat("%s%s", STRFMT(instance->path), STRFMT(entry->GetInternalName())); Task task = { .string = path, @@ -805,6 +805,11 @@ int ListCallback(EsElement *element, EsMessage *message) { EsElement *element = message->createItem.item; element->messageUser = ListItemMessage; ListItemCreated(element, message->createItem.index, false); + } else if (message->type == ES_MSG_LIST_VIEW_CONTEXT_MENU) { + EsMenu *menu = EsMenuCreate(element, ES_MENU_AT_CURSOR); + EsMenuAddCommand(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(CommonClipboardCut), EsCommandByID(instance, ES_COMMAND_CUT)); + EsMenuAddCommand(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(CommonClipboardCopy), EsCommandByID(instance, ES_COMMAND_COPY)); + EsMenuShow(menu); } else if (message->type == ES_MSG_MOUSE_RIGHT_CLICK) { EsMenu *menu = EsMenuCreate(element, ES_MENU_AT_CURSOR); diff --git a/desktop/api.cpp b/desktop/api.cpp index 717093b..1a690a3 100644 --- a/desktop/api.cpp +++ b/desktop/api.cpp @@ -62,6 +62,7 @@ struct EnumString { const char *cName; int value; }; #define DESKTOP_MSG_FILE_TYPES_GET (14) #define DESKTOP_MSG_UNHANDLED_KEY_EVENT (15) #define DESKTOP_MSG_START_USER_TASK (16) +#define DESKTOP_MSG_SET_PROGRESS (17) struct EsFileStore { #define FILE_STORE_HANDLE (1) @@ -350,6 +351,23 @@ int64_t EsSystemConfigurationReadInteger(const char *section, ptrdiff_t sectionB return EsSystemConfigurationGroupReadInteger(group, key, keyBytes, defaultValue); } +void SystemConfigurationUnload() { + for (uintptr_t i = 0; i < api.systemConfigurationGroups.Length(); i++) { + for (uintptr_t j = 0; j < api.systemConfigurationGroups[i].itemCount; j++) { + EsHeapFree(api.systemConfigurationGroups[i].items[j].key); + EsHeapFree(api.systemConfigurationGroups[i].items[j].value); + } + + EsHeapFree(api.systemConfigurationGroups[i].section); + EsHeapFree(api.systemConfigurationGroups[i].sectionClass); + + Array items = { api.systemConfigurationGroups[i].items }; + items.Free(); + } + + api.systemConfigurationGroups.Free(); +} + void SystemConfigurationLoad(char *file, size_t fileBytes) { EsINIState s = {}; s.buffer = file; @@ -784,6 +802,16 @@ EsMessage *EsMessageReceive() { EsAssert(message.message.instanceSave.file->operationComplete); FileStoreCloseHandle(message.message.instanceSave.file); } else if (message.message.type == ES_MSG_APPLICATION_EXIT) { +#ifdef DEBUG_BUILD + GlyphCacheFree(); + FreeUnusedStyles(); + theming.loadedStyles.Free(); + SystemConfigurationUnload(); + api.mountPoints.Free(); + api.postBox.Free(); + api.timers.Free(); + EsPrint("ES_MSG_APPLICATION_EXIT - Heap allocation count: %d.\n", heap.allocationsCount); +#endif EsProcessTerminateCurrent(); } else if (message.message.type == ES_MSG_INSTANCE_DESTROY) { api.openInstanceCount--; @@ -1181,7 +1209,7 @@ extern "C" void _start(EsProcessStartupInformation *_startupInformation) { uint8_t m = DESKTOP_MSG_SYSTEM_CONFIGURATION_GET; EsBuffer responseBuffer = { .canGrow = true }; MessageDesktop(&m, 1, ES_INVALID_HANDLE, &responseBuffer); - SystemConfigurationLoad((char *) responseBuffer.in, responseBuffer.bytes); + SystemConfigurationLoad((char *) responseBuffer.out, responseBuffer.bytes); } if (uiProcess) { @@ -1548,15 +1576,18 @@ const void *EsEmbeddedFileGet(const char *_name, ptrdiff_t nameBytes, size_t *by return nullptr; } -struct UserTask { +struct EsUserTask { EsUserTaskCallback callback; EsGeneric data; EsHandle taskHandle; + +#define USER_TASK_MINIMUM_TIME_BETWEEN_PROGRESS_MESSAGES_MS (50) + double lastProgressMs; }; void UserTaskThread(EsGeneric _task) { - UserTask *task = (UserTask *) _task.p; - task->callback(task->data); + EsUserTask *task = (EsUserTask *) _task.p; + task->callback(task, task->data); EsMessageMutexAcquire(); api.openInstanceCount--; // TODO Send ES_MSG_APPLICATION_EXIT if needed. @@ -1566,10 +1597,32 @@ void UserTaskThread(EsGeneric _task) { EsHeapFree(task); } -EsError EsUserTaskStart(EsUserTaskCallback callback, EsGeneric data) { +void EsUserTaskSetProgress(EsUserTask *task, double progress, EsFileOffsetDifference bytesPerSecond) { + (void) bytesPerSecond; + double timeMs = EsTimeStampMs(); + + if (timeMs - task->lastProgressMs >= USER_TASK_MINIMUM_TIME_BETWEEN_PROGRESS_MESSAGES_MS) { + task->lastProgressMs = timeMs; + progress = ClampDouble(0.0, 1.0, progress); + uint8_t buffer[1 + sizeof(double)]; + buffer[0] = DESKTOP_MSG_SET_PROGRESS; + EsMemoryCopy(buffer + 1, &progress, sizeof(double)); + MessageDesktop(buffer, sizeof(buffer), task->taskHandle); + } +} + +bool EsUserTaskIsRunning(EsUserTask *task) { + (void) task; + return true; // TODO. +} + +EsError EsUserTaskStart(EsUserTaskCallback callback, EsGeneric data, const char *title, ptrdiff_t titleBytes, uint32_t iconID) { + // TODO Only tell the Desktop about the task if it's going to take >1 seconds. + // Maybe check after 500ms if the task is <50% complete? + EsMessageMutexCheck(); - UserTask *task = (UserTask *) EsHeapAllocate(sizeof(UserTask), true); + EsUserTask *task = (EsUserTask *) EsHeapAllocate(sizeof(EsUserTask), true); if (!task) { return ES_ERROR_INSUFFICIENT_RESOURCES; @@ -1587,6 +1640,15 @@ EsError EsUserTaskStart(EsUserTaskCallback callback, EsGeneric data) { return ES_ERROR_INSUFFICIENT_RESOURCES; } + { + char buffer[4096]; + buffer[0] = DESKTOP_MSG_SET_ICON; + EsMemoryCopy(buffer + 1, &iconID, sizeof(uint32_t)); + MessageDesktop(buffer, 1 + sizeof(uint32_t), handle); + size_t bytes = EsStringFormat(buffer, 4096, "%c%s", DESKTOP_MSG_SET_TITLE, titleBytes == -1 ? EsCStringLength(title) : titleBytes, title); + MessageDesktop(buffer, bytes, handle); + } + task->callback = callback; task->data = data; task->taskHandle = handle; diff --git a/desktop/desktop.cpp b/desktop/desktop.cpp index fe7732f..588bb2c 100644 --- a/desktop/desktop.cpp +++ b/desktop/desktop.cpp @@ -151,6 +151,7 @@ struct ApplicationInstance { char title[128]; size_t titleBytes; uint32_t iconID; + double progress; }; const EsStyle styleNewTabContent = { @@ -195,6 +196,9 @@ struct { ClipboardInformation clipboardInformation; bool configurationModified; + + Array allOngoingUserTasks; + double totalUserTaskProgress; } desktop; int TaskBarButtonMessage(EsElement *element, EsMessage *message); @@ -731,12 +735,53 @@ int TaskBarMessage(EsElement *element, EsMessage *message) { return 0; } -int TaskBarTasksButtonMessage(EsElement *, EsMessage *message) { +void TaskBarTasksButtonUpdate() { + if (desktop.allOngoingUserTasks.Length()) { + if (EsElementIsHidden(desktop.tasksButton)) { + EsPanelStartMovementAnimation((EsPanel *) EsElementGetLayoutParent(desktop.tasksButton), 1.5f /* duration scale */); + EsElementStartTransition(desktop.tasksButton, ES_TRANSITION_FADE_IN, ES_ELEMENT_TRANSITION_ENTRANCE, 1.5f); + EsElementSetHidden(desktop.tasksButton, false); + } + + // TODO Maybe grow button. + } else { + // TODO Maybe shrink or hide button. + // Currently, this always hides it, but I don't think this is a good behaviour. + + if (!EsElementIsHidden(desktop.tasksButton)) { + EsPanelStartMovementAnimation((EsPanel *) EsElementGetLayoutParent(desktop.tasksButton), 1.5f /* duration scale */); + EsElementStartTransition(desktop.tasksButton, ES_TRANSITION_FADE_OUT, ES_ELEMENT_TRANSITION_HIDE_AFTER_COMPLETE, 1.5f); + } + } + + EsElementRepaint(desktop.tasksButton); +} + +int TaskBarTasksButtonMessage(EsElement *element, EsMessage *message) { if (message->type == ES_MSG_GET_WIDTH) { message->measure.width = GetConstantNumber("taskBarTasksButtonWidth"); - return ES_HANDLED; + } else if (message->type == ES_MSG_PAINT) { + char title[256]; + size_t titleBytes; + + if (desktop.allOngoingUserTasks.Length() > 1) { + // TODO Localization. + titleBytes = EsStringFormat(title, sizeof(title), "%d tasks" ELLIPSIS, desktop.allOngoingUserTasks.Length()); + } else if (desktop.allOngoingUserTasks.Length() == 1) { + titleBytes = EsStringFormat(title, sizeof(title), "%s", desktop.allOngoingUserTasks.First()->titleBytes, desktop.allOngoingUserTasks.First()->title); + } else { + titleBytes = 0; + } + + EsDrawContent(message->painter, element, ES_RECT_2S(message->painter->width, message->painter->height), title, titleBytes); } else if (message->type == ES_MSG_PAINT_ICON) { - float progress = 0.0f; // TODO. + double progress = !desktop.allOngoingUserTasks.Length() ? 1.0 : desktop.totalUserTaskProgress / desktop.allOngoingUserTasks.Length(); + + if (progress > 0.0 && progress <= 0.05) { + progress = 0.05; // Really small angles look strange; avoid them. + } else if (progress >= 0.9 && progress < 1.0) { + progress = (progress - 0.9) * 0.5 + 0.9; // Approach 95% from 90% at half speed to account for bad progress calculations. + } uint32_t color1 = GetConstantNumber("taskBarTasksButtonWheelColor1"); uint32_t color2 = GetConstantNumber("taskBarTasksButtonWheelColor2"); @@ -784,10 +829,11 @@ int TaskBarTasksButtonMessage(EsElement *, EsMessage *message) { } RastSurfaceDestroy(&surface); - return ES_HANDLED; + } else { + return 0; } - return 0; + return ES_HANDLED; } void ShutdownModalCreate() { @@ -1888,7 +1934,7 @@ void DesktopSetup() { desktop.taskBar.taskList.Initialise(panel, ES_CELL_FILL, ReorderListMessage, nullptr); desktop.taskBar.taskList.cName = "task list"; - desktop.tasksButton = EsButtonCreate(panel, ES_ELEMENT_HIDDEN, ES_STYLE_TASK_BAR_BUTTON, "Copying files" ELLIPSIS, -1); + desktop.tasksButton = EsButtonCreate(panel, ES_ELEMENT_HIDDEN, ES_STYLE_TASK_BAR_BUTTON); desktop.tasksButton->messageUser = TaskBarTasksButtonMessage; EsButton *shutdownButton = EsButtonCreate(panel, ES_FLAGS_DEFAULT, ES_STYLE_TASK_BAR_EXTRA); @@ -2095,15 +2141,13 @@ void DesktopSyscall(EsMessage *message, uint8_t *buffer, EsBuffer *pipe) { EsHandle targetWindowHandle = EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, windowHandle, processHandle, 0, ES_WINDOW_PROPERTY_EMBED_OWNER); EsBufferWrite(pipe, &targetWindowHandle, sizeof(targetWindowHandle)); - if (EsElementIsHidden(desktop.tasksButton)) { - EsPanelStartMovementAnimation((EsPanel *) EsElementGetLayoutParent(desktop.tasksButton), 1.5f /* duration scale */); - EsElementStartTransition(desktop.tasksButton, ES_TRANSITION_FADE_IN, ES_ELEMENT_TRANSITION_ENTRANCE, 1.5f); - EsElementSetHidden(desktop.tasksButton, false); - } + desktop.allOngoingUserTasks.Add(instance); + TaskBarTasksButtonUpdate(); } else if (!instance) { // ------------------------------------------------- // | Messages below here require a valid instance. | // ------------------------------------------------- + EsPrint("DesktopSyscall - Received message %d without an instance.\n", buffer[0]); } else if (buffer[0] == DESKTOP_MSG_SET_TITLE || buffer[0] == DESKTOP_MSG_SET_ICON) { if (buffer[0] == DESKTOP_MSG_SET_TITLE) { instance->titleBytes = EsStringFormat(instance->title, sizeof(instance->title), "%s", @@ -2121,6 +2165,15 @@ void DesktopSyscall(EsMessage *message, uint8_t *buffer, EsBuffer *pipe) { instance->tab->container->taskBarButton->Repaint(true); } } + } else if (buffer[0] == DESKTOP_MSG_SET_PROGRESS && message->desktop.bytes == 1 + sizeof(double) && instance->isUserTask) { + double progress; + EsMemoryCopy(&progress, buffer + 1, sizeof(double)); + + if (progress >= 0.0 && progress <= 1.0) { + desktop.totalUserTaskProgress += progress - instance->progress; + instance->progress = progress; + EsElementRepaint(desktop.tasksButton); + } } else if (buffer[0] == DESKTOP_MSG_REQUEST_SAVE) { ApplicationInstanceRequestSave(instance, (const char *) buffer + 1, message->desktop.bytes - 1); } else if (buffer[0] == DESKTOP_MSG_COMPLETE_SAVE) { @@ -2163,6 +2216,8 @@ void DesktopSyscall(EsMessage *message, uint8_t *buffer, EsBuffer *pipe) { if (message.message.type != ES_MSG_INVALID) { UIProcessWindowManagerMessage((EsWindow *) message.object, &message.message, nullptr); } + } else { + EsPrint("DesktopSyscall - Received unhandled message %d.\n", buffer[0]); } } @@ -2218,6 +2273,11 @@ void EmbeddedWindowDestroyed(EsObjectID id) { EsElementDestroy(instance->tab); } + } else if (instance->isUserTask) { + desktop.totalUserTaskProgress -= instance->progress; + EsElementRepaint(desktop.tasksButton); + desktop.allOngoingUserTasks.FindAndDeleteSwap(instance, false /* ignore if not found */); + TaskBarTasksButtonUpdate(); } EsHeapFree(instance); diff --git a/desktop/gui.cpp b/desktop/gui.cpp index ce1435c..ce73437 100644 --- a/desktop/gui.cpp +++ b/desktop/gui.cpp @@ -125,7 +125,7 @@ struct EsElement : EsElementPublic { Array children; uint32_t state; - uint8_t transitionType; + uint8_t transitionType, transitionFlags; uint16_t customStyleState; // ORed to the style state in RefreshStyle. uint16_t previousStyleState; // Set by RefreshStyleState. uint16_t transitionDurationMs, transitionTimeMs; @@ -337,6 +337,7 @@ struct EsPanel : EsElement { EsPanelBand *bands[2]; uintptr_t tableIndex; + // TODO This names overlap with fields in EsElement, they should probably be renamed. uint16_t transitionType; uint32_t transitionTimeMs, transitionLengthMs; @@ -1308,6 +1309,10 @@ EsRectangle UIGetTransitionEffectRectangle(EsRectangle bounds, EsTransitionType } void UIDrawTransitionEffect(EsPainter *painter, EsPaintTarget *sourceSurface, EsRectangle bounds, EsTransitionType type, double progress, bool to) { + if (type == ES_TRANSITION_FADE_OUT && to) { + return; + } + EsRectangle destinationRegion = UIGetTransitionEffectRectangle(bounds, type, progress, to); EsRectangle sourceRegion = ES_RECT_4(0, bounds.r - bounds.l, 0, bounds.b - bounds.t); uint16_t alpha = (to ? progress : (1 - progress)) * 255; @@ -1344,6 +1349,7 @@ void EsElementStartTransition(EsElement *element, EsTransitionType transitionTyp } element->transitionTimeMs = 0; + element->transitionFlags = flags; element->transitionDurationMs = durationMs; element->transitionType = transitionType; element->StartAnimating(); @@ -1673,6 +1679,10 @@ void ProcessAnimations() { if (!transitionComplete) { element->Repaint(true, ES_RECT_1(0)); + } else { + if (element->transitionFlags & ES_ELEMENT_TRANSITION_HIDE_AFTER_COMPLETE) { + EsElementSetHidden(element, true); + } } bool backgroundAnimationComplete = ThemeAnimationComplete(&element->animation); @@ -6248,7 +6258,10 @@ void UIHandleKeyMessage(EsWindow *window, EsMessage *message) { if (window->focused) { message->type = ES_MSG_KEY_TYPED; - if (EsMessageSend(window->focused, message) != 0) return; + + if (EsMessageSend(window->focused, message) == ES_HANDLED /* allow messageUser to reject input */) { + return; + } EsElement *element = window->focused; message->type = ES_MSG_KEY_DOWN; diff --git a/desktop/os.header b/desktop/os.header index ee4cc47..22c006a 100644 --- a/desktop/os.header +++ b/desktop/os.header @@ -19,6 +19,7 @@ opaque_type EsPaintTarget none; opaque_type EsUndoManager none; opaque_type EsHeap none; opaque_type EsFileStore none; +opaque_type EsUserTask none; type_name uint8_t EsNodeType; type_name intptr_t EsError; @@ -326,6 +327,7 @@ define ES_ERROR_CONNECTION_REFUSED (-68) define ES_ERROR_ILLEGAL_PATH (-69) define ES_ERROR_NODE_NOT_LOADED (-71) define ES_ERROR_DIRECTORY_ENTRY_BEING_REMOVED (-72) +define ES_ERROR_CANCELLED (-73) define ES_SYSTEM_CONSTANT_TIME_STAMP_UNITS_PER_MICROSECOND (0) define ES_SYSTEM_CONSTANT_OPTIMAL_WORK_QUEUE_THREAD_COUNT (1) @@ -666,6 +668,7 @@ define ES_MEMORY_RESERVE_COMMIT_ALL (1 << 0) define ES_PANEL_SWITCHER_DESTROY_PREVIOUS_AFTER_TRANSITION (1 << 0) define ES_ELEMENT_TRANSITION_ENTRANCE (1 << 0) +define ES_ELEMENT_TRANSITION_HIDE_AFTER_COMPLETE (1 << 1) define ES_TEXT_GET_CHARACTER_AT_POINT_MIDDLE (1 << 0) @@ -910,7 +913,7 @@ enum EsMessageType { // It will be removed from the `children` later (but before the next ES_MSG_LAYOUT message is received). ES_MSG_PRE_ADD_CHILD = 0x200D // An element has been created with this element as its parent, but is not yet added to the parent. ES_MSG_HIT_TEST = 0x200E // For non-rectangular elements: test whether a pixel should be considered inside the element. Set response to ES_HANDLED. - ES_MSG_KEY_TYPED = 0x2011 // Sent to the focused element when a key is pressed. + ES_MSG_KEY_TYPED = 0x2011 // Sent to the focused element when a key is pressed. Only if ES_HANDLED is returned the message will not propagate; this allows messageUser to block input processing by returning ES_REJECTED. ES_MSG_SCROLL_X = 0x2012 // The element has been horizontally scrolled. ES_MSG_SCROLL_Y = 0x2013 // The element has been vertically scrolled. ES_MSG_STRONG_FOCUS_END = 0x2014 // Sent once when the user 'clicks off' the element, even if a new element was not necessarily focused. @@ -1887,7 +1890,8 @@ function_pointer void EsUndoCallback(const void *item, EsUndoManager *manager, E function_pointer void EsMountPointEnumerationCallback(const char *prefix, size_t prefixBytes, EsGeneric context); function_pointer void EsListViewEnumerateVisibleItemsCallback(EsListView *view, EsElement *item, uint32_t group, EsGeneric index); function_pointer void EsFontEnumerationCallback(const EsFontInformation *information, EsGeneric context); -function_pointer void EsUserTaskCallback(EsGeneric data); +function_pointer void EsUserTaskCallback(EsUserTask *task, EsGeneric data); +function_pointer bool EsFileCopyCallback(EsFileOffset bytesCopied, EsFileOffset totalBytes, EsGeneric data); // Return false to cancel. // System. @@ -1933,7 +1937,7 @@ function EsError EsFileWriteAllFromHandle(EsHandle handle, const void *data, siz function EsError EsFileWriteAllGather(STRING filePath, const void **data, size_t *fileSize, size_t gatherCount); function EsError EsFileWriteAllGatherFromHandle(EsHandle handle, const void **data, size_t *fileSize, size_t gatherCount); function void *EsFileMap(STRING filePath, size_t *fileSize, uint32_t flags); -function EsError EsFileCopy(STRING source, STRING destination, void **copyBuffer = nullptr); // If you are copying lots of files, you can reuse the temporary copy buffer by storing the output copyBuffer; call EsHeapFree on it after the last copy. +function EsError EsFileCopy(STRING source, STRING destination, void **copyBuffer = ES_NULL, EsFileCopyCallback callback = ES_NULL, EsGeneric data = ES_NULL); // If you are copying lots of files, you can reuse the temporary copy buffer by storing the output copyBuffer; call EsHeapFree on it after the last copy. function EsError EsFileControl(EsHandle file, uint32_t flags); function EsFileInformation EsFileOpen(STRING path, uint32_t flags); @@ -2233,7 +2237,7 @@ function int EsCRTvsnprintf(char *buffer, size_t bufferSize, const char *format, function EsError EsClipboardAddText(EsClipboard clipboard, STRING text = BLANK_STRING); function bool EsClipboardHasFormat(EsClipboard clipboard, EsClipboardFormat format); function bool EsClipboardHasData(EsClipboard clipboard); -function char *EsClipboardReadText(EsClipboard clipboard, size_t *bytes, uint32_t *flags = nullptr); // Free with EsHeapFree. +function char *EsClipboardReadText(EsClipboard clipboard, size_t *bytes, uint32_t *flags = ES_NULL); // Free with EsHeapFree. function EsFileStore *EsClipboardOpen(EsClipboard clipboard); // Open the clipboard for writing. function EsError EsClipboardCloseAndAdd(EsClipboard clipboard, EsClipboardFormat format, EsFileStore *fileStore, uint32_t flags = ES_FLAGS_DEFAULT); @@ -2265,7 +2269,9 @@ function const EsApplicationStartupInformation *EsInstanceGetStartupInformation( function void EsInstanceOpenComplete(EsMessage *message, bool success, STRING errorText = BLANK_STRING); function void EsInstanceSaveComplete(EsMessage *message, bool success); -function EsError EsUserTaskStart(EsUserTaskCallback callback, EsGeneric data); +function EsError EsUserTaskStart(EsUserTaskCallback callback, EsGeneric data, STRING title, uint32_t iconID); +function void EsUserTaskSetProgress(EsUserTask *task, double progress, EsFileOffsetDifference bytesPerSecond); // Set bytesPerSecond to -1 if not applicable. +function bool EsUserTaskIsRunning(EsUserTask *task); // Returns false if the task was cancelled. // Message processing. diff --git a/desktop/syscall.cpp b/desktop/syscall.cpp index cca50f1..631856c 100644 --- a/desktop/syscall.cpp +++ b/desktop/syscall.cpp @@ -223,7 +223,8 @@ void *EsFileReadAll(const char *filePath, ptrdiff_t filePathLength, size_t *file return buffer; } -EsError EsFileCopy(const char *source, ptrdiff_t sourceBytes, const char *destination, ptrdiff_t destinationBytes, void **_copyBuffer) { +EsError EsFileCopy(const char *source, ptrdiff_t sourceBytes, const char *destination, ptrdiff_t destinationBytes, void **_copyBuffer, + EsFileCopyCallback callback, EsGeneric callbackData) { const size_t copyBufferBytes = 262144; void *copyBuffer = _copyBuffer && *_copyBuffer ? *_copyBuffer : EsHeapAllocate(copyBufferBytes, false); if (_copyBuffer) *_copyBuffer = copyBuffer; @@ -245,9 +246,25 @@ EsError EsFileCopy(const char *source, ptrdiff_t sourceBytes, const char *destin if (error == ES_SUCCESS) { for (uintptr_t i = 0; i < sourceFile.size; i += copyBufferBytes) { size_t bytesRead = EsFileReadSync(sourceFile.handle, i, copyBufferBytes, copyBuffer); - if (ES_CHECK_ERROR(bytesRead)) { error = bytesRead; break; } + + if (ES_CHECK_ERROR(bytesRead)) { + error = bytesRead; + break; + } + size_t bytesWritten = EsFileWriteSync(destinationFile.handle, i, bytesRead, copyBuffer); - if (ES_CHECK_ERROR(bytesWritten)) { error = bytesWritten; break; } + + if (ES_CHECK_ERROR(bytesWritten)) { + error = bytesWritten; + break; + } + + EsAssert(bytesRead == bytesWritten); + + if (callback && !callback(i + bytesWritten, sourceFile.size, callbackData)) { + error = ES_ERROR_CANCELLED; + break; + } } } diff --git a/desktop/text.cpp b/desktop/text.cpp index 5091da3..60443df 100644 --- a/desktop/text.cpp +++ b/desktop/text.cpp @@ -95,22 +95,25 @@ Font FontGet(EsFont key); // --------------------------------- Glyph cache. +void GlyphCacheFreeEntry() { + GlyphCacheEntry *entry = fontManagement.glyphCacheLRU.lastItem->thisItem; + fontManagement.glyphCacheLRU.Remove(&entry->itemLRU); + fontManagement.glyphCache.Delete(&entry->key); + EsAssert(fontManagement.glyphCacheBytes >= entry->dataBytes); + fontManagement.glyphCacheBytes -= entry->dataBytes; + EsHeapFree(entry->data); + EsHeapFree(entry); +} + void RegisterGlyphCacheEntry(GlyphCacheKey key, GlyphCacheEntry *entry) { entry->itemLRU.thisItem = entry; entry->key = key; *fontManagement.glyphCache.Put(&key) = entry; fontManagement.glyphCacheLRU.InsertStart(&entry->itemLRU); - fontManagement.glyphCacheBytes += entry->dataBytes; while (fontManagement.glyphCacheBytes > GLYPH_CACHE_MAX_SIZE) { - GlyphCacheEntry *leastRecentlyUsedGlyph = fontManagement.glyphCacheLRU.lastItem->thisItem; - fontManagement.glyphCacheLRU.Remove(&leastRecentlyUsedGlyph->itemLRU); - fontManagement.glyphCache.Delete(&leastRecentlyUsedGlyph->key); - EsAssert(fontManagement.glyphCacheBytes >= entry->dataBytes); // Negative glyph cache bytes. - fontManagement.glyphCacheBytes -= entry->dataBytes; - EsHeapFree(leastRecentlyUsedGlyph->data); - EsHeapFree(leastRecentlyUsedGlyph); + GlyphCacheFreeEntry(); } } @@ -126,6 +129,15 @@ GlyphCacheEntry *LookupGlyphCacheEntry(GlyphCacheKey key) { } } +void GlyphCacheFree() { + while (fontManagement.glyphCacheLRU.count) { + GlyphCacheFreeEntry(); + } + + EsAssert(fontManagement.glyphCache.Count() == 0); + fontManagement.glyphCache.Free(); +} + // --------------------------------- Font renderer. bool FontLoad(Font *font, const void *data, size_t dataBytes) { diff --git a/shared/strings.cpp b/shared/strings.cpp index cf22d29..ed13578 100644 --- a/shared/strings.cpp +++ b/shared/strings.cpp @@ -260,6 +260,8 @@ DEFINE_INTERFACE_STRING(FileManagerInvalidPath, "The current path does not lead DEFINE_INTERFACE_STRING(FileManagerInvalidDrive, "The drive containing this folder was disconnected."); DEFINE_INTERFACE_STRING(FileManagerRefresh, "Refresh"); DEFINE_INTERFACE_STRING(FileManagerListContextActions, "Actions"); +DEFINE_INTERFACE_STRING(FileManagerCopyTask, "Copying" ELLIPSIS); +DEFINE_INTERFACE_STRING(FileManagerMoveTask, "Moving" ELLIPSIS); // TODO System Monitor. diff --git a/util/api_table.ini b/util/api_table.ini index bb2f8b8..9bc28e8 100644 --- a/util/api_table.ini +++ b/util/api_table.ini @@ -434,3 +434,5 @@ EsListViewSelectNone=432 EsElementIsFocused=433 EsUserTaskStart=434 EsElementIsHidden=435 +EsUserTaskSetProgress=436 +EsUserTaskIsRunning=437