浏览代码

Editor multi-edit & multi-delete.
Reversed node selection logic: select nodes by default, and components when Shift is held down. This is to avoid mistakenly deleting components and leaving nodes intact when intending to do a node multi-delete.

Lasse Öörni 14 年之前
父节点
当前提交
4dd0c17651

+ 14 - 14
Bin/Data/Scripts/Editor/EditorCamera.as

@@ -455,7 +455,7 @@ void MoveCamera(float timeStep)
     }
     }
 
 
     // Move/rotate/scale object
     // Move/rotate/scale object
-    if (selectedNode !is null && ui.focusElement is null && input.keyDown[KEY_LCTRL])
+    if (editNode !is null && ui.focusElement is null && input.keyDown[KEY_LCTRL])
     {
     {
         bool changed = false;
         bool changed = false;
         Vector3 adjust(0, 0, 0);
         Vector3 adjust(0, 0, 0);
@@ -492,8 +492,8 @@ void MoveCamera(float timeStep)
                 if (!moveSnap)
                 if (!moveSnap)
                 {
                 {
                     if (axisMode == AXIS_LOCAL)
                     if (axisMode == AXIS_LOCAL)
-                        adjust = selectedNode.rotation * adjust;
-                    selectedNode.position = selectedNode.position + adjust * moveStep;
+                        adjust = editNode.rotation * adjust;
+                    editNode.position = editNode.position + adjust * moveStep;
                     changed = true;
                     changed = true;
                 }
                 }
                 break;
                 break;
@@ -501,11 +501,11 @@ void MoveCamera(float timeStep)
             case OBJ_ROTATE:
             case OBJ_ROTATE:
                 if (!rotateSnap)
                 if (!rotateSnap)
                 {
                 {
-                    Vector3 euler = selectedNode.rotation.eulerAngles;
+                    Vector3 euler = editNode.rotation.eulerAngles;
                     euler.x += adjust.z * rotateStep;
                     euler.x += adjust.z * rotateStep;
                     euler.y += adjust.x * rotateStep;
                     euler.y += adjust.x * rotateStep;
                     euler.z += adjust.y * rotateStep;
                     euler.z += adjust.y * rotateStep;
-                    selectedNode.rotation = Quaternion(euler);
+                    editNode.rotation = Quaternion(euler);
                     changed = true;
                     changed = true;
                 }
                 }
                 break;
                 break;
@@ -513,7 +513,7 @@ void MoveCamera(float timeStep)
             case OBJ_SCALE:
             case OBJ_SCALE:
                 if (!scaleSnap)
                 if (!scaleSnap)
                 {
                 {
-                    selectedNode.scale = selectedNode.scale + adjust * scaleStep;
+                    editNode.scale = editNode.scale + adjust * scaleStep;
                     changed = true;
                     changed = true;
                 }
                 }
                 break;
                 break;
@@ -527,7 +527,7 @@ void MoveCamera(float timeStep)
 
 
 void SteppedObjectManipulation(int key)
 void SteppedObjectManipulation(int key)
 {
 {
-    if (selectedNode is null)
+    if (editNode is null)
         return;
         return;
 
 
     // Do not react in non-snapped mode, because that is handled in frame update
     // Do not react in non-snapped mode, because that is handled in frame update
@@ -567,9 +567,9 @@ void SteppedObjectManipulation(int key)
     case OBJ_MOVE:
     case OBJ_MOVE:
         {
         {
             if (axisMode == AXIS_LOCAL)
             if (axisMode == AXIS_LOCAL)
-                adjust = selectedNode.rotation * adjust;
+                adjust = editNode.rotation * adjust;
 
 
-            Vector3 pos = selectedNode.position;
+            Vector3 pos = editNode.position;
             if (adjust.x != 0)
             if (adjust.x != 0)
             {
             {
                 pos.x += adjust.x * moveStep;
                 pos.x += adjust.x * moveStep;
@@ -585,13 +585,13 @@ void SteppedObjectManipulation(int key)
                 pos.z += adjust.z * moveStep;
                 pos.z += adjust.z * moveStep;
                 pos.z = Floor(pos.z / moveStep + 0.5) * moveStep;
                 pos.z = Floor(pos.z / moveStep + 0.5) * moveStep;
             }
             }
-            selectedNode.position = pos;
+            editNode.position = pos;
         }
         }
         break;
         break;
 
 
     case OBJ_ROTATE:
     case OBJ_ROTATE:
         {
         {
-            Vector3 rot = selectedNode.rotation.eulerAngles;
+            Vector3 rot = editNode.rotation.eulerAngles;
             if (adjust.z != 0)
             if (adjust.z != 0)
             {
             {
                 rot.x += adjust.z * rotateStep;
                 rot.x += adjust.z * rotateStep;
@@ -607,13 +607,13 @@ void SteppedObjectManipulation(int key)
                 rot.z += adjust.y * rotateStep;
                 rot.z += adjust.y * rotateStep;
                 rot.z = Floor(rot.z / rotateStep + 0.5) * rotateStep;
                 rot.z = Floor(rot.z / rotateStep + 0.5) * rotateStep;
             }
             }
-            selectedNode.rotation = Quaternion(rot);
+            editNode.rotation = Quaternion(rot);
         }
         }
         break;
         break;
 
 
     case OBJ_SCALE:
     case OBJ_SCALE:
         {
         {
-            Vector3 scale = selectedNode.scale;
+            Vector3 scale = editNode.scale;
             if (adjust.x != 0)
             if (adjust.x != 0)
             {
             {
                 scale.x += adjust.x * scaleStep;
                 scale.x += adjust.x * scaleStep;
@@ -629,7 +629,7 @@ void SteppedObjectManipulation(int key)
                 scale.z += adjust.z * scaleStep;
                 scale.z += adjust.z * scaleStep;
                 scale.z = Floor(scale.z / scaleStep + 0.5) * scaleStep;
                 scale.z = Floor(scale.z / scaleStep + 0.5) * scaleStep;
             }
             }
-            selectedNode.scale = scale;
+            editNode.scale = scale;
         }
         }
         break;
         break;
     }
     }

+ 206 - 139
Bin/Data/Scripts/Editor/EditorNodeWindow.as

@@ -51,7 +51,7 @@ Array<ResourcePicker@> resourcePickers;
 Array<VectorStruct@> vectorStructs;
 Array<VectorStruct@> vectorStructs;
 
 
 bool inLoadAttributeEditor = false;
 bool inLoadAttributeEditor = false;
-uint resourcePickID = 0;
+Array<uint> resourcePickIDs;
 uint resourcePickIndex = 0;
 uint resourcePickIndex = 0;
 uint resourcePickSubIndex = 0;
 uint resourcePickSubIndex = 0;
 ResourcePicker@ resourcePicker;
 ResourcePicker@ resourcePicker;
@@ -116,52 +116,61 @@ void UpdateNodeWindow()
     Text@ nodeTitle = nodeWindow.GetChild("NodeTitle", true);
     Text@ nodeTitle = nodeWindow.GetChild("NodeTitle", true);
     Text@ componentTitle = nodeWindow.GetChild("ComponentTitle", true);
     Text@ componentTitle = nodeWindow.GetChild("ComponentTitle", true);
 
 
-    if (selectedNode is null)
+    if (editNode is null)
     {
     {
-        if (selectedNodes.empty)
+        if (selectedNodes.length <= 1)
             nodeTitle.text = "No node";
             nodeTitle.text = "No node";
         else
         else
-            nodeTitle.text = "Multiple nodes";
+            nodeTitle.text = selectedNodes.length + " nodes";
     }
     }
     else
     else
     {
     {
         String idStr;
         String idStr;
-        if (selectedNode.id >= FIRST_LOCAL_ID)
-            idStr = "Local ID " + String(selectedNode.id - FIRST_LOCAL_ID);
+        if (editNode.id >= FIRST_LOCAL_ID)
+            idStr = "Local ID " + String(editNode.id - FIRST_LOCAL_ID);
         else
         else
-            idStr = "ID " + String(selectedNode.id);
-        nodeTitle.text = selectedNode.typeName + " (" + idStr + ")";
+            idStr = "ID " + String(editNode.id);
+        nodeTitle.text = editNode.typeName + " (" + idStr + ")";
     }
     }
 
 
-    if (selectedComponent is null)
+    if (editComponents.empty)
     {
     {
-        if (selectedComponents.empty)
+        if (selectedComponents.length <= 1)
             componentTitle.text = "No component";
             componentTitle.text = "No component";
         else
         else
-            componentTitle.text = "Multiple components";
+            componentTitle.text = selectedComponents.length + " components";
     }
     }
     else
     else
-        componentTitle.text = GetComponentTitle(selectedComponent, 0);
+    {
+        String multiplierText;
+        if (editComponents.length > 1)
+            multiplierText = " (" + editComponents.length + "x)";
+        componentTitle.text = GetComponentTitle(editComponents[0], 0) + multiplierText;
+    }
 
 
     UpdateAttributes(true);
     UpdateAttributes(true);
-
-    if (selectedNode !is null || selectedComponent !is null)
-        nodeWindow.visible = true;
 }
 }
 
 
 void UpdateAttributes(bool fullUpdate)
 void UpdateAttributes(bool fullUpdate)
 {
 {
     if (nodeWindow !is null)
     if (nodeWindow !is null)
     {
     {
-        UpdateAttributes(selectedNode, nodeWindow.GetChild("NodeAttributeList", true), fullUpdate);
-        UpdateAttributes(selectedComponent, nodeWindow.GetChild("ComponentAttributeList", true), fullUpdate);
+        Array<Serializable@> nodes;
+        Array<Serializable@> components;
+        if (editNode !is null)
+            nodes.Push(editNode);
+        for (uint i = 0; i < editComponents.length; ++i)
+            components.Push(editComponents[i]);
+
+        UpdateAttributes(nodes, nodeWindow.GetChild("NodeAttributeList", true), fullUpdate);
+        UpdateAttributes(components, nodeWindow.GetChild("ComponentAttributeList", true), fullUpdate);
     }
     }
 }
 }
 
 
