diff --git a/Assets/SpecklePlayground.unity b/Assets/SpecklePlayground.unity index 07a9cca..65b90bd 100644 --- a/Assets/SpecklePlayground.unity +++ b/Assets/SpecklePlayground.unity @@ -1849,6 +1849,19 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 762643242} m_CullTransparentMesh: 1 +--- !u!114 &816671309 +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: 141ce93d2d159c0448b5b8b33b1c0679, type: 3} + m_Name: + m_EditorClassIdentifier: + path: Assets/Resources --- !u!1 &869165413 GameObject: m_ObjectHideFlags: 0 @@ -2064,19 +2077,6 @@ Transform: m_Father: {fileID: 0} m_RootOrder: 6 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &916416845 -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: 141ce93d2d159c0448b5b8b33b1c0679, type: 3} - m_Name: - m_EditorClassIdentifier: - path: Assets/Resources --- !u!1 &1031574851 GameObject: m_ObjectHideFlags: 0 @@ -2773,6 +2773,98 @@ Canvas: m_SortingLayerID: 0 m_SortingOrder: 0 m_TargetDisplay: 0 +--- !u!1 &1377894061 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1377894063} + - component: {fileID: 1377894062} + m_Layer: 0 + m_Name: GameObject (2) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1377894062 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1377894061} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0bc895f6cb37b674995dc13b79783c55, type: 3} + m_Name: + m_EditorClassIdentifier: + k__BackingField: + rid: 6366722168162615296 + k__BackingField: + rid: 6366722168162615297 + k__BackingField: + rid: 6366722168162615298 + k__BackingField: + rid: 6366722168162615299 + references: + version: 2 + RefIds: + - rid: 6366722168162615296 + type: {class: AccountSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 1 + - rid: 6366722168162615297 + type: {class: StreamSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 2 + k__BackingField: 50 + k__BackingField: + rid: 6366722168162615296 + - rid: 6366722168162615298 + type: {class: BranchSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 5 + k__BackingField: 20 + k__BackingField: 15 + k__BackingField: + rid: 6366722168162615297 + - rid: 6366722168162615299 + type: {class: CommitSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 1 + k__BackingField: + rid: 6366722168162615298 +--- !u!4 &1377894063 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1377894061} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 10.1570635, y: -2.743827, z: 12.879177} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 11 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1390840085 +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: b3354e8208862c341940152f5340d41a, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!1 &1397973070 GameObject: m_ObjectHideFlags: 0 @@ -2874,8 +2966,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: nativeCaches: - - {fileID: 916416845} - - {fileID: 1951948666} + - {fileID: 1852085904} + - {fileID: 816671309} + - {fileID: 1390840085} --- !u!1 &1464556211 GameObject: m_ObjectHideFlags: 0 @@ -3568,6 +3661,19 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1762991479} m_CullTransparentMesh: 1 +--- !u!114 &1852085904 +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: 2a4a29c776298714c88f406ad39c6095, type: 3} + m_Name: + m_EditorClassIdentifier: + matchByName: 1 --- !u!1 &1885647142 GameObject: m_ObjectHideFlags: 0 @@ -3872,18 +3978,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1903798475} m_CullTransparentMesh: 1 ---- !u!114 &1951948666 -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: b3354e8208862c341940152f5340d41a, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &2014586909 GameObject: m_ObjectHideFlags: 0 @@ -4111,11 +4205,11 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: SelectedAccountIndex: 0 - SelectedStreamIndex: 8 - SelectedBranchIndex: 1 + SelectedStreamIndex: 0 + SelectedBranchIndex: 0 SelectedCommitIndex: 0 OldSelectedAccountIndex: 0 - OldSelectedStreamIndex: 8 + OldSelectedStreamIndex: 0 --- !u!114 &2060322469 MonoBehaviour: m_ObjectHideFlags: 0 @@ -4429,3 +4523,83 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2074142232} m_CullTransparentMesh: 1 +--- !u!1 &7221284245862074005 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7221284245862074011} + - component: {fileID: 7221284245862074008} + m_Layer: 0 + m_Name: GameObject (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &7221284245862074008 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7221284245862074005} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0bc895f6cb37b674995dc13b79783c55, type: 3} + m_Name: + m_EditorClassIdentifier: + k__BackingField: + rid: 6366722168162615296 + k__BackingField: + rid: 6366722168162615297 + k__BackingField: + rid: 6366722168162615298 + k__BackingField: + rid: 6366722168162615299 + references: + version: 2 + RefIds: + - rid: 6366722168162615296 + type: {class: AccountSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 1 + - rid: 6366722168162615297 + type: {class: StreamSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 2 + k__BackingField: 50 + k__BackingField: + rid: 6366722168162615296 + - rid: 6366722168162615298 + type: {class: BranchSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 3 + k__BackingField: 20 + k__BackingField: 15 + k__BackingField: + rid: 6366722168162615297 + - rid: 6366722168162615299 + type: {class: CommitSelection, ns: Speckle.ConnectorUnity, asm: Speckle.Connector} + data: + selectedIndex: 3 + k__BackingField: + rid: 6366722168162615298 +--- !u!4 &7221284245862074011 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7221284245862074005} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 10.1570635, y: -2.743827, z: 12.879177} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 10 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Assets/SpeckleReceiver.cs b/Assets/SpeckleReceiver.cs new file mode 100644 index 0000000..31c10b8 --- /dev/null +++ b/Assets/SpeckleReceiver.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Speckle.ConnectorUnity; +using Speckle.Core.Api; +using Speckle.Core.Credentials; +using Speckle.Core.Kits; +using Speckle.Core.Logging; +using Speckle.Core.Models; +using Speckle.Core.Transports; +using UnityEditor.VersionControl; +using UnityEngine; +using UnityEngine.Events; +using Task = System.Threading.Tasks.Task; + + +[ExecuteAlways] +[AddComponentMenu("Speckle/Speckle Receiver")] +[RequireComponent(typeof(RecursiveConverter))] +public class SpeckleReceiver : MonoBehaviour, ISerializationCallbackReceiver +{ + [field: SerializeReference] + public AccountSelection Account { get; protected set; } + + [field: SerializeReference] + public StreamSelection Stream { get; protected set; } + + [field: SerializeReference] + public BranchSelection Branch { get; protected set; } + + [field: SerializeReference] + public CommitSelection Commit { get; protected set; } + + public RecursiveConverter Converter { get; protected set; } + + private CancellationTokenSource cancellationTokenSource; + + public UnityEvent> OnProgressAction; + public UnityEvent OnErrorAction; + public UnityEvent OnTotalChildrenCountKnown; + public UnityEvent> OnComplete; + +#nullable enable + public void Awake() + { + Initialise(); + Account!.RefreshOptions(); + Converter = GetComponent(); + cancellationTokenSource = new CancellationTokenSource(); + } + + protected void Initialise() + { + Account ??= new AccountSelection(); + Stream ??= new StreamSelection(Account); + Branch ??= new BranchSelection(Stream); + Commit ??= new CommitSelection(Branch); + Stream.Initialise(); + Branch.Initialise(); + Commit.Initialise(); + if(Account.Options is not {Length: > 0}) + Account.RefreshOptions(); + } + + + public async void ReceiveAndConvert(CancellationToken token, Transform? parentDestination = null) + { + Account? account = Account.Selected; + if (account == null) throw new SpeckleException("Cannot receive: Selected Account is null"); + Client client = Account.Client ?? new Client(account); + Stream? stream = Stream.Selected; + if (stream == null) throw new SpeckleException("Cannot receive: Selected Stream is null"); + Commit? commit = Commit.Selected; + if (commit == null) throw new SpeckleException("Cannot receive: Selected Commit is null"); + + Base? commitObject = await ReceiveAsync( + token: token, + client: client, + streamId: stream.id, + objectId: commit.referencedObject, + commitId: commit.id, + onProgressAction: dict => OnProgressAction.Invoke(dict), + onErrorAction: (m, e) => OnErrorAction.Invoke(m, e), + onTotalChildrenCountKnown: c => OnTotalChildrenCountKnown.Invoke(c) + ); + + Dispatcher.Instance().Enqueue(() => + { + var converted = Converter.RecursivelyConvertToNative(commitObject, parentDestination); + OnComplete.Invoke(converted); + }); + } + + + public static async Task ReceiveAsync(CancellationToken token, + Client client, + string streamId, + string objectId, + string? commitId, + Action>? onProgressAction = null, + Action? onErrorAction = null, + Action? onTotalChildrenCountKnown = null) + { + Base? ret = null; + try + { + Analytics.TrackEvent(client.Account, Analytics.Events.Receive); + + if (token.IsCancellationRequested) token.ThrowIfCancellationRequested(); + + var transport = new ServerTransport(client.Account, streamId); + transport.CancellationToken = token; + + ret = await Operations.Receive( + objectId: objectId, + cancellationToken: token, + remoteTransport: transport, + onProgressAction: onProgressAction, + onErrorAction: onErrorAction, + onTotalChildrenCountKnown: onTotalChildrenCountKnown, + disposeTransports: true + ); + + if (token.IsCancellationRequested) token.ThrowIfCancellationRequested(); + + try + { + await client.CommitReceived(token, new CommitReceivedInput + { + streamId = streamId, + commitId = commitId, + message = $"received commit from {Application.unityVersion}", + sourceApplication = HostApplications.Unity.GetVersion(CoreUtils.GetHostAppVersion()) + }); + } + catch + { + // Do nothing! + } + } + catch(Exception e) + { + onErrorAction?.Invoke(e.Message, e); + throw; + } + + + return ret; + } + + public void OnDestroy() + { + cancellationTokenSource.Cancel(); + Account?.Dispose(); + } + + public void OnBeforeSerialize() + { + //pass + } + + public void OnAfterDeserialize() + { + Initialise(); + } +} diff --git a/Assets/SpeckleReceiver.cs.meta b/Assets/SpeckleReceiver.cs.meta new file mode 100644 index 0000000..b92935d --- /dev/null +++ b/Assets/SpeckleReceiver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0bc895f6cb37b674995dc13b79783c55 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/systems.speckle.speckle-unity/CommitSelection.cs b/Packages/systems.speckle.speckle-unity/CommitSelection.cs new file mode 100644 index 0000000..f36ae7b --- /dev/null +++ b/Packages/systems.speckle.speckle-unity/CommitSelection.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Speckle.Core.Api; +using Speckle.Core.Credentials; +using UnityEngine; + +#nullable enable +namespace Speckle.ConnectorUnity +{ + [Serializable] + public sealed class CommitSelection : OptionSelection + { + [field: SerializeReference] + public BranchSelection BranchSelection { get; private set; } + + public CommitSelection(BranchSelection branchSelection) + { + BranchSelection = branchSelection; + Initialise(); + + } + + public void Initialise() + { + BranchSelection.OnSelectionChange = RefreshOptions; + } + + protected override string? KeyFunction(Commit? value) => value?.id; + + public override void RefreshOptions() + { + Branch? branch = BranchSelection.Selected; + if (branch == null) return; + List commits = branch.commits.items; + GenerateOptions(commits, (_, i) => i == 0); + } + } + + [Serializable] + public sealed class BranchSelection : OptionSelection + { + [field: SerializeField, Range(1,100)] + public int BranchLimit { get; set; } = 30; + [field: SerializeField, Range(1,100)] + public int CommitLimit { get; set; } = 15; + + [field: SerializeReference] + public StreamSelection StreamSelection { get; private set; } + + public BranchSelection(StreamSelection streamSelection) + { + StreamSelection = streamSelection; + Initialise(); + } + + public void Initialise() + { + StreamSelection.OnSelectionChange = RefreshOptions; + } + + protected override string? KeyFunction(Branch? value) => value?.name; + + public override void RefreshOptions() + { + Stream? stream = StreamSelection.Selected; + if (stream == null) return; + List branches = StreamSelection.Client!.StreamGetBranches(stream.id, BranchLimit, CommitLimit).GetAwaiter().GetResult(); + GenerateOptions(branches, (b, _) => b.name == "main"); + } + } + + [Serializable] + public sealed class StreamSelection : OptionSelection + { + private const int DEFAULT_REQUEST_LIMIT = 50; + [field: SerializeField, Range(1,100)] + public int RequestLimit { get; set; } = DEFAULT_REQUEST_LIMIT; + + [field: SerializeReference] + public AccountSelection AccountSelection { get; private set; } + + public StreamSelection(AccountSelection accountSelection) + { + AccountSelection = accountSelection; + Initialise(); + } + + public void Initialise() + { + AccountSelection.OnSelectionChange = RefreshOptions; + } + + internal Client? Client => AccountSelection.Client; + + protected override string? KeyFunction(Stream? value) => value?.id; + public override void RefreshOptions() + { + if (Client == null) return; + List streams = Client.StreamsGet(RequestLimit).GetAwaiter().GetResult(); + GenerateOptions(streams, (_, i) => i == 0); + } + } + + + [Serializable] + public sealed class AccountSelection : OptionSelection, IDisposable + { + private Client? client; + public Client? Client + { + get + { + Account? account = Selected; + if (account == null) return client = null; + if (client == null || !client.Account.Equals(account)) return client = new Client(Selected); + return client; + } + } + + protected override string? KeyFunction(Account? value) => value?.id; + + public override void RefreshOptions() + { + GenerateOptions(AccountManager.GetAccounts().ToArray(), + isDefault: (a, _) => a.isDefault); + } + + public void Dispose() + { + client?.Dispose(); + } + } + + + [Serializable] + public abstract class OptionSelection where TOption : class + { + [SerializeField] + private int selectedIndex = -1; + + public int SelectedIndex + { + get => selectedIndex; + set + { + selectedIndex = value; + OnSelectionChange?.Invoke(); + } + } + + public TOption? Selected + { + get + { + if (Options == null) return null; + if (SelectedIndex < 0 || SelectedIndex >= Options.Length) return null; + return Options[SelectedIndex]; + } + } + + public TOption[] Options { get; protected set; } = Array.Empty(); + public Action? OnSelectionChange { get; set; } + + [return: NotNullIfNotNull("value")] + protected abstract string? KeyFunction(TOption? value); + + public abstract void RefreshOptions(); + + protected void GenerateOptions(IList source, Func isDefault) + { + List optionsToAdd = new List(source.Count); + int defaultOption = -1; + int index = 0; + foreach (TOption? a in source) + { + if (a == null) continue; + optionsToAdd.Add(a); + if (isDefault(a, index)) defaultOption = index; + index++; + } + + TOption? currentSelected = Selected; + bool selectionOutOfRange = SelectedIndex < 0 || SelectedIndex >= optionsToAdd.Count; + if (selectionOutOfRange + || (currentSelected != null + && KeyFunction(currentSelected) != KeyFunction(optionsToAdd[SelectedIndex]))) + { + selectedIndex = defaultOption; + } + + Options = optionsToAdd.ToArray(); + Debug.Log($"{this.GetType()} updated"); + OnSelectionChange?.Invoke(); + } + + } + + + + + // [Serializable] + // public abstract class OptionSelection : ISerializationCallbackReceiver where TOption : class + // { + // [SerializeReference] + // private int selectedIndex = -1; + // [SerializeReference] + // protected string? selectedId; + // public TOption? Selected + // { + // get + // { + // if (selectedIndex == -1) return null; + // if(Options.Any(a => a.Equals()) + // if (!Options.ContainsKey(selectedId)) return null; + // return Options[selectedId]; + // } + // set + // { + // selectedId = KeyFunction(value); + // OnSelectionChange?.Invoke(); + // } + // } + // + // //public IDictionary Options { get; protected set; } = new Dictionary(); + // public TOption[] Options { get; protected set; } = Array.Empty(); + // internal Action? OnSelectionChange { get; set; } + // + // [return: NotNullIfNotNull("value")] + // protected abstract string? KeyFunction(TOption? value); + // + // public abstract void RefreshOptions(); + // + // protected void GenerateOptions(IEnumerable source, Func isDefault) + // { + // Options.Clear(); + // TOption? defaultOption = null; + // int index = 0; + // foreach (TOption? a in source) + // { + // if (a == null) continue; + // Options.TryAdd(KeyFunction(a), a); + // if (isDefault(a, index)) defaultOption = a; + // index++; + // } + // + // if (selectedId == null || !Options.ContainsKey(selectedId)) + // { + // Selected = defaultOption; + // } + // } + // + // public virtual void OnAfterDeserialize() + // { + // RefreshOptions(); + // } + // + // public virtual void OnBeforeSerialize() { /*pass*/ } + // } +} diff --git a/Packages/systems.speckle.speckle-unity/CommitSelection.cs.meta b/Packages/systems.speckle.speckle-unity/CommitSelection.cs.meta new file mode 100644 index 0000000..fbb11e9 --- /dev/null +++ b/Packages/systems.speckle.speckle-unity/CommitSelection.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91a60a3a0e5293d499e746c08d071b28 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/systems.speckle.speckle-unity/Editor/StreamSelectionEditor.cs b/Packages/systems.speckle.speckle-unity/Editor/StreamSelectionEditor.cs new file mode 100644 index 0000000..05cc4a9 --- /dev/null +++ b/Packages/systems.speckle.speckle-unity/Editor/StreamSelectionEditor.cs @@ -0,0 +1,263 @@ + +using System; +using System.Collections.Generic; +using Speckle.Core.Api; +using Speckle.Core.Credentials; +using UnityEditor; +using UnityEditor.Experimental.GraphView; +using UnityEditor.UIElements; +using UnityEngine; + +#nullable enable +namespace Speckle.ConnectorUnity +{ + [CustomPropertyDrawer(typeof(AccountSelection))] + public sealed class AccountSelectionDrawer : OptionSelectionDrawer + { + protected override bool DisplayRefresh => true; + protected override string FormatOption(Account o) => $"{o.userInfo.email} | {o.serverInfo.name}"; + protected override int GUIDetailsPropertyCount => 4; + protected override void OnGUIDetails(Rect position, SerializedProperty property, GUIContent label, Account selection) + { + EditorGUI.BeginDisabledGroup(true); + position.height = DetailsTextHeight; + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Id", selection.userInfo.id); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Name", selection.userInfo.name); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Server", selection.serverInfo.name); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "URL", selection.serverInfo.url); + + EditorGUI.EndDisabledGroup(); + } + } + + [CustomPropertyDrawer(typeof(StreamSelection))] + public sealed class StreamSelectionDrawer : OptionSelectionDrawer + { + protected override bool DisplayRefresh => true; + + protected override string FormatOption(Stream o) => $"{o.name}"; + protected override int GUIDetailsPropertyCount => 7; + + protected override void OnGUIDetails(Rect position, SerializedProperty property, GUIContent label, Stream selection) + { + EditorGUI.BeginDisabledGroup(true); + position.height = DetailsTextHeight; + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Stream Id", selection.id); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Description", selection.description); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Is Public", selection.isPublic.ToString()); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Role", selection.role); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Created At", selection.createdAt); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Updated At", selection.updatedAt); + + EditorGUI.EndDisabledGroup(); + + position.y += DetailsTextHeight; + var nameField = EditorGUI.PropertyField(position, property.FindPropertyRelative($"<{nameof(StreamSelection.RequestLimit)}>k__BackingField")); + + } + } + + [CustomPropertyDrawer(typeof(BranchSelection))] + public sealed class BranchSelectionDrawer : OptionSelectionDrawer + { + protected override bool DisplayRefresh => true; + + protected override string FormatOption(Branch o) => $"{o.name}"; + protected override int GUIDetailsPropertyCount => 1; + + protected override void OnGUIDetails(Rect position, SerializedProperty property, GUIContent label, Branch selection) + { + EditorGUI.BeginDisabledGroup(true); + position.height = DetailsTextHeight; + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Description", selection.description); + + EditorGUI.EndDisabledGroup(); + } + } + + [CustomPropertyDrawer(typeof(CommitSelection))] + public sealed class CommitSelectionDrawer : OptionSelectionDrawer + { + protected override string FormatOption(Commit o) => $"{o.message} - {o.id}"; + protected override int GUIDetailsPropertyCount => 5; + + protected override void OnGUIDetails(Rect position, SerializedProperty property, GUIContent label, Commit selection) + { + EditorGUI.BeginDisabledGroup(true); + position.height = DetailsTextHeight; + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Commit Id", selection.id); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Author Name", selection.authorName); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Created At", selection.createdAt); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Source Application", selection.sourceApplication); + + position.y += DetailsTextHeight; + EditorGUI.TextField(position, "Reference Object Id", selection.referencedObject); + + EditorGUI.EndDisabledGroup(); + + } + } + + + public abstract class OptionSelectionDrawer : PropertyDrawer where TOption : class + { + private const float RefreshButtonWidthScale = 0.2f; + private const float PrefixIndentation = 100f; + protected readonly float DetailsTextHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing; + private bool foldOutStatus = false; + protected virtual bool DisplayRefresh => false; + protected abstract string FormatOption(TOption o); + protected abstract int GUIDetailsPropertyCount { get; } + + private string[] GetFormattedOptions(TOption[] options) + { + int optionsCount = options.Length; + string[] choices = new string[optionsCount]; + for (int i = 0; i < optionsCount; i++) + { + choices[i] = FormatOption(options[i]); + } + + return choices; + } + + protected abstract void OnGUIDetails(Rect position, SerializedProperty property, GUIContent label, TOption selection); + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + EditorGUI.BeginProperty(position, label, property); + var t = (OptionSelection)fieldInfo.GetValue(property.serializedObject.targetObject); + + var selectionRect = position;//EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); + selectionRect.x += PrefixIndentation + 5; + selectionRect.width -= PrefixIndentation + 5; + + var popupSize = DisplayRefresh + ? new Rect(selectionRect.x, selectionRect.y, selectionRect.width * (1-RefreshButtonWidthScale), DetailsTextHeight) + : selectionRect; + //TODO: fancy popup + + var selectedOption = t.Selected; + string selectedChoice = selectedOption != null ? FormatOption(selectedOption) : ""; + + if (GUI.Button(popupSize, selectedChoice, EditorStyles.popup)) + { + var windowPos = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); + var provider = ScriptableObject.CreateInstance(); + provider.Title = typeof(TOption).Name; + provider.listItems = GetFormattedOptions(t.Options);; + provider.onSetIndexCallback = o => { t.SelectedIndex = o;}; + SearchWindow.Open(new SearchWindowContext(windowPos), provider); + } + + if (DisplayRefresh) + { + var buttonSize = new Rect(selectionRect.x + popupSize.width , selectionRect.y, selectionRect.width * RefreshButtonWidthScale, DetailsTextHeight); + if (GUI.Button(buttonSize, "Refresh")) + { + t.RefreshOptions(); + } + } + + //TODO drop down with details + //EditorGUI.DropdownButton(position, "TEST", FocusType.Passive); + + //position.y += DetailsTextHeight; + { // Details drop down + int visiblePropCount = property.isExpanded ? GUIDetailsPropertyCount : 0; + var detailsHeight = new Vector2(PrefixIndentation, DetailsTextHeight + visiblePropCount * DetailsTextHeight); + var foldoutRect = new Rect(position.position, detailsHeight); + property.isExpanded = EditorGUI.BeginFoldoutHeaderGroup(foldoutRect, property.isExpanded, label); + if (property.isExpanded && selectedOption != null) + { + EditorGUI.indentLevel++; + OnGUIDetails(position, property, label, selectedOption); + EditorGUI.indentLevel--; + } + EditorGUI.EndFoldoutHeaderGroup(); + } + + EditorGUI.EndProperty(); + EditorUtility.SetDirty(property.serializedObject.targetObject); + + } + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var standardHeight = EditorGUIUtility.singleLineHeight; + + if (!property.isExpanded) return standardHeight; + + var detailsHeight = GUIDetailsPropertyCount * (standardHeight + EditorGUIUtility.standardVerticalSpacing); + + return standardHeight + detailsHeight; + } + + } + + #nullable disable + + public sealed class StringListSearchProvider : ScriptableObject, ISearchWindowProvider + { + public string Title { get; set; } + public string[] listItems; + + public Action onSetIndexCallback; + + public List CreateSearchTree(SearchWindowContext context) + { + List searchList = new(listItems.Length + 1) {new SearchTreeGroupEntry(new GUIContent(Title), 0)}; + + for(int i = 0; i < listItems.Length; i++) + { + SearchTreeEntry entry = new SearchTreeEntry(new GUIContent(listItems[i])) + { + level = 1, + userData = i + }; + searchList.Add(entry); + } + + return searchList; + } + + public bool OnSelectEntry(SearchTreeEntry SearchTreeEntry, SearchWindowContext context) + { + onSetIndexCallback?.Invoke((int)SearchTreeEntry.userData); + + return true; + } + } + + + +} diff --git a/Packages/systems.speckle.speckle-unity/Editor/StreamSelectionEditor.cs.meta b/Packages/systems.speckle.speckle-unity/Editor/StreamSelectionEditor.cs.meta new file mode 100644 index 0000000..9f14be3 --- /dev/null +++ b/Packages/systems.speckle.speckle-unity/Editor/StreamSelectionEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc44e364c0d7b8a4c95f87d0210054be +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: