Ver código fonte

Optionally remember last resource path in editor.
Remember last import path in editor.
Set node / scene / UI element picker paths from remembered resource path if possible.

Lasse Öörni 12 anos atrás
pai
commit
cf50bb9f39

+ 23 - 0
Bin/Data/Scripts/Editor.as

@@ -110,6 +110,7 @@ void LoadConfig()
     XMLElement uiElem = configElem.GetChild("ui");
     XMLElement hierarchyElem = configElem.GetChild("hierarchy");
     XMLElement inspectorElem = configElem.GetChild("attributeinspector");
+    XMLElement resourcesElem = configElem.GetChild("resources");
 
     if (!cameraElem.isNull)
     {
@@ -134,6 +135,23 @@ void LoadConfig()
         if (objectElem.HasAttribute("pickmode")) pickMode = objectElem.GetInt("pickmode");
     }
 
+    if (!resourcesElem.isNull)
+    {
+        if (resourcesElem.HasAttribute("rememberresourcepath")) rememberResourcePath = resourcesElem.GetBool("rememberresourcepath");
+        if (rememberResourcePath && resourcesElem.HasAttribute("resourcepath"))
+        {
+            String newResourcePath = resourcesElem.GetAttribute("resourcepath");
+            if (fileSystem.DirExists(newResourcePath))
+                SetResourcePath(resourcesElem.GetAttribute("resourcepath"), false);
+        }
+        if (resourcesElem.HasAttribute("importpath"))
+        {
+            String newImportPath = resourcesElem.GetAttribute("importpath");
+            if (fileSystem.DirExists(newImportPath))
+                uiImportPath = newImportPath;
+        }
+    }
+
     if (!renderingElem.isNull)
     {
         if (renderingElem.HasAttribute("texturequality")) renderer.textureQuality = renderingElem.GetInt("texturequality");
@@ -181,6 +199,7 @@ void SaveConfig()
     XMLElement uiElem = configElem.CreateChild("ui");
     XMLElement hierarchyElem = configElem.CreateChild("hierarchy");
     XMLElement inspectorElem = configElem.CreateChild("attributeinspector");
+    XMLElement resourcesElem = configElem.CreateChild("resources");
 
     // The save config may be called on error exit so some of the objects below could still be null
     if (camera !is null)
@@ -203,6 +222,10 @@ void SaveConfig()
     objectElem.SetAttribute("importoptions", importOptions);
     objectElem.SetInt("pickmode", pickMode);
 
+    resourcesElem.SetBool("rememberresourcepath", rememberResourcePath);
+    resourcesElem.SetAttribute("resourcepath", sceneResourcePath);
+    resourcesElem.SetAttribute("importpath", uiImportPath);
+
     if (renderer !is null)
     {
         renderingElem.SetInt("texturequality", renderer.textureQuality);

+ 2 - 2
Bin/Data/Scripts/Editor/AttributeEditor.as

@@ -21,7 +21,8 @@ Color normalTextColor(1.0f, 1.0f, 1.0f);
 Color modifiedTextColor(1.0f, 0.8f, 0.5f);
 Color nonEditableTextColor(0.7f, 0.7f, 0.7f);
 
-String sceneResourcePath;
+String sceneResourcePath = AddTrailingSlash(fileSystem.programDir + "Data");
+bool rememberResourcePath = true;
 
 WeakHandle testAnimState;
 
@@ -878,7 +879,6 @@ void InitResourcePicker()
     resourcePickers.Push(ResourcePicker("ScriptFile", scriptFilters));
     resourcePickers.Push(ResourcePicker("XMLFile", "*.xml"));
     resourcePickers.Push(ResourcePicker("Sound", soundFilters));
-    sceneResourcePath = AddTrailingSlash(fileSystem.programDir + "Data");
 }
 
 ResourcePicker@ GetResourcePicker(ShortStringHash resourceType)

+ 32 - 12
Bin/Data/Scripts/Editor/EditorScene.as

@@ -87,7 +87,7 @@ bool ResetScene()
     return true;
 }
 
-void SetResourcePath(String newPath, bool usePreferredDir = true)
+void SetResourcePath(String newPath, bool usePreferredDir = true, bool additive = false)
 {
     if (newPath.empty)
         return;
@@ -100,15 +100,34 @@ void SetResourcePath(String newPath, bool usePreferredDir = true)
     if (newPath == sceneResourcePath)
         return;
 
-    cache.ReleaseAllResources(false);
-    renderer.ReloadShaders();
-
     // Remove the old scene resource path if any. However make sure that the default data paths do not get removed
-    if (!sceneResourcePath.empty && !sceneResourcePath.Contains(fileSystem.programDir))
-        cache.RemoveResourceDir(sceneResourcePath);
+    if (!additive)
+    {
+        cache.ReleaseAllResources(false);
+        renderer.ReloadShaders();
+
+        if (!sceneResourcePath.empty && !sceneResourcePath.Contains(fileSystem.programDir))
+            cache.RemoveResourceDir(sceneResourcePath);
+    }
 
     cache.AddResourceDir(newPath);
-    sceneResourcePath = newPath;
+
+    if (!additive)
+    {
+        sceneResourcePath = newPath;
+        SetResourceSubPath(uiScenePath, newPath, "Scenes");
+        SetResourceSubPath(uiElementPath, newPath, "UI");
+        SetResourceSubPath(uiNodePath, newPath, "Objects");
+        SetResourceSubPath(uiScriptPath, newPath, "Scripts");
+    }
+}
+
+void SetResourceSubPath(String& outPath, const String&in basePath, const String&in subPath)
+{
+    if (fileSystem.DirExists(basePath + "/" + subPath))
+        outPath = AddTrailingSlash(basePath + "/" + subPath);
+    else
+        outPath = AddTrailingSlash(basePath);
 }
 
 bool LoadScene(const String&in fileName)
@@ -129,8 +148,10 @@ bool LoadScene(const String&in fileName)
     if (!file.open)
         return false;
 
-    // Add the new resource path
-    SetResourcePath(GetPath(fileName));
+    // Add the scene's resource path in case it's necessary
+    String newScenePath = GetPath(fileName);
+    if (!rememberResourcePath || !sceneResourcePath.StartsWith(newScenePath, false))
+        SetResourcePath(newScenePath);
 
     suppressSceneChanges = true;
 
@@ -257,9 +278,8 @@ void LoadNode(const String&in fileName)
 
     ui.cursor.shape = CS_BUSY;
 
-    // Before instantiating, set resource path if empty
-    if (sceneResourcePath.empty)
-        SetResourcePath(GetPath(fileName));
+    // Before instantiating, add object's resource path if necessary
+    SetResourcePath(GetPath(fileName), true, true);
 
     Vector3 position = GetNewNodePosition();
     Node@ newNode;

+ 10 - 0
Bin/Data/Scripts/Editor/EditorSettings.as

@@ -57,6 +57,9 @@ void UpdateEditorSettingsDialog()
     CheckBox@ applyMaterialListToggle = settingsDialog.GetChild("ApplyMaterialListToggle", true);
     applyMaterialListToggle.checked = applyMaterialList;
 
+    CheckBox@ rememberResourcePathToggle = settingsDialog.GetChild("RememberResourcePathToggle", true);
+    rememberResourcePathToggle.checked = rememberResourcePath;
+
     LineEdit@ importOptionsEdit = settingsDialog.GetChild("ImportOptionsEdit", true);
     importOptionsEdit.text = importOptions;
 
@@ -109,6 +112,7 @@ void UpdateEditorSettingsDialog()
         SubscribeToEvent(rotateSnapToggle, "Toggled", "EditRotateSnap");
         SubscribeToEvent(scaleSnapToggle, "Toggled", "EditScaleSnap");
         SubscribeToEvent(localIDToggle, "Toggled", "EditUseLocalIDs");
+        SubscribeToEvent(rememberResourcePathToggle, "Toggled", "EditRememberResourcePath");
         SubscribeToEvent(applyMaterialListToggle, "Toggled", "EditApplyMaterialList");
         SubscribeToEvent(importOptionsEdit, "TextChanged", "EditImportOptions");
         SubscribeToEvent(importOptionsEdit, "TextFinished", "EditImportOptions");
@@ -228,6 +232,12 @@ void EditUseLocalIDs(StringHash eventType, VariantMap& eventData)
     useLocalIDs = edit.checked;
 }
 