-void UpdateAttributes(Serializable@ serializable, ListView@ list, bool fullUpdate)
+void UpdateAttributes(Array<Serializable@>@ serializables, ListView@ list, bool fullUpdate)
 {
 {
     // If attributes have changed structurally, do a full update
     // If attributes have changed structurally, do a full update
-    uint count = GetAttributeEditorCount(serializable);
+    uint count = GetAttributeEditorCount(serializables);
     if (fullUpdate == false)
     if (fullUpdate == false)
     {
     {
         if (list.contentElement.numChildren != count)
         if (list.contentElement.numChildren != count)
@@ -183,19 +192,20 @@ void UpdateAttributes(Serializable@ serializable, ListView@ list, bool fullUpdat
         }
         }
     }
     }
 
 
-    if (serializable is null)
+    if (serializables.empty)
         return;
         return;
 
 
-    for (uint i = 0; i < serializable.numAttributes; ++i)
+    // If there are many serializables, they must share same attribute structure
+    for (uint i = 0; i < serializables[0].numAttributes; ++i)
     {
     {
-        AttributeInfo info = serializable.attributeInfos[i];
+        AttributeInfo info = serializables[0].attributeInfos[i];
         if (info.mode & AM_NOEDIT != 0)
         if (info.mode & AM_NOEDIT != 0)
             continue;
             continue;
 
 
         if (fullUpdate)
         if (fullUpdate)
-            CreateAttributeEditor(list, serializable, i);
+            CreateAttributeEditor(list, serializables, i);
 
 
-        LoadAttributeEditor(list, serializable, i);
+        LoadAttributeEditor(list, serializables, i);
     }
     }
     
     
     if (fullUpdate)
     if (fullUpdate)
@@ -210,16 +220,17 @@ void EditAttribute(StringHash eventType, VariantMap& eventData)
 
 
     UIElement@ attrEdit = eventData["Element"].GetUIElement();
     UIElement@ attrEdit = eventData["Element"].GetUIElement();
     UIElement@ parent = attrEdit.parent;
     UIElement@ parent = attrEdit.parent;
-    Serializable@ serializable = GetAttributeEditorTarget(attrEdit);
-    if (serializable is null)
+    Array<Serializable@>@ serializables = GetAttributeEditorTargets(attrEdit);
+    if (serializables.empty)
         return;
         return;
 
 
     uint index = attrEdit.vars["Index"].GetUInt();
     uint index = attrEdit.vars["Index"].GetUInt();
     uint subIndex = attrEdit.vars["SubIndex"].GetUInt();
     uint subIndex = attrEdit.vars["SubIndex"].GetUInt();
     bool intermediateEdit = eventType == StringHash("TextChanged");
     bool intermediateEdit = eventType == StringHash("TextChanged");
 
 
-    StoreAttributeEditor(parent, serializable, index, subIndex);
-    serializable.ApplyAttributes();
+    StoreAttributeEditor(parent, serializables, index, subIndex);
+    for (uint i = 0; i < serializables.length; ++i)
+        serializables[i].ApplyAttributes();
 
 
     // If not an intermediate edit, reload the editor fields with validated values
     // If not an intermediate edit, reload the editor fields with validated values
     // (attributes may have interactions; therefore we load everything, not just the value being edited)
     // (attributes may have interactions; therefore we load everything, not just the value being edited)
@@ -227,39 +238,40 @@ void EditAttribute(StringHash eventType, VariantMap& eventData)
         UpdateAttributes(false);
         UpdateAttributes(false);
 
 
     // If a model was loaded, update the scene hierarchy in case bones were recreated
     // If a model was loaded, update the scene hierarchy in case bones were recreated
-    if (serializable.attributes[index].GetResourceRef().type == ShortStringHash("Model"))
+    if (serializables[0].attributes[index].GetResourceRef().type == ShortStringHash("Model"))
     {
     {
-        if (selectedNode !is null)
-            UpdateSceneWindowNode(selectedNode);
+        if (editNode !is null)
+            UpdateSceneWindowNode(editNode);
     }
     }
     else
     else
     {
     {
         // If node name changed, update it in the scene window also
         // If node name changed, update it in the scene window also
-        if (serializable.attributeInfos[index].name == "Name" && selectedNode !is null)
-            UpdateSceneWindowNodeOnly(selectedNode);
+        if (serializables[0].attributeInfos[index].name == "Name" && editNode !is null)
+            UpdateSceneWindowNodeOnly(editNode);
     }
     }
 }
 }
 
 
