Przeglądaj źródła

Added GUIDictionaryField (WIP)
Added a generic method for cloning managed objects

BearishSun 10 lat temu
rodzic
commit
a92c1b167f

+ 510 - 0
MBansheeEditor/GUI/GUIDictionaryField.cs

@@ -0,0 +1,510 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using BansheeEngine;
+
+namespace BansheeEditor
+{
+    /// <summary>
+    /// Base class for objects that display GUI for a modifyable dictionary of elements. Elements can be added, modified or
+    /// removed.
+    /// </summary>
+    public abstract class GUIDictionaryFieldBase
+    {
+        private const int IndentAmount = 5;
+
+        protected List<GUIDictionaryFieldRow> rows = new List<GUIDictionaryFieldRow>();
+        protected GUILayoutX guiChildLayout;
+        protected GUILayoutX guiTitleLayout;
+        protected GUILayoutY guiContentLayout;
+        protected bool isExpanded;
+        protected int depth;
+
+        /// <summary>
+        /// Constructs a new GUI dictionary.
+        /// </summary>
+        protected GUIDictionaryFieldBase()
+        { }
+
+        /// <summary>
+        /// Updates the GUI dictionary contents. Must be called at least once in order for the contents to be populated.
+        /// </summary>
+        /// <typeparam name="T">Type of rows that are used to handle GUI for individual dictionary elements.</typeparam>
+        /// <param name="title">Label to display on the dictionary GUI title.</param>
+        /// <param name="empty">Should the created field represent a null object.</param>
+        /// <param name="numRows">Number of rows to create GUI for. Only matters for a non-null dictionary.</param>
+        /// <param name="layout">Layout to which to append the list GUI elements to.</param>
+        /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
+        ///                     nested containers whose backgrounds are overlaping. Also determines background style,
+        ///                     depths divisible by two will use an alternate style.</param>
+        protected void Update<T>(LocString title, bool empty, int numRows, GUILayout layout,
+            int depth = 0) where T : GUIDictionaryFieldRow, new()
+        {
+            Destroy();
+
+            this.depth = depth;
+
+            if (empty)
+            {
+                guiChildLayout = null;
+                guiContentLayout = null;
+                guiTitleLayout = layout.AddLayoutX();
+
+                guiTitleLayout.AddElement(new GUILabel(title));
+                guiTitleLayout.AddElement(new GUILabel("Empty", GUIOption.FixedWidth(100)));
+
+                GUIContent createIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Create));
+                GUIButton createBtn = new GUIButton(createIcon, GUIOption.FixedWidth(30));
+                createBtn.OnClick += OnCreateButtonClicked;
+                guiTitleLayout.AddElement(createBtn);
+            }
+            else
+            {
+                GUIToggle guiFoldout = new GUIToggle(title, EditorStyles.Foldout);
+                guiFoldout.Value = isExpanded;
+                guiFoldout.OnToggled += OnFoldoutToggled;
+
+                GUIContent clearIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Clear));
+                GUIButton guiClearBtn = new GUIButton(clearIcon, GUIOption.FixedWidth(30));
+                guiClearBtn.OnClick += OnClearButtonClicked;
+
+                guiTitleLayout = layout.AddLayoutX();
+                guiTitleLayout.AddElement(guiFoldout);
+                guiTitleLayout.AddElement(guiClearBtn);
+
+                if (numRows > 0)
+                {
+                    guiChildLayout = layout.AddLayoutX();
+                    guiChildLayout.AddSpace(IndentAmount);
+                    guiChildLayout.Enabled = isExpanded;
+
+                    GUIPanel guiContentPanel = guiChildLayout.AddPanel();
+                    GUILayoutX guiIndentLayoutX = guiContentPanel.AddLayoutX();
+                    guiIndentLayoutX.AddSpace(IndentAmount);
+                    GUILayoutY guiIndentLayoutY = guiIndentLayoutX.AddLayoutY();
+                    guiIndentLayoutY.AddSpace(IndentAmount);
+                    guiContentLayout = guiIndentLayoutY.AddLayoutY();
+                    guiIndentLayoutY.AddSpace(IndentAmount);
+                    guiIndentLayoutX.AddSpace(IndentAmount);
+                    guiChildLayout.AddSpace(IndentAmount);
+
+                    short backgroundDepth = (short)(Inspector.START_BACKGROUND_DEPTH - depth - 1);
+                    string bgPanelStyle = depth % 2 == 0
+                        ? EditorStyles.InspectorContentBgAlternate
+                        : EditorStyles.InspectorContentBg;
+
+                    GUIPanel backgroundPanel = guiContentPanel.AddPanel(backgroundDepth);
+                    GUITexture inspectorContentBg = new GUITexture(null, bgPanelStyle);
+                    backgroundPanel.AddElement(inspectorContentBg);
+
+                    for (int i = 0; i < numRows; i++)
+                    {
+                        GUIDictionaryFieldRow newRow = new T();
+                        newRow.BuildGUI(this, guiContentLayout, i, depth);
+
+                        rows.Add(newRow);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns the layout that is used for positioning the elements in the title bar.
+        /// </summary>
+        /// <returns>Horizontal layout for positioning the title bar elements.</returns>
+        public GUILayoutX GetTitleLayout()
+        {
+            return guiTitleLayout;
+        }
+
+        /// <summary>
+        /// Refreshes contents of all dictionary rows and checks if anything was modified.
+        /// </summary>
+        /// <returns>True if any entry in the list was modified, false otherwise.</returns>
+        public bool Refresh()
+        {
+            bool anythingModified = false;
+
+            for (int i = 0; i < rows.Count; i++)
+            {
+                bool updateGUI;
+
+                anythingModified |= rows[i].Refresh(out updateGUI);
+
+                if (updateGUI)
+                    rows[i].BuildGUI(this, guiContentLayout, i, depth);
+            }
+
+            return anythingModified;
+        }
+
+        /// <summary>
+        /// Destroys the GUI elements.
+        /// </summary>
+        public void Destroy()
+        {
+            if (guiTitleLayout != null)
+            {
+                guiTitleLayout.Destroy();
+                guiTitleLayout = null;
+            }
+
+            if (guiChildLayout != null)
+            {
+                guiChildLayout.Destroy();
+                guiChildLayout = null;
+            }
+
+            for (int i = 0; i < rows.Count; i++)
+                rows[i].Destroy();
+
+            rows.Clear();
+        }
+
+        /// <summary>
+        /// Gets a value of an element at the specified index in the list.
+        /// </summary>
+        /// <param name="key">Key of the element whose value to retrieve.</param>
+        /// <returns>Value of the list element at the specified key.</returns>
+        protected internal abstract object GetValue(object key);
+
+        /// <summary>
+        /// Sets a value of an element at the specified index in the list.
+        /// </summary>
+        /// <param name="key">Key of the element whose value to set.</param>
+        /// <param name="value">Value to assign to the element. Caller must ensure it is of valid type.</param>
+        protected internal abstract void SetValue(object key, object value);
+
+        /// <summary>
+        /// Checks does the element with the specified key exist in the dictionary.
+        /// </summary>
+        /// <param name="key">Key of the element to check for existence.</param>
+        /// <returns>True if the key exists in the dictionary, false otherwise.</returns>
+        protected internal abstract bool Contains(object key);
+
+        /// <summary>
+        /// Triggered when the user clicks on the expand/collapse toggle in the title bar.
+        /// </summary>
+        /// <param name="expanded">Determines whether the contents were expanded or collapsed.</param>
+        private void OnFoldoutToggled(bool expanded)
+        {
+            isExpanded = expanded;
+
+            if (guiChildLayout != null)
+                guiChildLayout.Enabled = isExpanded;
+        }
+
+        /// <summary>
+        /// Triggered when the user clicks on the create button on the title bar. Creates a brand new dictionary with zero
+        /// elements in the place of the current dictionary.
+        /// </summary>
+        protected abstract void OnCreateButtonClicked();
+
+        /// <summary>
+        /// Triggered when the user clicks on the clear button on the title bar. Deletes the current dictionary object.
+        /// </summary>
+        protected abstract void OnClearButtonClicked();
+
+        /// <summary>
+        /// Triggered when the user clicks on the delete button next to a dictionary entry. Deletes an element in the 
+        /// dictionary.
+        /// </summary>
+        /// <param name="key">Key of the element to remove.</param>
+        protected internal abstract void OnDeleteButtonClicked(object key);
+
+        /// <summary>
+        /// Triggered when the user clicks on the clone button next to a dictionary entry. Clones an element and
+        /// adds the clone to the dictionary. 
+        /// </summary>
+        /// <param name="key">Key of the element to clone.</param>
+        protected internal abstract void OnCloneButtonClicked(object key);
+    }
+
+    /// <summary>
+    /// Creates GUI elements that allow viewing and manipulation of a <see cref="Dictionary{TKey,TValue}"/>. When constructing the
+    /// object user can provide a custom type that manages GUI for individual dictionary elements.
+    /// </summary>
+    /// <typeparam name="Key">Type of key used by the dictionary.</typeparam>
+    /// <typeparam name="Value">Type of value stored in the dictionary.</typeparam>
+    public class GUIDictionaryField<Key, Value> : GUIDictionaryFieldBase
+    {
+        /// <summary>
+        /// Triggered when the reference array has been changed. This does not include changes that only happen to its 
+        /// internal elements.
+        /// </summary>
+        public Action<IDictionary> OnChanged;
+
+        /// <summary>
+        /// Triggered when an element in the list has been changed.
+        /// </summary>
+        public Action OnValueChanged;
+
+        /// <summary>
+        /// Array object whose contents are displayed.
+        /// </summary>
+        public IDictionary Dictionary { get { return dictionary; } }
+
+        protected IDictionary dictionary;
+        protected Type keyType;
+        protected Type valueType;
+
+        /// <summary>
+        /// Constructs a new empty dictionary GUI.
+        /// </summary>
+        public GUIDictionaryField()
+        { }
+
+        /// <summary>
+        /// Updates the GUI dictionary contents. Must be called at least once in order for the contents to be populated.
+        /// </summary>
+        /// <typeparam name="RowType">Type of rows that are used to handle GUI for individual dictionary elements.</typeparam>
+        /// <param name="title">Label to display on the list GUI title.</param>
+        /// <param name="dictionary">Object containing the data. Can be null.</param>
+        /// <param name="layout">Layout to which to append the list GUI elements to.</param>
+        /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
+        ///                     nested containers whose backgrounds are overlaping. Also determines background style,
+        ///                     depths divisible by two will use an alternate style.</param>
+        public void Update<RowType>(LocString title, Dictionary<Key, Value> dictionary, 
+            GUILayout layout, int depth = 0)
+            where RowType : GUIDictionaryFieldRow, new()
+        {
+            this.keyType = typeof(Key);
+            this.valueType = typeof(Value);
+            this.dictionary = dictionary;
+
+            if (dictionary != null)
+                base.Update<RowType>(title, false, dictionary.Count, layout, depth);
+            else
+                base.Update<RowType>(title, true, 0, layout, depth);
+        }
+
+        /// <inheritdoc/>
+        protected internal override object GetValue(object key)
+        {
+            return dictionary[key];
+        }
+
+        /// <inheritdoc/>
+        protected internal override void SetValue(object key, object value)
+        {
+            dictionary[key] = value;
+
+            if (OnValueChanged != null)
+                OnValueChanged();
+        }
+
+        /// <inheritdoc/>
+        protected internal override bool Contains(object key)
+        {
+            return dictionary.Contains(key);;
+        }
+
+        /// <inheritdoc/>
+        protected override void OnCreateButtonClicked()
+        {
+            dictionary = new Dictionary<Key, Value>();
+
+            if (OnChanged != null)
+                OnChanged(dictionary);
+        }
+
+        /// <inheritdoc/>
+        protected override void OnClearButtonClicked()
+        {
+            dictionary = null;
+
+            if (OnChanged != null)
+                OnChanged(dictionary);
+        }
+
+        /// <inheritdoc/>
+        protected internal override void OnDeleteButtonClicked(object key)
+        {
+            dictionary.Remove(key);
+
+            if (OnValueChanged != null)
+                OnValueChanged();
+        }
+
+        /// <summary>
+        /// Triggered when the user clicks on the clone button next to a dictionary entry. Clones the element and adds the 
+        /// clone to the dictionary. Non-value types must implement the <see cref="ICloneable"/> interface in order to be 
+        /// cloned. If it doesn't the clone will point to a null reference.
+        /// </summary>
+        /// <param name="key">Key of the element to clone.</param>
+        protected internal override void OnCloneButtonClicked(object key)
+        {
+            // TODO - Not supported
+
+            //int size = array.GetLength(0) + 1;
+            //Array newArray = Array.CreateInstance(arrayType.GetElementType(), size);
+
+            //object clonedEntry = null;
+            //for (int i = 0; i < array.GetLength(0); i++)
+            //{
+            //    object value = array.GetValue(i);
+            //    newArray.SetValue(value, i);
+
+            //    if (i == index)
+            //    {
+            //        if (value == null)
+            //            clonedEntry = null;
+            //        else
+            //        {
+            //            ValueType valueType = value as ValueType;
+            //            if (valueType != null)
+            //                clonedEntry = valueType;
+            //            else
+            //            {
+            //                ICloneable cloneable = value as ICloneable;
+
+            //                if (cloneable != null)
+            //                    clonedEntry = cloneable.Clone();
+            //                else
+            //                    clonedEntry = null;
+            //            }
+            //        }
+            //    }
+            //}
+
+            //newArray.SetValue(clonedEntry, size - 1);
+
+            //array = newArray;
+
+            //if (OnChanged != null)
+            //    OnChanged(array);
+        }
+    }
+
+    /// <summary>
+    /// Contains GUI elements for a single entry in a dictionary.
+    /// </summary>
+    public abstract class GUIDictionaryFieldRow
+    {
+        private GUILayoutX rowLayout;
+        private GUILayoutY keyLayout;
+        private GUILayoutY valueLayout;
+        private GUILayoutX titleLayout;
+        private bool localTitleLayout;
+        private GUIDictionaryFieldBase parent;
+
+        protected object key;
+        protected int depth;
+
+        /// <summary>
+        /// Constructs a new dictionary row object.
+        /// </summary>
+        protected GUIDictionaryFieldRow()
+        {
+
+        }
+
+        /// <summary>
+        /// (Re)creates all row GUI elements.
+        /// </summary>
+        /// <param name="parent">Parent array GUI object that the entry is contained in.</param>
+        /// <param name="parentLayout">Parent layout that row GUI elements will be added to.</param>
+        /// <param name="key">Key of the element to create GUI for.</param>
+        /// <param name="depth">Determines the depth at which the element is rendered.</param>
+        public void BuildGUI(GUIDictionaryFieldBase parent, GUILayout parentLayout, object key, int depth)
+        {
+            this.parent = parent;
+            this.key = key;
+            this.depth = depth;
+
+            if (rowLayout == null)
+                rowLayout = parentLayout.AddLayoutX();
+
+            if (keyLayout == null)
+                keyLayout = rowLayout.AddLayoutY();
+
+            if (valueLayout == null)
+                valueLayout = rowLayout.AddLayoutY();
+
+            CreateKeyGUI(keyLayout);
+            GUILayoutX externalTitleLayout = CreateValueGUI(valueLayout);
+            if (localTitleLayout || (titleLayout != null && titleLayout == externalTitleLayout))
+                return;
+
+            if (externalTitleLayout != null)
+            {
+                localTitleLayout = false;
+                titleLayout = externalTitleLayout;
+            }
+            else
+            {
+                GUILayoutY buttonCenter = rowLayout.AddLayoutY();
+                buttonCenter.AddFlexibleSpace();
+                titleLayout = buttonCenter.AddLayoutX();
+                buttonCenter.AddFlexibleSpace();
+
+                localTitleLayout = true;
+            }
+
+            GUIContent cloneIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Clone));
+            GUIContent deleteIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Delete));
+
+            GUIButton cloneBtn = new GUIButton(cloneIcon, GUIOption.FixedWidth(30));
+            GUIButton deleteBtn = new GUIButton(deleteIcon, GUIOption.FixedWidth(30));
+
+            cloneBtn.OnClick += () => parent.OnCloneButtonClicked(key);
+            deleteBtn.OnClick += () => parent.OnDeleteButtonClicked(key);
+
+            titleLayout.AddElement(cloneBtn);
+            titleLayout.AddElement(deleteBtn);
+        }
+
+        /// <summary>
+        /// Creates GUI elements specific to type in the key portion of a dictionary entry.
+        /// </summary>
+        /// <param name="layout">Layout to insert the row GUI elements to.</param>
+        protected abstract void CreateKeyGUI(GUILayoutY layout);
+
+        /// <summary>
+        /// Creates GUI elements specific to type in the key portion of a dictionary entry.
+        /// </summary>
+        /// <param name="layout">Layout to insert the row GUI elements to.</param>
+        /// <returns>An optional title bar layout that the standard dictionary buttons will be appended to.</returns>
+        protected abstract GUILayoutX CreateValueGUI(GUILayoutY layout);
+
+        /// <summary>
+        /// Refreshes the GUI for the dictionary row and checks if anything was modified.
+        /// </summary>
+        /// <param name="rebuildGUI">Determines should the field's GUI elements be updated due to modifications.</param>
+        /// <returns>True if any modifications were made, false otherwise.</returns>
+        internal protected virtual bool Refresh(out bool rebuildGUI)
+        {
+            rebuildGUI = false;
+            return false;
+        }
+
+        /// <summary>
+        /// Gets the value contained in this dictionary's row.
+        /// </summary>
+        /// <typeparam name="T">Type of the value. Must match the dictionary's element type.</typeparam>
+        /// <returns>Value in this dictionary's row.</returns>
+        protected T GetValue<T>()
+        {
+            return (T)parent.GetValue(key);
+        }
+
+        /// <summary>
+        /// Sets the value contained in this dictionary's row.
+        /// </summary>
+        /// <typeparam name="T">Type of the value. Must match the dictionary's element type.</typeparam>
+        /// <param name="value">Value to set.</param>
+        protected void SetValue<T>(T value)
+        {
+            parent.SetValue(key, value);
+        }
+
+        /// <summary>
+        /// Destroys all row GUI elements.
+        /// </summary>
+        public void Destroy()
+        {
+            rowLayout.Destroy();
+            rowLayout = null;
+        }
+    }
+}

+ 10 - 28
MBansheeEditor/GUI/GUIListField.cs

@@ -1,9 +1,5 @@
 using System;
 using System;
-using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using BansheeEngine;
 using BansheeEngine;
 
 
 namespace BansheeEditor
 namespace BansheeEditor
@@ -35,7 +31,7 @@ namespace BansheeEditor
         /// <typeparam name="T">Type of rows that are used to handle GUI for individual list elements.</typeparam>
         /// <typeparam name="T">Type of rows that are used to handle GUI for individual list elements.</typeparam>
         /// <param name="title">Label to display on the list GUI title.</param>
         /// <param name="title">Label to display on the list GUI title.</param>
         /// <param name="empty">Should the created field represent a null object.</param>
         /// <param name="empty">Should the created field represent a null object.</param>
-        /// <param name="numRows">Number of rows to create GUI for. Only matters for a non-empty list.</param>
+        /// <param name="numRows">Number of rows to create GUI for. Only matters for a non-null list.</param>
         /// <param name="layout">Layout to which to append the list GUI elements to.</param>
         /// <param name="layout">Layout to which to append the list GUI elements to.</param>
         /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
         /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
         ///                     nested containers whose backgrounds are overlaping. Also determines background style,
         ///                     nested containers whose backgrounds are overlaping. Also determines background style,
