Commit 29de0c28 authored by BlackAngle233's avatar BlackAngle233
Browse files

10.19 learned

parent 912976bb
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Teleport;
using UnityEngine;
using UnityEditor;
namespace Microsoft.MixedReality.Toolkit.Editor
{
[MixedRealityServiceInspector(typeof(IMixedRealityTeleportSystem))]
public class TeleportSystemInspector : BaseMixedRealityServiceInspector
{
private static readonly Color enabledColor = GUI.backgroundColor;
private static readonly Color disabledColor = Color.Lerp(enabledColor, Color.clear, 0.5f);
public override void DrawInspectorGUI(object target)
{
IMixedRealityTeleportSystem teleport = (IMixedRealityTeleportSystem)target;
EditorGUILayout.LabelField("Event Listeners", EditorStyles.boldLabel);
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("Event listeners will be populated once you enter play mode.", MessageType.Info);
return;
}
if (teleport.EventListeners.Count == 0)
{
EditorGUILayout.LabelField("(None found)", EditorStyles.miniLabel);
}
else
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
foreach (GameObject listener in teleport.EventListeners)
{
EditorGUILayout.ObjectField(listener.name, listener, typeof(GameObject), true);
}
EditorGUILayout.EndVertical();
}
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Editor;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
/// <summary>
/// Editor runtime controller for showing Project Configuration window and performance checks logging in current Unity project
/// </summary>
[InitializeOnLoad]
public class MixedRealityEditorSettings : IActiveBuildTargetChanged, IPreprocessBuildWithReport
{
private const string SessionKey = "_MixedRealityToolkit_Editor_ShownSettingsPrompts";
private static readonly string[] UwpRecommendedAudioSpatializers = { "MS HRTF Spatializer", "Microsoft Spatializer" };
#if UNITY_ANDROID
const string RenderingMode = "Single Pass Stereo";
#else
const string RenderingMode = "Single Pass Instanced";
#endif
public MixedRealityEditorSettings()
{
callbackOrder = 0;
}
static MixedRealityEditorSettings()
{
// Detect when we enter player mode so we can try checking for optimal configuration
EditorApplication.playModeStateChanged += OnPlayStateModeChanged;
// Subscribe to editor application update which will call us once the editor is initialized and running
EditorApplication.update += OnInit;
}
private static void OnInit()
{
// We only want to execute once to initialize, unsubscribe from update event
EditorApplication.update -= OnInit;
ShowProjectConfigurationDialog();
}
/// <inheritdoc />
public int callbackOrder { get; private set; }
/// <inheritdoc />
public void OnActiveBuildTargetChanged(BuildTarget previousTarget, BuildTarget newTarget)
{
IgnoreProjectConfigForSession = false;
}
/// <summary>
/// Session state wrapper that tracks whether to ignore checking Project Configuration for the current Unity session
/// </summary>
public static bool IgnoreProjectConfigForSession
{
get => SessionState.GetBool(SessionKey, false);
set => SessionState.SetBool(SessionKey, value);
}
/// <inheritdoc />
public void OnPreprocessBuild(BuildReport report)
{
if (MixedRealityProjectPreferences.RunOptimalConfiguration)
{
LogBuildConfigurationWarnings();
}
}
private static void OnPlayStateModeChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode && MixedRealityProjectPreferences.RunOptimalConfiguration)
{
LogConfigurationWarnings();
}
}
private static void ShowProjectConfigurationDialog()
{
if (!EditorApplication.isPlayingOrWillChangePlaymode
&& !IgnoreProjectConfigForSession
&& !MixedRealityProjectPreferences.IgnoreSettingsPrompt
&& !MixedRealityProjectConfigurator.IsProjectConfigured())
{
MixedRealityProjectConfiguratorWindow.ShowWindow();
}
}
/// <summary>
/// Checks critical project settings and suggests changes to optimize performance via logged warnings
/// </summary>
private static void LogConfigurationWarnings()
{
// Ensure compatibility with the pre-2019.3 XR architecture for customers / platforms
// with legacy requirements.
if (!XRSettingsUtilities.LegacyXREnabled)
{
Debug.LogWarning("<b>Virtual reality supported</b> not enabled. Check <i>XR Settings</i> under <i>Player Settings</i>");
}
if (!MixedRealityOptimizeUtils.IsOptimalRenderingPath())
{
Debug.LogWarning($"XR stereo rendering mode not set to <b>{RenderingMode}</b>. See <i>Mixed Reality Toolkit</i> > <i>Utilities</i> > <i>Optimize Window</i> tool for more information to improve performance");
}
// If targeting Windows Mixed Reality platform
if (MixedRealityOptimizeUtils.IsBuildTargetUWP())
{
if (!MixedRealityOptimizeUtils.IsDepthBufferSharingEnabled())
{
// If depth buffer sharing not enabled, advise to enable setting
Debug.LogWarning("<b>Depth Buffer Sharing</b> is not enabled to improve hologram stabilization. See <i>Mixed Reality Toolkit</i> > <i>Utilities</i> > <i>Optimize Window</i> tool for more information to improve performance");
}
if (!MixedRealityOptimizeUtils.IsWMRDepthBufferFormat16bit())
{
// If depth format is 24-bit, advise to consider 16-bit for performance.
Debug.LogWarning("<b>Depth Buffer Sharing</b> has 24-bit depth format selected. Consider using 16-bit for performance. See <i>Mixed Reality Toolkit</i> > <i>Utilities</i> > <i>Optimize Window</i> tool for more information to improve performance");
}
if (!UwpRecommendedAudioSpatializers.Contains(SpatializerUtilities.CurrentSpatializer))
{
Debug.LogWarning($"This application is not using the recommended <b>Audio Spatializer Plugin</b>. Go to <i>Project Settings</i> > <i>Audio</i> > <i>Spatializer Plugin</i> and select one of the following: {string.Join(", ", UwpRecommendedAudioSpatializers)}.");
}
}
else if (SpatializerUtilities.CurrentSpatializer == null)
{
Debug.LogWarning($"This application is not using an <b>Audio Spatializer Plugin</b>. Go to <i>Project Settings</i> > <i>Audio</i> > <i>Spatializer Plugin</i> and select one of the available options.");
}
}
/// <summary>
/// Checks critical project settings and suggests changes to optimize build performance via logged warnings
/// </summary>
private static void LogBuildConfigurationWarnings()
{
if (PlayerSettings.stripUnusedMeshComponents)
{
/// For more information please see <see href="https://microsoft.github.io/MixedRealityToolkit-Unity/Documentation/Performance/PerfGettingStarted.html#optimize-mesh-data">Optimize Mesh Data</see>
Debug.LogWarning("<b>Optimize Mesh Data</b> is enabled. This setting can drastically increase build times. It is recommended to disable this setting during development and re-enable during \"Master\" build creation. See <i>Player Settings</i> > <i>Other Settings</i> > <i>Optimize Mesh Data</i>");
}
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Editor;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Compilation;
using UnityEngine;
using MRConfig = Microsoft.MixedReality.Toolkit.Utilities.Editor.MixedRealityProjectConfigurator.Configurations;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
public class MixedRealityProjectConfiguratorWindow : EditorWindow
{
private readonly Dictionary<MRConfig, bool> trackToggles = new Dictionary<MRConfig, bool>()
{
{ MRConfig.ForceTextSerialization, true },
{ MRConfig.VisibleMetaFiles, true },
{ MRConfig.VirtualRealitySupported, true },
{ MRConfig.OptimalRenderingPath, true },
{ MRConfig.SpatialAwarenessLayer, true },
{ MRConfig.AudioSpatializer, true },
// UWP Capabilities
{ MRConfig.MicrophoneCapability, true },
{ MRConfig.InternetClientCapability, true },
{ MRConfig.SpatialPerceptionCapability, true },
#if UNITY_2019_3_OR_NEWER
{ MRConfig.EyeTrackingCapability, true },
#endif // UNITY_2019_3_OR_NEWER
// Android Settings
{ MRConfig.AndroidMultiThreadedRendering, true },
{ MRConfig.AndroidMinSdkVersion, true },
// iOS Settings
{ MRConfig.IOSMinOSVersion, true },
{ MRConfig.IOSArchitecture, true },
{ MRConfig.IOSCameraUsageDescription, true },
#if UNITY_2019_3_OR_NEWER
// A workaround for the Unity bug described in https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8326.
{ MRConfig.GraphicsJobWorkaround, true },
#endif // UNITY_2019_3_OR_NEWER
};
private const float Default_Window_Height = 640.0f;
private const float Default_Window_Width = 400.0f;
private const string None = "None";
private readonly GUIContent ApplyButtonContent = new GUIContent("Apply", "Apply configurations to this Unity Project");
private readonly GUIContent LaterButtonContent = new GUIContent("Later", "Do not show this pop-up notification until next session");
private readonly GUIContent IgnoreButtonContent = new GUIContent("Ignore", "Modify this preference under Edit > Project Settings > Mixed Reality Toolkit");
private bool showConfigurations = true;
/// <summary>
/// Show the MRTK Project Configurator utility window or focus if already opened
/// </summary>
[MenuItem("Mixed Reality Toolkit/Utilities/Configure Unity Project", false, 499)]
public static void ShowWindow()
{
// There should be only one configurator window open as a "pop-up". If already open, then just force focus on our instance
if (IsOpen)
{
Instance.Focus();
}
else
{
var window = CreateInstance<MixedRealityProjectConfiguratorWindow>();
window.titleContent = new GUIContent("MRTK Project Configurator", EditorGUIUtility.IconContent("_Popup").image);
window.position = new Rect(Screen.width / 2.0f, Screen.height / 2.0f, Default_Window_Height, Default_Window_Width);
window.ShowUtility();
}
}
public static MixedRealityProjectConfiguratorWindow Instance { get; private set; }
public static bool IsOpen => Instance != null;
private void OnEnable()
{
Instance = this;
CompilationPipeline.assemblyCompilationStarted += CompilationPipeline_assemblyCompilationStarted;
MixedRealityProjectConfigurator.SelectedSpatializer = SpatializerUtilities.CurrentSpatializer;
}
private void CompilationPipeline_assemblyCompilationStarted(string obj)
{
// There should be only one pop-up window which is generally tracked by IsOpen
// However, when recompiling, Unity will call OnDestroy for this window but not actually destroy the editor window
// This ensure we have a clean close on recompiles when this EditorWindow was open beforehand
Close();
}
private void OnGUI()
{
MixedRealityInspectorUtility.RenderMixedRealityToolkitLogo();
string foldoutHeader;
if (!MixedRealityProjectConfigurator.IsProjectConfigured())
{
foldoutHeader = "Modify Configurations";
RenderChoiceDialog();
}
else
{
foldoutHeader = "Configurations";
RenderConfiguredConfirmation();
}
EditorGUILayout.Space();
showConfigurations = EditorGUILayout.Foldout(showConfigurations, foldoutHeader, true);
if (showConfigurations)
{
RenderConfigurations();
}
}
private void RenderConfiguredConfirmation()
{
const string dialogTitle = "Project Configuration Complete";
const string dialogContent = "This Unity project is properly configured for the Mixed Reality Toolkit.";
EditorGUILayout.LabelField(dialogTitle, EditorStyles.boldLabel);
EditorGUILayout.LabelField(dialogContent);
}
private void RenderChoiceDialog()
{
const string dialogTitle = "Apply Default Settings?";
const string dialogContent = "The Mixed Reality Toolkit would like to auto-apply useful settings to this Unity project";
EditorGUILayout.LabelField(dialogTitle, EditorStyles.boldLabel);
EditorGUILayout.LabelField(dialogContent);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button(ApplyButtonContent))
{
ApplyConfigurations();
Close();
}
if (GUILayout.Button(LaterButtonContent))
{
MixedRealityEditorSettings.IgnoreProjectConfigForSession = true;
Close();
}
if (GUILayout.Button(IgnoreButtonContent))
{
MixedRealityProjectPreferences.IgnoreSettingsPrompt = true;
Close();
}
}
}
private Vector2 scrollPosition = Vector2.zero;
private void RenderConfigurations()
{
EditorGUILayout.LabelField("Enabled options will be applied to the project. Disabled items are already properly configured.");
EditorGUILayout.Space();
using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
{
scrollPosition = scrollView.scrollPosition;
EditorGUILayout.LabelField("Project Settings", EditorStyles.boldLabel);
RenderToggle(MRConfig.ForceTextSerialization, "Force text asset serialization");
RenderToggle(MRConfig.VisibleMetaFiles, "Enable visible meta files");
if (!MixedRealityOptimizeUtils.IsBuildTargetAndroid() && !MixedRealityOptimizeUtils.IsBuildTargetIOS() && XRSettingsUtilities.IsLegacyXRActive)
{
#if !UNITY_2019_3_OR_NEWER
RenderToggle(MRConfig.VirtualRealitySupported, "Enable VR supported");
#endif // !UNITY_2019_3_OR_NEWER
}
#if UNITY_2019_3_OR_NEWER
RenderToggle(MRConfig.OptimalRenderingPath, "Set Single Pass Instanced rendering path (legacy XR API)");
#else
#if UNITY_ANDROID
RenderToggle(MRConfig.OptimalRenderingPath, "Set Single Pass Stereo rendering path");
#else
RenderToggle(MRConfig.OptimalRenderingPath, "Set Single Pass Instanced rendering path");
#endif
#endif // UNITY_2019_3_OR_NEWER
RenderToggle(MRConfig.SpatialAwarenessLayer, "Set default Spatial Awareness layer");
PromptForAudioSpatializer();
EditorGUILayout.Space();
if (MixedRealityOptimizeUtils.IsBuildTargetUWP())
{
EditorGUILayout.LabelField("UWP Capabilities", EditorStyles.boldLabel);
RenderToggle(MRConfig.MicrophoneCapability, "Enable Microphone Capability");
RenderToggle(MRConfig.InternetClientCapability, "Enable Internet Client Capability");
RenderToggle(MRConfig.SpatialPerceptionCapability, "Enable Spatial Perception Capability");
#if UNITY_2019_3_OR_NEWER
RenderToggle(MRConfig.EyeTrackingCapability, "Enable Eye Gaze Input Capability");
RenderToggle(MRConfig.GraphicsJobWorkaround, "Avoid Unity 'PlayerSettings.graphicsJob' crash");
#endif // UNITY_2019_3_OR_NEWER
}
else
{
trackToggles[MRConfig.MicrophoneCapability] = false;
trackToggles[MRConfig.InternetClientCapability] = false;
trackToggles[MRConfig.SpatialPerceptionCapability] = false;
#if UNITY_2019_3_OR_NEWER
trackToggles[MRConfig.EyeTrackingCapability] = false;
trackToggles[MRConfig.GraphicsJobWorkaround] = false;
#endif // UNITY_2019_3_OR_NEWER
}
if (MixedRealityOptimizeUtils.IsBuildTargetAndroid())
{
EditorGUILayout.LabelField("Android Settings", EditorStyles.boldLabel);
RenderToggle(MRConfig.AndroidMultiThreadedRendering, "Disable Multi-Threaded Rendering");
RenderToggle(MRConfig.AndroidMinSdkVersion, "Set Minimum API Level");
}
if (MixedRealityOptimizeUtils.IsBuildTargetIOS())
{
EditorGUILayout.LabelField("iOS Settings", EditorStyles.boldLabel);
RenderToggle(MRConfig.IOSMinOSVersion, "Set Required OS Version");
RenderToggle(MRConfig.IOSArchitecture, "Set Required Architecture");
RenderToggle(MRConfig.IOSCameraUsageDescription, "Set Camera Usage Descriptions");
}
}
}
private void ApplyConfigurations()
{
var configurationFilter = new HashSet<MRConfig>();
foreach (var item in trackToggles)
{
if (item.Value)
{
configurationFilter.Add(item.Key);
}
}
MixedRealityProjectConfigurator.ConfigureProject(configurationFilter);
}
/// <summary>
/// Provide the user with the list of spatializers that can be selected.
/// </summary>
private void PromptForAudioSpatializer()
{
string selectedSpatializer = MixedRealityProjectConfigurator.SelectedSpatializer;
List<string> spatializers = new List<string>
{
None
};
spatializers.AddRange(SpatializerUtilities.InstalledSpatializers);
RenderDropDown(MRConfig.AudioSpatializer, "Audio spatializer:", spatializers.ToArray(), ref selectedSpatializer);
MixedRealityProjectConfigurator.SelectedSpatializer = selectedSpatializer;
}
private void RenderDropDown(MRConfig configKey, string title, string[] collection, ref string selection)
{
bool configured = MixedRealityProjectConfigurator.IsConfigured(configKey);
using (new EditorGUI.DisabledGroupScope(configured))
{
if (configured)
{
EditorGUILayout.LabelField(new GUIContent($"{title} {selection}", InspectorUIUtility.SuccessIcon));
}
else
{
int index = 0;
for (int i = 0; i < collection.Length; i++)
{
if (collection[i] != selection) { continue; }
index = i;
}
index = EditorGUILayout.Popup(title, index, collection, EditorStyles.popup);
selection = collection[index];
if (selection == None)
{
// The user selected "None", return null. Unity uses this string where null
// is the underlying value.
selection = null;
}
}
}
}
private void RenderToggle(MRConfig configKey, string title)
{
bool configured = MixedRealityProjectConfigurator.IsConfigured(configKey);
using (new EditorGUI.DisabledGroupScope(configured))
{
if (configured)
{
EditorGUILayout.LabelField(new GUIContent(title, InspectorUIUtility.SuccessIcon));
trackToggles[configKey] = false;
}
else
{
trackToggles[configKey] = EditorGUILayout.ToggleLeft(title, trackToggles[configKey]);
}
}
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(ProjectPreferences))]
internal class ProjectPreferencesInspector : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
EditorGUILayout.HelpBox("Use Project Settings > MRTK to edit project preferences or interact in code via ProjectPreferences.cs", MessageType.Warning);
DrawDefaultInspector();
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
/// <summary>
/// A collection of helper functions for adding InspectorFields to a custom Inspector
/// </summary>
public static class InspectorFieldsUtility
{
public static bool AreFieldsSame(SerializedProperty settings, List<InspectorFieldData> fieldList)
{
// If number of fields don't match, automatically not the same
if (settings.arraySize != fieldList.Count)
{
return false;
}
// If same number of fields, ensure union of two lists is a perfect match
for (int idx = 0; idx < settings.arraySize - 1; idx++)
{
SerializedProperty name = settings.GetArrayElementAtIndex(idx).FindPropertyRelative("Name");
if (fieldList.FindIndex(s => s.Name == name.stringValue) == -1)
{
return false;
}
}
return true;
}
/// <summary>
/// Update list of serialized PropertySettings from new or removed InspectorFields
/// </summary>
public static void UpdateSettingsList(SerializedProperty settings, List<InspectorFieldData> fieldList)
{
// Delete existing settings that now have missing field
// Remove data entries for existing setting matches
for (int idx = settings.arraySize - 1; idx >= 0; idx--)
{
SerializedProperty settingItem = settings.GetArrayElementAtIndex(idx);
SerializedProperty name = settingItem.FindPropertyRelative("Name");
int index = fieldList.FindIndex(s => s.Name == name.stringValue);
if (index != -1)
{
fieldList.RemoveAt(index);
}
else
{
settings.DeleteArrayElementAtIndex(idx);
}
}
AddFieldsToSettingsList(settings, fieldList);
}
/// <summary>
/// Create a new list of serialized PropertySettings from InspectorFields
/// </summary>
public static void ClearSettingsList(SerializedProperty settings, List<InspectorFieldData> data)
{
settings.ClearArray();
AddFieldsToSettingsList(settings, data);
}
/// <summary>
/// Adds InspectorFields to list of serialized PropertySettings
/// </summary>
public static void AddFieldsToSettingsList(SerializedProperty settings, List<InspectorFieldData> data)
{
for (int i = 0; i < data.Count; i++)
{
settings.InsertArrayElementAtIndex(settings.arraySize);
SerializedProperty settingItem = settings.GetArrayElementAtIndex(settings.arraySize - 1);
UpdatePropertySettings(settingItem, (int)data[i].Attributes.Type, data[i].Value);
SerializedProperty type = settingItem.FindPropertyRelative("Type");
SerializedProperty tooltip = settingItem.FindPropertyRelative("Tooltip");
SerializedProperty label = settingItem.FindPropertyRelative("Label");
SerializedProperty options = settingItem.FindPropertyRelative("Options");
SerializedProperty name = settingItem.FindPropertyRelative("Name");
type.enumValueIndex = (int)data[i].Attributes.Type;
tooltip.stringValue = data[i].Attributes.Tooltip;
label.stringValue = data[i].Attributes.Label;
name.stringValue = data[i].Name;
options.ClearArray();
if (data[i].Attributes.Options != null)
{
for (int j = 0; j < data[i].Attributes.Options.Length; j++)
{
options.InsertArrayElementAtIndex(j);
SerializedProperty item = options.GetArrayElementAtIndex(j);
item.stringValue = data[i].Attributes.Options[j];
}
}
}
}
/// <summary>
/// Update a property value in a serialized PropertySettings
/// </summary>
public static void UpdatePropertySettings(SerializedProperty prop, int type, object update)
{
SerializedProperty intValue = prop.FindPropertyRelative("IntValue");
SerializedProperty stringValue = prop.FindPropertyRelative("StringValue");
switch ((InspectorField.FieldTypes)type)
{
case InspectorField.FieldTypes.Float:
SerializedProperty floatValue = prop.FindPropertyRelative("FloatValue");
floatValue.floatValue = (float)update;
break;
case InspectorField.FieldTypes.Int:
intValue.intValue = (int)update;
break;
case InspectorField.FieldTypes.String:
stringValue.stringValue = (string)update;
break;
case InspectorField.FieldTypes.Bool:
SerializedProperty boolValue = prop.FindPropertyRelative("BoolValue");
boolValue.boolValue = (bool)update;
break;
case InspectorField.FieldTypes.Color:
SerializedProperty colorValue = prop.FindPropertyRelative("ColorValue");
colorValue.colorValue = (Color)update;
break;
case InspectorField.FieldTypes.DropdownInt:
intValue.intValue = (int)update;
break;
case InspectorField.FieldTypes.DropdownString:
stringValue.stringValue = (string)update;
break;
case InspectorField.FieldTypes.GameObject:
SerializedProperty gameObjectValue = prop.FindPropertyRelative("GameObjectValue");
gameObjectValue.objectReferenceValue = (GameObject)update;
break;
case InspectorField.FieldTypes.ScriptableObject:
SerializedProperty scriptableObjectValue = prop.FindPropertyRelative("ScriptableObjectValue");
scriptableObjectValue.objectReferenceValue = (ScriptableObject)update;
break;
case InspectorField.FieldTypes.Object:
SerializedProperty objectValue = prop.FindPropertyRelative("ObjectValue");
objectValue.objectReferenceValue = (UnityEngine.Object)update;
break;
case InspectorField.FieldTypes.Material:
SerializedProperty materialValue = prop.FindPropertyRelative("MaterialValue");
materialValue.objectReferenceValue = (Material)update;
break;
case InspectorField.FieldTypes.Texture:
SerializedProperty textureValue = prop.FindPropertyRelative("TextureValue");
textureValue.objectReferenceValue = (Texture)update;
break;
case InspectorField.FieldTypes.Vector2:
SerializedProperty vector2Value = prop.FindPropertyRelative("Vector2Value");
vector2Value.vector2Value = (Vector2)update;
break;
case InspectorField.FieldTypes.Vector3:
SerializedProperty vector3Value = prop.FindPropertyRelative("Vector3Value");
vector3Value.vector3Value = (Vector3)update;
break;
case InspectorField.FieldTypes.Vector4:
SerializedProperty vector4Value = prop.FindPropertyRelative("Vector4Value");
vector4Value.vector4Value = (Vector4)update;
break;
case InspectorField.FieldTypes.Curve:
SerializedProperty curveValue = prop.FindPropertyRelative("CurveValue");
curveValue.animationCurveValue = (AnimationCurve)update;
break;
case InspectorField.FieldTypes.Quaternion:
SerializedProperty quaternionValue = prop.FindPropertyRelative("QuaternionValue");
quaternionValue.quaternionValue = (Quaternion)update;
break;
case InspectorField.FieldTypes.AudioClip:
SerializedProperty audioClip = prop.FindPropertyRelative("AudioClipValue");
audioClip.objectReferenceValue = (AudioClip)update;
break;
case InspectorField.FieldTypes.Event:
// read only, do not update here or a new instance of the event will be created
break;
default:
break;
}
}
public static List<InspectorFieldData> GetInspectorFields(System.Object target)
{
List<InspectorFieldData> fields = new List<InspectorFieldData>();
Type myType = target.GetType();
foreach (PropertyInfo prop in myType.GetProperties())
{
var attrs = (InspectorField[])prop.GetCustomAttributes(typeof(InspectorField), false);
foreach (var attr in attrs)
{
fields.Add(new InspectorFieldData() { Name = prop.Name, Attributes = attr, Value = prop.GetValue(target, null) });
}
}
foreach (FieldInfo field in myType.GetFields())
{
var attrs = (InspectorField[])field.GetCustomAttributes(typeof(InspectorField), false);
foreach (var attr in attrs)
{
fields.Add(new InspectorFieldData() { Name = field.Name, Attributes = attr, Value = field.GetValue(target) });
}
}
return fields;
}
/// <summary>
/// Checks the type a property field and returns if it matches the passed in type
/// </summary>
public static bool IsPropertyType(SerializedProperty prop, InspectorField.FieldTypes type)
{
SerializedProperty propType = prop.FindPropertyRelative("Type");
return (InspectorField.FieldTypes)propType.intValue == type;
}
/// <summary>
/// Render a PropertySettings UI field based on the InspectorField Settings
/// </summary>
public static void DisplayPropertyField(SerializedProperty prop)
{
SerializedProperty type = prop.FindPropertyRelative("Type");
SerializedProperty label = prop.FindPropertyRelative("Label");
SerializedProperty tooltip = prop.FindPropertyRelative("Tooltip");
SerializedProperty options = prop.FindPropertyRelative("Options");
SerializedProperty intValue = prop.FindPropertyRelative("IntValue");
SerializedProperty stringValue = prop.FindPropertyRelative("StringValue");
Rect position;
GUIContent propLabel = new GUIContent(label.stringValue, tooltip.stringValue);
switch ((InspectorField.FieldTypes)type.intValue)
{
case InspectorField.FieldTypes.Float:
SerializedProperty floatValue = prop.FindPropertyRelative("FloatValue");
EditorGUILayout.PropertyField(floatValue, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.Int:
EditorGUILayout.PropertyField(intValue, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.String:
EditorGUILayout.PropertyField(stringValue, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.Bool:
SerializedProperty boolValue = prop.FindPropertyRelative("BoolValue");
EditorGUILayout.PropertyField(boolValue, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.Color:
SerializedProperty colorValue = prop.FindPropertyRelative("ColorValue");
EditorGUILayout.PropertyField(colorValue, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.DropdownInt:
position = EditorGUILayout.GetControlRect();
EditorGUI.BeginProperty(position, propLabel, intValue);
{
intValue.intValue = EditorGUI.Popup(position, label.stringValue, intValue.intValue, InspectorUIUtility.GetOptions(options));
}
EditorGUI.EndProperty();
break;
case InspectorField.FieldTypes.DropdownString:
string[] stringOptions = InspectorUIUtility.GetOptions(options);
int selection = InspectorUIUtility.GetOptionsIndex(options, stringValue.stringValue);
position = EditorGUILayout.GetControlRect();
EditorGUI.BeginProperty(position, propLabel, intValue);
{
int newIndex = EditorGUI.Popup(position, label.stringValue, selection, stringOptions);
if (selection != newIndex)
{
stringValue.stringValue = stringOptions[newIndex];
intValue.intValue = newIndex;
}
}
EditorGUI.EndProperty();
break;
case InspectorField.FieldTypes.GameObject:
SerializedProperty gameObjectValue = prop.FindPropertyRelative("GameObjectValue");
EditorGUILayout.PropertyField(gameObjectValue, new GUIContent(label.stringValue, tooltip.stringValue), false);
break;
case InspectorField.FieldTypes.ScriptableObject:
SerializedProperty scriptableObjectValue = prop.FindPropertyRelative("ScriptableObjectValue");
EditorGUILayout.PropertyField(scriptableObjectValue, new GUIContent(label.stringValue, tooltip.stringValue), false);
break;
case InspectorField.FieldTypes.Object:
SerializedProperty objectValue = prop.FindPropertyRelative("ObjectValue");
EditorGUILayout.PropertyField(objectValue, new GUIContent(label.stringValue, tooltip.stringValue), true);
break;
case InspectorField.FieldTypes.Material:
SerializedProperty materialValue = prop.FindPropertyRelative("MaterialValue");
EditorGUILayout.PropertyField(materialValue, new GUIContent(label.stringValue, tooltip.stringValue), false);
break;
case InspectorField.FieldTypes.Texture:
SerializedProperty textureValue = prop.FindPropertyRelative("TextureValue");
EditorGUILayout.PropertyField(textureValue, new GUIContent(label.stringValue, tooltip.stringValue), false);
break;
case InspectorField.FieldTypes.Vector2:
SerializedProperty vector2Value = prop.FindPropertyRelative("Vector2Value");
EditorGUILayout.PropertyField(vector2Value, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.Vector3:
SerializedProperty vector3Value = prop.FindPropertyRelative("Vector3Value");
EditorGUILayout.PropertyField(vector3Value, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.Vector4:
SerializedProperty vector4Value = prop.FindPropertyRelative("Vector4Value");
EditorGUILayout.PropertyField(vector4Value, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.Curve:
SerializedProperty curveValue = prop.FindPropertyRelative("CurveValue");
EditorGUILayout.PropertyField(curveValue, new GUIContent(label.stringValue, tooltip.stringValue));
break;
case InspectorField.FieldTypes.Quaternion:
SerializedProperty quaternionValue = prop.FindPropertyRelative("QuaternionValue");
Vector4 vect4 = new Vector4(quaternionValue.quaternionValue.x, quaternionValue.quaternionValue.y, quaternionValue.quaternionValue.z, quaternionValue.quaternionValue.w);
position = EditorGUILayout.GetControlRect();
EditorGUI.BeginProperty(position, propLabel, quaternionValue);
{
vect4 = EditorGUI.Vector4Field(position, propLabel, vect4);
quaternionValue.quaternionValue = new Quaternion(vect4.x, vect4.y, vect4.z, vect4.w);
}
EditorGUI.EndProperty();
break;
case InspectorField.FieldTypes.AudioClip:
SerializedProperty audioClip = prop.FindPropertyRelative("AudioClipValue");
EditorGUILayout.PropertyField(audioClip, new GUIContent(label.stringValue, tooltip.stringValue), false);
break;
case InspectorField.FieldTypes.Event:
SerializedProperty uEvent = prop.FindPropertyRelative("EventValue");
EditorGUILayout.PropertyField(uEvent, new GUIContent(label.stringValue, tooltip.stringValue));
break;
default:
break;
}
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditor.Experimental.SceneManagement;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
/// <summary>
/// This class has handy inspector UI utilities and functions.
/// </summary>
public static class InspectorUIUtility
{
// Colors
private static readonly Color PersonalThemeColorTint100 = new Color(1f, 1f, 1f);
private static readonly Color PersonalThemeColorTint75 = new Color(0.75f, 0.75f, 0.75f);
private static readonly Color PersonalThemeColorTint50 = new Color(0.5f, 0.5f, 0.5f);
private static readonly Color PersonalThemeColorTint25 = new Color(0.25f, 0.25f, 0.25f);
private static readonly Color PersonalThemeColorTint10 = new Color(0.10f, 0.10f, 0.10f);
private static readonly Color ProfessionalThemeColorTint100 = new Color(0f, 0f, 0f);
private static readonly Color ProfessionalThemeColorTint75 = new Color(0.25f, 0.25f, 0.25f);
private static readonly Color ProfessionalThemeColorTint50 = new Color(0.5f, 0.5f, 0.5f);
private static readonly Color ProfessionalThemeColorTint25 = new Color(0.75f, 0.75f, 0.75f);
private static readonly Color ProfessionalThemeColorTint10 = new Color(0.9f, 0.9f, 0.9f);
public static Color ColorTint100 => EditorGUIUtility.isProSkin ? ProfessionalThemeColorTint100 : PersonalThemeColorTint100;
public static Color ColorTint75 => EditorGUIUtility.isProSkin ? ProfessionalThemeColorTint75 : PersonalThemeColorTint75;
public static Color ColorTint50 => EditorGUIUtility.isProSkin ? ProfessionalThemeColorTint50 : PersonalThemeColorTint50;
public static Color ColorTint25 => EditorGUIUtility.isProSkin ? ProfessionalThemeColorTint25 : PersonalThemeColorTint25;
public static Color ColorTint10 => EditorGUIUtility.isProSkin ? ProfessionalThemeColorTint10 : PersonalThemeColorTint10;
// default UI sizes
public const int TitleFontSize = 14;
public const int HeaderFontSize = 11;
public const int DefaultFontSize = 10;
public const float DocLinkWidth = 175f;
// special characters
public static readonly string Minus = "\u2212";
public static readonly string Plus = "\u002B";
public static readonly string Astrisk = "\u2217";
public static readonly string Left = "\u02C2";
public static readonly string Right = "\u02C3";
public static readonly string Up = "\u02C4";
public static readonly string Down = "\u02C5";
public static readonly string Close = "\u2715";
public static readonly string Heart = "\u2661";
public static readonly string Star = "\u2606";
public static readonly string Emoji = "\u263A";
public static readonly Texture HelpIcon = EditorGUIUtility.IconContent("_Help").image;
public static readonly Texture SuccessIcon = EditorGUIUtility.IconContent("Collab").image;
public static readonly Texture WarningIcon = EditorGUIUtility.IconContent("console.warnicon").image;
public static readonly Texture InfoIcon = EditorGUIUtility.IconContent("console.infoicon").image;
/// <summary>
/// A data container for managing scrolling lists or nested drawers in custom inspectors.
/// </summary>
public struct ListSettings
{
public bool Show;
public Vector2 Scroll;
}
/// <summary>
/// Delegate for button callbacks, single index
/// </summary>
/// <param name="index">location of item in a serialized array</param>
/// <param name="prop">A serialize property containing information needed if the button was clicked</param>
public delegate void ListButtonEvent(int index, SerializedProperty prop = null);
/// <summary>
/// Delegate for button callbacks, multi-index for nested arrays
/// </summary>
/// <param name="indexArray">location of item in a serialized array</param>
/// <param name="prop">A serialize property containing information needed if the button was clicked</param>
public delegate void MultiListButtonEvent(int[] indexArray, SerializedProperty prop = null);
/// <summary>
/// Box style with left margin
/// </summary>
public static GUIStyle Box(int margin)
{
GUIStyle box = new GUIStyle(GUI.skin.box);
box.margin.left = margin;
return box;
}
/// <summary>
/// Help box style with left margin
/// </summary>
/// <param name="margin">amount of left margin</param>
/// <returns>Configured helpbox GUIStyle</returns>
public static GUIStyle HelpBox(int margin)
{
GUIStyle box = new GUIStyle(EditorStyles.helpBox);
box.margin.left = margin;
return box;
}
/// <summary>
/// Create a custom label style based on color and size
/// </summary>
public static GUIStyle LableStyle(int size, Color color)
{
GUIStyle labelStyle = new GUIStyle(EditorStyles.boldLabel);
labelStyle.fontStyle = FontStyle.Bold;
labelStyle.fontSize = size;
labelStyle.fixedHeight = size * 2;
labelStyle.normal.textColor = color;
return labelStyle;
}
/// <summary>
/// Helper function to render buttons correctly indented according to EditorGUI.indentLevel since GUILayout component don't respond naturally
/// </summary>
/// <param name="buttonText">text to place in button</param>
/// <param name="options">layout options</param>
/// <returns>true if button clicked, false if otherwise</returns>
public static bool RenderIndentedButton(string buttonText, params GUILayoutOption[] options)
{
return RenderIndentedButton(() => { return GUILayout.Button(buttonText, options); });
}
/// <summary>
/// Helper function to render buttons correctly indented according to EditorGUI.indentLevel since GUILayout component don't respond naturally
/// </summary>
/// <param name="content">What to draw in button</param>
/// <param name="style">Style configuration for button</param>
/// <param name="options">layout options</param>
/// <returns>true if button clicked, false if otherwise</returns>
public static bool RenderIndentedButton(GUIContent content, GUIStyle style, params GUILayoutOption[] options)
{
return RenderIndentedButton(() => { return GUILayout.Button(content, style, options); });
}
/// <summary>
/// Helper function to support primary overloaded version of this functionality
/// </summary>
/// <param name="renderButton">The code to render button correctly based on parameter types passed</param>
/// <returns>true if button clicked, false if otherwise</returns>
public static bool RenderIndentedButton(Func<bool> renderButton)
{
bool result = false;
GUILayout.BeginHorizontal();
GUILayout.Space(EditorGUI.indentLevel * 15);
result = renderButton();
GUILayout.EndHorizontal();
return result;
}
/// <summary>
/// Render documentation button routing to relevant URI
/// </summary>
/// <param name="docURL">documentation URL to open on button click</param>
/// <returns>true if button clicked, false otherwise</returns>
public static bool RenderDocumentationButton(string docURL, float width = DocLinkWidth)
{
if (!string.IsNullOrEmpty(docURL))
{
var buttonContent = new GUIContent()
{
image = HelpIcon,
text = " Documentation",
tooltip = docURL,
};
// The documentation button should always be enabled.
using (new GUIEnabledWrapper())
{
if (GUILayout.Button(buttonContent, EditorStyles.miniButton, GUILayout.MaxWidth(width)))
{
Application.OpenURL(docURL);
return true;
}
}
}
return false;
}
/// <summary>
/// Render a documentation header with button if Object contains HelpURLAttribute
/// </summary>
/// <param name="targetType">Type to test for HelpURLAttribute</param>
/// <returns>true if object drawn and button clicked, false otherwise</returns>
public static bool RenderHelpURL(Type targetType)
{
bool result = false;
if (targetType != null)
{
HelpURLAttribute helpURL = targetType.GetCustomAttribute<HelpURLAttribute>();
if (helpURL != null)
{
result = RenderDocumentationSection(helpURL.URL);
}
}
return result;
}
/// <summary>
/// Render a documentation header with button for given url value
/// </summary>
/// <param name="url">Url to open if button is clicked</param>
/// <returns>true if object drawn and button clicked, false otherwise</returns>
public static bool RenderDocumentationSection(string url)
{
bool result = false;
if (!string.IsNullOrEmpty(url))
{
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
result = RenderDocumentationButton(url);
}
}
return result;
}
/// <summary>
/// A button that is as wide as the label
/// </summary>
public static bool FlexButton(GUIContent label, int index, ListButtonEvent callback, SerializedProperty prop = null)
{
if (FlexButton(label))
{
callback(index, prop);
return true;
}
return false;
}
/// <summary>
/// A button that is as wide as the label
/// </summary>
/// <returns>true if button clicked, false otherwise</returns>
public static bool FlexButton(GUIContent label, int[] indexArr, MultiListButtonEvent callback, SerializedProperty prop = null)
{
if (FlexButton(label))
{
callback(indexArr, prop);
return true;
}
return false;
}
/// <summary>
/// A button that is as wide as the label
/// </summary>
/// <param name="label">content for button</param>
/// <returns>true if button clicked, false otherwise</returns>
public static bool FlexButton(GUIContent label)
{
GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
float buttonWidth = GUI.skin.button.CalcSize(label).x;
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button(label, buttonStyle, GUILayout.Width(buttonWidth)))
{
return true;
}
}
return false;
}
/// <summary>
/// A button that is as wide as the available space
/// </summary>
public static bool FullWidthButton(GUIContent label, float padding, int index, ListButtonEvent callback, SerializedProperty prop = null)
{
GUIStyle addStyle = new GUIStyle(GUI.skin.button);
addStyle.fixedHeight = 25;
float addButtonWidth = GUI.skin.button.CalcSize(label).x * padding;
bool triggered = false;
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button(label, addStyle, GUILayout.Width(addButtonWidth)))
{
callback(index, prop);
triggered = true;
}
GUILayout.FlexibleSpace();
}
return triggered;
}
/// <summary>
/// A button that is as wide as the available space
/// </summary>
public static bool FullWidthButton(GUIContent label, float padding, int[] indexArr, MultiListButtonEvent callback, SerializedProperty prop = null)
{
GUIStyle addStyle = new GUIStyle(GUI.skin.button);
addStyle.fixedHeight = 25;
float addButtonWidth = GUI.skin.button.CalcSize(label).x * padding;
bool triggered = false;
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.FlexibleSpace();
if (GUILayout.Button(label, addStyle, GUILayout.Width(addButtonWidth)))
{
callback(indexArr, prop);
triggered = true;
}
GUILayout.FlexibleSpace();
}
return triggered;
}
/// <summary>
/// A small button, good for a single icon like + or - with single index callback events
/// </summary>
/// <param name="label">content to place in the button</param>
/// <returns>true if button selected, false otherwise</returns>
public static bool SmallButton(GUIContent label, int index, ListButtonEvent callback, SerializedProperty prop = null)
{
if (SmallButton(label))
{
callback(index, prop);
return true;
}
return false;
}
/// <summary>
/// A small button, good for a single icon like + or - with multi-index callback events
/// </summary>
/// <param name="label">content to place in the button</param>
/// <returns>true if button selected, false otherwise</returns>
public static bool SmallButton(GUIContent label, int[] indexArr, MultiListButtonEvent callback, SerializedProperty prop = null)
{
if (SmallButton(label))
{
callback(indexArr, prop);
return true;
}
return false;
}
/// <summary>
/// A small button, good for a single icon like + or -
/// </summary>
/// <param name="label">content to place in the button</param>
/// <returns>true if button selected, false otherwise</returns>
public static bool SmallButton(GUIContent label)
{
GUIStyle smallButton = new GUIStyle(EditorStyles.miniButton);
float smallButtonWidth = GUI.skin.button.CalcSize(label).x;
if (GUILayout.Button(label, smallButton, GUILayout.Width(smallButtonWidth)))
{
return true;
}
return false;
}
/// <summary>
/// Large title format
/// </summary>
public static void DrawTitle(string title)
{
GUIStyle labelStyle = LableStyle(TitleFontSize, ColorTint50);
EditorGUILayout.LabelField(new GUIContent(title), labelStyle);
GUILayout.Space(TitleFontSize * 0.5f);
}
/// <summary>
/// Medium title format
/// </summary>
/// <param name="header">string content to render</param>
public static void DrawHeader(string header)
{
GUIStyle labelStyle = LableStyle(HeaderFontSize, ColorTint10);
EditorGUILayout.LabelField(new GUIContent(header), labelStyle);
}
/// <summary>
/// Draw a basic label
/// </summary>
public static void DrawLabel(string title, int size, Color color)
{
GUIStyle labelStyle = LableStyle(size, color);
EditorGUILayout.LabelField(new GUIContent(title), labelStyle);
}
/// <summary>
/// draw a label with a yellow coloring
/// </summary>
public static void DrawWarning(string warning)
{
Color prevColor = GUI.color;
GUI.color = MixedRealityInspectorUtility.WarningColor;
EditorGUILayout.BeginVertical(EditorStyles.textArea);
EditorGUILayout.LabelField(warning, EditorStyles.wordWrappedMiniLabel);
EditorGUILayout.EndVertical();
GUI.color = prevColor;
}
/// <summary>
/// draw a notice area, normal coloring
/// </summary>
public static void DrawNotice(string notice)
{
Color prevColor = GUI.color;
GUI.color = ColorTint100;
using (new EditorGUILayout.VerticalScope(EditorStyles.textArea))
{
EditorGUILayout.LabelField(notice, EditorStyles.wordWrappedMiniLabel);
}
GUI.color = prevColor;
}
/// <summary>
/// draw a notice with green coloring
/// </summary>
public static void DrawSuccess(string notice)
{
Color prevColor = GUI.color;
GUI.color = MixedRealityInspectorUtility.SuccessColor;
EditorGUILayout.BeginVertical(EditorStyles.textArea);
EditorGUILayout.LabelField(notice, EditorStyles.wordWrappedMiniLabel);
EditorGUILayout.EndVertical();
GUI.color = prevColor;
}
/// <summary>
/// draw a notice with red coloring
/// </summary>
public static void DrawError(string error)
{
Color prevColor = GUI.color;
GUI.color = MixedRealityInspectorUtility.ErrorColor;
EditorGUILayout.BeginVertical(EditorStyles.textArea);
EditorGUILayout.LabelField(error, EditorStyles.wordWrappedMiniLabel);
EditorGUILayout.EndVertical();
GUI.color = prevColor;
}
/// <summary>
/// Create a line across the negative space
/// </summary>
public static void DrawDivider()
{
EditorGUILayout.LabelField(string.Empty, GUI.skin.horizontalSlider);
}
/// <summary>
/// Draws a section start (initiated by the Header attribute)
/// </summary>
public static bool DrawSectionFoldout(string headerName, bool open = true, GUIStyle style = null)
{
if (style == null)
{
style = EditorStyles.foldout;
}
using (new EditorGUI.IndentLevelScope())
{
return EditorGUILayout.Foldout(open, headerName, true, style);
}
}
/// <summary>
/// Draws a section start with header name and save open/close state to given preference key in SessionState
/// </summary>
public static bool DrawSectionFoldoutWithKey(string headerName, string preferenceKey = null, GUIStyle style = null, bool defaultOpen = true)
{
bool showPref = SessionState.GetBool(preferenceKey, defaultOpen);
bool show = DrawSectionFoldout(headerName, showPref, style);
if (show != showPref)
{
SessionState.SetBool(preferenceKey, show);
}
return show;
}
/// <summary>
/// Draws a popup UI with PropertyField type features.
/// Displays prefab pending updates
/// </summary>
/// <param name="prop">serialized property corresponding to Enum</param>
/// <param name="label">label for property</param>
/// <param name="propValue">Current enum value for property</param>
/// <returns>New enum value after draw</returns>
public static Enum DrawEnumSerializedProperty(SerializedProperty prop, GUIContent label, Enum propValue)
{
return DrawEnumSerializedProperty(EditorGUILayout.GetControlRect(), prop, label, propValue);
}
/// <summary>
/// Draws a popup UI with PropertyField type features.
/// Displays prefab pending updates
/// </summary>
/// <param name="position">position to render the serialized property</param>
/// <param name="prop">serialized property corresponding to Enum</param>
/// <param name="label">label for property</param>
/// <param name="propValue">Current enum value for property</param>
/// <returns>New enum value after draw</returns>
public static Enum DrawEnumSerializedProperty(Rect position, SerializedProperty prop, GUIContent label, Enum propValue)
{
Enum result = propValue;
EditorGUI.BeginProperty(position, label, prop);
{
result = EditorGUI.EnumPopup(position, label, propValue);
prop.enumValueIndex = Convert.ToInt32(result);
}
EditorGUI.EndProperty();
return result;
}
/// <summary>
/// adjust list settings as things change
/// </summary>
public static List<ListSettings> AdjustListSettings(List<ListSettings> listSettings, int count)
{
if (listSettings == null)
{
listSettings = new List<ListSettings>();
}
int diff = count - listSettings.Count;
if (diff > 0)
{
for (int i = 0; i < diff; i++)
{
listSettings.Add(new ListSettings() { Show = false, Scroll = new Vector2() });
}
}
else if (diff < 0)
{
int removeCnt = 0;
for (int i = listSettings.Count - 1; i > -1; i--)
{
if (removeCnt > diff)
{
listSettings.RemoveAt(i);
removeCnt--;
}
}
}
return listSettings;
}
/// <summary>
/// Get an array of strings from a serialized list of strings, pop-up field helper
/// </summary>
public static string[] GetOptions(SerializedProperty options)
{
List<string> list = new List<string>();
for (int i = 0; i < options.arraySize; i++)
{
list.Add(options.GetArrayElementAtIndex(i).stringValue);
}
return list.ToArray();
}
/// <summary>
/// Get the index of a serialized array item based on its name, pop-up field helper
/// </summary>
public static int GetOptionsIndex(SerializedProperty options, string selection)
{
for (int i = 0; i < options.arraySize; i++)
{
if (options.GetArrayElementAtIndex(i).stringValue == selection)
{
return i;
}
}
return 0;
}
/// <summary>
/// Draws the contents of a scriptable inline inside a foldout. Depending on if there's an actual scriptable
/// linked, the values will be greyed out or editable in case the scriptable is created inside the serialized object.
/// </summary>
static public bool DrawScriptableFoldout<T>(SerializedProperty scriptable, string description, bool isExpanded) where T : ScriptableObject
{
isExpanded = EditorGUILayout.Foldout(isExpanded, description, true, MixedRealityStylesUtility.BoldFoldoutStyle);
if (isExpanded)
{
using (new EditorGUI.IndentLevelScope())
{
if (scriptable.objectReferenceValue == null)
{
// If there's no scriptable linked we're creating a local instance that allows to store a
// local version of the scriptable in the serialized object owning the scriptable property.
scriptable.objectReferenceValue = ScriptableObject.CreateInstance<T>();
}
// We have currently 5 different states to display to the user:
// 1. the scriptable is local to the object instance
// 2. the scriptable is a linked nested scriptable inside of the currently displayed prefab
// 3. the scriptable is a linked nested scriptable inside of another prefab
// 4. the scriptable is a shared standalone asset
// 5. the scriptable slot is empty but inside of a prefab asset which needs special handling as
// prefabs can only display linked or nested scriptables.
// Depending on the type of link we show the user the scriptable configuration either:
// case 1 &2: editable inlined
// case 3 & 4: greyed out readonly
// case 5: only show the link
// case 5 -> can't create and/or store the local scriptable above - show link
bool isStoredAsset = (scriptable.objectReferenceValue == null) ? false : AssetDatabase.Contains(scriptable.objectReferenceValue);
bool isEmptyInStagedPrefab = !isStoredAsset && ((Component)scriptable.serializedObject.targetObject).gameObject.scene.path == "";
if (scriptable.objectReferenceValue == null || isEmptyInStagedPrefab)
{
EditorGUILayout.HelpBox("No scriptable " + scriptable.displayName + " linked to this prefab. Prefabs can't store " +
"local versions of scriptables and need to be linked to a scriptable asset.", MessageType.Warning);
EditorGUILayout.PropertyField(scriptable, new GUIContent(scriptable.displayName + " (Empty): "));
}
else
{
bool isNestedInCurrentPrefab = false;
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
var instancePath = AssetDatabase.GetAssetPath(scriptable.objectReferenceValue);
isNestedInCurrentPrefab = (instancePath != "" && instancePath == prefabStage.prefabAssetPath);
}
if (isStoredAsset && !isNestedInCurrentPrefab)
{
// case 3 & 4 - greyed out drawer
bool isMainAsset = AssetDatabase.IsMainAsset(scriptable.objectReferenceValue);
var sharedAssetPath = AssetDatabase.GetAssetPath(scriptable.objectReferenceValue);
if (isMainAsset)
{
EditorGUILayout.HelpBox("Editing a shared " + scriptable.displayName + ", located at " + sharedAssetPath, MessageType.Warning);
}
else
{
EditorGUILayout.HelpBox("Editing a nested " + scriptable.displayName + ", located inside of " + sharedAssetPath, MessageType.Warning);
}
EditorGUILayout.PropertyField(scriptable, new GUIContent(scriptable.displayName + " (Shared asset): "));
// In case there's a shared scriptable linked we're disabling the inlined scriptable properties
// (this will render them grayed out) so users won't accidentally modify the shared scriptable.
GUI.enabled = false;
DrawScriptableSubEditor(scriptable);
GUI.enabled = true;
}
else
{
// case 1 & 2 - inline editable drawer
if (isNestedInCurrentPrefab)
{
EditorGUILayout.HelpBox("Editing a nested version of " + scriptable.displayName + ".", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox("Editing a local version of " + scriptable.displayName + ".", MessageType.Info);
}
EditorGUILayout.PropertyField(scriptable, new GUIContent(scriptable.displayName + " (local): "));
DrawScriptableSubEditor(scriptable);
}
}
}
}
return isExpanded;
}
/// <summary>
/// Draws a foldout enlisting all components (or derived types) of the given type attached to the passed gameobject.
/// Adds a button for adding any of the component (or dervied types) and a follow button to highlight existing attached components.
/// </summary>
static public bool DrawComponentTypeFoldout<T>(GameObject gameObject, bool isExpanded, string typeDescription) where T : MonoBehaviour
{
isExpanded = EditorGUILayout.Foldout(isExpanded, typeDescription + "s", true);
if (isExpanded)
{
if (EditorGUILayout.DropdownButton(new GUIContent("Add " + typeDescription), FocusType.Keyboard))
{
// create the menu and add items to it
GenericMenu menu = new GenericMenu();
var type = typeof(T);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetLoadableTypes())
.Where(p => type.IsAssignableFrom(p) && !p.IsAbstract);
foreach (var derivedType in types)
{
menu.AddItem(new GUIContent(derivedType.Name), false, t => gameObject.AddComponent((Type)t), derivedType);
}
menu.ShowAsContext();
}
var constraints = gameObject.GetComponents<T>();
foreach (var constraint in constraints)
{
EditorGUILayout.BeginHorizontal();
string constraintName = constraint.GetType().Name;
EditorGUILayout.LabelField(constraintName);
if (GUILayout.Button("Go to component"))
{
Highlighter.Highlight("Inspector", $"{ObjectNames.NicifyVariableName(constraintName)} (Script)");
EditorGUIUtility.ExitGUI();
}
EditorGUILayout.EndHorizontal();
}
}
return isExpanded;
}
static private void DrawScriptableSubEditor(SerializedProperty scriptable)
{
if (scriptable.objectReferenceValue != null)
{
UnityEditor.Editor configEditor = UnityEditor.Editor.CreateEditor(scriptable.objectReferenceValue);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.Space();
configEditor.OnInspectorGUI();
EditorGUILayout.Space();
EditorGUILayout.EndVertical();
}
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Physics;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(BaseMixedRealityLineDataProvider))]
public class BaseLineDataProviderInspector : UnityEditor.Editor
{
private const string DrawLinePointsKey = "MRTK_Line_Inspector_DrawLinePoints";
private const string BasicSettingsFoldoutKey = "MRTK_Line_Inspector_BasicSettings";
private const string DrawLineRotationsKey = "MRTK_Line_Inspector_DrawLineRotations";
private const string EditorSettingsFoldoutKey = "MRTK_Line_Inspector_EditorSettings";
private const string RotationArrowLengthKey = "MRTK_Line_Inspector_RotationArrowLength";
private const string RotationSettingsFoldoutKey = "MRTK_Line_Inspector_RotationSettings";
private const string ManualUpVectorLengthKey = "MRTK_Line_Inspector_ManualUpVectorLength";
private const string LinePreviewResolutionKey = "MRTK_Line_Inspector_LinePreviewResolution";
private const string DistortionSettingsFoldoutKey = "MRTK_Line_Inspector_DistortionSettings";
private const string DrawLineManualUpVectorsKey = "MRTK_Line_Inspector_DrawLineManualUpVectors";
private const float ManualUpVectorHandleSizeModifier = 0.1f;
private static readonly GUIContent BasicSettingsContent = new GUIContent("Basic Settings");
private static readonly GUIContent EditorSettingsContent = new GUIContent("Editor Settings");
private static readonly GUIContent ManualUpVectorContent = new GUIContent("Manual Up Vectors");
private static readonly GUIContent RotationSettingsContent = new GUIContent("Rotation Settings");
private static readonly GUIContent DistortionSettingsContent = new GUIContent("Distortion Settings");
private static bool basicSettingsFoldout = true;
private static bool editorSettingsFoldout = false;
private static bool rotationSettingsFoldout = true;
private static bool distortionSettingsFoldout = true;
protected static int LinePreviewResolution = 16;
protected static bool DrawLinePoints = false;
protected static bool DrawLineRotations = false;
protected static bool DrawLineManualUpVectors = false;
protected static float ManualUpVectorLength = 1f;
protected static float RotationArrowLength = 0.5f;
private SerializedProperty transformMode;
private SerializedProperty customLineTransform;
private SerializedProperty lineStartClamp;
private SerializedProperty lineEndClamp;
private SerializedProperty loops;
private SerializedProperty rotationType;
private SerializedProperty flipUpVector;
private SerializedProperty originOffset;
private SerializedProperty manualUpVectorBlend;
private SerializedProperty manualUpVectors;
private SerializedProperty velocitySearchRange;
private SerializedProperty distorters;
private SerializedProperty distortionMode;
private SerializedProperty distortionStrength;
private SerializedProperty distortionEnabled;
private SerializedProperty uniformDistortionStrength;
private ReorderableList manualUpVectorList;
protected BaseMixedRealityLineDataProvider LineData;
protected bool RenderLinePreview = true;
protected virtual void OnEnable()
{
basicSettingsFoldout = SessionState.GetBool(BasicSettingsFoldoutKey, basicSettingsFoldout);
editorSettingsFoldout = SessionState.GetBool(EditorSettingsFoldoutKey, editorSettingsFoldout);
rotationSettingsFoldout = SessionState.GetBool(RotationSettingsFoldoutKey, rotationSettingsFoldout);
distortionSettingsFoldout = SessionState.GetBool(DistortionSettingsFoldoutKey, distortionSettingsFoldout);
LinePreviewResolution = SessionState.GetInt(LinePreviewResolutionKey, LinePreviewResolution);
DrawLinePoints = SessionState.GetBool(DrawLinePointsKey, DrawLinePoints);
DrawLineRotations = SessionState.GetBool(DrawLineRotationsKey, DrawLineRotations);
RotationArrowLength = SessionState.GetFloat(RotationArrowLengthKey, RotationArrowLength);
DrawLineManualUpVectors = SessionState.GetBool(DrawLineManualUpVectorsKey, DrawLineManualUpVectors);
ManualUpVectorLength = SessionState.GetFloat(ManualUpVectorLengthKey, ManualUpVectorLength);
LineData = (BaseMixedRealityLineDataProvider)target;
transformMode = serializedObject.FindProperty("transformMode");
customLineTransform = serializedObject.FindProperty("customLineTransform");
lineStartClamp = serializedObject.FindProperty("lineStartClamp");
lineEndClamp = serializedObject.FindProperty("lineEndClamp");
loops = serializedObject.FindProperty("loops");
rotationType = serializedObject.FindProperty("rotationMode");
flipUpVector = serializedObject.FindProperty("flipUpVector");
originOffset = serializedObject.FindProperty("originOffset");
manualUpVectorBlend = serializedObject.FindProperty("manualUpVectorBlend");
manualUpVectors = serializedObject.FindProperty("manualUpVectors");
velocitySearchRange = serializedObject.FindProperty("velocitySearchRange");
distorters = serializedObject.FindProperty("distorters");
distortionMode = serializedObject.FindProperty("distortionMode");
distortionStrength = serializedObject.FindProperty("distortionStrength");
distortionEnabled = serializedObject.FindProperty("distortionEnabled");
uniformDistortionStrength = serializedObject.FindProperty("uniformDistortionStrength");
manualUpVectorList = new ReorderableList(serializedObject, manualUpVectors, false, true, true, true);
manualUpVectorList.drawElementCallback += DrawManualUpVectorListElement;
manualUpVectorList.drawHeaderCallback += DrawManualUpVectorHeader;
RenderLinePreview = LineData.gameObject.GetComponent<BaseMixedRealityLineRenderer>() == null;
var newDistorters = LineData.gameObject.GetComponents<Distorter>();
distorters.arraySize = newDistorters.Length;
for (int i = 0; i < newDistorters.Length; i++)
{
var distorterProperty = distorters.GetArrayElementAtIndex(i);
distorterProperty.objectReferenceValue = newDistorters[i];
}
serializedObject.ApplyModifiedProperties();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
editorSettingsFoldout = EditorGUILayout.Foldout(editorSettingsFoldout, EditorSettingsContent, true);
SessionState.SetBool(EditorSettingsFoldoutKey, editorSettingsFoldout);
if (editorSettingsFoldout)
{
using (new EditorGUI.IndentLevelScope())
{
EditorGUI.BeginChangeCheck();
using (new EditorGUI.DisabledGroupScope(!RenderLinePreview))
{
EditorGUI.BeginChangeCheck();
LinePreviewResolution = EditorGUILayout.IntSlider("Preview Resolution", LinePreviewResolution, 2, 128);
if (EditorGUI.EndChangeCheck())
{
SessionState.SetInt(LinePreviewResolutionKey, LinePreviewResolution);
}
EditorGUI.BeginChangeCheck();
DrawLinePoints = EditorGUILayout.Toggle("Draw Line Points", DrawLinePoints);
if (EditorGUI.EndChangeCheck())
{
SessionState.SetBool(DrawLinePointsKey, DrawLinePoints);
}
}
EditorGUI.BeginChangeCheck();
DrawLineRotations = EditorGUILayout.Toggle("Draw Line Rotations", DrawLineRotations);
if (EditorGUI.EndChangeCheck())
{
SessionState.SetBool(DrawLineRotationsKey, DrawLineRotations);
}
if (DrawLineRotations)
{
EditorGUI.BeginChangeCheck();
RotationArrowLength = EditorGUILayout.Slider("Rotation Arrow Length", RotationArrowLength, 0.01f, 5f);
if (EditorGUI.EndChangeCheck())
{
SessionState.SetFloat(RotationArrowLengthKey, RotationArrowLength);
}
}
EditorGUI.BeginChangeCheck();
DrawLineManualUpVectors = EditorGUILayout.Toggle("Draw Manual Up Vectors", DrawLineManualUpVectors);
if (EditorGUI.EndChangeCheck())
{
SessionState.SetBool(DrawLineManualUpVectorsKey, DrawLineManualUpVectors);
}
if (DrawLineManualUpVectors)
{
EditorGUI.BeginChangeCheck();
ManualUpVectorLength = EditorGUILayout.Slider("Manual Up Vector Length", ManualUpVectorLength, 1f, 10f);
if (EditorGUI.EndChangeCheck())
{
SessionState.SetFloat(ManualUpVectorLengthKey, ManualUpVectorLength);
}
}
if (EditorGUI.EndChangeCheck())
{
SceneView.RepaintAll();
}
}
}
basicSettingsFoldout = EditorGUILayout.Foldout(basicSettingsFoldout, BasicSettingsContent, true);
SessionState.SetBool(BasicSettingsFoldoutKey, basicSettingsFoldout);
if (basicSettingsFoldout)
{
using (new EditorGUI.IndentLevelScope())
{
EditorGUILayout.PropertyField(transformMode);
EditorGUILayout.PropertyField(customLineTransform);
EditorGUILayout.PropertyField(lineStartClamp);
EditorGUILayout.PropertyField(lineEndClamp);
EditorGUILayout.PropertyField(loops);
}
}
rotationSettingsFoldout = EditorGUILayout.Foldout(rotationSettingsFoldout, RotationSettingsContent, true);
SessionState.SetBool(RotationSettingsFoldoutKey, rotationSettingsFoldout);
if (rotationSettingsFoldout)
{
using (new EditorGUI.IndentLevelScope())
{
EditorGUILayout.PropertyField(rotationType);
EditorGUILayout.PropertyField(flipUpVector);
EditorGUILayout.PropertyField(originOffset);
EditorGUILayout.PropertyField(velocitySearchRange);
if (DrawLineManualUpVectors)
{
manualUpVectorList.DoLayoutList();
if (GUILayout.Button("Normalize Up Vectors"))
{
for (int i = 0; i < manualUpVectors.arraySize; i++)
{
var manualUpVectorProperty = manualUpVectors.GetArrayElementAtIndex(i);
Vector3 upVector = manualUpVectorProperty.vector3Value;
if (upVector == Vector3.zero)
{
upVector = Vector3.up;
}
manualUpVectorProperty.vector3Value = upVector.normalized;
}
}
EditorGUILayout.PropertyField(manualUpVectorBlend);
}
}
}
distortionSettingsFoldout = EditorGUILayout.Foldout(distortionSettingsFoldout, DistortionSettingsContent, true);
SessionState.SetBool(DistortionSettingsFoldoutKey, distortionSettingsFoldout);
if (distortionSettingsFoldout)
{
if (distorters.arraySize <= 0)
{
EditorGUILayout.HelpBox("No distorters attached to this line.\nTry adding a distortion component.", MessageType.Info);
}
using (new EditorGUI.IndentLevelScope())
{
EditorGUILayout.PropertyField(distortionEnabled);
EditorGUILayout.PropertyField(distortionMode);
EditorGUILayout.PropertyField(distortionStrength);
EditorGUILayout.PropertyField(uniformDistortionStrength);
}
}
serializedObject.ApplyModifiedProperties();
}
protected virtual void OnSceneGUI()
{
if (DrawLineManualUpVectors)
{
if (LineData.ManualUpVectors == null || LineData.ManualUpVectors.Length < 2)
{
LineData.ManualUpVectors = new[] { Vector3.up, Vector3.up };
}
for (int i = 0; i < LineData.ManualUpVectors.Length; i++)
{
float normalizedLength = (1f / (LineData.ManualUpVectors.Length - 1)) * i;
var position = LineData.GetPoint(normalizedLength);
float handleSize = HandleUtility.GetHandleSize(position);
LineData.ManualUpVectors[i] = MixedRealityInspectorUtility.VectorHandle(LineData, position, LineData.ManualUpVectors[i], false, true, ManualUpVectorLength * handleSize, handleSize * ManualUpVectorHandleSizeModifier);
}
}
if (Application.isPlaying)
{
Handles.EndGUI();
return;
}
Vector3 firstPosition = LineData.FirstPoint;
Vector3 lastPosition = firstPosition;
for (int i = 1; i < LinePreviewResolution; i++)
{
Vector3 currentPosition;
Quaternion rotation;
if (i == LinePreviewResolution - 1)
{
currentPosition = LineData.LastPoint;
rotation = LineData.GetRotation(LineData.PointCount - 1);
}
else
{
float normalizedLength = (1f / (LinePreviewResolution - 1)) * i;
currentPosition = LineData.GetPoint(normalizedLength);
rotation = LineData.GetRotation(normalizedLength);
}
if (RenderLinePreview)
{
Handles.color = Color.magenta;
Handles.DrawLine(lastPosition, currentPosition);
}
if (DrawLineRotations)
{
float arrowSize = HandleUtility.GetHandleSize(currentPosition) * RotationArrowLength;
Handles.color = MixedRealityInspectorUtility.LineVelocityColor;
Handles.color = Color.Lerp(MixedRealityInspectorUtility.LineVelocityColor, Handles.zAxisColor, 0.75f);
Handles.ArrowHandleCap(0, currentPosition, Quaternion.LookRotation(rotation * Vector3.forward), arrowSize, EventType.Repaint);
Handles.color = Color.Lerp(MixedRealityInspectorUtility.LineVelocityColor, Handles.xAxisColor, 0.75f);
Handles.ArrowHandleCap(0, currentPosition, Quaternion.LookRotation(rotation * Vector3.right), arrowSize, EventType.Repaint);
Handles.color = Color.Lerp(MixedRealityInspectorUtility.LineVelocityColor, Handles.yAxisColor, 0.75f);
Handles.ArrowHandleCap(0, currentPosition, Quaternion.LookRotation(rotation * Vector3.up), arrowSize, EventType.Repaint);
}
lastPosition = currentPosition;
}
if (LineData.Loops && RenderLinePreview)
{
Handles.color = Color.magenta;
Handles.DrawLine(lastPosition, firstPosition);
}
}
private static void DrawManualUpVectorHeader(Rect rect)
{
EditorGUI.LabelField(rect, ManualUpVectorContent);
}
private void DrawManualUpVectorListElement(Rect rect, int index, bool isActive, bool isFocused)
{
EditorGUI.BeginChangeCheck();
var property = manualUpVectors.GetArrayElementAtIndex(index);
property.vector3Value = EditorGUI.Vector3Field(rect, GUIContent.none, property.vector3Value);
if (EditorGUI.EndChangeCheck())
{
EditorUtility.SetDirty(target);
}
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(BezierDataProvider))]
public class BezierDataProviderInspector : BaseLineDataProviderInspector
{
private const float HandleSizeModifier = 0.04f;
private const float PickSizeModifier = 0.06f;
private SerializedProperty controlPoints;
private SerializedProperty useLocalTangentPoints;
private BezierDataProvider bezierData;
private int selectedHandleIndex = -1;
protected override void OnEnable()
{
base.OnEnable();
controlPoints = serializedObject.FindProperty("controlPoints");
useLocalTangentPoints = serializedObject.FindProperty("useLocalTangentPoints");
bezierData = (BezierDataProvider)target;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
// We always draw line points for bezier.
DrawLinePoints = true;
EditorGUILayout.PropertyField(controlPoints, true);
EditorGUILayout.PropertyField(useLocalTangentPoints);
serializedObject.ApplyModifiedProperties();
}
protected override void OnSceneGUI()
{
base.OnSceneGUI();
for (int i = 0; i < 4; i++)
{
serializedObject.Update();
bool isTangentHandle = i % 3 != 0;
bool isLastPoint = i == 3;
var controlPointPosition = LineData.GetPoint(i);
var controlPointProperty = controlPoints.FindPropertyRelative("point" + (i + 1));
// Draw our tangent lines
Handles.color = Color.gray;
if (i == 0)
{
Handles.DrawLine(LineData.GetPoint(0), LineData.GetPoint(1));
}
else if (!isTangentHandle)
{
Handles.DrawLine(LineData.GetPoint(i), LineData.GetPoint(i - 1));
if (!isLastPoint)
{
Handles.DrawLine(LineData.GetPoint(i), LineData.GetPoint(i + 1));
}
}
Handles.color = isTangentHandle ? Color.white : Color.green;
float handleSize = HandleUtility.GetHandleSize(controlPointPosition);
if (Handles.Button(controlPointPosition, Quaternion.identity, handleSize * HandleSizeModifier, handleSize * PickSizeModifier, Handles.DotHandleCap))
{
selectedHandleIndex = i;
}
// Draw our handles
if (Tools.current == Tool.Move && selectedHandleIndex == i)
{
EditorGUI.BeginChangeCheck();
var newTargetPosition = Handles.PositionHandle(controlPointPosition, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(LineData, "Change Bezier Point Position");
LineData.SetPoint(i, newTargetPosition);
}
}
serializedObject.ApplyModifiedProperties();
}
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(EllipseLineDataProvider))]
public class EllipseLineDataProviderInspector : BaseLineDataProviderInspector
{
private SerializedProperty resolution;
private SerializedProperty radius;
private Vector2 tempRadius;
protected override void OnEnable()
{
base.OnEnable();
// Bump up the resolution, in case it's too low
if (LinePreviewResolution < 32)
{
LinePreviewResolution = 32;
}
resolution = serializedObject.FindProperty("resolution");
radius = serializedObject.FindProperty("radius");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.LabelField("Ellipse Settings");
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(resolution);
var prevRadius = radius.vector2Value;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(radius);
if (EditorGUI.EndChangeCheck())
{
bool update = false;
tempRadius = radius.vector2Value;
if (radius.vector2Value.x <= 0)
{
tempRadius.x = prevRadius.x;
update = true;
}
if (radius.vector2Value.y <= 0)
{
tempRadius.y = prevRadius.x;
update = true;
}
if (update)
{
radius.vector2Value = tempRadius;
}
}
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(ParabolaPhysicalLineDataProvider))]
public class ParabolaPhysicalLineDataProviderInspector : BaseLineDataProviderInspector
{
private SerializedProperty gravity;
private SerializedProperty velocity;
private SerializedProperty direction;
private SerializedProperty distanceMultiplier;
private SerializedProperty useCustomGravity;
protected override void OnEnable()
{
base.OnEnable();
gravity = serializedObject.FindProperty("gravity");
velocity = serializedObject.FindProperty("velocity");
direction = serializedObject.FindProperty("direction");
distanceMultiplier = serializedObject.FindProperty("distanceMultiplier");
useCustomGravity = serializedObject.FindProperty("useCustomGravity");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.LabelField("Physical Parabola Line Settings");
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(velocity);
EditorGUILayout.PropertyField(direction);
EditorGUILayout.PropertyField(distanceMultiplier);
EditorGUILayout.PropertyField(useCustomGravity);
if (useCustomGravity.boolValue)
{
EditorGUILayout.PropertyField(gravity);
}
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(ParabolaConstrainedLineDataProvider))]
public class ParabolicConstrainedLineDataProviderInspector : BaseLineDataProviderInspector
{
private SerializedProperty height;
private SerializedProperty endPoint;
private SerializedProperty upDirection;
protected override void OnEnable()
{
base.OnEnable();
height = serializedObject.FindProperty("height");
endPoint = serializedObject.FindProperty("endPoint");
upDirection = serializedObject.FindProperty("upDirection");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
EditorGUILayout.LabelField("Constrained Parabola Line Settings");
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(height);
EditorGUILayout.PropertyField(upDirection);
EditorGUILayout.PropertyField(endPoint);
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
}
protected override void OnSceneGUI()
{
base.OnSceneGUI();
serializedObject.Update();
var rotation = endPoint.FindPropertyRelative("rotation");
if (Tools.current == Tool.Move)
{
EditorGUI.BeginChangeCheck();
Vector3 newTargetPosition = Handles.PositionHandle(LineData.GetPoint(1), Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(LineData, "Change Parabola Point Position");
LineData.SetPoint(1, newTargetPosition);
}
}
else if (Tools.current == Tool.Rotate)
{
EditorGUI.BeginChangeCheck();
Quaternion newTargetRotation = Handles.RotationHandle(rotation.quaternionValue, LineData.GetPoint(1));
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(LineData, "Change Parabola Point Rotation");
rotation.quaternionValue = newTargetRotation;
}
}
serializedObject.ApplyModifiedProperties();
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(RectangleLineDataProvider))]
public class RectangleLineDataProviderInspector : BaseLineDataProviderInspector
{
private SerializedProperty height;
private SerializedProperty width;
private SerializedProperty zOffset;
protected override void OnEnable()
{
base.OnEnable();
height = serializedObject.FindProperty("height");
width = serializedObject.FindProperty("width");
zOffset = serializedObject.FindProperty("zOffset");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
// Rectangles only support 4 points, so our preview will reflect that.
LinePreviewResolution = 4;
// Rectangle doesn't support line rotations
DrawLineRotations = false;
serializedObject.Update();
EditorGUILayout.LabelField("Rectangle Settings");
EditorGUI.indentLevel++;
var prevHeight = height.floatValue;
var prevWidth = width.floatValue;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(height);
EditorGUILayout.PropertyField(width);
EditorGUILayout.PropertyField(zOffset);
if (EditorGUI.EndChangeCheck())
{
if (height.floatValue <= 0)
{
height.floatValue = prevHeight;
}
if (width.floatValue <= 0)
{
width.floatValue = prevWidth;
}
}
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
}
protected override void OnSceneGUI()
{
if (Application.isPlaying || !RenderLinePreview)
{
return;
}
Vector3 firstPos = LineData.GetPoint(0);
Vector3 lastPos = firstPos;
Handles.color = Color.magenta;
for (int i = 1; i < LineData.PointCount; i++)
{
Vector3 currentPos = LineData.GetPoint(i);
Handles.DrawLine(lastPos, currentPos);
lastPos = currentPos;
}
Handles.DrawLine(lastPos, firstPos);
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(SimpleLineDataProvider))]
public class SimpleLineDataProviderInspector : BaseLineDataProviderInspector
{
private SerializedProperty endPoint;
protected override void OnEnable()
{
base.OnEnable();
endPoint = serializedObject.FindProperty("endPoint");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
// We only have two points.
LinePreviewResolution = 2;
EditorGUILayout.LabelField("Simple Line Settings");
EditorGUI.indentLevel++;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(endPoint);
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
}
protected override void OnSceneGUI()
{
base.OnSceneGUI();
serializedObject.Update();
var rotation = endPoint.FindPropertyRelative("rotation");
if (Tools.current == Tool.Move)
{
EditorGUI.BeginChangeCheck();
Vector3 newTargetPosition = Handles.PositionHandle(LineData.GetPoint(1), Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(LineData, "Change Simple Point Position");
LineData.SetPoint(1, newTargetPosition);
}
}
else if (Tools.current == Tool.Rotate)
{
EditorGUI.BeginChangeCheck();
Quaternion newTargetRotation = Handles.RotationHandle(rotation.quaternionValue, LineData.GetPoint(1));
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(LineData, "Change Simple Point Rotation");
rotation.quaternionValue = newTargetRotation;
}
}
serializedObject.ApplyModifiedProperties();
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
[CustomEditor(typeof(SplineDataProvider))]
public class SplineDataProviderInspector : BaseLineDataProviderInspector
{
private const float OverlappingPointThreshold = 0.015f;
private const float HandleSizeModifier = 0.04f;
private const float PickSizeModifier = 0.06f;
private static readonly HashSet<int> OverlappingPointIndexes = new HashSet<int>();
private static readonly Vector2 ControlPointButtonSize = new Vector2(16, 16);
private static readonly Vector2 LeftControlPointPositionOffset = Vector2.left * 12;
private static readonly Vector2 RightControlPointPositionOffset = Vector2.right * 24;
private static readonly Vector2 ControlPointButtonHandleOffset = Vector3.up * 24;
private static readonly GUIContent PositionContent = new GUIContent("Position");
private static readonly GUIContent RotationContent = new GUIContent("Rotation");
private static readonly GUIContent AddControlPointContent = new GUIContent("+", "Add a control point");
private static readonly GUIContent RemoveControlPointContent = new GUIContent("-", "Remove a control point");
private static readonly GUIContent ControlPointHeaderContent = new GUIContent("Spline Control Points", "The current control points for the spline.");
private static bool controlPointFoldout = true;
private SerializedProperty controlPoints;
private SerializedProperty alignAllControlPoints;
private SplineDataProvider splineData;
private ReorderableList controlPointList;
private int selectedHandleIndex = -1;
protected override void OnEnable()
{
base.OnEnable();
splineData = (SplineDataProvider)target;
controlPoints = serializedObject.FindProperty("controlPoints");
alignAllControlPoints = serializedObject.FindProperty("alignAllControlPoints");
controlPointList = new ReorderableList(serializedObject, controlPoints, false, false, false, false)
{
elementHeight = EditorGUIUtility.singleLineHeight * 3
};
controlPointList.drawElementCallback += DrawControlPointElement;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
// We always draw line points for splines.
DrawLinePoints = true;
EditorGUILayout.LabelField("Spline Settings");
EditorGUI.indentLevel++;
EditorGUILayout.PropertyField(alignAllControlPoints);
GUILayout.BeginHorizontal();
if (GUILayout.Button("Add New Point", GUILayout.Width(48f), GUILayout.ExpandWidth(true)))
{
AddControlPoint();
}
GUI.enabled = controlPoints.arraySize > 4;
if (GUILayout.Button("Remove Last Point", GUILayout.Width(48f), GUILayout.ExpandWidth(true)))
{
RemoveControlPoint();
}
GUI.enabled = true;
GUILayout.EndHorizontal();
controlPointFoldout = EditorGUILayout.Foldout(controlPointFoldout, ControlPointHeaderContent, true);
if (controlPointFoldout)
{
// If we found overlapping points, provide an option to auto-separate them
if (OverlappingPointIndexes.Count > 0)
{
EditorGUILayout.HelpBox("We noticed some of your control points have the same position.", MessageType.Warning);
if (GUILayout.Button("Fix overlapping points"))
{
// Move them slightly out of the way
foreach (int pointIndex in OverlappingPointIndexes)
{
var controlPointProperty = controlPoints.GetArrayElementAtIndex(pointIndex);
var position = controlPointProperty.FindPropertyRelative("position");
position.vector3Value += Random.onUnitSphere * OverlappingPointThreshold * 2;
}
OverlappingPointIndexes.Clear();
}
}
controlPointList.DoLayoutList();
}
EditorGUI.indentLevel--;
serializedObject.ApplyModifiedProperties();
}
protected override void OnSceneGUI()
{
base.OnSceneGUI();
// We skip the first point as it should always remain at the GameObject's local origin (Pose.ZeroIdentity)
for (int i = 1; i < controlPoints?.arraySize; i++)
{
bool isTangentHandle = i % 3 != 0;
serializedObject.Update();
bool isLastPoint = i == controlPoints.arraySize - 1;
var controlPointPosition = LineData.GetPoint(i);
var controlPointProperty = controlPoints.GetArrayElementAtIndex(i);
var controlPointRotation = controlPointProperty.FindPropertyRelative("rotation");
// Draw our tangent lines
Handles.color = Color.gray;
if (i == 1)
{
Handles.DrawLine(LineData.GetPoint(0), LineData.GetPoint(1));
}
else if (!isTangentHandle)
{
Handles.DrawLine(LineData.GetPoint(i), LineData.GetPoint(i - 1));
if (!isLastPoint)
{
Handles.DrawLine(LineData.GetPoint(i), LineData.GetPoint(i + 1));
}
}
Handles.color = isTangentHandle ? Color.white : Color.green;
float handleSize = HandleUtility.GetHandleSize(controlPointPosition);
if (Handles.Button(controlPointPosition, controlPointRotation.quaternionValue, handleSize * HandleSizeModifier, handleSize * PickSizeModifier, Handles.DotHandleCap))
{
selectedHandleIndex = i;
}
// Draw our handles
if (Tools.current == Tool.Move && selectedHandleIndex == i)
{
EditorGUI.BeginChangeCheck();
var newTargetPosition = Handles.PositionHandle(controlPointPosition, controlPointRotation.quaternionValue);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(LineData, "Change Spline Point Position");
LineData.SetPoint(i, newTargetPosition);
}
if (isLastPoint)
{
DrawSceneControlOptionButtons(controlPointPosition);
}
}
else if (Tools.current == Tool.Rotate && selectedHandleIndex == i)
{
EditorGUI.BeginChangeCheck();
Quaternion newTargetRotation = Handles.RotationHandle(controlPointRotation.quaternionValue, controlPointPosition);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(LineData, "Change Spline Point Rotation");
controlPointRotation.quaternionValue = newTargetRotation;
}
}
serializedObject.ApplyModifiedProperties();
}
// Check for overlapping points
OverlappingPointIndexes.Clear();
for (int i = 0; i < splineData.ControlPoints.Length; i++)
{
for (int j = 0; j < splineData.ControlPoints.Length; j++)
{
if (i == j)
{
continue;
}
if (Vector3.Distance(splineData.ControlPoints[i].Position, splineData.ControlPoints[j].Position) < OverlappingPointThreshold)
{
if (i != 0)
{
OverlappingPointIndexes.Add(i);
}
if (j != 0)
{
OverlappingPointIndexes.Add(j);
}
break;
}
}
}
}
private void AddControlPoint()
{
serializedObject.Update();
Undo.RecordObject(LineData, "Add Spline Control Point");
var newControlPoints = new MixedRealityPose[3];
Vector3 direction = LineData.GetVelocity(0.99f);
float distance = Mathf.Max(LineData.UnClampedWorldLength * 0.05f, OverlappingPointThreshold * 5);
newControlPoints[0].Position = LineData.LastPoint + (direction * distance);
newControlPoints[1].Position = newControlPoints[0].Position + (direction * distance);
newControlPoints[2].Position = newControlPoints[1].Position + (direction * distance);
for (int i = 0; i < 3; i++)
{
controlPoints.arraySize = controlPoints.arraySize + 1;
var newControlPointProperty = controlPoints.GetArrayElementAtIndex(controlPoints.arraySize - 1);
newControlPointProperty.FindPropertyRelative("position").vector3Value = newControlPoints[i].Position;
newControlPointProperty.FindPropertyRelative("rotation").quaternionValue = Quaternion.identity;
}
serializedObject.ApplyModifiedProperties();
}
private void RemoveControlPoint()
{
if (controlPoints.arraySize <= 4) { return; }
serializedObject.Update();
Undo.RecordObject(LineData, "Remove Spline Control Point");
controlPoints.DeleteArrayElementAtIndex(controlPoints.arraySize - 1);
controlPoints.DeleteArrayElementAtIndex(controlPoints.arraySize - 1);
controlPoints.DeleteArrayElementAtIndex(controlPoints.arraySize - 1);
serializedObject.ApplyModifiedProperties();
}
private void DrawSceneControlOptionButtons(Vector3 position)
{
Handles.BeginGUI();
var buttonPosition = HandleUtility.WorldToGUIPoint(position);
var buttonRect = new Rect(buttonPosition + ControlPointButtonHandleOffset, ControlPointButtonSize);
// Move the button slightly to the left
buttonRect.position += LeftControlPointPositionOffset;
if (GUI.Button(buttonRect, AddControlPointContent))
{
AddControlPoint();
}
if (controlPoints.arraySize > 4)
{
// Move the button slightly to the right
buttonRect.position += RightControlPointPositionOffset;
if (GUI.Button(buttonRect, RemoveControlPointContent))
{
RemoveControlPoint();
}
}
Handles.EndGUI();
}
private void DrawControlPointElement(Rect rect, int index, bool isActive, bool isFocused)
{
bool lastMode = EditorGUIUtility.wideMode;
EditorGUIUtility.wideMode = true;
var lastLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = 88f;
var property = controlPoints.GetArrayElementAtIndex(index);
var fieldHeight = EditorGUIUtility.singleLineHeight * 0.5f;
var labelRect = new Rect(rect.x - 8f, rect.y + fieldHeight * 2, rect.width, EditorGUIUtility.singleLineHeight);
var positionRect = new Rect(rect.x, rect.y + fieldHeight, rect.width, EditorGUIUtility.singleLineHeight);
var rotationRect = new Rect(rect.x, rect.y + fieldHeight * 3, rect.width, EditorGUIUtility.singleLineHeight);
EditorGUI.LabelField(labelRect, $"{index + 1}");
EditorGUI.indentLevel++;
GUI.enabled = index != 0;
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(positionRect, property.FindPropertyRelative("position"), PositionContent);
bool hasPositionChanged = EditorGUI.EndChangeCheck();
var rotationProperty = property.FindPropertyRelative("rotation");
EditorGUI.BeginChangeCheck();
var newEulerRotation = EditorGUI.Vector3Field(rotationRect, RotationContent, rotationProperty.quaternionValue.eulerAngles);
bool hasRotationChanged = EditorGUI.EndChangeCheck();
if (hasRotationChanged)
{
rotationProperty.quaternionValue = Quaternion.Euler(newEulerRotation);
}
if (hasPositionChanged || hasRotationChanged)
{
EditorUtility.SetDirty(target);
}
GUI.enabled = true;
EditorGUI.indentLevel--;
EditorGUIUtility.wideMode = lastMode;
EditorGUIUtility.labelWidth = lastLabelWidth;
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Editor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
/// <summary>
/// This class has handy inspector utilities and functions.
/// </summary>
public static class MixedRealityInspectorUtility
{
#region Colors
public static Color DefaultBackgroundColor
{
get
{
return EditorGUIUtility.isProSkin
? new Color32(56, 56, 56, 255)
: new Color32(194, 194, 194, 255);
}
}
public static readonly Color DisabledColor = new Color(0.6f, 0.6f, 0.6f);
public static readonly Color WarningColor = new Color(1f, 0.85f, 0.6f);
public static readonly Color ErrorColor = new Color(1f, 0.55f, 0.5f);
public static readonly Color SuccessColor = new Color(0.8f, 1f, 0.75f);
public static readonly Color SectionColor = new Color(0.85f, 0.9f, 1f);
public static readonly Color DarkColor = new Color(0.1f, 0.1f, 0.1f);
public static readonly Color HandleColorSquare = new Color(0.0f, 0.9f, 1f);
public static readonly Color HandleColorCircle = new Color(1f, 0.5f, 1f);
public static readonly Color HandleColorSphere = new Color(1f, 0.5f, 1f);
public static readonly Color HandleColorAxis = new Color(0.0f, 1f, 0.2f);
public static readonly Color HandleColorRotation = new Color(0.0f, 1f, 0.2f);
public static readonly Color HandleColorTangent = new Color(0.1f, 0.8f, 0.5f, 0.7f);
public static readonly Color LineVelocityColor = new Color(0.9f, 1f, 0f, 0.8f);
#endregion Colors
public const float DottedLineScreenSpace = 4.65f;
public const string DefaultConfigProfileName = "DefaultMixedRealityToolkitConfigurationProfile";
// StandardAssets/Textures/MRTK_Logo_Black.png
private const string LogoLightThemeGuid = "c2c00ef21cc44bcfa09695879e0ebecd";
// StandardAssets/Textures/MRTK_Logo_White.png
private const string LogoDarkThemeGuid = "84643a20fa6b4fa7969ef84ad2e40992";
public static readonly Texture2D LogoLightTheme = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(LogoLightThemeGuid));
public static readonly Texture2D LogoDarkTheme = AssetDatabase.LoadAssetAtPath<Texture2D>(AssetDatabase.GUIDToAssetPath(LogoDarkThemeGuid));
private const string CloneProfileHelpLabel = "Currently viewing a MRTK default profile. It is recommended to clone defaults and modify a custom profile.";
private const string CloneProfileHelpLockedLabel = "Clone this default profile to edit properties below";
/// <summary>
/// Check and make sure we have a Mixed Reality Toolkit and an active profile.
/// </summary>
/// <returns>True if the Mixed Reality Toolkit is properly initialized.</returns>
public static bool CheckMixedRealityConfigured(bool renderEditorElements = false)
{
if (!MixedRealityToolkit.IsInitialized)
{ // Don't proceed
return false;
}
if (!MixedRealityToolkit.Instance.HasActiveProfile)
{
if (renderEditorElements)
{
EditorGUILayout.HelpBox("No Active Profile set on the Mixed Reality Toolkit.", MessageType.Error);
}
return false;
}
return true;
}
/// <summary>
/// If MRTK is not initialized in scene, adds and initializes instance to current scene
/// </summary>
public static void AddMixedRealityToolkitToScene(MixedRealityToolkitConfigurationProfile configProfile = null)
{
if (!MixedRealityToolkit.IsInitialized)
{
MixedRealityToolkit newInstance = new GameObject("MixedRealityToolkit").AddComponent<MixedRealityToolkit>();
MixedRealityToolkit.SetActiveInstance(newInstance);
Selection.activeObject = newInstance;
if (configProfile == null)
{
// if we don't have a profile set we get the default profile
newInstance.ActiveProfile = GetDefaultConfigProfile();
}
else
{
newInstance.ActiveProfile = configProfile;
}
}
}
/// <summary>
/// Render the Mixed Reality Toolkit Logo.
/// </summary>
public static void RenderMixedRealityToolkitLogo()
{
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUILayout.Label(EditorGUIUtility.isProSkin ? LogoDarkTheme : LogoLightTheme, GUILayout.MaxHeight(96f));
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.Space(3f);
}
/// <summary>
/// Found at https://answers.unity.com/questions/960413/editor-window-how-to-center-a-window.html
/// </summary>
public static Rect GetEditorMainWindowPos()
{
var containerWinType = AppDomain.CurrentDomain.GetAllDerivedTypes(typeof(ScriptableObject)).FirstOrDefault(t => t.Name == "ContainerWindow");
if (containerWinType == null)
{
throw new MissingMemberException("Can't find internal type ContainerWindow. Maybe something has changed inside Unity");
}
var showModeField = containerWinType.GetField("m_ShowMode", BindingFlags.NonPublic | BindingFlags.Instance);
var positionProperty = containerWinType.GetProperty("position", BindingFlags.Public | BindingFlags.Instance);
if (showModeField == null || positionProperty == null)
{
throw new MissingFieldException("Can't find internal fields 'm_ShowMode' or 'position'. Maybe something has changed inside Unity");
}
var windows = Resources.FindObjectsOfTypeAll(containerWinType);
foreach (var win in windows)
{
var showMode = (int)showModeField.GetValue(win);
if (showMode == 4) // main window
{
var pos = (Rect)positionProperty.GetValue(win, null);
return pos;
}
}
throw new NotSupportedException("Can't find internal main window. Maybe something has changed inside Unity");
}
private static Type[] GetAllDerivedTypes(this AppDomain appDomain, Type aType)
{
var result = new List<Type>();
var assemblies = appDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
var types = assembly.GetLoadableTypes();
result.AddRange(types.Where(type => type.IsSubclassOf(aType)));
}
return result.ToArray();
}
/// <summary>
/// Centers an editor window on the main display.
/// </summary>
public static void CenterOnMainWin(this EditorWindow window)
{
var main = GetEditorMainWindowPos();
var pos = window.position;
float w = (main.width - pos.width) * 0.5f;
float h = (main.height - pos.height) * 0.5f;
pos.x = main.x + w;
pos.y = main.y + h;
window.position = pos;
}
#region Handles
/// <summary>
/// Draw an axis move handle.
/// </summary>
/// <param name="target"><see href="https://docs.unity3d.com/ScriptReference/Object.html">Object</see> that is undergoing the transformation. Also used for recording undo.</param>
/// <param name="origin">The initial position of the axis.</param>
/// <param name="direction">The direction the axis is facing.</param>
/// <param name="distance">Distance from the axis.</param>
/// <param name="handleSize">Optional handle size.</param>
/// <param name="autoSize">Optional, auto sizes the handles based on position and handle size.</param>
/// <param name="recordUndo">Optional, records undo state.</param>
/// <returns>The new <see cref="float"/> value.</returns>
public static float AxisMoveHandle(Object target, Vector3 origin, Vector3 direction, float distance, float handleSize = 0.2f, bool autoSize = true, bool recordUndo = true)
{
Vector3 position = origin + (direction.normalized * distance);
Handles.color = HandleColorAxis;
if (autoSize)
{
handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f);
}
Handles.DrawDottedLine(origin, position, DottedLineScreenSpace);
Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(direction), handleSize * 2, EventType.Repaint);
Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize, Vector3.zero, Handles.CircleHandleCap);
if (recordUndo)
{
float newDistance = Vector3.Distance(origin, newPosition);
if (!distance.Equals(newDistance))
{
Undo.RegisterCompleteObjectUndo(target, target.name);
distance = newDistance;
}
}
return distance;
}
/// <summary>
/// Returns the default config profile, if it exists.
/// </summary>
public static MixedRealityToolkitConfigurationProfile GetDefaultConfigProfile()
{
var allConfigProfiles = ScriptableObjectExtensions.GetAllInstances<MixedRealityToolkitConfigurationProfile>();
return GetDefaultConfigProfile(allConfigProfiles);
}
/// <summary>
/// Given a list of MixedRealityToolkitConfigurationProfile objects, returns
/// the one that matches the default profile name.
/// </summary>
public static MixedRealityToolkitConfigurationProfile GetDefaultConfigProfile(MixedRealityToolkitConfigurationProfile[] allProfiles)
{
for (int i = 0; i < allProfiles.Length; i++)
{
if (allProfiles[i].name == DefaultConfigProfileName)
{
return allProfiles[i];
}
}
return null;
}
/// <summary>
/// Draw a Circle Move Handle.
/// </summary>
/// <param name="target"><see href="https://docs.unity3d.com/ScriptReference/Object.html">Object</see> that is undergoing the transformation. Also used for recording undo.</param>
/// <param name="position">The position to draw the handle.</param>
/// <param name="xScale">Scale the new value on the x axis by this amount.</param>
/// <param name="yScale">Scale the new value on the x axis by this amount.</param>
/// <param name="zScale">Scale the new value on the x axis by this amount.</param>
/// <param name="handleSize">Optional handle size.</param>
/// <param name="autoSize">Optional, auto sizes the handles based on position and handle size.</param>
/// <param name="recordUndo">Optional, records undo state.</param>
/// <returns>The new <see href="https://docs.unity3d.com/ScriptReference/Vector3.html">Vector3</see> value.</returns>
public static Vector3 CircleMoveHandle(Object target, Vector3 position, float xScale = 1f, float yScale = 1f, float zScale = 1f, float handleSize = 0.2f, bool autoSize = true, bool recordUndo = true)
{
Handles.color = HandleColorCircle;
if (autoSize)
{
handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f);
}
Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize, Vector3.zero, Handles.CircleHandleCap);
if (recordUndo && position != newPosition)
{
Undo.RegisterCompleteObjectUndo(target, target.name);
position.x = Mathf.Lerp(position.x, newPosition.x, Mathf.Clamp01(xScale));
position.y = Mathf.Lerp(position.z, newPosition.y, Mathf.Clamp01(yScale));
position.z = Mathf.Lerp(position.y, newPosition.z, Mathf.Clamp01(zScale));
}
return position;
}
/// <summary>
/// Draw a square move handle.
/// </summary>
/// <param name="target"><see href="https://docs.unity3d.com/ScriptReference/Object.html">Object</see> that is undergoing the transformation. Also used for recording undo.</param>
/// <param name="position">The position to draw the handle.</param>
/// <param name="xScale">Scale the new value on the x axis by this amount.</param>
/// <param name="yScale">Scale the new value on the x axis by this amount.</param>
/// <param name="zScale">Scale the new value on the x axis by this amount.</param>
/// <param name="handleSize">Optional handle size.</param>
/// <param name="autoSize">Optional, auto sizes the handles based on position and handle size.</param>
/// <param name="recordUndo">Optional, records undo state.</param>
/// <returns>The new <see href="https://docs.unity3d.com/ScriptReference/Vector3.html">Vector3</see> value.</returns>
public static Vector3 SquareMoveHandle(Object target, Vector3 position, float xScale = 1f, float yScale = 1f, float zScale = 1f, float handleSize = 0.2f, bool autoSize = true, bool recordUndo = true)
{
Handles.color = HandleColorSquare;
if (autoSize)
{
handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f);
}
// Multiply square handle to match other types
Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize * 0.8f, Vector3.zero, Handles.RectangleHandleCap);
if (recordUndo && position != newPosition)
{
Undo.RegisterCompleteObjectUndo(target, target.name);
position.x = Mathf.Lerp(position.x, newPosition.x, Mathf.Clamp01(xScale));
position.y = Mathf.Lerp(position.z, newPosition.y, Mathf.Clamp01(yScale));
position.z = Mathf.Lerp(position.y, newPosition.z, Mathf.Clamp01(zScale));
}
return position;
}
/// <summary>
/// Draw a sphere move handle.
/// </summary>
/// <param name="target"><see href="https://docs.unity3d.com/ScriptReference/Object.html">Object</see> that is undergoing the transformation. Also used for recording undo.</param>
/// <param name="position">The position to draw the handle.</param>
/// <param name="xScale">Scale the new value on the x axis by this amount.</param>
/// <param name="yScale">Scale the new value on the x axis by this amount.</param>
/// <param name="zScale">Scale the new value on the x axis by this amount.</param>
/// <param name="handleSize">Optional handle size.</param>
/// <param name="autoSize">Optional, auto sizes the handles based on position and handle size.</param>
/// <param name="recordUndo">Optional, records undo state.</param>
/// <returns>The new <see href="https://docs.unity3d.com/ScriptReference/Vector3.html">Vector3</see> value.</returns>
public static Vector3 SphereMoveHandle(Object target, Vector3 position, float xScale = 1f, float yScale = 1f, float zScale = 1f, float handleSize = 0.2f, bool autoSize = true, bool recordUndo = true)
{
Handles.color = HandleColorSphere;
if (autoSize)
{
handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f);
}
// Multiply sphere handle size to match other types
Vector3 newPosition = Handles.FreeMoveHandle(position, Quaternion.identity, handleSize * 2, Vector3.zero, Handles.SphereHandleCap);
if (recordUndo && position != newPosition)
{
Undo.RegisterCompleteObjectUndo(target, target.name);
position.x = Mathf.Lerp(position.x, newPosition.x, Mathf.Clamp01(xScale));
position.y = Mathf.Lerp(position.z, newPosition.y, Mathf.Clamp01(yScale));
position.z = Mathf.Lerp(position.y, newPosition.z, Mathf.Clamp01(zScale));
}
return position;
}
/// <summary>
/// Draw a vector handle.
/// </summary>
/// <param name="target"><see href="https://docs.unity3d.com/ScriptReference/Object.html">Object</see> that is undergoing the transformation. Also used for recording undo.</param>
/// <param name="normalize">Optional, Normalize the new vector value.</param>
/// <param name="clamp">Optional, Clamp new vector's value based on the distance to the origin.</param>
/// <param name="handleLength">Optional, handle length.</param>
/// <param name="handleSize">Optional, handle size.</param>
/// <param name="autoSize">Optional, auto sizes the handles based on position and handle size.</param>
/// <param name="recordUndo">Optional, records undo state.</param>
/// <returns>The new <see href="https://docs.unity3d.com/ScriptReference/Vector3.html">Vector3</see> value.</returns>
public static Vector3 VectorHandle(Object target, Vector3 origin, Vector3 vector, bool normalize = true, bool clamp = true, float handleLength = 1f, float handleSize = 0.1f, bool recordUndo = true, bool autoSize = true)
{
Handles.color = HandleColorTangent;
if (autoSize)
{
handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(origin) * handleSize, 0.75f);
}
Vector3 handlePosition = origin + (vector * handleLength);
float distanceToOrigin = Vector3.Distance(origin, handlePosition) / handleLength;
if (normalize)
{
vector.Normalize();
}
else
{
// If the handle isn't normalized, brighten based on distance to origin
Handles.color = Color.Lerp(Color.gray, HandleColorTangent, distanceToOrigin * 0.85f);
if (clamp)
{
// To indicate that we're at the clamped limit, make the handle 'pop' slightly larger
if (distanceToOrigin >= 0.98f)
{
Handles.color = Color.Lerp(HandleColorTangent, Color.white, 0.5f);
handleSize *= 1.5f;
}
}
}
// Draw a line from origin to origin + direction
Handles.DrawLine(origin, handlePosition);
Quaternion rotation = Quaternion.identity;
if (vector != Vector3.zero)
{
rotation = Quaternion.LookRotation(vector);
}
Vector3 newPosition = Handles.FreeMoveHandle(handlePosition, rotation, handleSize, Vector3.zero, Handles.DotHandleCap);
if (recordUndo && handlePosition != newPosition)
{
Undo.RegisterCompleteObjectUndo(target, target.name);
vector = (newPosition - origin).normalized;
// If we normalize, we're done
// Otherwise, multiply the vector by the distance between origin and target
if (!normalize)
{
distanceToOrigin = Vector3.Distance(origin, newPosition) / handleLength;
if (clamp)
{
distanceToOrigin = Mathf.Clamp01(distanceToOrigin);
}
vector *= distanceToOrigin;
}
}
return vector;
}
/// <summary>
/// Draw a rotation handle.
/// </summary>
/// <param name="target"><see href="https://docs.unity3d.com/ScriptReference/Object.html">Object</see> that is undergoing the transformation. Also used for recording undo.</param>
/// <param name="position">The position to draw the handle.</param>
/// <param name="rotation">The rotation to draw the handle.</param>
/// <param name="handleSize">Optional, handle size.</param>
/// <param name="autoSize">Optional, auto sizes the handles based on position and handle size.</param>
/// <param name="recordUndo">Optional, records undo state.</param>
/// <returns>The new <see href="https://docs.unity3d.com/ScriptReference/Quaternion.html">Quaternion</see> value.</returns>
public static Quaternion RotationHandle(Object target, Vector3 position, Quaternion rotation, float handleSize = 0.2f, bool autoSize = true, bool recordUndo = true)
{
Handles.color = HandleColorRotation;
if (autoSize)
{
handleSize = Mathf.Lerp(handleSize, HandleUtility.GetHandleSize(position) * handleSize, 0.75f);
}
// Make rotation handles larger so they can overlay movement handles
Quaternion newRotation = Handles.FreeRotateHandle(rotation, position, handleSize * 2);
if (recordUndo)
{
Handles.color = Handles.zAxisColor;
Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(newRotation * Vector3.forward), handleSize * 2, EventType.Repaint);
Handles.color = Handles.xAxisColor;
Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(newRotation * Vector3.right), handleSize * 2, EventType.Repaint);
Handles.color = Handles.yAxisColor;
Handles.ArrowHandleCap(0, position, Quaternion.LookRotation(newRotation * Vector3.up), handleSize * 2, EventType.Repaint);
if (rotation != newRotation)
{
Undo.RegisterCompleteObjectUndo(target, target.name);
rotation = newRotation;
}
}
return rotation;
}
#endregion Handles
#region Profiles
private static readonly GUIContent NewProfileContent = new GUIContent("+", "Create New Profile");
private static Dictionary<Object, UnityEditor.Editor> profileEditorCache = new Dictionary<Object, UnityEditor.Editor>();
/// <summary>
/// Draws an editor for a profile object.
/// </summary>
public static void DrawSubProfileEditor(Object profileObject, bool renderProfileInBox)
{
if (profileObject == null)
{
return;
}
UnityEditor.Editor subProfileEditor = null;
if (!profileEditorCache.TryGetValue(profileObject, out subProfileEditor))
{
subProfileEditor = UnityEditor.Editor.CreateEditor(profileObject);
profileEditorCache.Add(profileObject, subProfileEditor);
}
// If this is a default MRTK configuration profile, ask it to render as a sub-profile
if (typeof(BaseMixedRealityToolkitConfigurationProfileInspector).IsAssignableFrom(subProfileEditor.GetType()))
{
BaseMixedRealityToolkitConfigurationProfileInspector configProfile = (BaseMixedRealityToolkitConfigurationProfileInspector)subProfileEditor;
configProfile.RenderAsSubProfile = true;
}
var subProfile = profileObject as BaseMixedRealityProfile;
if (subProfile != null && !subProfile.IsCustomProfile)
{
string msg = MixedRealityProjectPreferences.LockProfiles ? CloneProfileHelpLockedLabel : CloneProfileHelpLabel;
EditorGUILayout.HelpBox(msg, MessageType.Warning);
}
if (renderProfileInBox)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
}
else
{
EditorGUILayout.BeginVertical();
}
EditorGUILayout.Space();
subProfileEditor.OnInspectorGUI();
EditorGUILayout.Space();
EditorGUILayout.EndVertical();
}
/// <summary>
/// Draws a dropdown with all available profiles of profilyType.
/// </summary>
/// <returns>True if property was changed.</returns>
public static bool DrawProfileDropDownList(SerializedProperty property, BaseMixedRealityProfile profile, Object oldProfileObject, Type profileType, bool showAddButton)
{
bool changed = false;
using (new EditorGUILayout.HorizontalScope())
{
// Pull profile instances and profile content from cache
ScriptableObject[] profileInstances = MixedRealityProfileUtility.GetProfilesOfType(profileType);
GUIContent[] profileContent = MixedRealityProfileUtility.GetProfilePopupOptionsByType(profileType);
// Set our selected index to our '(None)' option by default
int selectedIndex = 0;
// Find our selected index
for (int i = 0; i < profileInstances.Length; i++)
{
if (profileInstances[i] == oldProfileObject)
{ // Our profile content has a '(None)' option at the start
selectedIndex = i + 1;
break;
}
}
int newIndex = EditorGUILayout.Popup(
new GUIContent(oldProfileObject != null ? "" : property.displayName),
selectedIndex,
profileContent,
GUILayout.ExpandWidth(true));
property.objectReferenceValue = (newIndex > 0) ? profileInstances[newIndex - 1] : null;
changed = property.objectReferenceValue != oldProfileObject;
// Draw a button that finds the profile in the project window
if (property.objectReferenceValue != null)
{
// The view asset button should always be enabled.
using (new GUIEnabledWrapper())
{
if (GUILayout.Button("View Asset", EditorStyles.miniButton, GUILayout.Width(80)))
{
EditorGUIUtility.PingObject(property.objectReferenceValue);
}
}
}
// Draw the clone button
if (property.objectReferenceValue == null)
{
if (showAddButton && MixedRealityProfileUtility.IsConcreteProfileType(profileType))
{
if (GUILayout.Button(NewProfileContent, EditorStyles.miniButton, GUILayout.Width(20f)))
{
ScriptableObject instance = ScriptableObject.CreateInstance(profileType);
var newProfile = instance.CreateAsset(AssetDatabase.GetAssetPath(Selection.activeObject)) as BaseMixedRealityProfile;
property.objectReferenceValue = newProfile;
property.serializedObject.ApplyModifiedProperties();
changed = true;
}
}
}
else
{
var renderedProfile = property.objectReferenceValue as BaseMixedRealityProfile;
Debug.Assert(renderedProfile != null);
if (GUILayout.Button(new GUIContent("Clone", "Replace with a copy of the default profile."), EditorStyles.miniButton, GUILayout.Width(45f)))
{
MixedRealityProfileCloneWindow.OpenWindow(profile, renderedProfile, property);
}
}
}
return changed;
}
#endregion
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Editor;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
/// <summary>
/// This class has utilities and functions for working with profiles in the Unity editor.
/// </summary>
public static class MixedRealityProfileUtility
{
/// <summary>
/// Private class that listens for asset modifications and updates caches.
/// </summary>
private class AssetImportListener : UnityEditor.AssetPostprocessor
{
public static Action OnAssetsChanged { get; set; }
public static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
if (Application.isPlaying)
{
return;
}
OnAssetsChanged?.Invoke();
}
}
private static Dictionary<Type, ScriptableObject[]> profileCaches = new Dictionary<Type, ScriptableObject[]>();
private static Dictionary<Type, GUIContent[]> profileContentCaches = new Dictionary<Type, GUIContent[]>();
private static Dictionary<Type, Type[]> profileTypesForServiceCaches = new Dictionary<Type, Type[]>();
/// <summary>
/// Returns an array of profiles that match profile type.
/// </summary>
public static ScriptableObject[] GetProfilesOfType(Type profileType)
{
ScriptableObject[] profilesOfType = null;
if (!profileCaches.TryGetValue(profileType, out profilesOfType))
{
profilesOfType = ScriptableObjectExtensions.GetAllInstances(profileType);
profileCaches.Add(profileType, profilesOfType);
}
return profilesOfType;
}
/// <summary>
/// Returns an array of GUIContent for use in a dropdown for a type of profile.
/// Includes a (None) option at the start. This means that the array length will always be 1 greater than the available profiles.
/// </summary>
public static GUIContent[] GetProfilePopupOptionsByType(Type profileType)
{
GUIContent[] profileContent = null;
if (!profileContentCaches.TryGetValue(profileType, out profileContent))
{
ScriptableObject[] profilesOfType = GetProfilesOfType(profileType);
profileContent = new GUIContent[profilesOfType.Length + 1];
profileContent[0] = new GUIContent("(None)");
for (int i = 0; i < profilesOfType.Length; i++)
{
profileContent[i + 1] = new GUIContent(profilesOfType[i].name);
}
profileContentCaches.Add(profileType, profileContent);
}
return profileContent;
}
/// <summary>
/// Returns true if the given profile type is designed to configure the given service.
/// </summary>
public static bool IsProfileForService(Type profileType, Type serviceType)
{
foreach (MixedRealityServiceProfileAttribute serviceProfileAttribute in profileType.GetCustomAttributes(typeof(MixedRealityServiceProfileAttribute), true))
{
bool requirementsMet = true;
foreach (Type requiredType in serviceProfileAttribute.RequiredTypes)
{
if (!requiredType.IsAssignableFrom(serviceType))
{
requirementsMet = false;
break;
}
}
if (requirementsMet)
{
foreach (Type excludedType in serviceProfileAttribute.ExcludedTypes)
{
if (excludedType.IsAssignableFrom(serviceType))
{
requirementsMet = false;
break;
}
}
}
return requirementsMet;
}
return false;
}
/// <summary>
/// Returns true if profile is NOT a BaseMixedRealityProfile class type.
/// </summary>
public static bool IsConcreteProfileType(Type profileType)
{
return profileType != typeof(BaseMixedRealityProfile);
}
/// <summary>
/// Given a service type, finds all sub-classes of BaseMixedRealityProfile that are
/// designed to configure that service.
/// </summary>
public static IReadOnlyCollection<Type> GetProfileTypesForService(Type serviceType)
{
if (serviceType == null)
{
return Array.Empty<Type>();
}
Type[] types;
if (!profileTypesForServiceCaches.TryGetValue(serviceType, out types))
{
HashSet<Type> allTypes = new HashSet<Type>();
ScriptableObject[] allProfiles = GetProfilesOfType(typeof(BaseMixedRealityProfile));
for (int i = 0; i < allProfiles.Length; i++)
{
ScriptableObject profile = allProfiles[i];
if (IsProfileForService(profile.GetType(), serviceType))
{
allTypes.Add(profile.GetType());
}
}
types = allTypes.ToArray();
profileTypesForServiceCaches.Add(serviceType, types);
}
return types;
}
[InitializeOnLoadMethod]
private static void InitializeOnLoad()
{
AssetImportListener.OnAssetsChanged += AssetImportListener_OnAssetsChange;
}
private static void AssetImportListener_OnAssetsChange()
{
RefreshProfileCaches();
}
private static void RefreshProfileCaches()
{
List<Type> cachedTypes = new List<Type>(profileCaches.Keys);
profileContentCaches.Clear();
foreach (Type profileType in cachedTypes)
{
profileCaches[profileType] = ScriptableObjectExtensions.GetAllInstances(profileType);
}
}
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor
{
public static class MixedRealityStylesUtility
{
/// <summary>
/// Default style for foldouts with bold title
/// </summary>
public static readonly GUIStyle BoldFoldoutStyle =
new GUIStyle(EditorStyles.foldout)
{
fontStyle = FontStyle.Bold
};
/// <summary>
/// Default style for foldouts with bold large font size title
/// </summary>
public static readonly GUIStyle BoldTitleFoldoutStyle =
new GUIStyle(EditorStyles.foldout)
{
fontStyle = FontStyle.Bold,
fontSize = InspectorUIUtility.TitleFontSize,
};
/// <summary>
/// Default style for foldouts with large font size title
/// </summary>
public static readonly GUIStyle TitleFoldoutStyle =
new GUIStyle(EditorStyles.foldout)
{
fontSize = InspectorUIUtility.TitleFontSize,
};
/// <summary>
/// Default style for controller mapping buttons
/// </summary>
public static readonly GUIStyle ControllerButtonStyle = new GUIStyle("LargeButton")
{
imagePosition = ImagePosition.ImageAbove,
fixedHeight = 128,
fontStyle = FontStyle.Bold,
stretchHeight = true,
stretchWidth = true,
wordWrap = true,
fontSize = 10,
};
/// <summary>
/// Default style for bold large font size title
/// </summary>
public static readonly GUIStyle BoldLargeTitleStyle = new GUIStyle(EditorStyles.largeLabel)
{
fontSize = InspectorUIUtility.TitleFontSize,
fontStyle = FontStyle.Bold,
};
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using UnityEditor;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor.Search
{
/// <summary>
/// Struct for storing search results
/// </summary>
public struct FieldSearchResult
{
public SerializedProperty Property;
public int MatchStrength;
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Editor;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor.Search
{
/// <summary>
/// Utility class for drawing search interface.
/// Draws a search field by default. When search is active, draws search results.
/// Also handles the business of storing search configuration and results so searching objects don't have to.
/// </summary>
public static class MixedRealitySearchInspectorUtility
{
private static readonly string searchDisplaySearchFieldKey = "MixedRealityToolkitInspector.SearchField";
private static readonly string searchDisplayRequireAllKeywordsKey = "MixedRealityToolkitInspector.RequireAllKeywords";
private static readonly string searchDisplayOptionsFoldoutKey = "MixedRealityToolkitInspector.SearchOptionsFoldout";
private static readonly string searchDisplaySearchTooltipsKey = "MixedRealityToolkitInspector.SearchTooltips";
private static readonly string searchDisplaySearchFieldContentKey = "MixedRealityToolkitInspector.SearchFieldContent";
private static SearchConfig config;
private static SearchConfig prevConfig;
private static UnityEngine.Object activeTarget;
private static List<ProfileSearchResult> searchResults = new List<ProfileSearchResult>();
/// <summary>
/// Draws a search field and (if results have been returned) search results.
/// </summary>
/// <returns>True if search results are being displayed.</returns>
public static bool DrawSearchInterface(UnityEngine.Object target)
{
if (target == null)
{
return false;
}
bool drewSearchGUI = false;
if (target != activeTarget)
{
activeTarget = target;
config = new SearchConfig();
prevConfig = new SearchConfig();
searchResults.Clear();
}
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("Search For: ", GUILayout.MaxWidth(70));
string searchString = SessionState.GetString(searchDisplaySearchFieldKey, string.Empty);
config.SearchFieldString = EditorGUILayout.TextField(SessionState.GetString(searchDisplaySearchFieldKey, string.Empty), GUILayout.ExpandWidth(true));
if (GUILayout.Button("Clear", EditorStyles.miniButton, GUILayout.MaxWidth(50)))
{
config.SearchFieldString = string.Empty;
}
}
config.RequireAllKeywords = SessionState.GetBool(searchDisplayRequireAllKeywordsKey, true);
config.SearchTooltips = SessionState.GetBool(searchDisplaySearchTooltipsKey, true);
config.SearchFieldContent = SessionState.GetBool(searchDisplaySearchFieldContentKey, false);
if (!string.IsNullOrEmpty(config.SearchFieldString))
{
// If we're searching for something, draw the search GUI
DrawSearchResultInterface(target);
drewSearchGUI = true;
}
// Store search settings
SessionState.SetString(searchDisplaySearchFieldKey, config.SearchFieldString);
SessionState.SetBool(searchDisplayRequireAllKeywordsKey, config.RequireAllKeywords);
SessionState.SetBool(searchDisplaySearchTooltipsKey, config.SearchTooltips);
SessionState.SetBool(searchDisplaySearchFieldContentKey, config.SearchFieldContent);
return drewSearchGUI;
}
private static void DrawSearchResultInterface(UnityEngine.Object target)
{
bool optionsFoldout = SessionState.GetBool(searchDisplayOptionsFoldoutKey, false);
optionsFoldout = EditorGUILayout.Foldout(optionsFoldout, "Search Preferences", true);
SessionState.SetBool(searchDisplayOptionsFoldoutKey, optionsFoldout);
if (optionsFoldout)
{
config.RequireAllKeywords = EditorGUILayout.Toggle("Require All Keywords", config.RequireAllKeywords);
config.SearchTooltips = EditorGUILayout.Toggle("Search Tooltips", config.SearchTooltips);
config.SearchFieldContent = EditorGUILayout.Toggle("Search Field Content", config.SearchFieldContent);
}
if (!config.Equals(prevConfig) && !MixedRealitySearchUtility.Searching)
{
MixedRealitySearchUtility.StartProfileSearch(target, config, OnSearchComplete);
searchResults.Clear();
prevConfig = config;
}
#region display results
using (new EditorGUILayout.VerticalScope())
{
if (searchResults.Count == 0)
{
if (MixedRealitySearchUtility.Searching)
{
EditorGUILayout.HelpBox("Searching...", MessageType.Info);
}
else
{
EditorGUILayout.HelpBox("No search results. Try selecting a subject or entering a keyword.", MessageType.Warning);
}
}
int numDisplayedSearchResults = 0;
if (searchResults.Count > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Results:");
foreach (ProfileSearchResult search in searchResults)
{
if (search.Fields.Count == 0)
{ // Don't display results with no fields
continue;
}
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("Fields found in: ", EditorStyles.boldLabel, GUILayout.MaxWidth(105));
EditorGUILayout.ObjectField(search.Profile, typeof(UnityEngine.Object), false, GUILayout.ExpandWidth(true));
if (GUILayout.Button("View Asset", GUILayout.MaxWidth(75)))
{
Selection.activeObject = search.Profile;
EditorGUIUtility.PingObject(search.Profile);
}
}
if (MixedRealityProjectPreferences.LockProfiles && !search.IsCustomProfile)
{
EditorGUILayout.HelpBox("Clone this profile to edit default properties.", MessageType.Warning);
}
using (new EditorGUI.DisabledGroupScope(MixedRealityProjectPreferences.LockProfiles && !search.IsCustomProfile))
{
using (new EditorGUI.IndentLevelScope(1))
{
EditorGUILayout.Space();
foreach (FieldSearchResult r in search.Fields)
{
numDisplayedSearchResults++;
GUI.color = Color.white;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(r.Property, true);
if (EditorGUI.EndChangeCheck())
{
r.Property.serializedObject.ApplyModifiedProperties();
}
EditorGUILayout.Space();
}
}
}
}
EditorGUILayout.Space();
}
}
}
#endregion
}
private static void OnSearchComplete(bool cancelled, UnityEngine.Object target, IReadOnlyCollection<ProfileSearchResult> results)
{
searchResults.Clear();
if (cancelled || target != MixedRealitySearchInspectorUtility.activeTarget)
{ // We've started searching something else, ignore
return;
}
searchResults.AddRange(results);
// Force editors to repaint so the results are displayed
foreach (UnityEditor.Editor e in Resources.FindObjectsOfTypeAll<UnityEditor.Editor>())
{
e.Repaint();
}
}
}
}
\ No newline at end of file
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.SceneSystem;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Editor.Search
{
/// <summary>
/// Utility for retrieving a Unity object's serialized fields with a configurable search.
/// </summary>
public static class MixedRealitySearchUtility
{
/// <summary>
/// True if a search is being executed. This must be false before calling StartProfileSearch.
/// </summary>
public static bool Searching { get { return activeTask != null && !activeTask.IsCompleted; } }
private const int maxChildSearchDepth = 5;
private const int minSearchStringLength = 3;
private static Task activeTask;
/// <summary>
/// Field names that shouldn't be displayed in a profile field search.
/// </summary>
private static readonly HashSet<string> serializedPropertiesToIgnore = new HashSet<string>()
{
// Unity base class fields
"m_Name",
"m_Script",
"m_Enabled",
"m_GameObject",
"m_ObjectHideFlags",
"m_CorrespondingSourceObject",
"m_PrefabInstance",
"m_PrefabAsset",
"m_EditorHideFlags",
"m_EditorClassIdentifier",
// Profile base class fields
"isCustomProfile",
};
/// <summary>
/// Field types that don't need their child properties displayed.
/// </summary>
private static readonly HashSet<string> serializedPropertyTypesToFlatten = new HashSet<string>()
{
typeof(SystemType).Name,
typeof(SceneInfo).Name,
};
/// <summary>
/// Starts a profile search. 'Searching' must be false or an exception will be thrown.
/// </summary>
/// <param name="profile">Profile object to search.</param>
/// <param name="config">Configuration for the search.</param>
/// <param name="onSearchComplete">Action to invoke once search is complete - delivers final results.</param>
public static async void StartProfileSearch(UnityEngine.Object profile, SearchConfig config, Action<bool, UnityEngine.Object, IReadOnlyCollection<ProfileSearchResult>> onSearchComplete)
{
if (activeTask != null && !activeTask.IsCompleted)
{
throw new Exception("Can't start a new search until the old one has completed.");
}
List<ProfileSearchResult> searchResults = new List<ProfileSearchResult>();
// Validate search configuration
if (string.IsNullOrEmpty(config.SearchFieldString))
{ // If the config is empty, bail early
onSearchComplete?.Invoke(true, profile, searchResults);
return;
}
// Generate keywords if we haven't yet
if (config.Keywords == null)
{
config.Keywords = new HashSet<string>(config.SearchFieldString.Split(new string[] { " ", "," }, StringSplitOptions.RemoveEmptyEntries));
config.Keywords.RemoveWhere(s => s.Length < minSearchStringLength);
}
if (config.Keywords.Count == 0)
{ // If there are no useful keywords, bail early
onSearchComplete?.Invoke(true, profile, searchResults);
return;
}
// Launch the search task
bool cancelled = false;
try
{
activeTask = SearchProfileField(profile, config, searchResults);
await activeTask;
}
catch (Exception e)
{
// Profile was probably deleted in the middle of searching.
Debug.LogException(e);
cancelled = true;
}
finally
{
searchResults.Sort(delegate (ProfileSearchResult r1, ProfileSearchResult r2)
{
if (r1.ProfileMatchStrength != r2.ProfileMatchStrength)
{
return r2.ProfileMatchStrength.CompareTo(r1.ProfileMatchStrength);
}
else
{
return r2.Profile.name.CompareTo(r1.Profile.name);
}
});
searchResults.RemoveAll(r => r.Fields.Count <= 0);
onSearchComplete?.Invoke(cancelled, profile, searchResults);
}
}
private static async Task SearchProfileField(UnityEngine.Object profile, SearchConfig config, List<ProfileSearchResult> searchResults)
{
await Task.Yield();
// The result that we will return, if not empty
ProfileSearchResult result = new ProfileSearchResult();
result.Profile = profile;
BaseMixedRealityProfile baseProfile = (profile as BaseMixedRealityProfile);
result.IsCustomProfile = (baseProfile != null) ? baseProfile.IsCustomProfile : false;
searchResults.Add(result);
// Go through the profile's serialized fields
foreach (SerializedProperty property in GatherProperties(profile))
{
if (CheckFieldForProfile(property))
{
await SearchProfileField(property.objectReferenceValue, config, searchResults);
}
else
{
CheckFieldForKeywords(property, config, result);
}
}
if (result.Fields.Count > 0)
{
result.Fields.Sort(delegate (FieldSearchResult r1, FieldSearchResult r2)
{
if (r1.MatchStrength != r2.MatchStrength)
{
return r2.MatchStrength.CompareTo(r1.MatchStrength);
}
return r2.Property.name.CompareTo(r1.Property.name);
});
}
}
private static bool CheckFieldForProfile(SerializedProperty property)
{
bool isProfileField = false;
if (property.propertyType == SerializedPropertyType.ObjectReference && property.objectReferenceValue != null)
{
Type referenceType = property.objectReferenceValue.GetType();
isProfileField = (typeof(BaseMixedRealityProfile).IsAssignableFrom(referenceType));
}
return isProfileField;
}
private static IEnumerable<SerializedProperty> GatherProperties(UnityEngine.Object profile)
{
List<SerializedProperty> properties = new List<SerializedProperty>();
SerializedProperty iterator = new SerializedObject(profile).GetIterator();
bool hasNextProperty = iterator.Next(true);
while (hasNextProperty)
{
if (!serializedPropertiesToIgnore.Contains(iterator.name) && iterator.depth < maxChildSearchDepth)
{
properties.Add(iterator.Copy());
}
if (serializedPropertyTypesToFlatten.Contains(iterator.type))
{
hasNextProperty = iterator.Next(false);
}
else
{
hasNextProperty = iterator.Next(true);
}
}
return properties;
}
private static void CheckFieldForKeywords(SerializedProperty property, SearchConfig config, ProfileSearchResult result)
{
int numMatchedKeywords = 0;
int numExactMatches = 0;
int numFieldMatches = 0;
int numTooltipMatches = 0;
int numContentMatches = 0;
string propertyName = property.name.ToLower();
string toolTip = property.tooltip.ToLower();
foreach (string keyword in config.Keywords)
{
bool keywordMatch = false;
if (propertyName.Contains(keyword))
{
keywordMatch = true;
numFieldMatches++;
if (propertyName == keyword)
{
numExactMatches++;
}
}
if (config.SearchTooltips)
{
if (toolTip.Contains(keyword))
{
keywordMatch = true;
numTooltipMatches++;
}
}
if (config.SearchFieldContent)
{
switch (property.propertyType)
{
case SerializedPropertyType.ObjectReference:
if (property.objectReferenceValue != null && property.objectReferenceValue.name.ToLower().Contains(keyword))
{
keywordMatch = true;
numContentMatches++;
}
break;
case SerializedPropertyType.String:
if (!string.IsNullOrEmpty(property.stringValue) && property.stringValue.ToLower().Contains(keyword))
{
keywordMatch = true;
numContentMatches++;
}
break;
}
}
if (keywordMatch)
{
numMatchedKeywords++;
}
}
bool requirementsMet = numMatchedKeywords > 0;
if (config.RequireAllKeywords && config.Keywords.Count > 1)
{
requirementsMet &= numMatchedKeywords >= config.Keywords.Count;
}
if (requirementsMet)
{
int matchStrength = numMatchedKeywords + numExactMatches;
if (numMatchedKeywords >= config.Keywords.Count)
{ // If we match all keywords in a multi-keyword search, double the score
matchStrength *= 2;
}
// Weight the score based on match type
matchStrength += numFieldMatches * 3;
matchStrength += numTooltipMatches * 2;
matchStrength += numContentMatches * 1;
result.ProfileMatchStrength += matchStrength;
result.Fields.Add(new FieldSearchResult()
{
Property = property.Copy(),
MatchStrength = numMatchedKeywords,
});
}
}
}
}
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment