/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.Assertions; namespace Oculus.Interaction.Editor { /// /// A utility class for building custom editors with less work required. /// public class EditorBase { private SerializedObject _serializedObject; private HashSet _hiddenProperties = new HashSet(); private HashSet _skipProperties = new HashSet(); private Dictionary> _customDrawers = new Dictionary>(); private Dictionary _sections = new Dictionary(); private List _orderedSections = new List(); public class Section { public string title; public bool isFoldout; public List properties = new List(); private string _foldoutKey = null; private bool _foldout = false; public bool Foldout { get { return _foldout; } set { if (_foldout == value) { return; } _foldout = value; if (_foldoutKey != null) { EditorPrefs.SetBool(_foldoutKey, _foldout); } } } public bool HasSomethingToDraw() { return properties != null && properties.Count > 0; } public Section(string title, bool isFoldout, string foldoutKey = null) { this.title = title; this.isFoldout = isFoldout; this._foldoutKey = isFoldout && !string.IsNullOrEmpty(foldoutKey) ? foldoutKey : null; this.Foldout = _foldoutKey != null ? EditorPrefs.GetBool(_foldoutKey, false) : false; } } public EditorBase(SerializedObject serializedObject) { _serializedObject = serializedObject; } #region Sections private Section GetOrCreateSection(string sectionName) { if (_sections.TryGetValue(sectionName, out Section existingSection)) { return existingSection; } Section section = CreateSection(sectionName, false); return section; } public Section CreateSection(string sectionName, bool isFoldout, string foldoutKey = null) { if (_sections.TryGetValue(sectionName, out Section existingSection)) { Debug.LogError($"Section {sectionName} already exists"); return null; } Section section = new Section(sectionName, isFoldout, foldoutKey); _sections.Add(sectionName, section); _orderedSections.Add(sectionName); return section; } public void CreateSections(Dictionary sections, bool isFoldout) { foreach (var sectionData in sections) { CreateSection(sectionData.Key, isFoldout); AddToSection(sectionData.Key, sectionData.Value); } } public void AddToSection(string sectionName, params string[] properties) { if (properties.Length == 0 || !ValidateProperties(properties)) { return; } Section section = GetOrCreateSection(sectionName); foreach (var property in properties) { section.properties.Add(property); _skipProperties.Add(property); } } #endregion #region API /// /// Call in OnEnable with one or more property names to hide them from the inspector. /// /// This is preferable to using [HideInInspector] because it still allows the property to /// be viewed when using the Inspector debug mode. /// public void Hide(params string[] properties) { Assert.IsTrue(properties.Length > 0, "Should always hide at least one property."); if (!ValidateProperties(properties)) { return; } _hiddenProperties.UnionWith(properties); } /// /// Call in OnInit to specify a custom drawer for a single property. Whenever the property is drawn, /// it will use the provided property drawer instead of the default one. /// public void Draw(string property, Action drawer) { if (!ValidateProperties(property)) { return; } _customDrawers.Add(property, drawer); } /// /// Call in OnInit to specify a custom drawer for a single property. Include an extra property that gets /// lumped in with the primary property. The extra property is not drawn normally, and is instead grouped in /// with the primary property. Can be used in situations where a collection of properties need to be drawn together. /// public void Draw(string property, string withExtra0, Action drawer) { if (!ValidateProperties(property, withExtra0)) { return; } Hide(withExtra0); Draw(property, p => { drawer(p, _serializedObject.FindProperty(withExtra0)); }); } public void Draw(string property, string withExtra0, string withExtra1, Action drawer) { if (!ValidateProperties(property, withExtra0, withExtra1)) { return; } Hide(withExtra0); Hide(withExtra1); Draw(property, p => { drawer(p, _serializedObject.FindProperty(withExtra0), _serializedObject.FindProperty(withExtra1)); }); } public void Draw(string property, string withExtra0, string withExtra1, string withExtra2, Action drawer) { if (!ValidateProperties(property, withExtra0, withExtra1, withExtra2)) { return; } Hide(withExtra0); Hide(withExtra1); Hide(withExtra2); Draw(property, p => { drawer(p, _serializedObject.FindProperty(withExtra0), _serializedObject.FindProperty(withExtra1), _serializedObject.FindProperty(withExtra2)); }); } public void Draw(string property, string withExtra0, string withExtra1, string withExtra2, string withExtra3, Action drawer) { if (!ValidateProperties(property, withExtra0, withExtra1, withExtra2, withExtra3)) { return; } Hide(withExtra0); Hide(withExtra1); Hide(withExtra2); Hide(withExtra3); Draw(property, p => { drawer(p, _serializedObject.FindProperty(withExtra0), _serializedObject.FindProperty(withExtra1), _serializedObject.FindProperty(withExtra2), _serializedObject.FindProperty(withExtra3)); }); } #endregion #region IMPLEMENTATION /// /// Indicates if the property in the serializedObject /// has been assigned to a section /// /// The name of the property in the serialized object /// True if the property has been added to a section public bool IsInSection(string property) { return _skipProperties.Contains(property); } /// /// Indicates if the property in the serializedObject /// needs to be hidden. /// Hidden properties are typically drawn in a custom way /// so they don't need to be drawn with the default methods. /// /// The name of the property in the serialized object /// True if the property has been hidden public bool IsHidden(string property) { return _hiddenProperties.Contains(property); } /// /// Draws all the visible (non hidden)properties in the serialized /// object following this order: /// First all properties that has not been added to sections, /// in the order they appear in the Component. /// Then all the sections (indented and in foldouts) in the order /// they were created, with the internal properties ordered in the /// order the properties were added to the section. /// /// If a special property drawer was specified it will use it when /// drawing said property. /// /// If a property was hidden, it will not present it in any section. /// public void DrawFullInspector() { SerializedProperty it = _serializedObject.GetIterator(); it.NextVisible(enterChildren: true); //Draw script header EditorGUI.BeginDisabledGroup(true); EditorGUILayout.PropertyField(it); EditorGUI.EndDisabledGroup(); EditorGUI.BeginChangeCheck(); while (it.NextVisible(enterChildren: false)) { //Don't draw skip properties in this pass, we will draw them after everything else if (IsInSection(it.name)) { continue; } DrawProperty(it); } foreach (string sectionKey in _orderedSections) { DrawSection(sectionKey); } _serializedObject.ApplyModifiedProperties(); } /// /// Draws all the properties in the section in the order /// they were added. /// /// The name of the section public void DrawSection(string sectionName) { Section section = _sections[sectionName]; DrawSection(section); } private void DrawSection(Section section) { if (!section.HasSomethingToDraw()) { return; } if (section.isFoldout) { section.Foldout = EditorGUILayout.Foldout(section.Foldout, section.title); if (!section.Foldout) { return; } EditorGUI.indentLevel++; } foreach (string prop in section.properties) { DrawProperty(_serializedObject.FindProperty(prop)); } if (section.isFoldout) { EditorGUI.indentLevel--; } } private void DrawProperty(SerializedProperty property) { try { //Don't draw hidden properties if (IsHidden(property.name)) { return; } //Then draw the property itself, using a custom drawer if needed Action customDrawer; if (_customDrawers.TryGetValue(property.name, out customDrawer)) { customDrawer(property); } else { EditorGUILayout.PropertyField(property, includeChildren: true); } } catch (ExitGUIException e) { // "exception handling in your GUI code, should not catch this exception type" // https://docs.unity3d.com/ScriptReference/ExitGUIException.html throw e; } catch (Exception e) { Debug.LogError($"Error drawing '{property.name}' property {e.Message}"); Debug.LogException(e); } } private bool ValidateProperties(params string[] properties) { foreach (var property in properties) { if (_serializedObject.FindProperty(property) == null) { Debug.LogWarning( $"Could not find property {property}, maybe it was deleted or renamed?"); return false; } } return true; } #endregion } }