@@ -218,28 +214,27 @@ namespace BansheeEditor
         protected abstract void OnClearButtonClicked();
         protected abstract void OnClearButtonClicked();
 
 
         /// <summary>
         /// <summary>
-        /// Triggered when the user clicks on the delete button next to the list entry. Deletes an element in the list.
+        /// Triggered when the user clicks on the delete button next to a list entry. Deletes an element in the list.
         /// </summary>
         /// </summary>
         /// <param name="index">Sequential index of the element in the list to remove.</param>
         /// <param name="index">Sequential index of the element in the list to remove.</param>
         protected internal abstract void OnDeleteButtonClicked(int index);
         protected internal abstract void OnDeleteButtonClicked(int index);
 
 
         /// <summary>
         /// <summary>
-        /// Triggered when the user clicks on the clone button next to the list entry. Clones an element in the list and
-        /// adds the clone to the back of the list. Non-value types must implement the <see cref="ICloneable"/> interface 
-        /// in order to be cloned. If it doesn't the clone will point to a null reference.
+        /// Triggered when the user clicks on the clone button next to a list entry. Clones the element and adds the clone 
+        /// to the back of the list. 
         /// </summary>
         /// </summary>
         /// <param name="index">Sequential index of the element in the list to clone.</param>
         /// <param name="index">Sequential index of the element in the list to clone.</param>
         protected internal abstract void OnCloneButtonClicked(int index);
         protected internal abstract void OnCloneButtonClicked(int index);
 
 
         /// <summary>
         /// <summary>
