added more utilty packges

This commit is contained in:
Sebastian Bularca
2026-03-29 18:59:24 +02:00
parent ee97b2fec3
commit 71b432e253
131 changed files with 7674 additions and 54 deletions

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jovian.Utilities;
namespace Nox.Game {
public interface IModfiersFactory {
IReadOnlyCollection<ModifierDefinition> GetAll();
ModifierDefinition GetById(ModifierIds modifierId);
ModifierDefinition GetById(Guid modifierId);
IReadOnlyCollection<ModifierDefinition> GetModifiersFor(CharacterDefinition character);
bool TryAddModifier(CharacterDefinition character, string modiferId);
}
@@ -20,7 +21,6 @@ namespace Nox.Game {
[Serializable]
public sealed class ModifierDefinition {
[ReadOnlyField]
public System.Guid id = Guid.NewGuid();
public StatType statType;
public AttributeType attributeType;
@@ -44,7 +44,7 @@ namespace Nox.Game {
public IReadOnlyCollection<ModifierDefinition> GetAll() {
return modifiersRegistry.modifiersData.modifiers;
}
public ModifierDefinition GetById(ModifierIds modifierId) {
public ModifierDefinition GetById(Guid modifierId) {
return modifiersRegistry.modifiersData.modifiers.FirstOrDefault(m => m.id == modifierId);
}
public IReadOnlyCollection<ModifierDefinition> GetModifiersFor(CharacterDefinition character) {

View File

@@ -6,14 +6,14 @@ namespace Nox.Game {
public interface IPerkFactory {
IReadOnlyCollection<PerkDefinition> GetAll();
PerkDefinition GetById(PerksIds perkId);
PerkDefinition GetById(Guid perkId);
IReadOnlyCollection<PerkDefinition> GetPerksFor(CharacterDefinition character);
bool TryAddPerk(CharacterDefinition character, PerksIds perkId);
bool TryAddPerk(CharacterDefinition character, Guid perkId);
}
[Serializable]
public sealed class PerkDefinition {
public PerksIds id;
public Guid id;
public string name;
public ModifiersData modifiers = new ();
}
@@ -24,7 +24,7 @@ namespace Nox.Game {
}
public sealed class PerkFactory : IPerkFactory {
private readonly Dictionary<PerksIds, PerkDefinition> perkPool = new ();
private readonly Dictionary<Guid, PerkDefinition> perkPool = new ();
public PerkFactory(PerksRegistry perksRegistry) {
if(!perksRegistry) {
@@ -40,7 +40,7 @@ namespace Nox.Game {
return perkPool.Values.ToList();
}
public PerkDefinition GetById(PerksIds perkId) {
public PerkDefinition GetById(Guid perkId) {
perkPool.TryGetValue(perkId, out var perk);
return perk;
}
@@ -57,7 +57,7 @@ namespace Nox.Game {
return perkPool.Values.Where(p => !ownedPerkIds.Contains(p.id)).ToList();
}
public bool TryAddPerk(CharacterDefinition character, PerksIds perkId) {
public bool TryAddPerk(CharacterDefinition character, Guid perkId) {
if(character == null) {
return false;
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 40aa0aa493d1bb745a531cdb360e7e62
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 67fe3f48aa2b3b349a7b99381bebb12d, type: 3}
m_Name: logger-settings
m_EditorClassIdentifier: Jovian.Logger::Jovian.Logger.LoggerSettings
enableGlobalLogging: 1
globalFilters: []
loggerColors:
infoColor: {r: 1, g: 1, b: 1, a: 1}
warningColor: {r: 1, g: 0.92156863, b: 0.015686275, a: 1}
errorColor: {r: 1, g: 0, b: 0, a: 1}
assertColor: {r: 1, g: 0.3, b: 0.2, a: 1}
exceptionColor: {r: 1, g: 0, b: 0.7, a: 1}
spamColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 97634890f80d79941abb209746593eef
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 400491600b7856d4dbb01f428e9dfbfb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8dc5c6745ff8d4443955c7fd0c3dfb0d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace InspectorToolkit {
public class AnimatorParameterAttribute : PropertyAttribute {
public readonly string animatorPropertyName;
public bool allParameterTypes;
public AnimatorControllerParameterType parameterType;
public AnimatorParameterAttribute(string animatorPropertyName) {
this.animatorPropertyName = animatorPropertyName;
allParameterTypes = true;
}
public AnimatorParameterAttribute(string animatorPropertyName, AnimatorControllerParameterType parameterType) {
this.animatorPropertyName = animatorPropertyName;
allParameterTypes = false;
this.parameterType = parameterType;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(AnimatorParameterAttribute))]
public class AnimatorParameterAttributeDrawer : PropertyDrawer {
private bool hasAnimator;
private Animator animator;
private string[] parameterNames;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
if(parameterNames == null) {
var animatorParameterAttribute = (AnimatorParameterAttribute)attribute;
string animatorFieldName = animatorParameterAttribute.animatorPropertyName;
parameterNames = UpdateParameterNames(property, animatorFieldName, animatorParameterAttribute.allParameterTypes, animatorParameterAttribute.parameterType);
}
EditorGUI.BeginChangeCheck();
bool didChange = false;
if(parameterNames is { Length: > 0 }) {
int parameterIndex = Array.IndexOf(parameterNames, property.stringValue);
int newIndex = EditorGUI.Popup(position, parameterIndex, parameterNames);
if(parameterIndex != newIndex) {
property.stringValue = parameterNames[newIndex];
didChange = true;
}
}
else {
EditorGUI.PropertyField(position, property, GUIContent.none);
}
int indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
if(EditorGUI.EndChangeCheck() || didChange) {
property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.indentLevel = indentLevel;
EditorGUI.EndProperty();
}
private string[] UpdateParameterNames(SerializedProperty property, string animatorFieldName, bool allParameterTypesAllowed, AnimatorControllerParameterType parameterType) {
int pathIndexOfPropertyName = property.propertyPath.LastIndexOf(property.name, StringComparison.Ordinal);
bool isTopLevelObject = pathIndexOfPropertyName == 0;
SerializedProperty animatorProperty;
if(isTopLevelObject) {
animatorProperty = property.serializedObject.FindProperty(animatorFieldName);
}
else {
SerializedProperty parentProperty = property.serializedObject.FindProperty(property.propertyPath.Substring(0, pathIndexOfPropertyName - 1));
animatorProperty = parentProperty.FindPropertyRelative(animatorFieldName);
}
if(animatorProperty is { objectReferenceValue: Animator targetAnimator }) {
if(!targetAnimator.gameObject.activeSelf ||
!targetAnimator.gameObject.activeInHierarchy ||
targetAnimator.runtimeAnimatorController == null) {
return new string[] { }; // return an empty array so it uses the fallback of just text
}
AnimatorControllerParameter[] parameters = targetAnimator.parameters;
if(allParameterTypesAllowed) {
return parameters.Select(p => p.name).ToArray();
}
else {
return parameters.Where(p => p.type == parameterType).Select(p => p.name).ToArray();
}
}
return new[] { "<MISSING>" };
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c43d05448be2cce45a0c4a41227752aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using System.Reflection;
using UnityEditor;
#endif
namespace InspectorToolkit {
[AttributeUsage(AttributeTargets.Field)]
public abstract class ArrayAttribute : Attribute {
#if UNITY_EDITOR
public virtual void OnPreGUI(ref Rect position, SerializedProperty property, ref GUIContent label) {
}
public virtual void OnPostGUI(ref Rect position, SerializedProperty property, ref GUIContent label) {
}
#endif
}
#if UNITY_EDITOR
public static class ArrayAttributePropertyHandler {
public static bool DrawArrayProperty(SerializedProperty property) {
return DrawArrayProperty(property,
new GUIContent(ObjectNames.NicifyVariableName(property.name), property.tooltip),
true
);
}
public static bool DrawArrayProperty(SerializedProperty property, GUIContent label, bool includeChildren, params GUILayoutOption[] options) {
Rect rect = EditorGUILayout.GetControlRect(LabelHasContent(label), EditorGUI.GetPropertyHeight(property, label, includeChildren), options);
bool hasArrayAttributes = TryGetAttributes(property, true, out List<ArrayAttribute> arrayAttributes);
bool isGuiEnabled = GUI.enabled;
Color contentColor = GUI.contentColor;
Color backgroundColor = GUI.backgroundColor;
Color guiColor = GUI.color;
Rect preRect = new Rect(rect);
Rect postRect = new Rect(rect);
if (hasArrayAttributes) {
foreach (ArrayAttribute arrayAttribute in arrayAttributes) {
arrayAttribute.OnPreGUI(ref preRect, property, ref label);
}
}
bool hasChildPropertiesNotShown = EditorGUI.PropertyField(preRect, property, label, includeChildren);
if (hasArrayAttributes) {
foreach (ArrayAttribute arrayAttribute in arrayAttributes) {
arrayAttribute.OnPostGUI(ref postRect, property, ref label);
}
}
GUI.enabled = isGuiEnabled;
GUI.contentColor = contentColor;
GUI.backgroundColor = backgroundColor;
GUI.color = guiColor;
return hasChildPropertiesNotShown;
}
private static bool LabelHasContent(GUIContent label)
{
return label == null || label.text != string.Empty || (UnityEngine.Object) label.image != (UnityEngine.Object) null;
}
public static bool TryGetAttributes<TAttribute>(SerializedProperty serializedProperty, bool inherit, out List<TAttribute> attributes)
where TAttribute : Attribute {
if(serializedProperty == null) {
throw new ArgumentNullException(nameof(serializedProperty));
}
Type targetObjectType = serializedProperty.serializedObject.targetObject.GetType();
if(targetObjectType == null) {
throw new ArgumentException($"Could not find the {nameof(targetObjectType)} of {nameof(serializedProperty)}");
}
attributes = new List<TAttribute>();
foreach(string pathSegment in serializedProperty.propertyPath.Split('.')) {
FieldInfo fieldInfo = targetObjectType.GetRuntimeField(pathSegment);
if(fieldInfo != null) {
IEnumerable<TAttribute> fieldAttributes = fieldInfo.GetCustomAttributes<TAttribute>(inherit);
foreach (TAttribute fieldAttribute in fieldAttributes) {
attributes.Add(fieldAttribute);
}
}
PropertyInfo propertyInfo = targetObjectType.GetRuntimeProperty(pathSegment);
if(propertyInfo != null) {
IEnumerable<TAttribute> fieldAttributes = propertyInfo.GetCustomAttributes<TAttribute>(inherit);
foreach (TAttribute fieldAttribute in fieldAttributes) {
attributes.Add(fieldAttribute);
}
}
}
return attributes.Count > 0;
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 08ca9e3facae5974ca19daee6aa978ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jovian.Utilities;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorToolkit
{
/// <summary>
/// Finds all objects in the project matching <c>filter</c> string and lists alphabetically in a dropdown (if they are <c>assetType</c>)
/// </summary>
public class AssetDropdownAttribute : PropertyAttribute
{
public readonly Type assetType;
public readonly string filter;
public AssetDropdownAttribute(Type assetType, string filter)
{
this.assetType = assetType;
this.filter = filter;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(AssetDropdownAttribute))]
public class AssetDropdownAttributeDrawer : PropertyDrawer
{
private bool isInitialized;
private bool hasValidTarget;
private List<Object> objects;
private string[] values;
private int selectedIndex = -1;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
Debug.LogError("Required property to be an ObjectReference.");
EditorGUI.PropertyField(position, property, label);
return;
}
if (!isInitialized)
{
Initialize();
}
EditorGUI.BeginChangeCheck();
selectedIndex = property.objectReferenceValue != null ? Array.IndexOf(values, property.objectReferenceValue.name) : -1;
selectedIndex = EditorGUI.Popup(position, label.text, selectedIndex, values);
if (Event.current.type == EventType.MouseDown && Event.current.modifiers == EventModifiers.Shift &&
GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition))
{
isInitialized = false;
}
if (EditorGUI.EndChangeCheck())
{
property.objectReferenceValue = objects[selectedIndex];
property.serializedObject.ApplyModifiedProperties();
}
}
private void Initialize()
{
AssetDropdownAttribute assetDropdownAttribute = (AssetDropdownAttribute)attribute;
objects = AssetUtility.FindAllObjectsInProject(assetDropdownAttribute.assetType, assetDropdownAttribute.filter);
objects.Sort(SortByName);
values = objects.Select(obj => obj.name).ToArray();
isInitialized = true;
}
private static int SortByName(Object a, Object b) {
return string.Compare(a.name, b.name, StringComparison.Ordinal);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3b5e8f6d90c14e4ca8c2af794bcf4f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using System;
using UnityEngine;
namespace InspectorToolkit.Internal
{
[AttributeUsage(AttributeTargets.Field)]
public abstract class AttributeBase : PropertyAttribute
{
#if UNITY_EDITOR
/// <summary>
/// Validation is called before all other methods.
/// Once in OnGUI and once in GetPropertyHeight
/// </summary>
public virtual void ValidateProperty(UnityEditor.SerializedProperty property)
{
}
public virtual void OnBeforeGUI(Rect position, UnityEditor.SerializedProperty property, GUIContent label)
{
}
/// <summary>
/// Called once per AttributeBase group.
/// I.e. if something with higher order is drawn, later will be skipped
/// </summary>
/// <returns>false if nothing is drawn</returns>
public virtual bool OnGUI(Rect position, UnityEditor.SerializedProperty property, GUIContent label)
{
return false;
}
public virtual void OnAfterGUI(Rect position, UnityEditor.SerializedProperty property, GUIContent label)
{
}
/// <summary>
/// Overriding occurs just like OnGUI. Once per group, attribute with higher priority first
/// </summary>
/// <returns>Null if not overrided</returns>
public virtual float? OverrideHeight()
{
return null;
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 816fc9d7c08c47f8b84318dce8cecf88
timeCreated: 1582017264

View File

@@ -0,0 +1,62 @@
#if UNITY_EDITOR
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace InspectorToolkit.Internal
{
[CustomPropertyDrawer(typeof(AttributeBase), true)]
public class AttributeBaseDrawer : PropertyDrawer
{
private AttributeBase[] _cachedAttributes;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
CacheAttributes();
for (var i = _cachedAttributes.Length - 1; i >= 0; i--)
{
var overriden = _cachedAttributes[i].OverrideHeight();
if (overriden != null) return overriden.Value;
}
return base.GetPropertyHeight(property, label);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
CacheAttributes();
bool drawn = false;
for (var i = _cachedAttributes.Length - 1; i >= 0; i--)
{
var ab = _cachedAttributes[i];
ab.ValidateProperty(property);
ab.OnBeforeGUI(position, property, label);
// Draw the things with higher priority first. If drawn once - skip drawing
if (!drawn)
{
if (ab.OnGUI(position, property, label)) drawn = true;
}
ab.OnAfterGUI(position, property, label);
}
if (!drawn) EditorGUI.PropertyField(position, property, label);
}
private void CacheAttributes()
{
if (_cachedAttributes.IsNullOrEmpty())
{
_cachedAttributes = fieldInfo
.GetCustomAttributes(typeof(AttributeBase), false)
.OrderBy(s => ((PropertyAttribute) s).order)
.Select(a => a as AttributeBase).ToArray();
}
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ce6a191490f7425a984c36a53cebd36d
timeCreated: 1582017102

View File

@@ -0,0 +1,102 @@
using Jovian.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
#endif
namespace InspectorToolkit {
public enum AutoFindScope {
Self,
SelfAndChildren,
SelfAndParent,
Scene,
Project
}
public class AutoFindAttribute : PropertyAttribute, IActionableAttribute {
public readonly AutoFindScope scope;
public readonly string filter;
public readonly bool isPrefab;
public AutoFindAttribute() : this(string.Empty) { }
public AutoFindAttribute(string filter) : this(AutoFindScope.SelfAndChildren, filter) { }
public AutoFindAttribute(AutoFindScope scope) : this(scope, string.Empty) { }
public AutoFindAttribute(AutoFindScope scope, bool isPrefab) : this(scope, isPrefab, string.Empty) { }
public AutoFindAttribute(AutoFindScope scope, string filter) : this(scope, false, filter) { }
public AutoFindAttribute(AutoFindScope scope, bool isPrefab, string filter) {
this.scope = scope;
this.isPrefab = isPrefab;
this.filter = filter;
}
#if UNITY_EDITOR
public void Trigger(SerializedProperty property) {
Search(property, false);
}
public void Search(SerializedProperty property, bool autoAddIfMissing) {
AutoFindUtil.Search(this, property, autoAddIfMissing);
}
#endif
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(AutoFindAttribute))]
public class AutoFindAttributeDrawer : PropertyDrawer {
private GUIContent searchButtonContent;
private GUIStyle searchButtonStyle;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
if (searchButtonStyle == null || searchButtonContent == null) {
searchButtonStyle = new GUIStyle(GUI.skin.button);
searchButtonStyle.padding = new RectOffset(1, 1, 1, 1);
searchButtonStyle.imagePosition = ImagePosition.ImageOnly;
searchButtonContent =
EditorGUIUtility.IconContent(EditorGUIUtility.isProSkin ? "d_Search Icon" : "Search Icon");
}
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
EditorGUI.BeginChangeCheck();
float height = Mathf.Min(20f, position.height) - 2f;
var buttonRect = new Rect(position);
buttonRect.width = 18f;
buttonRect.height = height;
buttonRect.y += 1f;
buttonRect.x -= buttonRect.width + 2f;
if (GUI.Button(buttonRect, searchButtonContent, searchButtonStyle)) {
if (attribute is AutoFindAttribute autoFindAttribute) {
autoFindAttribute.Search(property, (Event.current.modifiers & EventModifiers.Shift) != 0);
}
}
EditorGUI.PropertyField(position, property, GUIContent.none);
int indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
if (EditorGUI.EndChangeCheck()) {
property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.indentLevel = indentLevel;
EditorGUI.EndProperty();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 51175a247ebc7d24eb3034828da2e103
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using UnityEditor;
using UnityEngine;
namespace InspectorToolkit
{
public class BitmaskAttribute : PropertyAttribute {
public string[] groupNames;
public BitmaskAttribute(params string[] groupNames) {
this.groupNames = groupNames;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
[CustomPropertyDrawer(typeof(BitmaskAttribute))]
public class BitmaskAttributeDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if(property.propertyType != SerializedPropertyType.Integer) {
GUI.Label(position, "Error, SerializedProperty must be 'Integer' type");
return;
}
if(attribute is not BitmaskAttribute bitmaskAttribute) {
return;
}
string[] groups = bitmaskAttribute.groupNames;
if(groups.Length == 0 || groups.Length > 8) {
GUI.Label(position, "Error, bitmask group count not valid");
return;
}
EditorGUI.BeginProperty(position, label, property);
property.intValue = EditorGUI.MaskField(position, label, property.intValue, groups);
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d458f9ebb11d49449bbb0acefc84ea2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,198 @@
// ----------------------------------------------------------------------------
// Author: Kaynn, Yeo Wen Qin
// https://github.com/Kaynn-Cahya
// Date: 26/02/2019
// ----------------------------------------------------------------------------
using System;
using Jovian.Utilities;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorToolkit {
[AttributeUsage(AttributeTargets.Method)]
public class ButtonMethodAttribute : PropertyAttribute {
public readonly ButtonMethodDrawOrder DrawOrder;
public readonly PlayModeVisibility Visibility;
public ButtonMethodAttribute(PlayModeVisibility visibility, ButtonMethodDrawOrder drawOrder = ButtonMethodDrawOrder.AfterInspector) {
Visibility = visibility;
DrawOrder = drawOrder;
}
public ButtonMethodAttribute(ButtonMethodDrawOrder drawOrder = ButtonMethodDrawOrder.AfterInspector) {
DrawOrder = drawOrder;
Visibility = PlayModeVisibility.Always;
}
}
public enum ButtonMethodDrawOrder {
BeforeInspector,
AfterInspector
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
public class ButtonMethodHandler {
public class MethodData {
public MethodInfo methodInfo;
public ParameterData[] parameterDatas;
public string niceName;
public PlayModeVisibility visibility;
public ButtonMethodDrawOrder order;
}
public class ParameterData {
public ParameterInfo parameterInfo;
public string niceName;
public object value;
}
// TODO - replace with MethodDatas;
public readonly List<(MethodInfo Method, string Name, PlayModeVisibility visibility, ButtonMethodDrawOrder order)> TargetMethods;
public int Amount => TargetMethods?.Count ?? 0;
public readonly List<MethodData> MethodDatas;
public bool HasMethods => MethodDatas?.Count > 0;
private readonly Object _target;
public Object Target => _target;
public ButtonMethodHandler(Object target) {
_target = target;
Type type = target.GetType();
BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
IEnumerable<MemberInfo> members = type.GetMembers(bindings).Where(IsButtonMethod);
foreach(MemberInfo member in members) {
MethodInfo method = member as MethodInfo;
if(method == null) continue;
if(IsValidMember(method, member)) {
ButtonMethodAttribute attribute = (ButtonMethodAttribute)Attribute.GetCustomAttribute(method, typeof(ButtonMethodAttribute));
TargetMethods ??= new List<(MethodInfo, string, PlayModeVisibility, ButtonMethodDrawOrder)>();
TargetMethods.Add((method, ObjectNames.NicifyVariableName(method.Name), attribute.Visibility, attribute.DrawOrder));
MethodDatas ??= new();
ParameterInfo[] parameterInfos = method.GetParameters();
ParameterData[] parameterDatas = new ParameterData[parameterInfos.Length];
for(int i = 0; i < parameterInfos.Length; i++) {
ParameterInfo info = parameterInfos[i];
object value;
if(info.HasDefaultValue) {
value = info.DefaultValue;
}
else if(info.ParameterType == typeof(string)) {
value = "String";
}
else if(info.ParameterType == typeof(Object) || info.ParameterType.IsSubclassOf(typeof(Object))) {
value = null;
}
else {
value = Activator.CreateInstance(info.ParameterType);
}
parameterDatas[i] = new ParameterData() {
niceName = ObjectNames.NicifyVariableName(info.Name),
parameterInfo = info,
value = value
};
}
MethodData methodData = new MethodData() {
methodInfo = method,
niceName = ObjectNames.NicifyVariableName(method.Name),
visibility = attribute.Visibility,
order = attribute.DrawOrder,
parameterDatas = parameterDatas
};
MethodDatas.Add(methodData);
}
}
}
public bool HasAnyVisibleMethods() {
if(MethodDatas == null || MethodDatas.Count == 0) return false;
foreach (MethodData methodData in MethodDatas) {
if (methodData.visibility.IsVisible()) {
return true;
}
}
return false;
}
public void OnBeforeInspectorGUI() {
if(MethodDatas == null || MethodDatas.Count == 0) return;
DrawButtonMethods(ButtonMethodDrawOrder.BeforeInspector);
}
private void DrawButtonMethods(ButtonMethodDrawOrder drawOrder) {
foreach(MethodData method in MethodDatas) {
if(method.order != drawOrder) continue;
if(!method.visibility.IsVisible()) continue;
if(method.parameterDatas.Length > 0) {
GUILayout.BeginVertical(EditorStyles.helpBox);
}
if(GUILayout.Button(method.niceName)) {
object result = method.methodInfo.Invoke(_target, method.parameterDatas.Select(p=>p.value).ToArray());
if(result != null) {
Debug.Log($"{method.niceName} => {result}");
}
}
foreach (ParameterData p in method.parameterDatas) {
p.value = InspectorGUIUtility.DrawField(p.niceName, p.parameterInfo.ParameterType, p.value);
}
if(method.parameterDatas.Length > 0) {
GUILayout.EndVertical();
}
}
}
public void OnAfterInspectorGUI() {
if(MethodDatas == null || MethodDatas.Count == 0) return;
DrawButtonMethods(ButtonMethodDrawOrder.AfterInspector);
}
public void Invoke(MethodInfo method) => InvokeMethod(_target, method);
private void InvokeMethod(Object target, MethodInfo method) {
object result = method.Invoke(target, null);
if(result != null) {
string message = $"{result} \nResult of Method '{method.Name}' invocation on object {target.name}";
Debug.Log(message, target);
}
}
private bool IsButtonMethod(MemberInfo memberInfo) {
return Attribute.IsDefined(memberInfo, typeof(ButtonMethodAttribute));
}
private bool IsValidMember(MethodInfo method, MemberInfo member) {
if(method == null) {
Debug.LogWarning(
$"Property <color=brown>{member.Name}</color>.Reason: Member is not a method but has EditorButtonAttribute!");
return false;
}
return true;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e96e2e6d8d1626c4abf4c413bc489af0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,135 @@
using UnityEngine;
namespace InspectorToolkit
{
/// <summary>
/// Validate a string field to only allow or disallow a set of pre-defined
/// characters on typing.
/// </summary>
public class CharactersRangeAttribute : PropertyAttribute
{
public readonly string Characters;
public readonly CharacterRangeMode Mode;
public readonly bool IgnoreCase;
public CharactersRangeAttribute(string characters, CharacterRangeMode mode = CharacterRangeMode.Allow,
bool ignoreCase = true)
{
Characters = characters;
Mode = mode;
IgnoreCase = ignoreCase;
}
}
public enum CharacterRangeMode
{
/// <summary>
/// Only characters in range will be allowed
/// </summary>
Allow,
/// <summary>
/// Characters in range will be removed from the string
/// </summary>
Disallow,
/// <summary>
/// Highlight the field if any of the specified characters were fould
/// </summary>
WarningIfAny,
/// <summary>
/// Highlight the field if some characters in string are not the characters from specified range
/// </summary>
WarningIfNotMatch
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
using EditorTools;
using System.Linq;
[CustomPropertyDrawer(typeof(CharactersRangeAttribute))]
public class CharacterRangeAttributeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.String)
{
MyGUI.DrawColouredRect(position, MyGUI.Colors.Red);
EditorGUI.LabelField(position, new GUIContent("", "[CharacterRangeAttribute] used with non-string property"));
}
else
{
var charactersRange = (CharactersRangeAttribute)attribute;
var mode = charactersRange.Mode;
var ignoreCase = charactersRange.IgnoreCase;
var filter = charactersRange.Characters;
var allow = mode == CharacterRangeMode.Allow || mode == CharacterRangeMode.WarningIfNotMatch;
var warning = mode == CharacterRangeMode.WarningIfAny || mode == CharacterRangeMode.WarningIfNotMatch;
if (ignoreCase) filter = filter.ToUpper();
var filteredCharacters = property.stringValue.Distinct()
.Where(c =>
{
if (ignoreCase) c = char.ToUpper(c);
return filter.Contains(c) ^ allow;
});
bool ifMatch = mode == CharacterRangeMode.WarningIfAny;
bool ifNotMatch = mode == CharacterRangeMode.WarningIfNotMatch;
bool anyFiltered = filteredCharacters.Any();
bool warn = (ifMatch && anyFiltered || ifNotMatch && anyFiltered);
var originalPosition = position;
DrawWarning();
position.width -= 20;
property.stringValue = EditorGUI.TextField(position, label, property.stringValue);
DrawTooltip();
if (!warning)
{
property.stringValue = filteredCharacters.Aggregate(
property.stringValue,
(p, c) => p.Replace(c.ToString(), ""));
}
if (warn)
{
GUILayoutUtility.GetRect(GUIContent.none, EditorStyles.objectField);
position = originalPosition;
position.y += EditorGUIUtility.singleLineHeight;
DrawWarning();
position.x += EditorGUIUtility.labelWidth;
var warningContent = new GUIContent("Containing disallowed characters!");
EditorGUI.LabelField(position, warningContent, EditorStyles.miniBoldLabel);
}
property.serializedObject.ApplyModifiedProperties();
void DrawWarning()
{
if (!warning) return;
if (property.stringValue.Length == 0) warn = false;
MyGUI.DrawColouredRect(position, warn ? MyGUI.Colors.Yellow : Color.clear);
}
void DrawTooltip()
{
string tooltip = "Characters range ";
if (mode == CharacterRangeMode.Allow || mode == CharacterRangeMode.WarningIfNotMatch) tooltip += "is allowed:";
else tooltip += "not allowed:";
tooltip += $"\n[{filter}]";
position.x += position.width + 2;
position.width = 18;
var tooltipContent = new GUIContent(MyGUI.EditorIcons.Help);
tooltipContent.tooltip = tooltip;
EditorGUI.LabelField(position, tooltipContent, EditorStyles.label);
}
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: add63a4947a64f2428030472426ecba4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using System.Text;
using UnityEditor;
#endif
namespace InspectorToolkit {
public class CompactDrawerAttribute : PropertyAttribute { }
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(CompactDrawerAttribute))]
public class CompactDrawerAttributeDrawer : PropertyDrawer {
private static readonly List<SerializedProperty> cacheProperties = new();
private static readonly StringBuilder stringBuilder = new();
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
return EditorGUIUtility.singleLineHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
cacheProperties.Clear();
stringBuilder.Clear();
EditorGUI.BeginProperty(position, label, property);
stringBuilder.Append(" (");
IEnumerator enumerator = property.GetEnumerator();
while (enumerator.MoveNext()) {
SerializedProperty childProperty = (SerializedProperty)enumerator.Current;
cacheProperties.Add(childProperty!.Copy()); // Copy is very important since .Current changes its content while enumerating
stringBuilder.Append(ObjectNames.NicifyVariableName(childProperty!.name));
stringBuilder.Append(", ");
}
if (cacheProperties.Count > 0) {
stringBuilder.Remove(stringBuilder.Length - 2, 2);
stringBuilder.Append(")");
}
else {
stringBuilder.Clear();
}
GUIContent newLabel = new($"{label}{stringBuilder}", label.tooltip);
Rect labelRect = new(position) { height = EditorGUIUtility.singleLineHeight };
Rect fieldRect = EditorGUI.PrefixLabel(labelRect, newLabel);
fieldRect.height = EditorGUIUtility.singleLineHeight;
int childCount = cacheProperties.Count;
int childIndex = 0;
float spacing = EditorGUIUtility.standardVerticalSpacing;
float fullWidth = fieldRect.width / childCount;
float widthMinusSpacing = fullWidth - spacing * (childCount - 1) / childCount;
int indentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
for (int i = 0, c = cacheProperties.Count; i < c; i++) {
SerializedProperty childProperty = cacheProperties[i];
Rect childFieldRect = new(fieldRect) {
x = Mathf.CeilToInt(fieldRect.x + fullWidth * childIndex),
width = widthMinusSpacing
};
EditorGUI.PropertyField(childFieldRect, childProperty, GUIContent.none);
childIndex++;
}
EditorGUI.indentLevel = indentLevel;
EditorGUI.EndProperty();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d56876fd24830a4c99f6963c8f15435
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,70 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace InspectorToolkit {
public class CompactVector4Attribute : PropertyAttribute {
public CompactVector4Attribute() {
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(CompactVector4Attribute))]
public class CompactVector4AttributeDrawer : PropertyDrawer {
private const int CHILD_COUNT = 4;
private const float LABEL_WIDTH = 13f;
private const float INDENT_WIDTH = 15f;
private const float CHILD_HORIZONTAL_SPACING = 4f;
private static readonly string[] childPropertyNames = new string[] { "x", "y", "z", "w" };
private bool IsCompactLayout => Screen.width <= 333f;
private bool IsNoLabelLayout => Screen.width <= 240f;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
if(IsCompactLayout) { // Unity Vector2 + Vector3 go multi-line at this point
return EditorGUIUtility.singleLineHeight * 2f + EditorGUIUtility.standardVerticalSpacing;
}
return EditorGUIUtility.singleLineHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
if(property.propertyType != SerializedPropertyType.Vector4) {
GUI.Label(position, "Error, SerializedProperty must be 'Vector4' type");
return;
}
EditorGUI.BeginProperty(position, label, property);
if(IsCompactLayout) {
EditorGUIUtility.labelWidth = position.width;
}
Rect labelRect = new Rect(position) { height = EditorGUIUtility.singleLineHeight };
Rect fieldRect = EditorGUI.PrefixLabel(labelRect, label);
fieldRect.height = EditorGUIUtility.singleLineHeight;
if(IsCompactLayout) {
fieldRect.y = labelRect.yMax + EditorGUIUtility.standardVerticalSpacing;
fieldRect.x = labelRect.x + (IsNoLabelLayout ? 0f : INDENT_WIDTH); // 15 = single indent
fieldRect.width = position.xMax - fieldRect.xMin;
}
EditorGUIUtility.labelWidth = IsNoLabelLayout ? 0.01f : LABEL_WIDTH;
float childSpacing = IsNoLabelLayout ? CHILD_HORIZONTAL_SPACING / 2f : CHILD_HORIZONTAL_SPACING;
float childWidth = ((fieldRect.width + childSpacing) / CHILD_COUNT) - childSpacing;
for(int i = 0; i < CHILD_COUNT; i++) {
SerializedProperty childProperty = property.FindPropertyRelative(childPropertyNames[i]);
Rect childRect = new Rect(fieldRect);
childRect.width = childWidth; // add spacing
childRect.x += i * (childWidth + childSpacing);
EditorGUI.PropertyField(childRect, childProperty);
}
EditorGUI.EndProperty();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bc29630bf159f4742a4ea87bb9eaccdc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,322 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
namespace InspectorToolkit {
/// <summary>
/// Conditionally Show/Hide field in inspector, based on some other field value
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class ConditionalFieldAttribute : PropertyAttribute {
public readonly string FieldToCheck;
public readonly string[] CompareValues;
public readonly bool Inverse;
/// <param name="fieldToCheck">String name of field to check value</param>
/// <param name="inverse">Inverse check result</param>
/// <param name="compareValues">On which values field will be shown in inspector</param>
public ConditionalFieldAttribute(string fieldToCheck, bool inverse = false, params object[] compareValues) {
FieldToCheck = fieldToCheck;
Inverse = inverse;
CompareValues = compareValues.Select(c => c.ToString().ToUpper()).ToArray();
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
using EditorTools;
using UnityEditor;
[CustomPropertyDrawer(typeof(ConditionalFieldAttribute))]
public class ConditionalFieldAttributeDrawer : PropertyDrawer {
private bool _toShow = true;
/// <summary>
/// Key is Associated with drawer type (the T in [CustomPropertyDrawer(typeof(T))])
/// Value is PropertyDrawer Type
/// </summary>
private static Dictionary<Type, Type> _allPropertyDrawersInDomain;
private bool _initialized;
private PropertyDrawer _customAttributeDrawer;
private PropertyDrawer _customTypeDrawer;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
if(!(attribute is ConditionalFieldAttribute conditional)) return 0;
Initialize(property);
var propertyToCheck = ConditionalFieldUtility.FindRelativeProperty(property, conditional.FieldToCheck);
_toShow = ConditionalFieldUtility.PropertyIsVisible(propertyToCheck, conditional.Inverse, conditional.CompareValues);
if(!_toShow) return 0;
if(_customAttributeDrawer != null) return _customAttributeDrawer.GetPropertyHeight(property, label);
if(_customTypeDrawer != null) return _customTypeDrawer.GetPropertyHeight(property, label);
return EditorGUI.GetPropertyHeight(property);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
if(!_toShow) return;
if(_customAttributeDrawer != null) TryUseAttributeDrawer();
else if(_customTypeDrawer != null) TryUseTypeDrawer();
else EditorGUI.PropertyField(position, property, label, true);
void TryUseAttributeDrawer() {
try {
_customAttributeDrawer.OnGUI(position, property, label);
}
catch(Exception e) {
EditorGUI.PropertyField(position, property, label);
LogWarning("Unable to use Custom Attribute Drawer " + _customAttributeDrawer.GetType() + " : " + e, property);
}
}
void TryUseTypeDrawer() {
try {
_customTypeDrawer.OnGUI(position, property, label);
}
catch(Exception e) {
EditorGUI.PropertyField(position, property, label);
LogWarning("Unable to instantiate " + fieldInfo.FieldType + " : " + e, property);
}
}
}
private void Initialize(SerializedProperty property) {
if(_initialized) return;
CacheAllDrawersInDomain();
TryGetCustomAttributeDrawer();
TryGetCustomTypeDrawer();
_initialized = true;
void CacheAllDrawersInDomain() {
if(!_allPropertyDrawersInDomain.IsNullOrEmpty()) return;
_allPropertyDrawersInDomain = new Dictionary<Type, Type>();
var propertyDrawerType = typeof(PropertyDrawer);
var allDrawerTypesInDomain = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(x => x.GetTypes())
.Where(t => propertyDrawerType.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
foreach(var type in allDrawerTypesInDomain) {
var drawerAttribute = CustomAttributeData.GetCustomAttributes(type).FirstOrDefault();
if(drawerAttribute == null) continue;
var associatedType = drawerAttribute.ConstructorArguments.FirstOrDefault().Value as Type;
if(associatedType == null) continue;
if(_allPropertyDrawersInDomain.ContainsKey(associatedType)) continue;
_allPropertyDrawersInDomain.Add(associatedType, type);
}
}
void TryGetCustomAttributeDrawer() {
if(fieldInfo == null) return;
//Get the second attribute flag
var secondAttribute = (PropertyAttribute)fieldInfo.GetCustomAttributes(typeof(PropertyAttribute), false)
.FirstOrDefault(a => !(a is ConditionalFieldAttribute));
if(secondAttribute == null) return;
var genericAttributeType = secondAttribute.GetType();
//Get the associated attribute drawer
if(!_allPropertyDrawersInDomain.ContainsKey(genericAttributeType)) return;
var customAttributeDrawerType = _allPropertyDrawersInDomain[genericAttributeType];
var customAttributeData = fieldInfo.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType == secondAttribute.GetType());
if(customAttributeData == null) return;
//Create drawer for custom attribute
try {
_customAttributeDrawer = (PropertyDrawer)Activator.CreateInstance(customAttributeDrawerType);
var attributeField = customAttributeDrawerType.GetField("m_Attribute", BindingFlags.Instance | BindingFlags.NonPublic);
if(attributeField != null) attributeField.SetValue(_customAttributeDrawer, secondAttribute);
}
catch(Exception e) {
LogWarning("Unable to construct drawer for " + secondAttribute.GetType() + " : " + e, property);
}
}
void TryGetCustomTypeDrawer() {
if(fieldInfo == null) return;
// Skip checks for mscorlib.dll
if(fieldInfo.FieldType.Module.ScopeName.Equals(typeof(int).Module.ScopeName)) return;
// Of all property drawers in the assembly we need to find one that affects target type
// or one of the base types of target type
Type fieldDrawerType = null;
Type fieldType = fieldInfo.FieldType;
while(fieldType != null) {
if(_allPropertyDrawersInDomain.ContainsKey(fieldType)) {
fieldDrawerType = _allPropertyDrawersInDomain[fieldType];
break;
}
fieldType = fieldType.BaseType;
}
if(fieldDrawerType == null) return;
//Create instances of each (including the arguments)
try {
_customTypeDrawer = (PropertyDrawer)Activator.CreateInstance(fieldDrawerType);
}
catch(Exception e) {
LogWarning("No constructor available in " + fieldType + " : " + e, property);
return;
}
//Reassign the attribute field in the drawer so it can access the argument values
var attributeField = fieldDrawerType.GetField("m_Attribute", BindingFlags.Instance | BindingFlags.NonPublic);
if(attributeField != null) attributeField.SetValue(_customTypeDrawer, attribute);
var fieldInfoField = fieldDrawerType.GetField("m_FieldInfo", BindingFlags.Instance | BindingFlags.NonPublic);
if(fieldInfoField != null) fieldInfoField.SetValue(_customTypeDrawer, fieldInfo);
}
}
private void LogWarning(string log, SerializedProperty property) {
var warning = "Property <color=brown>" + fieldInfo.Name + "</color>";
if(fieldInfo != null && fieldInfo.DeclaringType != null)
warning += " on behaviour <color=brown>" + fieldInfo.DeclaringType.Name + "</color>";
warning += " caused: " + log;
Debug.LogWarning(warning, property.serializedObject.targetObject);
}
}
public static class ConditionalFieldUtility {
#region Property Is Visible
public static bool PropertyIsVisible(SerializedProperty property, bool inverse, string[] compareAgainst) {
if(property == null) return true;
string asString = property.AsStringValue().ToUpper();
if(compareAgainst != null && compareAgainst.Length > 0) {
var matchAny = CompareAgainstValues(asString, compareAgainst, IsFlagsEnum());
if(inverse) matchAny = !matchAny;
return matchAny;
}
bool someValueAssigned = asString != "FALSE" && asString != "0" && asString != "NULL";
if(someValueAssigned) return !inverse;
return inverse;
bool IsFlagsEnum() {
if(property.propertyType != SerializedPropertyType.Enum) return false;
var value = property.GetValue();
if(value == null) return false;
return value.GetType().GetCustomAttribute<FlagsAttribute>() != null;
}
}
/// <summary>
/// True if the property value matches any of the values in '_compareValues'
/// </summary>
private static bool CompareAgainstValues(string propertyValueAsString, string[] compareAgainst, bool handleFlags) {
if(!handleFlags) return ValueMatches(propertyValueAsString);
var separateFlags = propertyValueAsString.Split(',');
foreach(var flag in separateFlags) {
if(ValueMatches(flag.Trim())) return true;
}
return false;
bool ValueMatches(string value) {
foreach(var compare in compareAgainst) if(value == compare) return true;
return false;
}
}
#endregion
#region Find Relative Property
public static SerializedProperty FindRelativeProperty(SerializedProperty property, string propertyName) {
if(property.depth == 0) return property.serializedObject.FindProperty(propertyName);
var path = property.propertyPath.Replace(".Array.data[", "[");
var elements = path.Split('.');
var nestedProperty = NestedPropertyOrigin(property, elements);
// if nested property is null = we hit an array property
if(nestedProperty == null) {
var cleanPath = path.Substring(0, path.IndexOf('['));
var arrayProp = property.serializedObject.FindProperty(cleanPath);
var target = arrayProp.serializedObject.targetObject;
var who = "Property <color=brown>" + arrayProp.name + "</color> in object <color=brown>" + target.name + "</color> caused: ";
var warning = who + "Array fields is not supported by [ConditionalFieldAttribute]. Consider to use <color=blue>CollectionWrapper</color>";
Debug.LogWarning(warning, target);
return null;
}
return nestedProperty.FindPropertyRelative(propertyName);
}
// For [Serialized] types with [Conditional] fields
private static SerializedProperty NestedPropertyOrigin(SerializedProperty property, string[] elements) {
SerializedProperty parent = null;
for(int i = 0; i < elements.Length - 1; i++) {
var element = elements[i];
int index = -1;
if(element.Contains("[")) {
index = Convert.ToInt32(element.Substring(element.IndexOf("[", StringComparison.Ordinal))
.Replace("[", "").Replace("]", ""));
element = element.Substring(0, element.IndexOf("[", StringComparison.Ordinal));
}
parent = i == 0
? property.serializedObject.FindProperty(element)
: parent != null
? parent.FindPropertyRelative(element)
: null;
if(index >= 0 && parent != null) parent = parent.GetArrayElementAtIndex(index);
}
return parent;
}
#endregion
#region Behaviour Property Is Visible
public static bool BehaviourPropertyIsVisible(UnityEngine.Object obj, string propertyName, ConditionalFieldAttribute appliedAttribute) {
if(string.IsNullOrEmpty(appliedAttribute.FieldToCheck)) return true;
var so = new SerializedObject(obj);
var property = so.FindProperty(propertyName);
var targetProperty = FindRelativeProperty(property, appliedAttribute.FieldToCheck);
return PropertyIsVisible(targetProperty, appliedAttribute.Inverse, appliedAttribute.CompareValues);
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f0c8c14a34cc7b4c8ee94ff351a2902
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace InspectorToolkit
{
public class ConstantsSelectionAttribute : PropertyAttribute
{
public readonly Type SelectFromType;
public ConstantsSelectionAttribute(Type type)
{
SelectFromType = type;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
using EditorTools;
[CustomPropertyDrawer(typeof(ConstantsSelectionAttribute))]
public class ConstantsSelectionAttributeDrawer : PropertyDrawer
{
private ConstantsSelectionAttribute _attribute;
private readonly List<MemberInfo> _constants = new List<MemberInfo>();
private string[] _names;
private object[] _values;
private Type _targetType;
private int _selectedValueIndex;
private bool _valueFound;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (_attribute == null) Initialize(property);
if (_values.IsNullOrEmpty() || _selectedValueIndex < 0)
{
EditorGUI.PropertyField(position, property, label);
return;
}
if (!_valueFound && _selectedValueIndex == 0) MyGUI.DrawColouredRect(position, MyGUI.Colors.Yellow);
EditorGUI.BeginChangeCheck();
_selectedValueIndex = EditorGUI.Popup(position, label.text, _selectedValueIndex, _names);
if (EditorGUI.EndChangeCheck())
{
fieldInfo.SetValue(property.serializedObject.targetObject, _values[_selectedValueIndex]);
property.serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(property.serializedObject.targetObject);
}
}
private object GetValue(SerializedProperty property)
{
return fieldInfo.GetValue(property.serializedObject.targetObject);
}
private void Initialize(SerializedProperty property)
{
_attribute = (ConstantsSelectionAttribute) attribute;
_targetType = fieldInfo.FieldType;
var searchFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;
var allPublicStaticFields = _attribute.SelectFromType.GetFields(searchFlags);
var allPublicStaticProperties = _attribute.SelectFromType.GetProperties(searchFlags);
// IsLiteral determines if its value is written at compile time and not changeable
// IsInitOnly determines if the field can be set in the body of the constructor
// for C# a field which is readonly keyword would have both true but a const field would have only IsLiteral equal to true
foreach (FieldInfo field in allPublicStaticFields)
{
if ((field.IsInitOnly || field.IsLiteral) && field.FieldType == _targetType)
_constants.Add(field);
}
foreach (var prop in allPublicStaticProperties)
{
if (prop.PropertyType == _targetType) _constants.Add(prop);
}
if (_constants.IsNullOrEmpty()) return;
_names = new string[_constants.Count];
_values = new object[_constants.Count];
for (var i = 0; i < _constants.Count; i++)
{
_names[i] = _constants[i].Name;
_values[i] = GetValue(i);
}
var currentValue = GetValue(property);
if (currentValue != null)
{
for (var i = 0; i < _values.Length; i++)
{
if (currentValue.Equals(_values[i]))
{
_valueFound = true;
_selectedValueIndex = i;
}
}
}
if (!_valueFound)
{
_names = _names.InsertAt(0);
_values = _values.InsertAt(0);
var actualValue = GetValue(property);
var value = actualValue != null ? actualValue : "NULL";
_names[0] = "NOT FOUND: " + value;
_values[0] = actualValue;
}
}
private object GetValue(int index)
{
var member = _constants[index];
if (member.MemberType == MemberTypes.Field) return ((FieldInfo) member).GetValue(null);
if (member.MemberType == MemberTypes.Property) return ((PropertyInfo) member).GetValue(null);
return null;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d93e4517d0ec49138d37edab20cacd2f
timeCreated: 1582028131

View File

@@ -0,0 +1,85 @@
using System;
using System.Linq;
using UnityEngine;
namespace InspectorToolkit
{
/// <summary>
/// Create Popup with predefined values for string, int or float property
/// </summary>
public class DefinedValuesAttribute : PropertyAttribute
{
public readonly object[] ValuesArray;
public DefinedValuesAttribute(params object[] definedValues)
{
ValuesArray = definedValues;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(DefinedValuesAttribute))]
public class DefinedValuesAttributeDrawer : PropertyDrawer
{
private string[] _values;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var values = ((DefinedValuesAttribute) attribute).ValuesArray;
if (values.IsNullOrEmpty() || !TypeMatch(values[0].GetType(), property))
{
EditorGUI.PropertyField(position, property, label);
return;
}
if (_values.IsNullOrEmpty()) _values = values.Select(v => v.ToString()).ToArray();
var valType = values[0].GetType();
bool isString = valType == typeof(string);
bool isInt = valType == typeof(int);
bool isFloat = valType == typeof(float);
EditorGUI.BeginChangeCheck();
var newIndex = EditorGUI.Popup(position, label.text, GetSelectedIndex(), _values);
if (EditorGUI.EndChangeCheck()) ApplyNewValue(_values[newIndex]);
int GetSelectedIndex()
{
for (var i = 0; i < _values.Length; i++)
{
if (isString && property.stringValue == _values[i]) return i;
if (isInt && property.intValue == Convert.ToInt32(_values[i])) return i;
if (isFloat && Mathf.Approximately(property.floatValue, Convert.ToSingle(_values[i]))) return i;
}
return 0;
}
void ApplyNewValue(string newValue)
{
if (isString) property.stringValue = newValue;
if (isInt) property.intValue = Convert.ToInt32(newValue);
if (isFloat) property.floatValue = Convert.ToSingle(newValue);
property.serializedObject.ApplyModifiedProperties();
}
}
private bool TypeMatch(Type valType, SerializedProperty property)
{
if (valType == typeof(string) && property.propertyType == SerializedPropertyType.String) return true;
if (valType == typeof(int) && property.propertyType == SerializedPropertyType.Integer) return true;
if (valType == typeof(float) && property.propertyType == SerializedPropertyType.Float) return true;
return false;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bb25c9dc1c7ff1345ac286ba31c0af36
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,153 @@
using System.Collections.Generic;
using UnityEngine;
namespace InspectorToolkit
{
/// <summary>
/// Use to display inspector of property object
/// </summary>
public class DisplayInspectorAttribute : PropertyAttribute
{
public readonly bool DisplayScript;
public DisplayInspectorAttribute(bool displayScriptField = true)
{
DisplayScript = displayScriptField;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using EditorTools;
using UnityEditor;
[CustomPropertyDrawer(typeof(DisplayInspectorAttribute))]
public class DisplayInspectorAttributeDrawer : PropertyDrawer
{
private ButtonMethodHandler _buttonMethods;
private readonly Dictionary<Object, SerializedObject> _targets = new Dictionary<Object, SerializedObject>();
private SerializedObject GetTargetSO(Object targetObject)
{
SerializedObject target;
if (_targets.ContainsKey(targetObject)) target = _targets[targetObject];
else
{
_targets.Add(targetObject, new SerializedObject(targetObject));
target = _targets[targetObject];
}
target.Update();
return target;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
bool notValidType = property.propertyType != SerializedPropertyType.ObjectReference;
if (notValidType)
{
EditorGUI.LabelField(position, label.text, "Use [DisplayInspector] with MB or SO");
return;
}
if (((DisplayInspectorAttribute)attribute).DisplayScript || property.objectReferenceValue == null)
{
position.height = EditorGUI.GetPropertyHeight(property);
EditorGUI.PropertyField(position, property, label);
position.y += EditorGUI.GetPropertyHeight(property) + 4;
if (GUI.changed) property.serializedObject.ApplyModifiedProperties();
}
if (property.objectReferenceValue == null) return;
if (_buttonMethods == null) _buttonMethods = new ButtonMethodHandler(property.objectReferenceValue);
var startY = position.y - 2;
float startX = position.x;
var target = GetTargetSO(property.objectReferenceValue);
var propertyObject = target.GetIterator();
propertyObject.Next(true);
propertyObject.NextVisible(true);
var xPos = position.x + 10;
var width = position.width - 10;
bool expandedReorderable = false;
while (propertyObject.NextVisible(propertyObject.isExpanded && !expandedReorderable))
{
#if UNITY_2020_2_OR_NEWER
expandedReorderable = propertyObject.isExpanded && propertyObject.isArray &&
!propertyObject.IsAttributeDefined<NonReorderableAttribute>();
#endif
position.x = xPos + 10 * propertyObject.depth;
position.width = width - 10 * propertyObject.depth;
position.height = EditorGUI.GetPropertyHeight(propertyObject, expandedReorderable);
EditorGUI.PropertyField(position, propertyObject, expandedReorderable);
position.y += position.height + 4;
}
if (!_buttonMethods.TargetMethods.IsNullOrEmpty())
{
foreach (var method in _buttonMethods.TargetMethods)
{
position.height = EditorGUIUtility.singleLineHeight;
if (GUI.Button(position, method.Name)) _buttonMethods.Invoke(method.Method);
position.y += position.height;
}
}
var bgRect = position;
bgRect.y = startY - 5;
bgRect.x = startX - 10;
bgRect.width = 10;
bgRect.height = position.y - startY;
if (_buttonMethods.Amount > 0) bgRect.height += 5;
DrawColouredRect(bgRect, new Color(.6f, .6f, .8f, .5f));
target.ApplyModifiedProperties();
property.serializedObject.ApplyModifiedProperties();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
bool notValidType = property.propertyType != SerializedPropertyType.ObjectReference;
if (notValidType || property.objectReferenceValue == null) return base.GetPropertyHeight(property, label);
if (_buttonMethods == null) _buttonMethods = new ButtonMethodHandler(property.objectReferenceValue);
float height = ((DisplayInspectorAttribute)attribute).DisplayScript ? EditorGUI.GetPropertyHeight(property) + 4 : 0;
var target = GetTargetSO(property.objectReferenceValue);
var propertyObject = target.GetIterator();
propertyObject.Next(true);
propertyObject.NextVisible(true);
bool expandedReorderable = false;
while (propertyObject.NextVisible(propertyObject.isExpanded && !expandedReorderable))
{
#if UNITY_2020_2_OR_NEWER
expandedReorderable = propertyObject.isExpanded && propertyObject.isArray &&
!propertyObject.IsAttributeDefined<NonReorderableAttribute>();
#endif
height += EditorGUI.GetPropertyHeight(propertyObject, expandedReorderable) + 4;
}
if (_buttonMethods.Amount > 0) height += 4 + _buttonMethods.Amount * EditorGUIUtility.singleLineHeight;
return height;
}
private void DrawColouredRect(Rect rect, Color color)
{
var defaultBackgroundColor = GUI.backgroundColor;
GUI.backgroundColor = color;
GUI.Box(rect, "");
GUI.backgroundColor = defaultBackgroundColor;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0a699aecc941574e9faf6a546c0e663
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,329 @@
// ----------------------------------------------------------------------------
// Author: Dimitry, PixeyeHQ
// Project : UNITY FOLDOUT
// https://github.com/PixeyeHQ/InspectorFoldoutGroup
// Contacts : Pix - ask@pixeye.games
// Website : http://www.pixeye.games
// ----------------------------------------------------------------------------
using UnityEngine;
namespace InspectorToolkit
{
public class FoldoutAttribute : PropertyAttribute
{
public readonly string Name;
public readonly bool FoldEverything;
/// <summary>Adds the property to the specified foldout group.</summary>
/// <param name="name">Name of the foldout group.</param>
/// <param name="foldEverything">Toggle to put all properties to the specified group</param>
public FoldoutAttribute(string name, bool foldEverything = false)
{
FoldEverything = foldEverything;
Name = name;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
public class FoldoutAttributeHandler
{
private readonly Dictionary<string, CacheFoldProp> _cacheFoldouts = new Dictionary<string, CacheFoldProp>();
private readonly List<SerializedProperty> _props = new List<SerializedProperty>();
private bool _initialized;
private readonly IGUILayoutPropertyField guiLayoutPropertyField;
private readonly UnityEngine.Object _target;
private readonly SerializedObject _serializedObject;
public bool OverrideInspector => _props.Count > 0;
public FoldoutAttributeHandler(UnityEngine.Object target, SerializedObject serializedObject, IGUILayoutPropertyField guiLayoutPropertyField)
{
_target = target;
_serializedObject = serializedObject;
this.guiLayoutPropertyField = guiLayoutPropertyField;
}
public void OnDisable()
{
if (_target == null) return;
foreach (var c in _cacheFoldouts)
{
EditorPrefs.SetBool(string.Format($"{c.Value.Attribute.Name}{c.Value.Properties[0].name}{_target.name}"), c.Value.Expanded);
c.Value.Dispose();
}
}
public void Update()
{
_serializedObject.Update();
Setup();
}
public void OnInspectorGUI()
{
Header();
Body();
_serializedObject.ApplyModifiedProperties();
}
private void Header()
{
using (new EditorGUI.DisabledScope("m_Script" == _props[0].propertyPath))
{
//EditorGUILayout.Space();
EditorGUILayout.PropertyField(_props[0], true);
//EditorGUILayout.Space();
}
}
private void Body()
{
foreach (var pair in _cacheFoldouts)
{
EditorGUILayout.BeginVertical(StyleFramework.Box);
Foldout(pair.Value);
EditorGUILayout.EndVertical();
EditorGUI.indentLevel = 0;
}
for (var i = 1; i < _props.Count; i++) {
SerializedProperty prop = _props[i];
guiLayoutPropertyField.GUILayoutPropertyField(prop);
}
}
private void Foldout(CacheFoldProp cache)
{
cache.Expanded = EditorGUILayout.Foldout(cache.Expanded, cache.Attribute.Name, true, StyleFramework.FoldoutHeader);
var rect = GUILayoutUtility.GetLastRect();
rect.x -= 18;
rect.y -= 4;
rect.height += 8;
rect.width += 18;
EditorGUI.LabelField(rect, GUIContent.none, EditorStyles.helpBox);
if (cache.Expanded)
{
EditorGUILayout.Space(2);
foreach (SerializedProperty property in cache.Properties)
{
EditorGUILayout.BeginVertical(StyleFramework.BoxChild);
guiLayoutPropertyField.GUILayoutPropertyField(
property,
new GUIContent(ObjectNames.NicifyVariableName(property.name), property.tooltip),
true);
EditorGUILayout.EndVertical();
}
}
}
private void Setup()
{
if (_initialized) return;
FoldoutAttribute prevFold = default;
var length = EditorTypes.GetFields(_target, out var objectFields);
for (var i = 0; i < length; i++)
{
#region FOLDERS
var fold = Attribute.GetCustomAttribute(objectFields[i], typeof(FoldoutAttribute)) as FoldoutAttribute;
CacheFoldProp c;
if (fold == null)
{
if (prevFold != null && prevFold.FoldEverything)
{
if (!_cacheFoldouts.TryGetValue(prevFold.Name, out c))
{
_cacheFoldouts.Add(prevFold.Name,
new CacheFoldProp {Attribute = prevFold, Types = new HashSet<string> {objectFields[i].Name}});
}
else
{
c.Types.Add(objectFields[i].Name);
}
}
continue;
}
prevFold = fold;
if (!_cacheFoldouts.TryGetValue(fold.Name, out c))
{
var expanded = EditorPrefs.GetBool(string.Format($"{fold.Name}{objectFields[i].Name}{_target.name}"), false);
_cacheFoldouts.Add(fold.Name,
new CacheFoldProp {Attribute = fold, Types = new HashSet<string> {objectFields[i].Name}, Expanded = expanded});
}
else c.Types.Add(objectFields[i].Name);
#endregion
}
var property = _serializedObject.GetIterator();
var next = property.NextVisible(true);
if (next)
{
do
{
HandleFoldProp(property);
} while (property.NextVisible(false));
}
_initialized = true;
}
private void HandleFoldProp(SerializedProperty prop)
{
bool shouldBeFolded = false;
foreach (var pair in _cacheFoldouts)
{
if (pair.Value.Types.Contains(prop.name))
{
var pr = prop.Copy();
shouldBeFolded = true;
pair.Value.Properties.Add(pr);
break;
}
}
if (shouldBeFolded == false)
{
var pr = prop.Copy();
_props.Add(pr);
}
}
private class CacheFoldProp
{
public HashSet<string> Types = new HashSet<string>();
public readonly List<SerializedProperty> Properties = new List<SerializedProperty>();
public FoldoutAttribute Attribute;
public bool Expanded;
public void Dispose()
{
Properties.Clear();
Types.Clear();
Attribute = null;
}
}
}
static class StyleFramework
{
public static readonly GUIStyle Box;
public static readonly GUIStyle BoxChild;
public static readonly GUIStyle FoldoutHeader;
static StyleFramework()
{
FoldoutHeader = new GUIStyle(EditorStyles.foldout);
FoldoutHeader.overflow = new RectOffset(-10, 0, 3, 0);
FoldoutHeader.padding = new RectOffset(20, 0, 0, 0);
FoldoutHeader.margin = new RectOffset(0, 0, 0, 2);
FoldoutHeader.border = new RectOffset(2, 2, 2, 2);
Box = new GUIStyle(GUI.skin.box);
Box.padding = new RectOffset(18, 0, 4, 0);
BoxChild = new GUIStyle(GUI.skin.box);
BoxChild.padding = new RectOffset(0, 0, 0, 0);
BoxChild.margin = new RectOffset(0, 0, 0, 0);
}
}
static class EditorTypes
{
private static readonly Dictionary<int, List<FieldInfo>> Fields = new Dictionary<int, List<FieldInfo>>(FastComparable.Default);
private static readonly Dictionary<int, List<PropertyInfo>> Properties = new Dictionary<int, List<PropertyInfo>>(FastComparable.Default);
public static int GetFields(Object target, out List<FieldInfo> objectFields)
{
var t = target.GetType();
var hash = t.GetHashCode();
if (!Fields.TryGetValue(hash, out objectFields))
{
var typeTree = GetTypeTree(t);
objectFields = target.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.NonPublic)
.OrderByDescending(x => typeTree.IndexOf(x.DeclaringType))
.ToList();
Fields.Add(hash, objectFields);
}
return objectFields.Count;
}
public static int GetProperties(Object target, out List<PropertyInfo> objectProperties) {
var t = target.GetType();
var hash = t.GetHashCode();
if(!Properties.TryGetValue(hash, out objectProperties)) {
var typeTree = GetTypeTree(t);
objectProperties = target.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.NonPublic)
.OrderByDescending(x => typeTree.IndexOf(x.DeclaringType))
.ToList();
Properties.Add(hash, objectProperties);
}
return objectProperties.Count;
}
static IList<Type> GetTypeTree(Type t)
{
var types = new List<Type>();
while (t.BaseType != null)
{
types.Add(t);
t = t.BaseType;
}
return types;
}
}
internal class FastComparable : IEqualityComparer<int>
{
public static readonly FastComparable Default = new FastComparable();
public bool Equals(int x, int y)
{
return x == y;
}
public int GetHashCode(int obj)
{
return obj.GetHashCode();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 69f6d9b4f0e9f594dbcc767598fe91c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
using UnityEngine;
namespace InspectorToolkit {
public class InfoAttribute : PropertyAttribute {
public enum MessageType {
Info,
Warning,
Error
}
#if UNITY_EDITOR
public readonly UnityEditor.MessageType messageType;
public readonly string message;
#endif
public InfoAttribute(string message, MessageType messageType = MessageType.Info) {
#if UNITY_EDITOR
this.message = message;
switch(messageType) {
case MessageType.Error:
this.messageType = UnityEditor.MessageType.Error;
break;
case MessageType.Warning:
this.messageType = UnityEditor.MessageType.Warning;
break;
case MessageType.Info:
default:
this.messageType = UnityEditor.MessageType.Info;
break;
}
#endif
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
using UnityEditor;
[CustomPropertyDrawer(typeof(InfoAttribute))]
public class InfoAttributeDrawer : PropertyDrawer {
private const float MESSAGEBOX_HEIGHT = 40f;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
return base.GetPropertyHeight(property, label) + MESSAGEBOX_HEIGHT;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
var message = ((InfoAttribute)attribute).message;
var messageType = ((InfoAttribute)attribute).messageType;
var boxPosition = new Rect(position.x, position.y, position.width, MESSAGEBOX_HEIGHT);
EditorGUI.HelpBox(boxPosition, message, messageType);
var fieldPosition = new Rect(position.x, position.y + MESSAGEBOX_HEIGHT, position.width, 18f);
EditorGUI.PropertyField(fieldPosition, property, label);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 621eca357dcb59841acf613ba4b7dee5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using UnityEngine;
namespace InspectorToolkit
{
/// <summary>
/// Field will be Read-Only in Playmode
/// </summary>
public class InitializationFieldAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(InitializationFieldAttribute))]
public class InitializationFieldAttributeDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, true);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (Application.isPlaying) GUI.enabled = false;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dd5ef49202d6492bb39be27d203b0163
timeCreated: 1588577864

View File

@@ -0,0 +1,93 @@
using System;
using UnityEngine;
namespace InspectorToolkit {
public class InterfaceComponentAttribute : PropertyAttribute {
public readonly Type interfaceType;
public readonly bool allowMismatch;
public InterfaceComponentAttribute(Type interfaceType, bool allowMismatch = false) {
this.interfaceType = interfaceType;
this.allowMismatch = allowMismatch;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
using UnityEditor;
[CustomPropertyDrawer(typeof(InterfaceComponentAttribute))]
public class InterfaceComponentAttributeDrawer : PropertyDrawer {
private const float MESSAGE_HEIGHT = 40f;
private const float FIELD_HEIGHT = 18f;
private enum ValueImplementation {
Null,
Match,
Mismatch
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
ValueImplementation valueImplementation = GetValueInterfaceImplementation(property, ((InterfaceComponentAttribute)attribute).interfaceType);
return base.GetPropertyHeight(property, label) + (valueImplementation != ValueImplementation.Match ? MESSAGE_HEIGHT : 0f);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
InterfaceComponentAttribute interfaceAttribute = (InterfaceComponentAttribute)attribute;
Type interfaceType = interfaceAttribute.interfaceType;
bool allowMismatch = interfaceAttribute.allowMismatch;
UnityEngine.Object value = property.objectReferenceValue;
if(value is GameObject gameObject) {
value = gameObject.GetComponent(interfaceType) as Component;
if(value != null) {
property.objectReferenceValue = value;
property.serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
}
else if(value is Transform transform) {
value = transform.GetComponent(interfaceType) as Component;
if(value != null) {
property.objectReferenceValue = value;
property.serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
}
ValueImplementation valueImplementation = GetValueInterfaceImplementation(property, interfaceType);
string message = null;
if(valueImplementation == ValueImplementation.Null) {
message = $"Component must implement '{interfaceType.Name}'";
}
else if(valueImplementation == ValueImplementation.Mismatch) {
message = $"Component does not implement '{interfaceType.Name}'";
if(allowMismatch == false) {
property.objectReferenceValue = null;
property.serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
}
if(string.IsNullOrEmpty(message) == false) {
Rect boxPosition = new Rect(position.x, position.y, position.width, MESSAGE_HEIGHT - 2f);
EditorGUI.HelpBox(boxPosition, message, valueImplementation == ValueImplementation.Mismatch ? MessageType.Warning : MessageType.Info);
}
Rect fieldPosition = new Rect(position.x, position.yMax - FIELD_HEIGHT, position.width, FIELD_HEIGHT);
EditorGUI.PropertyField(fieldPosition, property, label);
}
private ValueImplementation GetValueInterfaceImplementation(SerializedProperty property, Type interfaceType) {
UnityEngine.Object value = property.objectReferenceValue;
if(value == null) {
return ValueImplementation.Null;
}
else if(interfaceType.IsAssignableFrom(property.objectReferenceValue.GetType())) {
return ValueImplementation.Match;
}
return ValueImplementation.Mismatch;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c3f94f6ba2d5454f8944e8f0f9479b2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
using UnityEngine;
namespace InspectorToolkit
{
public class LayerAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(LayerAttribute))]
public class LayerAttributeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.Integer)
{
if (!_checked) Warning(property);
EditorGUI.PropertyField(position, property, label);
return;
}
property.intValue = EditorGUI.LayerField(position, label, property.intValue);
}
private bool _checked;
private void Warning(SerializedProperty property)
{
Debug.LogWarning(string.Format("Property <color=brown>{0}</color> in object <color=brown>{1}</color> is of wrong type. Expected: Int",
property.name, property.serializedObject.targetObject));
_checked = true;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 979d3d332ae931a4f8dc8aaf52644f0c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using UnityEditor;
using UnityEngine;
namespace InspectorToolkit {
public class LinkAttribute : PropertyAttribute {
public readonly string message;
public readonly string url;
public LinkAttribute(string message, string url) {
this.message = message;
this.url = url;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(LinkAttribute))]
public class LinkAttributeDrawer : PropertyDrawer {
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
return base.GetPropertyHeight(property, label) + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
//If you don't capture these and recreate below, GUIContent label becomes replaces with LinkButtons GUIContent
string labelText = label.text;
string labelTooltip = label.tooltip;
string message = ((LinkAttribute)attribute).message;
string url = ((LinkAttribute)attribute).url;
var linkPosition = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
if(EditorGUI.LinkButton(linkPosition, message)) {
Application.OpenURL(url);
}
var fieldPosition = new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing, position.width, EditorGUIUtility.singleLineHeight);
EditorGUI.PropertyField(fieldPosition, property, new GUIContent(labelText,labelTooltip));
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65d3077f048889344952a1fd170f5810
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,201 @@
using InspectorToolkit.Internal;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using InspectorToolkit.EditorTools;
#endif
#pragma warning disable 0414
namespace InspectorToolkit
{
public class MaxValueAttribute : AttributeBase
{
private readonly float _x;
private readonly float _y;
private readonly float _z;
private readonly bool _vectorValuesSet;
public MaxValueAttribute(float value)
{
_x = value;
}
public MaxValueAttribute(float x, float y, float z)
{
_x = x;
_y = y;
_z = z;
_vectorValuesSet = true;
}
#if UNITY_EDITOR
private string _warning;
public override void ValidateProperty(SerializedProperty property)
{
if (!property.IsNumerical())
{
if (_warning == null) _warning = property.name + " caused: [MaxValueAttribute] used with non-numeric property";
return;
}
bool valueHandled = HandleValues(property);
if (valueHandled) property.serializedObject.ApplyModifiedProperties();
}
public override bool OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (_warning == null) return false;
EditorGUI.HelpBox(position, _warning, MessageType.Warning);
return true;
}
public override float? OverrideHeight()
{
if (_warning == null) return null;
return EditorGUIUtility.singleLineHeight;
}
#region Handle Value
/// <returns>true if fixed</returns>
private bool HandleValues(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Float:
case SerializedPropertyType.Integer:
return HandleNumerics(property);
case SerializedPropertyType.Vector2:
case SerializedPropertyType.Vector3:
case SerializedPropertyType.Vector4:
return HandleVectors(property);
case SerializedPropertyType.Vector2Int:
case SerializedPropertyType.Vector3Int:
return HandleIntVectors(property);
}
return false;
}
private bool HandleNumerics(SerializedProperty property)
{
var maxValue = _x;
if (property.propertyType == SerializedPropertyType.Integer && property.intValue > maxValue)
{
property.intValue = (int) maxValue;
return true;
}
if (property.propertyType == SerializedPropertyType.Float && property.floatValue > maxValue)
{
property.floatValue = maxValue;
return true;
}
return false;
}
private bool HandleVectors(SerializedProperty property)
{
var x = _x;
var y = _vectorValuesSet ? _y : x;
var z = _vectorValuesSet ? _z : x;
Vector4 vector = Vector4.zero;
switch (property.propertyType)
{
case SerializedPropertyType.Vector2:
vector = property.vector2Value;
break;
case SerializedPropertyType.Vector3:
vector = property.vector3Value;
break;
case SerializedPropertyType.Vector4:
vector = property.vector4Value;
break;
}
bool handled = false;
if (vector[0] > x)
{
vector[0] = x;
handled = true;
}
if (vector[1] > y)
{
vector[1] = y;
handled = true;
}
if (vector[2] > z)
{
vector[2] = z;
handled = true;
}
switch (property.propertyType)
{
case SerializedPropertyType.Vector2:
property.vector2Value = vector;
break;
case SerializedPropertyType.Vector3:
property.vector3Value = vector;
break;
case SerializedPropertyType.Vector4:
property.vector4Value = vector;
break;
}
return handled;
}
private bool HandleIntVectors(SerializedProperty property)
{
var x = (int) _x;
var y = _vectorValuesSet ? (int) _y : x;
var z = _vectorValuesSet ? (int) _z : x;
if (property.propertyType == SerializedPropertyType.Vector2Int)
{
var vector = property.vector2IntValue;
if (vector.x > x || vector.y > y)
{
property.vector2IntValue = new Vector2Int(
vector.x > x ? x : vector.x,
vector.y > y ? y : vector.y);
return true;
}
return false;
}
if (property.propertyType == SerializedPropertyType.Vector3Int)
{
var vector = property.vector3IntValue;
if (vector.x > x || vector.y > y || vector.z > z)
{
property.vector3IntValue = new Vector3Int(
vector.x > x ? x : vector.x,
vector.y > y ? y : vector.y,
vector.z > z ? z : vector.z);
return true;
}
return false;
}
return false;
}
#endregion
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8930225c73d74e5dbb7784e4c97eb1ef
timeCreated: 1582014887

View File

@@ -0,0 +1,133 @@
// ----------------------------------------------------------------------------
// Author: Richard Fine
// Source: https://bitbucket.org/richardfine/scriptableobjectdemo
// ----------------------------------------------------------------------------
using System;
using UnityEngine;
namespace InspectorToolkit {
public class MinMaxRangeAttribute : PropertyAttribute {
public MinMaxRangeAttribute(float min, float max) {
Min = min;
Max = max;
}
public readonly float Min;
public readonly float Max;
}
[Serializable]
public struct RangedFloat {
public float Min;
public float Max;
public RangedFloat(float min, float max) {
Min = min;
Max = max;
}
}
[Serializable]
public struct RangedInt {
public int Min;
public int Max;
public RangedInt(int min, int max) {
Min = min;
Max = max;
}
}
public static class RangedExtensions {
public static float LerpFromRange(this RangedFloat ranged, float t) {
return Mathf.Lerp(ranged.Min, ranged.Max, t);
}
public static float LerpFromRangeUnclamped(this RangedFloat ranged, float t) {
return Mathf.LerpUnclamped(ranged.Min, ranged.Max, t);
}
public static float LerpFromRange(this RangedInt ranged, float t) {
return Mathf.Lerp(ranged.Min, ranged.Max, t);
}
public static float LerpFromRangeUnclamped(this RangedInt ranged, float t) {
return Mathf.LerpUnclamped(ranged.Min, ranged.Max, t);
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
using UnityEditor;
[CustomPropertyDrawer(typeof(MinMaxRangeAttribute))]
public class MinMaxRangeIntAttributeDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
SerializedProperty minProp = property.FindPropertyRelative("Min");
SerializedProperty maxProp = property.FindPropertyRelative("Max");
if(minProp == null || maxProp == null) {
Debug.LogWarning("MinMaxRangeAttribute used on <color=brown>" +
property.name +
"</color>. Must be used on types with Min and Max fields",
property.serializedObject.targetObject);
return;
}
var minValid = minProp.propertyType == SerializedPropertyType.Integer || minProp.propertyType == SerializedPropertyType.Float;
var maxValid = maxProp.propertyType == SerializedPropertyType.Integer || maxProp.propertyType == SerializedPropertyType.Float;
if(!maxValid || !minValid || minProp.propertyType != maxProp.propertyType) {
Debug.LogWarning("MinMaxRangeAttribute used on <color=brown>" +
property.name +
"</color>. Min and Max fields must be of int or float type",
property.serializedObject.targetObject);
return;
}
MinMaxRangeAttribute rangeAttribute = (MinMaxRangeAttribute)attribute;
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
bool isInt = minProp.propertyType == SerializedPropertyType.Integer;
float minValue = isInt ? minProp.intValue : minProp.floatValue;
float maxValue = isInt ? maxProp.intValue : maxProp.floatValue;
float rangeMin = rangeAttribute.Min;
float rangeMax = rangeAttribute.Max;
const float rangeBoundsLabelWidth = 40f;
var rangeBoundsLabel1Rect = new Rect(position);
rangeBoundsLabel1Rect.width = rangeBoundsLabelWidth;
GUI.Label(rangeBoundsLabel1Rect, new GUIContent(minValue.ToString(isInt ? "F0" : "F2")));
position.xMin += rangeBoundsLabelWidth;
var rangeBoundsLabel2Rect = new Rect(position);
rangeBoundsLabel2Rect.xMin = rangeBoundsLabel2Rect.xMax - rangeBoundsLabelWidth;
GUI.Label(rangeBoundsLabel2Rect, new GUIContent(maxValue.ToString(isInt ? "F0" : "F2")));
position.xMax -= rangeBoundsLabelWidth;
EditorGUI.BeginChangeCheck();
EditorGUI.MinMaxSlider(position, ref minValue, ref maxValue, rangeMin, rangeMax);
if(EditorGUI.EndChangeCheck()) {
if(isInt) {
minProp.intValue = Mathf.RoundToInt(minValue);
maxProp.intValue = Mathf.RoundToInt(maxValue);
}
else {
minProp.floatValue = minValue;
maxProp.floatValue = maxValue;
}
}
EditorGUI.EndProperty();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa6fba45efae4ac4b9830e11066fb969
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,202 @@
using InspectorToolkit.Internal;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using InspectorToolkit.EditorTools;
#endif
#pragma warning disable 0414
namespace InspectorToolkit
{
public class MinValueAttribute : AttributeBase
{
private readonly float _x;
private readonly float _y;
private readonly float _z;
private readonly bool _vectorValuesSet;
public MinValueAttribute(float value)
{
_x = value;
}
public MinValueAttribute(float x, float y, float z)
{
_x = x;
_y = y;
_z = z;
_vectorValuesSet = true;
}
#if UNITY_EDITOR
private string _warning;
public override void ValidateProperty(SerializedProperty property)
{
if (!property.IsNumerical())
{
if (_warning == null) _warning = property.name + " caused: [MinValueAttribute] used with non-numeric property";
return;
}
bool valueHandled = HandleValues(property);
if (valueHandled) property.serializedObject.ApplyModifiedProperties();
}
public override bool OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (_warning == null) return false;
EditorGUI.HelpBox(position, _warning, MessageType.Warning);
return true;
}
public override float? OverrideHeight()
{
if (_warning == null) return null;
return EditorGUIUtility.singleLineHeight;
}
#region Handle Value
/// <returns>true if fixed</returns>
private bool HandleValues(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Float:
case SerializedPropertyType.Integer:
return HandleNumerics(property);
case SerializedPropertyType.Vector2:
case SerializedPropertyType.Vector3:
case SerializedPropertyType.Vector4:
return HandleVectors(property);
case SerializedPropertyType.Vector2Int:
case SerializedPropertyType.Vector3Int:
return HandleIntVectors(property);
}
return false;
}
private bool HandleNumerics(SerializedProperty property)
{
var minValue = _x;
if (property.propertyType == SerializedPropertyType.Integer && property.intValue < minValue)
{
property.intValue = (int) minValue;
return true;
}
if (property.propertyType == SerializedPropertyType.Float && property.floatValue < minValue)
{
property.floatValue = minValue;
return true;
}
return false;
}
private bool HandleVectors(SerializedProperty property)
{
var x = _x;
var y = _vectorValuesSet ? _y : x;
var z = _vectorValuesSet ? _z : x;
Vector4 vector = Vector4.zero;
switch (property.propertyType)
{
case SerializedPropertyType.Vector2:
vector = property.vector2Value;
break;
case SerializedPropertyType.Vector3:
vector = property.vector3Value;
break;
case SerializedPropertyType.Vector4:
vector = property.vector4Value;
break;
}
bool handled = false;
if (vector[0] < x)
{
vector[0] = x;
handled = true;
}
if (vector[1] < y)
{
vector[1] = y;
handled = true;
}
if (vector[2] < z)
{
vector[2] = z;
handled = true;
}
switch (property.propertyType)
{
case SerializedPropertyType.Vector2:
property.vector2Value = vector;
break;
case SerializedPropertyType.Vector3:
property.vector3Value = vector;
break;
case SerializedPropertyType.Vector4:
property.vector4Value = vector;
break;
}
return handled;
}
private bool HandleIntVectors(SerializedProperty property)
{
var x = (int) _x;
var y = _vectorValuesSet ? (int) _y : x;
var z = _vectorValuesSet ? (int) _z : x;
if (property.propertyType == SerializedPropertyType.Vector2Int)
{
var vector = property.vector2IntValue;
if (vector.x < x || vector.y < y)
{
property.vector2IntValue = new Vector2Int(
vector.x < x ? x : vector.x,
vector.y < y ? y : vector.y);
return true;
}
return false;
}
if (property.propertyType == SerializedPropertyType.Vector3Int)
{
var vector = property.vector3IntValue;
if (vector.x < x || vector.y < y || vector.z < z)
{
property.vector3IntValue = new Vector3Int(
vector.x < x ? x : vector.x,
vector.y < y ? y : vector.y,
vector.z < z ? z : vector.z);
return true;
}
return false;
}
return false;
}
#endregion
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7a6fc14799014e3ea82c8158bd477aca
timeCreated: 1582013139

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Jovian.Utilities;
using UnityEngine;
namespace InspectorToolkit
{
/// <summary>
/// Finds all objects in the project of 'objectType' then creates a dropdown showing all values matching 'memberName'
/// E.g. Find all 'ScriptableObject' and list their 'name' values in a list.
/// </summary>
public class ObjectsPropertyAttribute : PropertyAttribute
{
public readonly Type objectType;
public readonly string memberName;
public ObjectsPropertyAttribute(Type objectType, string memberName)
{
this.objectType = objectType;
this.memberName = memberName;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
using System.Reflection;
[CustomPropertyDrawer(typeof(ObjectsPropertyAttribute))]
public class ObjectsPropertyAttributeDrawer : PropertyDrawer
{
private bool isInitialized;
private bool hasValidTarget;
private string[] values;
private int selectedIndex = -1;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.String)
{
label.text = $"(!) {label.text}";
label.tooltip = $"ObjectPropertiesAttribute only works on strings.\n\n{label.tooltip}";
EditorGUI.PropertyField(position, property, label);
return;
}
if (!isInitialized)
{
Initialize(property.stringValue);
}
EditorGUI.BeginChangeCheck();
selectedIndex = Array.IndexOf(values, property.stringValue);
selectedIndex = EditorGUI.Popup(position, label.text, selectedIndex, values);
if (EditorGUI.EndChangeCheck())
{
property.stringValue = values[selectedIndex];
property.serializedObject.ApplyModifiedProperties();
}
}
private void Initialize(string currentValue)
{
ObjectsPropertyAttribute objectsPropertyAttribute = (ObjectsPropertyAttribute)attribute;
List<UnityEngine.Object> objectsOfType =
AssetUtility.FindAllAssetsInProject(objectsPropertyAttribute.objectType);
const BindingFlags searchFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance;
FieldInfo field = objectsPropertyAttribute.objectType.GetFields(searchFlags)
.FirstOrDefault(memberInfo => memberInfo.Name == objectsPropertyAttribute.memberName);
PropertyInfo property = objectsPropertyAttribute.objectType.GetProperties(searchFlags)
.FirstOrDefault(memberInfo => memberInfo.Name == objectsPropertyAttribute.memberName);
if (field != null)
{
values = objectsOfType.Select(obj => field.GetValue(obj).ToString()).ToArray();
}
else if (property != null)
{
values = objectsOfType.Select(obj => property.GetValue(obj).ToString()).ToArray();
}
else
{
values = new string[] { };
Debug.LogError($"Cannot find member '{objectsPropertyAttribute.memberName}' for type '{objectsPropertyAttribute.objectType.Name}'");
}
isInitialized = true;
selectedIndex = Array.IndexOf(values, currentValue);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 01fff4c8e285bbc43b74bc4b6e76eb04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,38 @@
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace InspectorToolkit {
public class OnValueChangedAttribute : PropertyAttribute {
public readonly string methodName;
public OnValueChangedAttribute(string methodName) {
this.methodName = methodName;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(OnValueChangedAttribute))]
public class OnValueChangedAttributeDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(position, property, label);
if(EditorGUI.EndChangeCheck()) {
property.serializedObject.ApplyModifiedProperties();
Object target = property.serializedObject.targetObject;
string methodName = ((OnValueChangedAttribute)attribute).methodName;
MethodInfo methodInfo = target.GetType().GetMethod(methodName);
if (methodInfo != null) {
methodInfo.Invoke(target, null);
}
else {
Debug.LogError($"No method named '{methodName}' found on {target}", target);
}
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88837c4ffdcc87f4c927e0ef8d2c4ba5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
using UnityEditor;
using UnityEngine;
namespace InspectorToolkit {
public class OptionalAttribute : PropertyAttribute { }
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(OptionalAttribute))]
public class OptionalAttributePropertyDrawer : PropertyDrawer {
private const float ICON_X_OFFSET = 17f;
private const float ICON_SIZE = 18f;
private GUIContent icon;
private GUIStyle style;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
if (property.propertyType != SerializedPropertyType.ObjectReference) {
Debug.LogError($"Optional should only be used on Unity Object references. Target='{property.serializedObject.targetObject.name}.{property.propertyPath}'");
EditorGUI.PropertyField(position, property, label);
return;
}
if(icon == null || style == null) {
icon = new GUIContent(EditorGUIUtility.IconContent("DotFrameDotted").image, "Optional");
style = new GUIStyle(GUI.skin.label)
{
padding = new RectOffset(0, 0, 0, 0),
imagePosition = ImagePosition.ImageOnly
};
}
if(property.objectReferenceValue == null) {
Rect iconRect = new Rect(position.x - ICON_X_OFFSET, position.y + 1, ICON_SIZE, EditorGUIUtility.singleLineHeight);
GUI.Label(iconRect, icon, style);
}
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(position, property, GUIContent.none);
if(EditorGUI.EndChangeCheck()) {
property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.EndProperty();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 303db5223d8c8674791938a5180540e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using UnityEngine;
namespace InspectorToolkit
{
public class OverrideLabelAttribute : PropertyAttribute
{
public readonly string NewLabel;
public OverrideLabelAttribute(string newLabel) => NewLabel = newLabel;
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(OverrideLabelAttribute))]
public class OverrideLabelDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
label.text = ((OverrideLabelAttribute)attribute).NewLabel;
EditorGUI.PropertyField(position, property, label);
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8a3118b2fddf33042bce8519708bf318
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,161 @@
// ----------------------------------------------------------------------------
// Author: Kaynn, Yeo Wen Qin
// https://github.com/Kaynn-Cahya
// Date: 17/02/2019
// ----------------------------------------------------------------------------
using UnityEngine;
namespace InspectorToolkit
{
public class PositiveValueOnlyAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
using EditorTools;
[CustomPropertyDrawer(typeof(PositiveValueOnlyAttribute))]
public class PositiveValueOnlyAttributeDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (!property.IsNumerical())
{
MyGUI.DrawColouredRect(position, MyGUI.Colors.Red);
EditorGUI.LabelField(position, new GUIContent("", "[PositiveValueOnly] used with non-numeric property"));
}
else
{
if (HandleNegativeValues(property)) property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.PropertyField(position, property, label, true);
}
/// <summary>
/// Set number value to 0 if less than 0
/// </summary>
/// <returns>true if fixed</returns>
private bool HandleNegativeValues(SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Float:
case SerializedPropertyType.Integer:
return HandleNumerics(property);
case SerializedPropertyType.Vector2:
case SerializedPropertyType.Vector3:
case SerializedPropertyType.Vector4:
return HandleVectors(property);
case SerializedPropertyType.Vector2Int:
case SerializedPropertyType.Vector3Int:
return HandleIntVectors(property);
}
return false;
}
private bool HandleNumerics(SerializedProperty property)
{
if (property.propertyType == SerializedPropertyType.Integer && property.intValue < 0)
{
property.intValue = 0;
return true;
}
if (property.propertyType == SerializedPropertyType.Float && property.floatValue < 0)
{
property.floatValue = 0;
return true;
}
return false;
}
private bool HandleVectors(SerializedProperty property)
{
Vector4 vector = Vector4.zero;
switch (property.propertyType)
{
case SerializedPropertyType.Vector2:
vector = property.vector2Value;
break;
case SerializedPropertyType.Vector3:
vector = property.vector3Value;
break;
case SerializedPropertyType.Vector4:
vector = property.vector4Value;
break;
}
bool handled = false;
for (int i = 0; i < 4; ++i)
{
if (vector[i] < 0f)
{
vector[i] = 0;
handled = true;
}
}
switch (property.propertyType)
{
case SerializedPropertyType.Vector2:
property.vector2Value = vector;
break;
case SerializedPropertyType.Vector3:
property.vector3Value = vector;
break;
case SerializedPropertyType.Vector4:
property.vector4Value = vector;
break;
}
return handled;
}
private bool HandleIntVectors(SerializedProperty property)
{
if (property.propertyType == SerializedPropertyType.Vector2Int)
{
var vector = property.vector2IntValue;
if (vector.x > 0 && vector.y > 0) return false;
property.vector2IntValue = new Vector2Int(
vector.x < 0 ? 0 : vector.x,
vector.y < 0 ? 0 : vector.y);
return true;
}
if (property.propertyType == SerializedPropertyType.Vector3Int)
{
var vector = property.vector3IntValue;
if (vector.x > 0 && vector.y > 0 && vector.z > 0) return false;
property.vector3Value = new Vector3(
vector.x < 0 ? 0 : vector.x,
vector.y < 0 ? 0 : vector.y,
vector.z < 0 ? 0 : vector.z);
return true;
}
return false;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f63e31912630bf4e88f4f2e9eab2f7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using UnityEngine;
namespace InspectorToolkit
{
public class ReadOnlyAttribute : ConditionalFieldAttribute
{
public ReadOnlyAttribute() : base("") { }
/// <param name="fieldToCheck">String name of field to check value</param>
/// <param name="inverse">Inverse check result</param>
/// <param name="compareValues">On which values field will be shown in inspector</param>
public ReadOnlyAttribute(string fieldToCheck, bool inverse = false, params object[] compareValues)
: base(fieldToCheck, inverse, compareValues) { }
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyAttributeDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, true);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (!(attribute is ReadOnlyAttribute conditional)) return;
bool enabled = false;
if (!string.IsNullOrEmpty(conditional.FieldToCheck))
{
var propertyToCheck = ConditionalFieldUtility.FindRelativeProperty(property, conditional.FieldToCheck);
enabled = !ConditionalFieldUtility.PropertyIsVisible(propertyToCheck, conditional.Inverse, conditional.CompareValues);
}
GUI.enabled = enabled;
EditorGUI.PropertyField(position, property, label, true);
GUI.enabled = true;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44c78368338beb948bdffa8db7179a0b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,121 @@
using System.Text.RegularExpressions;
using UnityEngine;
namespace InspectorToolkit
{
/// <summary>
/// Validate a string field by regex expression
/// RegexMode.Match will keep only matching content
/// RegexMode.Replace will keep all except regex match
/// </summary>
public class RegexStringAttribute : PropertyAttribute
{
public readonly Regex Regex;
public readonly RegexStringMode AttributeMode;
public RegexStringAttribute(string regex, RegexStringMode mode = RegexStringMode.Match, RegexOptions options = RegexOptions.None)
{
Regex = new Regex(regex, options);
AttributeMode = mode;
}
}
public enum RegexStringMode
{
/// <summary>
/// Keep only parts of the string that Match the Expression
/// </summary>
Match,
/// <summary>
/// Remove from the string parts that not match the Expression
/// </summary>
Replace,
/// <summary>
/// Highlight the field if any of the parts of the string matching the Expression
/// </summary>
WarningIfMatch,
/// <summary>
/// Highlight the field if some parts of the string not matching the Expression
/// </summary>
WarningIfNotMatch
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
using EditorTools;
[CustomPropertyDrawer(typeof(RegexStringAttribute))]
public class RegexStringAttributeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.String)
{
MyGUI.DrawColouredRect(position, MyGUI.Colors.Red);
EditorGUI.LabelField(position, new GUIContent("", "[RegexStringAttribute] used with non-string property"));
}
else
{
var regex = (RegexStringAttribute)attribute;
var mode = regex.AttributeMode;
bool ifMatch = mode == RegexStringMode.WarningIfMatch;
bool ifNotMatch = mode == RegexStringMode.WarningIfNotMatch;
bool anyMatching = regex.Regex.IsMatch(property.stringValue);
bool warn = (ifMatch && anyMatching) || (ifNotMatch && !anyMatching);
var originalPosition = position;
DrawWarning();
position.width -= 20;
EditorGUI.PropertyField(position, property, label, true);
DrawTooltip();
if (GUI.changed)
{
if (mode == RegexStringMode.Replace) OnReplace();
if (mode == RegexStringMode.Match) OnKeepMatching();
}
if (warn)
{
GUILayoutUtility.GetRect(GUIContent.none, EditorStyles.objectField);
position = originalPosition;
position.y += EditorGUIUtility.singleLineHeight;
DrawWarning();
position.x += EditorGUIUtility.labelWidth;
var warningContent = new GUIContent("Regex rule violated!");
EditorGUI.LabelField(position, warningContent, EditorStyles.miniBoldLabel);
}
property.serializedObject.ApplyModifiedProperties();
void OnReplace() => property.stringValue = regex.Regex.Replace(property.stringValue, "");
void OnKeepMatching() => property.stringValue = regex.Regex.KeepMatching(property.stringValue);
void DrawWarning()
{
if (!ifMatch && !ifNotMatch) return;
MyGUI.DrawColouredRect(position, warn ? MyGUI.Colors.Yellow : Color.clear);
}
void DrawTooltip()
{
string tooltip = "Regex field: ";
if (mode == RegexStringMode.Match || mode == RegexStringMode.WarningIfNotMatch) tooltip += "match expression";
else tooltip += "remove expression";
tooltip += $"\n[{regex.Regex}]";
position.x += position.width + 2;
position.width = 18;
var tooltipContent = new GUIContent(MyGUI.EditorIcons.Help);
tooltipContent.tooltip = tooltip;
EditorGUI.LabelField(position, tooltipContent, EditorStyles.label);
}
}
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 38cf7a3ca1a04872b56d3cc1d90997b6
timeCreated: 1623076361

View File

@@ -0,0 +1,86 @@
using System;
using InspectorToolkit.Internal;
using Jovian.Utilities.Editor;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace InspectorToolkit {
public class RequireArraySizeAttribute : ArrayAttribute, IPropertyCondition {
public readonly int size;
public readonly SizeComparison comparison;
public enum SizeComparison {
Equal,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual
}
public RequireArraySizeAttribute(int size, SizeComparison comparison = SizeComparison.Equal) {
this.size = size;
this.comparison = comparison;
}
#if UNITY_EDITOR
private bool DoesArraySizeMeetCondition(int arraySize) {
return comparison switch {
SizeComparison.Equal => arraySize == size,
SizeComparison.LessThan => arraySize < size,
SizeComparison.LessThanOrEqual => arraySize <= size,
SizeComparison.GreaterThan => arraySize > size,
SizeComparison.GreaterThanOrEqual => arraySize >= size,
_ => throw new ArgumentOutOfRangeException()
};
}
private string GetConditionString(int arraySize) {
return comparison switch {
SizeComparison.Equal => $"Array.size must be equal to {size} (current={arraySize})",
SizeComparison.LessThan => $"Array.size must be less than {size} (current={arraySize})",
SizeComparison.LessThanOrEqual => $"Array.size must be less than or equal to {size} (current={arraySize})",
SizeComparison.GreaterThan => $"Array.size must be greater than {size} (current={arraySize})",
SizeComparison.GreaterThanOrEqual => $"Array.size must be greater than or equal to {size} (current={arraySize})",
_ => throw new ArgumentOutOfRangeException()
};
}
public bool DoesPropertyMeetCondition(SerializedProperty property, out string description) {
description = string.Empty;
if (!property.isArray && (property.propertyPath.EndsWith(".Array.size") || EditorSerializationUtility.IsPropertyAnArrayElement(property))) {
return true; // don't check array conditions for elements
}
if (!property.isArray) {
description = $"{property.name} must be an array";
return false;
}
if (!(property.isArray && DoesArraySizeMeetCondition(property.arraySize))) {
description = GetConditionString(property.arraySize);
return false;
}
return true;
}
public override void OnPreGUI(ref Rect position, SerializedProperty property, ref GUIContent label) {
if (!DoesPropertyMeetCondition(property, out string description)) {
RequiredUtil.LayoutRequired(ref position, description, indent: true);
}
}
public override void OnPostGUI(ref Rect position, SerializedProperty property, ref GUIContent label) {
if (!DoesPropertyMeetCondition(property, out string description)) {
RequiredUtil.DrawRequired(ref position, description, indent: true);
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ebe88cf8a45d4df19c15769d67ccbf11
timeCreated: 1709801667

View File

@@ -0,0 +1,21 @@
using System;
namespace InspectorToolkit
{
[AttributeUsage(AttributeTargets.Class)]
public class RequireLayerAttribute : Attribute
{
public readonly string LayerName;
public readonly int LayerIndex = -1;
public RequireLayerAttribute(string layer)
{
LayerName = layer;
}
public RequireLayerAttribute(int layer)
{
LayerIndex = layer;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a239ed93c95b4382a478c97d2bec7768
timeCreated: 1567174003

View File

@@ -0,0 +1,54 @@
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
public class RequireLayerOrTagAttributeHandler
{
static RequireLayerOrTagAttributeHandler()
{
EditorApplication.playModeStateChanged += AutoSaveWhenPlaymodeStarts;
}
private static void AutoSaveWhenPlaymodeStarts(PlayModeStateChange obj)
{
if (EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isPlaying)
{
var components = Object.FindObjectsOfType<Component>();
foreach (var component in components)
{
foreach (var attribute in component.GetType().GetCustomAttributes(true))
{
var layerAttribute = attribute as RequireLayerAttribute;
if (layerAttribute != null)
{
var requiredLayer = layerAttribute.LayerName != null ?
LayerMask.NameToLayer(layerAttribute.LayerName) :
layerAttribute.LayerIndex;
if (component.gameObject.layer == requiredLayer) continue;
Debug.LogWarning("Layer of " + component.name + " changed by RequireLayerAttribute to " + layerAttribute.LayerName);
component.gameObject.layer = requiredLayer;
EditorUtility.SetDirty(component);
continue;
}
var tagAttribute = attribute as RequireTagAttribute;
if (tagAttribute != null)
{
if (component.CompareTag(tagAttribute.Tag)) continue;
Debug.LogWarning("Tag of " + component.name + " changed by RequireTagAttribute to " + tagAttribute.Tag);
component.gameObject.tag = tagAttribute.Tag;
EditorUtility.SetDirty(component);
}
}
}
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4b54e6bb2f8b4c84abe977357edeb0fc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,95 @@
using UnityEngine;
#if UNITY_EDITOR
using System.Reflection;
using UnityEditor;
using InspectorToolkit.EditorTools;
#endif
namespace InspectorToolkit {
/// <summary>
/// Does not work on objects with children - will break their UI
/// </summary>
public class RequireMethodAttribute : PropertyAttribute, IPropertyCondition {
private readonly string methodName;
public RequireMethodAttribute(string methodName) {
this.methodName = methodName;
}
#if UNITY_EDITOR
public bool DoesPropertyMeetCondition(SerializedProperty property, out string description) {
Object target = property.serializedObject.targetObject;
MethodInfo methodInfo = target.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
description = string.Empty;
if (methodInfo != null && methodInfo.ReturnParameter?.ParameterType == typeof(bool)) {
ParameterInfo[] methodParameters = methodInfo.GetParameters();
object[] parameterValues = null;
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == property.GetValue().GetType()) {
parameterValues = new[] { property.GetValue() };
}
object returnValue = methodInfo.Invoke(target, parameterValues);
if (returnValue is bool boolReturnValue) {
if (!boolReturnValue) {
description = $"{property.name} is failing {methodName}.";
return false;
}
return true;
}
else {
description = $"{property.name} {methodName} is invalid - does not return a boolean.";
return false;
}
}
description = $"{property.name} {methodName} is invalid - no method can be found on {target}.";
return false;
}
#endif
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(RequireMethodAttribute))]
public class RequireMethodAttributePropertyDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
RequireMethodAttribute requireMethodAttribute = (RequireMethodAttribute)attribute;
if (property.hasChildren) {
EditorGUI.LabelField(position, new GUIContent(property.displayName), new GUIContent("RequireMethod GUI does not work on properties with children"));
return;
}
Color guiColor = GUI.color;
if (!requireMethodAttribute.DoesPropertyMeetCondition(property, out string description)) {
RequiredUtil.LayoutRequired(ref position, description, false);
RequiredUtil.DrawRequired(ref position, description, false);
}
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(position, property, GUIContent.none);
if (EditorGUI.EndChangeCheck()) {
property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.EndProperty();
GUI.color = guiColor;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1587de26320cce74981351e58d6e7b26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
using System;
namespace InspectorToolkit
{
[AttributeUsage(AttributeTargets.Class)]
public class RequireTagAttribute : Attribute
{
public string Tag;
public RequireTagAttribute(string tag)
{
Tag = tag;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4ab3a785f0a04eeeb2b90de6430cef9e
timeCreated: 1567412854

View File

@@ -0,0 +1,280 @@
using System;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
namespace InspectorToolkit {
public class RequiredAttribute : PropertyAttribute, IPropertyCondition {
public enum Scope {
None,
SelfOrChild,
SceneOnly,
SceneOrParentPrefabOnly
// ProjectOnly
/*
* ProjectOnly has a quirk I can't figure out. If you reference a prefab in the project then it works fine.
* However if you also have an instance of that prefab if the same scene/prefab then it will become a reference to that instance
* instead of the one in the project. Even the icon changes in Unity. I think this may be a Unity Bug.
*/
}
public readonly Scope scope;
public RequiredAttribute() {
this.scope = Scope.None;
}
public RequiredAttribute(Scope scope) {
this.scope = scope;
}
#if UNITY_EDITOR
public bool DoesPropertyMeetCondition(SerializedProperty property, out string description) {
description = string.Empty;
if (property.propertyType != SerializedPropertyType.ObjectReference) {
description = $"{property.name} is not an ObjectReference";
return false;
}
bool hasValue = property.objectReferenceValue != null;
if (scope == Scope.None) {
if (!hasValue) {
description = $"{property.name} requires a value";
return false;
}
return true;
}
if (scope == Scope.SelfOrChild) {
if (!hasValue) {
description = $"{property.name} requires a value";
return false;
}
if (!TryGetTargetGameObject(property, out GameObject parentGameObject)) {
description = $"{property.name} does not have a GameObject parent";
return false;
}
if (!TryGetGameObject(property.objectReferenceValue, out GameObject childGameObject)) {
description = $"{property.name} has a non-GameObject value";
return false;
}
if (!IsObjectSelfOrChild(parentGameObject, childGameObject)) {
description = $"{property.name} value is not itself or it's child";
return false;
}
return true;
}
if (scope == Scope.SceneOnly) {
if (property.serializedObject.targetObject is ScriptableObject) {
description = $"{property.name} SceneOnly is not supported on ScriptableObjects";
return false;
}
if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) {
description = $"{property.name} does not have a GameObject parent";
return false;
}
// scene is invalid - this is a prefab in the project.
if (thisGameObject.scene.IsValid() == false) {
// scene isn't valid, can't check
description = $"{property.name} has a reference to GameObject not in a Scene";
return true;
}
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
// scene is valid - prefab stage exists - this is a prefab
if (prefabStage != null) {
return true;
}
if (!hasValue) {
description = $"{property.name} requires a value";
return false;
}
return true;
}
if (scope == Scope.SceneOrParentPrefabOnly) {
if (property.serializedObject.targetObject is ScriptableObject) {
description = $"{property.name} SceneOrParentPrefabOnly is not supported on ScriptableObjects";
return false;
}
if (!TryGetTargetGameObject(property, out GameObject thisGameObject)) {
description = $"{property.name} does not have a GameObject parent";
return false;
}
// scene is invalid - this is a prefab in the project.
if (thisGameObject.scene.IsValid() == false) {
// scene isn't valid, can't check
description = $"{property.name} has a reference to GameObject not in a Scene";
return true;
}
bool hasTargetGameObject = TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject);
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
// scene is valid - prefab stage exists - this is a prefab
if (prefabStage != null) {
// if the prefab open has this requirement on its root object
if (prefabStage.prefabContentsRoot == thisGameObject) {
if (hasTargetGameObject) {
// if the value assigned is a gameObject
if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) {
// then fail if it's within the prefab
description = $"{property.name} requires an object external to its prefab / hierarchy";
return false;
}
else {
return true;
}
}
else {
return true; // if we don't have a target then it is valid (only inside this prefab of ourselves)
}
}
// if the prefab open is a different prefab, then we're valid if a value is assigned
if (!IsObjectSelfOrChild(thisGameObject, targetGameObject)) {
description = $"{property.name} requires an object external to its prefab / hierarchy";
return false;
}
else {
return true;
}
}
// not in a prefab, value must be set to an object not within the scope of this attribute
if (!(hasValue && !IsObjectSelfOrChild(thisGameObject, targetGameObject))) {
description = $"{property.name} requires an object external to its prefab / hierarchy";
return false;
}
else {
return true;
}
}
/*
See note above why this is not enabled
if (scope == Scope.ProjectOnly) {
if (!hasValue) {
return false;
}
bool isPersistent = EditorUtility.IsPersistent(property.objectReferenceValue);
bool objectHasAssetPath = !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(property.objectReferenceValue));
bool hasNoScene = false;
bool isPrefabRoot = false;
bool isInAssetDatabase = AssetDatabase.Contains(property.objectReferenceValue);
if (TryGetGameObject(property.objectReferenceValue, out GameObject targetGameObject)) {
isPrefabRoot = PrefabUtility.IsOutermostPrefabInstanceRoot(targetGameObject);
if (targetGameObject.scene.IsValid() == false) {
// scene isn't valid, can't check
//return true;
hasNoScene = true;
}
}
// if the target has a path, then it is an asset and that is valid!
return objectHasAssetPath;
}*/
if (!hasValue) {
description = $"{property.name} requires a value";
return false;
}
return true;
}
private static bool TryGetTargetGameObject(SerializedProperty property, out GameObject gameObject) {
return TryGetGameObject(property.serializedObject.targetObject, out gameObject);
}
private static bool TryGetGameObject(Object obj, out GameObject gameObject) {
if (obj == null) {
gameObject = null;
return false;
}
gameObject = obj switch {
Component component => component.gameObject,
GameObject targetGameObject => targetGameObject,
_ => null
};
return gameObject != null;
}
private static bool IsObjectSelfOrChild(GameObject parentObject, GameObject queryObject) {
if (parentObject == null || queryObject == null) {
return false;
}
return parentObject == queryObject || queryObject.transform.IsChildOf(parentObject.transform);
}
#endif
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(RequiredAttribute))]
public class RequiredAttributePropertyDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
if (property.propertyType != SerializedPropertyType.ObjectReference) {
Debug.LogError($"Required should only be used on Unity Object references. Target='{property.serializedObject.targetObject.name}.{property.propertyPath}'");
EditorGUI.PropertyField(position, property, label);
return;
}
RequiredAttribute requiredAttribute = (RequiredAttribute)attribute;
Color guiColor = GUI.color;
if (!requiredAttribute.DoesPropertyMeetCondition(property, out string text)) {
RequiredUtil.LayoutRequired(ref position, text, false);
RequiredUtil.DrawRequired(ref position, text, false);
}
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, label);
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(position, property, GUIContent.none);
if (EditorGUI.EndChangeCheck()) {
property.serializedObject.ApplyModifiedProperties();
}
EditorGUI.EndProperty();
GUI.color = guiColor;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20675fbb6f9e5764a9aa6e8efb4b8150
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,77 @@
// ----------------------------------------------------------------------------
// Author: Anton
// https://github.com/antontidev
// ----------------------------------------------------------------------------
using System;
using System.Linq;
using UnityEngine;
namespace InspectorToolkit
{
/// <summary>
/// Used to pick scene from inspector.
/// Consider to use <see cref="SceneReference"/> type instead as it is more flexible
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class SceneAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(SceneAttribute))]
public class SceneDrawer : PropertyDrawer
{
private SceneAttribute _attribute;
private string[] _scenesInBuild;
private int _index;
private void Initialize(string initialValue)
{
if (_attribute != null) return;
_attribute = (SceneAttribute)attribute;
_scenesInBuild = new string[EditorBuildSettings.scenes.Length + 1];
_index = 0;
for (var i = 0; i < EditorBuildSettings.scenes.Length; i++)
{
var formatted = EditorBuildSettings.scenes[i].path.Split('/').Last().Replace(".unity", string.Empty);
if (initialValue == formatted) _index = i + 1;
formatted += $" [{i}]";
_scenesInBuild[i + 1] = formatted;
}
var defaultValue = "NULL";
if (initialValue.NotNullOrEmpty() && _index == 0) defaultValue = "NOT FOUND: " + initialValue;
_scenesInBuild[0] = defaultValue;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.String)
{
EditorGUI.LabelField(position, label.text, "Use [Scene] with strings.");
return;
}
Initialize(property.stringValue);
var newIndex = EditorGUI.Popup(position, label.text, _index, _scenesInBuild);
if (newIndex != _index)
{
_index = newIndex;
var value = _scenesInBuild[_index];
property.stringValue = newIndex == 0 ? string.Empty : value.Substring(0, value.IndexOf('[') - 1);
property.serializedObject.ApplyModifiedProperties();
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2e2f0d183cdeeb4a9e0d11deb8f782b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,496 @@
// ----------------------------------------------------------------------------
// Author: Ryan Hipple
// Date: 05/01/2018
// Source: https://github.com/roboryantron/UnityEditorJunkie
// ----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace InspectorToolkit
{
/// <summary>
/// Put this attribute on a public (or SerializeField) enum in a
/// MonoBehaviour or ScriptableObject to get an improved enum selector
/// popup. The enum list is scrollable and can be filtered by typing.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class SearchableEnumAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
/// <summary>
/// Draws the custom enum selector popup for enum fields using the
/// SearchableEnumAttribute.
/// </summary>
[CustomPropertyDrawer(typeof(SearchableEnumAttribute))]
public class SearchableEnumAttributeDrawer : PropertyDrawer
{
private const string TYPE_ERROR = "SearchableEnum can only be used on enum fields.";
/// <summary>
/// Cache of the hash to use to resolve the ID for the drawer.
/// </summary>
private int idHash;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// If this is not used on an enum, show an error
if (property.type != "Enum")
{
GUIStyle errorStyle = "CN EntryErrorIconSmall";
Rect r = new Rect(position);
r.width = errorStyle.fixedWidth;
position.xMin = r.xMax;
GUI.Label(r, "", errorStyle);
GUI.Label(position, TYPE_ERROR);
return;
}
// By manually creating the control ID, we can keep the ID for the
// label and button the same. This lets them be selected together
// with the keyboard in the inspector, much like a normal popup.
if (idHash == 0) idHash = "SearchableEnumAttributeDrawer".GetHashCode();
int id = GUIUtility.GetControlID(idHash, FocusType.Keyboard, position);
label = EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(position, id, label);
GUIContent buttonText =
new GUIContent(property.enumDisplayNames[property.enumValueIndex]);
if (DropdownButton(id, position, buttonText))
{
Action<int> onSelect = i =>
{
property.enumValueIndex = i;
property.serializedObject.ApplyModifiedProperties();
};
SearchablePopup.Show(position, property.enumDisplayNames,
property.enumValueIndex, onSelect);
}
EditorGUI.EndProperty();
}
/// <summary>
/// A custom button drawer that allows for a controlID so that we can
/// sync the button ID and the label ID to allow for keyboard
/// navigation like the built-in enum drawers.
/// </summary>
private static bool DropdownButton(int id, Rect position, GUIContent content)
{
Event current = Event.current;
switch (current.type)
{
case EventType.MouseDown:
if (position.Contains(current.mousePosition) && current.button == 0)
{
Event.current.Use();
return true;
}
break;
case EventType.KeyDown:
if (GUIUtility.keyboardControl == id && current.character == '\n')
{
Event.current.Use();
return true;
}
break;
case EventType.Repaint:
EditorStyles.popup.Draw(position, content, id, false);
break;
}
return false;
}
}
/// <summary>
/// A popup window that displays a list of options and may use a search
/// string to filter the displayed content.
/// </summary>
public class SearchablePopup : PopupWindowContent
{
#region -- Constants --------------------------------------------------
/// <summary> Height of each element in the popup list. </summary>
private const float ROW_HEIGHT = 16.0f;
/// <summary> How far to indent list entries. </summary>
private const float ROW_INDENT = 8.0f;
/// <summary> Name to use for the text field for search. </summary>
private const string SEARCH_CONTROL_NAME = "EnumSearchText";
#endregion -- Constants -----------------------------------------------
#region -- Static Functions -------------------------------------------
/// <summary> Show a new SearchablePopup. </summary>
/// <param name="activatorRect">
/// Rectangle of the button that triggered the popup.
/// </param>
/// <param name="options">List of strings to choose from.</param>
/// <param name="current">
/// Index of the currently selected string.
/// </param>
/// <param name="onSelectionMade">
/// Callback to trigger when a choice is made.
/// </param>
public static void Show(Rect activatorRect, string[] options, int current, Action<int> onSelectionMade)
{
SearchablePopup win =
new SearchablePopup(options, current, onSelectionMade);
PopupWindow.Show(activatorRect, win);
}
/// <summary>
/// Force the focused window to redraw. This can be used to make the
/// popup more responsive to mouse movement.
/// </summary>
private static void Repaint()
{
EditorWindow.focusedWindow.Repaint();
}
/// <summary> Draw a generic box. </summary>
/// <param name="rect">Where to draw.</param>
/// <param name="tint">Color to tint the box.</param>
private static void DrawBox(Rect rect, Color tint)
{
Color c = GUI.color;
GUI.color = tint;
GUI.Box(rect, "", Selection);
GUI.color = c;
}
#endregion -- Static Functions ----------------------------------------
#region -- Helper Classes ---------------------------------------------
/// <summary>
/// Stores a list of strings and can return a subset of that list that
/// matches a given filter string.
/// </summary>
private class FilteredList
{
/// <summary>
/// An entry in the filtered list, mapping the text to the
/// original index.
/// </summary>
public struct Entry
{
public int index;
public string text;
}
/// <summary> All possible items in the list. </summary>
private readonly string[] allItems;
/// <summary> Create a new filtered list. </summary>
/// <param name="items">All The items to filter.</param>
public FilteredList(string[] items)
{
allItems = items;
Entries = new List<Entry>();
UpdateFilter("");
}
/// <summary> The current string filtering the list. </summary>
public string Filter { get; private set; }
/// <summary> All valid entries for the current filter. </summary>
public List<Entry> Entries { get; private set; }
/// <summary> Total possible entries in the list. </summary>
public int MaxLength
{
get { return allItems.Length; }
}
/// <summary>
/// Sets a new filter string and updates the Entries that match the
/// new filter if it has changed.
/// </summary>
/// <param name="filter">String to use to filter the list.</param>
/// <returns>
/// True if the filter is updated, false if newFilter is the same
/// as the current Filter and no update is necessary.
/// </returns>
public bool UpdateFilter(string filter)
{
if (Filter == filter)
return false;
Filter = filter;
Entries.Clear();
for (int i = 0; i < allItems.Length; i++)
{
if (string.IsNullOrEmpty(Filter) || allItems[i].ToLower().Contains(Filter.ToLower()))
{
Entry entry = new Entry
{
index = i,
text = allItems[i]
};
if (string.Equals(allItems[i], Filter, StringComparison.CurrentCultureIgnoreCase))
Entries.Insert(0, entry);
else
Entries.Add(entry);
}
}
return true;
}
}
#endregion -- Helper Classes ------------------------------------------
#region -- Private Variables ------------------------------------------
/// <summary> Callback to trigger when an item is selected. </summary>
private readonly Action<int> onSelectionMade;
/// <summary>
/// Index of the item that was selected when the list was opened.
/// </summary>
private readonly int currentIndex;
/// <summary>
/// Container for all available options that does the actual string
/// filtering of the content.
/// </summary>
private readonly FilteredList list;
/// <summary> Scroll offset for the vertical scroll area. </summary>
private Vector2 scroll;
/// <summary>
/// Index of the item under the mouse or selected with the keyboard.
/// </summary>
private int hoverIndex;
/// <summary>
/// An item index to scroll to on the next draw.
/// </summary>
private int scrollToIndex;
/// <summary>
/// An offset to apply after scrolling to scrollToIndex. This can be
/// used to control if the selection appears at the top, bottom, or
/// center of the popup.
/// </summary>
private float scrollOffset;
#endregion -- Private Variables ---------------------------------------
#region -- GUI Styles -------------------------------------------------
// GUIStyles implicitly cast from a string. This triggers a lookup into
// the current skin which will be the editor skin and lets us get some
// built-in styles.
private static readonly GUIStyle SearchBox = "ToolbarSearchTextField";
private static readonly GUIStyle CancelButton = "ToolbarSearchCancelButton";
private static readonly GUIStyle DisabledCancelButton = "ToolbarSearchCancelButtonEmpty";
private static readonly GUIStyle Selection = "SelectionRect";
#endregion -- GUI Styles ----------------------------------------------
#region -- Initialization ---------------------------------------------
private SearchablePopup(string[] names, int currentIndex, Action<int> onSelectionMade)
{
list = new FilteredList(names);
this.currentIndex = currentIndex;
this.onSelectionMade = onSelectionMade;
hoverIndex = currentIndex;
scrollToIndex = currentIndex;
scrollOffset = GetWindowSize().y - ROW_HEIGHT * 2;
}
#endregion -- Initialization ------------------------------------------
#region -- PopupWindowContent Overrides -------------------------------
public override void OnOpen()
{
base.OnOpen();
// Force a repaint every frame to be responsive to mouse hover.
EditorApplication.update += Repaint;
}
public override void OnClose()
{
base.OnClose();
EditorApplication.update -= Repaint;
}
public override Vector2 GetWindowSize()
{
return new Vector2(base.GetWindowSize().x,
Mathf.Min(600, list.MaxLength * ROW_HEIGHT +
EditorStyles.toolbar.fixedHeight));
}
public override void OnGUI(Rect rect)
{
Rect searchRect = new Rect(0, 0, rect.width, EditorStyles.toolbar.fixedHeight);
Rect scrollRect = Rect.MinMaxRect(0, searchRect.yMax, rect.xMax, rect.yMax);
HandleKeyboard();
DrawSearch(searchRect);
DrawSelectionArea(scrollRect);
}
#endregion -- PopupWindowContent Overrides ----------------------------
#region -- GUI --------------------------------------------------------
private void DrawSearch(Rect rect)
{
if (Event.current.type == EventType.Repaint)
EditorStyles.toolbar.Draw(rect, false, false, false, false);
Rect searchRect = new Rect(rect);
searchRect.xMin += 6;
searchRect.xMax -= 6;
searchRect.y += 2;
searchRect.width -= CancelButton.fixedWidth;
GUI.FocusControl(SEARCH_CONTROL_NAME);
GUI.SetNextControlName(SEARCH_CONTROL_NAME);
string newText = GUI.TextField(searchRect, list.Filter, SearchBox);
if (list.UpdateFilter(newText))
{
hoverIndex = 0;
scroll = Vector2.zero;
}
searchRect.x = searchRect.xMax;
searchRect.width = CancelButton.fixedWidth;
if (string.IsNullOrEmpty(list.Filter))
GUI.Box(searchRect, GUIContent.none, DisabledCancelButton);
else if (GUI.Button(searchRect, "x", CancelButton))
{
list.UpdateFilter("");
scroll = Vector2.zero;
}
}
private void DrawSelectionArea(Rect scrollRect)
{
Rect contentRect = new Rect(0, 0,
scrollRect.width - GUI.skin.verticalScrollbar.fixedWidth,
list.Entries.Count * ROW_HEIGHT);
scroll = GUI.BeginScrollView(scrollRect, scroll, contentRect);
Rect rowRect = new Rect(0, 0, scrollRect.width, ROW_HEIGHT);
for (int i = 0; i < list.Entries.Count; i++)
{
if (scrollToIndex == i &&
(Event.current.type == EventType.Repaint
|| Event.current.type == EventType.Layout))
{
Rect r = new Rect(rowRect);
r.y += scrollOffset;
GUI.ScrollTo(r);
scrollToIndex = -1;
scroll.x = 0;
}
if (rowRect.Contains(Event.current.mousePosition))
{
if (Event.current.type == EventType.MouseMove ||
Event.current.type == EventType.ScrollWheel)
hoverIndex = i;
if (Event.current.type == EventType.MouseDown)
{
onSelectionMade(list.Entries[i].index);
EditorWindow.focusedWindow.Close();
}
}
DrawRow(rowRect, i);
rowRect.y = rowRect.yMax;
}
GUI.EndScrollView();
}
private void DrawRow(Rect rowRect, int i)
{
if (list.Entries[i].index == currentIndex)
DrawBox(rowRect, Color.cyan);
else if (i == hoverIndex)
DrawBox(rowRect, Color.white);
Rect labelRect = new Rect(rowRect);
labelRect.xMin += ROW_INDENT;
GUI.Label(labelRect, list.Entries[i].text);
}
/// <summary>
/// Process keyboard input to navigate the choices or make a selection.
/// </summary>
private void HandleKeyboard()
{
if (Event.current.type == EventType.KeyDown)
{
if (Event.current.keyCode == KeyCode.DownArrow)
{
hoverIndex = Mathf.Min(list.Entries.Count - 1, hoverIndex + 1);
Event.current.Use();
scrollToIndex = hoverIndex;
scrollOffset = ROW_HEIGHT;
}
if (Event.current.keyCode == KeyCode.UpArrow)
{
hoverIndex = Mathf.Max(0, hoverIndex - 1);
Event.current.Use();
scrollToIndex = hoverIndex;
scrollOffset = -ROW_HEIGHT;
}
if (Event.current.keyCode == KeyCode.Return)
{
if (hoverIndex >= 0 && hoverIndex < list.Entries.Count)
{
onSelectionMade(list.Entries[hoverIndex].index);
EditorWindow.focusedWindow.Close();
}
}
if (Event.current.keyCode == KeyCode.Escape)
{
EditorWindow.focusedWindow.Close();
}
}
}
#endregion -- GUI -----------------------------------------------------
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d4c1ac06e38adc9418191cf707dc1c63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
using UnityEngine;
namespace InspectorToolkit
{
public class SeparatorAttribute : PropertyAttribute
{
public readonly string Title;
public readonly bool WithOffset;
public SeparatorAttribute()
{
Title = "";
}
public SeparatorAttribute(string title, bool withOffset = false)
{
Title = title;
WithOffset = withOffset;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
using UnityEditor;
[CustomPropertyDrawer(typeof(SeparatorAttribute))]
public class SeparatorAttributeDrawer : DecoratorDrawer
{
private SeparatorAttribute Separator => (SeparatorAttribute) attribute;
public override float GetHeight() => Separator.WithOffset ? 40 : Separator.Title.IsNullOrEmpty() ? 28 : 32;
public override void OnGUI(Rect position)
{
var title = Separator.Title;
if (title.IsNullOrEmpty())
{
position.height = 1;
position.y += 14;
GUI.Box(position, string.Empty);
}
else
{
Vector2 textSize = GUI.skin.label.CalcSize(new GUIContent(title));
float separatorWidth = (position.width - textSize.x) / 2 - 5;
position.y += 19;
GUI.Box(new Rect(position.xMin, position.yMin, separatorWidth, 1), string.Empty);
GUI.Label(new Rect(position.xMin + separatorWidth + 5, position.yMin - 10, textSize.x, 20), title);
GUI.Box(new Rect(position.xMin + separatorWidth + 10 + textSize.x, position.yMin, separatorWidth, 1), "");
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5b55cf9f9427edf4f94a66bc2a6b14b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,374 @@
using System;
using System.Linq;
using UnityEngine;
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using Jovian.Utilities;
using UnityObject = UnityEngine.Object;
#endif
namespace InspectorToolkit {
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ShowInInspectorAttribute : PropertyAttribute {
public readonly PlayModeVisibility Visibility;
public readonly bool isEdittingSupported;
public ShowInInspectorAttribute() {
Visibility = PlayModeVisibility.PlayMode;
isEdittingSupported = false;
}
public ShowInInspectorAttribute(PlayModeVisibility playModeVisibility, bool isEdittingSupported = false) {
Visibility = playModeVisibility;
this.isEdittingSupported = isEdittingSupported;
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
public class ShowInInspectorAttributeHandler {
private const int MAX_DEPTH = 10;
private bool _initialized;
private readonly UnityObject target;
private readonly SerializedObject serializedObject;
private List<Member> showMembers;
private GUIStyle labelStyle;
private GUIStyle tooltipStyle;
private class Member {
public MemberInfo member;
public readonly PlayModeVisibility visibility;
public readonly bool isEditingSupported;
private readonly object parent;
public bool supportsExpansion;
public bool expanded;
public Member parentMember;
public int depth;
public string niceName;
public string name;
public bool IsChild => parentMember != null;
public bool IsParentExpanded => parentMember.expanded;
public Member(MemberInfo member,
PlayModeVisibility visibility,
bool isEditingSupported,
object parent,
int depth,
Member parentMember) {
this.member = member;
this.visibility = visibility;
this.isEditingSupported = isEditingSupported;
this.parent = parent;
this.parentMember = parentMember;
this.depth = depth;
name = member.Name;
niceName = ObjectNames.NicifyVariableName(name);
}
public bool TryGetValueAndData(out object value) {
switch (member) {
case FieldInfo fieldInfo:
value = fieldInfo.GetValue(parent);
return true;
case PropertyInfo propertyInfo:
value = propertyInfo.GetValue(parent);
return true;
default:
value = null;
return false;
}
}
public Type GetValueType() {
return member switch {
FieldInfo fieldInfo => fieldInfo.FieldType,
PropertyInfo propertyInfo => propertyInfo.PropertyType,
_ => throw new NotSupportedException()
};
}
public void SetValue(object value) {
switch (member) {
case FieldInfo fieldInfo:
fieldInfo.SetValue(parent, value);
break;
case PropertyInfo propertyInfo:
propertyInfo.SetValue(parent, value);
break;
}
}
}
public bool HasValues => showMembers.Count > 0;
public ShowInInspectorAttributeHandler(UnityObject target, SerializedObject serializedObject) {
this.target = target;
this.serializedObject = serializedObject;
showMembers = new();
}
public void Update() {
serializedObject.Update();
Setup();
}
private void Setup() {
if (_initialized) {
return;
}
showMembers.Clear();
int fieldCount = EditorTypes.GetFields(target, out List<FieldInfo> objectFields);
for (int i = 0; i < fieldCount; i++) {
var showAttribute = Attribute.GetCustomAttribute(objectFields[i], typeof(ShowInInspectorAttribute)) as ShowInInspectorAttribute;
if (showAttribute == null) {
continue;
}
SetupMember(objectFields[i], objectFields[i].GetValue(target), showAttribute.Visibility, showAttribute.isEdittingSupported, target);
}
int propertiesCount = EditorTypes.GetProperties(target, out List<PropertyInfo> objectProperties);
for (int i = 0; i < propertiesCount; i++) {
var showAttribute = Attribute.GetCustomAttribute(objectProperties[i], typeof(ShowInInspectorAttribute)) as ShowInInspectorAttribute;
if (showAttribute == null) {
continue;
}
SetupMember(objectProperties[i], objectProperties[i].GetValue(target), showAttribute.Visibility, showAttribute.isEdittingSupported, target);
}
_initialized = true;
}
private void SetupMember(MemberInfo memberInfo,
object memberValue,
PlayModeVisibility visibility,
bool isEditingSupported,
object parent,
int depth = 0,
Member parentMember = null) {
Member member = new(memberInfo, visibility, isEditingSupported, parent, depth, parentMember);
if (depth >= MAX_DEPTH) {
return;
}
showMembers.Add(member);
if (memberValue != null && CanValueExpand(memberValue)) {
Type memberType = memberValue.GetType();
foreach (FieldInfo fieldInfo in memberType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
ShowInInspectorAttribute attribute = fieldInfo.GetCustomAttribute<ShowInInspectorAttribute>();
if (attribute != null) {
SetupMember(fieldInfo, fieldInfo.GetValue(memberValue), attribute.Visibility, attribute.isEdittingSupported, memberValue, depth + 1, member);
member.supportsExpansion = true;
}
}
foreach (PropertyInfo propertyInfo in memberType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
ShowInInspectorAttribute attribute = propertyInfo.GetCustomAttribute<ShowInInspectorAttribute>();
if (attribute != null) {
SetupMember(propertyInfo, propertyInfo.GetValue(memberValue), attribute.Visibility, attribute.isEdittingSupported, memberValue, depth + 1, member);
member.supportsExpansion = true;
}
}
}
}
private bool TryGetValueTypeFromEnumerable(object value, out Type type) {
Type[] arguments = value.GetType().GenericTypeArguments;
if (arguments.Length > 0) {
if (value is IDictionary) {
type = value.GetType().GenericTypeArguments[1]; // value
return true;
}
else if (value is IEnumerable) {
type = value.GetType().GenericTypeArguments[0];
return true;
}
}
type = null;
return false;
}
public void OnInspectorGUI(Editor editor) {
labelStyle ??= new GUIStyle(EditorStyles.label) {
alignment = TextAnchor.UpperLeft,
padding = new RectOffset(0, 0, 2, 2)
};
tooltipStyle ??= new GUIStyle(EditorStyles.miniTextField) {
fixedHeight = 0f,
stretchHeight = true,
wordWrap = true,
stretchWidth = false,
imagePosition = ImagePosition.TextOnly,
clipping = TextClipping.Overflow,
border = new RectOffset(0, 0, 0, 0)
};
int indentLevel = EditorGUI.indentLevel;
foreach (Member entry in showMembers) {
if (entry.visibility.IsVisible() == false || (entry.IsChild && entry.IsParentExpanded == false)) {
continue;
}
Type valueType = entry.GetValueType();
string stringValue = "";
object value = null;
EditorGUI.indentLevel = indentLevel + entry.depth;
if (entry.TryGetValueAndData(out value)) {
if (value is IList list) {
stringValue = list.ListToString(true);
}
else if (value is IEnumerable enumerable) {
stringValue = enumerable.EnumerableToString(true);
}
else {
stringValue = value?.ToString();
}
}
else {
stringValue = "<NULL>";
}
string labelName = entry.niceName;
if (entry.supportsExpansion) {
entry.expanded = EditorGUILayout.Foldout(entry.expanded, labelName, true);
}
else {
bool isGUIEnabled = GUI.enabled;
bool allowEdit = entry.isEditingSupported;
GUI.enabled = allowEdit;
Rect entryRect;
if (value is IEnumerable enumerable && TryGetValueTypeFromEnumerable(value, out Type enumValueType)) {
string labelText = $"{labelName}";
if (value is ICollection collection) {
labelText += $" (Count={collection.Count})";
}
string collectionInfo = $"({value.GetType().Name}<{enumValueType.Name}>)";
GUILayout.BeginHorizontal();
entry.expanded = EditorGUILayout.Foldout(entry.expanded, labelText, false);
GUILayout.FlexibleSpace();
GUILayout.Label(collectionInfo, EditorStyles.miniLabel);
GUILayout.EndHorizontal();
entryRect = GUILayoutUtility.GetLastRect();
if (entry.expanded) {
EditorGUI.indentLevel++;
int counter = 0;
if (enumerable is IDictionary dictionary) {
foreach (DictionaryEntry keyValuePair in dictionary) {
object newValue = InspectorGUIUtility.DrawField(keyValuePair.Key.ToString(), enumValueType, keyValuePair.Value);
if (newValue != value && allowEdit) {
entry.SetValue(newValue);
}
}
}
else {
foreach (var item in enumerable) {
object newValue = InspectorGUIUtility.DrawField((counter++).ToString(), enumValueType, item);
if (newValue != value && allowEdit) {
entry.SetValue(newValue);
}
}
}
EditorGUI.indentLevel--;
}
}
else {
object newValue = InspectorGUIUtility.DrawField(labelName, valueType, value);
entryRect = GUILayoutUtility.GetLastRect();
if (newValue != value && allowEdit) {
entry.SetValue(newValue);
}
}
GUI.enabled = isGUIEnabled;
if (entryRect.Contains(Event.current.mousePosition)) {
Vector2 tooltipPosition = Event.current.mousePosition;
float width = entryRect.width - (tooltipPosition.x - entryRect.x);
tooltipStyle.fixedWidth = width;
var tooltipContent = new GUIContent(stringValue, stringValue);
float height = tooltipStyle.CalcHeight(tooltipContent, width);
Vector2 tooltipSize = new(width, height);
tooltipPosition.y -= tooltipSize.y;
var tooltipRect = new Rect(tooltipPosition, tooltipSize);
GUI.Label(tooltipRect, tooltipContent, tooltipStyle);
if (Event.current.type == EventType.ContextClick) {
GenericMenu menu = new();
menu.AddItem(new GUIContent("Copy"), false, () => { EditorGUIUtility.systemCopyBuffer = stringValue; });
menu.ShowAsContext();
Event.current.Use();
}
editor.Repaint();
}
}
}
EditorGUI.indentLevel = indentLevel;
}
private static readonly Type[] NonExpandedTypes = new Type[] {
typeof(string),
typeof(decimal),
typeof(DateTime),
typeof(DateTimeOffset),
typeof(TimeSpan),
typeof(Guid)
};
// Use this to stop expansion of types we dont want, including Unity Objects - they just link to the object.
private static bool CanValueExpand(object value) {
if (value == null) {
return false;
}
Type type = value.GetType();
if (IsSimpleType(type)) {
return false;
}
if (type == typeof(UnityEngine.Object) || type.IsSubclassOf(typeof(UnityEngine.Object))) {
return false;
}
return true;
}
//https://stackoverflow.com/a/32337906/584774
private static bool IsSimpleType(Type type) {
return type.IsPrimitive || type.IsEnum || NonExpandedTypes.Contains(type) || Convert.GetTypeCode(type) != TypeCode.Object ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0]));
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39458dd0676f9c0449d334871e5e1bb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,113 @@
// ----------------------------------------------------------------------------
// Author: Kaynn, Yeo Wen Qin
// https://github.com/Kaynn-Cahya
// Date: 11/02/2019
// ----------------------------------------------------------------------------
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace InspectorToolkit
{
public class SpriteLayerAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
[CustomPropertyDrawer(typeof(SpriteLayerAttribute))]
public class SpriteLayerAttributeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.Integer)
{
if (!_checkedType) PropertyTypeWarning(property);
EditorGUI.PropertyField(position, property, label);
return;
}
var spriteLayerNames = GetSpriteLayerNames();
HandleSpriteLayerSelectionUI(position, property, label, spriteLayerNames);
}
private bool _checkedType;
private void PropertyTypeWarning(SerializedProperty property)
{
Debug.LogWarning(string.Format("Property <color=brown>{0}</color> in object <color=brown>{1}</color> is of wrong type. Expected: Int",
property.name, property.serializedObject.targetObject));
_checkedType = true;
}
private void HandleSpriteLayerSelectionUI(Rect position, SerializedProperty property, GUIContent label, string[] spriteLayerNames)
{
EditorGUI.BeginProperty(position, label, property);
// To show which sprite layer is currently selected.
int currentSpriteLayerIndex;
bool layerFound = TryGetSpriteLayerIndexFromProperty(out currentSpriteLayerIndex, spriteLayerNames, property);
if (!layerFound)
{
// Set to default layer. (Previous layer was removed)
Debug.Log(string.Format(
"Property <color=brown>{0}</color> in object <color=brown>{1}</color> is set to the default layer. Reason: previously selected layer was removed.",
property.name, property.serializedObject.targetObject));
property.intValue = 0;
currentSpriteLayerIndex = 0;
}
int selectedSpriteLayerIndex = EditorGUI.Popup(position, label.text, currentSpriteLayerIndex, spriteLayerNames);
// Change property value if user selects a new sprite layer.
if (selectedSpriteLayerIndex != currentSpriteLayerIndex)
{
property.intValue = SortingLayer.NameToID(spriteLayerNames[selectedSpriteLayerIndex]);
}
EditorGUI.EndProperty();
}
#region Util
private bool TryGetSpriteLayerIndexFromProperty(out int index, string[] spriteLayerNames, SerializedProperty property)
{
// To keep the property's value consistent, after the layers have been sorted around.
string layerName = SortingLayer.IDToName(property.intValue);
// Return the index where on it matches.
for (int i = 0; i < spriteLayerNames.Length; ++i)
{
if (spriteLayerNames[i].Equals(layerName))
{
index = i;
return true;
}
}
// The current layer was removed.
index = -1;
return false;
}
private string[] GetSpriteLayerNames()
{
string[] result = new string[SortingLayer.layers.Length];
for (int i = 0; i < result.Length; ++i)
{
result[i] = SortingLayer.layers[i].name;
}
return result;
}
#endregion
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1f05661da7d39cf418051ccfb13eb5a9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using UnityEngine;
namespace InspectorToolkit {
public class SuffixAttribute : PropertyAttribute {
public readonly string suffix;
public SuffixAttribute(string suffix) => this.suffix = suffix;
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
using UnityEditor;
[CustomPropertyDrawer(typeof(SuffixAttribute))]
public class SuffixDrawer : PropertyDrawer {
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
var suffix = new GUIContent(((SuffixAttribute)attribute).suffix);
var suffixSize = GUI.skin.label.CalcSize(suffix);
suffixSize.x += GUI.skin.label.margin.left;
var fieldPosition = position;
fieldPosition.width -= suffixSize.x;
var suffixPosition = position;
suffixPosition.x += fieldPosition.width + GUI.skin.label.margin.left;
suffixPosition.width = suffixSize.x;
EditorGUI.PropertyField(fieldPosition, property, label);
var enabled = GUI.enabled;
GUI.enabled = false;
GUI.Label(suffixPosition, suffix);
GUI.enabled = enabled;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 292774cd51b5c464ca08b8bc1805e662
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
// ----------------------------------------------------------------------------
// Author: Kaynn, Yeo Wen Qin
// https://github.com/Kaynn-Cahya
// Date: 11/02/2019
// ----------------------------------------------------------------------------
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace InspectorToolkit
{
public class TagAttribute : PropertyAttribute
{
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal
{
[CustomPropertyDrawer(typeof(TagAttribute))]
public class TagAttributeDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.String)
{
if (!_checked) Warning(property);
EditorGUI.PropertyField(position, property, label);
return;
}
property.stringValue = EditorGUI.TagField(position, label, property.stringValue);
}
private bool _checked;
private void Warning(SerializedProperty property)
{
Debug.LogWarning(string.Format("Property <color=brown>{0}</color> in object <color=brown>{1}</color> is of wrong type. Expected: String",
property.name, property.serializedObject.targetObject));
_checked = true;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4112d636c944a774aae43810656bde88
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,132 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace InspectorToolkit {
public class UniformVector3Attribute : PropertyAttribute {
public UniformVector3Attribute() {
}
}
}
#if UNITY_EDITOR
namespace InspectorToolkit.Internal {
[CustomPropertyDrawer(typeof(UniformVector3Attribute))]
public class UniformVector3AttributeDrawer : PropertyDrawer {
private GUIContent linkedIconContent;
private GUIContent unlinkedIconContent;
private GUIStyle linkButtonStyle;
private const int CHILD_COUNT = 3;
private const float LABEL_WIDTH = 13f;
private const float COMBINED_LABEL_WIDTH = 26f;
private const float INDENT_WIDTH = 15f;
private const float CHILD_HORIZONTAL_SPACING = 4f;
private static readonly string[] childPropertyNames = new string[] { "x", "y", "z" };
private static bool IsCompactLayout => Screen.width <= 333f;
private static bool IsNoLabelLayout => Screen.width <= 240f;
private enum LinkState {
Unknown,
Linked,
Unlinked
}
private LinkState linkState = LinkState.Unknown;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
if(IsCompactLayout) { // Unity Vector2 + Vector3 go multi-line at this point
return EditorGUIUtility.singleLineHeight * 2f + EditorGUIUtility.standardVerticalSpacing;
}
return EditorGUIUtility.singleLineHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
if(property.propertyType != SerializedPropertyType.Vector3) {
GUI.Label(position, "Error, SerializedProperty must be 'Vector3' type");
return;
}
if(linkedIconContent == null || unlinkedIconContent == null || linkButtonStyle == null) {
linkedIconContent = EditorGUIUtility.IconContent(EditorGUIUtility.isProSkin ? "d_Linked" : "Linked");
unlinkedIconContent = EditorGUIUtility.IconContent(EditorGUIUtility.isProSkin ? "d_Unlinked" : "Unlinked");
linkButtonStyle = new GUIStyle(GUI.skin.button);
linkButtonStyle.padding = new RectOffset(1, 1, 0, 0);
linkButtonStyle.imagePosition = ImagePosition.ImageOnly;
}
if(linkState == LinkState.Unknown) {
linkState = GetLinkState(property);
}
EditorGUI.BeginProperty(position, label, property);
if(IsCompactLayout) {
EditorGUIUtility.labelWidth = position.width;
}
Rect labelRect = new Rect(position) { height = EditorGUIUtility.singleLineHeight };
Rect fieldRect = EditorGUI.PrefixLabel(labelRect, label);
fieldRect.height = EditorGUIUtility.singleLineHeight;
float height = Mathf.Min(20f, position.height) - 2f;
Rect buttonRect = new Rect(fieldRect);
buttonRect.width = 18f;
buttonRect.height = height;
buttonRect.y += 1f;
buttonRect.x -= buttonRect.width + 2f;
if(GUI.Button(buttonRect, linkState == LinkState.Linked ? linkedIconContent : unlinkedIconContent, linkButtonStyle)) {
linkState = linkState == LinkState.Linked ? LinkState.Unlinked : LinkState.Linked;
}
if(IsCompactLayout) {
fieldRect.y = labelRect.yMax + EditorGUIUtility.standardVerticalSpacing;
fieldRect.x = labelRect.x + (IsNoLabelLayout ? 0f : INDENT_WIDTH); // 15 = single indent
fieldRect.width = position.xMax - fieldRect.xMin;
}
if(linkState == LinkState.Linked) {
EditorGUIUtility.labelWidth = IsNoLabelLayout ? 0.01f : COMBINED_LABEL_WIDTH;
SerializedProperty childProperty = property.FindPropertyRelative(childPropertyNames[0]);
EditorGUI.BeginChangeCheck();
float newValue = EditorGUI.FloatField(fieldRect, "XYZ", childProperty.floatValue);
if(EditorGUI.EndChangeCheck()) {
for(int i = 0; i < CHILD_COUNT; i++) {
SerializedProperty otherChildProperty = property.FindPropertyRelative(childPropertyNames[i]);
otherChildProperty.floatValue = newValue;
}
}
}
else {
EditorGUIUtility.labelWidth = IsNoLabelLayout ? 0.01f : LABEL_WIDTH;
float childSpacing = IsNoLabelLayout ? CHILD_HORIZONTAL_SPACING / 2f : CHILD_HORIZONTAL_SPACING;
float childWidth = ((fieldRect.width + childSpacing) / CHILD_COUNT) - childSpacing;
for(int i = 0; i < CHILD_COUNT; i++) {
SerializedProperty childProperty = property.FindPropertyRelative(childPropertyNames[i]);
Rect childRect = new Rect(fieldRect);
childRect.width = childWidth; // add spacing
childRect.x += i * (childWidth + childSpacing);
EditorGUI.PropertyField(childRect, childProperty);
}
}
EditorGUI.EndProperty();
}
private LinkState GetLinkState(SerializedProperty property) {
float value0 = property.FindPropertyRelative(childPropertyNames[0]).floatValue;
for(int i = 1; i < CHILD_COUNT; i++) {
SerializedProperty childProperty = property.FindPropertyRelative(childPropertyNames[i]);
if(Mathf.Approximately(value0, childProperty.floatValue) == false) {
return LinkState.Unlinked;
}
}
return LinkState.Linked;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 140e2c93656440341b95fd7ae6396b7f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6b11d21170b13bc458c03fd935af0f63
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dd2b08b6aac506f4eabedf9dff5a11cb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,580 @@
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace InspectorToolkit.EditorTools {
public static class MyGUI {
#region Colors
public static class Colors {
public static readonly Color Red = new Color(.8f, .6f, .6f);
public static readonly Color Green = new Color(.4f, .6f, .4f);
public static readonly Color Blue = new Color(.6f, .6f, .8f);
public static readonly Color Gray = new Color(.3f, .3f, .3f);
public static readonly Color Yellow = new Color(.8f, .8f, .2f, .6f);
public static readonly Color Brown = new Color(.7f, .5f, .2f, .6f);
}
#endregion
#region Characters
public static class Characters {
public const string ArrowUp = "▲";
public const string ArrowDown = "▼";
public const string ArrowLeft = "◀";
public const string ArrowRight = "▶";
public const string ArrowLeftLight = "←";
public const string ArrowRightLight = "→";
public const string ArrowTopRightLight = "↘";
public const string Check = "✓";
public const string Cross = "×";
}
#endregion
#region Editor Icons
public static class EditorIcons {
public static GUIContent Plus => EditorGUIUtility.IconContent("Toolbar Plus");
public static GUIContent Minus => EditorGUIUtility.IconContent("Toolbar Minus");
public static GUIContent Refresh => EditorGUIUtility.IconContent("Refresh");
public static GUIContent ConsoleInfo => EditorGUIUtility.IconContent("console.infoicon.sml");
public static GUIContent ConsoleWarning => EditorGUIUtility.IconContent("console.warnicon.sml");
public static GUIContent ConsoleError => EditorGUIUtility.IconContent("console.erroricon.sml");
public static GUIContent Check => EditorGUIUtility.IconContent("FilterSelectedOnly");
public static GUIContent Cross => EditorGUIUtility.IconContent("d_winbtn_win_close");
public static GUIContent Dropdown => EditorGUIUtility.IconContent("icon dropdown");
public static GUIContent EyeOn => EditorGUIUtility.IconContent("d_VisibilityOn");
public static GUIContent EyeOff => EditorGUIUtility.IconContent("d_VisibilityOff");
public static GUIContent Zoom => EditorGUIUtility.IconContent("d_ViewToolZoom");
public static GUIContent Help => EditorGUIUtility.IconContent("_Help");
public static GUIContent Favourite => EditorGUIUtility.IconContent("Favorite");
public static GUIContent Label => EditorGUIUtility.IconContent("FilterByLabel");
public static GUIContent Settings => EditorGUIUtility.IconContent("d_Settings");
public static GUIContent SettingsPopup => EditorGUIUtility.IconContent("_Popup");
public static GUIContent SettingsMixer => EditorGUIUtility.IconContent("Audio Mixer");
public static GUIContent Circle => EditorGUIUtility.IconContent("TestNormal");
public static GUIContent CircleYellow => EditorGUIUtility.IconContent("TestInconclusive");
public static GUIContent CircleDotted => EditorGUIUtility.IconContent("TestIgnored");
public static GUIContent CircleRed => EditorGUIUtility.IconContent("TestFailed");
}
#endregion
#region Editor Styles
/// <summary>
/// HelpBox with centered text alignment
/// </summary>
public static GUIStyle HelpBoxStyle {
get {
if(_helpBoxStyle != null) return _helpBoxStyle;
_helpBoxStyle = new GUIStyle(GUI.skin.GetStyle("HelpBox"));
_helpBoxStyle.alignment = TextAnchor.MiddleCenter;
return _helpBoxStyle;
}
}
private static GUIStyle _helpBoxStyle;
/// <summary>
/// ToolbarButtonStyle is not resizable by default
/// </summary>
public static GUIStyle ResizableToolbarButtonStyle {
get {
if(_resizableToolbarButtonStyle != null) return _resizableToolbarButtonStyle;
_resizableToolbarButtonStyle = new GUIStyle(EditorStyles.toolbarButton);
_resizableToolbarButtonStyle.fixedHeight = 0;
return _resizableToolbarButtonStyle;
}
}
private static GUIStyle _resizableToolbarButtonStyle;
/// <summary>
/// ToolbarButton with Border, Margin and Padding set to 0
/// </summary>
public static GUIStyle BorderlessToolbarButtonStyle {
get {
if(_borderlessToolbarButton != null) return _borderlessToolbarButton;
_borderlessToolbarButton = new GUIStyle(ResizableToolbarButtonStyle);
var emptyOffset = new RectOffset();
_borderlessToolbarButton.border = emptyOffset;
_borderlessToolbarButton.margin = emptyOffset;
_borderlessToolbarButton.padding = emptyOffset;
return _borderlessToolbarButton;
}
}
private static GUIStyle _borderlessToolbarButton;
/// <summary>
/// Style for a toggle button
/// </summary>
public static GUIStyle ButtonToggledStyle(bool toggled) {
if(!toggled) return EditorStyles.miniButton;
if(_buttonToggledStyle != null) return _buttonToggledStyle;
_buttonToggledStyle = new GUIStyle(EditorStyles.miniButton);
_buttonToggledStyle.normal.background = _buttonToggledStyle.active.background;
return _buttonToggledStyle;
}
private static GUIStyle _buttonToggledStyle;
/// <summary>
/// MiniButtonLeft/Middle/Right style based on array index
/// </summary>
/// <param name="index"></param>
/// <param name="collection"></param>
/// <returns></returns>
public static GUIStyle MiniButtonStyle(int index, Array collection) {
if(collection.Length == 1) return EditorStyles.miniButton;
if(index == 0) return EditorStyles.miniButtonLeft;
if(index == collection.Length - 1) return EditorStyles.miniButtonRight;
return EditorStyles.miniButtonMid;
}
#endregion
#region Draw Coloured lines and boxes
/// <summary>
/// Draw Separator within GuiLayout
/// </summary>
public static void Separator() {
var color = GUI.color;
EditorGUILayout.Space();
var spaceRect = EditorGUILayout.GetControlRect();
var separatorRectPosition = new Vector2(spaceRect.position.x, spaceRect.position.y + spaceRect.height / 2);
var separatorRect = new Rect(separatorRectPosition, new Vector2(spaceRect.width, 1));
GUI.color = Color.white;
GUI.Box(separatorRect, GUIContent.none);
GUI.color = color;
}
/// <summary>
/// Draw Line within GUILayout
/// </summary>
public static void DrawLine(Color color, bool withSpace = false) {
if(withSpace) EditorGUILayout.Space();
var defaultBackgroundColor = GUI.backgroundColor;
GUI.backgroundColor = color;
GUILayout.Box("", GUILayout.ExpandWidth(true), GUILayout.Height(1));
GUI.backgroundColor = defaultBackgroundColor;
if(withSpace) EditorGUILayout.Space();
}
/// <summary>
/// Draw line within Rect and get Rect back with offset
/// </summary>
public static Rect DrawLine(Color color, Rect rect) {
var h = rect.height;
var defaultBackgroundColor = GUI.backgroundColor;
GUI.backgroundColor = color;
rect.y += 5;
rect.height = 1;
GUI.Box(rect, "");
rect.y += 5;
GUI.backgroundColor = defaultBackgroundColor;
rect.height = h;
return rect;
}
/// <summary>
/// Draw Rect filled with Color
/// </summary>
public static void DrawColouredRect(Rect rect, Color color) {
var defaultBackgroundColor = GUI.backgroundColor;
GUI.backgroundColor = color;
GUI.Box(rect, "");
GUI.backgroundColor = defaultBackgroundColor;
}
/// <summary>
/// Draw background Line within GUILayout
/// </summary>
public static void DrawBackgroundLine(Color color, int yOffset = 0) {
var defColor = GUI.color;
GUI.color = color;
var rect = GUILayoutUtility.GetLastRect();
rect.center = new Vector2(rect.center.x, rect.center.y + 6 + yOffset);
rect.height = 17;
GUI.DrawTexture(rect, EditorGUIUtility.whiteTexture);
GUI.color = defColor;
}
/// <summary>
/// Draw background Line of height
/// </summary>
public static void DrawBackgroundBox(Color color, int height, int yOffset = 0) {
var defColor = GUI.color;
GUI.color = color;
var rect = GUILayoutUtility.GetLastRect();
rect.center = new Vector2(rect.center.x, rect.center.y + 6 + yOffset);
rect.height = height;
GUI.DrawTexture(rect, EditorGUIUtility.whiteTexture);
GUI.color = defColor;
}
#endregion
#region Property Field
/// <summary>
/// Make a field for SerializedProperty and check if changed
/// </summary>
public static bool PropertyField(SerializedProperty property, params GUILayoutOption[] options) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(property, options);
return EditorGUI.EndChangeCheck();
}
/// <summary>
/// Make a field for SerializedProperty and check if changed
/// </summary>
public static bool PropertyField(SerializedProperty property, GUIContent label, params GUILayoutOption[] options) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(property, label, options);
return EditorGUI.EndChangeCheck();
}
#endregion
#region SerializedProperty Manipulation Buttons
/// <summary>
/// Move array element at index on index-1 position
/// </summary>
public static void MoveArrayElementUpButton(this SerializedProperty property, int elementAt) {
var guiState = GUI.enabled;
if(elementAt <= 0) GUI.enabled = false;
if(UpButton()) {
EditorApplication.delayCall += () => {
property.MoveArrayElement(elementAt, elementAt - 1);
property.serializedObject.ApplyModifiedProperties();
};
}
GUI.enabled = guiState;
}
/// <summary>
/// Move array element at index on index+1 position
/// </summary>
public static void MoveArrayElementDownButton(this SerializedProperty property, int elementAt) {
var guiState = GUI.enabled;
if(elementAt >= property.arraySize - 1) GUI.enabled = false;
if(DownButton()) {
EditorApplication.delayCall += () => {
property.MoveArrayElement(elementAt, elementAt + 1);
property.serializedObject.ApplyModifiedProperties();
};
}
GUI.enabled = guiState;
}
/// <summary>
/// Add new array element to property and get it as SerializedProperty
/// </summary>
public static SerializedProperty NewArrayElementButton(this SerializedProperty property) {
if(PlusButton()) {
return property.NewElement();
}
return null;
}
/// <summary>
/// Remove array element at index
/// </summary>
public static void RemoveElementButton(this SerializedProperty property, int elementAt) {
var guiState = GUI.enabled;
if(elementAt < 0 || elementAt >= property.arraySize - 1) GUI.enabled = false;
if(CrossButton()) {
EditorApplication.delayCall += () => {
property.DeleteArrayElementAtIndex(elementAt);
property.serializedObject.ApplyModifiedProperties();
};
}
GUI.enabled = guiState;
}
#endregion
#region Drop Area
/// <summary>
/// Drag-and-Drop Area to catch objects of specific type
/// </summary>
/// <typeparam name="T">Asset type to catch</typeparam>
/// <param name="areaText">Label to display</param>
/// <param name="height">Height of the Drop Area</param>
/// <param name="allowExternal">Allow to drag external files and import as unity assets</param>
/// <param name="externalImportFolder">Path relative to Assets folder</param>
/// <returns>Received objects. Null if none received</returns>
public static T[] DropArea<T>(string areaText, float height, bool allowExternal = false,
string externalImportFolder = null) where T : Object {
Event currentEvent = Event.current;
Rect dropArea = GUILayoutUtility.GetRect(0.0f, height, GUILayout.ExpandWidth(true));
var style = new GUIStyle(GUI.skin.box);
style.alignment = TextAnchor.MiddleCenter;
GUI.Box(dropArea, areaText, style);
bool dragEvent = currentEvent.type == EventType.DragUpdated || currentEvent.type == EventType.DragPerform;
if(!dragEvent) return null;
bool overDropArea = dropArea.Contains(currentEvent.mousePosition);
if(!overDropArea) return null;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if(currentEvent.type != EventType.DragPerform) return null;
DragAndDrop.AcceptDrag();
Event.current.Use();
List<T> result = new List<T>();
bool anyExternal = DragAndDrop.paths.Length > 0 &&
DragAndDrop.paths.Length > DragAndDrop.objectReferences.Length;
if(allowExternal && anyExternal) {
var folderToLoad = "/";
if(!string.IsNullOrEmpty(externalImportFolder)) {
folderToLoad = "/" + externalImportFolder.Replace("Assets/", "").Trim('/', '\\') + "/";
}
List<string> importedFiles = new List<string>();
foreach(string externalPath in DragAndDrop.paths) {
if(externalPath.Length == 0) continue;
try {
var filename = Path.GetFileName(externalPath);
var relativePath = folderToLoad + filename;
Directory.CreateDirectory(Application.dataPath + folderToLoad);
FileUtil.CopyFileOrDirectory(externalPath, Application.dataPath + relativePath);
importedFiles.Add("Assets" + relativePath);
}
catch(Exception ex) {
Debug.LogException(ex);
}
}
AssetDatabase.Refresh();
foreach(var importedFile in importedFiles) {
var asset = AssetDatabase.LoadAssetAtPath<T>(importedFile);
if(asset != null) {
result.Add(asset);
Debug.Log("Asset imported at path: " + importedFile);
}
else AssetDatabase.DeleteAsset(importedFile);
}
}
else {
foreach(Object dragged in DragAndDrop.objectReferences) {
var validObject = dragged as T ?? AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GetAssetPath(dragged));
if(validObject != null) result.Add(validObject);
}
}
return result.Count > 0 ? result.OrderBy(o => o.name).ToArray() : null;
}
/// <summary>
/// Drag-and-Drop Area to get paths of received objects
/// </summary>
/// <param name="areaText">Label to display</param>
/// <param name="height">Height of the Drop Area</param>
/// <returns>Received paths</returns>
public static string[] DropAreaPaths(string areaText, float height) {
Event currentEvent = Event.current;
Rect dropArea = GUILayoutUtility.GetRect(0.0f, height, GUILayout.ExpandWidth(true));
var style = new GUIStyle(GUI.skin.box);
style.alignment = TextAnchor.MiddleCenter;
GUI.Box(dropArea, areaText, style);
bool dragEvent = currentEvent.type == EventType.DragUpdated || currentEvent.type == EventType.DragPerform;
if(!dragEvent) return null;
bool overDropArea = dropArea.Contains(currentEvent.mousePosition);
if(!overDropArea) return null;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if(currentEvent.type != EventType.DragPerform) return null;
DragAndDrop.AcceptDrag();
Event.current.Use();
return DragAndDrop.paths;
}
#endregion
#region Browse Buttons
/// <summary>
/// Creates a filepath textfield with a browse button. Opens the open file panel.
/// </summary>
public static string BrowsFileLabel(string name, float labelWidth, string path, string extension) {
EditorGUILayout.BeginHorizontal();
GUILayout.Label(name, GUILayout.MaxWidth(labelWidth));
string filepath = EditorGUILayout.TextField(path);
if(GUILayout.Button("Browse")) {
filepath = EditorUtility.OpenFilePanel(name, path, extension);
}
EditorGUILayout.EndHorizontal();
return filepath;
}
/// <summary>
/// Creates a folder path textfield with a browse button. Opens the save folder panel.
/// </summary>
public static string BrowseFolderLabel(string name, float labelWidth, string path) {
EditorGUILayout.BeginHorizontal();
string filepath = EditorGUILayout.TextField(name, path, GUILayout.MaxWidth(labelWidth));
if(GUILayout.Button("Browse", GUILayout.MaxWidth(60))) {
filepath = EditorUtility.SaveFolderPanel(name, path, "Folder");
}
EditorGUILayout.EndHorizontal();
return filepath;
}
#endregion
#region Predefined Buttons
/// <summary>
/// Display Button with ArrowUI
/// </summary>
public static bool UpButton() {
return GUILayout.Button(Characters.ArrowUp, EditorStyles.toolbarButton, GUILayout.Width(18));
}
public static bool DownButton() {
return GUILayout.Button(Characters.ArrowDown, EditorStyles.toolbarButton, GUILayout.Width(18));
}
public static bool PlusButton() {
return GUILayout.Button("+", EditorStyles.toolbarButton, GUILayout.Width(18));
}
public static bool CrossButton() {
return GUILayout.Button(Characters.Cross, EditorStyles.toolbarButton, GUILayout.Width(18));
}
#endregion
/// <summary>
/// Creates a toolbar that is filled in from an Enum. Useful for setting tool modes.
/// </summary>
public static Enum EnumToolbar(Enum selected) {
string[] toolbar = Enum.GetNames(selected.GetType());
Array values = Enum.GetValues(selected.GetType());
for(int i = 0; i < toolbar.Length; i++) {
string toolName = toolbar[i];
toolName = toolName.Replace("_", " ");
toolbar[i] = toolName;
}
int selectedIndex = 0;
while(selectedIndex < values.Length) {
if(selected.ToString() == values.GetValue(selectedIndex).ToString()) {
break;
}
selectedIndex++;
}
selectedIndex = GUILayout.Toolbar(selectedIndex, toolbar);
return (Enum)values.GetValue(selectedIndex);
}
/// <summary>
/// Creates an array foldout like in inspectors
/// </summary>
public static string[] ArrayFoldout(string label, string[] array, ref bool foldout) {
EditorGUILayout.BeginVertical();
EditorGUIUtility.labelWidth = 0;
EditorGUIUtility.fieldWidth = 0;
foldout = EditorGUILayout.Foldout(foldout, label);
string[] newArray = array;
if(foldout) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.Space();
EditorGUILayout.BeginVertical();
int arraySize = EditorGUILayout.IntField("Size", array.Length);
if(arraySize != array.Length)
newArray = new string[arraySize];
for(int i = 0; i < arraySize; i++) {
string entry = "";
if(i < array.Length)
entry = array[i];
newArray[i] = EditorGUILayout.TextField("Element " + i, entry);
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
return newArray;
}
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More