-uint GetAttributeEditorCount(Serializable@ serializable)
+uint GetAttributeEditorCount(Array<Serializable@>@ serializables)
 {
 {
     uint count = 0;
     uint count = 0;
 
 
-    if (serializable !is null)
+    if (!serializables.empty)
     {
     {
-        for (uint i = 0; i < serializable.numAttributes; ++i)
+        /// \todo If multi-editing, this only counts the editor count of the first serializable
+        for (uint i = 0; i < serializables[0].numAttributes; ++i)
         {
         {
-            AttributeInfo info = serializable.attributeInfos[i];
+            AttributeInfo info = serializables[0].attributeInfos[i];
             if (info.mode & AM_NOEDIT != 0)
             if (info.mode & AM_NOEDIT != 0)
                 continue;
                 continue;
             // Resource editors have the title + the editor row itself, so count 2
             // Resource editors have the title + the editor row itself, so count 2
             if (info.type == VAR_RESOURCEREF)
             if (info.type == VAR_RESOURCEREF)
                 count += 2;
                 count += 2;
             else if (info.type == VAR_RESOURCEREFLIST)
             else if (info.type == VAR_RESOURCEREFLIST)
-                count += 2 * serializable.attributes[i].GetResourceRefList().length;
-            else if (info.type == VAR_VARIANTVECTOR && GetVectorStruct(serializable, i) !is null)
-                count += serializable.attributes[i].GetVariantVector().length;
+                count += 2 * serializables[0].attributes[i].GetResourceRefList().length;
+            else if (info.type == VAR_VARIANTVECTOR && GetVectorStruct(serializables, i) !is null)
+                count += serializables[0].attributes[i].GetVariantVector().length;
             else if (info.type == VAR_VARIANTMAP)
             else if (info.type == VAR_VARIANTMAP)
-                count += serializable.attributes[i].GetVariantMap().length;
+                count += serializables[0].attributes[i].GetVariantMap().length;
             else
             else
                 ++count;
                 ++count;
         }
         }
@@ -304,37 +316,63 @@ UIElement@ CreateAttributeEditorParent(ListView@ list, String name, uint index,
     return editorParent;
     return editorParent;
 }
 }
 
 
-LineEdit@ CreateAttributeLineEdit(UIElement@ parent, Serializable@ serializable, uint index, uint subIndex)
+LineEdit@ CreateAttributeLineEdit(UIElement@ parent, Array<Serializable@>@ serializables, uint index, uint subIndex)
 {
 {
     LineEdit@ attrEdit = LineEdit();
     LineEdit@ attrEdit = LineEdit();
     attrEdit.SetStyle(uiStyle, "EditorAttributeEdit");
     attrEdit.SetStyle(uiStyle, "EditorAttributeEdit");
     attrEdit.SetFixedHeight(ATTR_HEIGHT - 2);
     attrEdit.SetFixedHeight(ATTR_HEIGHT - 2);
     attrEdit.vars["Index"] = index;
     attrEdit.vars["Index"] = index;
     attrEdit.vars["SubIndex"] = subIndex;
     attrEdit.vars["SubIndex"] = subIndex;
-    SetAttributeEditorID(attrEdit, serializable);
+    SetAttributeEditorID(attrEdit, serializables);
     parent.AddChild(attrEdit);
     parent.AddChild(attrEdit);
     return attrEdit;
     return attrEdit;
 }
 }
 
 
-void SetAttributeEditorID(UIElement@ attrEdit, Serializable@ serializable)
+void SetAttributeEditorID(UIElement@ attrEdit, Array<Serializable@>@ serializables)
 {
 {
-    // Serializable does not expose the ID, so must cast into both Node & Component
-    Node@ node = cast<Node>(serializable);
-    Component@ component = cast<Component>(serializable);
+    // All target serializables must be either nodes or components, so can check the first for the type
+    Node@ node = cast<Node>(serializables[0]);
+    Array<Variant> ids;
     if (node !is null)
     if (node !is null)
-        attrEdit.vars["NodeID"] = node.id;
-    else if (component !is null)
-        attrEdit.vars["ComponentID"] = component.id;
+    {
+        for (uint i = 0; i < serializables.length; ++i)
+            ids.Push(Variant(cast<Node>(serializables[i]).id));
+        attrEdit.vars["NodeIDs"] = ids;
+    }
+    else
+    {
+        for (uint i = 0; i < serializables.length; ++i)
+            ids.Push(Variant(cast<Component>(serializables[i]).id));
+        attrEdit.vars["ComponentIDs"] = ids;
+    }
 }
 }
 
 
-Serializable@ GetAttributeEditorTarget(UIElement@ attrEdit)
+Array<Serializable@>@ GetAttributeEditorTargets(UIElement@ attrEdit)
 {
 {
-    if (attrEdit.vars.Contains("NodeID"))
-        return editorScene.GetNode(attrEdit.vars["NodeID"].GetUInt());
-    else if (attrEdit.vars.Contains("ComponentID"))
-        return editorScene.GetComponent(attrEdit.vars["ComponentID"].GetUInt());
-    else
-        return null;
+    Array<Serializable@> ret;
+
+    if (attrEdit.vars.Contains("NodeIDs"))
+    {
+        Array<Variant>@ ids = attrEdit.vars["NodeIDs"].GetVariantVector();
+        for (uint i = 0; i < ids.length; ++i)
+        {
+            Node@ node = editorScene.GetNode(ids[i].GetUInt());
+            if (node !is null)
+                ret.Push(node);
+        }
+    }
+    if (attrEdit.vars.Contains("ComponentIDs"))
+    {
+        Array<Variant>@ ids = attrEdit.vars["ComponentIDs"].GetVariantVector();
+        for (uint i = 0; i < ids.length; ++i)
+        {
+            Component@ component = editorScene.GetComponent(ids[i].GetUInt());
+            if (component !is null)
+                ret.Push(component);
+        }
+    }
+    
+    return ret;
 }
 }
 
 
 UIElement@ GetAttributeEditorParent(UIElement@ parent, uint index, uint subIndex)
 UIElement@ GetAttributeEditorParent(UIElement@ parent, uint index, uint subIndex)
@@ -342,32 +380,32 @@ UIElement@ GetAttributeEditorParent(UIElement@ parent, uint index, uint subIndex
     return parent.GetChild("Edit" + String(index) + "_" + String(subIndex), true);
     return parent.GetChild("Edit" + String(index) + "_" + String(subIndex), true);
 }
 }
 
 
-VectorStruct@ GetVectorStruct(Serializable@ serializable, uint index)
+VectorStruct@ GetVectorStruct(Array<Serializable@>@ serializables, uint index)
 {
 {
-    AttributeInfo info = serializable.attributeInfos[index];
+    AttributeInfo info = serializables[0].attributeInfos[index];
     for (uint i = 0; i < vectorStructs.length; ++i)
     for (uint i = 0; i < vectorStructs.length; ++i)
     {
     {
-        if (vectorStructs[i].componentTypeName == serializable.typeName && vectorStructs[i].attributeName ==
+        if (vectorStructs[i].componentTypeName == serializables[0].typeName && vectorStructs[i].attributeName ==
             info.name)
             info.name)
             return vectorStructs[i];
             return vectorStructs[i];
     }
     }
     return null;
     return null;
 }
 }
 
 
-void CreateAttributeEditor(ListView@ list, Serializable@ serializable, uint index)
+void CreateAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, uint index)
 {
 {
-    AttributeInfo info = serializable.attributeInfos[index];
-    CreateAttributeEditor(list, serializable, info.name, info.type, info.enumNames, index, 0);
+    AttributeInfo info = serializables[0].attributeInfos[index];
+    CreateAttributeEditor(list, serializables, info.name, info.type, info.enumNames, index, 0);
 }
 }
 
 
-UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
+UIElement@ CreateAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, const String&in name, VariantType type, Array<String>@ enumNames, uint index, uint subIndex)
 {
 {
     UIElement@ parent;
     UIElement@ parent;
 
 
     if (type == VAR_STRING)
     if (type == VAR_STRING)
     {
     {
         parent = CreateAttributeEditorParent(list, name, index, subIndex);
         parent = CreateAttributeEditorParent(list, name, index, subIndex);
-        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializable, index, subIndex);
+        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
         SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
         SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
         SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
         SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
     }
     }
@@ -379,7 +417,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
         attrEdit.SetFixedSize(16, 16);
         attrEdit.SetFixedSize(16, 16);
         attrEdit.vars["Index"] = index;
         attrEdit.vars["Index"] = index;
         attrEdit.vars["SubIndex"] = subIndex;
         attrEdit.vars["SubIndex"] = subIndex;
-        SetAttributeEditorID(attrEdit, serializable);
+        SetAttributeEditorID(attrEdit, serializables);
         parent.AddChild(attrEdit);
         parent.AddChild(attrEdit);
         SubscribeToEvent(attrEdit, "Toggled", "EditAttribute");
         SubscribeToEvent(attrEdit, "Toggled", "EditAttribute");
     }
     }
@@ -394,7 +432,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
 
 
         for (uint i = 0; i < numCoords; ++i)
         for (uint i = 0; i < numCoords; ++i)
         {
         {
-            LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializable, index, subIndex);
+            LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
             SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
             SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
             SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
             SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
         }
         }
@@ -406,7 +444,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
         if (enumNames is null || enumNames.empty)
         if (enumNames is null || enumNames.empty)
         {
         {
             // No enums, create a numeric editor
             // No enums, create a numeric editor
-            LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializable, index, subIndex);
+            LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
             SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
             SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute");
             SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
             SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
         }
         }
@@ -419,7 +457,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
             attrEdit.vars["Index"] = index;
             attrEdit.vars["Index"] = index;
             attrEdit.vars["SubIndex"] = subIndex;
             attrEdit.vars["SubIndex"] = subIndex;
             attrEdit.SetLayout(LM_HORIZONTAL, 0, IntRect(4, 1, 4, 1));
             attrEdit.SetLayout(LM_HORIZONTAL, 0, IntRect(4, 1, 4, 1));