-        /// Triggered when the user clicks on the move up button next to the list entry. Moves an element from the current
+        /// Triggered when the user clicks on the move up button next to a list entry. Moves an element from the current
         /// list index to the one right before it, if not at zero.
         /// list index to the one right before it, if not at zero.
         /// </summary>
         /// </summary>
         /// <param name="index">Sequential index of the element in the list to move.</param>
         /// <param name="index">Sequential index of the element in the list to move.</param>
         protected internal abstract void OnMoveUpButtonClicked(int index);
         protected internal abstract void OnMoveUpButtonClicked(int index);
 
 
         /// <summary>
         /// <summary>
-        /// Triggered when the user clicks on the move down button next to the list entry. Moves an element from the current
+        /// Triggered when the user clicks on the move down button next to a list entry. Moves an element from the current
         /// list index to the one right after it, if the element isn't already the last element.
         /// list index to the one right after it, if the element isn't already the last element.
         /// </summary>
         /// </summary>
         /// <param name="index">Sequential index of the element in the list to move.</param>
         /// <param name="index">Sequential index of the element in the list to move.</param>
@@ -283,7 +278,7 @@ namespace BansheeEditor
         /// <typeparam name="RowType">Type of rows that are used to handle GUI for individual list elements.</typeparam>
         /// <typeparam name="RowType">Type of rows that are used to handle GUI for individual list elements.</typeparam>
         /// <typeparam name="ElementType">Type of elements stored in the array.</typeparam>
         /// <typeparam name="ElementType">Type of elements stored in the array.</typeparam>
         /// <param name="title">Label to display on the list GUI title.</param>
         /// <param name="title">Label to display on the list GUI title.</param>