+void EditRememberResourcePath(StringHash eventType, VariantMap& eventData)
+{
+    CheckBox@ edit = eventData["Element"].GetUIElement();
+    rememberResourcePath = edit.checked;
+}
+
 void EditApplyMaterialList(StringHash eventType, VariantMap& eventData)
 {
     CheckBox@ edit = eventData["Element"].GetUIElement();

+ 596 - 596
Bin/Data/Scripts/Editor/EditorUIElement.as

@@ -1,596 +1,596 @@
-// Urho3D editor UI-element handling
-
-UIElement@ editorUIElement;
-XMLFile@ uiElementDefaultStyle;
-Array<String> availableStyles;
-
-UIElement@ editUIElement;
-Array<Serializable@> selectedUIElements;
-Array<Serializable@> editUIElements;
-
-Array<XMLFile@> uiElementCopyBuffer;
-
-bool suppressUIElementChanges = false;
-
-// Registered UIElement user variable reverse mappings
-VariantMap uiElementVarNames;
-
-const ShortStringHash FILENAME_VAR("FileName");
-const ShortStringHash MODIFIED_VAR("Modified");
-const ShortStringHash 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
-
-    // 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)
-    {
-        log.Warning("UI element is already opened: " + fileName);
-        return;
-    }
-
-    // Always load from the filesystem, not from resource paths
-    if (!fileSystem.FileExists(fileName))
-    {
-        log.Error("No such file: " + fileName);
-        return;
-    }
-
-    File file(fileName, FILE_READ);
-    if (!file.open)
-        return;
-
-    // Add the new resource path
-    SetResourcePath(GetPath(fileName));
-
-    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();
-    }
-
-    suppressUIElementChanges = false;
-}
-
-bool CloseUILayout()
-{
-    ui.cursor.shape = CS_BUSY;
-
-    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;
-
-    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;
-
-    File file(fileName, FILE_WRITE);
-    if (!file.open)
-        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);
-    if (success)
-    {
-        FilterInternalVars(rootElem);
-        success = elementData.Save(file);
-        if (success)
-        {
-            element.vars[FILENAME_VAR] = fileName;
-            SetUIElementModified(element, false);
-        }
-    }
-
-    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))
-    {
-        log.Error("No such file: " + fileName);
-        return;
-    }
-
-    File file(fileName, FILE_READ);
-    if (!file.open)
-        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;
-
-    File file(fileName, FILE_WRITE);
-    if (!file.open)
-        return false;
-
-    XMLFile@ elementData = XMLFile();
-    XMLElement rootElem = elementData.CreateRoot("element");
-    bool success = editUIElement.SaveXML(rootElem);
-    if (success)
-    {
-        FilterInternalVars(rootElem);
-        success = elementData.Save(file);
-        if (success)
-            editUIElement.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
-    }
-
-    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))
-    {
-        log.Error("No such file: " + fileName);
-        return;
-    }
-
-    File file(fileName, FILE_READ);
-    if (!file.open)
-        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<uint> 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;
-}
+// Urho3D editor UI-element handling
+
+UIElement@ editorUIElement;
+XMLFile@ uiElementDefaultStyle;
+Array<String> availableStyles;
+
+UIElement@ editUIElement;
+Array<Serializable@> selectedUIElements;
+Array<Serializable@> editUIElements;
+
+Array<XMLFile@> uiElementCopyBuffer;
+
+bool suppressUIElementChanges = false;
+
+// Registered UIElement user variable reverse mappings
+VariantMap uiElementVarNames;
+
+const ShortStringHash FILENAME_VAR("FileName");
+const ShortStringHash MODIFIED_VAR("Modified");
+const ShortStringHash 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
+
+    // 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)
+    {
+        log.Warning("UI element is already opened: " + fileName);
+        return;
+    }
+
+    // Always load from the filesystem, not from resource paths
+    if (!fileSystem.FileExists(fileName))
+    {
+        log.Error("No such file: " + fileName);
+        return;
+    }
+
+    File file(fileName, FILE_READ);
+    if (!file.open)
+        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();
+    }
+
+    suppressUIElementChanges = false;
+}
+
+bool CloseUILayout()
+{
+    ui.cursor.shape = CS_BUSY;
+
+    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;
+
+    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;
+
+    File file(fileName, FILE_WRITE);
+    if (!file.open)
+        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);
+    if (success)
+    {
+        FilterInternalVars(rootElem);
+        success = elementData.Save(file);
+        if (success)
+        {
+            element.vars[FILENAME_VAR] = fileName;
+            SetUIElementModified(element, false);
+        }
+    }
+
+    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))
+    {
+        log.Error("No such file: " + fileName);
+        return;
+    }
+
+    File file(fileName, FILE_READ);
+    if (!file.open)
+        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;
+
+    File file(fileName, FILE_WRITE);
+    if (!file.open)
+        return false;
+
+    XMLFile@ elementData = XMLFile();
+    XMLElement rootElem = elementData.CreateRoot("element");
+    bool success = editUIElement.SaveXML(rootElem);
+    if (success)
+    {
+        FilterInternalVars(rootElem);
+        success = elementData.Save(file);
+        if (success)
+            editUIElement.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
+    }
+
+    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))
+    {
+        log.Error("No such file: " + fileName);
+        return;
+    }
+
+    File file(fileName, FILE_READ);
+    if (!file.open)
+        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<uint> 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;
+}