-            SetAttributeEditorID(attrEdit, serializable);
+            SetAttributeEditorID(attrEdit, serializables);
 
 
             for (uint i = 0; i < enumNames.length; ++i)
             for (uint i = 0; i < enumNames.length; ++i)
             {
             {
@@ -437,8 +475,8 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
     }
     }
     if (type == VAR_RESOURCEREF)
     if (type == VAR_RESOURCEREF)
     {
     {
-        ShortStringHash resourceType = serializable.attributeInfos[index].type == VAR_RESOURCEREF ?
-            serializable.attributes[index].GetResourceRef().type : serializable.attributes[index].GetResourceRefList().type;
+        ShortStringHash resourceType = serializables[0].attributeInfos[index].type == VAR_RESOURCEREF ?
+            serializables[0].attributes[index].GetResourceRef().type : serializables[0].attributes[index].GetResourceRefList().type;
 
 
         // Create the resource name on a separate non-interactive line to allow for more space
         // Create the resource name on a separate non-interactive line to allow for more space
         CreateAttributeEditorParentTitle(list, name);
         CreateAttributeEditorParentTitle(list, name);
@@ -449,7 +487,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
         spacer.SetFixedSize(10, ATTR_HEIGHT - 2);
         spacer.SetFixedSize(10, ATTR_HEIGHT - 2);
         parent.AddChild(spacer);
         parent.AddChild(spacer);
 
 
-        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializable, index, subIndex);
+        LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex);
         attrEdit.vars["Type"] = resourceType.value;
         attrEdit.vars["Type"] = resourceType.value;
         SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
         SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute");
 
 
@@ -462,7 +500,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
         pickButton.SetFixedSize(36, ATTR_HEIGHT - 2);
         pickButton.SetFixedSize(36, ATTR_HEIGHT - 2);
         pickButton.vars["Index"] = index;
         pickButton.vars["Index"] = index;
         pickButton.vars["SubIndex"] = subIndex;
         pickButton.vars["SubIndex"] = subIndex;
-        SetAttributeEditorID(pickButton, serializable);
+        SetAttributeEditorID(pickButton, serializables);
 
 
         Text@ pickButtonText = Text();
         Text@ pickButtonText = Text();
         pickButtonText.SetStyle(uiStyle, "EditorAttributeText");
         pickButtonText.SetStyle(uiStyle, "EditorAttributeText");
@@ -481,7 +519,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
         openButton.SetFixedSize(36, 16);
         openButton.SetFixedSize(36, 16);
         openButton.vars["Index"] = index;
         openButton.vars["Index"] = index;
         openButton.vars["SubIndex"] = subIndex;
         openButton.vars["SubIndex"] = subIndex;
-        SetAttributeEditorID(openButton, serializable);
+        SetAttributeEditorID(openButton, serializables);
 
 
         Text@ openButtonText = Text();
         Text@ openButtonText = Text();
         openButtonText.SetStyle(uiStyle, "EditorAttributeText");
         openButtonText.SetStyle(uiStyle, "EditorAttributeText");
