// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif // UNITY_EDITOR
namespace Microsoft.MixedReality.GraphicsTools
{
///
/// A scriptable object which contains data and UI functionality to swap object references within a scene.
///
[CreateAssetMenu(fileName = "Theme", menuName = "Graphics Tools/Theme Collection")]
public class ThemeCollection : ScriptableObject
{
[System.Serializable]
public class Theme
{
///
/// The name of the theme (for the user only).
///
public string Name;
///
/// The assets that represent the theme.
///
public List Assets;
}
///
/// The list of themes within the collection. Must contain at least 2 items.
///
public List Themes = new List();
///
/// The number of assets in each theme. Must contain at least 1 item.
///
public int AssetCount = 1;
}
#if UNITY_EDITOR
///
/// Custom inspector for the ThemeCollection scriptable object. In the same file for portability.
///
[CustomEditor(typeof(ThemeCollection))]
public class ThemeCollectionEditor : UnityEditor.Editor
{
// Table
private SerializedProperty themeCount;
private SerializedProperty assetCount;
private Vector2 scrollPosition;
// Controls.
private int selectedThemeIndex;
private enum SelectionMode
{
WholeScene,
SelectionWithChildren,
SelectionWithoutChildren
}
private SelectionMode selectionMode;
void OnEnable()
{
themeCount = serializedObject.FindProperty($"{nameof(ThemeCollection.Themes)}.Array.size");
assetCount = serializedObject.FindProperty("AssetCount");
}
public override void OnInspectorGUI()
{
DrawTable();
ThemeCollection themeCollection = serializedObject.targetObject as ThemeCollection;
if (themeCollection != null)
{
DrawControls(themeCollection);
}
}
private void DrawTable()
{
serializedObject.Update();
EditorGUILayout.BeginVertical("Box");
{
EditorGUILayout.BeginHorizontal();
{
EditorGUILayout.PropertyField(themeCount, new GUIContent("Themes (Minimum of 2)"));
int value = themeCount.intValue;
if (GUILayout.Button("+"))
{
++value;
}
if (GUILayout.Button("-"))
{
--value;
}
themeCount.intValue = Mathf.Max(value, 2);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
{
EditorGUILayout.PropertyField(assetCount, new GUIContent("Assets (Minimum of 1)"));
int value = assetCount.intValue;
if (GUILayout.Button("+"))
{
++value;
}
if (GUILayout.Button("-"))
{
--value;
}
assetCount.intValue = Mathf.Max(value, 1);
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
const float minThemeWidth = 128.0f;
const float minThemeHeight = 18.0f;
int themeCountInt = themeCount.intValue;
int assetCountInt = assetCount.intValue;
EditorGUILayout.BeginHorizontal("Box");
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height((3 + assetCountInt) * minThemeHeight));
{
EditorGUILayout.BeginHorizontal("Box");
{
for (int i = 0; i < themeCountInt; ++i)
{
SerializedProperty themeData = serializedObject.FindProperty($"{nameof(ThemeCollection.Themes)}.Array.data[{i}]");
EditorGUILayout.BeginVertical("Box");
{
SerializedProperty themeDataName = themeData.FindPropertyRelative(nameof(ThemeCollection.Theme.Name));
EditorGUILayout.PropertyField(themeDataName, GUIContent.none, false, GUILayout.MinWidth(minThemeWidth));
SerializedProperty themeDataAssets = themeData.FindPropertyRelative($"{nameof(ThemeCollection.Theme.Assets)}.Array");
SerializedProperty assetsCount = themeDataAssets.FindPropertyRelative("size");
assetsCount.intValue = assetCountInt;
for (int j = 0; j < assetCountInt; ++j)
{
SerializedProperty assetsData = themeDataAssets.FindPropertyRelative($"data[{j}]");
EditorGUILayout.PropertyField(assetsData, GUIContent.none, false, GUILayout.MinWidth(minThemeWidth));
}
}
EditorGUILayout.EndVertical();
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
EditorGUILayout.EndHorizontal();
serializedObject.ApplyModifiedProperties();
}
private void DrawControls(ThemeCollection themeCollection)
{
EditorGUILayout.BeginVertical("Box");
{
List displayedOptions = new List(themeCollection.Themes.Count);
for (int i = 0; i < themeCollection.Themes.Count; ++i)
{
var theme = themeCollection.Themes[i];
displayedOptions.Add(GetThemeName(theme, i));
}
selectedThemeIndex = EditorGUILayout.Popup("Selected Theme", selectedThemeIndex, displayedOptions.ToArray());
selectionMode = (SelectionMode)EditorGUILayout.EnumPopup("Selection Mode", selectionMode);
string warning;
GUI.enabled = ValidateThemeCollection(themeCollection, out warning);
GameObject[] gameObjects = null;
if (GUI.enabled == true)
{
GUI.enabled = ValidateSelection(selectionMode, out gameObjects, out warning);
}
EditorGUILayout.Space();
if (GUILayout.Button("Apply"))
{
Apply(gameObjects,
themeCollection,
themeCollection.Themes[selectedThemeIndex],
selectionMode != SelectionMode.SelectionWithoutChildren);
}
if (GUI.enabled == false)
{
EditorGUILayout.HelpBox(warning, MessageType.Warning);
GUI.enabled = true;
}
}
EditorGUILayout.EndVertical();
}
private static bool ValidateThemeCollection(ThemeCollection themeCollection, out string warning)
{
// Make sure all themes have valid assets and each row contains similar types.
for (int i = 0; i < themeCollection.Themes[0].Assets.Count ; ++i)
{
System.Type type = null;
for (int j = 0; j < themeCollection.Themes.Count; ++j)
{
if (themeCollection.Themes[j].Assets[i] == null)
{
warning = $"Theme \"{GetThemeName(themeCollection.Themes[j], j)}\" asset index {i} is null.";
return false;
}
System.Type currentType = themeCollection.Themes[j].Assets[i].GetType();
if (j == 0)
{
type = currentType;
}
else if(currentType != type)
{
warning = $"Theme \"{GetThemeName(themeCollection.Themes[j], j)}\" asset index {i} is of mismatched type. Expected \"{type}\" and got \"{currentType}\".";
return false;
}
}
}
warning = string.Empty;
return true;
}
private static bool ValidateSelection(SelectionMode selectionMode, out GameObject[] gameObjects, out string warning)
{
switch (selectionMode)
{
case SelectionMode.WholeScene:
{
gameObjects = UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects();
if (gameObjects.Length == 0)
{
warning = "The scene is empty, please create at least one game object.";
return false;
}
}
break;
default:
case SelectionMode.SelectionWithChildren:
case SelectionMode.SelectionWithoutChildren:
{
gameObjects = Selection.gameObjects;
if (gameObjects.Length == 0)
{
warning = "Please select at least one game object in the scene. Locking this panel may help in selection.";
return false;
}
}
break;
}
warning = string.Empty;
return true;
}
private static void Apply(GameObject[] gameObjects, ThemeCollection themeCollection, ThemeCollection.Theme selectedTheme, bool recurse)
{
int progress = 0;
foreach (var gameObject in gameObjects)
{
EditorUtility.DisplayProgressBar("Applying Theme Change", "Please wait...", (float)progress / gameObjects.Length);
if (recurse)
{
SwapObjectReferencesRecurse(gameObject, themeCollection, selectedTheme);
}
else
{
SwapObjectReferences(gameObject, themeCollection, selectedTheme);
}
++progress;
}
EditorUtility.ClearProgressBar();
}
private static void SwapObjectReferences(GameObject gameObject, ThemeCollection themeCollection, ThemeCollection.Theme selectedTheme)
{
var components = gameObject.GetComponents();
foreach (var component in components)
{
if (component == null)
{
continue;
}
SerializedObject serializedObject = new SerializedObject(component);
SerializedProperty property = serializedObject.GetIterator();
bool modified = false;
while (property.NextVisible(true))
{
if (property.propertyType == SerializedPropertyType.ObjectReference &&
property.objectReferenceValue != null)
{
Object currentAsset = property.objectReferenceValue;
// Does the current asset match any non-selected theme(s) asset(s)?
if (currentAsset != null)
{
foreach (var theme in themeCollection.Themes)
{
if (theme == selectedTheme)
{
continue;
}
int assetIndex = 0;
foreach (var asset in theme.Assets)
{
if (asset == currentAsset)
{
property.objectReferenceValue = selectedTheme.Assets[assetIndex];
modified = true;
}
++assetIndex;
}
}
}
}
}
if (modified == true)
{
property.serializedObject.ApplyModifiedProperties();
}
}
}
private static void SwapObjectReferencesRecurse(GameObject gameObject, ThemeCollection themeCollection, ThemeCollection.Theme selectedTheme)
{
SwapObjectReferences(gameObject, themeCollection, selectedTheme);
foreach (Transform child in gameObject.transform)
{
SwapObjectReferencesRecurse(child.gameObject, themeCollection, selectedTheme);
}
}
private static string GetThemeName(ThemeCollection.Theme theme, int index)
{
return string.IsNullOrEmpty(theme.Name) ? $"Unnamed {index}" : theme.Name;
}
}
#endif // UNITY_EDITOR
}