+ 18 - 6
Bin/Data/UI/EditorSettingsDialog.xml

@@ -194,6 +194,20 @@
             </element>
         </element>
     </element>
+    <element>
+        <attribute name="Min Size" value="0 17" />
+        <attribute name="Max Size" value="2147483647 17" />
+        <attribute name="Layout Mode" value="Horizontal" />
+        <attribute name="Layout Spacing" value="8" />
+        <element type="Text">
+            <attribute name="Text" value="AssetImporter options" />
+        </element>
+        <element type="LineEdit">
+            <attribute name="Name" value="ImportOptionsEdit" />
+            <attribute name="Min Size" value="100 0" />
+            <attribute name="Max Size" value="100 2147483647" />
+        </element>
+    </element>
     <element>
         <attribute name="Min Size" value="0 17" />
         <attribute name="Max Size" value="2147483647 17" />
@@ -223,13 +237,11 @@
         <attribute name="Max Size" value="2147483647 17" />
         <attribute name="Layout Mode" value="Horizontal" />
         <attribute name="Layout Spacing" value="8" />
-        <element type="Text">
-            <attribute name="Text" value="AssetImporter options" />
+        <element type="CheckBox">
+            <attribute name="Name" value="RememberResourcePathToggle" />
         </element>
-        <element type="LineEdit">
-            <attribute name="Name" value="ImportOptionsEdit" />
-            <attribute name="Min Size" value="100 0" />
-            <attribute name="Max Size" value="100 2147483647" />
+        <element type="Text">
+            <attribute name="Text" value="Remember resource path" />
         </element>
     </element>
     <element type="BorderImage" style="EditorDivider" />

+ 3 - 1
Docs/GettingStarted.dox

@@ -424,10 +424,12 @@ Press right mouse button in the 3D view if you want to defocus the active window
 
 When you start with an empty scene, set the resource path first (%File -> %Set resource path). This is the base directory, under which the subdirectories Models, Materials & Textures will be created as you import assets.
 
-Scenes should be saved either into this base directory, or into its immediate subdirectory, named for example Scenes or Levels. When loading a scene, the resource path will be set automatically.
+Scenes should be saved either into this base directory, or into its immediate subdirectory, named for example Scenes or Levels.
 
 Check the Editor settings window so that the camera parameters match the size of the objects you are using.
 
+The "Remember resource path" option in the settings window controls whether the resource path you set will be remembered on the next run.
+
 The editor settings will be saved on exit to a file Urho3D\Editor\Config.xml in the My Documents directory. Delete this file if you want to revert the settings to defaults.
 
 \section EditorInstructions_Editing Editing

+ 1 - 1
Docs/Urho3D.dox

@@ -51,6 +51,7 @@ For release history and major changes, see \ref History.
 Urho3D development, contributions and bugfixes by:
 - Lasse &Ouml;&ouml;rni ([email protected], AgentC at GameDev.net)
 - Wei Tjong Yao
+- Aster Jian
 - Colin Barrett
 - Erik Beran
 - Carlo Carollo
@@ -58,7 +59,6 @@ Urho3D development, contributions and bugfixes by:
 - Chris Friesen
 - Alex Fuller
 - Mika Heinonen
-- Aster Jian
 - Jason Kinzer
 - Pete Leigh
 - Paul Noome

+ 1 - 1
Readme.txt

@@ -12,6 +12,7 @@ Credits
 Urho3D development, contributions and bugfixes by:
 - Lasse Öörni ([email protected], AgentC at GameDev.net)
 - Wei Tjong Yao
+- Aster Jian
 - Colin Barrett
 - Erik Beran
 - Carlo Carollo
@@ -19,7 +20,6 @@ Urho3D development, contributions and bugfixes by:
 - Chris Friesen
 - Alex Fuller
 - Mika Heinonen
-- Aster Jian
 - Jason Kinzer
 - Pete Leigh
 - Paul Noome