diff --git a/apps/image_editor.cpp b/apps/image_editor.cpp index 6097618..704f8ea 100644 --- a/apps/image_editor.cpp +++ b/apps/image_editor.cpp @@ -84,11 +84,11 @@ const EsStyle styleBitmapSizeTextbox = { }; const EsStyle styleImageMenuTable = { + .inherit = ES_STYLE_PANEL_FORM_TABLE, + .metrics = { - .mask = ES_THEME_METRICS_GAP_ALL | ES_THEME_METRICS_INSETS, + .mask = ES_THEME_METRICS_INSETS, .insets = ES_RECT_4(20, 20, 5, 8), - .gapMajor = 6, - .gapMinor = 6, }, }; diff --git a/apps/installer.cpp b/apps/installer.cpp index b41e4dc..a129044 100644 --- a/apps/installer.cpp +++ b/apps/installer.cpp @@ -49,14 +49,6 @@ const EsStyle styleButtonsRow = { }, }; -const EsStyle styleCustomizeTable = { - .metrics = { - .mask = ES_THEME_METRICS_GAP_MAJOR | ES_THEME_METRICS_GAP_MINOR, - .gapMajor = 7, - .gapMinor = 7, - }, -}; - InstallerMetadata *metadata; Array connectedDrives; EsListView *drivesList; @@ -1085,7 +1077,7 @@ void _start() { EsTextDisplayCreate(panelCustomizeOptions, ES_CELL_H_FILL, ES_STYLE_TEXT_HEADING0, INTERFACE_STRING(InstallerTitle)); EsTextDisplayCreate(panelCustomizeOptions, ES_CELL_H_FILL, ES_STYLE_TEXT_HEADING1, INTERFACE_STRING(InstallerCustomizeOptions)); - EsPanel *table = EsPanelCreate(panelCustomizeOptions, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL | ES_PANEL_TABLE, &styleCustomizeTable); + EsPanel *table = EsPanelCreate(panelCustomizeOptions, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL | ES_PANEL_TABLE, ES_STYLE_PANEL_FORM_TABLE); EsPanelSetBands(table, 2 /* columns */); EsTextDisplayCreate(table, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(InstallerUserName)); diff --git a/apps/samples/converter.cpp b/apps/samples/converter.cpp index 53dcf2f..42a1718 100644 --- a/apps/samples/converter.cpp +++ b/apps/samples/converter.cpp @@ -13,16 +13,6 @@ const EsStyle stylePanelStack = { }, }; -// Define the metrics for panelForm. -const EsStyle stylePanelForm = { - .metrics = { - .mask = ES_THEME_METRICS_GAP_MAJOR - | ES_THEME_METRICS_GAP_MINOR, - .gapMajor = 5, // Spacing between columns. - .gapMinor = 8, // Spacing between rows. - }, -}; - // Global variables. EsTextbox *textboxRate; EsTextbox *textboxAmount; @@ -74,10 +64,10 @@ void _start() { // Add a second layout panel to panelStack to contain the elements of the form. EsPanel *panelForm = EsPanelCreate( - panelStack, // Add it to panelStack. - ES_PANEL_TABLE // Use table layout. - | ES_PANEL_HORIZONTAL, // Left to right, then top to bottom. - &stylePanelForm); + panelStack, // Add it to panelStack. + ES_PANEL_TABLE // Use table layout. + | ES_PANEL_HORIZONTAL, // Left to right, then top to bottom. + ES_STYLE_PANEL_FORM_TABLE); // Use the standard metrics for a form. // Set the number of columns for the panelForm's table layout. EsPanelSetBands(panelForm, 2); diff --git a/apps/test.cpp b/apps/test.cpp index f3fee21..06ed79a 100644 --- a/apps/test.cpp +++ b/apps/test.cpp @@ -151,6 +151,19 @@ void InitialiseInstance(EsInstance *instance) { EsPanel *panel = EsPanelCreate(instance->window, ES_CELL_FILL, &stylePanel); EsButtonCreate(panel, ES_BUTTON_CHECKBOX, 0, "Checkbox"); + + EsButtonOnCommand(EsButtonCreate(panel, ES_FLAGS_DEFAULT, 0, "Alert 1"), [] (EsInstance *, EsElement *element, EsCommand *) { + EsDialogShowAlert(element->window, "Title", -1, "Content.", -1, ES_ICON_DIALOG_WARNING, ES_DIALOG_ALERT_OK_BUTTON); + }); + + EsPanel *table = EsPanelCreate(panel, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL | ES_PANEL_TABLE_H_JUSTIFY); + EsPanelSetBands(table, 3); + + for (uintptr_t i = 0; i < 8; i++) { + EsButtonCreate(table, ES_FLAGS_DEFAULT, 0, "Justified columns"); + } + +#if 0 EsButtonOnCommand(EsButtonCreate(panel, ES_FLAGS_DEFAULT, 0, "Crash"), [] (EsInstance *, EsElement *, EsCommand *) { EsAssert(false); }); EsButtonOnCommand(EsButtonCreate(panel, ES_FLAGS_DEFAULT, 0, "Hang"), [] (EsInstance *, EsElement *, EsCommand *) { while (true); }); EsButtonOnCommand(EsButtonCreate(panel, ES_FLAGS_DEFAULT, 0, "Wait"), [] (EsInstance *, EsElement *, EsCommand *) { EsSleep(8000); }); @@ -176,6 +189,7 @@ void InitialiseInstance(EsInstance *instance) { EsButtonOnCommand(EsButtonCreate(panel, ES_FLAGS_DEFAULT, 0, "Crash 6"), [] (EsInstance *, EsElement *, EsCommand *) { EsMemoryCopy(nullptr, nullptr, 1); }); +#endif EsButtonOnCommand(EsButtonCreate(panel, ES_FLAGS_DEFAULT, 0, "Move file"), [] (EsInstance *, EsElement *, EsCommand *) { EsPathMove("0:/A Study in Scarlet.txt", -1, "0:/moved.txt", -1, ES_PATH_MOVE_ALLOW_COPY_AND_DELETE); diff --git a/desktop/api.cpp b/desktop/api.cpp index 0dcd528..73de673 100644 --- a/desktop/api.cpp +++ b/desktop/api.cpp @@ -225,6 +225,9 @@ struct APIInstance { commandSave, commandShowInFileManager; + char *newName; + size_t newNameBytes; + const char *applicationName; size_t applicationNameBytes; @@ -235,6 +238,8 @@ struct APIInstance { EsFileStore *fileStore; + bool closeAfterSaveCompletes; + // Do not propagate messages about this instance to the application. // Currently only used for inspectors. bool internalOnly; @@ -247,7 +252,7 @@ struct APIInstance { // For the file menu. EsPanel *fileMenuNameSwitcher; EsPanel *fileMenuNamePanel; - EsTextbox *fileMenuNameTextbox; + EsTextbox *fileMenuNameTextbox; // Also used by the file save dialog. }; MountPoint *NodeAddMountPoint(const char *prefix, size_t prefixBytes, EsHandle base, bool queryInformation) { @@ -651,6 +656,86 @@ void InstanceSave(EsInstance *_instance) { } } +void InstanceClose(EsInstance *instance) { + if (EsCommandByID(instance, ES_COMMAND_SAVE)->disabled) { + EsInstanceDestroy(instance); + return; + } + + // The document has unsaved changes. + // Ask the user if they want to save. + + // TODO Handling shutdown. + + APIInstance *apiInstance = (APIInstance *) instance->_private; + char content[512]; + size_t contentBytes; + const char *cTitle; + + if (apiInstance->startupInformation->filePathBytes) { + cTitle = interfaceString_FileCloseWithModificationsTitle; + contentBytes = EsStringFormat(content, sizeof(content), interfaceString_FileCloseWithModificationsContent, + apiInstance->startupInformation->filePathBytes, apiInstance->startupInformation->filePath); + } else { + cTitle = interfaceString_FileCloseNewTitle; + contentBytes = EsStringFormat(content, sizeof(content), interfaceString_FileCloseNewContent, + apiInstance->applicationNameBytes, apiInstance->applicationName); + } + + // NOTE Duplicated from EsDialogShowAlert. + // TODO Is there a good way to make more modular dialogs? + EsElement *dialog = EsDialogShow(instance->window); + EsPanel *heading = EsPanelCreate(dialog, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_DIALOG_HEADING); + EsIconDisplayCreate(heading, ES_FLAGS_DEFAULT, 0, ES_ICON_DIALOG_WARNING); + EsTextDisplayCreate(heading, ES_CELL_H_FILL | ES_CELL_V_CENTER, ES_STYLE_TEXT_HEADING2, cTitle); + EsPanel *contentArea = EsPanelCreate(dialog, ES_CELL_H_FILL | ES_PANEL_VERTICAL, ES_STYLE_DIALOG_CONTENT); + EsTextDisplayCreate(contentArea, ES_CELL_H_FILL, ES_STYLE_TEXT_PARAGRAPH, content, contentBytes); + EsPanel *buttonArea = EsPanelCreate(dialog, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE, ES_STYLE_DIALOG_BUTTON_AREA); + + if (!apiInstance->startupInformation->filePathBytes) { + EsPanel *row = EsPanelCreate(contentArea, ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); + EsTextDisplayCreate(row, ES_FLAGS_DEFAULT, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(FileCloseNewName)); + EsTextbox *textbox = EsTextboxCreate(row); + EsInstanceClassEditorSettings *editorSettings = &apiInstance->editorSettings; + EsTextboxInsert(textbox, editorSettings->newDocumentFileName, editorSettings->newDocumentFileNameBytes); + EsElementFocus(textbox); + TextboxSelectSectionBeforeFileExtension(textbox, editorSettings->newDocumentFileName, editorSettings->newDocumentFileNameBytes); + apiInstance->fileMenuNameTextbox = textbox; + } + + EsButton *button; + + button = EsButtonCreate(buttonArea, ES_BUTTON_CANCEL, 0, INTERFACE_STRING(CommonCancel)); + + EsButtonOnCommand(button, [] (EsInstance *instance, EsElement *, EsCommand *) { + EsDialogClose(instance->window); + }); + + button = EsButtonCreate(buttonArea, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_DANGEROUS, INTERFACE_STRING(FileCloseWithModificationsDelete)); + + EsButtonOnCommand(button, [] (EsInstance *instance, EsElement *, EsCommand *) { + EsInstanceDestroy(instance); + }); + + button = EsButtonCreate(buttonArea, ES_BUTTON_DEFAULT, 0, INTERFACE_STRING(FileCloseWithModificationsSave)); + + EsButtonOnCommand(button, [] (EsInstance *instance, EsElement *, EsCommand *) { + APIInstance *apiInstance = (APIInstance *) instance->_private; + + if (apiInstance->startupInformation->filePathBytes) { + InstanceSave(instance); + } else { + InstanceRenameFromTextbox(instance->window, apiInstance, apiInstance->fileMenuNameTextbox); + } + + apiInstance->closeAfterSaveCompletes = true; + }); + + if (apiInstance->startupInformation->filePathBytes) { + EsElementFocus(button); + } +} + void FileStoreCloseHandle(EsFileStore *fileStore) { EsMessageMutexCheck(); // TODO Remove this limitation? EsAssert(fileStore->handles < 0x80000000); @@ -934,10 +1019,12 @@ EsMessage *EsMessageReceive() { if (instance->startupInformation) { EsHeapFree((void *) instance->startupInformation->filePath); + EsHeapFree((void *) instance->startupInformation->containingFolder); } EsHeapFree(instance->startupInformation); EsHeapFree(instance->documentPath); + EsHeapFree(instance->newName); for (uintptr_t i = 0; i < instance->commands.Count(); i++) { EsCommand *command = instance->commands[i]; @@ -1012,7 +1099,10 @@ EsMessage *EsMessageReceive() { } } else if (type == ES_MSG_TAB_CLOSE_REQUEST) { EsInstance *instance = InstanceFromWindowID(message.message.tabOperation.id); - if (instance) EsInstanceDestroy(instance); + + if (instance) { + InstanceClose(instance); + } } else if (type == ES_MSG_INSTANCE_SAVE_RESPONSE) { EsMessage m = {}; m.type = ES_MSG_INSTANCE_SAVE; @@ -1046,11 +1136,15 @@ EsMessage *EsMessageReceive() { EsInstance *instance = InstanceFromWindowID(message.message.tabOperation.id); if (instance) { + APIInstance *apiInstance = (APIInstance *) instance->_private; + if (message.message.tabOperation.error == ES_SUCCESS) { EsRectangle bounds = EsElementGetWindowBounds(instance->window->toolbarSwitcher); EsAnnouncementShow(instance->window, ES_FLAGS_DEFAULT, (bounds.l + bounds.r) / 2, bounds.b, INTERFACE_STRING(FileRenameSuccess)); } else { + char buffer[512]; const char *errorMessage = interfaceString_FileSaveErrorUnknown; + ptrdiff_t errorMessageBytes = -1; switch (message.message.tabOperation.error) { case ES_ERROR_FILE_DOES_NOT_EXIST: @@ -1068,14 +1162,20 @@ EsMessage *EsMessageReceive() { case ES_ERROR_INSUFFICIENT_RESOURCES: errorMessage = interfaceString_FileSaveErrorResourcesLow; break; - case ES_ERROR_FILE_ALREADY_EXISTS: - errorMessage = interfaceString_FileSaveErrorAlreadyExists; - break; + + case ES_ERROR_FILE_ALREADY_EXISTS: { + errorMessage = buffer; + errorMessageBytes = EsStringFormat(buffer, sizeof(buffer), interfaceString_FileSaveErrorAlreadyExists, + apiInstance->newNameBytes, apiInstance->newName); + } break; } EsDialogShowAlert(instance->window, INTERFACE_STRING(FileCannotRename), - errorMessage, -1, ES_ICON_DIALOG_ERROR, ES_DIALOG_ALERT_OK_BUTTON); + errorMessage, errorMessageBytes, ES_ICON_DIALOG_ERROR, ES_DIALOG_ALERT_OK_BUTTON); } + + EsHeapFree(apiInstance->newName); + apiInstance->newName = nullptr; } } else if (type == ES_MSG_INSTANCE_DOCUMENT_RENAMED) { char *buffer = (char *) EsHeapAllocate(message.message.tabOperation.bytes, false); @@ -1279,8 +1379,16 @@ void EsInstanceSaveComplete(EsMessage *message, bool success) { EsAnnouncementShow(instance->window, ES_FLAGS_DEFAULT, (bounds.l + bounds.r) / 2, bounds.b, message, messageBytes); EsHeapFree(message); EsCommandSetDisabled(&apiInstance->commandShowInFileManager, false); + + if (apiInstance->closeAfterSaveCompletes) { + EsInstanceDestroy(instance); + } } else { + apiInstance->closeAfterSaveCompletes = false; + + char buffer[512]; const char *errorMessage = interfaceString_FileSaveErrorUnknown; + ptrdiff_t errorMessageBytes = -1; switch (message->instanceSave.file->error) { case ES_ERROR_FILE_DOES_NOT_EXIST: @@ -1310,16 +1418,19 @@ void EsInstanceSaveComplete(EsMessage *message, bool success) { case ES_ERROR_INSUFFICIENT_RESOURCES: errorMessage = interfaceString_FileSaveErrorResourcesLow; break; - case ES_ERROR_FILE_ALREADY_EXISTS: - errorMessage = interfaceString_FileSaveErrorAlreadyExists; - break; case ES_ERROR_TOO_MANY_FILES_WITH_NAME: errorMessage = interfaceString_FileSaveErrorTooManyFiles; break; + + case ES_ERROR_FILE_ALREADY_EXISTS: { + errorMessage = buffer; + errorMessageBytes = EsStringFormat(buffer, sizeof(buffer), interfaceString_FileSaveErrorAlreadyExists, + apiInstance->newNameBytes, apiInstance->newName); + } break; } EsDialogShowAlert(instance->window, INTERFACE_STRING(FileCannotSave), - errorMessage, -1, ES_ICON_DIALOG_ERROR, ES_DIALOG_ALERT_OK_BUTTON); + errorMessage, errorMessageBytes, ES_ICON_DIALOG_ERROR, ES_DIALOG_ALERT_OK_BUTTON); } } diff --git a/desktop/desktop.cpp b/desktop/desktop.cpp index be2c5a3..5a6ca03 100644 --- a/desktop/desktop.cpp +++ b/desktop/desktop.cpp @@ -1226,7 +1226,7 @@ void ShutdownModalCreate() { EsTextDisplayCreate(EsPanelCreate(dialog, ES_PANEL_VERTICAL | ES_CELL_H_FILL, ES_STYLE_DIALOG_CONTENT), ES_CELL_H_FILL, ES_STYLE_TEXT_PARAGRAPH, INTERFACE_STRING(DesktopConfirmShutdown))->cName = "dialog contents"; EsPanel *buttonArea = EsPanelCreate(dialog, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_CELL_H_FILL, ES_STYLE_DIALOG_BUTTON_AREA); - EsButton *cancelButton = EsButtonCreate(buttonArea, ES_BUTTON_DEFAULT, 0, INTERFACE_STRING(CommonCancel)); + EsButton *cancelButton = EsButtonCreate(buttonArea, ES_BUTTON_DEFAULT | ES_BUTTON_CANCEL, 0, INTERFACE_STRING(CommonCancel)); EsButton *restartButton = EsButtonCreate(buttonArea, ES_FLAGS_DEFAULT, 0, INTERFACE_STRING(DesktopRestartAction)); EsButton *shutdownButton = EsButtonCreate(buttonArea, ES_FLAGS_DEFAULT, ES_STYLE_PUSH_BUTTON_DANGEROUS, INTERFACE_STRING(DesktopShutdownAction)); EsElementFocus(cancelButton); diff --git a/desktop/gui.cpp b/desktop/gui.cpp index dfb8966..bf91ad7 100644 --- a/desktop/gui.cpp +++ b/desktop/gui.cpp @@ -443,14 +443,14 @@ struct EsWindow : EsElement { EsWindowStyle windowStyle; uint32_t windowWidth, windowHeight; - bool willUpdate, toolbarFillMode, destroyInstanceAfterClose, hasDialog, doNotPaint; + bool willUpdate, toolbarFillMode, destroyInstanceAfterClose, doNotPaint; bool restoreOnNextMove, resetPositionOnNextMove, receivedFirstResize, isMaximised; bool hovering, activated, appearActivated; bool visualizeRepaints, visualizeLayoutBounds, visualizePaintSteps; // Inspector properties. EsElement *mainPanel, *toolbar; EsPanel *toolbarSwitcher; - EsElement *dialogWrapper; + Array dialogs; EsPoint mousePosition; @@ -728,6 +728,7 @@ void UIWindowDestroy(EsWindow *window) { EsSyscall(ES_SYSCALL_WINDOW_CLOSE, window->handle, 0, 0, 0); EsHandleClose(window->handle); window->checkVisible.Free(); + window->dialogs.Free(); window->handle = ES_INVALID_HANDLE; } @@ -1918,9 +1919,6 @@ void LayoutTable(EsPanel *panel, EsMessage *message) { EsRectangle insets = panel->GetInsets(); -#define TABLE_AXIS_HORIZONTAL (0) -#define TABLE_AXIS_VERTICAL (1) - for (int _axis = 0; _axis < 2; _axis++) { int axis = (~panel->flags & ES_PANEL_HORIZONTAL) ? (1 - _axis) : _axis; int gapSize = _axis ? panel->GetGapMinor() : panel->GetGapMajor(); @@ -1941,7 +1939,7 @@ void LayoutTable(EsPanel *panel, EsMessage *message) { size = 0; } else { int alternate = _axis ? calculatedSize[1 - axis][i] : 0; - size = axis == TABLE_AXIS_HORIZONTAL ? child->GetWidth(alternate) : child->GetHeight(alternate); + size = axis ? child->GetHeight(alternate) : child->GetWidth(alternate); } if (debug) EsPrint("\tChild %d (%z) in cells %d->%d has size %d\n", i, child->cName, child->tableCell.from[axis], child->tableCell.to[axis], size); @@ -2091,7 +2089,20 @@ void LayoutTable(EsPanel *panel, EsMessage *message) { calculatedSize[axis][i] = size; } - // Step 5: Calculate the position of the bands. + // Step 5: Calculate justification gap. + + if ((axis ? (panel->flags & ES_PANEL_TABLE_V_JUSTIFY) : (panel->flags & ES_PANEL_TABLE_H_JUSTIFY)) + && panel->bandCount[axis] > 1 && message->type == ES_MSG_LAYOUT) { + int32_t usedSize = 0; + + for (int i = 0; i < panel->bandCount[axis]; i++) { + usedSize += calculatedProperties[axis][i].preferredSize; + } + + gapSize = (in[axis] - usedSize) / (panel->bandCount[axis] - 1); + } + + // Step 6: Calculate the position of the bands. int position = insetStart; @@ -2099,14 +2110,14 @@ void LayoutTable(EsPanel *panel, EsMessage *message) { if (i) position += gapSize; EsPanelBand *band = calculatedProperties[axis] + i; int size = band->preferredSize; - band->maximumSize = position; // Aliasing maximumSize with position. + band->maximumSize = position; // HACK Aliasing maximumSize with position. position += size; } out[axis] = position + insetEnd; } - // Step 6: Move the children to their new location. + // Step 7: Move the children to their new location. if (message->type == ES_MSG_GET_WIDTH) { message->measure.width = out[0]; @@ -3513,21 +3524,25 @@ int ProcessDialogClosingMessage(EsElement *element, EsMessage *message) { void EsDialogClose(EsWindow *window) { EsMessageMutexCheck(); - EsAssert(window->hasDialog); + EsAssert(window->dialogs.Length()); - EsAssert(window->dialogWrapper->children[0]->messageClass == ProcessPanelMessage); - window->dialogWrapper->children[0]->messageClass = ProcessDialogClosingMessage; - EsElementStartTransition(window->dialogWrapper->children[0], ES_TRANSITION_ZOOM_OUT_LIGHT, ES_ELEMENT_TRANSITION_EXIT, 1.0f); + EsElement *dialog = window->dialogs.Pop(); - window->dialogWrapper = nullptr; - window->children[0]->children[0]->state &= ~UI_STATE_BLOCK_INTERACTION; - window->children[1]->state &= ~UI_STATE_BLOCK_INTERACTION; - window->hasDialog = false; + EsAssert(dialog->messageClass == ProcessPanelMessage); + dialog->messageClass = ProcessDialogClosingMessage; + EsElementStartTransition(dialog, ES_TRANSITION_ZOOM_OUT_LIGHT, ES_ELEMENT_TRANSITION_EXIT, 1.0f); - if (window->inactiveFocus) { - EsElementFocus(window->inactiveFocus, false); - window->inactiveFocus->Repaint(true); - window->inactiveFocus = nullptr; + if (!window->dialogs.Length()) { + window->children[0]->children[0]->state &= ~UI_STATE_BLOCK_INTERACTION; + window->children[1]->state &= ~UI_STATE_BLOCK_INTERACTION; + + if (window->inactiveFocus) { + EsElementFocus(window->inactiveFocus, false); + window->inactiveFocus->Repaint(true); + window->inactiveFocus = nullptr; + } + } else { + window->dialogs.Last()->state &= ~UI_STATE_BLOCK_INTERACTION; } } @@ -3539,7 +3554,7 @@ EsElement *EsDialogShowAlert(EsWindow *window, const char *title, ptrdiff_t titl if (!heading) return nullptr; if (iconID) { - EsIconDisplayCreate(heading, ES_FLAGS_DEFAULT, {}, iconID); + EsIconDisplayCreate(heading, ES_FLAGS_DEFAULT, 0, iconID); } EsTextDisplayCreate(heading, ES_CELL_H_FILL | ES_CELL_V_CENTER, ES_STYLE_TEXT_HEADING2, @@ -3552,7 +3567,7 @@ EsElement *EsDialogShowAlert(EsWindow *window, const char *title, ptrdiff_t titl if (!buttonArea) return nullptr; if (flags & ES_DIALOG_ALERT_OK_BUTTON) { - EsButton *button = EsButtonCreate(buttonArea, ES_BUTTON_DEFAULT, 0, "OK"); + EsButton *button = EsButtonCreate(buttonArea, ES_BUTTON_DEFAULT | ES_BUTTON_CANCEL, 0, INTERFACE_STRING(CommonOK)); EsButtonOnCommand(button, [] (EsInstance *instance, EsElement *, EsCommand *) { EsDialogClose(instance->window); }); EsElementFocus(button); } @@ -3562,10 +3577,9 @@ EsElement *EsDialogShowAlert(EsWindow *window, const char *title, ptrdiff_t titl EsElement *EsDialogShow(EsWindow *window) { // TODO Show on a separate window? - // TODO Maybe allow nested dialogs? + // TODO Support dialogs owned by other processes. EsAssert(window->windowStyle == ES_WINDOW_NORMAL); // Can only show dialogs on normal windows. - EsAssert(!window->hasDialog); // Cannot nest dialogs. if (window->focused) { window->inactiveFocus = window->focused; @@ -3575,15 +3589,20 @@ EsElement *EsDialogShow(EsWindow *window) { } EsElement *mainStack = window->children[0]; - mainStack->children[0]->state |= UI_STATE_BLOCK_INTERACTION; // Main content. - window->children[1]->state |= UI_STATE_BLOCK_INTERACTION; // Toolbar. - window->hasDialog = true; - window->dialogWrapper = EsPanelCreate(mainStack, ES_PANEL_VERTICAL | ES_CELL_FILL, ES_STYLE_DIALOG_WRAPPER); - window->dialogWrapper->cName = "dialog wrapper"; - EsPanel *dialog = EsPanelCreate(window->dialogWrapper, ES_PANEL_VERTICAL | ES_CELL_SHRINK, ES_STYLE_DIALOG_SHADOW); + if (!window->dialogs.Length()) { + mainStack->children[0]->state |= UI_STATE_BLOCK_INTERACTION; // Main content. + window->children[1]->state |= UI_STATE_BLOCK_INTERACTION; // Toolbar. + } else { + window->dialogs.Last()->state |= UI_STATE_BLOCK_INTERACTION; + } + + EsElement *wrapper = EsPanelCreate(mainStack, ES_PANEL_VERTICAL | ES_CELL_FILL, ES_STYLE_DIALOG_WRAPPER); + wrapper->cName = "dialog wrapper"; + EsPanel *dialog = EsPanelCreate(wrapper, ES_PANEL_VERTICAL | ES_CELL_SHRINK, ES_STYLE_DIALOG_SHADOW); dialog->cName = "dialog"; EsElementStartTransition(dialog, ES_TRANSITION_ZOOM_OUT_LIGHT, ES_FLAGS_DEFAULT, 1.0f); + window->dialogs.Add(dialog); return dialog; } @@ -3874,8 +3893,11 @@ int ProcessButtonMessage(EsElement *element, EsMessage *message) { if (button->window->enterButton == button) { button->customStyleState &= ~THEME_STATE_DEFAULT_BUTTON; button->window->enterButton = button->window->defaultEnterButton; - button->window->enterButton->customStyleState |= THEME_STATE_DEFAULT_BUTTON; - button->window->enterButton->MaybeRefreshStyle(); + + if (button->window->enterButton) { + button->window->enterButton->customStyleState |= THEME_STATE_DEFAULT_BUTTON; + button->window->enterButton->MaybeRefreshStyle(); + } } } else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) { EsBufferFormat(message->getContent.buffer, "'%s'", button->labelBytes, button->label); @@ -3932,7 +3954,9 @@ EsButton *EsButtonCreate(EsElement *parent, uint64_t flags, const EsStyle *style button->window->defaultEnterButton = button; button->window->enterButton = button; button->customStyleState |= THEME_STATE_DEFAULT_BUTTON; - } else if (flags & ES_BUTTON_CANCEL) { + } + + if (flags & ES_BUTTON_CANCEL) { button->window->escapeButton = button; } @@ -5330,19 +5354,25 @@ const EsStyle styleFileMenuNameTextbox = { }, }; +void InstanceRenameFromTextbox(EsWindow *window, APIInstance *instance, EsTextbox *textbox) { + size_t newNameBytes; + char *newName = EsTextboxGetContents(textbox, &newNameBytes); + uint8_t *buffer = (uint8_t *) EsHeapAllocate(1 + newNameBytes, false); + buffer[0] = DESKTOP_MSG_RENAME; + EsMemoryCopy(buffer + 1, newName, newNameBytes); + MessageDesktop(buffer, 1 + newNameBytes, window->handle); + EsHeapFree(buffer); + EsHeapFree(instance->newName); + instance->newName = newName; + instance->newNameBytes = newNameBytes; +} + int FileMenuNameTextboxMessage(EsElement *element, EsMessage *message) { if (message->type == ES_MSG_TEXTBOX_EDIT_END) { APIInstance *instance = (APIInstance *) element->instance->_private; if (!message->endEdit.rejected && !message->endEdit.unchanged) { - size_t newNameBytes; - char *newName = EsTextboxGetContents(instance->fileMenuNameTextbox, &newNameBytes); - uint8_t *buffer = (uint8_t *) EsHeapAllocate(1 + newNameBytes, false); - buffer[0] = DESKTOP_MSG_RENAME; - EsMemoryCopy(buffer + 1, newName, newNameBytes); - MessageDesktop(buffer, 1 + newNameBytes, element->instance->window->handle); - EsHeapFree(buffer); - EsHeapFree(newName); + InstanceRenameFromTextbox(element->instance->window, instance, instance->fileMenuNameTextbox); EsElementDestroy(element->window); } else { EsPanelSwitchTo(instance->fileMenuNameSwitcher, instance->fileMenuNamePanel, ES_TRANSITION_SLIDE_DOWN); @@ -5354,11 +5384,28 @@ int FileMenuNameTextboxMessage(EsElement *element, EsMessage *message) { return 0; } +void TextboxSelectSectionBeforeFileExtension(EsTextbox *textbox, const char *name, ptrdiff_t nameBytes) { + uintptr_t extensionOffset = 0; + + if (nameBytes == -1) { + nameBytes = EsCStringLength(name); + } + + for (intptr_t i = 1; i < nameBytes; i++) { + if (name[i] == '.') { + extensionOffset = i; + } + } + + if (extensionOffset) { + EsTextboxSetSelection(textbox, 0, 0, 0, extensionOffset); + } +} + void FileMenuRename(EsInstance *_instance, EsElement *, EsCommand *) { APIInstance *instance = (APIInstance *) _instance->_private; EsTextboxClear(instance->fileMenuNameTextbox, false); - uintptr_t extensionOffset = 0; const char *initialName = nullptr; ptrdiff_t initialNameBytes = 0; @@ -5376,17 +5423,10 @@ void FileMenuRename(EsInstance *_instance, EsElement *, EsCommand *) { } EsTextboxInsert(instance->fileMenuNameTextbox, initialName, initialNameBytes, false); - - for (intptr_t i = 1; i < initialNameBytes; i++) { - if (initialName[i] == '.') { - extensionOffset = i; - } - } - EsPanelSwitchTo(instance->fileMenuNameSwitcher, instance->fileMenuNameTextbox, ES_TRANSITION_SLIDE_UP); EsElementFocus(instance->fileMenuNameTextbox); EsTextboxStartEdit(instance->fileMenuNameTextbox); - if (extensionOffset) EsTextboxSetSelection(instance->fileMenuNameTextbox, 0, 0, 0, extensionOffset); + TextboxSelectSectionBeforeFileExtension(instance->fileMenuNameTextbox, initialName, initialNameBytes); instance->fileMenuNameTextbox->messageUser = FileMenuNameTextboxMessage; } @@ -6577,7 +6617,7 @@ int AccessKeyLayerMessage(EsElement *element, EsMessage *message) { } void AccessKeyModeEnter(EsWindow *window) { - if (window->hasDialog || gui.menuMode || gui.accessKeyMode || window->windowStyle != ES_WINDOW_NORMAL) { + if (window->dialogs.Length() || gui.menuMode || gui.accessKeyMode || window->windowStyle != ES_WINDOW_NORMAL) { return; } @@ -6795,7 +6835,7 @@ bool UIHandleKeyMessage(EsWindow *window, EsMessage *message) { return true; } - if (!window->hasDialog) { + if (!window->dialogs.Length()) { // TODO Sort out what commands can be used from within dialogs and menus. if (!gui.keyboardShortcutNames.itemCount) UIInitialiseKeyboardShortcutNamesTable(); diff --git a/desktop/os.header b/desktop/os.header index 4b3131a..edcae7b 100644 --- a/desktop/os.header +++ b/desktop/os.header @@ -475,15 +475,8 @@ define ES_PANEL_SWITCHER_MEASURE_SHOWN (0 << 15) // Use the shown child to det define ES_PANEL_SWITCHER_MEASURE_LARGEST (1 << 15) // Use the largest child to determine size. // For ES_PANEL_TABLE. -// TODO Implement these! -define ES_PANEL_H_LEFT (1 << 16) -define ES_PANEL_H_RIGHT (1 << 17) -define ES_PANEL_H_CENTER (1 << 18) -define ES_PANEL_H_JUSTIFY (1 << 19) -define ES_PANEL_V_TOP (1 << 20) -define ES_PANEL_V_BOTTOM (1 << 21) -define ES_PANEL_V_CENTER (1 << 22) -define ES_PANEL_V_JUSTIFY (1 << 23) +define ES_PANEL_TABLE_H_JUSTIFY (1 << 16) +define ES_PANEL_TABLE_V_JUSTIFY (1 << 17) define ES_PANEL_RADIO_GROUP (1 << 30) // Determines how arrow keys/tabs behave. diff --git a/desktop/settings.cpp b/desktop/settings.cpp index fc22c3f..dfbe5a9 100644 --- a/desktop/settings.cpp +++ b/desktop/settings.cpp @@ -85,14 +85,6 @@ const EsStyle styleSettingsNumberTextbox = { }, }; -const EsStyle styleSettingsTable = { - .metrics = { - .mask = ES_THEME_METRICS_GAP_MAJOR | ES_THEME_METRICS_GAP_MINOR, - .gapMajor = 7, - .gapMinor = 7, - }, -}; - const EsStyle styleSettingsCheckboxGroup = { .metrics = { .mask = ES_THEME_METRICS_GAP_MAJOR | ES_THEME_METRICS_GAP_MINOR, @@ -307,7 +299,7 @@ void SettingsPageUnimplemented(EsElement *element, SettingsPage *page) { EsPanel *container = EsPanelCreate(content, ES_PANEL_VERTICAL | ES_CELL_H_SHRINK, &styleSettingsGroupContainer2); SettingsAddTitle(container, page); - EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, &styleSettingsTable); + EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsIconDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, ES_ICON_DIALOG_WARNING); EsTextDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, "Work in progress" ELLIPSIS); } @@ -529,7 +521,7 @@ void SettingsPageMouse(EsElement *element, SettingsPage *page) { SettingsAddSlider(container, INTERFACE_STRING(DesktopSettingsMouseCursorTrails), 'T', "general", "cursor_trails", 0, 7, 8, INTERFACE_STRING(DesktopSettingsMouseCursorTrailsNone), INTERFACE_STRING(DesktopSettingsMouseCursorTrailsMany)); - table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, &styleSettingsTable); + table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsPanelSetBands(table, 2); SettingsAddNumberBox(table, INTERFACE_STRING(DesktopSettingsMouseLinesPerScrollNotch), 'S', "general", "scroll_lines_per_notch", @@ -542,7 +534,7 @@ void SettingsPageMouse(EsElement *element, SettingsPage *page) { EsSpacerCreate(container, ES_CELL_H_FILL, ES_STYLE_BUTTON_GROUP_SEPARATOR); - table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, &styleSettingsTable); + table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsPanelSetBands(table, 2); SettingsAddNumberBox(table, INTERFACE_STRING(DesktopSettingsMouseDoubleClickSpeed), 'D', "general", "click_chain_timeout_ms", @@ -561,11 +553,11 @@ void SettingsPageKeyboard(EsElement *element, SettingsPage *page) { EsPanel *container = EsPanelCreate(content, ES_PANEL_VERTICAL | ES_CELL_H_SHRINK, &styleSettingsGroupContainer2); SettingsAddTitle(container, page); - EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, &styleSettingsTable); + EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsIconDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, ES_ICON_DIALOG_WARNING); EsTextDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, "Work in progress" ELLIPSIS); - EsPanel *table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, &styleSettingsTable); + EsPanel *table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsPanelSetBands(table, 2); EsTextbox *textbox; @@ -606,11 +598,11 @@ void SettingsPageDisplay(EsElement *element, SettingsPage *page) { EsPanel *container = EsPanelCreate(content, ES_PANEL_VERTICAL | ES_CELL_H_SHRINK, &styleSettingsGroupContainer2); SettingsAddTitle(container, page); - EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, &styleSettingsTable); + EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsIconDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, ES_ICON_DIALOG_WARNING); EsTextDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, "Work in progress" ELLIPSIS); - EsPanel *table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, &styleSettingsTable); + EsPanel *table = EsPanelCreate(container, ES_CELL_H_FILL | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsPanelSetBands(table, 2); SettingsAddNumberBox(table, INTERFACE_STRING(DesktopSettingsDisplayUIScale), 'S', "general", "ui_scale", 100, 400, INTERFACE_STRING(CommonUnitPercent), 0.05, 5); @@ -687,11 +679,11 @@ void SettingsPageTheme(EsElement *element, SettingsPage *page) { EsPanel *container = EsPanelCreate(content, ES_PANEL_VERTICAL | ES_CELL_H_SHRINK, &styleSettingsGroupContainer2); SettingsAddTitle(container, page); - EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, &styleSettingsTable); + EsPanel *warningRow = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsIconDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, ES_ICON_DIALOG_WARNING); EsTextDisplayCreate(warningRow, ES_FLAGS_DEFAULT, 0, "Work in progress" ELLIPSIS); - EsPanel *table = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, &styleSettingsTable); + EsPanel *table = EsPanelCreate(container, ES_CELL_H_CENTER | ES_PANEL_TABLE | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_FORM_TABLE); EsPanelSetBands(table, 2); EsTextDisplayCreate(table, ES_CELL_H_RIGHT, 0, INTERFACE_STRING(DesktopSettingsThemeWallpaper)); diff --git a/desktop/styles.header b/desktop/styles.header index 0608bea..f71f235 100644 --- a/desktop/styles.header +++ b/desktop/styles.header @@ -55,6 +55,7 @@ private define ES_STYLE_PANEL_CONTAINER_WINDOW_ROOT (ES_STYLE_CAST(1307)) private define ES_STYLE_PANEL_CRASH_INFO (ES_STYLE_CAST(1309)) define ES_STYLE_PANEL_DOCUMENT (ES_STYLE_CAST(1547)) define ES_STYLE_PANEL_FILLED (ES_STYLE_CAST(1313)) +define ES_STYLE_PANEL_FORM_TABLE (ES_STYLE_CAST(1671)) define ES_STYLE_PANEL_GROUP_BOX (ES_STYLE_CAST(1315)) define ES_STYLE_PANEL_INSET (ES_STYLE_CAST(1641)) private define ES_STYLE_PANEL_INSPECTOR_WINDOW_CONTAINER (ES_STYLE_CAST(1317)) diff --git a/res/Theme Source.dat b/res/Theme Source.dat index a35ebe1..d6d4ecd 100644 Binary files a/res/Theme Source.dat and b/res/Theme Source.dat differ diff --git a/res/Themes/Theme.dat b/res/Themes/Theme.dat index 8c2faf6..6116e66 100644 Binary files a/res/Themes/Theme.dat and b/res/Themes/Theme.dat differ diff --git a/shared/strings.cpp b/shared/strings.cpp index 8e287ed..8847da6 100644 --- a/shared/strings.cpp +++ b/shared/strings.cpp @@ -5,12 +5,15 @@ #define ELLIPSIS "…" #define HYPHENATION_POINT "‧" +#define OPEN_SPEECH "\u201C" +#define CLOSE_SPEECH "\u201D" #define SYSTEM_BRAND_SHORT "Essence" // Common. DEFINE_INTERFACE_STRING(CommonErrorTitle, "Error"); +DEFINE_INTERFACE_STRING(CommonOK, "OK"); DEFINE_INTERFACE_STRING(CommonCancel, "Cancel"); DEFINE_INTERFACE_STRING(CommonUndo, "Undo"); @@ -162,7 +165,7 @@ DEFINE_INTERFACE_STRING(FileSaveErrorTooLarge, "The drive does not support files DEFINE_INTERFACE_STRING(FileSaveErrorConcurrentAccess, "Another application is modifying the file."); DEFINE_INTERFACE_STRING(FileSaveErrorDriveFull, "The drive is full. Try deleting some files to free up space."); DEFINE_INTERFACE_STRING(FileSaveErrorResourcesLow, "The system is low on resources. Close some applcations and try again."); -DEFINE_INTERFACE_STRING(FileSaveErrorAlreadyExists, "There is already a file with this name."); +DEFINE_INTERFACE_STRING(FileSaveErrorAlreadyExists, "There is already a file called " OPEN_SPEECH "%s" CLOSE_SPEECH " in this folder."); DEFINE_INTERFACE_STRING(FileSaveErrorTooManyFiles, "Too many files already have the same name."); DEFINE_INTERFACE_STRING(FileSaveErrorUnknown, "An unknown error occurred. Please try again later."); @@ -171,6 +174,14 @@ DEFINE_INTERFACE_STRING(FileLoadErrorDrive, "The drive containing the file was u DEFINE_INTERFACE_STRING(FileLoadErrorResourcesLow, "The system is low on resources. Close some applcations and try again."); DEFINE_INTERFACE_STRING(FileLoadErrorUnknown, "An unknown error occurred. Please try again later."); +DEFINE_INTERFACE_STRING(FileCloseWithModificationsTitle, "Do you want to save this document?"); +DEFINE_INTERFACE_STRING(FileCloseWithModificationsContent, "You need to save your changes to " OPEN_SPEECH "%s" CLOSE_SPEECH " before you can close it."); +DEFINE_INTERFACE_STRING(FileCloseWithModificationsSave, "Save and close"); +DEFINE_INTERFACE_STRING(FileCloseWithModificationsDelete, "Discard"); +DEFINE_INTERFACE_STRING(FileCloseNewTitle, "Do you want to keep this document?"); +DEFINE_INTERFACE_STRING(FileCloseNewContent, "You need to save it before you can close " OPEN_SPEECH "%s" CLOSE_SPEECH "."); +DEFINE_INTERFACE_STRING(FileCloseNewName, "Name:"); + // Image Editor. DEFINE_INTERFACE_STRING(ImageEditorToolBrush, "Brush");