@@ -493,21 +531,21 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
     }
     }
     if (type == VAR_RESOURCEREFLIST)
     if (type == VAR_RESOURCEREFLIST)
     {
     {
-        uint numRefs = serializable.attributes[index].GetResourceRefList().length;
+        uint numRefs = serializables[0].attributes[index].GetResourceRefList().length;
         for (uint i = 0; i < numRefs; ++i)
         for (uint i = 0; i < numRefs; ++i)
-            CreateAttributeEditor(list, serializable, name, VAR_RESOURCEREF, null, index, i);
+            CreateAttributeEditor(list, serializables, name, VAR_RESOURCEREF, null, index, i);
     }
     }
     if (type == VAR_VARIANTVECTOR)
     if (type == VAR_VARIANTVECTOR)
     {
     {
-        VectorStruct@ vectorStruct = GetVectorStruct(serializable, index);
+        VectorStruct@ vectorStruct = GetVectorStruct(serializables, index);
         if (vectorStruct is null)
         if (vectorStruct is null)
             return null;
             return null;
         uint nameIndex = 0;
         uint nameIndex = 0;
 
 
-        Array<Variant>@ vector = serializable.attributes[index].GetVariantVector();
+        Array<Variant>@ vector = serializables[0].attributes[index].GetVariantVector();
         for (uint i = 0; i < vector.length; ++i)
         for (uint i = 0; i < vector.length; ++i)
         {
         {
-            CreateAttributeEditor(list, serializable, vectorStruct.variableNames[nameIndex], vector[i].type, null, index, i);
+            CreateAttributeEditor(list, serializables, vectorStruct.variableNames[nameIndex], vector[i].type, null, index, i);
             ++nameIndex;
             ++nameIndex;
             if (nameIndex >= vectorStruct.variableNames.length)
             if (nameIndex >= vectorStruct.variableNames.length)
                 nameIndex = vectorStruct.restartIndex;
                 nameIndex = vectorStruct.restartIndex;
@@ -515,12 +553,12 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
     }
     }
     if (type == VAR_VARIANTMAP)
     if (type == VAR_VARIANTMAP)
     {
     {
-        VariantMap map = serializable.attributes[index].GetVariantMap();
+        VariantMap map = serializables[0].attributes[index].GetVariantMap();
         Array<ShortStringHash>@ keys = map.keys;
         Array<ShortStringHash>@ keys = map.keys;
         for (uint i = 0; i < keys.length; ++i)
         for (uint i = 0; i < keys.length; ++i)
         {
         {
             Variant value = map[keys[i]];
             Variant value = map[keys[i]];
-            parent = CreateAttributeEditor(list, serializable, editorScene.GetVarName(keys[i]) + " (Var)", value.type, null, index, i);
+            parent = CreateAttributeEditor(list, serializables, editorScene.GetVarName(keys[i]) + " (Var)", value.type, null, index, i);
             // Add the variant key to the parent
             // Add the variant key to the parent
             parent.vars["Key"] = keys[i].value;
             parent.vars["Key"] = keys[i].value;
         }
         }
@@ -529,7 +567,7 @@ UIElement@ CreateAttributeEditor(ListView@ list, Serializable@ serializable, con
     return parent;
     return parent;
 }
 }
 
 
-void LoadAttributeEditor(ListView@ list, Serializable@ serializable, uint index)
+void LoadAttributeEditor(ListView@ list, Array<Serializable@>@ serializables, uint index)
 {
 {
     UIElement@ parent = GetAttributeEditorParent(list, index, 0);
     UIElement@ parent = GetAttributeEditorParent(list, index, 0);
     if (parent is null)
     if (parent is null)
@@ -537,9 +575,20 @@ void LoadAttributeEditor(ListView@ list, Serializable@ serializable, uint index)
 
 
     inLoadAttributeEditor = true;
     inLoadAttributeEditor = true;
 
 
-    AttributeInfo info = serializable.attributeInfos[index];
-    Variant value = serializable.attributes[index];
-    LoadAttributeEditor(parent, value, value.type, info.enumNames);
+    AttributeInfo info = serializables[0].attributeInfos[index];
+    bool sameValue = true;
+    Variant value = serializables[0].attributes[index];
+    for (uint i = 1; i < serializables.length; ++i)
+    {
+        if (serializables[i].attributes[index] != value)
+        {
+            sameValue = false;
+            break;
+        }
+    }
+
+    if (sameValue)
+        LoadAttributeEditor(parent, value, value.type, info.enumNames);
 
 
     inLoadAttributeEditor = false;
     inLoadAttributeEditor = false;
 }
 }
@@ -547,7 +596,7 @@ void LoadAttributeEditor(ListView@ list, Serializable@ serializable, uint index)
 void LoadAttributeEditor(UIElement@ parent, Variant value, VariantType type, Array<String>@ enumNames)
 void LoadAttributeEditor(UIElement@ parent, Variant value, VariantType type, Array<String>@ enumNames)
 {
 {
     uint index = parent.vars["Index"].GetUInt();
     uint index = parent.vars["Index"].GetUInt();
-    
+
     if (type == VAR_STRING)
     if (type == VAR_STRING)
     {
     {
         LineEdit@ attrEdit = parent.children[1];
         LineEdit@ attrEdit = parent.children[1];
@@ -688,37 +737,47 @@ void LoadAttributeEditor(UIElement@ parent, Variant value, VariantType type, Arr
     }
     }
 }
 }
 
 
-void StoreAttributeEditor(UIElement@ parent, Serializable@ serializable, uint index, uint subIndex)
+void StoreAttributeEditor(UIElement@ parent, Array<Serializable@>@ serializables, uint index, uint subIndex)
 {
 {
-    AttributeInfo info = serializable.attributeInfos[index];
+    AttributeInfo info = serializables[0].attributeInfos[index];
 
 
     if (info.type == VAR_RESOURCEREFLIST)
     if (info.type == VAR_RESOURCEREFLIST)
     {
     {
-        ResourceRefList refList = serializable.attributes[index].GetResourceRefList();
-        Variant newValue = GetEditorValue(parent, VAR_RESOURCEREF, null);
-        ResourceRef ref = newValue.GetResourceRef();
-        refList.ids[subIndex] = ref.id;
-        serializable.attributes[index] = Variant(refList);
+        for (uint i = 0; i < serializables.length; ++i)
+        {
+            ResourceRefList refList = serializables[i].attributes[index].GetResourceRefList();
+            Variant newValue = GetEditorValue(parent, VAR_RESOURCEREF, null);
+            ResourceRef ref = newValue.GetResourceRef();
+            refList.ids[subIndex] = ref.id;
+            serializables[i].attributes[index] = Variant(refList);
+        }
     }
     }
     else if (info.type == VAR_VARIANTVECTOR)
     else if (info.type == VAR_VARIANTVECTOR)
     {
     {
-        Array<Variant>@ vector = serializable.attributes[index].GetVariantVector();
-        Variant newValue = GetEditorValue(parent, vector[subIndex].type, null);
-        vector[subIndex] = newValue;
-        serializable.attributes[index] = Variant(vector);
+        for (uint i = 0; i < serializables.length; ++i)
+        {
+            Array<Variant>@ vector = serializables[i].attributes[index].GetVariantVector();
+            Variant newValue = GetEditorValue(parent, vector[subIndex].type, null);
+            vector[subIndex] = newValue;
+            serializables[i].attributes[index] = Variant(vector);
+        }
     }
     }
     else if (info.type == VAR_VARIANTMAP)
     else if (info.type == VAR_VARIANTMAP)
     {
     {
-        VariantMap map = serializable.attributes[index].GetVariantMap();
-        ShortStringHash key(parent.vars["Key"].GetUInt());
-        Variant newValue = GetEditorValue(parent, map[key].type, null);
-        map[key] = newValue;
-        serializable.attributes[index] = Variant(map);
+        for (uint i = 0; i < serializables.length; ++i)
+        {
+            VariantMap map = serializables[i].attributes[index].GetVariantMap();
+            ShortStringHash key(parent.vars["Key"].GetUInt());
+            Variant newValue = GetEditorValue(parent, map[key].type, null);
+            map[key] = newValue;
+            serializables[i].attributes[index] = Variant(map);
+        }
     }
     }
     else
     else
     {
     {
         Variant newValue = GetEditorValue(parent, info.type, info.enumNames);
         Variant newValue = GetEditorValue(parent, info.type, info.enumNames);
-        serializable.attributes[index] = newValue;
+        for (uint i = 0; i < serializables.length; ++i)
+            serializables[i].attributes[index] = newValue;
     }
     }
 }
 }
 
 
@@ -823,27 +882,27 @@ void PickResource(StringHash eventType, VariantMap& eventData)
 
 
     UIElement@ button = eventData["Element"].GetUIElement();
     UIElement@ button = eventData["Element"].GetUIElement();
     LineEdit@ attrEdit = button.parent.children[1];
     LineEdit@ attrEdit = button.parent.children[1];
-    // Note: nodes never contain resources. Therefore can assume the target is always a component
-    Component@ target = GetAttributeEditorTarget(attrEdit);
-    if (target is null)
+
+    Array<Serializable@>@ targets = GetAttributeEditorTargets(attrEdit);
+    if (targets.empty)
         return;
         return;
 
 
     resourcePickIndex = attrEdit.vars["Index"].GetUInt();
     resourcePickIndex = attrEdit.vars["Index"].GetUInt();
     resourcePickSubIndex = attrEdit.vars["SubIndex"].GetUInt();
     resourcePickSubIndex = attrEdit.vars["SubIndex"].GetUInt();
-    AttributeInfo info = target.attributeInfos[resourcePickIndex];
+    AttributeInfo info = targets[0].attributeInfos[resourcePickIndex];
 
 
     if (info.type == VAR_RESOURCEREF)
     if (info.type == VAR_RESOURCEREF)
     {
     {
-        String resourceType = GetTypeName(target.attributes[resourcePickIndex].GetResourceRef().type);
+        String resourceType = GetTypeName(targets[0].attributes[resourcePickIndex].GetResourceRef().type);
         // Hack: if the resource is a light's shape texture, change resource type according to light type
         // Hack: if the resource is a light's shape texture, change resource type according to light type
         // (TextureCube for point light)
         // (TextureCube for point light)
-        if (info.name == "Light Shape Texture" && cast<Light>(target).lightType == LIGHT_POINT)
+        if (info.name == "Light Shape Texture" && cast<Light>(targets[0]).lightType == LIGHT_POINT)
             resourceType = "TextureCube";
             resourceType = "TextureCube";
         @resourcePicker = GetResourcePicker(resourceType);
         @resourcePicker = GetResourcePicker(resourceType);
     }
     }
     else if (info.type == VAR_RESOURCEREFLIST)
     else if (info.type == VAR_RESOURCEREFLIST)
     {
     {
-        String resourceType = GetTypeName(target.attributes[resourcePickIndex].GetResourceRefList().type);
+        String resourceType = GetTypeName(targets[0].attributes[resourcePickIndex].GetResourceRefList().type);
         @resourcePicker = GetResourcePicker(resourceType);
         @resourcePicker = GetResourcePicker(resourceType);
     }
     }
     else
     else
@@ -852,7 +911,10 @@ void PickResource(StringHash eventType, VariantMap& eventData)
     if (resourcePicker is null)
     if (resourcePicker is null)
         return;
         return;
 
 
-    resourcePickID = target.id;
+    resourcePickIDs.Clear();
+    for (uint i = 0; i < targets.length; ++i)
+        resourcePickIDs.Push(cast<Component>(targets[i]).id);
+
     String lastPath = resourcePicker.lastPath;
     String lastPath = resourcePicker.lastPath;
     if (lastPath.empty)
     if (lastPath.empty)
         lastPath = sceneResourcePath;
         lastPath = sceneResourcePath;
@@ -873,13 +935,12 @@ void PickResourceDone(StringHash eventType, VariantMap& eventData)
 
 
     if (!eventData["OK"].GetBool())
     if (!eventData["OK"].GetBool())
     {
     {
-        resourcePickID = 0;
+        resourcePickIDs.Clear();
         @resourcePicker = null;
         @resourcePicker = null;
         return;
         return;
     }
     }
 
 
-    Component@ target = editorScene.GetComponent(resourcePickID);
-    if (target is null || resourcePicker is null)
+    if (resourcePicker is null)
         return;
         return;
 
 
     // Validate the resource. It must come from within a registered resource directory, and be loaded successfully
     // Validate the resource. It must come from within a registered resource directory, and be loaded successfully
@@ -898,42 +959,48 @@ void PickResourceDone(StringHash eventType, VariantMap& eventData)
         return;
         return;
 
 
     bool isModel = false;
     bool isModel = false;
-    AttributeInfo info = target.attributeInfos[resourcePickIndex];
-    if (info.type == VAR_RESOURCEREF)
-    {
-        ResourceRef ref = target.attributes[resourcePickIndex].GetResourceRef();
-        ref.type = ShortStringHash(resourcePicker.resourceType);
-        ref.id = StringHash(resourceName);
-        target.attributes[resourcePickIndex] = Variant(ref);
-        target.ApplyAttributes();
-        isModel = ref.type == ShortStringHash("Model");
-    }
-    else if (info.type == VAR_RESOURCEREFLIST)
+    
+    for (uint i = 0; i < resourcePickIDs.length; ++i)
     {
     {
-        ResourceRefList refList = target.attributes[resourcePickIndex].GetResourceRefList();
-        if (resourcePickSubIndex < refList.length)
+        Component@ target = editorScene.GetComponent(resourcePickIDs[i]);
+
+        AttributeInfo info = target.attributeInfos[resourcePickIndex];
+        if (info.type == VAR_RESOURCEREF)
         {
         {
-            refList.ids[resourcePickSubIndex] = StringHash(resourceName);
-            target.attributes[resourcePickIndex] = Variant(refList);
+            ResourceRef ref = target.attributes[resourcePickIndex].GetResourceRef();
+            ref.type = ShortStringHash(resourcePicker.resourceType);
+            ref.id = StringHash(resourceName);
+            target.attributes[resourcePickIndex] = Variant(ref);
             target.ApplyAttributes();
             target.ApplyAttributes();
+            isModel = ref.type == ShortStringHash("Model");
+        }
+        else if (info.type == VAR_RESOURCEREFLIST)
+        {
+            ResourceRefList refList = target.attributes[resourcePickIndex].GetResourceRefList();
+            if (resourcePickSubIndex < refList.length)
+            {
+                refList.ids[resourcePickSubIndex] = StringHash(resourceName);
+                target.attributes[resourcePickIndex] = Variant(refList);
+                target.ApplyAttributes();
+            }
         }
         }
     }
     }
 
 
     UpdateAttributes(false);
     UpdateAttributes(false);
 
 
     // If a model was loaded, update the scene hierarchy in case bones were recreated
     // If a model was loaded, update the scene hierarchy in case bones were recreated
-    if (isModel && selectedNode !is null)
-        UpdateSceneWindowNode(selectedNode);
+    if (isModel && editNode !is null)
+        UpdateSceneWindowNode(editNode);
 
 
-    resourcePickID = 0;
+    resourcePickIDs.Clear();
     @resourcePicker = null;
     @resourcePicker = null;
 }
 }
 
 
 void PickResourceCanceled()
 void PickResourceCanceled()
 {
 {
-    if (resourcePickID != 0)
+    if (!resourcePickIDs.empty)
     {
     {
-        resourcePickID = 0;
+        resourcePickIDs.Clear();
         @resourcePicker = null;
         @resourcePicker = null;
         CloseFileSelector();
         CloseFileSelector();
     }
     }
@@ -962,7 +1029,7 @@ void OpenResource(StringHash eventType, VariantMap& eventData)
 
 
 void CreateNewVariable(StringHash eventType, VariantMap& eventData)
 void CreateNewVariable(StringHash eventType, VariantMap& eventData)
 {
 {
-    if (selectedNode is null)
+    if (editNode is null)
         return;
         return;
 
 
     DropDownList@ dropDown = eventData["Element"].GetUIElement();
     DropDownList@ dropDown = eventData["Element"].GetUIElement();
@@ -997,14 +1064,14 @@ void CreateNewVariable(StringHash eventType, VariantMap& eventData)
     }
     }
 
 
     // If we overwrite an existing variable, must recreate the editor(s) for the correct type
     // If we overwrite an existing variable, must recreate the editor(s) for the correct type
-    bool overwrite = selectedNode.vars.Contains(sanitatedVarName);
-    selectedNode.vars[sanitatedVarName] = newValue;
+    bool overwrite = editNode.vars.Contains(sanitatedVarName);
+    editNode.vars[sanitatedVarName] = newValue;
     UpdateAttributes(overwrite);
     UpdateAttributes(overwrite);
 }
 }
 
 
 void DeleteVariable(StringHash eventType, VariantMap& eventData)
 void DeleteVariable(StringHash eventType, VariantMap& eventData)
 {
 {
-    if (selectedNode is null)
+    if (editNode is null)
         return;
         return;
 
 
     LineEdit@ nameEdit = nodeWindow.GetChild("VarNameEdit", true);
     LineEdit@ nameEdit = nodeWindow.GetChild("VarNameEdit", true);
@@ -1012,6 +1079,6 @@ void DeleteVariable(StringHash eventType, VariantMap& eventData)
     if (sanitatedVarName.empty)
     if (sanitatedVarName.empty)
         return;
         return;
 
 
-    selectedNode.vars.Erase(sanitatedVarName);
+    editNode.vars.Erase(sanitatedVarName);
     UpdateAttributes(false);
     UpdateAttributes(false);
 }
 }

+ 20 - 10
Bin/Data/Scripts/Editor/EditorScene.as

@@ -20,10 +20,10 @@ bool physicsDebug = false;
 bool octreeDebug = false;
 bool octreeDebug = false;
 int pickMode = PICK_GEOMETRIES;
 int pickMode = PICK_GEOMETRIES;
 
 
-Component@ selectedComponent;
-Node@ selectedNode;
-Array<Component@> selectedComponents;
 Array<Node@> selectedNodes;
 Array<Node@> selectedNodes;
+Array<Component@> selectedComponents;
+Node@ editNode;
+Array<Component@> editComponents;
 
 
 Array<int> pickModeDrawableFlags = {
 Array<int> pickModeDrawableFlags = {
     DRAWABLE_GEOMETRY,
     DRAWABLE_GEOMETRY,
@@ -33,10 +33,10 @@ Array<int> pickModeDrawableFlags = {
 
 
 void ClearSelection()
 void ClearSelection()
 {
 {
-    selectedComponent = null;
-    selectedNode = null;
-    selectedComponents.Clear();
     selectedNodes.Clear();
     selectedNodes.Clear();
+    selectedComponents.Clear();
+    editNode = null;
+    editComponents.Clear();
 }
 }
 
 
 void CreateScene()
 void CreateScene()
@@ -286,7 +286,7 @@ void ScenePostRenderUpdate()
             drawable.DrawDebugGeometry(debug, false);
             drawable.DrawDebugGeometry(debug, false);
         else
         else
         {
         {
-            CollisionShape@ shape = cast<CollisionShape>(selectedComponent);
+            CollisionShape@ shape = cast<CollisionShape>(selectedComponents[i]);
             if (shape !is null)
             if (shape !is null)
                 shape.DrawDebugGeometry(debug, false);
                 shape.DrawDebugGeometry(debug, false);
         }
         }
@@ -298,7 +298,7 @@ void ScenePostRenderUpdate()
         editorScene.physicsWorld.DrawDebugGeometry(true);
         editorScene.physicsWorld.DrawDebugGeometry(true);
     if (octreeDebug && editorScene.octree !is null)
     if (octreeDebug && editorScene.octree !is null)
         editorScene.octree.DrawDebugGeometry(true);
         editorScene.octree.DrawDebugGeometry(true);
-    
+
     SceneRaycast(false);
     SceneRaycast(false);
 }
 }
 
 
@@ -366,9 +366,19 @@ void SceneRaycast(bool mouseClick)
     {
     {
         bool multiselect = input.qualifierDown[QUAL_CTRL];
         bool multiselect = input.qualifierDown[QUAL_CTRL];
         if (input.qualifierDown[QUAL_SHIFT])
         if (input.qualifierDown[QUAL_SHIFT])
-            SelectNode(selected.node, multiselect);
-        else
+        {
+            // If we are selecting components, but have nodes in existing selection, do not multiselect to prevent confusion
+            if (!selectedNodes.empty)
+                multiselect = false;
             SelectComponent(selected, multiselect);
             SelectComponent(selected, multiselect);
+        }
+        else
+        {
+            // If we are selecting nodes, but have components in existing selection, do not multiselect to prevent confusion
+            if (!selectedComponents.empty)
+                multiselect = false;
+            SelectNode(selected.node, multiselect);
+        }
     }
     }
 }
 }
 
 

+ 138 - 100
Bin/Data/Scripts/Editor/EditorSceneWindow.as

@@ -396,7 +396,7 @@ void SelectNode(Node@ node, bool multiselect)
             if (parentItem != 0 && parentItem < numItems)
             if (parentItem != 0 && parentItem < numItems)
                 list.SetChildItemsVisible(parentItem, true);
                 list.SetChildItemsVisible(parentItem, true);
         }
         }
-        // This causes an event to be sent, in response we set selectedComponent & selectedNode, and refresh editors
+        // This causes an event to be sent, in response we set the node/component selections, and refresh editors
         if (!multiselect)
         if (!multiselect)
             list.selection = nodeItem;
             list.selection = nodeItem;
         else
         else
@@ -457,7 +457,7 @@ void SelectComponent(Component@ component, bool multiselect)
                 list.SetChildItemsVisible(nodeItem, true);
                 list.SetChildItemsVisible(nodeItem, true);
             list.items[componentItem].visible = true;
             list.items[componentItem].visible = true;
         }
         }
-        // This causes an event to be sent, in response we set selectedComponent & selectedNode, and refresh editors
+        // This causes an event to be sent, in response we set the node/component selections, and refresh editors
         if (!multiselect)
         if (!multiselect)
             list.selection = componentItem;
             list.selection = componentItem;
         else
         else
@@ -472,62 +472,66 @@ void SelectComponent(Component@ component, bool multiselect)
 
 
 void HandleNodeListSelectionChange()
 void HandleNodeListSelectionChange()
 {
 {
-    ListView@ list = sceneWindow.GetChild("NodeList", true);
+    ClearSelection();
 
 
+    ListView@ list = sceneWindow.GetChild("NodeList", true);
     Array<uint> indices = list.selections;
     Array<uint> indices = list.selections;
-    ClearSelection();
-    if (indices.length == 1)
+
+    for (uint i = 0; i < indices.length; ++i)
     {
     {
-        uint index = indices[0];
-        selectedNode = GetListNode(index);
-        if (selectedNode !is null)
-            selectedNodes.Push(selectedNode);
-        selectedComponent = GetListComponent(index);
-        if (selectedComponent !is null)
-            selectedComponents.Push(selectedComponent);
+        uint index = indices[i];
+        UIElement@ item = list.items[index];
+        int type = item.vars["Type"].GetInt();
+        if (type == ITEM_COMPONENT)
+        {
+            Component@ comp = GetListComponent(index);
+            if (comp !is null)
+                selectedComponents.Push(comp);
+        }
+        else if (type == ITEM_NODE)
+        {
+            Node@ node = GetListNode(index);
+            if (node !is null)
+                selectedNodes.Push(node);
+        }
     }
     }
-    else
+
+    // If only one node selected, use it for editing
+    if (selectedNodes.length == 1)
+        editNode = selectedNodes[0];
+
+    // If selection contains only components, and they have a common node, use it for editing
+    if (selectedNodes.empty && !selectedComponents.empty)
     {
     {
-        for (uint i = 0; i < indices.length; ++i)
+        Node@ commonNode;
+        for (uint i = 0; i < selectedComponents.length; ++i)
         {
         {
-            uint index = indices[i];
-            UIElement@ item = list.items[index];
-            int type = item.vars["Type"].GetInt();
-            if (type == ITEM_COMPONENT)
+            if (i == 0)
+                commonNode = selectedComponents[i].node;
+            else
             {
             {
-                Component@ comp = GetListComponent(index);
-                if (comp !is null)
-                    selectedComponents.Push(comp);
-            }
-            else if (type == ITEM_NODE)
-            {
-                Node@ node = GetListNode(index);
-                if (node !is null)
-                    selectedNodes.Push(node);
+                if (selectedComponents[i].node !is commonNode)
+                    commonNode = null;
             }
             }
         }
         }
-
-        // If only one node is selected in a multi-select, show it in the node window
-        if (selectedNodes.length == 1)
-            selectedNode = selectedNodes[0];
-        else if (selectedComponents.length == 1)
-            selectedComponent = selectedComponents[0];
-        // If selection contains only components, and they have a common node, show it in the node window
-        else if (selectedNodes.empty && selectedComponents.length > 1)
+        editNode = commonNode;
+    }
+    
+    // Now check if the component(s) can be edited. If many selected, must have same type
+    if (!selectedComponents.empty)
+    {
+        ShortStringHash compType = selectedComponents[0].type;
+        bool sameType = true;
+        for (uint i = 1; i < selectedComponents.length; ++i)
         {
         {
-            Node@ commonNode;
-            for (uint i = 0; i < selectedComponents.length; ++i)
+            if (selectedComponents[i].type != compType)
             {
             {
-                if (i == 0)
-                    commonNode = selectedComponents[i].node;
-                else
-                {
-                    if (selectedComponents[i].node !is commonNode)
-                        commonNode = null;
-                }
+                sameType = false;
+                break;
             }
             }
-            selectedNode = commonNode;
         }
         }
+        if (sameType)
+            editComponents = selectedComponents;
     }
     }
 
 
     UpdateNodeWindow();
     UpdateNodeWindow();
@@ -552,8 +556,6 @@ void HandleNodeListItemDoubleClick(StringHash eventType, VariantMap& eventData)
 void HandleNodeListKey(StringHash eventType, VariantMap& eventData)
 void HandleNodeListKey(StringHash eventType, VariantMap& eventData)
 {
 {
     int key = eventData["Key"].GetInt();
     int key = eventData["Key"].GetInt();
-
-    /// \todo Add required functionality
 }
 }
 
 
 void HandleDragDropTest(StringHash eventType, VariantMap& eventData)
 void HandleDragDropTest(StringHash eventType, VariantMap& eventData)
@@ -685,7 +687,7 @@ void HandleCreateNode(StringHash eventType, VariantMap& eventData)
 
 
 void HandleCreateComponent(StringHash eventType, VariantMap& eventData)
 void HandleCreateComponent(StringHash eventType, VariantMap& eventData)
 {
 {
-    if (selectedNode is null)
+    if (editNode is null)
         return;
         return;
 
 
     DropDownList@ list = eventData["Element"].GetUIElement();
     DropDownList@ list = eventData["Element"].GetUIElement();
@@ -694,12 +696,12 @@ void HandleCreateComponent(StringHash eventType, VariantMap& eventData)
         return;
         return;
 
 
     // If this is the root node, do not allow to create duplicate scene-global components
     // If this is the root node, do not allow to create duplicate scene-global components
-    if (selectedNode is editorScene && CheckForExistingGlobalComponent(selectedNode, text.text))
+    if (editNode is editorScene && CheckForExistingGlobalComponent(editNode, text.text))
         return;
         return;
 
 
     // For now, make a local node's all components local
     // For now, make a local node's all components local
     /// \todo Allow to specify the createmode
     /// \todo Allow to specify the createmode
-    Component@ newComponent = selectedNode.CreateComponent(text.text, selectedNode.id < FIRST_LOCAL_ID ? REPLICATED : LOCAL);
+    Component@ newComponent = editNode.CreateComponent(text.text, editNode.id < FIRST_LOCAL_ID ? REPLICATED : LOCAL);
 
 
     UpdateAndFocusComponent(newComponent);
     UpdateAndFocusComponent(newComponent);
 }
 }
@@ -723,90 +725,122 @@ bool CheckForExistingGlobalComponent(Node@ node, const String&in typeName)
         return node.HasComponent(typeName);
         return node.HasComponent(typeName);
 }
 }
 
 
