// Urho3D editor UI-element handling UIElement@ editorUIElement; XMLFile@ uiElementDefaultStyle; Array availableStyles; UIElement@ editUIElement; Array selectedUIElements; Array editUIElements; Array uiElementCopyBuffer; bool suppressUIElementChanges = false; // Registered UIElement user variable reverse mappings VariantMap uiElementVarNames; const StringHash FILENAME_VAR("FileName"); const StringHash MODIFIED_VAR("Modified"); const StringHash CHILD_ELEMENT_FILENAME_VAR("ChildElemFileName"); void ClearUIElementSelection() { editUIElement = null; selectedUIElements.Clear(); editUIElements.Clear(); } void CreateRootUIElement() { // Create a root UIElement only once here, do not confuse this with ui.root itself editorUIElement = ui.root.CreateChild("UIElement"); editorUIElement.name = "UI"; editorUIElement.SetSize(graphics.width, graphics.height); editorUIElement.traversalMode = TM_DEPTH_FIRST; // This is needed for root-like element to prevent artifacts editorUIElement.priority = -1000; // All user-created UI elements have lowest priority so they do not cover editor's windows // This is needed to distinguish our own element events from Editor's UI element events editorUIElement.elementEventSender = true; SubscribeToEvent(editorUIElement, "ElementAdded", "HandleUIElementAdded"); SubscribeToEvent(editorUIElement, "ElementRemoved", "HandleUIElementRemoved"); // Since this root UIElement is not being handled by above handlers, update it into hierarchy list manually as another list root item UpdateHierarchyItem(M_MAX_UNSIGNED, editorUIElement, null); } bool NewUIElement(const String&in typeName) { // If no edit element then parented to root UIElement@ parent = editUIElement !is null ? editUIElement : editorUIElement; UIElement@ element = parent.CreateChild(typeName); if (element !is null) { // Use the predefined UI style if set, otherwise use editor's own UI style XMLFile@ defaultStyle = uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle; if (editUIElement is null) { // If parented to root, set the internal variables element.vars[FILENAME_VAR] = ""; element.vars[MODIFIED_VAR] = false; // set a default UI style element.defaultStyle = defaultStyle; // and position the newly created element at center CenterDialog(element); } // Apply the auto style element.style = AUTO_STYLE; // Do not allow UI subsystem to reorder children while editing the element in the editor element.sortChildren = false; // Create an undo action for the create CreateUIElementAction action; action.Define(element); SaveEditAction(action); SetUIElementModified(element); FocusUIElement(element); } return true; } void ResetSortChildren(UIElement@ element) { element.sortChildren = false; // Perform the action recursively for child elements for (uint i = 0; i < element.numChildren; ++i) ResetSortChildren(element.children[i]); } void OpenUILayout(const String&in fileName) { if (fileName.empty) return; ui.cursor.shape = CS_BUSY; // Check if the UI element has been opened before if (editorUIElement.GetChild(FILENAME_VAR, Variant(fileName)) !is null) { MessageBox("UI element is already opened.\n" + fileName); return; } // Always load from the filesystem, not from resource paths if (!fileSystem.FileExists(fileName)) { MessageBox("No such file.\n" + fileName); return; } File file(fileName, FILE_READ); if (!file.open) { MessageBox("Could not open file.\n" + fileName); return; } // Add the UI layout's resource path in case it's necessary SetResourcePath(GetPath(fileName), true, true); XMLFile@ xmlFile = XMLFile(); xmlFile.Load(file); suppressUIElementChanges = true; // If uiElementDefaultStyle is not set then automatically fallback to use the editor's own default style UIElement@ element = ui.LoadLayout(xmlFile, uiElementDefaultStyle); if (element !is null) { element.vars[FILENAME_VAR] = fileName; element.vars[MODIFIED_VAR] = false; // Do not allow UI subsystem to reorder children while editing the element in the editor ResetSortChildren(element); // Register variable names from the 'enriched' XMLElement, if any RegisterUIElementVar(xmlFile.root); editorUIElement.AddChild(element); UpdateHierarchyItem(element); FocusUIElement(element); ClearEditActions(); } else MessageBox("Could not load UI layout successfully!\nSee Urho3D.log for more detail."); suppressUIElementChanges = false; } bool CloseUILayout() { ui.cursor.shape = CS_BUSY; if (messageBoxCallback is null) { for (uint i = 0; i < selectedUIElements.length; ++i) { UIElement@ element = GetTopLevelUIElement(selectedUIElements[i]); if (element !is null && element.vars[MODIFIED_VAR].GetBool()) { MessageBox@ messageBox = MessageBox("UI layout has been modified.\nContinue to close?", "Warning"); if (messageBox.window !is null) { Button@ cancelButton = messageBox.window.GetChild("CancelButton", true); cancelButton.visible = true; cancelButton.focus = true; SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement"); messageBoxCallback = @CloseUILayout; return false; } } } } else messageBoxCallback = null; suppressUIElementChanges = true; for (uint i = 0; i < selectedUIElements.length; ++i) { UIElement@ element = GetTopLevelUIElement(selectedUIElements[i]); if (element !is null) { element.Remove(); UpdateHierarchyItem(GetListIndex(element), null, null); } } hierarchyList.ClearSelection(); ClearEditActions(); suppressUIElementChanges = false; return true; } bool CloseAllUILayouts() { ui.cursor.shape = CS_BUSY; if (messageBoxCallback is null) { for (uint i = 0; i < editorUIElement.numChildren; ++i) { UIElement@ element = editorUIElement.children[i]; if (element !is null && element.vars[MODIFIED_VAR].GetBool()) { MessageBox@ messageBox = MessageBox("UI layout has been modified.\nContinue to close?", "Warning"); if (messageBox.window !is null) { Button@ cancelButton = messageBox.window.GetChild("CancelButton", true); cancelButton.visible = true; cancelButton.focus = true; SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement"); messageBoxCallback = @CloseAllUILayouts; return false; } } } } else messageBoxCallback = null; suppressUIElementChanges = true; editorUIElement.RemoveAllChildren(); UpdateHierarchyItem(editorUIElement, true); // Reset element ID number generator uiElementNextID = UI_ELEMENT_BASE_ID + 1; hierarchyList.ClearSelection(); ClearEditActions(); suppressUIElementChanges = false; return true; } bool SaveUILayout(const String&in fileName) { if (fileName.empty) return false; ui.cursor.shape = CS_BUSY; MakeBackup(fileName); File file(fileName, FILE_WRITE); if (!file.open) { MessageBox("Could not open file.\n" + fileName); return false; } UIElement@ element = GetTopLevelUIElement(editUIElement); if (element is null) return false; XMLFile@ elementData = XMLFile(); XMLElement rootElem = elementData.CreateRoot("element"); bool success = element.SaveXML(rootElem); RemoveBackup(success, fileName); if (success) { FilterInternalVars(rootElem); success = elementData.Save(file); if (success) { element.vars[FILENAME_VAR] = fileName; SetUIElementModified(element, false); } } if (!success) MessageBox("Could not save UI layout successfully!\nSee Urho3D.log for more detail."); return success; } bool SaveUILayoutWithExistingName() { ui.cursor.shape = CS_BUSY; UIElement@ element = GetTopLevelUIElement(editUIElement); if (element is null) return false; String fileName = element.GetVar(FILENAME_VAR).GetString(); if (fileName.empty) return PickFile(); // No name yet, so pick one else return SaveUILayout(fileName); } void LoadChildUIElement(const String&in fileName) { if (fileName.empty) return; ui.cursor.shape = CS_BUSY; if (!fileSystem.FileExists(fileName)) { MessageBox("No such file.\n" + fileName); return; } File file(fileName, FILE_READ); if (!file.open) { MessageBox("Could not open file.\n" + fileName); return; } XMLFile@ xmlFile = XMLFile(); xmlFile.Load(file); suppressUIElementChanges = true; if (editUIElement.LoadChildXML(xmlFile, uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle)) { XMLElement rootElem = xmlFile.root; uint index = rootElem.HasAttribute("index") ? rootElem.GetUInt("index") : editUIElement.numChildren - 1; UIElement@ element = editUIElement.children[index]; ResetSortChildren(element); RegisterUIElementVar(xmlFile.root); element.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName; if (index == editUIElement.numChildren - 1) UpdateHierarchyItem(element); else // If not last child, find the list index of the next sibling as the insertion index UpdateHierarchyItem(GetListIndex(editUIElement.children[index + 1]), element, hierarchyList.items[GetListIndex(editUIElement)]); SetUIElementModified(element); // Create an undo action for the load CreateUIElementAction action; action.Define(element); SaveEditAction(action); FocusUIElement(element); } suppressUIElementChanges = false; } bool SaveChildUIElement(const String&in fileName) { if (fileName.empty) return false; ui.cursor.shape = CS_BUSY; MakeBackup(fileName); File file(fileName, FILE_WRITE); if (!file.open) { MessageBox("Could not open file.\n" + fileName); return false; } XMLFile@ elementData = XMLFile(); XMLElement rootElem = elementData.CreateRoot("element"); bool success = editUIElement.SaveXML(rootElem); RemoveBackup(success, fileName); if (success) { FilterInternalVars(rootElem); success = elementData.Save(file); if (success) editUIElement.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName; } if (!success) MessageBox("Could not save child UI element successfully!\nSee Urho3D.log for more detail."); return success; } void SetUIElementDefaultStyle(const String&in fileName) { if (fileName.empty) return; ui.cursor.shape = CS_BUSY; // Always load from the filesystem, not from resource paths if (!fileSystem.FileExists(fileName)) { MessageBox("No such file.\n" + fileName); return; } File file(fileName, FILE_READ); if (!file.open) { MessageBox("Could not open file.\n" + fileName); return; } uiElementDefaultStyle = XMLFile(); uiElementDefaultStyle.Load(file); // Remove the existing style list to ensure it gets repopulated again with the new default style file availableStyles.Clear(); // Refresh Attribute Inspector when it is currently showing attributes of UI-element item type as the existing styles in the style drop down list are not valid anymore if (!editUIElements.empty) attributesFullDirty = true; } // Prepare XPath query object only once and use it multiple times XPathQuery filterInternalVarsQuery("//attribute[@name='Variables']/variant"); void FilterInternalVars(XMLElement source) { XPathResultSet resultSet = filterInternalVarsQuery.Evaluate(source); XMLElement resultElem = resultSet.firstResult; while (resultElem.notNull) { String name = GetVariableName(resultElem.GetUInt("hash")); if (name.empty) { XMLElement parent = resultElem.parent; // If variable name is empty (or unregistered) then it is an internal variable and should be removed if (parent.RemoveChild(resultElem)) { // If parent does not have any children anymore then remove the parent also if (!parent.HasChild("variant")) parent.parent.RemoveChild(parent); } } else // If it is registered then it is a user-defined variable, so 'enrich' the XMLElement to store the variable name in plaintext resultElem.SetAttribute("name", name); resultElem = resultElem.nextResult; } } XPathQuery registerUIElemenVarsQuery("//attribute[@name='Variables']/variant/@name"); void RegisterUIElementVar(XMLElement source) { XPathResultSet resultSet = registerUIElemenVarsQuery.Evaluate(source); XMLElement resultAttr = resultSet.firstResult; // Since we are selecting attribute, the resultset is in attribute context while (resultAttr.notNull) { String name = resultAttr.GetAttribute(); uiElementVarNames[name] = name; resultAttr = resultAttr.nextResult; } } UIElement@ GetTopLevelUIElement(UIElement@ element) { // Only top level UI-element contains the FILENAME_VAR while (element !is null && !element.vars.Contains(FILENAME_VAR)) element = element.parent; return element; } void SetUIElementModified(UIElement@ element, bool flag = true) { element = GetTopLevelUIElement(element); if (element !is null && element.GetVar(MODIFIED_VAR).GetBool() != flag) { element.vars[MODIFIED_VAR] = flag; UpdateHierarchyItemText(GetListIndex(element), element.visible, GetUIElementTitle(element)); } } XPathQuery availableStylesXPathQuery("/elements/element[@auto='false']/@type"); void GetAvailableStyles() { // Use the predefined UI style if set, otherwise use editor's own UI style XMLFile@ defaultStyle = uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle; XMLElement rootElem = defaultStyle.root; XPathResultSet resultSet = availableStylesXPathQuery.Evaluate(rootElem); XMLElement resultElem = resultSet.firstResult; while (resultElem.notNull) { availableStyles.Push(resultElem.GetAttribute()); resultElem = resultElem.nextResult; } availableStyles.Sort(); } void PopulateStyleList(DropDownList@ styleList) { if (availableStyles.empty) GetAvailableStyles(); for (uint i = 0; i < availableStyles.length; ++i) { Text@ choice = Text(); styleList.AddItem(choice); choice.style = "EditorEnumAttributeText"; choice.text = availableStyles[i]; } } bool UIElementCut() { return UIElementCopy() && UIElementDelete(); } bool UIElementCopy() { ui.cursor.shape = CS_BUSY; uiElementCopyBuffer.Clear(); for (uint i = 0; i < selectedUIElements.length; ++i) { XMLFile@ xml = XMLFile(); XMLElement rootElem = xml.CreateRoot("element"); selectedUIElements[i].SaveXML(rootElem); uiElementCopyBuffer.Push(xml); } return true; } void ResetDuplicateID(UIElement@ element) { // If it is a duplicate copy then the element ID need to be regenerated by resetting it now to empty if (GetListIndex(element) != NO_ITEM) element.vars[UI_ELEMENT_ID_VAR] = Variant(); // Perform the action recursively for child elements for (uint i = 0; i < element.numChildren; ++i) ResetDuplicateID(element.children[i]); } bool UIElementPaste() { ui.cursor.shape = CS_BUSY; // Group for storing undo actions EditActionGroup group; // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent suppressUIElementChanges = true; for (uint i = 0; i < uiElementCopyBuffer.length; ++i) { XMLElement rootElem = uiElementCopyBuffer[i].root; if (editUIElement.LoadChildXML(rootElem, null)) { UIElement@ element = editUIElement.children[editUIElement.numChildren - 1]; ResetDuplicateID(element); UpdateHierarchyItem(element); SetUIElementModified(editUIElement); // Create an undo action CreateUIElementAction action; action.Define(element); group.actions.Push(action); } } SaveEditActionGroup(group); suppressUIElementChanges = false; return true; } bool UIElementDelete() { ui.cursor.shape = CS_BUSY; BeginSelectionModify(); // Clear the selection now to prevent deleted elements from being reselected hierarchyList.ClearSelection(); // Group for storing undo actions EditActionGroup group; for (uint i = 0; i < selectedUIElements.length; ++i) { UIElement@ element = selectedUIElements[i]; if (element.parent is null) continue; // Already deleted uint index = GetListIndex(element); // Create undo action DeleteUIElementAction action; action.Define(element); group.actions.Push(action); SetUIElementModified(element); element.Remove(); // If deleting only one element, select the next item in the same index if (selectedUIElements.length == 1) hierarchyList.selection = index; } SaveEditActionGroup(group); EndSelectionModify(); return true; } bool UIElementSelectAll() { BeginSelectionModify(); Array indices; uint baseIndex = GetListIndex(editorUIElement); indices.Push(baseIndex); int baseIndent = hierarchyList.items[baseIndex].indent; for (uint i = baseIndex + 1; i < hierarchyList.numItems; ++i) { if (hierarchyList.items[i].indent <= baseIndent) break; indices.Push(i); } hierarchyList.SetSelections(indices); EndSelectionModify(); return true; } bool UIElementResetToDefault() { ui.cursor.shape = CS_BUSY; // Group for storing undo actions EditActionGroup group; // Reset selected elements to their default values for (uint i = 0; i < selectedUIElements.length; ++i) { UIElement@ element = selectedUIElements[i]; ResetAttributesAction action; action.Define(element); group.actions.Push(action); element.ResetToDefault(); action.SetInternalVars(element); element.ApplyAttributes(); for (uint j = 0; j < element.numAttributes; ++j) PostEditAttribute(element, j); SetUIElementModified(element); } SaveEditActionGroup(group); attributesFullDirty = true; return true; } bool UIElementChangeParent(UIElement@ sourceElement, UIElement@ targetElement) { ReparentUIElementAction action; action.Define(sourceElement, targetElement); SaveEditAction(action); sourceElement.parent = targetElement; SetUIElementModified(targetElement); return sourceElement.parent is targetElement; }