diff --git a/.gitignore b/.gitignore index e61209c..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,260 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -bin/ -obj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# General -*.orig -*.bak - -dll/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 80a7021..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: csharp -solution: Unity-Weld.sln - -script: - - msbuild /p:Configuration=Debug Unity-Weld.sln \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 74cdcb0..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,53 +0,0 @@ -# How to contribute to Unity Weld - -👍🎉 First off, thanks for taking the time to contribute! 🎉👍 - - -## Reporting bugs - -Any bug reports are useful, although there are a few things you can do that will -make it easier for maintainers and other users to understand your report, -reproduce the behaviour, and find related reports. - - - **Use a clear and descriptive title** for the issue to identify the problem. - - **Describe the exact steps which reproduce the problem** in as many details - as possible. Code examples or links to repos to reproduce the issue are - always helpful. - - **Describe the behaviour you observed after following the steps** and point - out what exactly the problem is with that behaviour. - - **Explain which behaviour you expected to see instead and why.** This makes - it easier to work out what went wrong and whether the behaviour was in fact - a bug or was by design. - - **Check the existing open issues on GitHub** to see if someone else has had - the same problem as you. - -Make sure to file bugs as [GitHub issues](https://github.com/Real-Serious-Games/Unity-Weld/issues), -since that way everyone working on the library can see it and potentially help. - - -## Pull requests - -All new code committed to the repository must be reviewed by at least one other -person on the Unity Weld team before it can be merged. - -Before we merge pull requests there are a few things we look for to make sure -the code is maintainable and up the same standard as the rest of the library. - - - Make sure that the [examples project](https://github.com/Real-Serious-Games/Unity-Weld-Examples) - still works correctly with the new changes. - - Check that your code conforms to the same style as existing code. Ensure that - your editor is set up to read the [.editorconfig](http://editorconfig.org/) - file for consistent spacing and line endings. We also try to keep our code - style consistent with the [Microsoft C# Coding Conventions](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/inside-a-program/coding-conventions) - and [Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/index). - - Make sure that the [Travis CI build](https://travis-ci.org/Real-Serious-Games/Unity-Weld) - succeeds. This should run automatically when you create a pull request, but - should have the same result as building the solution and running all the - tests locally. We will not accept any pull requests that fail to build or - contain failing tests. - - If you have added a new feature, add a section to README.md describing the - feature and how to use it. - -In addition, if your pull request breaks any existing functionality you'll need -to provide a good justification for why it's worth breaking backwards -compatibility. \ No newline at end of file diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..67ad180 --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 72f2efac3eed8eb4f98c8797f42bf210 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/AnimatorParameterBindingEditor.cs b/Editor/AnimatorParameterBindingEditor.cs similarity index 90% rename from UnityWeld_Editor/AnimatorParameterBindingEditor.cs rename to Editor/AnimatorParameterBindingEditor.cs index d4c2e01..8120a88 100644 --- a/UnityWeld_Editor/AnimatorParameterBindingEditor.cs +++ b/Editor/AnimatorParameterBindingEditor.cs @@ -21,17 +21,12 @@ public class AnimatorParameterBindingEditor : BaseBindingEditor private bool viewModelPropertyPrefabModified; private bool viewPropertyPrefabModified; - private void OnEnable() + protected override void OnEnabled() { // Initialise reference to target script targetScript = (AnimatorParameterBinding)target; - Type adapterType; - - viewAdapterOptionsFade = new AnimBool( - ShouldShowAdapterOptions(targetScript.ViewAdapterTypeName, out adapterType) - ); - + viewAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ViewAdapterId, out _)); viewAdapterOptionsFade.valueChanged.AddListener(Repaint); } @@ -40,17 +35,11 @@ private void OnDisable() viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); } - public override void OnInspectorGUI() + protected override void OnInspector() { - if(CannotModifyInPlayMode()) - { - return; - } - UpdatePrefabModifiedProperties(); - var defaultLabelStyle = EditorStyles.label.fontStyle; - EditorStyles.label.fontStyle = viewPropertyPrefabModified ? FontStyle.Bold : defaultLabelStyle; + EditorStyles.label.fontStyle = viewPropertyPrefabModified ? FontStyle.Bold : DefaultFontStyle; var animatorParameters = GetAnimatorParameters(); @@ -60,7 +49,6 @@ public override void OnInspectorGUI() return; } - Type viewPropertyType; ShowAnimatorParametersMenu( new GUIContent("View property", "Property on the view to bind to"), updatedValue => @@ -70,7 +58,7 @@ public override void OnInspectorGUI() }, new AnimatorParameterTypeAndName(targetScript.AnimatorParameterName, targetScript.AnimatorParameterType), animatorParameters, - out viewPropertyType + out var viewPropertyType ); // Don't let the user set anything else until they've chosen a view property. @@ -80,39 +68,36 @@ out viewPropertyType GUI.enabled = false; } - var viewAdapterTypeNames = GetAdapterTypeNames( - type => viewPropertyType == null || - TypeResolver.FindAdapterAttribute(type).OutputType == viewPropertyType - ); + var viewAdapterTypeNames = TypeResolver.GetAdapterIds( + adapterInfo => viewPropertyType == null || adapterInfo.OutType == viewPropertyType); - EditorStyles.label.fontStyle = viewAdapterPrefabModified ? FontStyle.Bold : defaultLabelStyle; + EditorStyles.label.fontStyle = viewAdapterPrefabModified ? FontStyle.Bold : DefaultFontStyle; ShowAdapterMenu( new GUIContent("View adapter", "Adapter that converts values sent from the view-model to the view."), viewAdapterTypeNames, - targetScript.ViewAdapterTypeName, + targetScript.ViewAdapterId, newValue => { // Get rid of old adapter options if we changed the type of the adapter. - if (newValue != targetScript.ViewAdapterTypeName) + if (newValue != targetScript.ViewAdapterId) { Undo.RecordObject(targetScript, "Set view adapter options"); targetScript.ViewAdapterOptions = null; } UpdateProperty( - updatedValue => targetScript.ViewAdapterTypeName = updatedValue, - targetScript.ViewAdapterTypeName, + updatedValue => targetScript.ViewAdapterId = updatedValue, + targetScript.ViewAdapterId, newValue, "Set view adapter" ); } ); - Type adapterType; - viewAdapterOptionsFade.target = ShouldShowAdapterOptions(targetScript.ViewAdapterTypeName, out adapterType); + viewAdapterOptionsFade.target = ShouldShowAdapterOptions(targetScript.ViewAdapterId, out var adapterType); - EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified ? FontStyle.Bold : defaultLabelStyle; + EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified ? FontStyle.Bold : DefaultFontStyle; ShowAdapterOptionsMenu( "View adapter options", @@ -124,9 +109,9 @@ out viewPropertyType EditorGUILayout.Space(); - EditorStyles.label.fontStyle = viewModelPropertyPrefabModified ? FontStyle.Bold : defaultLabelStyle; + EditorStyles.label.fontStyle = viewModelPropertyPrefabModified ? FontStyle.Bold : DefaultFontStyle; - var adaptedViewPropertyType = AdaptTypeBackward(viewPropertyType, targetScript.ViewAdapterTypeName); + var adaptedViewPropertyType = AdaptTypeBackward(viewPropertyType, targetScript.ViewAdapterId); ShowViewModelPropertyMenu( new GUIContent("View-model property", "Property on the view-model to bind to."), TypeResolver.FindBindableProperties(targetScript), @@ -137,8 +122,6 @@ out viewPropertyType GUI.enabled = guiPreviouslyEnabled; - EditorStyles.label.fontStyle = defaultLabelStyle; - EditorGUILayout.Space(); } diff --git a/Editor/AnimatorParameterBindingEditor.cs.meta b/Editor/AnimatorParameterBindingEditor.cs.meta new file mode 100644 index 0000000..bade969 --- /dev/null +++ b/Editor/AnimatorParameterBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 922b7495c089af14c907aceb395b0cba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/BaseBindingEditor.cs b/Editor/BaseBindingEditor.cs similarity index 85% rename from UnityWeld_Editor/BaseBindingEditor.cs rename to Editor/BaseBindingEditor.cs index 1339474..e335f31 100644 --- a/UnityWeld_Editor/BaseBindingEditor.cs +++ b/Editor/BaseBindingEditor.cs @@ -1,390 +1,410 @@ -using System; -using System.Linq; -using System.Reflection; -using UnityEditor; -using UnityEngine; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; - -namespace UnityWeld_Editor -{ - /// - /// A base editor for Unity-Weld bindings. - /// - public class BaseBindingEditor : Editor - { - /// - /// Sets the specified value and sets dirty to true if it doesn't match the old value. - /// - protected void UpdateProperty(Action setter, TValue oldValue, TValue newValue, string undoActionName) - where TValue : class - { - if (newValue == oldValue) - { - return; - } - - Undo.RecordObject(target, undoActionName); - - setter(newValue); - - InspectorUtils.MarkSceneDirty(((Component)target).gameObject); - } - - /// - /// Display the adapters popup menu. - /// - protected static void ShowAdapterMenu( - GUIContent label, - string[] adapterTypeNames, - string curValue, - Action valueUpdated - ) - { - var adapterMenu = new[] { "None" } - .Concat(adapterTypeNames) - .Select(typeName => new GUIContent(typeName)) - .ToArray(); - - var curSelectionIndex = Array.IndexOf(adapterTypeNames, curValue) + 1; // +1 to account for 'None'. - var newSelectionIndex = EditorGUILayout.Popup( - label, - curSelectionIndex, - adapterMenu - ); - - if (newSelectionIndex == curSelectionIndex) - { - return; - } - - if (newSelectionIndex == 0) - { - valueUpdated(null); // No adapter selected. - } - else - { - valueUpdated(adapterTypeNames[newSelectionIndex - 1]); // -1 to account for 'None'. - } - } - - /// - /// Display a popup menu for selecting a property from a view-model. - /// - protected void ShowViewModelPropertyMenu( - GUIContent label, - BindableMember[] bindableProperties, - Action propertyValueSetter, - string curPropertyValue, - Func menuEnabled - ) - { - InspectorUtils.DoPopup( - new GUIContent(curPropertyValue), - label, - prop => string.Concat(prop.ViewModelType, "/", prop.MemberName, " : ", prop.Member.PropertyType.Name), - prop => menuEnabled(prop.Member), - prop => prop.ToString() == curPropertyValue, - prop => - { - UpdateProperty( - propertyValueSetter, - curPropertyValue, - prop.ToString(), - "Set view-model property" - ); - }, - bindableProperties - .OrderBy(property => property.ViewModelTypeName) - .ThenBy(property => property.MemberName) - .ToArray() - ); - } - - /// - /// Class used to wrap property infos - /// - private class OptionInfo - { - public OptionInfo(string menuName, BindableMember property) - { - this.MenuName = menuName; - this.Property = property; - } - - public string MenuName { get; private set; } - - public BindableMember Property { get; private set; } - } - - /// - /// The string used to show that no option is selected in the property menu. - /// - private static readonly string NoneOptionString = "None"; - - /// - /// Display a popup menu for selecting a property from a view-model. - /// - protected void ShowViewModelPropertyMenuWithNone( - GUIContent label, - BindableMember[] bindableProperties, - Action propertyValueSetter, - string curPropertyValue, - Func menuEnabled - ) - { - var options = bindableProperties - .Select(prop => new OptionInfo( - string.Concat(prop.ViewModelType, "/", prop.MemberName, " : ", prop.Member.PropertyType.Name), - prop - )) - .OrderBy(option => option.Property.ViewModelTypeName) - .ThenBy(option => option.Property.MemberName); - - var noneOption = new OptionInfo(NoneOptionString, null); - - InspectorUtils.DoPopup( - new GUIContent(string.IsNullOrEmpty(curPropertyValue) ? NoneOptionString : curPropertyValue), - label, - option => option.MenuName, - option => option.MenuName == NoneOptionString ? true : menuEnabled(option.Property.Member), - option => - { - if (option == noneOption) - { - return string.IsNullOrEmpty(curPropertyValue); - } - - return option.ToString() == curPropertyValue; - }, - option => UpdateProperty( - propertyValueSetter, - curPropertyValue, - option.Property == null ? string.Empty : option.ToString(), - "Set view-model property" - ), - new[] { noneOption } - .Concat(options) - .ToArray() - ); - } - - /// - /// Shows a dropdown for selecting a property in the UI to bind to. - /// - protected void ShowViewPropertyMenu( - GUIContent label, - BindableMember[] properties, - Action propertyValueSetter, - string curPropertyValue, - out Type selectedPropertyType - ) - { - var propertyNames = properties - .Select(m => m.ToString()) - .ToArray(); - var selectedIndex = Array.IndexOf(propertyNames, curPropertyValue); - var content = properties.Select(prop => new GUIContent(string.Concat( - prop.ViewModelTypeName, - "/", - prop.MemberName, - " : ", - prop.Member.PropertyType.Name - ))) - .ToArray(); - - var newSelectedIndex = EditorGUILayout.Popup(label, selectedIndex, content); - if (newSelectedIndex != selectedIndex) - { - var newSelectedProperty = properties[newSelectedIndex]; - - UpdateProperty( - propertyValueSetter, - curPropertyValue, - newSelectedProperty.ToString(), - "Set view property" - ); - - selectedPropertyType = newSelectedProperty.Member.PropertyType; - } - else - { - if (selectedIndex < 0) - { - selectedPropertyType = null; - return; - } - - selectedPropertyType = properties[selectedIndex].Member.PropertyType; - } - } - - /// - /// Show dropdown for selecting a UnityEvent to bind to. - /// - protected void ShowEventMenu( - BindableEvent[] events, - Action propertyValueSetter, - string curPropertyValue - ) - { - var eventNames = events - .Select(BindableEventToString) - .ToArray(); - var selectedIndex = Array.IndexOf(eventNames, curPropertyValue); - var content = events - .Select(evt => new GUIContent(evt.ComponentType.Name + "." + evt.Name)) - .ToArray(); - - var newSelectedIndex = EditorGUILayout.Popup( - new GUIContent("View event", "Event on the view to bind to."), - selectedIndex, - content - ); - - if (newSelectedIndex == selectedIndex) - { - return; - } - - var selectedEvent = events[newSelectedIndex]; - UpdateProperty( - propertyValueSetter, - curPropertyValue, - BindableEventToString(selectedEvent), - "Set bound event" - ); - } - - /// - /// Returns whether or not we should show an adapter options selector for the specified - /// adapter type and finds the type for the specified type name. - /// - protected static bool ShouldShowAdapterOptions(string adapterTypeName, out Type adapterType) - { - // Don't show selector until an adapter has been selected. - if (string.IsNullOrEmpty(adapterTypeName)) - { - adapterType = null; - return false; - } - - var adapterAttribute = FindAdapterAttribute(adapterTypeName); - if (adapterAttribute == null) - { - adapterType = null; - return false; - } - - adapterType = adapterAttribute.OptionsType; - - // Don't show selector unless the current adapter has its own overridden - // adapter options type. - return adapterType != typeof(AdapterOptions); - } - - /// - /// Show a field for selecting an AdapterOptions object matching the specified type of adapter. - /// - protected void ShowAdapterOptionsMenu( - string label, - Type adapterOptionsType, - Action propertyValueSetter, - AdapterOptions currentPropertyValue, - float fadeAmount - ) - { - if (EditorGUILayout.BeginFadeGroup(fadeAmount)) - { - EditorGUI.indentLevel++; - - var newAdapterOptions = (AdapterOptions)EditorGUILayout.ObjectField( - label, - currentPropertyValue, - adapterOptionsType, - false - ); - - EditorGUI.indentLevel--; - - UpdateProperty( - propertyValueSetter, - currentPropertyValue, - newAdapterOptions, - "Set adapter options" - ); - } - EditorGUILayout.EndFadeGroup(); - } - - /// - /// Displays helpbox in inspector if the editor is playing, and returns the same thing - /// - protected static bool CannotModifyInPlayMode() - { - if (EditorApplication.isPlaying) - { - EditorGUILayout.HelpBox("Exit play mode to make changes.", MessageType.Info); - return true; - } - return false; - } - - /// - /// Find the adapter attribute for a named adapter type. - /// - protected static AdapterAttribute FindAdapterAttribute(string adapterName) - { - if (!string.IsNullOrEmpty(adapterName)) - { - var adapterType = TypeResolver.FindAdapterType(adapterName); - if (adapterType != null) - { - return TypeResolver.FindAdapterAttribute(adapterType); - } - } - - return null; - } - - /// - /// Pass a type through an adapter and get the result. - /// - protected static Type AdaptTypeBackward(Type inputType, string adapterName) - { - var adapterAttribute = FindAdapterAttribute(adapterName); - - return adapterAttribute != null ? adapterAttribute.InputType : inputType; - } - - /// - /// Pass a type through an adapter and get the result. - /// - protected static Type AdaptTypeForward(Type inputType, string adapterName) - { - var adapterAttribute = FindAdapterAttribute(adapterName); - - return adapterAttribute != null ? adapterAttribute.OutputType : inputType; - } - - /// - /// Convert a BindableEvent to a uniquely identifiable string. - /// - private static string BindableEventToString(BindableEvent evt) - { - return string.Concat(evt.ComponentType.ToString(), ".", evt.Name); - } - - /// - /// Returns an array of all the names of adapter types that match the - /// provided prediate function. - /// - protected static string[] GetAdapterTypeNames(Func adapterSelectionPredicate) - { - return TypeResolver.TypesWithAdapterAttribute - .Where(adapterSelectionPredicate) - .Select(type => type.ToString()) - .ToArray(); - } - } -} +using System; +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + /// + /// A base editor for Unity-Weld bindings. + /// + public abstract class BaseBindingEditor : Editor + { + protected FontStyle DefaultFontStyle; + private SerializedProperty _autoConnectionProperty; + + /// + /// Sets the specified value and sets dirty to true if it doesn't match the old value. + /// + protected void UpdateProperty(Action setter, TValue oldValue, TValue newValue, string undoActionName) + where TValue : class + { + if (newValue == oldValue) + { + return; + } + + Undo.RecordObject(target, undoActionName); + + setter(newValue); + + InspectorUtils.MarkSceneDirty(((Component)target).gameObject); + } + + /// + /// Display the adapters popup menu. + /// + protected static void ShowAdapterMenu( + GUIContent label, + string[] adapterTypeNames, + string curValue, + Action valueUpdated + ) + { + var adapterMenu = new[] { "None" } + .Concat(adapterTypeNames) + .Select(typeName => new GUIContent(typeName)) + .ToArray(); + + var curSelectionIndex = Array.IndexOf(adapterTypeNames, curValue) + 1; // +1 to account for 'None'. + var newSelectionIndex = EditorGUILayout.Popup( + label, + curSelectionIndex, + adapterMenu + ); + + if (newSelectionIndex == curSelectionIndex) + { + return; + } + + if (newSelectionIndex == 0) + { + valueUpdated(null); // No adapter selected. + } + else + { + valueUpdated(adapterTypeNames[newSelectionIndex - 1]); // -1 to account for 'None'. + } + } + + protected abstract void OnEnabled(); + protected abstract void OnInspector(); + + private void OnEnable() + { + _autoConnectionProperty = serializedObject.FindProperty("_isAutoConnection"); + OnEnabled(); + } + + public sealed override void OnInspectorGUI() + { + serializedObject.Update(); + + if (CannotModifyInPlayMode()) + { + GUI.enabled = false; + } + + DefaultFontStyle = EditorStyles.label.fontStyle; + + if (_autoConnectionProperty != null) + { + EditorGUILayout.PropertyField(_autoConnectionProperty); + } + + OnInspector(); + + serializedObject.ApplyModifiedProperties(); + + EditorStyles.label.fontStyle = DefaultFontStyle; + } + + protected void ShowAutoConnection(bool currentValue, Action valueSetter) + { + var newValue = EditorGUILayout.Toggle("Auto Connection", currentValue); + if (newValue != currentValue) + { + valueSetter(newValue); + } + } + + /// + /// Display a popup menu for selecting a property from a view-model. + /// + protected void ShowViewModelPropertyMenu( + GUIContent label, + BindableMember[] bindableProperties, + Action propertyValueSetter, + string curPropertyValue, + Func menuEnabled + ) + { + InspectorUtils.DoPopup( + new GUIContent(curPropertyValue), + label, + prop => string.Concat(prop.ViewModelType, "/", prop.MemberName, " : ", prop.Member.PropertyType.Name), + prop => menuEnabled(prop.Member), + prop => prop.ToString() == curPropertyValue, + prop => + { + UpdateProperty( + propertyValueSetter, + curPropertyValue, + prop.ToString(), + "Set view-model property" + ); + }, + bindableProperties + .OrderBy(property => property.ViewModelTypeName) + .ThenBy(property => property.MemberName) + .ToArray() + ); + } + + /// + /// Class used to wrap property infos + /// + private class OptionInfo + { + public OptionInfo(string menuName, BindableMember property) + { + this.MenuName = menuName; + this.Property = property; + } + + public string MenuName { get; private set; } + + public BindableMember Property { get; private set; } + } + + /// + /// The string used to show that no option is selected in the property menu. + /// + private static readonly string NoneOptionString = "None"; + + /// + /// Display a popup menu for selecting a property from a view-model. + /// + protected void ShowViewModelPropertyMenuWithNone( + GUIContent label, + BindableMember[] bindableProperties, + Action propertyValueSetter, + string curPropertyValue, + Func menuEnabled + ) + { + var options = bindableProperties + .Select(prop => new OptionInfo( + string.Concat(prop.ViewModelType, "/", prop.MemberName, " : ", prop.Member.PropertyType.Name), + prop + )) + .OrderBy(option => option.Property.ViewModelTypeName) + .ThenBy(option => option.Property.MemberName); + + var noneOption = new OptionInfo(NoneOptionString, null); + + InspectorUtils.DoPopup( + new GUIContent(string.IsNullOrEmpty(curPropertyValue) ? NoneOptionString : curPropertyValue), + label, + option => option.MenuName, + option => option.MenuName == NoneOptionString ? true : menuEnabled(option.Property.Member), + option => + { + if (option == noneOption) + { + return string.IsNullOrEmpty(curPropertyValue); + } + + return option.ToString() == curPropertyValue; + }, + option => UpdateProperty( + propertyValueSetter, + curPropertyValue, + option.Property == null ? string.Empty : option.ToString(), + "Set view-model property" + ), + new[] { noneOption } + .Concat(options) + .ToArray() + ); + } + + /// + /// Shows a dropdown for selecting a property in the UI to bind to. + /// + protected void ShowViewPropertyMenu( + GUIContent label, + BindableMember[] properties, + Action propertyValueSetter, + string curPropertyValue, + out Type selectedPropertyType + ) + { + var propertyNames = properties + .Select(m => m.ToString()) + .ToArray(); + var selectedIndex = Array.IndexOf(propertyNames, curPropertyValue); + var content = properties.Select(prop => new GUIContent(string.Concat( + prop.ViewModelTypeName, + "/", + prop.MemberName, + " : ", + prop.Member.PropertyType.Name + ))) + .ToArray(); + + var newSelectedIndex = EditorGUILayout.Popup(label, selectedIndex, content); + if (newSelectedIndex != selectedIndex) + { + var newSelectedProperty = properties[newSelectedIndex]; + + UpdateProperty( + propertyValueSetter, + curPropertyValue, + newSelectedProperty.ToString(), + "Set view property" + ); + + selectedPropertyType = newSelectedProperty.Member.PropertyType; + } + else + { + if (selectedIndex < 0) + { + selectedPropertyType = null; + return; + } + + selectedPropertyType = properties[selectedIndex].Member.PropertyType; + } + } + + /// + /// Show dropdown for selecting a UnityEvent to bind to. + /// + protected void ShowEventMenu( + BindableEvent[] events, + Action propertyValueSetter, + string curPropertyValue + ) + { + var eventNames = events + .Select(BindableEventToString) + .ToArray(); + var selectedIndex = Array.IndexOf(eventNames, curPropertyValue); + var content = events + .Select(evt => new GUIContent(evt.ComponentType.Name + "." + evt.Name)) + .ToArray(); + + var newSelectedIndex = EditorGUILayout.Popup( + new GUIContent("View event", "Event on the view to bind to."), + selectedIndex, + content + ); + + if (newSelectedIndex == selectedIndex) + { + return; + } + + var selectedEvent = events[newSelectedIndex]; + UpdateProperty( + propertyValueSetter, + curPropertyValue, + BindableEventToString(selectedEvent), + "Set bound event" + ); + } + + /// + /// Returns whether or not we should show an adapter options selector for the specified + /// adapter type and finds the type for the specified type name. + /// + protected static bool ShouldShowAdapterOptions(string adapterId, out Type adapterType) + { + adapterType = null; + + // Don't show selector until an adapter has been selected. + if (string.IsNullOrEmpty(adapterId)) + { + return false; + } + + if (!TypeResolver.TryGetAdapter(adapterId, out var adapterInfo)) + { + return false; + } + + adapterType = adapterInfo.OptionsType; + + // Don't show selector unless the current adapter has its own overridden + // adapter options type. + return adapterType != typeof(AdapterOptions); + } + + /// + /// Show a field for selecting an AdapterOptions object matching the specified type of adapter. + /// + protected void ShowAdapterOptionsMenu( + string label, + Type adapterOptionsType, + Action propertyValueSetter, + AdapterOptions currentPropertyValue, + float fadeAmount + ) + { + if (EditorGUILayout.BeginFadeGroup(fadeAmount)) + { + EditorGUI.indentLevel++; + + var newAdapterOptions = (AdapterOptions)EditorGUILayout.ObjectField( + label, + currentPropertyValue, + adapterOptionsType, + false + ); + + EditorGUI.indentLevel--; + + UpdateProperty( + propertyValueSetter, + currentPropertyValue, + newAdapterOptions, + "Set adapter options" + ); + } + EditorGUILayout.EndFadeGroup(); + } + + /// + /// Displays helpbox in inspector if the editor is playing, and returns the same thing + /// + protected static bool CannotModifyInPlayMode() + { + if (EditorApplication.isPlaying) + { + EditorGUILayout.HelpBox("Exit play mode to make changes.", MessageType.Info); + return true; + } + return false; + } + + /// + /// Pass a type through an adapter and get the result. + /// + protected static Type AdaptTypeBackward(Type inputType, string adapterId) + { + if (!TypeResolver.TryGetAdapter(adapterId, out var adapterInfo)) + { + return inputType; + } + + return adapterInfo.InType; + } + + /// + /// Pass a type through an adapter and get the result. + /// + protected static Type AdaptTypeForward(Type inputType, string adapterId) + { + if (!TypeResolver.TryGetAdapter(adapterId, out var adapterInfo)) + { + return inputType; + } + + return adapterInfo.OutType; + } + + /// + /// Convert a BindableEvent to a uniquely identifiable string. + /// + private static string BindableEventToString(BindableEvent evt) + { + return string.Concat(evt.ComponentType.ToString(), ".", evt.Name); + } + } +} diff --git a/Editor/BaseBindingEditor.cs.meta b/Editor/BaseBindingEditor.cs.meta new file mode 100644 index 0000000..c134762 --- /dev/null +++ b/Editor/BaseBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2109485ee1df45439d88eef4c5b03a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/CollectionBindingEditor.cs b/Editor/CollectionBindingEditor.cs new file mode 100644 index 0000000..4462a3c --- /dev/null +++ b/Editor/CollectionBindingEditor.cs @@ -0,0 +1,66 @@ +using UnityEditor; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + [CustomEditor(typeof(CollectionBinding))] + class CollectionBindingEditor : BaseBindingEditor + { + private CollectionBinding _targetScript; + private SerializedProperty _templateInitialPoolCountProperty; + private SerializedProperty _itemsContainerProperty; + private SerializedProperty _templatesProperty; + + private bool _viewModelPrefabModified; + + protected override void OnEnabled() + { + // Initialise everything + _targetScript = (CollectionBinding)target; + _templateInitialPoolCountProperty = serializedObject.FindProperty("_templateInitialPoolCount"); + _itemsContainerProperty = serializedObject.FindProperty("_itemsContainer"); + _templatesProperty = serializedObject.FindProperty("_templates"); + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + EditorGUILayout.PropertyField(_templateInitialPoolCountProperty); + EditorGUILayout.PropertyField(_itemsContainerProperty); + EditorGUILayout.PropertyField(_templatesProperty, true); + + EditorStyles.label.fontStyle = _viewModelPrefabModified ? FontStyle.Bold : DefaultFontStyle; + ShowViewModelPropertyMenu( + new GUIContent("View-model property", "Property on the view-model to bind to."), + TypeResolver.FindBindableCollectionProperties(_targetScript), + updatedValue => _targetScript.ViewModelPropertyName = updatedValue, + _targetScript.ViewModelPropertyName, + property => true + ); + } + + /// + /// Check whether each of the properties on the object have been changed from the value in the prefab. + /// + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, Next(false) + // will iterate through the properties. + property.Next(true); + do + { + switch (property.name) + { + case "viewModelPropertyName": + _viewModelPrefabModified = property.prefabOverride; + break; + } + } + while (property.Next(false)); + } + } +} diff --git a/Editor/CollectionBindingEditor.cs.meta b/Editor/CollectionBindingEditor.cs.meta new file mode 100644 index 0000000..ee4f03c --- /dev/null +++ b/Editor/CollectionBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5a455a8f5047aca4fa9c7c67ef994596 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/EventBindingEditor.cs b/Editor/EventBindingEditor.cs similarity index 87% rename from UnityWeld_Editor/EventBindingEditor.cs rename to Editor/EventBindingEditor.cs index 49fd5f1..808432b 100644 --- a/UnityWeld_Editor/EventBindingEditor.cs +++ b/Editor/EventBindingEditor.cs @@ -1,110 +1,102 @@ -using System.Linq; -using System.Reflection; -using UnityEditor; -using UnityEngine; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; - -namespace UnityWeld_Editor -{ - [CustomEditor(typeof(EventBinding))] - public class EventBindingEditor : BaseBindingEditor - { - private EventBinding targetScript; - - // Whether or not the values on our target match its prefab. - private bool viewEventPrefabModified; - private bool viewModelMethodPrefabModified; - - private void OnEnable() - { - targetScript = (EventBinding)target; - } - - public override void OnInspectorGUI() - { - if (CannotModifyInPlayMode()) - { - GUI.enabled = false; - } - - UpdatePrefabModifiedProperties(); - - var defaultLabelStyle = EditorStyles.label.fontStyle; - EditorStyles.label.fontStyle = viewEventPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowEventMenu( - UnityEventWatcher.GetBindableEvents(targetScript.gameObject) - .OrderBy(evt => evt.Name) - .ToArray(), - updatedValue => targetScript.ViewEventName = updatedValue, - targetScript.ViewEventName - ); - - EditorStyles.label.fontStyle = viewModelMethodPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowMethodMenu(targetScript, TypeResolver.FindBindableMethods(targetScript)); - - EditorStyles.label.fontStyle = defaultLabelStyle; - } - - /// - /// Draws the dropdown for selecting a method from bindableViewModelMethods - /// - private void ShowMethodMenu( - EventBinding targetScript, - BindableMember[] bindableMethods - ) - { - var tooltip = "Method on the view-model to bind to."; - - InspectorUtils.DoPopup( - new GUIContent(targetScript.ViewModelMethodName), - new GUIContent("View-model method", tooltip), - m => m.ViewModelType + "/" + m.MemberName, - m => true, - m => m.ToString() == targetScript.ViewModelMethodName, - m => UpdateProperty( - updatedValue => targetScript.ViewModelMethodName = updatedValue, - targetScript.ViewModelMethodName, - m.ToString(), - "Set bound view-model method" - ), - bindableMethods - .OrderBy(m => m.ViewModelTypeName) - .ThenBy(m => m.MemberName) - .ToArray() - ); - } - - /// - /// Check whether each of the properties on the object have been changed - /// from the value in the prefab. - /// - private void UpdatePrefabModifiedProperties() - { - var property = serializedObject.GetIterator(); - // Need to call Next(true) to get the first child. Once we have it, - // Next(false) will iterate through the properties. - property.Next(true); - do - { - switch (property.name) - { - case "viewEventName": - viewEventPrefabModified = property.prefabOverride; - break; - - case "viewModelMethodName": - viewModelMethodPrefabModified = property.prefabOverride; - break; - } - } - while (property.Next(false)); - } - } -} +using System.Linq; +using System.Reflection; +using UnityEditor; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + [CustomEditor(typeof(EventBinding))] + public class EventBindingEditor : BaseBindingEditor + { + private EventBinding targetScript; + + // Whether or not the values on our target match its prefab. + private bool viewEventPrefabModified; + private bool viewModelMethodPrefabModified; + + protected override void OnEnabled() + { + targetScript = (EventBinding)target; + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + EditorStyles.label.fontStyle = viewEventPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowEventMenu( + UnityEventWatcher.GetBindableEvents(targetScript.gameObject) + .OrderBy(evt => evt.Name) + .ToArray(), + updatedValue => targetScript.ViewEventName = updatedValue, + targetScript.ViewEventName + ); + + EditorStyles.label.fontStyle = viewModelMethodPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowMethodMenu(targetScript, TypeResolver.FindBindableMethods(targetScript)); + } + + /// + /// Draws the dropdown for selecting a method from bindableViewModelMethods + /// + private void ShowMethodMenu( + EventBinding targetScript, + BindableMember[] bindableMethods + ) + { + var tooltip = "Method on the view-model to bind to."; + + InspectorUtils.DoPopup( + new GUIContent(targetScript.ViewModelMethodName), + new GUIContent("View-model method", tooltip), + m => m.ViewModelType + "/" + m.MemberName, + m => true, + m => m.ToString() == targetScript.ViewModelMethodName, + m => UpdateProperty( + updatedValue => targetScript.ViewModelMethodName = updatedValue, + targetScript.ViewModelMethodName, + m.ToString(), + "Set bound view-model method" + ), + bindableMethods + .OrderBy(m => m.ViewModelTypeName) + .ThenBy(m => m.MemberName) + .ToArray() + ); + } + + /// + /// Check whether each of the properties on the object have been changed + /// from the value in the prefab. + /// + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, + // Next(false) will iterate through the properties. + property.Next(true); + do + { + switch (property.name) + { + case "viewEventName": + viewEventPrefabModified = property.prefabOverride; + break; + + case "viewModelMethodName": + viewModelMethodPrefabModified = property.prefabOverride; + break; + } + } + while (property.Next(false)); + } + } +} diff --git a/Editor/EventBindingEditor.cs.meta b/Editor/EventBindingEditor.cs.meta new file mode 100644 index 0000000..2347c6c --- /dev/null +++ b/Editor/EventBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 375353919d68d544fadc2369d1a012d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/InspectorUtils.cs b/Editor/InspectorUtils.cs similarity index 97% rename from UnityWeld_Editor/InspectorUtils.cs rename to Editor/InspectorUtils.cs index 5d005f7..bb993dd 100644 --- a/UnityWeld_Editor/InspectorUtils.cs +++ b/Editor/InspectorUtils.cs @@ -1,141 +1,141 @@ -using System; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; - -namespace UnityWeld_Editor -{ - /// - /// Common utilities for custom inspectors. - /// - internal class InspectorUtils - { - /// - /// Show a popup menu with some items disabled and a label to its left. - /// - public static void DoPopup( - GUIContent content, - GUIContent label, - Func menuName, - Func menuEnabled, - Func isSelected, - Action callback, - T[] items) - { - var labelRect = EditorGUILayout.GetControlRect(false, 16f, EditorStyles.popup); - var controlId = GUIUtility.GetControlID(FocusType.Keyboard, labelRect); - - var buttonRect = EditorGUI.PrefixLabel(labelRect, controlId, label); - - ShowPopupButton( - buttonRect, - labelRect, - controlId, - content, - () => ShowMenu(menuName, menuEnabled, isSelected, callback, items, buttonRect) - ); - } - - /// - /// Shows the button for a popup/dropdown control, with a label. - /// - private static void ShowPopupButton(Rect buttonRect, Rect labelRect, int controlId, GUIContent currentlySelected, Action popup) - { - var currentEvent = Event.current; - var eventType = currentEvent.type; - var style = EditorStyles.popup; - - switch (eventType) - { - case EventType.KeyDown: - if (MainActionKeyForControl(currentEvent, controlId)) - { - popup(); - currentEvent.Use(); - } - break; - - case EventType.Repaint: - style.Draw(buttonRect, currentlySelected, controlId, false); - break; - - case EventType.MouseDown: - if (currentEvent.button != 0) - { - return; - } - - if (buttonRect.Contains(currentEvent.mousePosition)) - { - popup(); - GUIUtility.keyboardControl = controlId; - currentEvent.Use(); - } - else if (labelRect.Contains(currentEvent.mousePosition)) - { - GUIUtility.keyboardControl = controlId; - currentEvent.Use(); - } - break; - } - } - - /// - /// Returns whether the specified control has been activated by a key press. - /// - private static bool MainActionKeyForControl(Event evt, int controlId) - { - if (GUIUtility.keyboardControl != controlId) - { - return false; - } - bool modifierPressed = evt.alt || evt.shift || evt.command || evt.control; - if (!modifierPressed && evt.type == EventType.KeyDown && evt.character == ' ') - { - evt.Use(); - return false; - } - return evt.type == EventType.KeyDown - && (evt.keyCode == KeyCode.Space || evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) - && !modifierPressed; - } - - /// - /// Show a menu with some items disabled. Has a callback that will be called when an item is selected with the index of the selected item. - /// Takes a dictionary of options and whether or not they should be enabled. - /// - private static void ShowMenu(Func menuName, Func menuEnabled, Func isSelected, Action callback, T[] items, Rect position) - { - var menu = new GenericMenu(); - - for (var i = 0; i < items.Length; i++) - { - // Need to cache index so that it doesn't get passed through to the callback by reference. - int index = i; - var item = items[index]; - - var content = new GUIContent(menuName(item)); - - if (menuEnabled(item)) - { - menu.AddItem(content, isSelected(item), () => callback(item)); - } - else - { - menu.AddDisabledItem(content); - } - } - - menu.DropDown(position); - } - - /// - /// Tell Unity that a change has been made to a specified object and we have to save the scene. - /// - public static void MarkSceneDirty(GameObject gameObject) - { - // TODO: Undo.RecordObject also marks the scene dirty, so this will no longer be necessary once undo support is added. - EditorSceneManager.MarkSceneDirty(gameObject.scene); - } - } -} +using System; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; + +namespace UnityWeld_Editor +{ + /// + /// Common utilities for custom inspectors. + /// + internal class InspectorUtils + { + /// + /// Show a popup menu with some items disabled and a label to its left. + /// + public static void DoPopup( + GUIContent content, + GUIContent label, + Func menuName, + Func menuEnabled, + Func isSelected, + Action callback, + T[] items) + { + var labelRect = EditorGUILayout.GetControlRect(false, 16f, EditorStyles.popup); + var controlId = GUIUtility.GetControlID(FocusType.Keyboard, labelRect); + + var buttonRect = EditorGUI.PrefixLabel(labelRect, controlId, label); + + ShowPopupButton( + buttonRect, + labelRect, + controlId, + content, + () => ShowMenu(menuName, menuEnabled, isSelected, callback, items, buttonRect) + ); + } + + /// + /// Shows the button for a popup/dropdown control, with a label. + /// + private static void ShowPopupButton(Rect buttonRect, Rect labelRect, int controlId, GUIContent currentlySelected, Action popup) + { + var currentEvent = Event.current; + var eventType = currentEvent.type; + var style = EditorStyles.popup; + + switch (eventType) + { + case EventType.KeyDown: + if (MainActionKeyForControl(currentEvent, controlId)) + { + popup(); + currentEvent.Use(); + } + break; + + case EventType.Repaint: + style.Draw(buttonRect, currentlySelected, controlId, false); + break; + + case EventType.MouseDown: + if (currentEvent.button != 0) + { + return; + } + + if (buttonRect.Contains(currentEvent.mousePosition)) + { + popup(); + GUIUtility.keyboardControl = controlId; + currentEvent.Use(); + } + else if (labelRect.Contains(currentEvent.mousePosition)) + { + GUIUtility.keyboardControl = controlId; + currentEvent.Use(); + } + break; + } + } + + /// + /// Returns whether the specified control has been activated by a key press. + /// + private static bool MainActionKeyForControl(Event evt, int controlId) + { + if (GUIUtility.keyboardControl != controlId) + { + return false; + } + bool modifierPressed = evt.alt || evt.shift || evt.command || evt.control; + if (!modifierPressed && evt.type == EventType.KeyDown && evt.character == ' ') + { + evt.Use(); + return false; + } + return evt.type == EventType.KeyDown + && (evt.keyCode == KeyCode.Space || evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) + && !modifierPressed; + } + + /// + /// Show a menu with some items disabled. Has a callback that will be called when an item is selected with the index of the selected item. + /// Takes a dictionary of options and whether or not they should be enabled. + /// + private static void ShowMenu(Func menuName, Func menuEnabled, Func isSelected, Action callback, T[] items, Rect position) + { + var menu = new GenericMenu(); + + for (var i = 0; i < items.Length; i++) + { + // Need to cache index so that it doesn't get passed through to the callback by reference. + int index = i; + var item = items[index]; + + var content = new GUIContent(menuName(item)); + + if (menuEnabled(item)) + { + menu.AddItem(content, isSelected(item), () => callback(item)); + } + else + { + menu.AddDisabledItem(content); + } + } + + menu.DropDown(position); + } + + /// + /// Tell Unity that a change has been made to a specified object and we have to save the scene. + /// + public static void MarkSceneDirty(GameObject gameObject) + { + // TODO: Undo.RecordObject also marks the scene dirty, so this will no longer be necessary once undo support is added. + EditorSceneManager.MarkSceneDirty(gameObject.scene); + } + } +} diff --git a/Editor/InspectorUtils.cs.meta b/Editor/InspectorUtils.cs.meta new file mode 100644 index 0000000..723ab92 --- /dev/null +++ b/Editor/InspectorUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d7b4b21f4dc5f004689c8d4d822d8a09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/OneWayPropertyBindingEditor.cs b/Editor/OneWayPropertyBindingEditor.cs similarity index 82% rename from UnityWeld_Editor/OneWayPropertyBindingEditor.cs rename to Editor/OneWayPropertyBindingEditor.cs index 910ac2c..23634db 100644 --- a/UnityWeld_Editor/OneWayPropertyBindingEditor.cs +++ b/Editor/OneWayPropertyBindingEditor.cs @@ -1,188 +1,173 @@ -using System; -using System.Linq; -using UnityEditor; -using UnityEditor.AnimatedValues; -using UnityEngine; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; - -namespace UnityWeld_Editor -{ - [CustomEditor(typeof(OneWayPropertyBinding))] - class OneWayPropertyBindingEditor : BaseBindingEditor - { - private OneWayPropertyBinding targetScript; - - private AnimBool viewAdapterOptionsFade; - - // Whether each property in the target differs from the prefab it uses. - private bool viewAdapterPrefabModified; - private bool viewAdapterOptionsPrefabModified; - private bool viewModelPropertyPrefabModified; - private bool viewPropertyPrefabModified; - - private void OnEnable() - { - // Initialise reference to target script - targetScript = (OneWayPropertyBinding)target; - - Type adapterType; - - viewAdapterOptionsFade = new AnimBool( - ShouldShowAdapterOptions(targetScript.ViewAdapterTypeName, out adapterType) - ); - - viewAdapterOptionsFade.valueChanged.AddListener(Repaint); - } - - private void OnDisable() - { - viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); - } - - public override void OnInspectorGUI() - { - if (CannotModifyInPlayMode()) - { - GUI.enabled = false; - } - - UpdatePrefabModifiedProperties(); - - var defaultLabelStyle = EditorStyles.label.fontStyle; - EditorStyles.label.fontStyle = viewPropertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - Type viewPropertyType; - ShowViewPropertyMenu( - new GUIContent("View property", "Property on the view to bind to"), - PropertyFinder.GetBindableProperties(targetScript.gameObject) - .OrderBy(prop => prop.ViewModelTypeName) - .ThenBy(prop => prop.MemberName) - .ToArray(), - updatedValue => targetScript.ViewPropertyName = updatedValue, - targetScript.ViewPropertyName, - out viewPropertyType - ); - - // Don't let the user set anything else until they've chosen a view property. - var guiPreviouslyEnabled = GUI.enabled; - if (string.IsNullOrEmpty(targetScript.ViewPropertyName)) - { - GUI.enabled = false; - } - - var viewAdapterTypeNames = GetAdapterTypeNames( - type => viewPropertyType == null || - TypeResolver.FindAdapterAttribute(type).OutputType == viewPropertyType - ); - - EditorStyles.label.fontStyle = viewAdapterPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowAdapterMenu( - new GUIContent( - "View adapter", - "Adapter that converts values sent from the view-model to the view." - ), - viewAdapterTypeNames, - targetScript.ViewAdapterTypeName, - newValue => - { - // Get rid of old adapter options if we changed the type of the adapter. - if (newValue != targetScript.ViewAdapterTypeName) - { - Undo.RecordObject(targetScript, "Set view adapter options"); - targetScript.ViewAdapterOptions = null; - } - - UpdateProperty( - updatedValue => targetScript.ViewAdapterTypeName = updatedValue, - targetScript.ViewAdapterTypeName, - newValue, - "Set view adapter" - ); - } - ); - - Type adapterType; - viewAdapterOptionsFade.target = ShouldShowAdapterOptions( - targetScript.ViewAdapterTypeName, - out adapterType - ); - - EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowAdapterOptionsMenu( - "View adapter options", - adapterType, - options => targetScript.ViewAdapterOptions = options, - targetScript.ViewAdapterOptions, - viewAdapterOptionsFade.faded - ); - - EditorGUILayout.Space(); - - EditorStyles.label.fontStyle = viewModelPropertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - var adaptedViewPropertyType = AdaptTypeBackward( - viewPropertyType, - targetScript.ViewAdapterTypeName - ); - ShowViewModelPropertyMenu( - new GUIContent( - "View-model property", - "Property on the view-model to bind to." - ), - TypeResolver.FindBindableProperties(targetScript), - updatedValue => targetScript.ViewModelPropertyName = updatedValue, - targetScript.ViewModelPropertyName, - property => property.PropertyType == adaptedViewPropertyType - ); - - GUI.enabled = guiPreviouslyEnabled; - - EditorStyles.label.fontStyle = defaultLabelStyle; - } - - /// - /// Check whether each of the properties on the object have been changed - /// from the value in the prefab. - /// - private void UpdatePrefabModifiedProperties() - { - var property = serializedObject.GetIterator(); - // Need to call Next(true) to get the first child. Once we have it, Next(false) - // will iterate through the properties. - property.Next(true); - do - { - switch (property.name) - { - case "viewAdapterTypeName": - viewAdapterPrefabModified = property.prefabOverride; - break; - - case "viewAdapterOptions": - viewAdapterOptionsPrefabModified = property.prefabOverride; - break; - - case "viewModelPropertyName": - viewModelPropertyPrefabModified = property.prefabOverride; - break; - - case "viewPropertyName": - viewPropertyPrefabModified = property.prefabOverride; - break; - } - } - while (property.Next(false)); - } - } -} +using System; +using System.Linq; +using UnityEditor; +using UnityEditor.AnimatedValues; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + [CustomEditor(typeof(OneWayPropertyBinding))] + class OneWayPropertyBindingEditor : BaseBindingEditor + { + private OneWayPropertyBinding targetScript; + + private AnimBool viewAdapterOptionsFade; + + // Whether each property in the target differs from the prefab it uses. + private bool viewAdapterPrefabModified; + private bool viewAdapterOptionsPrefabModified; + private bool viewModelPropertyPrefabModified; + private bool viewPropertyPrefabModified; + + protected override void OnEnabled() + { + // Initialise reference to target script + targetScript = (OneWayPropertyBinding)target; + + viewAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ViewAdapterId, out _)); + viewAdapterOptionsFade.valueChanged.AddListener(Repaint); + } + + private void OnDisable() + { + viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + var defaultLabelStyle = EditorStyles.label.fontStyle; + EditorStyles.label.fontStyle = viewPropertyPrefabModified + ? FontStyle.Bold + : defaultLabelStyle; + + Type viewPropertyType; + ShowViewPropertyMenu( + new GUIContent("View property", "Property on the view to bind to"), + PropertyFinder.GetBindableProperties(targetScript.gameObject), + updatedValue => targetScript.ViewPropertyName = updatedValue, + targetScript.ViewPropertyName, + out viewPropertyType + ); + + // Don't let the user set anything else until they've chosen a view property. + var guiPreviouslyEnabled = GUI.enabled; + if (string.IsNullOrEmpty(targetScript.ViewPropertyName)) + { + GUI.enabled = false; + } + + var viewAdapterTypeNames = TypeResolver.GetAdapterIds( + o => viewPropertyType == null || o.OutType == viewPropertyType); + + EditorStyles.label.fontStyle = viewAdapterPrefabModified + ? FontStyle.Bold + : defaultLabelStyle; + + ShowAdapterMenu( + new GUIContent( + "View adapter", + "Adapter that converts values sent from the view-model to the view." + ), + viewAdapterTypeNames, + targetScript.ViewAdapterId, + newValue => + { + // Get rid of old adapter options if we changed the type of the adapter. + if (newValue != targetScript.ViewAdapterId) + { + Undo.RecordObject(targetScript, "Set view adapter options"); + targetScript.ViewAdapterOptions = null; + } + + UpdateProperty( + updatedValue => targetScript.ViewAdapterId = updatedValue, + targetScript.ViewAdapterId, + newValue, + "Set view adapter" + ); + } + ); + + Type adapterType; + viewAdapterOptionsFade.target = ShouldShowAdapterOptions( + targetScript.ViewAdapterId, + out adapterType + ); + + EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified + ? FontStyle.Bold + : defaultLabelStyle; + + ShowAdapterOptionsMenu( + "View adapter options", + adapterType, + options => targetScript.ViewAdapterOptions = options, + targetScript.ViewAdapterOptions, + viewAdapterOptionsFade.faded + ); + + EditorGUILayout.Space(); + + EditorStyles.label.fontStyle = viewModelPropertyPrefabModified + ? FontStyle.Bold + : defaultLabelStyle; + + var adaptedViewPropertyType = AdaptTypeBackward( + viewPropertyType, + targetScript.ViewAdapterId + ); + ShowViewModelPropertyMenu( + new GUIContent( + "View-model property", + "Property on the view-model to bind to." + ), + TypeResolver.FindBindableProperties(targetScript), + updatedValue => targetScript.ViewModelPropertyName = updatedValue, + targetScript.ViewModelPropertyName, + property => property.PropertyType == adaptedViewPropertyType + ); + + GUI.enabled = guiPreviouslyEnabled; + + EditorStyles.label.fontStyle = defaultLabelStyle; + } + + /// + /// Check whether each of the properties on the object have been changed + /// from the value in the prefab. + /// + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, Next(false) + // will iterate through the properties. + property.Next(true); + do + { + switch (property.name) + { + case "viewAdapterTypeName": + viewAdapterPrefabModified = property.prefabOverride; + break; + + case "viewAdapterOptions": + viewAdapterOptionsPrefabModified = property.prefabOverride; + break; + + case "viewModelPropertyName": + viewModelPropertyPrefabModified = property.prefabOverride; + break; + + case "viewPropertyName": + viewPropertyPrefabModified = property.prefabOverride; + break; + } + } + while (property.Next(false)); + } + } +} diff --git a/Editor/OneWayPropertyBindingEditor.cs.meta b/Editor/OneWayPropertyBindingEditor.cs.meta new file mode 100644 index 0000000..72d5068 --- /dev/null +++ b/Editor/OneWayPropertyBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 362976f353b49a545856b39257e9eb02 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/SubViewModelBindingEditor.cs b/Editor/SubViewModelBindingEditor.cs similarity index 81% rename from UnityWeld_Editor/SubViewModelBindingEditor.cs rename to Editor/SubViewModelBindingEditor.cs index 62108e4..c0a0751 100644 --- a/UnityWeld_Editor/SubViewModelBindingEditor.cs +++ b/Editor/SubViewModelBindingEditor.cs @@ -1,101 +1,91 @@ -using UnityEngine; -using UnityEditor; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; -using System.Linq; -using System.Reflection; - -namespace UnityWeld_Editor -{ - /// - /// Inspector window for SubViewModelBinding - /// - [CustomEditor(typeof(SubViewModelBinding))] - public class SubViewModelBindingEditor : BaseBindingEditor - { - private SubViewModelBinding targetScript; - - /// - /// Whether or not the value on our target matches its prefab. - /// - private bool propertyPrefabModified; - - private void OnEnable() - { - targetScript = (SubViewModelBinding)target; - } - - public override void OnInspectorGUI() - { - if (CannotModifyInPlayMode()) - { - GUI.enabled = false; - } - - UpdatePrefabModifiedProperties(); - - var bindableProperties = FindBindableProperties(); - - var defaultLabelStyle = EditorStyles.label.fontStyle; - EditorStyles.label.fontStyle = propertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowViewModelPropertyMenu( - new GUIContent( - "Sub view-model property", - "The property on the top level view model containing the sub view-model" - ), - bindableProperties, - updatedValue => - { - targetScript.ViewModelPropertyName = updatedValue; - - targetScript.ViewModelTypeName = bindableProperties - .Single(prop => prop.ToString() == updatedValue) - .Member.PropertyType.ToString(); - }, - targetScript.ViewModelPropertyName, - p => true - ); - - EditorStyles.label.fontStyle = defaultLabelStyle; - } - - private BindableMember[] FindBindableProperties() - { - return TypeResolver.FindBindableProperties(targetScript) - .Where(prop => prop.Member.PropertyType - .GetCustomAttributes(typeof(BindingAttribute), false) - .Any() - ) - .ToArray(); - } - - /// - /// Check whether each of the properties on the object have been changed - /// from the value in the prefab. - /// - private void UpdatePrefabModifiedProperties() - { - var property = serializedObject.GetIterator(); - // Need to call Next(true) to get the first child. Once we have it, Next(false) - // will iterate through the properties. - - propertyPrefabModified = false; - property.Next(true); - do - { - switch (property.name) - { - case "viewModelPropertyName": - case "viewModelTypeName": - propertyPrefabModified = property.prefabOverride - || propertyPrefabModified; - break; - } - } - while (property.Next(false)); - } - } +using UnityEngine; +using UnityEditor; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; +using System.Linq; +using System.Reflection; + +namespace UnityWeld_Editor +{ + /// + /// Inspector window for SubViewModelBinding + /// + [CustomEditor(typeof(SubViewModelBinding))] + public class SubViewModelBindingEditor : BaseBindingEditor + { + private SubViewModelBinding targetScript; + + /// + /// Whether or not the value on our target matches its prefab. + /// + private bool propertyPrefabModified; + + protected override void OnEnabled() + { + targetScript = (SubViewModelBinding)target; + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + var bindableProperties = FindBindableProperties(); + + EditorStyles.label.fontStyle = propertyPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowViewModelPropertyMenu( + new GUIContent( + "Sub view-model property", + "The property on the top level view model containing the sub view-model" + ), + bindableProperties, + updatedValue => + { + targetScript.ViewModelPropertyName = updatedValue; + + targetScript.ViewModelTypeName = bindableProperties + .Single(prop => prop.ToString() == updatedValue) + .Member.PropertyType.ToString(); + }, + targetScript.ViewModelPropertyName, + p => true + ); + } + + private BindableMember[] FindBindableProperties() + { + return TypeResolver.FindBindableProperties(targetScript) + .Where(prop => prop.Member.PropertyType.HasBindingAttribute() + ) + .ToArray(); + } + + /// + /// Check whether each of the properties on the object have been changed + /// from the value in the prefab. + /// + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, Next(false) + // will iterate through the properties. + + propertyPrefabModified = false; + property.Next(true); + do + { + switch (property.name) + { + case "viewModelPropertyName": + case "viewModelTypeName": + propertyPrefabModified = property.prefabOverride + || propertyPrefabModified; + break; + } + } + while (property.Next(false)); + } + } } \ No newline at end of file diff --git a/Editor/SubViewModelBindingEditor.cs.meta b/Editor/SubViewModelBindingEditor.cs.meta new file mode 100644 index 0000000..b22cf19 --- /dev/null +++ b/Editor/SubViewModelBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a40c8489f721eb24991cefbc29d4e6fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/TemplateBindingEditor.cs b/Editor/TemplateBindingEditor.cs similarity index 56% rename from UnityWeld_Editor/TemplateBindingEditor.cs rename to Editor/TemplateBindingEditor.cs index 1b8ca3d..221d4be 100644 --- a/UnityWeld_Editor/TemplateBindingEditor.cs +++ b/Editor/TemplateBindingEditor.cs @@ -1,94 +1,66 @@ -using UnityEditor; -using UnityEngine; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; - -namespace UnityWeld_Editor -{ - [CustomEditor(typeof(TemplateBinding))] - class TemplateBindingEditor : BaseBindingEditor - { - private TemplateBinding targetScript; - - private bool viewModelPrefabModified; - private bool templatesRootPrefabModified; - - private void OnEnable() - { - targetScript = (TemplateBinding)target; - } - - public override void OnInspectorGUI() - { - if (CannotModifyInPlayMode()) - { - GUI.enabled = false; - } - - UpdatePrefabModifiedProperties(); - - var defaultLabelStyle = EditorStyles.label.fontStyle; - EditorStyles.label.fontStyle = viewModelPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowViewModelPropertyMenu( - new GUIContent( - "Template property", - "Property on the view model to use for selecting templates." - ), - TypeResolver.FindBindableProperties(targetScript), - updatedValue => targetScript.ViewModelPropertyName = updatedValue, - targetScript.ViewModelPropertyName, - property => true - ); - - EditorStyles.label.fontStyle = templatesRootPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - UpdateProperty( - updatedValue => targetScript.TemplatesRoot = updatedValue, - targetScript.TemplatesRoot, - (GameObject)EditorGUILayout.ObjectField( - new GUIContent( - "Templates root object", - "Parent object to the objects we want to use as templates." - ), - targetScript.TemplatesRoot, - typeof(GameObject), - true - ), - "Set template binding root object" - ); - - EditorStyles.label.fontStyle = defaultLabelStyle; - } - - /// - /// Check whether each of the properties on the object have been changed - /// from the value in the prefab. - /// - private void UpdatePrefabModifiedProperties() - { - var property = serializedObject.GetIterator(); - // Need to call Next(true) to get the first child. Once we have it, Next(false) - // will iterate through the properties. - property.Next(true); - do - { - switch (property.name) - { - case "viewModelPropertyName": - viewModelPrefabModified = property.prefabOverride; - break; - - case "templatesRoot": - templatesRootPrefabModified = property.prefabOverride; - break; - } - } - while (property.Next(false)); - } - } -} +using UnityEditor; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + [CustomEditor(typeof(TemplateBinding))] + class TemplateBindingEditor : BaseBindingEditor + { + private TemplateBinding targetScript; + + private bool viewModelPrefabModified; + private SerializedProperty _templatesProperty; + + protected override void OnEnabled() + { + targetScript = (TemplateBinding)target; + _templatesProperty = serializedObject.FindProperty("_templates"); + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + EditorStyles.label.fontStyle = viewModelPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowViewModelPropertyMenu( + new GUIContent( + "Template property", + "Property on the view model to use for selecting templates." + ), + TypeResolver.FindBindableProperties(targetScript), + updatedValue => targetScript.ViewModelPropertyName = updatedValue, + targetScript.ViewModelPropertyName, + property => true + ); + + EditorGUILayout.PropertyField(_templatesProperty, true); + } + + /// + /// Check whether each of the properties on the object have been changed + /// from the value in the prefab. + /// + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, Next(false) + // will iterate through the properties. + property.Next(true); + do + { + switch (property.name) + { + case "viewModelPropertyName": + viewModelPrefabModified = property.prefabOverride; + break; + } + } + while (property.Next(false)); + } + } +} diff --git a/Editor/TemplateBindingEditor.cs.meta b/Editor/TemplateBindingEditor.cs.meta new file mode 100644 index 0000000..1fe8975 --- /dev/null +++ b/Editor/TemplateBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b4ca5578372f3924fb0e72cdea97397d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/TemplateEditor.cs b/Editor/TemplateEditor.cs similarity index 86% rename from UnityWeld_Editor/TemplateEditor.cs rename to Editor/TemplateEditor.cs index cd8abef..106773c 100644 --- a/UnityWeld_Editor/TemplateEditor.cs +++ b/Editor/TemplateEditor.cs @@ -1,96 +1,90 @@ -using System; -using System.Linq; -using UnityEditor; -using UnityEngine; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; - -namespace UnityWeld_Editor -{ - /// - /// Editor for template bindings with a dropdown for selecting what view model - /// to bind to. - /// - [CustomEditor(typeof(Template))] - public class TemplateEditor : BaseBindingEditor - { - private Template targetScript; - - /// - /// Whether the value on our target matches its prefab. - /// - private bool propertyPrefabModified; - - private void OnEnable() - { - targetScript = (Template)target; - } - - public override void OnInspectorGUI() - { - if (CannotModifyInPlayMode()) - { - GUI.enabled = false; - } - - UpdatePrefabModifiedProperties(); - - var availableViewModels = TypeResolver.TypesWithBindingAttribute - .Select(type => type.ToString()) - .OrderBy(name => name) - .ToArray(); - - var selectedIndex = Array.IndexOf( - availableViewModels, - targetScript.ViewModelTypeName - ); - - var defaultLabelStyle = EditorStyles.label.fontStyle; - EditorStyles.label.fontStyle = propertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - var newSelectedIndex = EditorGUILayout.Popup( - new GUIContent( - "Template view model", - "Type of the view model that this template will be bound to when it is instantiated." - ), - selectedIndex, - availableViewModels - .Select(viewModel => new GUIContent(viewModel)) - .ToArray() - ); - - EditorStyles.label.fontStyle = defaultLabelStyle; - - UpdateProperty(newValue => targetScript.ViewModelTypeName = newValue, - selectedIndex < 0 - ? string.Empty - : availableViewModels[selectedIndex], - newSelectedIndex < 0 - ? string.Empty - : availableViewModels[newSelectedIndex], - "Set bound view-model for template" - ); - } - - /// - /// Check whether each of the properties on the object have been changed from the value in the prefab. - /// - private void UpdatePrefabModifiedProperties() - { - var property = serializedObject.GetIterator(); - // Need to call Next(true) to get the first child. Once we have it, Next(false) - // will iterate through the properties. - property.Next(true); - do - { - if (property.name == "viewModelTypeName") - { - propertyPrefabModified = property.prefabOverride; - } - } - while (property.Next(false)); - } - } -} +using System; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + /// + /// Editor for template bindings with a dropdown for selecting what view model + /// to bind to. + /// + [CustomEditor(typeof(Template))] + public class TemplateEditor : BaseBindingEditor + { + private Template targetScript; + + /// + /// Whether the value on our target matches its prefab. + /// + private bool propertyPrefabModified; + + protected override void OnEnabled() + { + targetScript = (Template)target; + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + var availableViewModels = TypeResolver.TypesWithBindingAttribute + .Select(type => type.ToString()) + .OrderBy(name => name) + .ToArray(); + + var selectedIndex = Array.IndexOf( + availableViewModels, + targetScript.ViewModelTypeName + ); + + EditorStyles.label.fontStyle = propertyPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + var newSelectedIndex = EditorGUILayout.Popup( + new GUIContent( + "Template view model", + "Type of the view model that this template will be bound to when it is instantiated." + ), + selectedIndex, + availableViewModels + .Select(viewModel => new GUIContent(viewModel)) + .ToArray() + ); + + EditorStyles.label.fontStyle = DefaultFontStyle; + + UpdateProperty(newValue => targetScript.ViewModelTypeName = newValue, + selectedIndex < 0 + ? string.Empty + : availableViewModels[selectedIndex], + newSelectedIndex < 0 + ? string.Empty + : availableViewModels[newSelectedIndex], + "Set bound view-model for template" + ); + } + + /// + /// Check whether each of the properties on the object have been changed from the value in the prefab. + /// + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, Next(false) + // will iterate through the properties. + property.Next(true); + do + { + if (property.name == "viewModelTypeName") + { + propertyPrefabModified = property.prefabOverride; + } + } + while (property.Next(false)); + } + } +} diff --git a/Editor/TemplateEditor.cs.meta b/Editor/TemplateEditor.cs.meta new file mode 100644 index 0000000..641fe8d --- /dev/null +++ b/Editor/TemplateEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41e9680918dbe374abf0efb0d8d9884d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/ToggleActiveBindingEditor.cs b/Editor/ToggleActiveBindingEditor.cs similarity index 76% rename from UnityWeld_Editor/ToggleActiveBindingEditor.cs rename to Editor/ToggleActiveBindingEditor.cs index 91847e1..61485f2 100644 --- a/UnityWeld_Editor/ToggleActiveBindingEditor.cs +++ b/Editor/ToggleActiveBindingEditor.cs @@ -1,153 +1,137 @@ -using System; -using UnityEditor; -using UnityEditor.AnimatedValues; -using UnityEngine; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; - -namespace UnityWeld_Editor -{ - [CustomEditor(typeof(ToggleActiveBinding))] - public class ToggleActiveBindingEditor : BaseBindingEditor - { - private ToggleActiveBinding targetScript; - - private AnimBool viewAdapterOptionsFade; - - private bool viewAdapterPrefabModified; - private bool viewAdapterOptionsPrefabModified; - private bool viewModelPropertyPrefabModified; - - private void OnEnable() - { - targetScript = (ToggleActiveBinding)target; - - Type adapterType; - - viewAdapterOptionsFade = new AnimBool( - ShouldShowAdapterOptions(targetScript.ViewAdapterTypeName, out adapterType) - ); - - viewAdapterOptionsFade.valueChanged.AddListener(Repaint); - } - - private void OnDisable() - { - viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); - } - - public override void OnInspectorGUI() - { - if (CannotModifyInPlayMode()) - { - GUI.enabled = false; - } - - UpdatePrefabModifiedProperties(); - - var defaultLabelStyle = EditorStyles.label.fontStyle; - - var viewPropertyType = typeof(bool); - - var viewAdapterTypeNames = GetAdapterTypeNames( - type => TypeResolver.FindAdapterAttribute(type).OutputType == viewPropertyType - ); - - EditorStyles.label.fontStyle = viewAdapterPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowAdapterMenu( - new GUIContent( - "View adapter", - "Adapter that converts values sent from the view-model to the view." - ), - viewAdapterTypeNames, - targetScript.ViewAdapterTypeName, - newValue => - { - // Get rid of old adapter options if we changed the type of the adapter. - if (newValue != targetScript.ViewAdapterTypeName) - { - Undo.RecordObject(targetScript, "Set view adapter options"); - targetScript.ViewAdapterOptions = null; - } - - UpdateProperty( - updatedValue => targetScript.ViewAdapterTypeName = updatedValue, - targetScript.ViewAdapterTypeName, - newValue, - "Set view adapter" - ); - } - ); - - Type adapterType; - viewAdapterOptionsFade.target = ShouldShowAdapterOptions( - targetScript.ViewAdapterTypeName, - out adapterType - ); - - EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowAdapterOptionsMenu( - "View adapter options", - adapterType, - options => targetScript.ViewAdapterOptions = options, - targetScript.ViewAdapterOptions, - viewAdapterOptionsFade.faded - ); - - EditorGUILayout.Space(); - - EditorStyles.label.fontStyle = viewModelPropertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - var adaptedViewPropertyType = AdaptTypeBackward( - viewPropertyType, - targetScript.ViewAdapterTypeName - ); - ShowViewModelPropertyMenu( - new GUIContent( - "View-model property", - "Property on the view-model to bind to." - ), - TypeResolver.FindBindableProperties(targetScript), - updatedValue => targetScript.ViewModelPropertyName = updatedValue, - targetScript.ViewModelPropertyName, - property => property.PropertyType == adaptedViewPropertyType - ); - - EditorStyles.label.fontStyle = defaultLabelStyle; - } - - private void UpdatePrefabModifiedProperties() - { - var property = serializedObject.GetIterator(); - // Need to call Next(true) to get the first child. Once we have it, Next(false) - // will iterate through the properties. - property.Next(true); - do - { - switch (property.name) - { - case "viewAdapterTypeName": - viewAdapterPrefabModified = property.prefabOverride; - break; - - case "viewAdapterOptions": - viewAdapterOptionsPrefabModified = property.prefabOverride; - break; - - case "viewModelPropertyName": - viewModelPropertyPrefabModified = property.prefabOverride; - break; - } - } - while (property.Next(false)); - } - } -} +using System; +using UnityEditor; +using UnityEditor.AnimatedValues; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + [CustomEditor(typeof(ToggleActiveBinding))] + public class ToggleActiveBindingEditor : BaseBindingEditor + { + private ToggleActiveBinding targetScript; + + private AnimBool viewAdapterOptionsFade; + + private bool viewAdapterPrefabModified; + private bool viewAdapterOptionsPrefabModified; + private bool viewModelPropertyPrefabModified; + + protected override void OnEnabled() + { + targetScript = (ToggleActiveBinding)target; + + viewAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ViewAdapterId, out _)); + viewAdapterOptionsFade.valueChanged.AddListener(Repaint); + } + + private void OnDisable() + { + viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + var viewPropertyType = typeof(bool); + + var viewAdapterTypeNames = TypeResolver.GetAdapterIds(o => o.OutType == viewPropertyType); + + EditorStyles.label.fontStyle = viewAdapterPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowAdapterMenu( + new GUIContent( + "View adapter", + "Adapter that converts values sent from the view-model to the view." + ), + viewAdapterTypeNames, + targetScript.ViewAdapterId, + newValue => + { + // Get rid of old adapter options if we changed the type of the adapter. + if (newValue != targetScript.ViewAdapterId) + { + Undo.RecordObject(targetScript, "Set view adapter options"); + targetScript.ViewAdapterOptions = null; + } + + UpdateProperty( + updatedValue => targetScript.ViewAdapterId = updatedValue, + targetScript.ViewAdapterId, + newValue, + "Set view adapter" + ); + } + ); + + Type adapterType; + viewAdapterOptionsFade.target = ShouldShowAdapterOptions( + targetScript.ViewAdapterId, + out adapterType + ); + + EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowAdapterOptionsMenu( + "View adapter options", + adapterType, + options => targetScript.ViewAdapterOptions = options, + targetScript.ViewAdapterOptions, + viewAdapterOptionsFade.faded + ); + + EditorGUILayout.Space(); + + EditorStyles.label.fontStyle = viewModelPropertyPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + var adaptedViewPropertyType = AdaptTypeBackward( + viewPropertyType, + targetScript.ViewAdapterId + ); + ShowViewModelPropertyMenu( + new GUIContent( + "View-model property", + "Property on the view-model to bind to." + ), + TypeResolver.FindBindableProperties(targetScript), + updatedValue => targetScript.ViewModelPropertyName = updatedValue, + targetScript.ViewModelPropertyName, + property => property.PropertyType == adaptedViewPropertyType + ); + } + + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, Next(false) + // will iterate through the properties. + property.Next(true); + do + { + switch (property.name) + { + case "viewAdapterTypeName": + viewAdapterPrefabModified = property.prefabOverride; + break; + + case "viewAdapterOptions": + viewAdapterOptionsPrefabModified = property.prefabOverride; + break; + + case "viewModelPropertyName": + viewModelPropertyPrefabModified = property.prefabOverride; + break; + } + } + while (property.Next(false)); + } + } +} diff --git a/Editor/ToggleActiveBindingEditor.cs.meta b/Editor/ToggleActiveBindingEditor.cs.meta new file mode 100644 index 0000000..61740f6 --- /dev/null +++ b/Editor/ToggleActiveBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e2ae3e18ed11fbd46af8d4ae70099c0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld_Editor/TwoWayPropertyBindingEditor.cs b/Editor/TwoWayPropertyBindingEditor.cs similarity index 80% rename from UnityWeld_Editor/TwoWayPropertyBindingEditor.cs rename to Editor/TwoWayPropertyBindingEditor.cs index 20c1f28..cd0c93e 100644 --- a/UnityWeld_Editor/TwoWayPropertyBindingEditor.cs +++ b/Editor/TwoWayPropertyBindingEditor.cs @@ -1,367 +1,340 @@ -using System; -using System.Linq; -using UnityEditor; -using UnityEditor.AnimatedValues; -using UnityEngine; -using UnityWeld.Binding; -using UnityWeld.Binding.Internal; - -namespace UnityWeld_Editor -{ - [CustomEditor(typeof(TwoWayPropertyBinding))] - class TwoWayPropertyBindingEditor : BaseBindingEditor - { - private TwoWayPropertyBinding targetScript; - - private AnimBool viewAdapterOptionsFade; - private AnimBool viewModelAdapterOptionsFade; - private AnimBool exceptionAdapterOptionsFade; - - // Whether properties in the target script differ from the value in the prefab. - // Needed to know which ones to display as bold in the inspector. - private bool viewEventPrefabModified; - private bool viewPropertyPrefabModified; - private bool viewAdapterPrefabModified; - private bool viewAdapterOptionsPrefabModified; - - private bool viewModelPropertyPrefabModified; - private bool viewModelAdapterPrefabModified; - private bool viewModelAdapterOptionsPrefabModified; - - private bool exceptionPropertyPrefabModified; - private bool exceptionAdapterPrefabModified; - private bool exceptionAdapterOptionsPrefabModified; - - private void OnEnable() - { - targetScript = (TwoWayPropertyBinding)target; - - Type adapterType; - viewAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions( - targetScript.ViewAdapterTypeName, - out adapterType - )); - viewModelAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions( - targetScript.ViewModelAdapterTypeName, - out adapterType - )); - exceptionAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions( - targetScript.ExceptionAdapterTypeName, - out adapterType - )); - - viewAdapterOptionsFade.valueChanged.AddListener(Repaint); - viewModelAdapterOptionsFade.valueChanged.AddListener(Repaint); - exceptionAdapterOptionsFade.valueChanged.AddListener(Repaint); - } - - private void OnDisable() - { - viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); - viewModelAdapterOptionsFade.valueChanged.RemoveListener(Repaint); - exceptionAdapterOptionsFade.valueChanged.RemoveListener(Repaint); - } - - public override void OnInspectorGUI() - { - if (CannotModifyInPlayMode()) - { - GUI.enabled = false; - } - - UpdatePrefabModifiedProperties(); - - var defaultLabelStyle = EditorStyles.label.fontStyle; - - EditorStyles.label.fontStyle = viewEventPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowEventMenu( - UnityEventWatcher.GetBindableEvents(targetScript.gameObject) - .OrderBy(evt => evt.Name) - .ToArray(), - updatedValue => targetScript.ViewEventName = updatedValue, - targetScript.ViewEventName - ); - - EditorStyles.label.fontStyle = viewPropertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - Type viewPropertyType; - ShowViewPropertyMenu( - new GUIContent("View property", "Property on the view to bind to"), - PropertyFinder.GetBindableProperties(targetScript.gameObject) - .OrderBy(prop => prop.ViewModelTypeName) - .ThenBy(prop => prop.MemberName) - .ToArray(), - updatedValue => targetScript.ViewPropertName = updatedValue, - targetScript.ViewPropertName, - out viewPropertyType - ); - - // Don't let the user set other options until they've set the event and view property. - var guiPreviouslyEnabled = GUI.enabled; - if (string.IsNullOrEmpty(targetScript.ViewEventName) - || string.IsNullOrEmpty(targetScript.ViewPropertName)) - { - GUI.enabled = false; - } - - var viewAdapterTypeNames = GetAdapterTypeNames( - type => viewPropertyType == null || - TypeResolver.FindAdapterAttribute(type).OutputType == viewPropertyType - ); - - EditorStyles.label.fontStyle = viewAdapterPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowAdapterMenu( - new GUIContent( - "View adapter", - "Adapter that converts values sent from the view-model to the view." - ), - viewAdapterTypeNames, - targetScript.ViewAdapterTypeName, - newValue => - { - // Get rid of old adapter options if we changed the type of the adapter. - if (newValue != targetScript.ViewAdapterTypeName) - { - Undo.RecordObject(targetScript, "Set view adapter options"); - targetScript.ViewAdapterOptions = null; - } - - UpdateProperty( - updatedValue => targetScript.ViewAdapterTypeName = updatedValue, - targetScript.ViewAdapterTypeName, - newValue, - "Set view adapter" - ); - } - ); - - EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - Type viewAdapterType; - viewAdapterOptionsFade.target = ShouldShowAdapterOptions( - targetScript.ViewAdapterTypeName, - out viewAdapterType - ); - ShowAdapterOptionsMenu( - "View adapter options", - viewAdapterType, - options => targetScript.ViewAdapterOptions = options, - targetScript.ViewAdapterOptions, - viewAdapterOptionsFade.faded - ); - - EditorGUILayout.Space(); - - EditorStyles.label.fontStyle = viewModelPropertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - var adaptedViewPropertyType = AdaptTypeBackward( - viewPropertyType, - targetScript.ViewAdapterTypeName - ); - ShowViewModelPropertyMenu( - new GUIContent( - "View-model property", - "Property on the view-model to bind to." - ), - TypeResolver.FindBindableProperties(targetScript), - updatedValue => targetScript.ViewModelPropertyName = updatedValue, - targetScript.ViewModelPropertyName, - prop => prop.PropertyType == adaptedViewPropertyType - ); - - var viewModelAdapterTypeNames = GetAdapterTypeNames( - type => adaptedViewPropertyType == null || - TypeResolver.FindAdapterAttribute(type).OutputType == adaptedViewPropertyType - ); - - EditorStyles.label.fontStyle = viewModelAdapterPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowAdapterMenu( - new GUIContent( - "View-model adapter", - "Adapter that converts from the view back to the view-model" - ), - viewModelAdapterTypeNames, - targetScript.ViewModelAdapterTypeName, - newValue => - { - if (newValue != targetScript.ViewModelAdapterTypeName) - { - Undo.RecordObject(targetScript, "Set view-model adapter options"); - targetScript.ViewModelAdapterOptions = null; - } - - UpdateProperty( - updatedValue => targetScript.ViewModelAdapterTypeName = updatedValue, - targetScript.ViewModelAdapterTypeName, - newValue, - "Set view-model adapter" - ); - } - ); - - EditorStyles.label.fontStyle = viewModelAdapterOptionsPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - Type viewModelAdapterType; - viewModelAdapterOptionsFade.target = ShouldShowAdapterOptions( - targetScript.ViewModelAdapterTypeName, - out viewModelAdapterType - ); - ShowAdapterOptionsMenu( - "View-model adapter options", - viewModelAdapterType, - options => targetScript.ViewModelAdapterOptions = options, - targetScript.ViewModelAdapterOptions, - viewModelAdapterOptionsFade.faded - ); - - EditorGUILayout.Space(); - - var expectionAdapterTypeNames = GetAdapterTypeNames( - type => TypeResolver.FindAdapterAttribute(type).InputType == typeof(Exception) - ); - - EditorStyles.label.fontStyle = exceptionPropertyPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - var adaptedExceptionPropertyType = AdaptTypeForward( - typeof(Exception), - targetScript.ExceptionAdapterTypeName - ); - ShowViewModelPropertyMenuWithNone( - new GUIContent( - "Exception property", - "Property on the view-model to bind the exception to." - ), - TypeResolver.FindBindableProperties(targetScript), - updatedValue => targetScript.ExceptionPropertyName = updatedValue, - targetScript.ExceptionPropertyName, - prop => prop.PropertyType == adaptedExceptionPropertyType - ); - - EditorStyles.label.fontStyle = exceptionAdapterPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - ShowAdapterMenu( - new GUIContent( - "Exception adapter", - "Adapter that handles exceptions thrown by the view-model adapter" - ), - expectionAdapterTypeNames, - targetScript.ExceptionAdapterTypeName, - newValue => - { - if (newValue != targetScript.ExceptionAdapterTypeName) - { - Undo.RecordObject(targetScript, "Set exception adapter options"); - targetScript.ExceptionAdapterOptions = null; - } - - UpdateProperty( - updatedValue => targetScript.ExceptionAdapterTypeName = updatedValue, - targetScript.ExceptionAdapterTypeName, - newValue, - "Set exception adapter" - ); - } - ); - - EditorStyles.label.fontStyle = exceptionAdapterOptionsPrefabModified - ? FontStyle.Bold - : defaultLabelStyle; - - Type exceptionAdapterType; - exceptionAdapterOptionsFade.target = ShouldShowAdapterOptions( - targetScript.ExceptionAdapterTypeName, - out exceptionAdapterType - ); - ShowAdapterOptionsMenu( - "Exception adapter options", - exceptionAdapterType, - options => targetScript.ExceptionAdapterOptions = options, - targetScript.ExceptionAdapterOptions, - exceptionAdapterOptionsFade.faded - ); - - EditorStyles.label.fontStyle = defaultLabelStyle; - - GUI.enabled = guiPreviouslyEnabled; - } - - /// - /// Check whether each of the properties on the object have been changed - /// from the value in the prefab. - /// - private void UpdatePrefabModifiedProperties() - { - var property = serializedObject.GetIterator(); - // Need to call Next(true) to get the first child. Once we have it, Next(false) - // will iterate through the properties. - property.Next(true); - do - { - switch (property.name) - { - case "viewEventName": - viewEventPrefabModified = property.prefabOverride; - break; - - case "viewPropertyName": - viewPropertyPrefabModified = property.prefabOverride; - break; - - case "viewAdapterTypeName": - viewAdapterPrefabModified = property.prefabOverride; - break; - - case "viewAdapterOptions": - viewAdapterOptionsPrefabModified = property.prefabOverride; - break; - - case "viewModelPropertyName": - viewModelPropertyPrefabModified = property.prefabOverride; - break; - - case "viewModelAdapterTypeName": - viewModelAdapterPrefabModified = property.prefabOverride; - break; - - case "viewModelAdapterOptions": - viewModelAdapterOptionsPrefabModified = property.prefabOverride; - break; - - case "exceptionPropertyName": - exceptionPropertyPrefabModified = property.prefabOverride; - break; - - case "exceptionAdapterTypeName": - exceptionAdapterPrefabModified = property.prefabOverride; - break; - - case "exceptionAdapterOptions": - exceptionAdapterOptionsPrefabModified = property.prefabOverride; - break; - } - } - while (property.Next(false)); - } - } -} +using System; +using System.Linq; +using UnityEditor; +using UnityEditor.AnimatedValues; +using UnityEngine; +using UnityWeld.Binding; +using UnityWeld.Binding.Internal; + +namespace UnityWeld_Editor +{ + [CustomEditor(typeof(TwoWayPropertyBinding))] + class TwoWayPropertyBindingEditor : BaseBindingEditor + { + private TwoWayPropertyBinding targetScript; + + private AnimBool viewAdapterOptionsFade; + private AnimBool viewModelAdapterOptionsFade; + private AnimBool exceptionAdapterOptionsFade; + + // Whether properties in the target script differ from the value in the prefab. + // Needed to know which ones to display as bold in the inspector. + private bool viewEventPrefabModified; + private bool viewPropertyPrefabModified; + private bool viewAdapterPrefabModified; + private bool viewAdapterOptionsPrefabModified; + + private bool viewModelPropertyPrefabModified; + private bool viewModelAdapterPrefabModified; + private bool viewModelAdapterOptionsPrefabModified; + + private bool exceptionPropertyPrefabModified; + private bool exceptionAdapterPrefabModified; + private bool exceptionAdapterOptionsPrefabModified; + + protected override void OnEnabled() + { + targetScript = (TwoWayPropertyBinding)target; + + viewAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ViewAdapterId, out _)); + viewModelAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ViewModelAdapterId, out _)); + exceptionAdapterOptionsFade = new AnimBool(ShouldShowAdapterOptions(targetScript.ExceptionAdapterTypeName, out _)); + + viewAdapterOptionsFade.valueChanged.AddListener(Repaint); + viewModelAdapterOptionsFade.valueChanged.AddListener(Repaint); + exceptionAdapterOptionsFade.valueChanged.AddListener(Repaint); + } + + private void OnDisable() + { + viewAdapterOptionsFade.valueChanged.RemoveListener(Repaint); + viewModelAdapterOptionsFade.valueChanged.RemoveListener(Repaint); + exceptionAdapterOptionsFade.valueChanged.RemoveListener(Repaint); + } + + protected override void OnInspector() + { + UpdatePrefabModifiedProperties(); + + EditorStyles.label.fontStyle = viewEventPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowEventMenu( + UnityEventWatcher.GetBindableEvents(targetScript.gameObject) + .OrderBy(evt => evt.Name) + .ToArray(), + updatedValue => targetScript.ViewEventName = updatedValue, + targetScript.ViewEventName + ); + + EditorStyles.label.fontStyle = viewPropertyPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + Type viewPropertyType; + ShowViewPropertyMenu( + new GUIContent("View property", "Property on the view to bind to"), + PropertyFinder.GetBindableProperties(targetScript.gameObject), + updatedValue => targetScript.ViewPropertyName = updatedValue, + targetScript.ViewPropertyName, + out viewPropertyType + ); + + // Don't let the user set other options until they've set the event and view property. + var guiPreviouslyEnabled = GUI.enabled; + if (string.IsNullOrEmpty(targetScript.ViewEventName) + || string.IsNullOrEmpty(targetScript.ViewPropertyName)) + { + GUI.enabled = false; + } + + var viewAdapterTypeNames = TypeResolver.GetAdapterIds( + o => viewPropertyType == null || o.OutType == viewPropertyType); + + EditorStyles.label.fontStyle = viewAdapterPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowAdapterMenu( + new GUIContent( + "View adapter", + "Adapter that converts values sent from the view-model to the view." + ), + viewAdapterTypeNames, + targetScript.ViewAdapterId, + newValue => + { + // Get rid of old adapter options if we changed the type of the adapter. + if (newValue != targetScript.ViewAdapterId) + { + Undo.RecordObject(targetScript, "Set view adapter options"); + targetScript.ViewAdapterOptions = null; + } + + UpdateProperty( + updatedValue => targetScript.ViewAdapterId = updatedValue, + targetScript.ViewAdapterId, + newValue, + "Set view adapter" + ); + } + ); + + EditorStyles.label.fontStyle = viewAdapterOptionsPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + Type viewAdapterType; + viewAdapterOptionsFade.target = ShouldShowAdapterOptions( + targetScript.ViewAdapterId, + out viewAdapterType + ); + ShowAdapterOptionsMenu( + "View adapter options", + viewAdapterType, + options => targetScript.ViewAdapterOptions = options, + targetScript.ViewAdapterOptions, + viewAdapterOptionsFade.faded + ); + + EditorGUILayout.Space(); + + EditorStyles.label.fontStyle = viewModelPropertyPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + var adaptedViewPropertyType = AdaptTypeBackward( + viewPropertyType, + targetScript.ViewAdapterId + ); + ShowViewModelPropertyMenu( + new GUIContent( + "View-model property", + "Property on the view-model to bind to." + ), + TypeResolver.FindBindableProperties(targetScript), + updatedValue => targetScript.ViewModelPropertyName = updatedValue, + targetScript.ViewModelPropertyName, + prop => prop.PropertyType == adaptedViewPropertyType + ); + + var viewModelAdapterTypeNames = TypeResolver.GetAdapterIds( + o => adaptedViewPropertyType == null || o.OutType == adaptedViewPropertyType); + + EditorStyles.label.fontStyle = viewModelAdapterPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowAdapterMenu( + new GUIContent( + "View-model adapter", + "Adapter that converts from the view back to the view-model" + ), + viewModelAdapterTypeNames, + targetScript.ViewModelAdapterId, + newValue => + { + if (newValue != targetScript.ViewModelAdapterId) + { + Undo.RecordObject(targetScript, "Set view-model adapter options"); + targetScript.ViewModelAdapterOptions = null; + } + + UpdateProperty( + updatedValue => targetScript.ViewModelAdapterId = updatedValue, + targetScript.ViewModelAdapterId, + newValue, + "Set view-model adapter" + ); + } + ); + + EditorStyles.label.fontStyle = viewModelAdapterOptionsPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + Type viewModelAdapterType; + viewModelAdapterOptionsFade.target = ShouldShowAdapterOptions( + targetScript.ViewModelAdapterId, + out viewModelAdapterType + ); + ShowAdapterOptionsMenu( + "View-model adapter options", + viewModelAdapterType, + options => targetScript.ViewModelAdapterOptions = options, + targetScript.ViewModelAdapterOptions, + viewModelAdapterOptionsFade.faded + ); + + EditorGUILayout.Space(); + + var expectionAdapterTypeNames = TypeResolver.GetAdapterIds( + o => o.InType == typeof(Exception)); + + EditorStyles.label.fontStyle = exceptionPropertyPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + var adaptedExceptionPropertyType = AdaptTypeForward( + typeof(Exception), + targetScript.ExceptionAdapterTypeName + ); + ShowViewModelPropertyMenuWithNone( + new GUIContent( + "Exception property", + "Property on the view-model to bind the exception to." + ), + TypeResolver.FindBindableProperties(targetScript), + updatedValue => targetScript.ExceptionPropertyName = updatedValue, + targetScript.ExceptionPropertyName, + prop => prop.PropertyType == adaptedExceptionPropertyType + ); + + EditorStyles.label.fontStyle = exceptionAdapterPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + ShowAdapterMenu( + new GUIContent( + "Exception adapter", + "Adapter that handles exceptions thrown by the view-model adapter" + ), + expectionAdapterTypeNames, + targetScript.ExceptionAdapterTypeName, + newValue => + { + if (newValue != targetScript.ExceptionAdapterTypeName) + { + Undo.RecordObject(targetScript, "Set exception adapter options"); + targetScript.ExceptionAdapterOptions = null; + } + + UpdateProperty( + updatedValue => targetScript.ExceptionAdapterTypeName = updatedValue, + targetScript.ExceptionAdapterTypeName, + newValue, + "Set exception adapter" + ); + } + ); + + EditorStyles.label.fontStyle = exceptionAdapterOptionsPrefabModified + ? FontStyle.Bold + : DefaultFontStyle; + + Type exceptionAdapterType; + exceptionAdapterOptionsFade.target = ShouldShowAdapterOptions( + targetScript.ExceptionAdapterTypeName, + out exceptionAdapterType + ); + ShowAdapterOptionsMenu( + "Exception adapter options", + exceptionAdapterType, + options => targetScript.ExceptionAdapterOptions = options, + targetScript.ExceptionAdapterOptions, + exceptionAdapterOptionsFade.faded + ); + + GUI.enabled = guiPreviouslyEnabled; + } + + /// + /// Check whether each of the properties on the object have been changed + /// from the value in the prefab. + /// + private void UpdatePrefabModifiedProperties() + { + var property = serializedObject.GetIterator(); + // Need to call Next(true) to get the first child. Once we have it, Next(false) + // will iterate through the properties. + property.Next(true); + do + { + switch (property.name) + { + case "viewEventName": + viewEventPrefabModified = property.prefabOverride; + break; + + case "viewPropertyName": + viewPropertyPrefabModified = property.prefabOverride; + break; + + case "viewAdapterTypeName": + viewAdapterPrefabModified = property.prefabOverride; + break; + + case "viewAdapterOptions": + viewAdapterOptionsPrefabModified = property.prefabOverride; + break; + + case "viewModelPropertyName": + viewModelPropertyPrefabModified = property.prefabOverride; + break; + + case "viewModelAdapterTypeName": + viewModelAdapterPrefabModified = property.prefabOverride; + break; + + case "viewModelAdapterOptions": + viewModelAdapterOptionsPrefabModified = property.prefabOverride; + break; + + case "exceptionPropertyName": + exceptionPropertyPrefabModified = property.prefabOverride; + break; + + case "exceptionAdapterTypeName": + exceptionAdapterPrefabModified = property.prefabOverride; + break; + + case "exceptionAdapterOptions": + exceptionAdapterOptionsPrefabModified = property.prefabOverride; + break; + } + } + while (property.Next(false)); + } + } +} diff --git a/Editor/TwoWayPropertyBindingEditor.cs.meta b/Editor/TwoWayPropertyBindingEditor.cs.meta new file mode 100644 index 0000000..5945f49 --- /dev/null +++ b/Editor/TwoWayPropertyBindingEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c7ae872ed2100b46a600b9b54780bf0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UnityWeld.Editor.asmdef b/Editor/UnityWeld.Editor.asmdef new file mode 100644 index 0000000..e1b44dc --- /dev/null +++ b/Editor/UnityWeld.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "UnityWeld.Editor", + "rootNamespace": "UnityWeld_Editor", + "references": [ + "GUID:af0045edb0742c34c9f8aefb8323e52c" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Editor/UnityWeld.Editor.asmdef.meta b/Editor/UnityWeld.Editor.asmdef.meta new file mode 100644 index 0000000..de9c452 --- /dev/null +++ b/Editor/UnityWeld.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c29ab574f158e5945b4ec0492d3c47e7 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/LICENSE.md.meta b/LICENSE.md.meta new file mode 100644 index 0000000..5205a0d --- /dev/null +++ b/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 950ecacf92f7f7743b5e5d373d964b7f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index e0d38fe..6bdae33 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,16 @@ # Unity-Weld -[![NuGet](https://img.shields.io/nuget/dt/RSG.UnityWeld.svg)](https://www.nuget.org/packages/RSG.UnityWeld/) -[![NuGet](https://img.shields.io/nuget/v/RSG.UnityWeld.svg)](https://www.nuget.org/packages/RSG.UnityWeld/) -[![Build Status](https://travis-ci.org/Real-Serious-Games/Unity-Weld.svg?branch=master)](https://travis-ci.org/Real-Serious-Games/Unity-Weld) - - *[MVVM-style](https://msdn.microsoft.com/en-us/library/hh848246.aspx) data-binding system for Unity.* -Unity-Weld is a library for Unity 5+ that enables two-way data binding between Unity UI widgets and game/business logic code. This reduces boiler-plate code that would otherwise be necessary for things like updating the UI when a property changes, removes the need for messy links between objects in the scene that can be broken easily, and allows easier unit testing of code by providing a layer of abstraction between the UI and your core logic code. +Unity-Weld is a library for Unity 2019+ that enables two-way data binding between Unity UI widgets and game/business logic code. This reduces boiler-plate code that would otherwise be necessary for things like updating the UI when a property changes, removes the need for messy links between objects in the scene that can be broken easily, and allows easier unit testing of code by providing a layer of abstraction between the UI and your core logic code. A series of articles on Unity Weld has been published on [What Could Possibly Go Wrong](http://www.what-could-possibly-go-wrong.com/bringing-mvvm-to-unity-part-1-about-mvvm-and-unity-weld). -Example Unity project can be found here: [https://github.com/Real-Serious-Games/Unity-Weld-Examples](https://github.com/Real-Serious-Games/Unity-Weld-Examples). +FOR ORIGINAL FORK: Example Unity project can be found here: [https://github.com/Real-Serious-Games/Unity-Weld-Examples](https://github.com/Real-Serious-Games/Unity-Weld-Examples). ## Installation To install Unity-Weld in a new or existing Unity project: - - Load `Unity-Weld.sln` in Visual Studio and build it - - Copy `UnityWeld.dll` into your Unity project and place in any directory within `Assets` - - Copy `UnityWeld_Editor.dll` into your Unity project and place it inside an `Editor` folder within `Assets` - -Alternatively, just copy the `UnityWeld/Binding` and `UnityWeld/Widgets` folders into your `Assets` directory in your Unity project, and copy all the .cs files in `UnityWeld_Editor` to a folder named `Editor` inside your `Assets` directory. - - -## Getting started - -Check out the [Unity-Weld-Examples](https://github.com/Real-Serious-Games/Unity-Weld-Examples) repository for some examples of how to use Unity-Weld. - -[API docmentation](https://github.com/Real-Serious-Games/Unity-Weld/wiki) is on our wiki. + Option 1: use package.json to locally install as UPM package using UPM package mananger + Option 2: generate upm package using npm tool (npm pack), upload to your feed and add your feed to Unity Package Manager -If you're interested in getting involved feel free to check out the [roadmap on Trello](https://trello.com/b/KVFUvGR0), or submit a pull request. Make sure to read our [contributing guide](CONTRIBUTING) first. +Alternatively, just copy the `Editor` to `Scripts/UnityWeld/Editor` and `Runtime` to `Scripts/UnityWeld/Runtime` into your `Assets` directory in your Unity project. diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..9776615 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 43a15502f0f199743b3e22834c7408ee +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..3d52be9 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7ece62dfb5dd94349a31b908b4e1355d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/AOTOptimisationHelper.cs b/Runtime/AOTOptimisationHelper.cs similarity index 97% rename from UnityWeld/AOTOptimisationHelper.cs rename to Runtime/AOTOptimisationHelper.cs index d348c88..258b022 100644 --- a/UnityWeld/AOTOptimisationHelper.cs +++ b/Runtime/AOTOptimisationHelper.cs @@ -1,54 +1,54 @@ -using UnityEngine; -using UnityEngine.EventSystems; -using UnityWeld.Binding.Internal; -// ReSharper disable UnusedMember.Global -// ReSharper disable UnusedMember.Local -// ReSharper disable UnusedVariable - -#pragma warning disable 219 // Disable warning that variable is never used - -namespace UnityWeld -{ - /// - /// In order for certain generic types to not be optimised-out by IL2CPP for - /// platforms like Xbox One, iPhone and WebGL, we need to reference them at - /// least once in code instead of just calling them via reflection. - /// - /// See this page for more details: - /// https://docs.unity3d.com/Manual/TroubleShootingIPhone.html - /// In the section "The game crashes with the error message “ExecutionEngineException: - /// Attempting to JIT compile method ‘SometType`1<SomeValueType>:.ctor ()’ while - /// running with –aot-only.”" - /// - internal class AOTOptimisationHelper - { - // Even though this method is never called, the fact that it exists will - // ensure the compiler includes the types referenced in it so that we can - // later refer to those via reflection. - private void EnsureGenericTypes() - { - // Used by InputField - var strEventBinder = new UnityEventBinder(null, null); - - // Used by Slider and Scrollbar - var floatEventBinder = new UnityEventBinder(null, null); - - // Used by Toggle - var boolEventBinder = new UnityEventBinder(null, null); - - // Used by Dropdown - var intEventBinder = new UnityEventBinder(null, null); - - // Used by ScrollRect - var vector2EventBinder = new UnityEventBinder(null, null); - - // Used by ColorTween - var colorEventBinder = new UnityEventBinder(null, null); - - // Used by EventTrigger - var baseEventDataEventBinder = new UnityEventBinder(null, null); - } - } -} - +using UnityEngine; +using UnityEngine.EventSystems; +using UnityWeld.Binding.Internal; +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedMember.Local +// ReSharper disable UnusedVariable + +#pragma warning disable 219 // Disable warning that variable is never used + +namespace UnityWeld +{ + /// + /// In order for certain generic types to not be optimised-out by IL2CPP for + /// platforms like Xbox One, iPhone and WebGL, we need to reference them at + /// least once in code instead of just calling them via reflection. + /// + /// See this page for more details: + /// https://docs.unity3d.com/Manual/TroubleShootingIPhone.html + /// In the section "The game crashes with the error message “ExecutionEngineException: + /// Attempting to JIT compile method ‘SometType`1<SomeValueType>:.ctor ()’ while + /// running with –aot-only.”" + /// + internal class AOTOptimisationHelper + { + // Even though this method is never called, the fact that it exists will + // ensure the compiler includes the types referenced in it so that we can + // later refer to those via reflection. + private void EnsureGenericTypes() + { + // Used by InputField + var strEventBinder = new UnityEventBinder(null, null); + + // Used by Slider and Scrollbar + var floatEventBinder = new UnityEventBinder(null, null); + + // Used by Toggle + var boolEventBinder = new UnityEventBinder(null, null); + + // Used by Dropdown + var intEventBinder = new UnityEventBinder(null, null); + + // Used by ScrollRect + var vector2EventBinder = new UnityEventBinder(null, null); + + // Used by ColorTween + var colorEventBinder = new UnityEventBinder(null, null); + + // Used by EventTrigger + var baseEventDataEventBinder = new UnityEventBinder(null, null); + } + } +} + #pragma warning restore 219 \ No newline at end of file diff --git a/Runtime/AOTOptimisationHelper.cs.meta b/Runtime/AOTOptimisationHelper.cs.meta new file mode 100644 index 0000000..a7e09c3 --- /dev/null +++ b/Runtime/AOTOptimisationHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b067127a7e01f0408d7cb11797df5c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Binding.meta b/Runtime/Binding.meta new file mode 100644 index 0000000..8f559c5 --- /dev/null +++ b/Runtime/Binding.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1f7dff4136c1c10459fc630a239805ee +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/AbstractMemberBinding.cs b/Runtime/Binding/AbstractMemberBinding.cs similarity index 62% rename from UnityWeld/Binding/AbstractMemberBinding.cs rename to Runtime/Binding/AbstractMemberBinding.cs index 11afd27..b3e9324 100644 --- a/UnityWeld/Binding/AbstractMemberBinding.cs +++ b/Runtime/Binding/AbstractMemberBinding.cs @@ -1,191 +1,197 @@ -using System.Linq; -using UnityEngine; -using UnityWeld.Binding.Exceptions; -using UnityWeld.Binding.Internal; - -namespace UnityWeld.Binding -{ - /// - /// Base class for binders to Unity MonoBehaviours. - /// - [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] - public abstract class AbstractMemberBinding : MonoBehaviour, IMemberBinding - { - /// - /// Initialise this binding. Used when we first start the scene. - /// Detaches any attached view models, finds available view models afresh and then connects the binding. - /// - public virtual void Init() - { - Disconnect(); - - Connect(); - } - - /// - /// Scan up the hierarchy and find a view model that corresponds to the specified name. - /// - private object FindViewModel(string viewModelName) - { - var trans = transform; - while (trans != null) - { - var components = trans.GetComponents(); - var monoBehaviourViewModel = components - .FirstOrDefault(component => component.GetType().ToString() == viewModelName); - if (monoBehaviourViewModel != null) - { - return monoBehaviourViewModel; - } - - var providedViewModel = components - .Select(component => component as IViewModelProvider) - .Where(component => component != null) - .FirstOrDefault( - viewModelBinding => viewModelBinding.GetViewModelTypeName() == viewModelName && -#pragma warning disable 252,253 // Warning says unintended reference comparison, but we do want to compare references - (object)viewModelBinding != this -#pragma warning restore 252,253 - ); - - if (providedViewModel != null) - { - return providedViewModel.GetViewModel(); - } - - trans = trans.parent; - } - - throw new ViewModelNotFoundException(string.Format("Tried to get view model {0} but it could not be found on " - + "object {1}. Check that a ViewModelBinding for that view model exists further up in " - + "the scene hierarchy. ", viewModelName, gameObject.name) - ); - } - - /// - /// Find the type of the adapter with the specified name and create it. - /// - protected static IAdapter CreateAdapter(string adapterTypeName) - { - if (string.IsNullOrEmpty(adapterTypeName)) - { - return null; - } - - var adapterType = TypeResolver.FindAdapterType(adapterTypeName); - if (adapterType == null) - { - throw new NoSuchAdapterException(adapterTypeName); - } - - if (!typeof(IAdapter).IsAssignableFrom(adapterType)) - { - throw new InvalidAdapterException(string.Format("Type '{0}' does not implement IAdapter and cannot be used as an adapter.", adapterTypeName)); - } - - return AdapterResolver.CreateAdapter(adapterType); - } - - /// - /// Make a property end point for a property on the view model. - /// - protected PropertyEndPoint MakeViewModelEndPoint(string viewModelPropertyName, string adapterTypeName, AdapterOptions adapterOptions) - { - string propertyName; - object viewModel; - ParseViewModelEndPointReference(viewModelPropertyName, out propertyName, out viewModel); - - var adapter = CreateAdapter(adapterTypeName); - - return new PropertyEndPoint(viewModel, propertyName, adapter, adapterOptions, "view-model", this); - } - - /// - /// Parse an end-point reference including a type name and member name separated by a period. - /// - protected static void ParseEndPointReference(string endPointReference, out string memberName, out string typeName) - { - var lastPeriodIndex = endPointReference.LastIndexOf('.'); - if (lastPeriodIndex == -1) - { - throw new InvalidEndPointException( - "No period was found, expected end-point reference in the following format: .. " + - "Provided end-point reference: " + endPointReference - ); - } - - typeName = endPointReference.Substring(0, lastPeriodIndex); - memberName = endPointReference.Substring(lastPeriodIndex + 1); - //Due to (undocumented) unity behaviour, some of their components do not work with the namespace when using GetComponent(""), and all of them work without the namespace - //So to be safe, we remove all namespaces from any component that starts with UnityEngine - if (typeName.StartsWith("UnityEngine.")) - { - typeName = typeName.Substring(typeName.LastIndexOf('.') + 1); - } - if (typeName.Length == 0 || memberName.Length == 0) - { - throw new InvalidEndPointException( - "Bad format for end-point reference, expected the following format: .. " + - "Provided end-point reference: " + endPointReference - ); - } - } - - /// - /// Parse an end-point reference and search up the hierarchy for the named view-model. - /// - protected void ParseViewModelEndPointReference(string endPointReference, out string memberName, out object viewModel) - { - string viewModelName; - ParseEndPointReference(endPointReference, out memberName, out viewModelName); - - viewModel = FindViewModel(viewModelName); - if (viewModel == null) - { - throw new ViewModelNotFoundException("Failed to find view-model in hierarchy: " + viewModelName); - } - } - - /// - /// Parse an end-point reference and get the component for the view. - /// - protected void ParseViewEndPointReference(string endPointReference, out string memberName, out Component view) - { - string boundComponentType; - ParseEndPointReference(endPointReference, out memberName, out boundComponentType); - - view = GetComponent(boundComponentType); - if (view == null) - { - throw new ComponentNotFoundException("Failed to find component on current game object: " + boundComponentType); - } - } - - /// - /// Connect to all the attached view models - /// - public abstract void Connect(); - - /// - /// Disconnect from all attached view models. - /// - public abstract void Disconnect(); - - /// - /// Standard MonoBehaviour awake message, do not call this explicitly. - /// Initialises the binding. - /// - protected void Awake() - { - Init(); - } - - /// - /// Clean up when the game object is destroyed. - /// - public void OnDestroy() - { - Disconnect(); - } - } -} +using System.Linq; +using UnityEngine; +using UnityWeld.Binding.Exceptions; +using UnityWeld.Binding.Internal; + +namespace UnityWeld.Binding +{ + /// + /// Base class for binders to Unity MonoBehaviours. + /// + [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] + public abstract class AbstractMemberBinding : MonoBehaviour, IMemberBinding + { + private bool _isInitCalled; + + [SerializeField, Header("Automatically bind once on \"OnEnable()\"")] + private bool _isAutoConnection; + + + /// + /// Initialise this binding. Used when we first start the scene. + /// Detaches any attached view models, finds available view models afresh and then connects the binding. + /// + public virtual void Init() + { + if(_isAutoConnection && !gameObject.activeInHierarchy) + { + return; //wait for enabling + } + + if (_isInitCalled) + { + return; //avoid double connect + } + + _isInitCalled = true; + + Disconnect(); + Connect(); + } + + /// + /// Scan up the hierarchy and find a view model that corresponds to the specified name. + /// + private object FindViewModel(string viewModelName) + { + var trans = transform; + while(trans != null) + { + using(var cache = trans.gameObject.GetComponentsWithCache(false)) + { + var monoBehaviourViewModel = cache.Components + .FirstOrDefault(component => component.GetType().ToString() == viewModelName); + if(monoBehaviourViewModel != null) + { + return monoBehaviourViewModel; + } + + var providedViewModel = cache.Components + .Select(component => component.GetViewModelData()) + .Where(component => component != null) + .FirstOrDefault(viewModelData => viewModelData.TypeName == viewModelName); + + if(providedViewModel != null) + { + return providedViewModel.Model; + } + } + + trans = trans.parent; + } + + throw new ViewModelNotFoundException( + $"Tried to get view model {viewModelName} but it could not be found on " + + $"object {gameObject.name}. Check that a ViewModelBinding for that view model exists further up in " + + "the scene hierarchy. " + ); + } + + /// + /// Make a property end point for a property on the view model. + /// + protected PropertyEndPoint MakeViewModelEndPoint(string viewModelPropertyName, string adapterId, + AdapterOptions adapterOptions) + { + string propertyName; + object viewModel; + ParseViewModelEndPointReference(viewModelPropertyName, out propertyName, out viewModel); + + var adapter = TypeResolver.GetAdapter(adapterId); + return new PropertyEndPoint(viewModel, propertyName, adapter, adapterOptions, "view-model", this); + } + + /// + /// Parse an end-point reference including a type name and member name separated by a period. + /// + protected static void ParseEndPointReference(string endPointReference, out string memberName, + out string typeName) + { + var lastPeriodIndex = endPointReference.LastIndexOf('.'); + if(lastPeriodIndex == -1) + { + throw new InvalidEndPointException( + "No period was found, expected end-point reference in the following format: .. " + + "Provided end-point reference: " + endPointReference + ); + } + + typeName = endPointReference.Substring(0, lastPeriodIndex); + memberName = endPointReference.Substring(lastPeriodIndex + 1); + //Due to (undocumented) unity behaviour, some of their components do not work with the namespace when using GetComponent(""), and all of them work without the namespace + //So to be safe, we remove all namespaces from any component that starts with UnityEngine + if(typeName.StartsWith("UnityEngine.")) + { + typeName = typeName.Substring(typeName.LastIndexOf('.') + 1); + } + + if(typeName.Length == 0 || memberName.Length == 0) + { + throw new InvalidEndPointException( + "Bad format for end-point reference, expected the following format: .. " + + "Provided end-point reference: " + endPointReference + ); + } + } + + /// + /// Parse an end-point reference and search up the hierarchy for the named view-model. + /// + protected void ParseViewModelEndPointReference(string endPointReference, out string memberName, + out object viewModel) + { + string viewModelName; + ParseEndPointReference(endPointReference, out memberName, out viewModelName); + + viewModel = FindViewModel(viewModelName); + if(viewModel == null) + { + throw new ViewModelNotFoundException("Failed to find view-model in hierarchy: " + viewModelName); + } + } + + /// + /// Parse an end-point reference and get the component for the view. + /// + protected void ParseViewEndPointReference(string endPointReference, out string memberName, out Component view) + { + string boundComponentType; + ParseEndPointReference(endPointReference, out memberName, out boundComponentType); + + view = GetComponent(boundComponentType); + if(view == null) + { + throw new ComponentNotFoundException("Failed to find component on current game object: " + + boundComponentType); + } + } + + /// + /// Connect to all the attached view models + /// + public abstract void Connect(); + + /// + /// Disconnect from all attached view models. + /// + public abstract void Disconnect(); + + /// + /// Standard MonoBehaviour awake message, do not call this explicitly. + /// Initialises the binding. + /// + protected void OnEnable() + { + if (!_isAutoConnection || _isInitCalled) + { + return; + } + + Init(); + } + + /// + /// Clean up when the game object is destroyed. + /// + public virtual void OnDestroy() + { + Disconnect(); + } + + public void ResetBinding() + { + _isInitCalled = false; + Disconnect(); + } + } +} \ No newline at end of file diff --git a/Runtime/Binding/AbstractMemberBinding.cs.meta b/Runtime/Binding/AbstractMemberBinding.cs.meta new file mode 100644 index 0000000..e2b33b9 --- /dev/null +++ b/Runtime/Binding/AbstractMemberBinding.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cbecddc181c8f044dadab0f55a663934 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Binding/AbstractTemplateSelector.cs b/Runtime/Binding/AbstractTemplateSelector.cs new file mode 100644 index 0000000..49512b5 --- /dev/null +++ b/Runtime/Binding/AbstractTemplateSelector.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Assertions; +using UnityWeld.Binding.Exceptions; +using UnityWeld.Binding.Internal; + +namespace UnityWeld.Binding +{ + public abstract class AbstractTemplateSelector : AbstractMemberBinding + { + [Header("Set templates for collection")] + [SerializeField] private Template[] _templates; + [SerializeField] private string viewModelPropertyName = string.Empty; + + private IDictionary _availableTemplates; + + /// + /// All the child objects that have been created, indexed by the view they are connected to. + /// + private readonly IDictionary _instantiatedTemplates = new Dictionary(); + + + /// + /// The view-model, cached during connection. + /// + protected object ViewModel; + + /// + /// Watches the view-model property for changes. + /// + protected PropertyWatcher ViewModelPropertyWatcher; + + /// + /// The name of the property we are binding to on the view model. + /// + public string ViewModelPropertyName + { + get => viewModelPropertyName; + set => viewModelPropertyName = value; + } + + public Template[] Templates + { + get => _templates; + set => _templates = value; + } + + /// + /// All available templates indexed by the view model the are for. + /// + protected IDictionary AvailableTemplates + { + get + { + if(_availableTemplates == null) + { + CacheTemplates(); + } + + return _availableTemplates; + } + } + + // Cache available templates. + private void CacheTemplates() + { + if(_templates == null || _templates.Length == 0) + { + return; + } + + _availableTemplates = new Dictionary(); + + + _availableTemplates = _templates + .Select(template => + { + var typeName = template.GetViewModelTypeName(); + var type = TypeResolver.TypesWithBindingAttribute + .FirstOrDefault(t => string.Equals(t.ToString(), typeName, StringComparison.Ordinal)); + + if (type == null) + { + throw new Exception($"Unable to find type with binding attribute: {typeName}. Template name: {template.name}"); + } + + return (type, template); + }) + .ToDictionary(o => o.type, o => o.template); + } + + /// + /// Returns the template that best matches the specified type. + /// + private Template FindTemplateForType(Type templateType) + { + var possibleMatches = FindTypesMatchingTemplate(templateType); + // .OrderBy(m => m.Key) + // .ToList(); + + if(possibleMatches.Count == 0) + { + throw new TemplateNotFoundException("Could not find any template matching type " + templateType); + } + + if(possibleMatches.Count > 1) + { + throw new AmbiguousTypeException("Multiple templates were found that match type " + templateType + + ". This can be caused by providing multiple templates that match types " + + templateType + + " inherits from the same level. Remove one or provide a template that more specifically matches the type."); + } + + return AvailableTemplates[possibleMatches[0]]; + } + + private static List GetTypeWithInterfaces(Type originalType) + { + var interfaces = originalType.GetInterfaces(); + var result = new List(interfaces.Length + 1) + { + originalType + }; + result.AddRange(interfaces); + return result; + } + + private static List GetBaseTypeWithInterfaces(Type originalType) + { + var interfaces = originalType.GetInterfaces(); + var result = new List(interfaces.Length + 1); + if(originalType.BaseType != null) + { + result.Add(originalType.BaseType); + } + + result.AddRange(interfaces); + return result; + } + + private List FindTypesMatchingTemplate(Type originalType) + { + var result = new List(); + var levelToCheck = GetTypeWithInterfaces(originalType); + + while(levelToCheck.Count > 0) + { + var validTypesList = new List(); + var newLevelToCheck = new List(); + foreach(var type in levelToCheck) + { + newLevelToCheck.AddRange(GetBaseTypeWithInterfaces(type)); + + if(AvailableTemplates.ContainsKey(type)) + { + validTypesList.Add(type); + } + } + + if(validTypesList.Count > 0) + { + return validTypesList; + } + + if(newLevelToCheck.Count > 0) + { + levelToCheck = newLevelToCheck; + } + } + + return result; + } + + protected virtual Template CloneTemplate(Template template) + { + return Instantiate(template, transform); + } + + protected virtual void OnTemplateDestroy(Template template) + { + template.SetBindings(false); + } + + /// + /// Create a clone of the template object and bind it to the specified view model. + /// Place the new object under the parent at the specified index, or 0 if no index + /// is specified. + /// + protected void InstantiateTemplate(object templateViewModel, int index = 0) + { + Assert.IsNotNull(templateViewModel, "Cannot instantiate child with null view model"); + + // Select template. + var selectedTemplate = FindTemplateForType(templateViewModel.GetType()); + var newObject = CloneTemplate(selectedTemplate); + + newObject.transform.SetSiblingIndex(index); + _instantiatedTemplates.Add(templateViewModel, newObject); + + // Set up child bindings before we activate the template object so that they will be configured properly before trying to connect. + newObject.InitChildBindings(templateViewModel); + newObject.gameObject.SetActive(true); + } + + /// + /// Recursively look in the type, interfaces it implements and types it inherits + /// from for a type that matches a template. Also store how many steps away from + /// the specified template the found template was. + /// + // private IEnumerable> FindTypesMatchingTemplate(Type t, int index = 0) + // { + // var baseType = t.BaseType; + // if (baseType != null && !baseType.IsInterface) + // { + // foreach (var type in FindTypesMatchingTemplate(baseType, index + 1)) + // { + // yield return type; + // } + // } + // + // foreach (var interfaceType in t.GetInterfaces()) + // { + // foreach (var type in FindTypesMatchingTemplate(interfaceType, index + 1)) + // { + // yield return type; + // } + // } + // + // if (AvailableTemplates.Keys.Contains(t)) + // { + // yield return new KeyValuePair(index, t); + // } + // } + + /// + /// Destroys the instantiated template associated with the provided object. + /// + protected void DestroyTemplate(object viewModelToDestroy) + { + var template = _instantiatedTemplates[viewModelToDestroy]; + OnTemplateDestroy(template); + _instantiatedTemplates.Remove(viewModelToDestroy); + } + + /// + /// Destroys all instantiated templates. + /// + protected void DestroyAllTemplates() + { + var templates = _instantiatedTemplates.Values.ToList(); + _instantiatedTemplates.Clear(); + + foreach(var template in templates) + { + OnTemplateDestroy(template); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Binding/AbstractTemplateSelector.cs.meta b/Runtime/Binding/AbstractTemplateSelector.cs.meta new file mode 100644 index 0000000..0cbf205 --- /dev/null +++ b/Runtime/Binding/AbstractTemplateSelector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdd4994f243a54845b03b9b5252bef5b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/AdapterOptions.cs b/Runtime/Binding/AdapterOptions.cs similarity index 94% rename from UnityWeld/Binding/AdapterOptions.cs rename to Runtime/Binding/AdapterOptions.cs index 64eda1e..bc25a9d 100644 --- a/UnityWeld/Binding/AdapterOptions.cs +++ b/Runtime/Binding/AdapterOptions.cs @@ -1,11 +1,11 @@ -using UnityEngine; - -namespace UnityWeld.Binding -{ - /// - /// Base class for adapter options. - /// - public abstract class AdapterOptions : ScriptableObject - { - } -} +using UnityEngine; + +namespace UnityWeld.Binding +{ + /// + /// Base class for adapter options. + /// + public abstract class AdapterOptions : ScriptableObject + { + } +} diff --git a/Runtime/Binding/AdapterOptions.cs.meta b/Runtime/Binding/AdapterOptions.cs.meta new file mode 100644 index 0000000..1094484 --- /dev/null +++ b/Runtime/Binding/AdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1bcafe11ba8818644a977088aeea1865 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Binding/Adapters.meta b/Runtime/Binding/Adapters.meta new file mode 100644 index 0000000..74a6d36 --- /dev/null +++ b/Runtime/Binding/Adapters.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 873eaa7364d9ac84cae6386ffe467530 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Binding/Adapters/AdapterInfo.cs b/Runtime/Binding/Adapters/AdapterInfo.cs new file mode 100644 index 0000000..e525e8f --- /dev/null +++ b/Runtime/Binding/Adapters/AdapterInfo.cs @@ -0,0 +1,62 @@ +using System; + +namespace UnityWeld.Binding.Adapters +{ + public interface IAdapterInfo + { + string Id { get; } + Type InType { get; } + Type OutType { get; } + Type OptionsType { get; } + + object Convert(object valueIn, object options); + } + + public class AdapterInfo : IAdapterInfo + { + private readonly Func _converter; + + public string Id { get; private set; } + public Type InType { get; private set; } + public Type OutType { get; private set; } + public Type OptionsType { get; private set; } + + public AdapterInfo(Func converter, string id) + { + _converter = converter; + Id = id; + InType = typeof(TIn); + OutType = typeof(TOut); + OptionsType = null; + } + + public object Convert(object valueIn, object options) + { + return _converter((TIn) valueIn); + } + } + + public class AdapterInfo : IAdapterInfo + { + private readonly Func _converter; + + public string Id { get; private set; } + public Type InType { get; private set; } + public Type OutType { get; private set; } + public Type OptionsType { get; private set; } + + public AdapterInfo(Func converter, string id) + { + _converter = converter; + Id = id; + InType = typeof(TIn); + OutType = typeof(TOut); + OptionsType = typeof(TOptions); + } + + public object Convert(object valueIn, object options) + { + return _converter((TIn) valueIn, (TOptions)options); + } + } +} \ No newline at end of file diff --git a/Runtime/Binding/Adapters/AdapterInfo.cs.meta b/Runtime/Binding/Adapters/AdapterInfo.cs.meta new file mode 100644 index 0000000..9e054c7 --- /dev/null +++ b/Runtime/Binding/Adapters/AdapterInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a78e3117732f4c0458547dc16b810843 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Binding/Adapters/BaseAdapters.cs b/Runtime/Binding/Adapters/BaseAdapters.cs new file mode 100644 index 0000000..14a2f34 --- /dev/null +++ b/Runtime/Binding/Adapters/BaseAdapters.cs @@ -0,0 +1,162 @@ +using System; +using System.Globalization; +using UnityEngine; +using UnityEngine.UI; +using UnityWeld.Binding.Internal; + +namespace UnityWeld.Binding.Adapters +{ + public static class BaseAdapters + { + /// + /// Return inverted bool value + /// + public static bool BoolInversion(bool inValue) + { + return !inValue; + } + + /// + /// Returns either the value of TrueColor from the specified adapter options if + /// the input value was true, or FalseColor if it was false. + /// + public static Color BoolToColor(bool valueIn, BoolToColorAdapterOptions options) + { + return valueIn ? options.TrueColor : options.FalseColor; + } + + /// + /// Returns either the value of TrueColors from the specified adapter options if + /// the input value was true, or FalseColors if it was false. + /// + public static ColorBlock BoolToColorBlock(bool valueIn, BoolToColorBlockAdapterOptions options) + { + return valueIn ? options.TrueColors : options.FalseColors; + } + + /// + /// Adapter for converting from a bool to a string. + /// + public static string BoolToString(bool valueIn, BoolToStringAdapterOptions options) + { + return valueIn ? options.TrueValueString : options.FalseValueString; + } + + /// + /// Adapter that converts a single Color to one of the colors inside a ColorBlock + /// + public static ColorBlock ColorToColorBlock(Color valueIn, ColorToColorBlockAdapterOptions options) + { + var colorBlock = options.DefaultColors; + switch (options.OverrideColor) + { + case ColorToColorBlockAdapterOptions.Role.Disabled: + colorBlock.disabledColor = valueIn; + break; + case ColorToColorBlockAdapterOptions.Role.Highlighed: + colorBlock.highlightedColor = valueIn; + break; + case ColorToColorBlockAdapterOptions.Role.Normal: + colorBlock.normalColor = valueIn; + break; + case ColorToColorBlockAdapterOptions.Role.Pressed: + colorBlock.pressedColor = valueIn; + break; + } + + return colorBlock; + } + + /// + /// Adapter for converting from a DateTime to an OADate as a float. + /// + public static float DateTimeToOADate(DateTime valueIn) + { + return (float)valueIn.ToOADate(); + } + + /// + /// Adapter for converting from a DateTime to a string. + /// + public static string DateTimeToString(DateTime valueIn, DateTimeToStringAdapterOptions options) + { + return valueIn.ToString(options.Format); + } + + /// + /// Adapter for converting from a float as an OADate to a DateTime. + /// + public static DateTime FloatToDateTime(float valueIn) + { + return DateTime.FromOADate(valueIn); + } + + /// + /// Adapter that converts a float to a string. + /// + public static string FloatToString(float valueIn, FloatToStringAdapterOptions options) + { + return valueIn.ToString(options.Format); + } + + /// + /// Adapter for converting from an int to a string. + /// + public static string IntToString(int valueIn) + { + return valueIn.ToString(); + } + + /// + /// Adapter for converting from a string to a DateTime, using a specified culture. + /// + public static DateTime StringCultureToDateTime(string valueIn, StringCultureToDateTimeAdapterOptions options) + { + var culture = new CultureInfo(options.CultureName); + return DateTime.Parse(valueIn, culture); + } + + /// + /// String to bool adapter that returns false if the string is null or empty, + /// otherwise true. + /// + public static bool StringEmptyToBool(string valueIn) + { + return !string.IsNullOrEmpty(valueIn); + } + + /// + /// Adapter that parses a string as a float. + /// + public static float StringToFloat(string valueIn) + { + return float.Parse(valueIn); + } + + /// + /// Adapter for converting from a string to an int. + /// + public static int StringToInt(string valueIn) + { + return int.Parse(valueIn); + } + + public static void RegisterBaseAdapters() + { + TypeResolver.RegisterAdapter(new AdapterInfo(BoolInversion, nameof(BoolInversion))); + TypeResolver.RegisterAdapter(new AdapterInfo(BoolToColor, nameof(BoolToColor))); + TypeResolver.RegisterAdapter(new AdapterInfo(BoolToColorBlock, nameof(BoolToColorBlock))); + TypeResolver.RegisterAdapter(new AdapterInfo(BoolToString, nameof(BoolToString))); + TypeResolver.RegisterAdapter(new AdapterInfo(ColorToColorBlock, nameof(ColorToColorBlock))); + TypeResolver.RegisterAdapter(new AdapterInfo(DateTimeToOADate, nameof(DateTimeToOADate))); + TypeResolver.RegisterAdapter(new AdapterInfo(DateTimeToString, nameof(DateTimeToString))); + TypeResolver.RegisterAdapter(new AdapterInfo(FloatToDateTime, nameof(FloatToDateTime))); + TypeResolver.RegisterAdapter(new AdapterInfo(FloatToString, nameof(FloatToString))); + TypeResolver.RegisterAdapter(new AdapterInfo(IntToString, nameof(IntToString))); + TypeResolver.RegisterAdapter(new AdapterInfo(StringCultureToDateTime, nameof(StringCultureToDateTime))); + TypeResolver.RegisterAdapter(new AdapterInfo(StringEmptyToBool, nameof(StringEmptyToBool))); + TypeResolver.RegisterAdapter(new AdapterInfo(StringToFloat, nameof(StringToFloat))); + TypeResolver.RegisterAdapter(new AdapterInfo(StringToInt, nameof(StringToInt))); + } + } +} \ No newline at end of file diff --git a/Runtime/Binding/Adapters/BaseAdapters.cs.meta b/Runtime/Binding/Adapters/BaseAdapters.cs.meta new file mode 100644 index 0000000..d7bc5b6 --- /dev/null +++ b/Runtime/Binding/Adapters/BaseAdapters.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 11b125bd4fcd32544a4529539831abb0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/Adapters/BoolToColorAdapterOptions.cs b/Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs similarity index 96% rename from UnityWeld/Binding/Adapters/BoolToColorAdapterOptions.cs rename to Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs index 6c76ac9..7f586bd 100644 --- a/UnityWeld/Binding/Adapters/BoolToColorAdapterOptions.cs +++ b/Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs @@ -1,22 +1,22 @@ -using UnityEngine; - -namespace UnityWeld.Binding.Adapters -{ - /// - /// Options for converting from a bool to a Unity color. - /// - [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to Color adapter options")] - [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] - public class BoolToColorAdapterOptions : AdapterOptions - { - /// - /// The value used when the bool is false. - /// - public Color FalseColor; - - /// - /// The value used when the bool is true. - /// - public Color TrueColor; - } +using UnityEngine; + +namespace UnityWeld.Binding.Adapters +{ + /// + /// Options for converting from a bool to a Unity color. + /// + [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to Color adapter options")] + [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] + public class BoolToColorAdapterOptions : AdapterOptions + { + /// + /// The value used when the bool is false. + /// + public Color FalseColor; + + /// + /// The value used when the bool is true. + /// + public Color TrueColor; + } } \ No newline at end of file diff --git a/Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs.meta b/Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs.meta new file mode 100644 index 0000000..95aec89 --- /dev/null +++ b/Runtime/Binding/Adapters/BoolToColorAdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c67f699f0789684ea6213979b0827dc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/Adapters/BoolToColorBlockAdapterOptions.cs b/Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs similarity index 96% rename from UnityWeld/Binding/Adapters/BoolToColorBlockAdapterOptions.cs rename to Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs index ab01571..020bfbb 100644 --- a/UnityWeld/Binding/Adapters/BoolToColorBlockAdapterOptions.cs +++ b/Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs @@ -1,25 +1,25 @@ -using UnityEngine; -using UnityEngine.UI; - -namespace UnityWeld.Binding.Adapters -{ - /// - /// Options for converting from a bool to a Unity color. - /// - [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to ColorBlock adapter options")] - [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] - public class BoolToColorBlockAdapterOptions : AdapterOptions - { - /// - /// The value used when the bool is false. - /// - [Header("False colors")] - public ColorBlock FalseColors; - - /// - /// The value used when the bool is true. - /// - [Header("True colors")] - public ColorBlock TrueColors; - } +using UnityEngine; +using UnityEngine.UI; + +namespace UnityWeld.Binding.Adapters +{ + /// + /// Options for converting from a bool to a Unity color. + /// + [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to ColorBlock adapter options")] + [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] + public class BoolToColorBlockAdapterOptions : AdapterOptions + { + /// + /// The value used when the bool is false. + /// + [Header("False colors")] + public ColorBlock FalseColors; + + /// + /// The value used when the bool is true. + /// + [Header("True colors")] + public ColorBlock TrueColors; + } } \ No newline at end of file diff --git a/Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs.meta b/Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs.meta new file mode 100644 index 0000000..044f065 --- /dev/null +++ b/Runtime/Binding/Adapters/BoolToColorBlockAdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9283b532ed1dea84a92d639815719846 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/Adapters/BoolToStringAdapterOptions.cs b/Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs similarity index 96% rename from UnityWeld/Binding/Adapters/BoolToStringAdapterOptions.cs rename to Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs index 48f9188..47361d7 100644 --- a/UnityWeld/Binding/Adapters/BoolToStringAdapterOptions.cs +++ b/Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs @@ -1,22 +1,22 @@ -using UnityEngine; - -namespace UnityWeld.Binding.Adapters -{ - /// - /// Options for converting from a bool to a string. - /// - [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to string adapter")] - [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] - public class BoolToStringAdapterOptions : AdapterOptions - { - /// - /// The value used when the bool is set to true. - /// - public string TrueValueString; - - /// - /// The value used when the bool is set to false. - /// - public string FalseValueString; - } +using UnityEngine; + +namespace UnityWeld.Binding.Adapters +{ + /// + /// Options for converting from a bool to a string. + /// + [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Bool to string adapter")] + [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] + public class BoolToStringAdapterOptions : AdapterOptions + { + /// + /// The value used when the bool is set to true. + /// + public string TrueValueString; + + /// + /// The value used when the bool is set to false. + /// + public string FalseValueString; + } } \ No newline at end of file diff --git a/Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs.meta b/Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs.meta new file mode 100644 index 0000000..3531bdc --- /dev/null +++ b/Runtime/Binding/Adapters/BoolToStringAdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f7217b080af9534dbc6da25f5d7d0a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/Adapters/ColorToColorBlockAdapterOptions.cs b/Runtime/Binding/Adapters/ColorToColorBlockAdapterOptions.cs similarity index 100% rename from UnityWeld/Binding/Adapters/ColorToColorBlockAdapterOptions.cs rename to Runtime/Binding/Adapters/ColorToColorBlockAdapterOptions.cs diff --git a/Runtime/Binding/Adapters/ColorToColorBlockAdapterOptions.cs.meta b/Runtime/Binding/Adapters/ColorToColorBlockAdapterOptions.cs.meta new file mode 100644 index 0000000..31cfb0c --- /dev/null +++ b/Runtime/Binding/Adapters/ColorToColorBlockAdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 799d5e0c6bc32b34791864bff0182127 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/Adapters/DateTimeToStringAdapterOptions.cs b/Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs similarity index 97% rename from UnityWeld/Binding/Adapters/DateTimeToStringAdapterOptions.cs rename to Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs index 49d8b95..a1e53a9 100644 --- a/UnityWeld/Binding/Adapters/DateTimeToStringAdapterOptions.cs +++ b/Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs @@ -1,18 +1,18 @@ -using UnityEngine; - -namespace UnityWeld.Binding.Adapters -{ - /// - /// Options for converting a DateTime to a string. - /// - [CreateAssetMenu(menuName = "Unity Weld/Adapter options/DateTime to string adapter")] - [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] - public class DateTimeToStringAdapterOptions : AdapterOptions - { - /// - /// Format passed in to the DateTime.ToString method. - /// See this page for details on usage: https://msdn.microsoft.com/en-us/library/zdtaw1bw(v=vs.110).aspx - /// - public string Format; - } +using UnityEngine; + +namespace UnityWeld.Binding.Adapters +{ + /// + /// Options for converting a DateTime to a string. + /// + [CreateAssetMenu(menuName = "Unity Weld/Adapter options/DateTime to string adapter")] + [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] + public class DateTimeToStringAdapterOptions : AdapterOptions + { + /// + /// Format passed in to the DateTime.ToString method. + /// See this page for details on usage: https://msdn.microsoft.com/en-us/library/zdtaw1bw(v=vs.110).aspx + /// + public string Format; + } } \ No newline at end of file diff --git a/Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs.meta b/Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs.meta new file mode 100644 index 0000000..d961727 --- /dev/null +++ b/Runtime/Binding/Adapters/DateTimeToStringAdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b22843ef765812418e95092ce894efd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/Adapters/FloatToStringAdapterOptions.cs b/Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs similarity index 97% rename from UnityWeld/Binding/Adapters/FloatToStringAdapterOptions.cs rename to Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs index d180f37..512d78a 100644 --- a/UnityWeld/Binding/Adapters/FloatToStringAdapterOptions.cs +++ b/Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs @@ -1,19 +1,19 @@ -using UnityEngine; - -namespace UnityWeld.Binding.Adapters -{ - /// - /// Options for the float to string adapter. - /// - [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Float to string adapter")] - [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] - public class FloatToStringAdapterOptions : AdapterOptions - { - /// - /// Options passed in to the Single.ToString() method. - /// Defaults to two decimal places. - /// See this page for more details: https://msdn.microsoft.com/en-us/library/f71z6k0c(v=vs.110).aspx - /// - public string Format = "0.00"; - } +using UnityEngine; + +namespace UnityWeld.Binding.Adapters +{ + /// + /// Options for the float to string adapter. + /// + [CreateAssetMenu(menuName = "Unity Weld/Adapter options/Float to string adapter")] + [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] + public class FloatToStringAdapterOptions : AdapterOptions + { + /// + /// Options passed in to the Single.ToString() method. + /// Defaults to two decimal places. + /// See this page for more details: https://msdn.microsoft.com/en-us/library/f71z6k0c(v=vs.110).aspx + /// + public string Format = "0.00"; + } } \ No newline at end of file diff --git a/Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs.meta b/Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs.meta new file mode 100644 index 0000000..2989926 --- /dev/null +++ b/Runtime/Binding/Adapters/FloatToStringAdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05fbf0bb044d2cb48ab5fb79aa4dc01c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs b/Runtime/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs similarity index 100% rename from UnityWeld/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs rename to Runtime/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs diff --git a/Runtime/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs.meta b/Runtime/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs.meta new file mode 100644 index 0000000..2a67ac8 --- /dev/null +++ b/Runtime/Binding/Adapters/StringCultureToDateTimeAdapterOptions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0237eab34a897cd44a3e5a7ee23180e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/AnimatorParameterBinding.cs b/Runtime/Binding/AnimatorParameterBinding.cs similarity index 95% rename from UnityWeld/Binding/AnimatorParameterBinding.cs rename to Runtime/Binding/AnimatorParameterBinding.cs index 68842cd..039d7a0 100644 --- a/UnityWeld/Binding/AnimatorParameterBinding.cs +++ b/Runtime/Binding/AnimatorParameterBinding.cs @@ -1,6 +1,7 @@ using System; using UnityEngine; using UnityEngine.Assertions; +using UnityEngine.Serialization; using UnityWeld.Binding.Internal; namespace UnityWeld.Binding @@ -18,14 +19,14 @@ public class AnimatorParameterBinding : AbstractMemberBinding /// /// Type of the adapter we're using to adapt between the view model property and UI property. /// - public string ViewAdapterTypeName + public string ViewAdapterId { - get { return viewAdapterTypeName; } - set { viewAdapterTypeName = value; } + get { return viewAdapterId; } + set { viewAdapterId = value; } } - [SerializeField] - private string viewAdapterTypeName; + [FormerlySerializedAs("viewAdapterTypeName")] [SerializeField] + private string viewAdapterId; /// /// Options for adapting from the view model to the UI property. @@ -189,7 +190,7 @@ public override void Connect() new PropertyEndPoint( this, propertyName, - CreateAdapter(viewAdapterTypeName), + TypeResolver.GetAdapter(viewAdapterId), viewAdapterOptions, "Animator", this diff --git a/Runtime/Binding/AnimatorParameterBinding.cs.meta b/Runtime/Binding/AnimatorParameterBinding.cs.meta new file mode 100644 index 0000000..9ba4f8c --- /dev/null +++ b/Runtime/Binding/AnimatorParameterBinding.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45fb8cae4c29b6841b9b92bae576adda +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/AnimatorParameterTrigger.cs b/Runtime/Binding/AnimatorParameterTrigger.cs similarity index 100% rename from UnityWeld/Binding/AnimatorParameterTrigger.cs rename to Runtime/Binding/AnimatorParameterTrigger.cs diff --git a/Runtime/Binding/AnimatorParameterTrigger.cs.meta b/Runtime/Binding/AnimatorParameterTrigger.cs.meta new file mode 100644 index 0000000..2d5fa58 --- /dev/null +++ b/Runtime/Binding/AnimatorParameterTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0912b8bc775e4d9468a9bd10b34ac65b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/BindingAttribute.cs b/Runtime/Binding/BindingAttribute.cs similarity index 97% rename from UnityWeld/Binding/BindingAttribute.cs rename to Runtime/Binding/BindingAttribute.cs index f95e8af..7f47b29 100644 --- a/UnityWeld/Binding/BindingAttribute.cs +++ b/Runtime/Binding/BindingAttribute.cs @@ -1,14 +1,14 @@ -using System; - -namespace UnityWeld.Binding -{ - /// - /// Mark a class, interface, method or property as bindable. Bindable methods and properties must - /// reside within classes or interfaces that have also been marked as bindable. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Interface, - Inherited = false)] - public class BindingAttribute : Attribute - { - } -} +using System; + +namespace UnityWeld.Binding +{ + /// + /// Mark a class, interface, method or property as bindable. Bindable methods and properties must + /// reside within classes or interfaces that have also been marked as bindable. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Interface, + Inherited = false)] + public class BindingAttribute : Attribute + { + } +} diff --git a/Runtime/Binding/BindingAttribute.cs.meta b/Runtime/Binding/BindingAttribute.cs.meta new file mode 100644 index 0000000..2fc9b7c --- /dev/null +++ b/Runtime/Binding/BindingAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 13db563d3ec5acc4f87ae93cadfd5203 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Binding/BindingHelper.cs b/Runtime/Binding/BindingHelper.cs new file mode 100644 index 0000000..9078764 --- /dev/null +++ b/Runtime/Binding/BindingHelper.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace UnityWeld.Binding +{ + public interface IComponentsCache : IDisposable{} + + public interface IComponentsCache : IComponentsCache + { + IList Components { get; } + } + + public static class BindingHelper + { + private class ComponentsCache : IComponentsCache + { + public IList Components { get; } + public Type Type { get; } + + public ComponentsCache(IList cache) + { + Components = cache; + Type = typeof(T); + } + + public void Dispose() + { + Components.Clear(); + InternalCache[Type].Enqueue((IList)Components); + } + } + + private static readonly Dictionary> InternalCache = new Dictionary>(); + + public static IComponentsCache GetComponentsWithCache(this GameObject gameObject, bool withChildren = true) + where T : Component + { + if(gameObject == null) + { + throw new ArgumentNullException(nameof(gameObject)); + } + + var type = typeof(T); + + List cache; + if(!InternalCache.TryGetValue(type, out var cacheQueue)) + { + InternalCache.Add(type, cacheQueue = new Queue()); + } + + if(cacheQueue.Count == 0) + { + cache = new List(100); + } + else + { + cache = (List)cacheQueue.Dequeue(); + cache.Clear(); + } + + if(withChildren) + { + gameObject.GetComponentsInChildren(true, cache); + } + else + { + gameObject.GetComponents(cache); + } + + return new ComponentsCache(cache); + } + } +} \ No newline at end of file diff --git a/Runtime/Binding/BindingHelper.cs.meta b/Runtime/Binding/BindingHelper.cs.meta new file mode 100644 index 0000000..58d5ab0 --- /dev/null +++ b/Runtime/Binding/BindingHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee47ae8bc4c72af478d3b7dfb4fe92c7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/BoundObservableList.cs b/Runtime/Binding/BoundObservableList.cs similarity index 90% rename from UnityWeld/Binding/BoundObservableList.cs rename to Runtime/Binding/BoundObservableList.cs index 38a3c2c..363c62c 100644 --- a/UnityWeld/Binding/BoundObservableList.cs +++ b/Runtime/Binding/BoundObservableList.cs @@ -1,230 +1,232 @@ -using System; -using System.Collections.Generic; -using System.Linq; - - -namespace UnityWeld.Binding -{ - /// - /// An observable list that is bound to source list. - /// - public class BoundObservableList : ObservableList, IDisposable - { - /// - /// The source list. - /// - private readonly ObservableList source; - - /// - /// Function that maps source items to dest items. - /// - private readonly Func itemMap; - - /// - /// Callback when new items are added. - /// - private readonly Action added; - - /// - /// Callback when items are removed. - /// - private readonly Action removed; - - /// - /// Callback invoked when the collection has changed. - /// - private readonly Action changed; - - /// - /// Cache that mimics the contents of the bound list. - /// This is so we know the items that were cleared when the list is reset. - /// - private readonly List cache; - - private bool disposed; - - public BoundObservableList(ObservableList source, Func itemMap) : - base(source.Select(itemMap)) - { - this.itemMap = itemMap; - this.source = source; - - source.CollectionChanged += source_CollectionChanged; - CollectionChanged += BoundObservableList_CollectionChanged; - - cache = new List(this); - } - - public BoundObservableList(ObservableList source, Func itemMap, Action added, Action removed) : - base(source.Select(itemMap)) - { - if (added == null) - { - throw new ArgumentNullException("added", "added must not be null."); - } - if (removed == null) - { - throw new ArgumentNullException("removed", "removed must not be null."); - } - - this.itemMap = itemMap; - this.source = source; - this.added = added; - this.removed = removed; - - foreach (var item in this) - { - added(item); - } - - source.CollectionChanged += source_CollectionChanged; - CollectionChanged += BoundObservableList_CollectionChanged; - cache = new List(this); - } - - public BoundObservableList(ObservableList source, Func itemMap, Action added, Action removed, Action changed) : - base(source.Select(itemMap)) - { - if (added == null) - { - throw new ArgumentNullException("added", "added must not be null."); - } - if (removed == null) - { - throw new ArgumentNullException("removed", "removed must not be null."); - } - - this.itemMap = itemMap; - this.source = source; - this.added = added; - this.removed = removed; - this.changed = changed; - - foreach (var item in this) - { - added(item); - } - - source.CollectionChanged += source_CollectionChanged; - CollectionChanged += BoundObservableList_CollectionChanged; - cache = new List(this); - } - - /// - /// Event raised when the source collection has changed. - /// - private void source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - var insertAt = e.NewStartingIndex; - - foreach (var item in e.NewItems) - { - var generatedItem = itemMap((SourceT)item); - - Insert(insertAt, generatedItem); - ++insertAt; - } - - break; - case NotifyCollectionChangedAction.Remove: - var removeAt = e.OldStartingIndex; - - for (var i = 0; i < e.OldItems.Count; i++) - { - RemoveAt(removeAt); - } - - break; - case NotifyCollectionChangedAction.Reset: - Clear(); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Event raised when items are added to the bound list. - /// - private void BoundObservableList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - var insertIndex = e.NewStartingIndex; - - foreach (var item in e.NewItems) - { - var typedItem = (DestT)item; - - if (added != null) - { - added(typedItem); - } - - cache.Insert(insertIndex, typedItem); // Keep the cache updated as new items come in. - ++insertIndex; - } - break; - - case NotifyCollectionChangedAction.Remove: - foreach (var item in e.OldItems) - { - var typedItem = (DestT)item; - - if (removed != null) - { - removed(typedItem); - } - - cache.RemoveAt(e.OldStartingIndex); // Keep the cache updated as items are removed. - } - break; - - case NotifyCollectionChangedAction.Reset: - if (removed != null) - { - foreach (var item in cache) - { - removed(item); - } - } - cache.Clear(); - break; - - default: - throw new ArgumentOutOfRangeException(); - } - - if (changed != null) - { - changed.Invoke(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposed) - { - return; - } - - if (disposing) - { - source.CollectionChanged -= source_CollectionChanged; - CollectionChanged -= BoundObservableList_CollectionChanged; - } - - disposed = true; - } - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + + +namespace UnityWeld.Binding +{ + /// + /// An observable list that is bound to source list. + /// + public class BoundObservableList : ObservableCollection, IDisposable + { + /// + /// The source list. + /// + private readonly ObservableCollection source; + + /// + /// Function that maps source items to dest items. + /// + private readonly Func itemMap; + + /// + /// Callback when new items are added. + /// + private readonly Action added; + + /// + /// Callback when items are removed. + /// + private readonly Action removed; + + /// + /// Callback invoked when the collection has changed. + /// + private readonly Action changed; + + /// + /// Cache that mimics the contents of the bound list. + /// This is so we know the items that were cleared when the list is reset. + /// + private readonly List cache; + + private bool disposed; + + public BoundObservableList(ObservableCollection source, Func itemMap) : + base(source.Select(itemMap)) + { + this.itemMap = itemMap; + this.source = source; + + source.CollectionChanged += source_CollectionChanged; + CollectionChanged += BoundObservableList_CollectionChanged; + + cache = new List(this); + } + + public BoundObservableList(ObservableCollection source, Func itemMap, Action added, Action removed) : + base(source.Select(itemMap)) + { + if (added == null) + { + throw new ArgumentNullException("added", "added must not be null."); + } + if (removed == null) + { + throw new ArgumentNullException("removed", "removed must not be null."); + } + + this.itemMap = itemMap; + this.source = source; + this.added = added; + this.removed = removed; + + foreach (var item in this) + { + added(item); + } + + source.CollectionChanged += source_CollectionChanged; + CollectionChanged += BoundObservableList_CollectionChanged; + cache = new List(this); + } + + public BoundObservableList(ObservableCollection source, Func itemMap, Action added, Action removed, Action changed) : + base(source.Select(itemMap)) + { + if (added == null) + { + throw new ArgumentNullException("added", "added must not be null."); + } + if (removed == null) + { + throw new ArgumentNullException("removed", "removed must not be null."); + } + + this.itemMap = itemMap; + this.source = source; + this.added = added; + this.removed = removed; + this.changed = changed; + + foreach (var item in this) + { + added(item); + } + + source.CollectionChanged += source_CollectionChanged; + CollectionChanged += BoundObservableList_CollectionChanged; + cache = new List(this); + } + + /// + /// Event raised when the source collection has changed. + /// + private void source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + var insertAt = e.NewStartingIndex; + + foreach (var item in e.NewItems) + { + var generatedItem = itemMap((SourceT)item); + + Insert(insertAt, generatedItem); + ++insertAt; + } + + break; + case NotifyCollectionChangedAction.Remove: + var removeAt = e.OldStartingIndex; + + for (var i = 0; i < e.OldItems.Count; i++) + { + RemoveAt(removeAt); + } + + break; + case NotifyCollectionChangedAction.Reset: + Clear(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Event raised when items are added to the bound list. + /// + private void BoundObservableList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + var insertIndex = e.NewStartingIndex; + + foreach (var item in e.NewItems) + { + var typedItem = (DestT)item; + + if (added != null) + { + added(typedItem); + } + + cache.Insert(insertIndex, typedItem); // Keep the cache updated as new items come in. + ++insertIndex; + } + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var item in e.OldItems) + { + var typedItem = (DestT)item; + + if (removed != null) + { + removed(typedItem); + } + + cache.RemoveAt(e.OldStartingIndex); // Keep the cache updated as items are removed. + } + break; + + case NotifyCollectionChangedAction.Reset: + if (removed != null) + { + foreach (var item in cache) + { + removed(item); + } + } + cache.Clear(); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + if (changed != null) + { + changed.Invoke(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + source.CollectionChanged -= source_CollectionChanged; + CollectionChanged -= BoundObservableList_CollectionChanged; + } + + disposed = true; + } + } +} diff --git a/Runtime/Binding/BoundObservableList.cs.meta b/Runtime/Binding/BoundObservableList.cs.meta new file mode 100644 index 0000000..aa2ef3f --- /dev/null +++ b/Runtime/Binding/BoundObservableList.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9422c5ec2af8a964f912e9ba4ccff225 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityWeld/Binding/CollectionBinding.cs b/Runtime/Binding/CollectionBinding.cs similarity index 56% rename from UnityWeld/Binding/CollectionBinding.cs rename to Runtime/Binding/CollectionBinding.cs index 86c25f1..0ea98a0 100644 --- a/UnityWeld/Binding/CollectionBinding.cs +++ b/Runtime/Binding/CollectionBinding.cs @@ -1,205 +1,266 @@ -using System; -using System.Collections; -using System.Linq; -using UnityEngine; -using UnityWeld.Binding.Exceptions; -using UnityWeld.Binding.Internal; - -namespace UnityWeld.Binding -{ - /// - /// Binds a property in the view-model that is a collection and instantiates copies - /// of template objects to bind to the items of the collection. - /// - /// Creates and destroys child objects when items are added and removed from a - /// collection that implements INotifyCollectionChanged, like ObservableList. - /// - [AddComponentMenu("Unity Weld/Collection Binding")] - [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] - public class CollectionBinding : AbstractTemplateSelector - { - /// - /// Collection that we have bound to. - /// - private IEnumerable viewModelCollectionValue; - - public override void Connect() - { - Disconnect(); - - string propertyName; - object newViewModel; - ParseViewModelEndPointReference( - ViewModelPropertyName, - out propertyName, - out newViewModel - ); - - viewModel = newViewModel; - - viewModelPropertyWatcher = new PropertyWatcher( - newViewModel, - propertyName, - NotifyPropertyChanged_PropertyChanged - ); - - BindCollection(); - } - - public override void Disconnect() - { - UnbindCollection(); - - if (viewModelPropertyWatcher != null) - { - viewModelPropertyWatcher.Dispose(); - viewModelPropertyWatcher = null; - } - - viewModel = null; - } - - private void NotifyPropertyChanged_PropertyChanged() - { - RebindCollection(); - } - - private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - // Add items that were added to the bound collection. - if (e.NewItems != null) - { - var list = viewModelCollectionValue as IList; - - foreach (var item in e.NewItems) - { - int index; - if (list == null) - { - // Default to adding the new object at the last index. - index = transform.childCount; - } - else - { - index = list.IndexOf(item); - } - InstantiateTemplate(item, index); - } - } - - break; - case NotifyCollectionChangedAction.Remove: - // TODO: respect item order - // Remove items that have been deleted. - if (e.OldItems != null) - { - foreach (var item in e.OldItems) - { - DestroyTemplate(item); - } - } - - break; - case NotifyCollectionChangedAction.Reset: - DestroyAllTemplates(); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - /// - /// Bind to the view model collection so we can monitor it for changes. - /// - private void BindCollection() - { - // Bind view model. - var viewModelType = viewModel.GetType(); - - string propertyName; - string viewModelName; - ParseEndPointReference( - ViewModelPropertyName, - out propertyName, - out viewModelName - ); - - var viewModelCollectionProperty = viewModelType.GetProperty(propertyName); - if (viewModelCollectionProperty == null) - { - throw new MemberNotFoundException( - "Expected property " - + ViewModelPropertyName + ", but it wasn't found on type " - + viewModelType + "." - ); - } - - // Get value from view model. - var viewModelValue = viewModelCollectionProperty.GetValue(viewModel, null); - if (viewModelValue == null) - { - throw new PropertyNullException( - "Cannot bind to null property in view: " - + ViewModelPropertyName - ); - } - - viewModelCollectionValue = viewModelValue as IEnumerable; - if (viewModelCollectionValue == null) - { - throw new InvalidTypeException( - "Property " - + ViewModelPropertyName - + " is not a collection and cannot be used to bind collections." - ); - } - - // Generate children - var collectionAsList = viewModelCollectionValue.Cast().ToList(); - for (var index = 0; index < collectionAsList.Count; index++) - { - InstantiateTemplate(collectionAsList[index], index); - } - - // Subscribe to collection changed events. - var collectionChanged = viewModelCollectionValue as INotifyCollectionChanged; - if (collectionChanged != null) - { - collectionChanged.CollectionChanged += Collection_CollectionChanged; - } - } - - /// - /// Unbind from the collection, stop monitoring it for changes. - /// - private void UnbindCollection() - { - DestroyAllTemplates(); - - // Unsubscribe from collection changed events. - if (viewModelCollectionValue != null) - { - var collectionChanged = viewModelCollectionValue as INotifyCollectionChanged; - if (collectionChanged != null) - { - collectionChanged.CollectionChanged -= Collection_CollectionChanged; - } - - viewModelCollectionValue = null; - } - } - - /// - /// Rebind to the collection when it has changed on the view-model. - /// - private void RebindCollection() - { - UnbindCollection(); - BindCollection(); - } - - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using UnityEngine; +using UnityWeld.Binding.Exceptions; +using UnityWeld.Binding.Internal; + +namespace UnityWeld.Binding +{ + /// + /// Binds a property in the view-model that is a collection and instantiates copies + /// of template objects to bind to the items of the collection. + /// + /// Creates and destroys child objects when items are added and removed from a + /// collection that implements INotifyCollectionChanged, like ObservableList. + /// + [AddComponentMenu("Unity Weld/Collection Binding")] + [HelpURL("https://github.com/Real-Serious-Games/Unity-Weld")] + public class CollectionBinding : AbstractTemplateSelector + { + private readonly IDictionary> _pool = new Dictionary>(); + + /// + /// Collection that we have bound to. + /// + private IEnumerable _viewModelCollectionValue; + + [SerializeField] + private Transform _itemsContainer; + [SerializeField] + private int _templateInitialPoolCount = 0; + + public Transform ItemsContainer + { + get => _itemsContainer; + set => _itemsContainer = value; + } + + public int TemplateInitialPoolCount + { + get => _templateInitialPoolCount; + set => _templateInitialPoolCount = value; + } + + protected Transform Container => _itemsContainer ? _itemsContainer : transform; + + public override void Connect() + { + Disconnect(); + + ParseViewModelEndPointReference( + ViewModelPropertyName, + out var propertyName, + out var newViewModel + ); + + ViewModel = newViewModel; + + ViewModelPropertyWatcher = new PropertyWatcher( + newViewModel, + propertyName, + NotifyPropertyChanged_PropertyChanged + ); + + BindCollection(); + } + + public override void Disconnect() + { + UnbindCollection(); + + if (ViewModelPropertyWatcher != null) + { + ViewModelPropertyWatcher.Dispose(); + ViewModelPropertyWatcher = null; + } + + ViewModel = null; + } + + private void NotifyPropertyChanged_PropertyChanged() + { + RebindCollection(); + } + + private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + // Add items that were added to the bound collection. + if (e.NewItems != null) + { + var list = _viewModelCollectionValue as IList; + + foreach (var item in e.NewItems) + { + int index; + if (list == null) + { + // Default to adding the new object at the last index. + index = transform.childCount; + } + else + { + index = list.IndexOf(item); + } + InstantiateTemplate(item, index); + } + } + + break; + case NotifyCollectionChangedAction.Remove: + // TODO: respect item order + // Remove items that have been deleted. + if (e.OldItems != null) + { + foreach (var item in e.OldItems) + { + DestroyTemplate(item); + } + } + + break; + case NotifyCollectionChangedAction.Reset: + DestroyAllTemplates(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Bind to the view model collection so we can monitor it for changes. + /// + private void BindCollection() + { + // Bind view model. + var viewModelType = ViewModel.GetType(); + + ParseEndPointReference(ViewModelPropertyName, out var propertyName, out _); + + var viewModelCollectionProperty = viewModelType.GetProperty(propertyName); + if (viewModelCollectionProperty == null) + { + throw new MemberNotFoundException( + $"Expected property {ViewModelPropertyName}, but it wasn't found on type {viewModelType}."); + } + + // Get value from view model. + var viewModelValue = viewModelCollectionProperty.GetValue(ViewModel, null); + if (viewModelValue == null) + { + return; + } + + _viewModelCollectionValue = viewModelValue as IEnumerable; + if (_viewModelCollectionValue == null) + { + throw new InvalidTypeException( + $"Property {ViewModelPropertyName} is not a collection and cannot be used to bind collections."); + } + + // Generate children + var collectionAsList = _viewModelCollectionValue.Cast().ToList(); + for (var index = 0; index < collectionAsList.Count; index++) + { + InstantiateTemplate(collectionAsList[index], index); + } + + // Subscribe to collection changed events. + if (_viewModelCollectionValue is INotifyCollectionChanged collectionChanged) + { + collectionChanged.CollectionChanged += Collection_CollectionChanged; + } + } + + /// + /// Unbind from the collection, stop monitoring it for changes. + /// + private void UnbindCollection() + { + DestroyAllTemplates(); + + // Unsubscribe from collection changed events. + if (_viewModelCollectionValue != null) + { + var collectionChanged = _viewModelCollectionValue as INotifyCollectionChanged; + if (collectionChanged != null) + { + collectionChanged.CollectionChanged -= Collection_CollectionChanged; + } + + _viewModelCollectionValue = null; + } + } + + /// + /// Rebind to the collection when it has changed on the view-model. + /// + private void RebindCollection() + { + UnbindCollection(); + BindCollection(); + } + + protected override Template CloneTemplate(Template template) + { + if(_pool.TryGetValue(template.ViewModelTypeName, out var pool) && pool.Count > 0) + { + return pool.Dequeue(); + } + + return NewTemplate(template); + } + + protected override void OnTemplateDestroy(Template template) + { + base.OnTemplateDestroy(template); + + PutTemplateToPool(template); + } + + private void PutTemplateToPool(Template template) + { + if(!_pool.TryGetValue(template.ViewModelTypeName, out var pool)) + { + _pool.Add(template.ViewModelTypeName, pool = new Queue