-void SceneDelete()
+bool SceneDelete()
 {
 {
-    if (selectedNode is null || !CheckSceneWindowFocus())
-        return;
+    if (!CheckSceneWindowFocus() || (selectedComponents.empty && selectedNodes.empty))
+        return false;
 
 
     ListView@ list = sceneWindow.GetChild("NodeList", true);
     ListView@ list = sceneWindow.GetChild("NodeList", true);
-    uint index = list.selection;
-    uint nodeIndex = GetNodeListIndex(selectedNode);
 
 
-    // Remove component
-    if (selectedComponent !is null)
+    // Remove components first
+    for (uint i = 0; i < selectedComponents.length; ++i)
     {
     {
         // Do not allow to remove the Octree, PhysicsWorld or DebugRenderer from the root node
         // Do not allow to remove the Octree, PhysicsWorld or DebugRenderer from the root node
-        if (selectedNode is editorScene && (selectedComponent.typeName == "Octree" || selectedComponent.typeName ==
-            "PhysicsWorld" || selectedComponent.typeName == "DebugRenderer"))
-            return;
-
-        uint id = selectedNode.id;
+        Component@ component = selectedComponents[i];
+        Node@ node = component.node;
+        
+        uint index = GetComponentListIndex(component);
+        uint nodeIndex = GetNodeListIndex(node);
+        if (index == NO_ITEM || nodeIndex == NO_ITEM)
+            continue;
+
+        if (node is editorScene && (component.typeName == "Octree" || component.typeName == "PhysicsWorld" ||
+            component.typeName == "DebugRenderer"))
+            continue;
+
+        uint id = node.id;
         BeginModify(id);
         BeginModify(id);
-        selectedNode.RemoveComponent(selectedComponent);
+        node.RemoveComponent(component);
         EndModify(id);
         EndModify(id);
 
 
-        UpdateSceneWindowNode(nodeIndex, selectedNode);
+        UpdateSceneWindowNode(nodeIndex, node);
 
 
-        // Select the next item in the same index
-        list.selection = index;
+        // If deleting only one component, select the next item in the same index
+        if (selectedComponents.length == 1 && selectedNodes.empty)
+        {
+            list.selection = index;
+            return true;
+        }
     }
     }
-    // Remove (parented) node
-    else
+
+    // Remove (parented) nodes last
+    for (uint i = 0; i < selectedNodes.length; ++i)
     {
     {
-        if (selectedNode.parent is null)
-            return;
+        Node@ node = selectedNodes[i];
+        if (node.parent is null)
+            continue;
 
 
-        uint id = selectedNode.id;
+        uint id = node.id;
+        uint nodeIndex = GetNodeListIndex(node);
 
 
         BeginModify(id);
         BeginModify(id);
-        selectedNode.Remove();
+        node.Remove();
         EndModify(id);
         EndModify(id);
 
 
-        ClearSelection();
-
         UpdateSceneWindowNode(nodeIndex, null);
         UpdateSceneWindowNode(nodeIndex, null);
 
 
         // Select the next item in the same index
         // Select the next item in the same index
-        list.selection = index;
+
+        // If deleting only one node, select the next item in the same index
+        if (selectedNodes.length == 1 && selectedComponents.empty)
+        {
+            list.selection = nodeIndex;
+            return true;
+        }
     }
     }
+
+    // If any kind of multi-delete was performed, the list selection should be clear now.
+    // Unfortunately that also means we did not get selection change events, so must update the selection arrays manually.
+    // Otherwise nodes/components may be left in the scene even after delete, as the selection arrays keep them alive.
+    HandleNodeListSelectionChange();
+    return true;
 }
 }
 
 
-void SceneCut()
+bool SceneCut()
 {
 {
-    SceneCopy();
-    SceneDelete();
+    if (SceneCopy())
+        return SceneDelete();
+    else
+        return false;
 }
 }
 
 
-void SceneCopy()
+bool SceneCopy()
 {
 {
-    if (selectedNode is null || !CheckSceneWindowFocus())
-        return;
+    /// \todo allow multi-copy
+    if (editNode is null || !CheckSceneWindowFocus())
+        return false;
+    Component@ editComponent = editComponents.length == 1 ? editComponents[0] : null;
 
 
     ListView@ list = sceneWindow.GetChild("NodeList", true);
     ListView@ list = sceneWindow.GetChild("NodeList", true);
 
 
     // Copy component
     // Copy component
-    if (selectedNode !is null && selectedComponent !is null)
+    if (editNode !is null && editComponent !is null)
     {
     {
         XMLElement rootElem = copyBuffer.CreateRoot("component");
         XMLElement rootElem = copyBuffer.CreateRoot("component");
-        selectedComponent.SaveXML(rootElem);
+        editComponent.SaveXML(rootElem);
         // Note: component type has to be saved manually
         // Note: component type has to be saved manually
-        rootElem.SetString("type", selectedComponent.typeName);
-        copyBufferLocal = selectedComponent.id >= FIRST_LOCAL_ID;
+        rootElem.SetString("type", editComponent.typeName);
+        copyBufferLocal = editComponent.id >= FIRST_LOCAL_ID;
+        return true;
     }
     }
     // Copy node. The root node can not be copied
     // Copy node. The root node can not be copied
-    else if (selectedNode !is null && selectedComponent is null && selectedNode !is editorScene)
+    else if (editNode !is null && editComponent is null && editNode !is editorScene)
     {
     {
         XMLElement rootElem = copyBuffer.CreateRoot("node");
         XMLElement rootElem = copyBuffer.CreateRoot("node");
-        selectedNode.SaveXML(rootElem);
-        copyBufferLocal = selectedNode.id >= FIRST_LOCAL_ID;
-        copyBufferExpanded = SaveExpandedStatus(GetNodeListIndex(selectedNode));
+        editNode.SaveXML(rootElem);
+        copyBufferLocal = editNode.id >= FIRST_LOCAL_ID;
+        copyBufferExpanded = SaveExpandedStatus(GetNodeListIndex(editNode));
+        return true;
     }
     }
+    
+    return false;
 }
 }
 
 
-void ScenePaste()
+bool ScenePaste()
 {
 {
-    if (selectedNode is null || !CheckSceneWindowFocus())
-        return;
+    /// \todo allow multi-paste
+    if (editNode is null || !CheckSceneWindowFocus())
+        return false;
 
 
     ListView@ list = sceneWindow.GetChild("NodeList", true);
     ListView@ list = sceneWindow.GetChild("NodeList", true);
     XMLElement rootElem = copyBuffer.root;
     XMLElement rootElem = copyBuffer.root;
@@ -814,26 +848,27 @@ void ScenePaste()
 
 
     if (mode == "component")
     if (mode == "component")
     {
     {
-        BeginModify(selectedNode.id);
+        BeginModify(editNode.id);
 
 
         // If this is the root node, do not allow to create duplicate scene-global components
         // If this is the root node, do not allow to create duplicate scene-global components
-        if (selectedNode is editorScene && CheckForExistingGlobalComponent(selectedNode, rootElem.GetAttribute("type")))
-            return;
+        if (editNode is editorScene && CheckForExistingGlobalComponent(editNode, rootElem.GetAttribute("type")))
+            return false;
 
 
         // If copied component was local, make the new local too
         // If copied component was local, make the new local too
-        Component@ newComponent = selectedNode.CreateComponent(rootElem.GetAttribute("type"), copyBufferLocal ? LOCAL :
+        Component@ newComponent = editNode.CreateComponent(rootElem.GetAttribute("type"), copyBufferLocal ? LOCAL :
             REPLICATED);
             REPLICATED);
         if (newComponent is null)
         if (newComponent is null)
         {
         {
-            EndModify(selectedNode.id);
-            return;
+            EndModify(editNode.id);
+            return false;
         }
         }
         newComponent.LoadXML(rootElem);
         newComponent.LoadXML(rootElem);
         newComponent.ApplyAttributes();
         newComponent.ApplyAttributes();
-        EndModify(selectedNode.id);
+        EndModify(editNode.id);
 
 
-        UpdateSceneWindowNode(selectedNode);
+        UpdateSceneWindowNode(editNode);
         list.selection = GetComponentListIndex(newComponent);
         list.selection = GetComponentListIndex(newComponent);
+        return true;
     }
     }
     else if (mode == "node")
     else if (mode == "node")
     {
     {
@@ -851,7 +886,10 @@ void ScenePaste()
         UpdateSceneWindowNode(addIndex, newNode);
         UpdateSceneWindowNode(addIndex, newNode);
         RestoreExpandedStatus(addIndex, copyBufferExpanded);
         RestoreExpandedStatus(addIndex, copyBufferExpanded);
         list.selection = addIndex;
         list.selection = addIndex;
+        return true;
     }
     }
+    
+    return false;
 }
 }
 
 
 bool SaveExpandedStatus(uint itemIndex)
 bool SaveExpandedStatus(uint itemIndex)

+ 4 - 1
Docs/GettingStarted.dox

@@ -503,7 +503,8 @@ Hint: to get some content to look at, run the TestScene example, and press F5. T
 \section EditorInstructions_Controls Controls
 \section EditorInstructions_Controls Controls
 
 
 \verbatim
 \verbatim
-Left mouse      - Select components. Hold Shift to select nodes.
+Left mouse      - Select nodes. Hold Shift to select components instead. Hold 
+                  Ctrl to multiselect.
 Right mouse     - Hold down and move mouse to rotate camera
 Right mouse     - Hold down and move mouse to rotate camera
 
 
 WSAD or arrows  - Move
 WSAD or arrows  - Move
@@ -559,6 +560,8 @@ While editing, you can execute script files using the "Run script" item in the %
 
 
 Currently, when you edit for example a material or texture, you need to manually reload scene resources (Ctrl+R) to make the changes visible.
 Currently, when you edit for example a material or texture, you need to manually reload scene resources (Ctrl+R) to make the changes visible.
 
 
+Components of same type can be multi-edited. Where attribute values differ, the attribute field will be left blank, but editing the attribute will apply the change to all components.
+
 \section EditorInstructions_Importing Importing
 \section EditorInstructions_Importing Importing
 
 
 The editor can import models or scenes from all the formats that the Open Asset Import Library supports, see http://assimp.sourceforge.net/main_features_formats.html
 The editor can import models or scenes from all the formats that the Open Asset Import Library supports, see http://assimp.sourceforge.net/main_features_formats.html