-        /// <param name="array">Object containing the list data. Cannot be null.</param>
+        /// <param name="array">Object containing the list data. Can be null.</param>
         /// <param name="layout">Layout to which to append the list GUI elements to.</param>
         /// <param name="layout">Layout to which to append the list GUI elements to.</param>
         /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
         /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
         ///                     nested containers whose backgrounds are overlaping. Also determines background style,
         ///                     nested containers whose backgrounds are overlaping. Also determines background style,
@@ -390,20 +385,7 @@ namespace BansheeEditor
                     if (value == null)
                     if (value == null)
                         clonedEntry = null;
                         clonedEntry = null;
                     else
                     else
-                    {
-                        ValueType valueType = value as ValueType;
-                        if (valueType != null)
-                            clonedEntry = valueType;
-                        else
-                        {
-                            ICloneable cloneable = value as ICloneable;
-
-                            if (cloneable != null)
-                                clonedEntry = cloneable.Clone();
-                            else
-                                clonedEntry = null;
-                        }
-                    }
+                        clonedEntry = SerializableUtility.Clone(value);
                 }
                 }
             }
             }
 
 
@@ -461,7 +443,7 @@ namespace BansheeEditor
         protected int depth;
         protected int depth;
 
 
         /// <summary>
         /// <summary>
