diff --git a/.github/README.md b/.github/README.md index 786a176..43790e1 100644 --- a/.github/README.md +++ b/.github/README.md @@ -52,8 +52,6 @@ Note that these connections are *one-directional*, meaning that assigning the in ![screenshot](images/img3.png) -Be aware that the changes made to a skin via the Unity Inspector might not be reflected to the UI immediately. To reflect these changes to the UI, you may have to click the *cog icon* while the skin is selected and select **Refresh UI**. - ### E.1. INSPECTOR ![screenshot](images/img4.png) @@ -65,6 +63,8 @@ RuntimeInspector works similar to the editor Inspector. It can expose commonly u - **Expose Properties**: determines which properties of the inspected object should be exposed - **Array Indices Start At One**: when enabled, exposed arrays and lists start their indices at 1 instead of 0 (just a visual change) - **Use Title Case Naming**: when enabled, variable names are displayed in title case format (e.g. *m_myVariable* becomes *My Variable*) +- **Show Add Component Button**: when enabled, *Add Component* button will appear while inspecting a GameObject +- **Show Remove Component Button**: when enabled, *Remove Component* button will appear under inspected components - **Show Tooltips**: when enabled, hovering over a variable's name for a while will show a tooltip displaying the variable's name. Can be useful for variables whose names are partially obscured - **Tooltip Delay**: determines how long the cursor should remain static over a variable's name before the tooltip appears. Has no effect if *Show Tooltips* is disabled - **Nest Limit**: imagine exposing a linked list. This variable defines how many nodes you can expose in the inspector starting from the initial node until the inspector stops exposing any further nodes @@ -188,7 +188,42 @@ public void DeleteAllPseudoScenes(); This helper component allows you to add an object's children to a pseudo-scene in the hierarchy. When a child is added to or removed from the object, this component refreshes the pseudo-scene automatically. If **HideOnDisable** is enabled, the object's children are removed from the pseudo-scene when the object is disabled. -### F.2. DRAGGED REFERENCE ITEMS +### F.2. COLOR PICKER + +You can access the built-in color picker via **ColorPicker.Instance** and then present it with the following function: + +```csharp +public void Show( ColorWheelControl.OnColorChangedDelegate onColorChanged, ColorWheelControl.OnColorChangedDelegate onColorConfirmed, Color initialColor, Canvas referenceCanvas ); +``` + +- **onColorChanged**: invoked regularly as the user changes the color. `ColorWheelControl.OnColorChangedDelegate` takes a *Color32* parameter +- **onColorConfirmed**: invoked when user submits the color via *OK* button +- **initialColor**: the initial value of the color picker +- **referenceCanvas**: if assigned, the reference canvas' properties will be copied to the color picker canvas + +You can change the color picker's visual appearance by assigning a *UISkin* to its **Skin** property. + +### F.3. OBJECT REFERENCE PICKER + +You can access the built-in object reference picker via **ObjectReferencePicker.Instance** and then present it with the following function: + +```csharp +public void Show( ReferenceCallback onReferenceChanged, ReferenceCallback onSelectionConfirmed, NameGetter referenceNameGetter, NameGetter referenceDisplayNameGetter, object[] references, object initialReference, bool includeNullReference, string title, Canvas referenceCanvas ); +``` + +- **onReferenceChanged**: invoked when the user selects a reference from the list. `ReferenceCallback` takes an *object* parameter +- **onSelectionConfirmed**: invoked when user submits the selected reference via *OK* button +- **referenceNameGetter**: `NameGetter` takes an *object* parameter and returns that object's name as string. The passed function will be used to sort the references list and compare the references' names with the search string +- **referenceDisplayNameGetter**: the passed function will be used to get display names for the references. Usually, the same function is passed to this parameter and the *referenceNameGetter* parameter +- **references**: array of references to pick from +- **initialReference**: initially selected reference +- **includeNullReference**: is set to *true*, a null reference option will be added to the top of the references list +- **title**: title of the object reference picker +- **referenceCanvas**: if assigned, the reference canvas' properties will be copied to the object reference picker canvas + +You can change the object reference picker's visual appearance by assigning a *UISkin* to its **Skin** property. + +### F.4. DRAGGED REFERENCE ITEMS In section **E.2**, it is mentioned that you can drag&drop objects from the hierarchy to the variables in the inspector to assign these objects to those variables. However, you are not limited with just hierarchy. There are two helper components that you can use to create dragged reference items for other objects: @@ -213,7 +248,7 @@ You can also use your own scripts to create dragged reference items by calling t public static DraggedReferenceItem CreateDraggedReferenceItem( Object reference, PointerEventData draggingPointer, UISkin skin = null ); ``` -### F.3. CUSTOM DRAWERS (EDITORS) +## G. CUSTOM DRAWERS (EDITORS) **NOTE:** if you just want to hide some fields/properties from the RuntimeInspector, simply use **Settings** asset's **Hidden Variables** list (mentioned in section **E.1**). @@ -222,7 +257,7 @@ You can introduce your own custom drawers to RuntimeInspector. These drawers wil - creating a drawer prefab and adding it to the **Settings** asset mentioned in section **E.1**. Each drawer extends from **InspectorField** base class. There is also an **ExpandableInspectorField** abstract class that allows you to create an expandable/collapsable drawer like arrays. Lastly, extending **ObjectReferenceField** class allows you to create drawers that can be assigned values via the reference picker or via drag&drop. This option provides the most flexibility because you'll be able to customize the drawer prefab as you wish. The downside is, you'll have to create a prefab asset and manually add it to RuntimeInspector's **Settings** asset. All built-in drawers use this method; they can be as simple as **BoolField** and **TransformField**, or as complex as **BoundsField**, **GameObjectField** and **ArrayField** - extending **IRuntimeInspectorCustomEditor** interface and decorating the class/struct with **RuntimeInspectorCustomEditor** attribute. This method is simpler because you won't have to create a prefab asset for the drawer. Created custom drawer will internally be used by *ObjectField* to populate its sub-drawers. This option should be sufficient for most use-cases. But imagine that you want to create a custom drawer for Matrix4x4 where the cells are displayed in a 4x4 grid. In this case, you must use the first method because you'll need a custom prefab with 16 InputFields organized in a 4x4 grid for it. But if you can represent the custom drawer you have in mind by using a combination of built-in drawers, then this second option should suffice -#### F.3.1. InspectorField +### G.1. InspectorField To have a standardized visual appearance across all the drawers, there are some common variables for each drawer: @@ -253,7 +288,7 @@ There are some special functions on drawers that are invoked on certain circumst - **void OnDepthChanged()**: called when the *Depth* property of the drawer is changed. Here, your custom drawers must add a padding to their content from left to comply with the nesting standard. This function is also called when the *Skin* changes - **void Refresh()**: called when the value of the bound object is refreshed. Drawers must refresh the values of their UI elements here. Invoked by RuntimeInspector at every **Refresh Interval** seconds -#### F.3.2. ExpandableInspectorField +### G.2. ExpandableInspectorField Custom drawers that extend **ExpandableInspectorField** have access to the following properties: @@ -281,13 +316,11 @@ There are also some helper functions in ExpandableInspectorField to easily creat - `InspectorField CreateDrawerForVariable( MemberInfo variable, string variableName = null )`: creates a drawer for the variable that the *MemberInfo* stores. This variable must be declared inside inspected object's class/struct or one of its base classes - `InspectorField CreateDrawer( Type variableType, string variableName, Getter getter, Setter setter, bool drawObjectsAsFields = true )`: similar to the *BindTo* function with the *Getter* and *Setter* parameters, allows you to use custom functions to get and set the value of the object that the sub-drawer is bound to -If you don't want the name of the variable to be title case formatted, you can enter an empty string as the **variableName** parameter and then set the *NameRaw* property of the returned *InspectorField* object. - -#### F.3.3. ObjectReferenceField +### G.3. ObjectReferenceField Drawers that extend **ObjectReferenceField** class have access to the `void OnReferenceChanged( Object reference )` function that is called when the reference assigned to that drawer is changed. -#### F.3.4. Helper Classes +### G.4. Helper Classes **PointerEventListener**: this is a simple helper component that invokes **PointerDown** event when its UI GameObject is pressed, **PointerUp** event when it is released and **PointerClick** event when it is clicked @@ -300,7 +333,7 @@ Drawers that extend **ObjectReferenceField** class have access to the `void OnRe - **OnValueChangedDelegate OnValueSubmitted**: called when user finishes editing the value of input field. Similar to *OnValueChanged*, a function that is registered to this event should parse the **input** and return *true* only if the input is valid - **bool CacheTextOnValueChange**: determines what will happen when user stops editing the input field while its contents are invalid (i.e. its background has turned red). If this variable is set to *true*, input field's text will revert to the latest value that returned *true* for OnValueChanged. Otherwise, the text will revert to the value input field had when it was focused -#### F.3.5. RuntimeInspectorCustomEditor Attribute +### G.5. RuntimeInspectorCustomEditor Attribute To create drawers without having to create a prefab for it, you can declara a class/struct that extends **IRuntimeInspectorCustomEditor** and has one or more **RuntimeInspectorCustomEditor** attributes. @@ -341,6 +374,8 @@ public class ColliderEditor : IRuntimeInspectorCustomEditor } ``` +--- + ![screenshot](images/CustomMeshRendererEditor.png) ```csharp @@ -365,6 +400,8 @@ public class MeshRendererEditor : IRuntimeInspectorCustomEditor } ``` +--- + ![screenshot](images/CustomCameraEditor.png) ```csharp diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector.cs index eafea0f..489d561 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector.cs @@ -83,6 +83,36 @@ public bool UseTitleCaseNaming } } + [SerializeField] + private bool m_showAddComponentButton = true; + public bool ShowAddComponentButton + { + get { return m_showAddComponentButton; } + set + { + if( m_showAddComponentButton != value ) + { + m_showAddComponentButton = value; + isDirty = true; + } + } + } + + [SerializeField] + private bool m_showRemoveComponentButton = true; + public bool ShowRemoveComponentButton + { + get { return m_showRemoveComponentButton; } + set + { + if( m_showRemoveComponentButton != value ) + { + m_showRemoveComponentButton = value; + isDirty = true; + } + } + } + [SerializeField] private bool m_showTooltips; public bool ShowTooltips { get { return m_showTooltips; } } @@ -183,6 +213,9 @@ public RuntimeHierarchy ConnectedHierarchy private Canvas m_canvas; public Canvas Canvas { get { return m_canvas; } } + // Used to make sure that the scrolled content always remains within the scroll view's boundaries + private PointerEventData nullPointerEventData; + public InspectedObjectChangingDelegate OnInspectedObjectChanging; private ComponentFilterDelegate m_componentFilter; @@ -211,6 +244,7 @@ private void Initialize() drawArea = scrollView.content; m_canvas = GetComponentInParent(); + nullPointerEventData = new PointerEventData( null ); GameObject poolParentGO = GameObject.Find( POOL_OBJECT_NAME ); if( poolParentGO == null ) @@ -272,6 +306,16 @@ private void OnTransformParentChanged() m_canvas = GetComponentInParent(); } +#if UNITY_EDITOR + protected override void OnValidate() + { + base.OnValidate(); + + if( UnityEditor.EditorApplication.isPlaying ) + isDirty = true; + } +#endif + protected override void Update() { base.Update(); @@ -358,6 +402,16 @@ public void RefreshDelayed() nextRefreshTime = 0f; } + // Makes sure that scroll view's contents are within scroll view's bounds + public void EnsureScrollViewIsWithinBounds() + { + // When scrollbar is snapped to the very bottom of the scroll view, sometimes OnScroll alone doesn't work + if( scrollView.normalizedPosition.y <= Mathf.Epsilon ) + scrollView.normalizedPosition = new Vector2( scrollView.normalizedPosition.x, 0.001f ); + + scrollView.OnScroll( nullPointerEventData ); + } + protected override void RefreshSkin() { background.color = Skin.BackgroundColor; diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ColorField.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ColorField.cs index dcf6d53..a8ee1a8 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ColorField.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ColorField.cs @@ -43,7 +43,7 @@ private void ShowColorPicker( PointerEventData eventData ) Color value = isColor32 ? (Color) (Color32) Value : (Color) Value; ColorPicker.Instance.Skin = Inspector.Skin; - ColorPicker.Instance.Show( OnColorChanged, value, Inspector.Canvas ); + ColorPicker.Instance.Show( OnColorChanged, null, value, Inspector.Canvas ); } private void OnColorChanged( Color32 color ) diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/GameObjectField.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/GameObjectField.cs index 2f02287..4d7b1a0 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/GameObjectField.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/GameObjectField.cs @@ -1,7 +1,9 @@ using System; using System.Reflection; +using System.Collections; using System.Collections.Generic; using UnityEngine; +using Object = UnityEngine.Object; namespace RuntimeInspectorNamespace { @@ -18,6 +20,11 @@ public class GameObjectField : ExpandableInspectorField private readonly List components = new List( 8 ); private readonly List componentsExpandedStates = new List(); + private Type[] addComponentTypes; + + internal static ExposedMethod addComponentMethod = new ExposedMethod( typeof( GameObjectField ).GetMethod( "AddComponentButtonClicked", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ), new RuntimeInspectorButtonAttribute( "Add Component", false, ButtonVisibility.InitializedObjects ), false ); + internal static ExposedMethod removeComponentMethod = new ExposedMethod( typeof( GameObjectField ).GetMethod( "RemoveComponentButtonClicked", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ), new RuntimeInspectorButtonAttribute( "Remove Component", false, ButtonVisibility.InitializedObjects ), true ); + public override void Initialize() { base.Initialize(); @@ -72,7 +79,11 @@ protected override void ClearElements() { componentsExpandedStates.Clear(); for( int i = 0; i < elements.Count; i++ ) - componentsExpandedStates.Add( ( elements[i] is ExpandableInspectorField ) ? ( (ExpandableInspectorField) elements[i] ).IsExpanded : false ); + { + // Don't keep track of non-expandable drawers' or destroyed components' expanded states + if( elements[i] is ExpandableInspectorField && ( elements[i].Value as Object ) ) + componentsExpandedStates.Add( ( (ExpandableInspectorField) elements[i] ).IsExpanded ); + } base.ClearElements(); } @@ -87,32 +98,29 @@ protected override void GenerateElements() StringField tagField = CreateDrawer( typeof( string ), "Tag", tagGetter, tagSetter ) as StringField; CreateDrawerForVariable( layerProp, "Layer" ); - for( int i = 0, j = elements.Count; i < components.Count; i++ ) + for( int i = 0, j = 0; i < components.Count; i++ ) { InspectorField componentDrawer = CreateDrawerForComponent( components[i] ); - if( componentDrawer != null ) - { - if( j < componentsExpandedStates.Count && componentsExpandedStates[j] && componentDrawer is ExpandableInspectorField ) - ( (ExpandableInspectorField) componentDrawer ).IsExpanded = true; - - j++; - } + if( componentDrawer as ExpandableInspectorField && j < componentsExpandedStates.Count && componentsExpandedStates[j++] ) + ( (ExpandableInspectorField) componentDrawer ).IsExpanded = true; } - if( nameField != null ) + if( nameField ) nameField.SetterMode = StringField.Mode.OnSubmit; - if( tagField != null ) + if( tagField ) tagField.SetterMode = StringField.Mode.OnSubmit; + if( Inspector.ShowAddComponentButton ) + CreateExposedMethodButton( addComponentMethod, () => this, ( value ) => { } ); + componentsExpandedStates.Clear(); } public override void Refresh() { - base.Refresh(); + // Refresh components components.Clear(); - GameObject go = Value as GameObject; if( go ) { @@ -127,6 +135,106 @@ public override void Refresh() if( Inspector.ComponentFilter != null ) Inspector.ComponentFilter( go, components ); } + + // Regenerate components' drawers, if necessary + base.Refresh(); + } + + [UnityEngine.Scripting.Preserve] // This method is bound to addComponentMethod + private void AddComponentButtonClicked() + { + GameObject target = (GameObject) Value; + if( !target ) + return; + + if( addComponentTypes == null ) + { + List componentTypes = new List( 128 ); + +#if UNITY_EDITOR || !NETFX_CORE + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); +#else + // Common Unity assemblies + IEnumerable assemblies = new HashSet + { + typeof( Transform ).Assembly, + typeof( RectTransform ).Assembly, + typeof( Rigidbody ).Assembly, + typeof( Rigidbody2D ).Assembly, + typeof( AudioSource ).Assembly + }; +#endif + // Search assemblies for Component types + foreach( Assembly assembly in assemblies ) + { +#if( NET_4_6 || NET_STANDARD_2_0 ) && ( UNITY_EDITOR || !NETFX_CORE ) + if( assembly.IsDynamic ) + continue; +#endif + try + { + foreach( Type type in assembly.GetExportedTypes() ) + { + if( !typeof( Component ).IsAssignableFrom( type ) ) + continue; + +#if UNITY_EDITOR || !NETFX_CORE + if( type.IsGenericType || type.IsAbstract ) +#else + if( type.GetTypeInfo().IsGenericType || type.GetTypeInfo().IsAbstract ) +#endif + continue; + + componentTypes.Add( type ); + } + } + catch( NotSupportedException ) { } + catch( System.IO.FileNotFoundException ) { } + catch( Exception e ) + { + Debug.LogError( "Couldn't search assembly for Component types: " + assembly.GetName().Name + "\n" + e.ToString() ); + } + } + + addComponentTypes = componentTypes.ToArray(); + } + + ObjectReferencePicker.Instance.Skin = Inspector.Skin; + ObjectReferencePicker.Instance.Show( + null, ( type ) => + { + // Make sure that RuntimeInspector is still inspecting this GameObject + if( type != null && target && Inspector && ( Inspector.InspectedObject as GameObject ) == target ) + { + target.AddComponent( (Type) type ); + Inspector.Refresh(); + } + }, + ( type ) => ( (Type) type ).FullName, + ( type ) => ( (Type) type ).FullName, + addComponentTypes, null, false, "Add Component", Inspector.Canvas ); + } + + [UnityEngine.Scripting.Preserve] // This method is bound to removeComponentMethod + private static void RemoveComponentButtonClicked( ExpandableInspectorField componentDrawer ) + { + if( !componentDrawer || !componentDrawer.Inspector ) + return; + + Component component = componentDrawer.Value as Component; + if( component && !( component is Transform ) ) + componentDrawer.StartCoroutine( RemoveComponentCoroutine( component, componentDrawer.Inspector ) ); + } + + private static IEnumerator RemoveComponentCoroutine( Component component, RuntimeInspector inspector ) + { + Destroy( component ); + + // Destroy operation doesn't take place immediately, wait for the component to be fully destroyed + yield return null; + + inspector.Refresh(); + inspector.EnsureScrollViewIsWithinBounds(); // Scroll view's contents can get out of bounds after removing a component } } } \ No newline at end of file diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/InspectorField.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/InspectorField.cs index f16803e..3ce19f5 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/InspectorField.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/InspectorField.cs @@ -283,7 +283,7 @@ public abstract class ExpandableInspectorField : InspectorField #pragma warning restore 0649 protected readonly List elements = new List( 8 ); - private readonly List exposedMethods = new List(); + protected readonly List exposedMethods = new List(); protected virtual int Length { get { return elements.Count; } } @@ -427,15 +427,18 @@ protected void RegenerateElements() { drawArea.gameObject.SetActive( true ); GenerateElements(); - GenerateMethods(); + GenerateExposedMethodButtons(); drawArea.gameObject.SetActive( m_isExpanded ); } } protected abstract void GenerateElements(); - private void GenerateMethods() + private void GenerateExposedMethodButtons() { + if( Inspector.ShowRemoveComponentButton && typeof( Component ).IsAssignableFrom( BoundVariableType ) && !typeof( Transform ).IsAssignableFrom( BoundVariableType ) ) + CreateExposedMethodButton( GameObjectField.removeComponentMethod, () => this, ( value ) => { } ); + ExposedMethod[] methods = BoundVariableType.GetExposedMethods(); if( methods != null ) { @@ -444,16 +447,7 @@ private void GenerateMethods() { ExposedMethod method = methods[i]; if( ( isInitialized && method.VisibleWhenInitialized ) || ( !isInitialized && method.VisibleWhenUninitialized ) ) - { - ExposedMethodField methodDrawer = (ExposedMethodField) Inspector.CreateDrawerForType( typeof( ExposedMethod ), drawArea, Depth + 1, false ); - if( methodDrawer != null ) - { - methodDrawer.BindTo( typeof( ExposedMethod ), string.Empty, () => Value, ( value ) => Value = value ); - methodDrawer.SetBoundMethod( method ); - - exposedMethods.Add( methodDrawer ); - } - } + CreateExposedMethodButton( method, () => Value, ( value ) => Value = value ); } } } @@ -510,7 +504,10 @@ public InspectorField CreateDrawerForVariable( MemberInfo variable, string varia InspectorField variableDrawer = Inspector.CreateDrawerForType( variableType, drawArea, Depth + 1, true, variable ); if( variableDrawer != null ) { - variableDrawer.BindTo( this, variable, variableName ); + variableDrawer.BindTo( this, variable, variableName == null ? null : string.Empty ); + if( variableName != null ) + variableDrawer.NameRaw = variableName; + elements.Add( variableDrawer ); } @@ -522,11 +519,28 @@ public InspectorField CreateDrawer( Type variableType, string variableName, Gett InspectorField variableDrawer = Inspector.CreateDrawerForType( variableType, drawArea, Depth + 1, drawObjectsAsFields ); if( variableDrawer != null ) { - variableDrawer.BindTo( variableType, variableName, getter, setter ); + variableDrawer.BindTo( variableType, variableName == null ? null : string.Empty, getter, setter ); + if( variableName != null ) + variableDrawer.NameRaw = variableName; + elements.Add( variableDrawer ); } return variableDrawer; } + + public ExposedMethodField CreateExposedMethodButton( ExposedMethod method, Getter getter, Setter setter ) + { + ExposedMethodField methodDrawer = (ExposedMethodField) Inspector.CreateDrawerForType( typeof( ExposedMethod ), drawArea, Depth + 1, false ); + if( methodDrawer != null ) + { + methodDrawer.BindTo( typeof( ExposedMethod ), string.Empty, getter, setter ); + methodDrawer.SetBoundMethod( method ); + + exposedMethods.Add( methodDrawer ); + } + + return methodDrawer; + } } } \ No newline at end of file diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ObjectReferenceField.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ObjectReferenceField.cs index 18acded..46c155e 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ObjectReferenceField.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Fields/ObjectReferenceField.cs @@ -50,7 +50,11 @@ private void ShowReferencePicker( PointerEventData eventData ) Object[] allReferences = Resources.FindObjectsOfTypeAll( BoundVariableType ); ObjectReferencePicker.Instance.Skin = Inspector.Skin; - ObjectReferencePicker.Instance.Show( OnReferenceChanged, BoundVariableType, allReferences, (Object) Value, Inspector.Canvas ); + ObjectReferencePicker.Instance.Show( + ( reference ) => OnReferenceChanged( (Object) reference ), null, + ( reference ) => (Object) reference ? ( (Object) reference ).name : "None", + ( reference ) => reference.GetNameWithType(), + allReferences, (Object) Value, true, "Select " + BoundVariableType.Name, Inspector.Canvas ); } private void InspectReference( PointerEventData eventData ) diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ColorPicker/ColorPicker.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ColorPicker/ColorPicker.cs index 0ae680f..985b739 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ColorPicker/ColorPicker.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ColorPicker/ColorPicker.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System; +using System.Globalization; using UnityEngine; using UnityEngine.UI; @@ -64,7 +65,7 @@ public static ColorPicker Instance private Canvas referenceCanvas; private Color initialValue; - private ColorWheelControl.OnColorChangedDelegate onColorChanged; + private ColorWheelControl.OnColorChangedDelegate onColorChanged, onColorConfirmed; protected override void Awake() { @@ -76,7 +77,20 @@ protected override void Awake() aInput.Initialize(); cancelButton.onClick.AddListener( Cancel ); - okButton.onClick.AddListener( Close ); + okButton.onClick.AddListener( () => + { + try + { + if( onColorConfirmed != null ) + onColorConfirmed( colorWheel.Color ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } + + Close(); + } ); } private void Start() @@ -102,7 +116,7 @@ private void Start() OnSelectedColorChanged( colorWheel.Color ); } - public void Show( ColorWheelControl.OnColorChangedDelegate onColorChanged, Color initialColor, Canvas referenceCanvas ) + public void Show( ColorWheelControl.OnColorChangedDelegate onColorChanged, ColorWheelControl.OnColorChangedDelegate onColorConfirmed, Color initialColor, Canvas referenceCanvas ) { initialValue = initialColor; @@ -111,6 +125,7 @@ public void Show( ColorWheelControl.OnColorChangedDelegate onColorChanged, Color alphaSlider.Color = initialColor; alphaSlider.Value = initialColor.a; this.onColorChanged = onColorChanged; + this.onColorConfirmed = onColorConfirmed; if( referenceCanvas && this.referenceCanvas != referenceCanvas ) { @@ -127,8 +142,15 @@ public void Show( ColorWheelControl.OnColorChangedDelegate onColorChanged, Color public void Cancel() { - if( colorWheel.Color != initialValue && onColorChanged != null ) - onColorChanged( initialValue ); + try + { + if( colorWheel.Color != initialValue && onColorChanged != null ) + onColorChanged( initialValue ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } Close(); } @@ -136,6 +158,8 @@ public void Cancel() public void Close() { onColorChanged = null; + onColorConfirmed = null; + gameObject.SetActive( false ); } @@ -166,8 +190,15 @@ private void OnSelectedColorChanged( Color32 color ) alphaSlider.Color = color; - if( onColorChanged != null ) - onColorChanged( color ); + try + { + if( onColorChanged != null ) + onColorChanged( color ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } } private void OnAlphaChanged( float alpha ) @@ -178,8 +209,15 @@ private void OnAlphaChanged( float alpha ) Color color = colorWheel.Color; color.a = alpha; - if( onColorChanged != null ) - onColorChanged( color ); + try + { + if( onColorChanged != null ) + onColorChanged( color ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } } private bool OnRGBAChanged( BoundInputField source, string input ) diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePicker.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePicker.cs index 571abe5..8b1bc0d 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePicker.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePicker.cs @@ -27,8 +27,11 @@ public static ObjectReferencePicker Instance } } - public delegate void OnReferenceChanged( Object reference ); - private OnReferenceChanged onReferenceChanged; + public delegate void ReferenceCallback( object reference ); + private ReferenceCallback onReferenceChanged, onSelectionConfirmed; + + public delegate string NameGetter( object reference ); + private NameGetter referenceNameGetter, referenceDisplayNameGetter; #pragma warning disable 0649 [SerializeField] @@ -73,12 +76,12 @@ public static ObjectReferencePicker Instance private Canvas referenceCanvas; - private readonly List references = new List( 64 ); - private readonly List filteredReferences = new List( 64 ); + private readonly List references = new List( 64 ); + private readonly List filteredReferences = new List( 64 ); - private Object initialValue; + private object initialValue; - private Object currentlySelectedObject; + private object currentlySelectedObject; private ObjectReferencePickerItem currentlySelectedItem; int IListViewAdapter.Count { get { return filteredReferences.Count; } } @@ -92,13 +95,30 @@ protected override void Awake() searchBar.onValueChanged.AddListener( OnSearchTextChanged ); cancelButton.onClick.AddListener( Cancel ); - okButton.onClick.AddListener( Close ); + okButton.onClick.AddListener( () => + { + try + { + if( onSelectionConfirmed != null ) + onSelectionConfirmed( currentlySelectedObject ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } + + Close(); + } ); } - public void Show( OnReferenceChanged onReferenceChanged, Type referenceType, Object[] references, Object initialReference, Canvas referenceCanvas ) + public void Show( ReferenceCallback onReferenceChanged, ReferenceCallback onSelectionConfirmed, NameGetter referenceNameGetter, NameGetter referenceDisplayNameGetter, object[] references, object initialReference, bool includeNullReference, string title, Canvas referenceCanvas ) { initialValue = initialReference; + this.onReferenceChanged = onReferenceChanged; + this.onSelectionConfirmed = onSelectionConfirmed; + this.referenceNameGetter = referenceNameGetter ?? ( ( reference ) => reference.GetNameWithType() ); + this.referenceDisplayNameGetter = referenceDisplayNameGetter ?? ( ( reference ) => reference.GetNameWithType() ); if( referenceCanvas && this.referenceCanvas != referenceCanvas ) { @@ -112,16 +132,29 @@ public void Show( OnReferenceChanged onReferenceChanged, Type referenceType, Obj panel.rectTransform.anchoredPosition = Vector2.zero; gameObject.SetActive( true ); - selectPromptText.text = "Select " + referenceType.Name; + selectPromptText.text = title; currentlySelectedObject = initialReference; - GenerateReferenceItems( references, referenceType ); + GenerateReferenceItems( references, includeNullReference ); + +#if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL + // On desktop platforms, automatically focus on search field + // We don't do the same on mobile because immediately showing the on-screen keyboard after presenting the window wouldn't be nice + searchBar.ActivateInputField(); +#endif } public void Cancel() { - if( currentlySelectedObject != initialValue && onReferenceChanged != null ) - onReferenceChanged( initialValue ); + try + { + if( currentlySelectedObject != initialValue && onReferenceChanged != null ) + onReferenceChanged( initialValue ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } Close(); } @@ -129,6 +162,9 @@ public void Cancel() public void Close() { onReferenceChanged = null; + onSelectionConfirmed = null; + referenceNameGetter = null; + referenceDisplayNameGetter = null; initialValue = null; currentlySelectedObject = null; currentlySelectedItem = null; @@ -161,29 +197,33 @@ protected override void RefreshSkin() listView.ResetList(); } - private void GenerateReferenceItems( Object[] references, Type referenceType ) + private void GenerateReferenceItems( object[] references, bool includeNullReference ) { this.references.Clear(); filteredReferences.Clear(); searchBar.text = string.Empty; - this.references.Add( null ); - Array.Sort( references, ( ref1, ref2 ) => ref1.GetName().CompareTo( ref2.GetName() ) ); + if( includeNullReference ) + this.references.Add( null ); + + Array.Sort( references, ( ref1, ref2 ) => referenceNameGetter( ref1 ).CompareTo( referenceNameGetter( ref2 ) ) ); - bool isTexture = referenceType == typeof( Texture ) || referenceType == typeof( Texture ) || referenceType == typeof( Sprite ); for( int i = 0; i < references.Length; i++ ) { - if( !references[i] ) - continue; - - if( references[i].hideFlags != HideFlags.None && references[i].hideFlags != HideFlags.NotEditable && - references[i].hideFlags != HideFlags.HideInHierarchy && references[i].hideFlags != HideFlags.HideInInspector ) - continue; + Object unityReference = references[i] as Object; + if( unityReference ) + { + if( unityReference.hideFlags != HideFlags.None && unityReference.hideFlags != HideFlags.NotEditable && + unityReference.hideFlags != HideFlags.HideInHierarchy && unityReference.hideFlags != HideFlags.HideInInspector ) + continue; - if( isTexture && references[i].name.StartsWith( SPRITE_ATLAS_PREFIX ) ) - continue; + if( ( unityReference is Texture || unityReference is Sprite ) && unityReference.name.StartsWith( SPRITE_ATLAS_PREFIX ) ) + continue; - this.references.Add( references[i] ); + this.references.Add( unityReference ); + } + else if( references[i] != null ) + this.references.Add( references[i] ); } OnSearchTextChanged( string.Empty ); @@ -206,7 +246,7 @@ private void OnSearchTextChanged( string value ) value = value.ToLowerInvariant(); for( int i = 0; i < references.Count; i++ ) { - if( references[i].GetName().ToLowerInvariant().Contains( value ) ) + if( referenceNameGetter( references[i] ).ToLowerInvariant().Contains( value ) ) filteredReferences.Add( references[i] ); } @@ -216,7 +256,7 @@ private void OnSearchTextChanged( string value ) void IListViewAdapter.SetItemContent( RecycledListItem item ) { ObjectReferencePickerItem it = (ObjectReferencePickerItem) item; - it.SetContent( filteredReferences[it.Position] ); + it.SetContent( filteredReferences[it.Position], referenceDisplayNameGetter( filteredReferences[it.Position] ) ); if( it.Reference == currentlySelectedObject ) { @@ -238,8 +278,15 @@ void IListViewAdapter.OnItemClicked( RecycledListItem item ) currentlySelectedObject = currentlySelectedItem.Reference; currentlySelectedItem.IsSelected = true; - if( onReferenceChanged != null ) - onReferenceChanged( currentlySelectedItem.Reference ); + try + { + if( onReferenceChanged != null ) + onReferenceChanged( currentlySelectedObject ); + } + catch( Exception e ) + { + Debug.LogException( e ); + } } public static void DestroyInstance() diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePickerItem.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePickerItem.cs index c38d4ae..d74e350 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePickerItem.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/ObjectReferencePicker/ObjectReferencePickerItem.cs @@ -18,7 +18,7 @@ public class ObjectReferencePickerItem : RecycledListItem private Text referenceNameText; #pragma warning restore 0649 - public Object Reference { get; private set; } + public object Reference { get; private set; } private int m_skinVersion = 0; private UISkin m_skin; @@ -71,12 +71,12 @@ private void Awake() GetComponent().PointerClick += ( eventData ) => OnClick(); } - public void SetContent( Object reference ) + public void SetContent( object reference, string displayName ) { Reference = reference; - referenceNameText.text = reference.GetNameWithType(); + referenceNameText.text = displayName; - Texture previewTex = reference.GetTexture(); + Texture previewTex = ( reference as Object ).GetTexture(); if( previewTex != null ) { texturePreview.gameObject.SetActive( true ); diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/RuntimeInspectorUtils.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/RuntimeInspectorUtils.cs index 33604f9..8085595 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/RuntimeInspectorUtils.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Helpers/RuntimeInspectorUtils.cs @@ -130,14 +130,6 @@ public static string ToTitleCase( this string str ) return stringBuilder.ToString(); } - public static string GetName( this Object obj ) - { - if( !obj ) - return "None"; - - return obj.name; - } - public static string GetNameWithType( this object obj, Type defaultType = null ) { if( obj.IsNull() ) diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/SkinnedWindow.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/SkinnedWindow.cs index d174aee..94c6f01 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/SkinnedWindow.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/SkinnedWindow.cs @@ -22,10 +22,15 @@ public UISkin Skin } #pragma warning restore 0649 +#if UNITY_EDITOR + private UISkin prevSkin; +#endif + protected virtual void Awake() { // Refresh skin - m_skinVersion = Skin.Version - 1; + if( m_skin ) + m_skinVersion = m_skin.Version - 1; // Unity 2017.2 bugfix gameObject.SetActive( false ); @@ -34,13 +39,26 @@ protected virtual void Awake() protected virtual void Update() { - if( m_skinVersion != Skin.Version ) + if( m_skin && m_skinVersion != m_skin.Version ) { - m_skinVersion = Skin.Version; + m_skinVersion = m_skin.Version; RefreshSkin(); + +#if UNITY_EDITOR + prevSkin = m_skin; +#endif } } +#if UNITY_EDITOR + protected virtual void OnValidate() + { + // Refresh skin if it is changed via Unity Inspector at runtime + if( UnityEditor.EditorApplication.isPlaying && m_skin != prevSkin ) + m_skinVersion = m_skin ? ( m_skin.Version - 1 ) : ( m_skinVersion - 1 ); + } +#endif + protected abstract void RefreshSkin(); } } \ No newline at end of file diff --git a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/UISkin.cs b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/UISkin.cs index 04b03bf..5cd8a79 100644 --- a/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/UISkin.cs +++ b/Plugins/RuntimeInspector/Scripts/RuntimeInspector/Skin/UISkin.cs @@ -14,6 +14,14 @@ private void Invalidate() m_version = Random.Range( int.MinValue, int.MaxValue ); } +#if UNITY_EDITOR + protected virtual void OnValidate() + { + // Refresh all UIs that use this skin + Invalidate(); + } +#endif + #pragma warning disable 0649 [SerializeField] private Font m_font; diff --git a/package.json b/package.json index d0dfa46..3a7f81a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.yasirkula.runtimeinspector", "displayName": "Runtime Inspector & Hierarchy", - "version": "1.5.4", + "version": "1.6.0", "documentationUrl": "https://github.com/yasirkula/UnityRuntimeInspector", "changelogUrl": "https://github.com/yasirkula/UnityRuntimeInspector/releases", "licensesUrl": "https://github.com/yasirkula/UnityRuntimeInspector/blob/master/LICENSE.txt",