mirror of
https://github.com/Siccity/xNode.git
synced 2026-03-26 22:49:02 +08:00
Refactored OdinInspector integration.
This commit is contained in:
parent
979bd5f7cf
commit
489767b313
98
Scripts/Editor/Drawers/Odin/AsDynamicPortAttributes.cs
Normal file
98
Scripts/Editor/Drawers/Odin/AsDynamicPortAttributes.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||||
|
using Sirenix.OdinInspector.Editor;
|
||||||
|
using System;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using XNode;
|
||||||
|
using static XNode.Node;
|
||||||
|
|
||||||
|
namespace XNodeEditor.Odin
|
||||||
|
{
|
||||||
|
internal abstract class AsDynamicPortAtribute : System.Attribute
|
||||||
|
{
|
||||||
|
internal string fieldName { get; set; }
|
||||||
|
internal int index { get; set; }
|
||||||
|
internal Node Node { get; set; }
|
||||||
|
|
||||||
|
internal ConnectionType connectionType { get; set; }
|
||||||
|
internal ShowBackingValue backingValue { get; set; }
|
||||||
|
|
||||||
|
internal NodePort Port
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Node.GetPort( $"{fieldName} {index}" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AsDynamicPortNoDataAtribute : AsDynamicPortAtribute { }
|
||||||
|
internal class AsDynamicPortWithDataAtribute : AsDynamicPortAtribute { }
|
||||||
|
|
||||||
|
internal struct AsDynamicPortScope : IDisposable
|
||||||
|
{
|
||||||
|
public AsDynamicPortScope( NodePort port )
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginVertical();
|
||||||
|
var rect = GUILayoutUtility.GetRect( 0f, float.MaxValue, 0f, 0f, GUI.skin.label, GUILayout.ExpandWidth( true ) );
|
||||||
|
if ( NodeEditor.isNodeEditor )
|
||||||
|
{
|
||||||
|
if ( port.IsInput )
|
||||||
|
{
|
||||||
|
NodeEditorGUILayout.PortField( new Vector2( rect.xMin - 42, rect.center.y ), port );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NodeEditorGUILayout.PortField( new Vector2( rect.xMax + 21, rect.center.y ), port );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.BeginVertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawerPriority( 0.4, 0, 0 )]
|
||||||
|
internal class AsDynamicPortNoDataAttributeDrawer<T> : OdinAttributeDrawer<AsDynamicPortNoDataAtribute, T>
|
||||||
|
{
|
||||||
|
protected override void DrawPropertyLayout( GUIContent label )
|
||||||
|
{
|
||||||
|
if ( Attribute.Port == null )
|
||||||
|
return;
|
||||||
|
|
||||||
|
using ( new AsDynamicPortScope( Attribute.Port ) )
|
||||||
|
CallNextDrawer( label );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawerPriority( 0.4, 0, 0 )]
|
||||||
|
internal class AsDynamicPortWithDataAtributeDrawer<T> : OdinAttributeDrawer<AsDynamicPortWithDataAtribute, T>
|
||||||
|
{
|
||||||
|
protected bool drawData = false;
|
||||||
|
|
||||||
|
protected override void DrawPropertyLayout( GUIContent label )
|
||||||
|
{
|
||||||
|
if ( Attribute.Port == null )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( Event.current.type == EventType.Layout )
|
||||||
|
drawData = Attribute.backingValue == ShowBackingValue.Always || Attribute.backingValue == ShowBackingValue.Unconnected && !Attribute.Port.IsConnected;
|
||||||
|
|
||||||
|
using ( new AsDynamicPortScope( Attribute.Port ) )
|
||||||
|
{
|
||||||
|
if ( drawData )
|
||||||
|
CallNextDrawer( label );
|
||||||
|
else
|
||||||
|
EditorGUILayout.LabelField( label );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 2fd590b2e9ea0bd49b6986a2ca9010ab
|
guid: 0ef93b42a7d5fe8459b6755d72583900
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
83
Scripts/Editor/Drawers/Odin/AsStaticPortAttributes.cs
Normal file
83
Scripts/Editor/Drawers/Odin/AsStaticPortAttributes.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||||
|
using Sirenix.OdinInspector.Editor;
|
||||||
|
using System;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using XNode;
|
||||||
|
using static XNode.Node;
|
||||||
|
|
||||||
|
namespace XNodeEditor.Odin
|
||||||
|
{
|
||||||
|
internal struct AsStaticPortScope : IDisposable
|
||||||
|
{
|
||||||
|
public AsStaticPortScope( NodePort port )
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginVertical();
|
||||||
|
var rect = GUILayoutUtility.GetRect( 0f, float.MaxValue, 0f, 0f, GUI.skin.label, GUILayout.ExpandWidth( true ) );
|
||||||
|
if ( NodeEditor.isNodeEditor )
|
||||||
|
{
|
||||||
|
if ( port.IsInput )
|
||||||
|
{
|
||||||
|
NodeEditorGUILayout.PortField( new Vector2( rect.xMin - 18, rect.center.y + 2 ), port );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NodeEditorGUILayout.PortField( new Vector2( rect.xMax + 2, rect.center.y + 2 ), port );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
EditorGUILayout.BeginVertical();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawerPriority( 0.4, 0, 0 )]
|
||||||
|
internal class InputAttributeDrawer<T> : OdinAttributeDrawer<InputAttribute, T>
|
||||||
|
{
|
||||||
|
protected bool drawData = false;
|
||||||
|
|
||||||
|
protected override void DrawPropertyLayout( GUIContent label )
|
||||||
|
{
|
||||||
|
NodePort port = ( Property.Tree.UnitySerializedObject.targetObject as Node ).GetInputPort( Property.Name );
|
||||||
|
if ( Event.current.type == EventType.Layout )
|
||||||
|
drawData = Attribute.backingValue == ShowBackingValue.Always || Attribute.backingValue == ShowBackingValue.Unconnected && !port.IsConnected;
|
||||||
|
|
||||||
|
using ( new AsStaticPortScope( port ) )
|
||||||
|
{
|
||||||
|
if ( drawData )
|
||||||
|
CallNextDrawer( label );
|
||||||
|
else
|
||||||
|
EditorGUILayout.LabelField( label );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DrawerPriority( 0.4, 0, 0 )]
|
||||||
|
internal class OutputAttributeDrawer<T> : OdinAttributeDrawer<OutputAttribute, T>
|
||||||
|
{
|
||||||
|
protected bool drawData = false;
|
||||||
|
|
||||||
|
protected override void DrawPropertyLayout( GUIContent label )
|
||||||
|
{
|
||||||
|
NodePort port = ( Property.Tree.UnitySerializedObject.targetObject as Node ).GetOutputPort( Property.Name );
|
||||||
|
if ( Event.current.type == EventType.Layout )
|
||||||
|
drawData = Attribute.backingValue == ShowBackingValue.Always || Attribute.backingValue == ShowBackingValue.Unconnected && !port.IsConnected;
|
||||||
|
|
||||||
|
using ( new AsStaticPortScope( port ) )
|
||||||
|
{
|
||||||
|
if ( drawData )
|
||||||
|
CallNextDrawer( label );
|
||||||
|
else
|
||||||
|
EditorGUILayout.LabelField( label );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: e7ebd8f2b42e2384aa109551dc46af88
|
guid: 36e90d7590dbcad418bc8ca94192d5e2
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
219
Scripts/Editor/Drawers/Odin/DynamicPortListBackedResolver.cs
Normal file
219
Scripts/Editor/Drawers/Odin/DynamicPortListBackedResolver.cs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||||
|
using Sirenix.OdinInspector;
|
||||||
|
using Sirenix.OdinInspector.Editor;
|
||||||
|
using Sirenix.Utilities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using XNode;
|
||||||
|
using static XNode.Node;
|
||||||
|
|
||||||
|
namespace XNodeEditor.Odin
|
||||||
|
{
|
||||||
|
[ResolverPriority( 10 )]
|
||||||
|
public class DynamicPortListBackedResolver<TList, TElement> : StrongListPropertyResolver<TList, TElement>
|
||||||
|
where TList : IList<TElement>
|
||||||
|
{
|
||||||
|
public override bool CanResolveForPropertyFilter( InspectorProperty property )
|
||||||
|
{
|
||||||
|
var input = property.GetAttribute<InputAttribute>();
|
||||||
|
if ( input != null )
|
||||||
|
return input.dynamicPortList;
|
||||||
|
|
||||||
|
var output = property.GetAttribute<OutputAttribute>();
|
||||||
|
if ( output != null )
|
||||||
|
return output.dynamicPortList;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, InspectorPropertyInfo> childInfos = new Dictionary<int, InspectorPropertyInfo>();
|
||||||
|
|
||||||
|
public override InspectorPropertyInfo GetChildInfo( int childIndex )
|
||||||
|
{
|
||||||
|
if ( childIndex < 0 || childIndex >= this.ChildCount )
|
||||||
|
{
|
||||||
|
throw new IndexOutOfRangeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectorPropertyInfo result;
|
||||||
|
|
||||||
|
if ( !this.childInfos.TryGetValue( childIndex, out result ) )
|
||||||
|
{
|
||||||
|
var attributes = this.Property.Attributes.Where( attr => !attr.GetType().IsDefined( typeof( DontApplyToListElementsAttribute ), true ) );
|
||||||
|
var labelTextAttribute = attributes.OfType<LabelTextAttribute>().SingleOrDefault();
|
||||||
|
var hideLabelAttribute = attributes.OfType<HideLabelAttribute>().SingleOrDefault();
|
||||||
|
|
||||||
|
var listDrawerSettingsAttribute = attributes.OfType<ListDrawerSettingsAttribute>().SingleOrDefault();
|
||||||
|
if ( listDrawerSettingsAttribute == null )
|
||||||
|
{
|
||||||
|
listDrawerSettingsAttribute = new ListDrawerSettingsAttribute() { Expanded = true, ShowPaging = false };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
listDrawerSettingsAttribute.Expanded = true;
|
||||||
|
listDrawerSettingsAttribute.ShowPaging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes = attributes
|
||||||
|
.Append( GetPortAttribute( Property.Name, childIndex ) )
|
||||||
|
.AppendIf( labelTextAttribute == null && hideLabelAttribute == null, new LabelTextAttribute( $"{Property.Name} {childIndex}" ) );
|
||||||
|
|
||||||
|
result = InspectorPropertyInfo.CreateValue(
|
||||||
|
name: CollectionResolverUtilities.DefaultIndexToChildName( childIndex ),
|
||||||
|
order: childIndex,
|
||||||
|
serializationBackend: this.Property.BaseValueEntry.SerializationBackend,
|
||||||
|
new GetterSetter<TList, TElement>(
|
||||||
|
getter: ( ref TList list ) => list[childIndex],
|
||||||
|
setter: ( ref TList list, TElement element ) => list[childIndex] = element ),
|
||||||
|
attributes: attributes.ToArray() );
|
||||||
|
|
||||||
|
this.childInfos[childIndex] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal AsDynamicPortWithDataAtribute GetPortAttribute( string fieldName, int index )
|
||||||
|
{
|
||||||
|
return new AsDynamicPortWithDataAtribute()
|
||||||
|
{
|
||||||
|
fieldName = fieldName,
|
||||||
|
index = index,
|
||||||
|
Node = node,
|
||||||
|
|
||||||
|
connectionType = connectionType,
|
||||||
|
backingValue = backingValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Add( TList collection, object value )
|
||||||
|
{
|
||||||
|
int nextId = this.ChildCount;
|
||||||
|
|
||||||
|
if ( IsInput )
|
||||||
|
this.node.AddDynamicInput( typeof( TElement ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
else
|
||||||
|
this.node.AddDynamicOutput( typeof( TElement ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
|
||||||
|
base.Add( collection, value );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InsertAt( TList collection, int index, object value )
|
||||||
|
{
|
||||||
|
int nextId = this.ChildCount;
|
||||||
|
|
||||||
|
// Remove happens before insert and we lose all the connections
|
||||||
|
// Add a new port at the end
|
||||||
|
if ( IsInput )
|
||||||
|
this.node.AddDynamicInput( typeof( TElement ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
else
|
||||||
|
this.node.AddDynamicOutput( typeof( TElement ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
|
||||||
|
var dynamicPorts = this.ports;
|
||||||
|
|
||||||
|
// Move everything down to make space
|
||||||
|
for ( int k = dynamicPorts.Count - 1; k > index; --k )
|
||||||
|
{
|
||||||
|
for ( int j = 0; j < dynamicPorts[k - 1].ConnectionCount; j++ )
|
||||||
|
{
|
||||||
|
XNode.NodePort other = dynamicPorts[k - 1].GetConnection( j );
|
||||||
|
dynamicPorts[k - 1].Disconnect( other );
|
||||||
|
dynamicPorts[k].Connect( other );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's just re-add connections to this node that were probably his
|
||||||
|
foreach ( var c in lastRemovedConnections )
|
||||||
|
dynamicPorts[index].Connect( c );
|
||||||
|
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
|
||||||
|
base.InsertAt( collection, index, value );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Remove( TList collection, object value )
|
||||||
|
{
|
||||||
|
int index = collection.IndexOf( (TElement)value );
|
||||||
|
RemoveAt( collection, index );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<NodePort> lastRemovedConnections = new List<NodePort>();
|
||||||
|
|
||||||
|
protected override void RemoveAt( TList collection, int index )
|
||||||
|
{
|
||||||
|
var dynamicPorts = this.ports;
|
||||||
|
|
||||||
|
if ( dynamicPorts[index] == null )
|
||||||
|
{
|
||||||
|
Debug.LogWarning( "No port found at index " + index + " - Skipped" );
|
||||||
|
}
|
||||||
|
else if ( dynamicPorts.Count <= index )
|
||||||
|
{
|
||||||
|
Debug.LogWarning( "DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + " - Skipped" );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
lastRemovedConnections.AddRange( dynamicPorts[index].GetConnections() );
|
||||||
|
|
||||||
|
// Clear the removed ports connections
|
||||||
|
dynamicPorts[index].ClearConnections();
|
||||||
|
// Move following connections one step up to replace the missing connection
|
||||||
|
for ( int k = index + 1; k < dynamicPorts.Count; k++ )
|
||||||
|
{
|
||||||
|
for ( int j = 0; j < dynamicPorts[k].ConnectionCount; j++ )
|
||||||
|
{
|
||||||
|
XNode.NodePort other = dynamicPorts[k].GetConnection( j );
|
||||||
|
dynamicPorts[k].Disconnect( other );
|
||||||
|
dynamicPorts[k - 1].Connect( other );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the last dynamic port, to avoid messing up the indexing
|
||||||
|
node.RemoveDynamicPort( dynamicPorts[dynamicPorts.Count() - 1].fieldName );
|
||||||
|
}
|
||||||
|
|
||||||
|
base.RemoveAt( collection, index );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Clear( TList collection )
|
||||||
|
{
|
||||||
|
foreach ( var port in ports )
|
||||||
|
node.RemoveDynamicPort( port );
|
||||||
|
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
|
||||||
|
base.Clear( collection );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Node node => ( Property.Tree.UnitySerializedObject.targetObject as Node );
|
||||||
|
protected List<NodePort> ports
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// This created a lot of garbage
|
||||||
|
List<NodePort> dynamicPorts = new List<NodePort>();
|
||||||
|
for ( int i = 0; i < int.MaxValue; ++i )
|
||||||
|
{
|
||||||
|
var nodePort = node.GetPort( $"{Property.Name} {i}" );
|
||||||
|
if ( nodePort == null )
|
||||||
|
break;
|
||||||
|
|
||||||
|
dynamicPorts.Add( nodePort );
|
||||||
|
}
|
||||||
|
return dynamicPorts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsInput => Property.GetAttribute<InputAttribute>() != null;
|
||||||
|
|
||||||
|
public ConnectionType connectionType => IsInput ? Property.GetAttribute<InputAttribute>().connectionType : Property.GetAttribute<OutputAttribute>().connectionType;
|
||||||
|
public TypeConstraint typeConstraint => IsInput ? Property.GetAttribute<InputAttribute>().typeConstraint : Property.GetAttribute<OutputAttribute>().typeConstraint;
|
||||||
|
public ShowBackingValue backingValue => IsInput ? Property.GetAttribute<InputAttribute>().backingValue : Property.GetAttribute<OutputAttribute>().backingValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f8fa0ef545c9f5049bbc8da047874fd1
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
250
Scripts/Editor/Drawers/Odin/DynamicPortListNoDataResolver.cs
Normal file
250
Scripts/Editor/Drawers/Odin/DynamicPortListNoDataResolver.cs
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||||
|
using Sirenix.OdinInspector;
|
||||||
|
using Sirenix.OdinInspector.Editor;
|
||||||
|
using Sirenix.OdinInspector.Editor.Drawers;
|
||||||
|
using Sirenix.Utilities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEngine;
|
||||||
|
using XNode;
|
||||||
|
using XNodeEditor.Odin;
|
||||||
|
using static XNode.Node;
|
||||||
|
|
||||||
|
namespace XNodeEditor
|
||||||
|
{
|
||||||
|
[DrawerPriority( 0.4, 0, 0 )]
|
||||||
|
public class SillyCollectionDrawer : CollectionDrawer<int>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DynamicPortListNoDataResolver<T> : DynamicPortListNoDataResolver<T, NodePort> { }
|
||||||
|
|
||||||
|
public class DynamicPortListNoDataResolver<TNotAList, TElement> : BaseOrderedCollectionResolver<TNotAList>
|
||||||
|
where TElement : NodePort
|
||||||
|
{
|
||||||
|
public override bool CanResolveForPropertyFilter( InspectorProperty property )
|
||||||
|
{
|
||||||
|
var input = property.GetAttribute<InputAttribute>();
|
||||||
|
if ( input != null )
|
||||||
|
return input.dynamicPortList;
|
||||||
|
|
||||||
|
var output = property.GetAttribute<OutputAttribute>();
|
||||||
|
if ( output != null )
|
||||||
|
return output.dynamicPortList;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, InspectorPropertyInfo> childInfos = new Dictionary<int, InspectorPropertyInfo>();
|
||||||
|
|
||||||
|
public override InspectorPropertyInfo GetChildInfo( int childIndex )
|
||||||
|
{
|
||||||
|
if ( childIndex < 0 || childIndex >= this.ChildCount )
|
||||||
|
{
|
||||||
|
throw new IndexOutOfRangeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectorPropertyInfo result;
|
||||||
|
|
||||||
|
if ( !this.childInfos.TryGetValue( childIndex, out result ) )
|
||||||
|
{
|
||||||
|
var attributes = this.Property.Attributes.Where( attr => !attr.GetType().IsDefined( typeof( DontApplyToListElementsAttribute ), true ) );
|
||||||
|
var labelTextAttribute = attributes.OfType<LabelTextAttribute>().SingleOrDefault();
|
||||||
|
var hideLabelAttribute = attributes.OfType<HideLabelAttribute>().SingleOrDefault();
|
||||||
|
|
||||||
|
var listDrawerSettingsAttribute = attributes.OfType<ListDrawerSettingsAttribute>().SingleOrDefault();
|
||||||
|
if ( listDrawerSettingsAttribute == null )
|
||||||
|
{
|
||||||
|
listDrawerSettingsAttribute = new ListDrawerSettingsAttribute() { Expanded = true, ShowPaging = false };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
listDrawerSettingsAttribute.Expanded = true;
|
||||||
|
listDrawerSettingsAttribute.ShowPaging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes = attributes
|
||||||
|
.Append( GetPortAttribute( Property.Name, childIndex ) )
|
||||||
|
.AppendIf( labelTextAttribute == null && hideLabelAttribute == null, new LabelTextAttribute( $"{Property.Name} {childIndex}" ) );
|
||||||
|
|
||||||
|
result = InspectorPropertyInfo.CreateValue(
|
||||||
|
name: CollectionResolverUtilities.DefaultIndexToChildName( childIndex ),
|
||||||
|
order: childIndex,
|
||||||
|
serializationBackend: this.Property.BaseValueEntry.SerializationBackend,
|
||||||
|
new GetterSetter<TNotAList, TElement>(
|
||||||
|
getter: ( ref TNotAList list ) => ports[childIndex], // Return absolutely nothing? Return a port?
|
||||||
|
setter: ( ref TNotAList list, TElement element ) => ports[childIndex] = element ),
|
||||||
|
attributes: attributes.ToArray() );
|
||||||
|
|
||||||
|
this.childInfos[childIndex] = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal AsDynamicPortNoDataAtribute GetPortAttribute( string fieldName, int index )
|
||||||
|
{
|
||||||
|
return new AsDynamicPortNoDataAtribute()
|
||||||
|
{
|
||||||
|
fieldName = fieldName,
|
||||||
|
index = index,
|
||||||
|
Node = node,
|
||||||
|
|
||||||
|
connectionType = connectionType,
|
||||||
|
backingValue = backingValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Add( TNotAList collection, object value )
|
||||||
|
{
|
||||||
|
int nextId = this.ChildCount;
|
||||||
|
|
||||||
|
if ( IsInput )
|
||||||
|
this.node.AddDynamicInput( typeof( TNotAList ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
else
|
||||||
|
this.node.AddDynamicOutput( typeof( TNotAList ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
|
||||||
|
//base.Add( collection, value );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InsertAt( TNotAList collection, int index, object value )
|
||||||
|
{
|
||||||
|
int nextId = this.ChildCount;
|
||||||
|
|
||||||
|
// Remove happens before insert and we lose all the connections
|
||||||
|
// Add a new port at the end
|
||||||
|
if ( IsInput )
|
||||||
|
this.node.AddDynamicInput( typeof( TNotAList ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
else
|
||||||
|
this.node.AddDynamicOutput( typeof( TNotAList ), connectionType, typeConstraint, $"{Property.Name} {nextId}" );
|
||||||
|
|
||||||
|
var dynamicPorts = this.ports;
|
||||||
|
|
||||||
|
// Move everything down to make space
|
||||||
|
for ( int k = dynamicPorts.Count - 1; k > index; --k )
|
||||||
|
{
|
||||||
|
for ( int j = 0; j < dynamicPorts[k - 1].ConnectionCount; j++ )
|
||||||
|
{
|
||||||
|
XNode.NodePort other = dynamicPorts[k - 1].GetConnection( j );
|
||||||
|
dynamicPorts[k - 1].Disconnect( other );
|
||||||
|
dynamicPorts[k].Connect( other );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's just re-add connections to this node that were probably his
|
||||||
|
foreach ( var c in lastRemovedConnections )
|
||||||
|
dynamicPorts[index].Connect( c );
|
||||||
|
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
|
||||||
|
//base.InsertAt( collection, index, value );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Remove( TNotAList collection, object value )
|
||||||
|
{
|
||||||
|
//int index = collection.IndexOf( (TElement)value );
|
||||||
|
//RemoveAt( collection, index );
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<NodePort> lastRemovedConnections = new List<NodePort>();
|
||||||
|
|
||||||
|
protected override void RemoveAt( TNotAList collection, int index )
|
||||||
|
{
|
||||||
|
var dynamicPorts = this.ports;
|
||||||
|
|
||||||
|
if ( dynamicPorts[index] == null )
|
||||||
|
{
|
||||||
|
Debug.LogWarning( "No port found at index " + index + " - Skipped" );
|
||||||
|
}
|
||||||
|
else if ( dynamicPorts.Count <= index )
|
||||||
|
{
|
||||||
|
Debug.LogWarning( "DynamicPorts[" + index + "] out of range. Length was " + dynamicPorts.Count + " - Skipped" );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
lastRemovedConnections.AddRange( dynamicPorts[index].GetConnections() );
|
||||||
|
|
||||||
|
// Clear the removed ports connections
|
||||||
|
dynamicPorts[index].ClearConnections();
|
||||||
|
// Move following connections one step up to replace the missing connection
|
||||||
|
for ( int k = index + 1; k < dynamicPorts.Count; k++ )
|
||||||
|
{
|
||||||
|
for ( int j = 0; j < dynamicPorts[k].ConnectionCount; j++ )
|
||||||
|
{
|
||||||
|
XNode.NodePort other = dynamicPorts[k].GetConnection( j );
|
||||||
|
dynamicPorts[k].Disconnect( other );
|
||||||
|
dynamicPorts[k - 1].Connect( other );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the last dynamic port, to avoid messing up the indexing
|
||||||
|
node.RemoveDynamicPort( dynamicPorts[dynamicPorts.Count() - 1].fieldName );
|
||||||
|
}
|
||||||
|
|
||||||
|
//base.RemoveAt( collection, index );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Clear( TNotAList collection )
|
||||||
|
{
|
||||||
|
foreach ( var port in ports )
|
||||||
|
node.RemoveDynamicPort( port );
|
||||||
|
|
||||||
|
lastRemovedConnections.Clear();
|
||||||
|
|
||||||
|
//base.Clear( collection );
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool ChildPropertyRequiresRefresh( int index, InspectorPropertyInfo info )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CollectionIsReadOnly( TNotAList collection )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int GetChildCount( TNotAList value )
|
||||||
|
{
|
||||||
|
return ports.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int ChildNameToIndex( string name )
|
||||||
|
{
|
||||||
|
return CollectionResolverUtilities.DefaultChildNameToIndex( name );
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Type ElementType => typeof( TElement );
|
||||||
|
|
||||||
|
protected Node node => ( Property.Tree.UnitySerializedObject.targetObject as Node );
|
||||||
|
protected List<TElement> ports
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// This created a lot of garbage
|
||||||
|
List<TElement> dynamicPorts = new List<TElement>();
|
||||||
|
for ( int i = 0; i < int.MaxValue; ++i )
|
||||||
|
{
|
||||||
|
var nodePort = node.GetPort( $"{Property.Name} {i}" );
|
||||||
|
if ( nodePort == null )
|
||||||
|
break;
|
||||||
|
|
||||||
|
dynamicPorts.Add( nodePort as TElement );
|
||||||
|
}
|
||||||
|
return dynamicPorts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsInput => Property.GetAttribute<InputAttribute>() != null;
|
||||||
|
|
||||||
|
public ConnectionType connectionType => IsInput ? Property.GetAttribute<InputAttribute>().connectionType : Property.GetAttribute<OutputAttribute>().connectionType;
|
||||||
|
public TypeConstraint typeConstraint => IsInput ? Property.GetAttribute<InputAttribute>().typeConstraint : Property.GetAttribute<OutputAttribute>().typeConstraint;
|
||||||
|
public ShowBackingValue backingValue => IsInput ? Property.GetAttribute<InputAttribute>().backingValue : Property.GetAttribute<OutputAttribute>().backingValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 16bd57b189b3213449470c2dce67f5ec
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -1,19 +1,20 @@
|
|||||||
#if UNITY_EDITOR && ODIN_INSPECTOR
|
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||||
|
using Sirenix.OdinInspector.Editor;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Sirenix.OdinInspector.Editor;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using XNode;
|
using XNode;
|
||||||
|
|
||||||
namespace XNodeEditor {
|
namespace XNodeEditor
|
||||||
|
{
|
||||||
internal class OdinNodeInGraphAttributeProcessor<T> : OdinAttributeProcessor<T> where T : Node {
|
internal class OdinNodeInGraphAttributeProcessor<T> : OdinAttributeProcessor<T> where T : Node {
|
||||||
public override bool CanProcessSelfAttributes(InspectorProperty property) {
|
public override bool CanProcessSelfAttributes(InspectorProperty property) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) {
|
public override bool CanProcessChildMemberAttributes(InspectorProperty parentProperty, MemberInfo member) {
|
||||||
if (!NodeEditor.inNodeEditor)
|
if (!NodeEditor.isNodeEditor)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (member.MemberType == MemberTypes.Field) {
|
if (member.MemberType == MemberTypes.Field) {
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
#if UNITY_EDITOR && ODIN_INSPECTOR
|
|
||||||
using Sirenix.OdinInspector;
|
|
||||||
using Sirenix.OdinInspector.Editor;
|
|
||||||
using Sirenix.Utilities.Editor;
|
|
||||||
using UnityEngine;
|
|
||||||
using XNode;
|
|
||||||
|
|
||||||
namespace XNodeEditor {
|
|
||||||
public class InputAttributeDrawer : OdinAttributeDrawer<XNode.Node.InputAttribute> {
|
|
||||||
protected override bool CanDrawAttributeProperty(InspectorProperty property) {
|
|
||||||
Node node = property.Tree.WeakTargets[0] as Node;
|
|
||||||
return node != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DrawPropertyLayout(GUIContent label) {
|
|
||||||
Node node = Property.Tree.WeakTargets[0] as Node;
|
|
||||||
NodePort port = node.GetInputPort(Property.Name);
|
|
||||||
|
|
||||||
if (!NodeEditor.inNodeEditor) {
|
|
||||||
if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected)
|
|
||||||
CallNextDrawer(label);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Property.Tree.WeakTargets.Count > 1) {
|
|
||||||
SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port != null) {
|
|
||||||
var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath);
|
|
||||||
if (portPropoerty == null) {
|
|
||||||
SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
var labelWidth = Property.GetAttribute<LabelWidthAttribute>();
|
|
||||||
if (labelWidth != null)
|
|
||||||
GUIHelper.PushLabelWidth(labelWidth.Width);
|
|
||||||
|
|
||||||
NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30));
|
|
||||||
|
|
||||||
if (labelWidth != null)
|
|
||||||
GUIHelper.PopLabelWidth();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
#if UNITY_EDITOR && ODIN_INSPECTOR
|
|
||||||
using Sirenix.OdinInspector;
|
|
||||||
using Sirenix.OdinInspector.Editor;
|
|
||||||
using Sirenix.Utilities.Editor;
|
|
||||||
using UnityEngine;
|
|
||||||
using XNode;
|
|
||||||
|
|
||||||
namespace XNodeEditor {
|
|
||||||
public class OutputAttributeDrawer : OdinAttributeDrawer<XNode.Node.OutputAttribute> {
|
|
||||||
protected override bool CanDrawAttributeProperty(InspectorProperty property) {
|
|
||||||
Node node = property.Tree.WeakTargets[0] as Node;
|
|
||||||
return node != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DrawPropertyLayout(GUIContent label) {
|
|
||||||
Node node = Property.Tree.WeakTargets[0] as Node;
|
|
||||||
NodePort port = node.GetOutputPort(Property.Name);
|
|
||||||
|
|
||||||
if (!NodeEditor.inNodeEditor) {
|
|
||||||
if (Attribute.backingValue == XNode.Node.ShowBackingValue.Always || Attribute.backingValue == XNode.Node.ShowBackingValue.Unconnected && !port.IsConnected)
|
|
||||||
CallNextDrawer(label);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Property.Tree.WeakTargets.Count > 1) {
|
|
||||||
SirenixEditorGUI.WarningMessageBox("Cannot draw ports with multiple nodes selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port != null) {
|
|
||||||
var portPropoerty = Property.Tree.GetUnityPropertyForPath(Property.UnityPropertyPath);
|
|
||||||
if (portPropoerty == null) {
|
|
||||||
SirenixEditorGUI.ErrorMessageBox("Port property missing at: " + Property.UnityPropertyPath);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
var labelWidth = Property.GetAttribute<LabelWidthAttribute>();
|
|
||||||
if (labelWidth != null)
|
|
||||||
GUIHelper.PushLabelWidth(labelWidth.Width);
|
|
||||||
|
|
||||||
NodeEditorGUILayout.PropertyField(portPropoerty, label == null ? GUIContent.none : label, true, GUILayout.MinWidth(30));
|
|
||||||
|
|
||||||
if (labelWidth != null)
|
|
||||||
GUIHelper.PopLabelWidth();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
26
Scripts/Editor/Drawers/Odin/SimpleNodePortDrawer.cs
Normal file
26
Scripts/Editor/Drawers/Odin/SimpleNodePortDrawer.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#if UNITY_EDITOR && ODIN_INSPECTOR
|
||||||
|
using Sirenix.OdinInspector.Editor;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using XNode;
|
||||||
|
|
||||||
|
namespace XNodeEditor.Odin
|
||||||
|
{
|
||||||
|
public class SimpleNodePortDrawer<T> : OdinValueDrawer<T>
|
||||||
|
where T : NodePort
|
||||||
|
{
|
||||||
|
protected override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
this.SkipWhenDrawing = !NodeEditor.isNodeEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawPropertyLayout( GUIContent label )
|
||||||
|
{
|
||||||
|
if ( label != null )
|
||||||
|
EditorGUILayout.LabelField( label );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
11
Scripts/Editor/Drawers/Odin/SimpleNodePortDrawer.cs.meta
Normal file
11
Scripts/Editor/Drawers/Odin/SimpleNodePortDrawer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e30f21284cedd4945ab30934cd164eee
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -21,7 +21,7 @@ namespace XNodeEditor {
|
|||||||
public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
|
public readonly static Dictionary<XNode.NodePort, Vector2> portPositions = new Dictionary<XNode.NodePort, Vector2>();
|
||||||
|
|
||||||
#if ODIN_INSPECTOR
|
#if ODIN_INSPECTOR
|
||||||
internal static bool inNodeEditor = false;
|
public static bool isNodeEditor { get; internal set; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public virtual void OnHeaderGUI() {
|
public virtual void OnHeaderGUI() {
|
||||||
@ -31,7 +31,7 @@ namespace XNodeEditor {
|
|||||||
/// <summary> Draws standard field editors for all public fields </summary>
|
/// <summary> Draws standard field editors for all public fields </summary>
|
||||||
public virtual void OnBodyGUI() {
|
public virtual void OnBodyGUI() {
|
||||||
#if ODIN_INSPECTOR
|
#if ODIN_INSPECTOR
|
||||||
inNodeEditor = true;
|
isNodeEditor = true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Unity specifically requires this to save/update any serial object.
|
// Unity specifically requires this to save/update any serial object.
|
||||||
@ -57,13 +57,13 @@ namespace XNodeEditor {
|
|||||||
if (excludes.Contains(iterator.name)) continue;
|
if (excludes.Contains(iterator.name)) continue;
|
||||||
NodeEditorGUILayout.PropertyField(iterator, true);
|
NodeEditorGUILayout.PropertyField(iterator, true);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
// Iterate through dynamic ports and draw them in the order in which they are serialized
|
// Iterate through dynamic ports and draw them in the order in which they are serialized
|
||||||
foreach (XNode.NodePort dynamicPort in target.DynamicPorts) {
|
foreach (XNode.NodePort dynamicPort in target.DynamicPorts) {
|
||||||
if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue;
|
if (NodeEditorGUILayout.IsDynamicPortListPort(dynamicPort)) continue;
|
||||||
NodeEditorGUILayout.PortField(dynamicPort);
|
NodeEditorGUILayout.PortField(dynamicPort);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ namespace XNodeEditor {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if ODIN_INSPECTOR
|
#if ODIN_INSPECTOR
|
||||||
inNodeEditor = false;
|
isNodeEditor = false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,10 +26,10 @@ namespace XNodeEditor.Internal {
|
|||||||
get {
|
get {
|
||||||
if (this._objectTree == null) {
|
if (this._objectTree == null) {
|
||||||
try {
|
try {
|
||||||
bool wasInEditor = NodeEditor.inNodeEditor;
|
bool wasInEditor = NodeEditor.isNodeEditor;
|
||||||
NodeEditor.inNodeEditor = true;
|
NodeEditor.isNodeEditor = true;
|
||||||
this._objectTree = PropertyTree.Create(this.serializedObject);
|
this._objectTree = PropertyTree.Create(this.serializedObject);
|
||||||
NodeEditor.inNodeEditor = wasInEditor;
|
NodeEditor.isNodeEditor = wasInEditor;
|
||||||
} catch (ArgumentException ex) {
|
} catch (ArgumentException ex) {
|
||||||
Debug.Log(ex);
|
Debug.Log(ex);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user