-        /// Constructs a new array row object.
+        /// Constructs a new list row object.
         /// </summary>
         /// </summary>
         protected GUIListFieldRow()
         protected GUIListFieldRow()
         {
         {
@@ -488,7 +470,7 @@ namespace BansheeEditor
                 contentLayout = rowLayout.AddLayoutY();
                 contentLayout = rowLayout.AddLayoutY();
 
 
             GUILayoutX externalTitleLayout = CreateGUI(contentLayout);
             GUILayoutX externalTitleLayout = CreateGUI(contentLayout);
-            if (localTitleLayout || titleLayout == externalTitleLayout)
+            if (localTitleLayout || (titleLayout != null && titleLayout == externalTitleLayout))
                 return;
                 return;
 
 
             if (externalTitleLayout != null)
             if (externalTitleLayout != null)

+ 1 - 0
MBansheeEditor/MBansheeEditor.csproj

@@ -48,6 +48,7 @@
     <Compile Include="DropDownWindow.cs" />
     <Compile Include="DropDownWindow.cs" />
     <Compile Include="FolderMonitor.cs" />
     <Compile Include="FolderMonitor.cs" />
     <Compile Include="GameWindow.cs" />
     <Compile Include="GameWindow.cs" />
+    <Compile Include="GUI\GUIDictionaryField.cs" />
     <Compile Include="GUI\GUIListField.cs" />
     <Compile Include="GUI\GUIListField.cs" />
     <Compile Include="GUI\GUIEnumField.cs" />
     <Compile Include="GUI\GUIEnumField.cs" />
     <Compile Include="GUI\GUIListBoxField.cs" />
     <Compile Include="GUI\GUIListBoxField.cs" />

+ 1 - 0
MBansheeEngine/MBansheeEngine.csproj

@@ -131,6 +131,7 @@
     <Compile Include="SerializableList.cs" />
     <Compile Include="SerializableList.cs" />
     <Compile Include="SerializableObject.cs" />
     <Compile Include="SerializableObject.cs" />
     <Compile Include="SerializableProperty.cs" />
     <Compile Include="SerializableProperty.cs" />
+    <Compile Include="SerializableUtility.cs" />
     <Compile Include="SerializeObject.cs" />
     <Compile Include="SerializeObject.cs" />
     <Compile Include="SerializeField.cs" />
     <Compile Include="SerializeField.cs" />
     <Compile Include="Shader.cs" />
     <Compile Include="Shader.cs" />

+ 28 - 0
MBansheeEngine/SerializableUtility.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace BansheeEngine
+{
+    /// <summary>
+    /// Provides utility methods dealing with object serialization.
+    /// </summary>
+    public static class SerializableUtility
+    {
+        /// <summary>
+        /// Clones the specified object. Non-serializable types and fields are ignored in clone. A deep copy is performed
+        /// on all serializable elements except for resources or game objects.
+        /// </summary>
+        /// <param name="original">Non-null reference to the object to clone. Object type must be serializable.</param>
+        /// <returns>Deep copy of the original object.</returns>
+        public static object Clone(object original)
+        {
+            return Internal_Clone(original);
+        }
+
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern object Internal_Clone(object original);
+    }
+}

+ 6 - 6
SBansheeEngine/Include/BsScriptAssemblyManager.h

@@ -40,6 +40,12 @@ namespace BansheeEngine
 		 */
 		 */
 		bool getSerializableObjectInfo(const String& ns, const String& typeName, std::shared_ptr<ManagedSerializableObjectInfo>& outInfo);
 		bool getSerializableObjectInfo(const String& ns, const String& typeName, std::shared_ptr<ManagedSerializableObjectInfo>& outInfo);
 
 
+		/**
+		 * @brief	Generates or retrieves a type info object for the specified managed class,
+		 *			if the class is serializable.
+		 */
+		ManagedSerializableTypeInfoPtr getTypeInfo(MonoClass* monoClass);
+
 		/**
 		/**
 		 * @brief	Checks if the managed serializable object info for the specified type exists.
 		 * @brief	Checks if the managed serializable object info for the specified type exists.
 		 *
 		 *
@@ -90,12 +96,6 @@ namespace BansheeEngine
 		 * @brief	Gets the managed class for BansheeEngine.SceneObject type.
 		 * @brief	Gets the managed class for BansheeEngine.SceneObject type.
 		 */
 		 */
 		MonoClass* getSceneObjectClass() const { return mSceneObjectClass; }
 		MonoClass* getSceneObjectClass() const { return mSceneObjectClass; }
-
-		/**
-		 * @brief	Generates or retrieves a type info object for the specified managed class,
-		 *			if the class is serializable.
-		 */
-		ManagedSerializableTypeInfoPtr determineType(MonoClass* monoClass);
 	private:
 	private:
 		/**
 		/**
 		 * @brief	Deletes all stored managed serializable object infos for all assemblies.
 		 * @brief	Deletes all stored managed serializable object infos for all assemblies.

+ 24 - 0
SBansheeEngine/Include/BsScriptSerializableUtility.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include "BsScriptEnginePrerequisites.h"
+#include "BsScriptObject.h"
+
+namespace BansheeEngine
+{
+	/**
+	 * @brief	Implements external methods for the SerializableUtility managed class.
+	 */
+	class BS_SCR_BE_EXPORT ScriptSerializableUtility : public ScriptObject<ScriptSerializableUtility>
+	{
+	public:
+		SCRIPT_OBJ(ENGINE_ASSEMBLY, "BansheeEngine", "SerializableUtility")
+
+	private:
+		ScriptSerializableUtility(MonoObject* instance);
+
+		/************************************************************************/
+		/* 								CLR HOOKS						   		*/
+		/************************************************************************/
+		static MonoObject* internal_Clone(MonoObject* original);
+	};
+}

+ 2 - 0
SBansheeEngine/SBansheeEngine.vcxproj

@@ -321,6 +321,7 @@
     <ClInclude Include="Include\BsScriptSerializableList.h" />
     <ClInclude Include="Include\BsScriptSerializableList.h" />
     <ClInclude Include="Include\BsScriptSerializableObject.h" />
     <ClInclude Include="Include\BsScriptSerializableObject.h" />
     <ClInclude Include="Include\BsScriptSerializableProperty.h" />
     <ClInclude Include="Include\BsScriptSerializableProperty.h" />
+    <ClInclude Include="Include\BsScriptSerializableUtility.h" />
     <ClInclude Include="Include\BsScriptShader.h" />
     <ClInclude Include="Include\BsScriptShader.h" />
     <ClInclude Include="Include\BsScriptSpriteTexture.h" />
     <ClInclude Include="Include\BsScriptSpriteTexture.h" />
     <ClInclude Include="Include\BsScriptStringTable.h" />
     <ClInclude Include="Include\BsScriptStringTable.h" />
@@ -418,6 +419,7 @@
     <ClCompile Include="Source\BsScriptSerializableList.cpp" />
     <ClCompile Include="Source\BsScriptSerializableList.cpp" />
     <ClCompile Include="Source\BsScriptSerializableObject.cpp" />
     <ClCompile Include="Source\BsScriptSerializableObject.cpp" />
     <ClCompile Include="Source\BsScriptSerializableProperty.cpp" />
     <ClCompile Include="Source\BsScriptSerializableProperty.cpp" />
+    <ClCompile Include="Source\BsScriptSerializableUtility.cpp" />
     <ClCompile Include="Source\BsScriptShader.cpp" />
     <ClCompile Include="Source\BsScriptShader.cpp" />
     <ClCompile Include="Source\BsScriptSpriteTexture.cpp" />
     <ClCompile Include="Source\BsScriptSpriteTexture.cpp" />
     <ClCompile Include="Source\BsScriptStringTable.cpp" />
     <ClCompile Include="Source\BsScriptStringTable.cpp" />

+ 6 - 0
SBansheeEngine/SBansheeEngine.vcxproj.filters

@@ -345,6 +345,9 @@
     <ClInclude Include="Include\BsScriptFontBitmap.h">
     <ClInclude Include="Include\BsScriptFontBitmap.h">
       <Filter>Header Files</Filter>
       <Filter>Header Files</Filter>
     </ClInclude>
     </ClInclude>
+    <ClInclude Include="Include\BsScriptSerializableUtility.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <ClCompile Include="Source\BsScriptTexture2D.cpp">
     <ClCompile Include="Source\BsScriptTexture2D.cpp">
@@ -629,5 +632,8 @@
     <ClCompile Include="Source\BsScriptFontBitmap.cpp">
     <ClCompile Include="Source\BsScriptFontBitmap.cpp">
       <Filter>Source Files</Filter>
       <Filter>Source Files</Filter>
     </ClCompile>
     </ClCompile>
+    <ClCompile Include="Source\BsScriptSerializableUtility.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
   </ItemGroup>
 </Project>
 </Project>

+ 6 - 6
SBansheeEngine/Source/BsScriptAssemblyManager.cpp

@@ -112,7 +112,7 @@ namespace BansheeEngine
 				if(field->isStatic())
 				if(field->isStatic())
 					continue;
 					continue;
 
 
-				ManagedSerializableTypeInfoPtr typeInfo = determineType(field->getType());
+				ManagedSerializableTypeInfoPtr typeInfo = getTypeInfo(field->getType());
 				if (typeInfo == nullptr)
 				if (typeInfo == nullptr)
 					continue;
 					continue;
 
 
@@ -169,7 +169,7 @@ namespace BansheeEngine
 		mAssemblyInfos.clear();
 		mAssemblyInfos.clear();
 	}
 	}
 
 
-	ManagedSerializableTypeInfoPtr ScriptAssemblyManager::determineType(MonoClass* monoClass)
+	ManagedSerializableTypeInfoPtr ScriptAssemblyManager::getTypeInfo(MonoClass* monoClass)
 	{
 	{
 		if(!mBaseTypesInitialized)
 		if(!mBaseTypesInitialized)
 			BS_EXCEPT(InvalidStateException, "Calling determineType without previously initializing base types.");
 			BS_EXCEPT(InvalidStateException, "Calling determineType without previously initializing base types.");
@@ -388,7 +388,7 @@ namespace BansheeEngine
 				MonoClass* itemClass = itemProperty.getReturnType();
 				MonoClass* itemClass = itemProperty.getReturnType();
 
 
 				if(itemClass != nullptr)
 				if(itemClass != nullptr)
-					typeInfo->mElementType = determineType(itemClass);
+					typeInfo->mElementType = getTypeInfo(itemClass);
 
 
 				return typeInfo;
 				return typeInfo;
 			}
 			}
@@ -407,11 +407,11 @@ namespace BansheeEngine
 
 
 				MonoClass* keyClass = keyProperty.getReturnType();
 				MonoClass* keyClass = keyProperty.getReturnType();
 				if(keyClass != nullptr)
 				if(keyClass != nullptr)
-					typeInfo->mKeyType = determineType(keyClass);
+					typeInfo->mKeyType = getTypeInfo(keyClass);
 
 
 				MonoClass* valueClass = valueProperty.getReturnType();
 				MonoClass* valueClass = valueProperty.getReturnType();
 				if(valueClass != nullptr)
 				if(valueClass != nullptr)
-					typeInfo->mValueType = determineType(valueClass);
+					typeInfo->mValueType = getTypeInfo(valueClass);
 
 
 				return typeInfo;
 				return typeInfo;
 			}
 			}
@@ -426,7 +426,7 @@ namespace BansheeEngine
 				{
 				{
 					MonoClass* monoElementClass = MonoManager::instance().findClass(elementClass);
 					MonoClass* monoElementClass = MonoManager::instance().findClass(elementClass);
 					if(monoElementClass != nullptr)
 					if(monoElementClass != nullptr)
-						typeInfo->mElementType = determineType(monoElementClass);
+						typeInfo->mElementType = getTypeInfo(monoElementClass);
 				}
 				}
 
 
 				typeInfo->mRank = (UINT32)mono_class_get_rank(monoClass->_getInternalClass());
 				typeInfo->mRank = (UINT32)mono_class_get_rank(monoClass->_getInternalClass());

+ 42 - 0
SBansheeEngine/Source/BsScriptSerializableUtility.cpp

@@ -0,0 +1,42 @@
+#include "BsScriptSerializableUtility.h"
+#include "BsMonoManager.h"
+#include "BsMonoClass.h"
+#include "BsMonoMethod.h"
+#include "BsMonoUtil.h"
+#include "BsScriptAssemblyManager.h"
+#include "BsManagedSerializableField.h"
+#include "BsMemorySerializer.h"
+
+namespace BansheeEngine
+{
+	ScriptSerializableUtility::ScriptSerializableUtility(MonoObject* instance)
+		:ScriptObject(instance)
+	{ }
+
+	void ScriptSerializableUtility::initRuntimeData()
+	{
+		metaData.scriptClass->addInternalCall("Internal_Clone", &ScriptSerializableUtility::internal_Clone);
+	}
+
+	MonoObject* ScriptSerializableUtility::internal_Clone(MonoObject* original)
+	{
+		if (original == nullptr)
+			return nullptr;
+
+		::MonoClass* monoClass = mono_object_get_class(original);
+		MonoClass* engineClass = MonoManager::instance().findClass(monoClass);
+
+		ManagedSerializableTypeInfoPtr typeInfo = ScriptAssemblyManager::instance().getTypeInfo(engineClass);
+		ManagedSerializableFieldDataPtr data = ManagedSerializableFieldData::create(typeInfo, original);
+
+		MemorySerializer ms;
+
+		// Note: This code unnecessarily encodes to binary and decodes from it. I could have added a specialized clone method that does it directly,
+		// but didn't feel the extra code was justified.
+		UINT32 size = 0;
+		UINT8* encodedData = ms.encode(data.get(), size);
+		ManagedSerializableFieldDataPtr clonedData = std::static_pointer_cast<ManagedSerializableFieldData>(ms.decode(encodedData, size));
+
+		return clonedData->getValueBoxed(typeInfo);
+	}
+}