diff options
author | Bertrand Le Roy <beleroy@microsoft.com> | 2017-11-21 03:45:16 +0300 |
---|---|---|
committer | Bertrand Le Roy <beleroy@microsoft.com> | 2017-11-21 03:45:16 +0300 |
commit | 7c565d5af6303017437a521d11c90ac1583016c2 (patch) | |
tree | 5a8eec3495a7f09a6e65988b14014d8e90b02bcc | |
parent | d271fb3b70d8cdc8ee1deb658af68833071289bd (diff) | |
parent | c5590c312d9aef1dd6a8b3987f5849de8da41edd (diff) |
Merge remote-tracking branch 'origin/master' into bleroy-BrushEditor
# Conflicts:
# Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs
# Xamarin.PropertyEditing.Windows/EditorPropertySelector.cs
# Xamarin.PropertyEditing.Windows/Themes/Resources.xaml
# Xamarin.PropertyEditing.Windows/Xamarin.PropertyEditing.Windows.csproj
# Xamarin.PropertyEditing/Properties/Resources.Designer.cs
# Xamarin.PropertyEditing/Properties/Resources.resx
# Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs
49 files changed, 1713 insertions, 453 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6134c88 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,95 @@ +# EditorConfig is awesome:http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Baseline +[*] +charset = utf-8 +indent_style = tab +trim_trailing_whitespace = true +max_line_length = 120 + +# MSBuild +[*.{csproj,proj,projitems,shproj,fsproj,target,props}] +indent_style = space +indent_size = 2 + +# XML config files +[*.{config,nuspec,resx}] +indent_style = space +indent_size = 2 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 + +# XAML files +[*.xaml] +indent_style = tab +indent_size = 4 + +# Dotnet code style settings: +[*.{cs,vb}] + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# CSharp code style settings: +[*.cs] + +# spaces before parens +csharp_space_between_method_declaration_name_and_open_parenthesis = true +csharp_space_between_method_call_name_and_opening_parenthesis = true +csharp_space_after_keywords_in_control_flow_statements = true + +# Newline settings +csharp_new_line_before_open_brace = types,methods,properties,events,indexers +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Switch indentation +csharp_indent_switch_labels = false + +# Prefer "var" everywhere it's apparent +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = false:suggestion +csharp_style_conditional_delegate_call = true:suggestion diff --git a/Xamarin.PropertyEditing.Mac.Standalone/Xamarin.PropertyEditing.Mac.Standalone.csproj b/Xamarin.PropertyEditing.Mac.Standalone/Xamarin.PropertyEditing.Mac.Standalone.csproj index 620721d..f835a47 100644 --- a/Xamarin.PropertyEditing.Mac.Standalone/Xamarin.PropertyEditing.Mac.Standalone.csproj +++ b/Xamarin.PropertyEditing.Mac.Standalone/Xamarin.PropertyEditing.Mac.Standalone.csproj @@ -26,7 +26,7 @@ <IncludeMonoRuntime>false</IncludeMonoRuntime>
<UseSGen>true</UseSGen>
<UseRefCounting>true</UseRefCounting>
- <Profiling>true</Profiling>
+ <Profiling>false</Profiling>
<HttpClientHandler></HttpClientHandler>
<LinkMode></LinkMode>
<XamMacArch></XamMacArch>
diff --git a/Xamarin.PropertyEditing.Mac/PropertyEditorPanel.cs b/Xamarin.PropertyEditing.Mac/PropertyEditorPanel.cs index 7e6d331..c963108 100644 --- a/Xamarin.PropertyEditing.Mac/PropertyEditorPanel.cs +++ b/Xamarin.PropertyEditing.Mac/PropertyEditorPanel.cs @@ -67,7 +67,7 @@ namespace Xamarin.PropertyEditing.Mac // Populate the Property Table editorProvider = value; - viewModel = new PanelViewModel (editorProvider); + viewModel = new PanelViewModel (editorProvider, TargetPlatform.Default); dataSource = new PropertyTableDataSource (viewModel); propertyTable.Delegate = new PropertyTableDelegate (dataSource); propertyTable.DataSource = dataSource; diff --git a/Xamarin.PropertyEditing.Mac/PropertyTableDataSource.cs b/Xamarin.PropertyEditing.Mac/PropertyTableDataSource.cs index f46220b..5153562 100644 --- a/Xamarin.PropertyEditing.Mac/PropertyTableDataSource.cs +++ b/Xamarin.PropertyEditing.Mac/PropertyTableDataSource.cs @@ -22,28 +22,29 @@ namespace Xamarin.PropertyEditing.Mac public override nint GetChildrenCount (NSOutlineView outlineView, NSObject item) { - if (this.vm.ArrangedProperties.Count == 0) + if (this.vm.ArrangedEditors.Count == 0) return 0; if (this.vm.ArrangeMode == PropertyArrangeMode.Name) - return this.vm.ArrangedProperties[0].Count; + return this.vm.ArrangedEditors[0].Count; if (item == null) - return this.vm.ArrangedProperties.Count; - else - return ((IGroupingList<string, PropertyViewModel>)((NSObjectFacade)item).Target).Count; + return this.vm.ArrangedEditors.Count; + else { + return ((IGroupingList<string, EditorViewModel>)((NSObjectFacade)item).Target).Count; + } } public override NSObject GetChild (NSOutlineView outlineView, nint childIndex, NSObject item) { object element; if (this.vm.ArrangeMode == PropertyArrangeMode.Name) { - element = (this.vm.ArrangedProperties[0][(int)childIndex]); + element = (this.vm.ArrangedEditors[0][(int)childIndex]); } else { if (item == null) - element = this.vm.ArrangedProperties[(int)childIndex]; + element = this.vm.ArrangedEditors[(int)childIndex]; else { - element = ((IGroupingList<string, PropertyViewModel>)((NSObjectFacade)item).Target)[(int)childIndex]; + element = ((IGroupingList<string, EditorViewModel>)((NSObjectFacade)item).Target)[(int)childIndex]; } } @@ -55,7 +56,7 @@ namespace Xamarin.PropertyEditing.Mac if (this.vm.ArrangeMode == PropertyArrangeMode.Name) return false; - return ((NSObjectFacade)item).Target is IGroupingList<string, PropertyViewModel>; + return ((NSObjectFacade)item).Target is IGroupingList<string, EditorViewModel>; } public NSObject GetFacade (object element) diff --git a/Xamarin.PropertyEditing.Mac/PropertyTableDelegate.cs b/Xamarin.PropertyEditing.Mac/PropertyTableDelegate.cs index 063a8c3..357209e 100644 --- a/Xamarin.PropertyEditing.Mac/PropertyTableDelegate.cs +++ b/Xamarin.PropertyEditing.Mac/PropertyTableDelegate.cs @@ -24,7 +24,7 @@ namespace Xamarin.PropertyEditing.Mac if (!String.IsNullOrWhiteSpace (this.dataSource.DataContext.FilterText)) { outlineView.ExpandItem (null, true); } else { - foreach (IGrouping<string, PropertyViewModel> g in this.dataSource.DataContext.ArrangedProperties) { + foreach (IGrouping<string, EditorViewModel> g in this.dataSource.DataContext.ArrangedEditors) { NSObject item; if (!this.dataSource.TryGetFacade (g, out item)) continue; @@ -43,7 +43,7 @@ namespace Xamarin.PropertyEditing.Mac { var facade = (NSObjectFacade)item; var vm = facade.Target as PropertyViewModel; - var group = facade.Target as IGroupingList<string, PropertyViewModel>; + var group = facade.Target as IGroupingList<string, EditorViewModel>; string cellIdentifier = (group == null) ? vm.GetType ().Name : group.Key; // Setup view based on the column @@ -78,7 +78,7 @@ namespace Xamarin.PropertyEditing.Mac throw new Exception ("Unknown column identifier: " + tableColumn.Identifier); } - PropertyEditorControl GetEditor (PropertyViewModel vm, NSOutlineView outlineView) + PropertyEditorControl GetEditor (EditorViewModel vm, NSOutlineView outlineView) { Type[] genericArgs = null; Type controlType; @@ -103,7 +103,7 @@ namespace Xamarin.PropertyEditing.Mac public override bool ShouldSelectItem (NSOutlineView outlineView, NSObject item) { - return (!(item is NSObjectFacade) || !(((NSObjectFacade)item).Target is IGroupingList<string, PropertyViewModel>)); + return (!(item is NSObjectFacade) || !(((NSObjectFacade)item).Target is IGroupingList<string, EditorViewModel>)); } public override void ItemDidExpand (NSNotification notification) @@ -112,7 +112,7 @@ namespace Xamarin.PropertyEditing.Mac return; NSObjectFacade facade = notification.UserInfo.Values[0] as NSObjectFacade; - var group = facade.Target as IGroupingList<string, PropertyViewModel>; + var group = facade.Target as IGroupingList<string, EditorViewModel>; if (group != null) this.dataSource.DataContext.SetIsExpanded (group.Key, isExpanded: true); } @@ -123,7 +123,7 @@ namespace Xamarin.PropertyEditing.Mac return; NSObjectFacade facade = notification.UserInfo.Values[0] as NSObjectFacade; - var group = facade.Target as IGroupingList<string, PropertyViewModel>; + var group = facade.Target as IGroupingList<string, EditorViewModel>; if (group != null) this.dataSource.DataContext.SetIsExpanded (group.Key, isExpanded: false); } @@ -131,12 +131,12 @@ namespace Xamarin.PropertyEditing.Mac public override nfloat GetRowHeight (NSOutlineView outlineView, NSObject item) { var facade = (NSObjectFacade)item; - var group = facade.Target as IGroupingList<string, PropertyViewModel>; + var group = facade.Target as IGroupingList<string, EditorViewModel>; if (group != null) { return 30; } - var vm = (PropertyViewModel)facade.Target; + var vm = (EditorViewModel)facade.Target; var editor = (PropertyEditorControl)outlineView.MakeView (vm.GetType ().Name + "edits", this); if (editor == null) { editor = GetEditor (vm, outlineView); @@ -148,7 +148,7 @@ namespace Xamarin.PropertyEditing.Mac private bool isExpanding; // set up the editor based on the type of view model - private PropertyEditorControl SetUpEditor (Type controlType, PropertyViewModel property, NSOutlineView outline) + private PropertyEditorControl SetUpEditor (Type controlType, EditorViewModel property, NSOutlineView outline) { var view = (PropertyEditorControl)Activator.CreateInstance (controlType); view.Identifier = property.GetType ().Name; diff --git a/Xamarin.PropertyEditing.Tests/IGetAndSet.cs b/Xamarin.PropertyEditing.Tests/IGetAndSet.cs deleted file mode 100644 index e334eb9..0000000 --- a/Xamarin.PropertyEditing.Tests/IGetAndSet.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Xamarin.PropertyEditing.Tests -{ - interface IGetAndSet - { - TValue GetValue<TValue> (object target); - void SetValue<TValue> (object target, TValue value); - } -} diff --git a/Xamarin.PropertyEditing.Tests/IPropertyConverter.cs b/Xamarin.PropertyEditing.Tests/IPropertyConverter.cs new file mode 100644 index 0000000..d83fcc7 --- /dev/null +++ b/Xamarin.PropertyEditing.Tests/IPropertyConverter.cs @@ -0,0 +1,9 @@ +using System; + +namespace Xamarin.PropertyEditing.Tests +{ + interface IPropertyConverter + { + bool TryConvert<TFrom> (TFrom fromValue, Type toType, out object toValue); + } +} diff --git a/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs b/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs index 27a587e..875a1f8 100644 --- a/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs +++ b/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using Cadenza.Collections; using Xamarin.PropertyEditing.Tests.MockPropertyInfo; @@ -8,19 +7,11 @@ namespace Xamarin.PropertyEditing.Tests.MockControls { public class MockControl { - private OrderedDictionary<string, IPropertyInfo> PropertyInfos { get; } - = new OrderedDictionary<string, IPropertyInfo> { }; - private OrderedDictionary<string, IEventInfo> EventInfos { get; } - = new OrderedDictionary<string, IEventInfo> { }; - internal IDictionary<IPropertyInfo, object> Values { get; } - = new Dictionary<IPropertyInfo, object> { }; - internal IDictionary<IEventInfo, string> EventHandlers { get; } - = new Dictionary<IEventInfo, string> { }; + public IReadOnlyDictionary<string, IPropertyInfo> Properties => this.properties; - public ICollection<IPropertyInfo> Properties => PropertyInfos.Values; - public ICollection<IEventInfo> Events => EventInfos.Values; + public IReadOnlyDictionary<string, IEventInfo> Events => this.events; - public IPropertyInfo AddProperty<T> (string name, string category = "", + public void AddProperty<T> (string name, string category = "", bool canWrite = true, bool flag = false, IEnumerable<Type> converterTypes = null) { @@ -30,33 +21,27 @@ namespace Xamarin.PropertyEditing.Tests.MockControls var enumPropertyInfoType = typeof (MockEnumPropertyInfo<,>) .MakeGenericType (underlyingType, typeof (T)); propertyInfo = (IPropertyInfo)Activator.CreateInstance (enumPropertyInfoType, name, category, canWrite, flag, converterTypes); - } - else { + } else { propertyInfo = new MockPropertyInfo<T> (name, category, canWrite, converterTypes); } - return AddProperty<T>(propertyInfo); + + AddProperty<T> (propertyInfo); } - public IPropertyInfo AddProperty<T>(IPropertyInfo propertyInfo) + public void AddProperty<T> (IPropertyInfo propertyInfo) { - PropertyInfos.Add (propertyInfo.Name, propertyInfo); - Values.Add (propertyInfo, new ValueInfo<T> { - Value = default (T), - Source = ValueSource.Local - }); - return propertyInfo; + this.properties.Add (propertyInfo.Name, propertyInfo); } - public IPropertyInfo AddReadOnlyProperty<T> (string name, string category = "") + public void AddReadOnlyProperty<T> (string name, string category = null) { - return AddProperty<T> (name, category, false); + AddProperty<T> (name, category, false); } public void AddEvent (string name) { var eventInfo = new MockEventInfo (name); - EventInfos.Add (name, eventInfo); - EventHandlers.Add (eventInfo, ""); + this.events.Add (name, eventInfo); } public void AddEvents (params string[] names) @@ -66,33 +51,9 @@ namespace Xamarin.PropertyEditing.Tests.MockControls } } - public IPropertyInfo GetPropertyInfo (string name) - => PropertyInfos[name]; - - public T GetValue<T> (string name) => GetValue<T> (PropertyInfos[name]); - - public T GetValue<T> (IPropertyInfo info) - { - var infoObject = Values[info]; - var valueInfo = infoObject as ValueInfo<T>; - if (valueInfo != null) - return valueInfo.Value; - return default(T); - } - - public void SetValue<T> (string name, T value) - { - SetValue(PropertyInfos[name], value); - } - - public void SetValue<T> (IPropertyInfo info, T value) - { - Values[info] = new ValueInfo<T> { - Value = value, - Source = ValueSource.Local - }; - } - public class NotImplemented { } + + private readonly OrderedDictionary<string, IEventInfo> events = new OrderedDictionary<string, IEventInfo> (); + private readonly OrderedDictionary<string, IPropertyInfo> properties = new OrderedDictionary<string, IPropertyInfo> (); } } diff --git a/Xamarin.PropertyEditing.Tests/MockControls/MockWpfControl.cs b/Xamarin.PropertyEditing.Tests/MockControls/MockWpfControl.cs index 504fecc..e9f9680 100644 --- a/Xamarin.PropertyEditing.Tests/MockControls/MockWpfControl.cs +++ b/Xamarin.PropertyEditing.Tests/MockControls/MockWpfControl.cs @@ -21,7 +21,7 @@ namespace Xamarin.PropertyEditing.Tests.MockControls AddProperty<NotImplemented> ("BindingGroup"); AddProperty<NotImplemented> ("BitmapEffect"); AddProperty<NotImplemented> ("BitmapEffectInput"); - AddProperty<NotImplemented> ("BorderBrush", Appearance); + AddProperty<CommonBrush> ("BorderBrush", Appearance); AddProperty<CommonThickness> ("BorderThickness", Appearance); AddProperty<NotImplemented> ("CacheMode"); AddProperty<NotImplemented> ("Clip"); diff --git a/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs b/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs index 32bf1a0..e9b8de3 100644 --- a/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs +++ b/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs @@ -1,4 +1,4 @@ -using System; +using System.Collections.Generic; using System.Threading.Tasks; using Xamarin.PropertyEditing.Reflection; using Xamarin.PropertyEditing.Tests.MockControls; @@ -10,10 +10,16 @@ namespace Xamarin.PropertyEditing.Tests { public Task<IObjectEditor> GetObjectEditorAsync (object item) { - var mockControl = item as MockControl; - if (mockControl != null) - return Task.FromResult<IObjectEditor> (new MockObjectEditor (mockControl)); - return Task.FromResult<IObjectEditor> (new ReflectionObjectEditor (item)); + if (this.editorCache.TryGetValue (item, out IObjectEditor cachedEditor)) { + return Task.FromResult (cachedEditor); + } + IObjectEditor editor = (item is MockControl mockControl) + ? (IObjectEditor)(new MockObjectEditor (mockControl) { SupportsDefault = true }) + : new ReflectionObjectEditor (item); + this.editorCache.Add (item, editor); + return Task.FromResult (editor); } + + private Dictionary<object, IObjectEditor> editorCache = new Dictionary<object, IObjectEditor> (); } }
\ No newline at end of file diff --git a/Xamarin.PropertyEditing.Tests/MockObjectEditor.cs b/Xamarin.PropertyEditing.Tests/MockObjectEditor.cs index 7e0030b..714f527 100644 --- a/Xamarin.PropertyEditing.Tests/MockObjectEditor.cs +++ b/Xamarin.PropertyEditing.Tests/MockObjectEditor.cs @@ -20,13 +20,17 @@ namespace Xamarin.PropertyEditing.Tests public MockObjectEditor (MockControl control) { - Properties = control.Properties.ToArray(); - values = control.Values; - Events = control.Events.ToArray (); - events = control.EventHandlers; + Properties = control.Properties.Values.ToArray(); + Events = control.Events.Values.ToArray(); Target = control; } + public bool SupportsDefault + { + get; + set; + } + public object Target { get; @@ -112,20 +116,21 @@ namespace Xamarin.PropertyEditing.Tests value.Value = (T)ValueEvaluator (property, value.ValueDescriptor); } - var mockControl = Target as MockControl; - if (mockControl != null) { - var mockPropertyInfo = property as IGetAndSet; - if (mockPropertyInfo != null) { - mockPropertyInfo.SetValue (mockControl, value.Value); - } - else { - values[property] = value; + object softValue = value; + + if (typeof(T) != property.Type) { + IPropertyConverter converter = property as IPropertyConverter; + + object v; + if (converter != null && converter.TryConvert (value.Value, property.Type, out v)) { + var softType = typeof(ValueInfo<>).MakeGenericType (property.Type); + softValue = Activator.CreateInstance (softType); + softType.GetProperty ("Value").SetValue (softValue, v); + softType.GetProperty ("Source").SetValue (softValue, value.Source); } } - else { - values[property] = value; - } - + + this.values[property] = softValue; PropertyChanged?.Invoke (this, new EditorPropertyChangedEventArgs (property)); } @@ -134,32 +139,37 @@ namespace Xamarin.PropertyEditing.Tests if (variation != null) throw new NotSupportedException (); // TODO - var mockControl = Target as MockControl; - if (mockControl != null) { - var mockPropertyInfo = property as IGetAndSet; - if (mockPropertyInfo != null) { - return new ValueInfo<T> { - Value = mockPropertyInfo.GetValue<T> (mockControl), - Source = ValueSource.Local - }; - } - } - object value; - if (values.TryGetValue (property, out value)) { + if (this.values.TryGetValue (property, out value)) { var info = value as ValueInfo<T>; if (info != null) return info; - else if (value != null) { + else if (value == null || value is T) { return new ValueInfo<T> { Value = (T)value, Source = ValueSource.Local }; + } else { + ValueSource source = ValueSource.Local; + Type valueType = value.GetType (); + if (valueType.IsConstructedGenericType && valueType.GetGenericTypeDefinition () == typeof(ValueInfo<>)) { + source = (ValueSource)valueType.GetProperty ("Source").GetValue (value); + value = valueType.GetProperty ("Value").GetValue (value); + } + + object newValue; + IPropertyConverter converter = property as IPropertyConverter; + if (converter != null && converter.TryConvert (value, typeof(T), out newValue)) { + return new ValueInfo<T> { + Source = source, + Value = (T)newValue + }; + } } } return new ValueInfo<T> { - Source = ValueSource.Local, + Source = (SupportsDefault) ? ValueSource.Default : ValueSource.Local, Value = default(T) }; } diff --git a/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockEnumPropertyInfo.cs b/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockEnumPropertyInfo.cs index 2ee05f5..053b915 100644 --- a/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockEnumPropertyInfo.cs +++ b/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockEnumPropertyInfo.cs @@ -44,19 +44,9 @@ namespace Xamarin.PropertyEditing.Tests.MockPropertyInfo public IReadOnlyDictionary<string, TUnderlying> PredefinedValues { get; } - private TUnderlying[] Values; - private ulong[] LongValues; - - /// <summary> - /// Gets the value or list of values of the described property on the target, as an `TUnderlying` - /// or as a `IEnumerable` of `TUnderlying`. - /// </summary> - /// <typeparam name="TValue">The result type desired, should be `TUnderlying` or an `IEnumerable` of `TUnderlying`.</typeparam> - /// <param name="target">The control from which to get the value.</param> - /// <returns>The value or list of values.</returns> - public override TValue GetValue<TValue> (MockControl target) + public override bool TryConvert<TFrom> (TFrom fromValue, Type toType, out object toValue) { - var enumerationType = typeof (TValue) + var enumerationType = toType .GetInterfaces () .FirstOrDefault (i => i.IsGenericType && i.GetGenericTypeDefinition () == typeof (IEnumerable<>)); if (enumerationType != null) { @@ -65,48 +55,38 @@ namespace Xamarin.PropertyEditing.Tests.MockPropertyInfo var listOfEnumeratedType = typeof (List<>).MakeGenericType (enumeratedType); // Build the list of values through a trip to uint64 unchecked { - var realValue = Convert.ToUInt64 (target.GetValue<TEnum> (this)); + var realValue = Convert.ToUInt64 (fromValue); var values = (IList)Activator.CreateInstance (listOfEnumeratedType); for (var i = 0; i < Values.Length; i++) { if ((LongValues[i] & realValue) != 0) values.Add (Convert.ChangeType(Values[i], enumeratedType)); } - return (TValue)values; + toValue = values; + return true; } - } - else { - // Get a single value. - var value = target.GetValue<TEnum> (this); - return (TValue)Convert.ChangeType (value, typeof (TValue)); - } - } - - /// <summary> - /// Sets a value or a list of values for the represented property on the target control. - /// </summary> - /// <typeparam name="TValue">An `IEnumerable` of the values to set, or the value to set.</typeparam> - /// <param name="target">The object on which the value must be set.</param> - /// <param name="value">The value to set.</param> - public override void SetValue<TValue> (MockControl target, TValue value) - { - var values = value as IEnumerable; - if (values != null) { + } else if (fromValue is IEnumerable) { if (!IsValueCombinable) - throw new ArgumentException ("Can't set a combined value on a non-combinable type", nameof (value)); + throw new ArgumentException ("Can't set a combined value on a non-combinable type", nameof(TFrom)); unchecked { - var realValue = Convert.ToUInt64 (default (TUnderlying)); - foreach (var val in values) { + var realValue = Convert.ToUInt64 (default(TUnderlying)); + foreach (var val in (IEnumerable) fromValue) { realValue |= Convert.ToUInt64 (val); } - target.SetValue (this, (TEnum)Enum.ToObject (typeof (TEnum), realValue)); + toValue = realValue; + return true; } + } else if (toType.IsEnum) { + toValue = Enum.ToObject (toType, fromValue); + return true; } - else { - target.SetValue (this, (TEnum)Enum.ToObject (typeof (TEnum), value)); - } + + return base.TryConvert (fromValue, toType, out toValue); } + + private TUnderlying[] Values; + private ulong[] LongValues; } } diff --git a/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs b/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs index a7a1799..dc44742 100644 --- a/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs +++ b/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs @@ -6,7 +6,7 @@ using Xamarin.PropertyEditing.Tests.MockControls; namespace Xamarin.PropertyEditing.Tests.MockPropertyInfo { - public class MockPropertyInfo<T> : IPropertyInfo, IGetAndSet, IEquatable<MockPropertyInfo<T>> + public class MockPropertyInfo<T> : IPropertyInfo, IPropertyConverter, IEquatable<MockPropertyInfo<T>> { public MockPropertyInfo (string name, string category = "", bool canWrite = true, IEnumerable<Type> converterTypes = null) { @@ -14,7 +14,7 @@ namespace Xamarin.PropertyEditing.Tests.MockPropertyInfo Category = category; CanWrite = canWrite; if (converterTypes != null) { - TypeConverters = converterTypes + this.typeConverters = converterTypes .Where (type => type != null && typeof (TypeConverter).IsAssignableFrom (type)) .Select (type => (TypeConverter)Activator.CreateInstance (type)) .ToArray(); @@ -25,83 +25,36 @@ namespace Xamarin.PropertyEditing.Tests.MockPropertyInfo public virtual Type Type => typeof (T); public string Category { get; } public bool CanWrite { get; } - public virtual ValueSources ValueSources => ValueSources.Local; + public virtual ValueSources ValueSources => ValueSources.Local | ValueSources.Default; static readonly PropertyVariation[] EmptyVariations = new PropertyVariation[0]; public virtual IReadOnlyList<PropertyVariation> Variations => EmptyVariations; static readonly IAvailabilityConstraint[] EmptyConstraints = new IAvailabilityConstraint[0]; public virtual IReadOnlyList<IAvailabilityConstraint> AvailabilityConstraints => EmptyConstraints; - private IReadOnlyList<TypeConverter> TypeConverters; - - public TValue GetValue<TValue> (object target) - { - return GetValue<TValue> ((MockControl)target); - } - - public void SetValue<TValue> (object target, TValue value) - { - SetValue ((MockControl)target, value); - } - - public virtual TValue GetValue<TValue> (MockControl target) - { - object value = target.GetValue<T> (this); - if (value is TValue) - return (TValue)value; - TValue converted; - if (TryConvertToValue (value, out converted)) { - return converted; - } - if (value == null) - return default (TValue); - return (TValue)(typeof (TValue) == typeof (string) - ? value.ToString () - : Convert.ChangeType (value, typeof (TValue))); - } - - public virtual void SetValue<TValue> (MockControl target, TValue value) - { - object realValue = value; - object converted; - if (TryConvertFromValue (value, out converted)) { - realValue = converted; - } - else if (realValue != null && !typeof (T).IsInstanceOfType (value)) { - realValue = Convert.ChangeType (value, typeof (T)); - } - - target.SetValue (this, (T)realValue); - } - - private bool TryConvertToValue<TValue> (object value, out TValue converted) + public virtual bool TryConvert<TFrom> (TFrom fromValue, Type toType, out object toValue) { - converted = default (TValue); - - if (TypeConverters == null) return false; - foreach (var converter in TypeConverters) { - if (converter.CanConvertTo (typeof (TValue))) { - converted = (TValue)converter.ConvertTo (value, typeof (TValue)); - return true; + toValue = null; + if (this.typeConverters != null) { + foreach (var converter in this.typeConverters) { + if (converter.CanConvertTo (toType)) { + toValue = converter.ConvertTo (fromValue, toType); + return true; + } } } - return false; - } - - private bool TryConvertFromValue<TValue> (TValue value, out object converted) - { - converted = null; - - if (TypeConverters == null) - return false; - - foreach (var converter in TypeConverters) { - if (converter.CanConvertFrom (typeof (TValue))) { - converted = converter.ConvertFrom (value); - return true; - } + if (toType == typeof(string)) { + toValue = fromValue?.ToString (); + return true; } + try { + toValue = Convert.ChangeType (fromValue, toType); + return true; + } catch { + + } + return false; } @@ -133,11 +86,15 @@ namespace Xamarin.PropertyEditing.Tests.MockPropertyInfo { var hashCode = 1861411795; unchecked { - hashCode = hashCode * -1521134295 + Name.GetHashCode (); - hashCode = hashCode * -1521134295 + Category.GetHashCode (); + if (Name != null) + hashCode = hashCode * -1521134295 + Name.GetHashCode (); + if (Category != null) + hashCode = hashCode * -1521134295 + Category.GetHashCode (); hashCode = hashCode * -1521134295 + CanWrite.GetHashCode (); } return hashCode; } + + private readonly IReadOnlyList<TypeConverter> typeConverters; } } diff --git a/Xamarin.PropertyEditing.Tests/MockPropertyProviderTests.cs b/Xamarin.PropertyEditing.Tests/MockPropertyProviderTests.cs index bb033e1..eee6d2b 100644 --- a/Xamarin.PropertyEditing.Tests/MockPropertyProviderTests.cs +++ b/Xamarin.PropertyEditing.Tests/MockPropertyProviderTests.cs @@ -9,6 +9,7 @@ using Xamarin.PropertyEditing.Tests.MockControls; namespace Xamarin.PropertyEditing.Tests { + /* [TestFixture] public class MockPropertyProviderTests { @@ -25,39 +26,7 @@ namespace Xamarin.PropertyEditing.Tests Assert.That (propertyInfo.Name, Is.EqualTo (TestClass.PropertyName)); Assert.That (propertyInfo.Type, Is.EqualTo (typeof (string))); } - - [Test] - public async Task MockSetValue () - { - var obj = new TestClass (); - - var provider = new MockEditorProvider (); - IObjectEditor editor = await provider.GetObjectEditorAsync (obj); - - const string value = "value"; - - var propertyInfo = editor.Properties.Single (); - await editor.SetValueAsync (propertyInfo, new ValueInfo<string> { - Value = value - }); - - Assert.That (obj.GetValue<string>(propertyInfo), Is.EqualTo (value)); - } - - [Test] - public async Task MockGetValue () - { - const string value = "value"; - var obj = new TestClass (value); - - var provider = new MockEditorProvider (); - IObjectEditor editor = await provider.GetObjectEditorAsync (obj); - - ValueInfo<string> info = await editor.GetValueAsync<string> (editor.Properties.Single ()); - Assert.That (info.Value, Is.EqualTo (value)); - Assert.That (info.Source, Is.EqualTo (ValueSource.Local)); - } - + [Test] public async Task MockSetValueConvert () { @@ -243,4 +212,5 @@ namespace Xamarin.PropertyEditing.Tests } } } + */ } diff --git a/Xamarin.PropertyEditing.Tests/ObservableLookupTests.cs b/Xamarin.PropertyEditing.Tests/ObservableLookupTests.cs new file mode 100644 index 0000000..f88b9b5 --- /dev/null +++ b/Xamarin.PropertyEditing.Tests/ObservableLookupTests.cs @@ -0,0 +1,71 @@ +using System.Collections.Specialized; +using System.Linq; +using NUnit.Framework; + +namespace Xamarin.PropertyEditing.Tests +{ + [TestFixture] + internal class ObservableLookupTests + { + [TestCase ("key")] + [TestCase (null)] + public void RemoveGroup (string key) + { + const string value = "value"; + var lookup = new ObservableLookup<string, string> (); + lookup.Add (key, value); + Assume.That (lookup.Contains (key), Is.True); + Assume.That (lookup[key], Contains.Item (value)); + + bool groupRemoved = false; + lookup.CollectionChanged += (sender, args) => { + if (args.Action == NotifyCollectionChangedAction.Remove) { + var g = args.OldItems[0] as IGrouping<string, string>; + if (g != null && g.Key == key) + groupRemoved = true; + } + }; + + var grouping = lookup[key]; + lookup.Remove (key); + + Assert.That (groupRemoved, Is.True); + Assert.That (lookup, Does.Not.Contain (grouping)); + } + + [TestCase ("key")] + [TestCase (null)] // The reality is that null as a key receives special treatment + public void RemoveLastItemInGroup (string key) + { + const string value = "value"; + var lookup = new ObservableLookup<string, string> (); + lookup.Add (key, value); + Assume.That (lookup.Contains (key), Is.True); + Assume.That (lookup[key], Contains.Item (value)); + + bool itemRemoved = false, groupRemoved = false; + lookup.CollectionChanged += (sender, args) => { + if (args.Action == NotifyCollectionChangedAction.Remove) { + var g = args.OldItems[0] as IGrouping<string, string>; + if (g != null && g.Key == key) + groupRemoved = true; + } + }; + + var grouping = lookup[key]; + ((INotifyCollectionChanged) grouping).CollectionChanged += (sender, args) => { + if (args.Action == NotifyCollectionChangedAction.Remove) { + if (args.OldItems[0] == value) + itemRemoved = true; + } + }; + + lookup.Remove (key, value); + + Assert.That (itemRemoved, Is.True); + Assert.That (groupRemoved, Is.True); + Assert.That (grouping, Does.Not.Contains (value)); + Assert.That (lookup, Does.Not.Contain (grouping)); + } + } +} diff --git a/Xamarin.PropertyEditing.Tests/PanelViewModelTests.cs b/Xamarin.PropertyEditing.Tests/PanelViewModelTests.cs index 4d537b3..69299f6 100644 --- a/Xamarin.PropertyEditing.Tests/PanelViewModelTests.cs +++ b/Xamarin.PropertyEditing.Tests/PanelViewModelTests.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; @@ -30,11 +31,11 @@ namespace Xamarin.PropertyEditing.Tests var editor = await provider.GetObjectEditorAsync (obj); Assume.That (editor.Properties.Count, Is.EqualTo (1)); - var vm = new PanelViewModel (provider); + var vm = new PanelViewModel (provider, TargetPlatform.Default); vm.SelectedObjects.Add (obj); Assert.That (vm.Properties, Is.Not.Empty); - Assert.That (vm.Properties[0].Property, Is.EqualTo (editor.Properties.Single ())); + Assert.That (((PropertyViewModel)vm.Properties[0]).Property, Is.EqualTo (editor.Properties.Single ())); } [Test] @@ -58,16 +59,16 @@ namespace Xamarin.PropertyEditing.Tests providerMock.Setup (ep => ep.GetObjectEditorAsync (obj1)).ReturnsAsync (editor1Mock.Object); providerMock.Setup (ep => ep.GetObjectEditorAsync (obj2)).ReturnsAsync (editor2Mock.Object); - var vm = new PanelViewModel (providerMock.Object); + var vm = new PanelViewModel (providerMock.Object, TargetPlatform.Default); vm.SelectedObjects.Add (obj1); Assume.That (vm.Properties.Count, Is.EqualTo (1)); - Assume.That (vm.Properties[0].Property, Is.EqualTo (sharedPropertyMock.Object)); + Assume.That (((PropertyViewModel)vm.Properties[0]).Property, Is.EqualTo (sharedPropertyMock.Object)); // Reflection property info equate actually fails on the same property across class/subclass vm.SelectedObjects.Add (obj2); Assert.That (vm.Properties.Count, Is.EqualTo (1)); - Assert.That (vm.Properties.Single ().Property, Is.EqualTo (sharedPropertyMock.Object)); + Assert.That (((PropertyViewModel)vm.Properties.Single()).Property, Is.EqualTo (sharedPropertyMock.Object)); } [Test] @@ -91,17 +92,17 @@ namespace Xamarin.PropertyEditing.Tests providerMock.Setup (ep => ep.GetObjectEditorAsync (obj1)).ReturnsAsync (editor1Mock.Object); providerMock.Setup (ep => ep.GetObjectEditorAsync (obj2)).ReturnsAsync (editor2Mock.Object); - var vm = new PanelViewModel (providerMock.Object); + var vm = new PanelViewModel (providerMock.Object, TargetPlatform.Default); vm.SelectedObjects.Add (obj2); Assume.That (vm.Properties.Count, Is.EqualTo (2)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (sharedPropertyMock.Object)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (subPropertyMock.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (sharedPropertyMock.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (subPropertyMock.Object)); // Reflection property info equate actually fails on the same property across class/subclass vm.SelectedObjects.Add (obj1); Assert.That (vm.Properties.Count, Is.EqualTo (1)); - Assert.That (vm.Properties.Select (v => v.Property), Contains.Item (sharedPropertyMock.Object)); + Assert.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (sharedPropertyMock.Object)); } [Test] @@ -113,7 +114,7 @@ namespace Xamarin.PropertyEditing.Tests var obj1 = new TestClass (); var obj2 = new TestClass (); - var vm = new PanelViewModel (provider); + var vm = new PanelViewModel (provider, TargetPlatform.Default); vm.SelectedObjects.Add (obj1); var property = vm.Properties[0]; @@ -136,7 +137,7 @@ namespace Xamarin.PropertyEditing.Tests var obj1 = new TestClass (); var obj2 = new TestClass (); - var vm = new PanelViewModel (provider); + var vm = new PanelViewModel (provider, TargetPlatform.Default); vm.SelectedObjects.Add (obj1); vm.SelectedObjects.Add (obj2); @@ -168,16 +169,16 @@ namespace Xamarin.PropertyEditing.Tests var provider = new Mock<IEditorProvider> (); provider.Setup (ep => ep.GetObjectEditorAsync (obj)).ReturnsAsync (editorMock.Object); - var vm = new PanelViewModel (provider.Object); + var vm = new PanelViewModel (provider.Object, TargetPlatform.Default); vm.SelectedObjects.Add (obj); Assume.That (vm.Properties.Count, Is.EqualTo (2)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty1.Object)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty2.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty1.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty2.Object)); properties.Remove (mockProperty2.Object); Assert.That (vm.Properties.Count, Is.EqualTo (1)); - Assert.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty1.Object)); + Assert.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty1.Object)); } [Test] @@ -198,17 +199,17 @@ namespace Xamarin.PropertyEditing.Tests var provider = new Mock<IEditorProvider> (); provider.Setup (ep => ep.GetObjectEditorAsync (obj)).ReturnsAsync (editorMock.Object); - var vm = new PanelViewModel (provider.Object); + var vm = new PanelViewModel (provider.Object, TargetPlatform.Default); vm.SelectedObjects.Add (obj); Assume.That (vm.Properties.Count, Is.EqualTo (1)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty1.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty1.Object)); properties.Add (mockProperty2.Object); Assert.That (vm.Properties.Count, Is.EqualTo (2)); - Assert.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty1.Object)); - Assert.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty2.Object)); + Assert.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty1.Object)); + Assert.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty2.Object)); } [Test] @@ -229,7 +230,7 @@ namespace Xamarin.PropertyEditing.Tests var provider = new Mock<IEditorProvider> (); provider.Setup (ep => ep.GetObjectEditorAsync (obj)).ReturnsAsync (editorMock.Object); - var vm = new PanelViewModel (provider.Object); + var vm = new PanelViewModel (provider.Object, TargetPlatform.Default); // We need access to the custom reset method here to ensure compliance // It's a bit hacky but this is unlikely to change. If it does, this test @@ -238,13 +239,13 @@ namespace Xamarin.PropertyEditing.Tests ((ObservableCollectionEx<object>)vm.SelectedObjects).Reset (new[] { obj }); Assume.That (vm.Properties.Count, Is.EqualTo (1)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty1.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty1.Object)); properties.Add (mockProperty2.Object); Assert.That (vm.Properties.Count, Is.EqualTo (2)); - Assert.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty1.Object)); - Assert.That (vm.Properties.Select (v => v.Property), Contains.Item (mockProperty2.Object)); + Assert.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty1.Object)); + Assert.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (mockProperty2.Object)); } [Test] @@ -271,11 +272,11 @@ namespace Xamarin.PropertyEditing.Tests providerMock.Setup (ep => ep.GetObjectEditorAsync (baseObj)).ReturnsAsync (baseEditorMock.Object); providerMock.Setup (ep => ep.GetObjectEditorAsync (derivedObj)).ReturnsAsync (derivedEditorMock.Object); - var vm = new PanelViewModel (providerMock.Object); + var vm = new PanelViewModel (providerMock.Object, TargetPlatform.Default); vm.SelectedObjects.AddItems (new[] { baseObj, derivedObj }); Assume.That (vm.Properties.Count, Is.EqualTo (1)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (baseProperty.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (baseProperty.Object)); derivedProperties.Remove (baseProperty.Object); Assert.That (vm.Properties, Is.Empty); @@ -305,11 +306,11 @@ namespace Xamarin.PropertyEditing.Tests providerMock.Setup (ep => ep.GetObjectEditorAsync (baseObj)).ReturnsAsync (baseEditorMock.Object); providerMock.Setup (ep => ep.GetObjectEditorAsync (derivedObj)).ReturnsAsync (derivedEditorMock.Object); - var vm = new PanelViewModel (providerMock.Object); + var vm = new PanelViewModel (providerMock.Object, TargetPlatform.Default); vm.SelectedObjects.AddItems (new[] { baseObj, derivedObj }); Assume.That (vm.Properties.Count, Is.EqualTo (1)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (baseProperty.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (baseProperty.Object)); vm.SelectedObjects.Remove (derivedObj); Assume.That (vm.Properties, Is.Not.Empty); @@ -343,11 +344,11 @@ namespace Xamarin.PropertyEditing.Tests providerMock.Setup (ep => ep.GetObjectEditorAsync (baseObj)).ReturnsAsync (baseEditorMock.Object); providerMock.Setup (ep => ep.GetObjectEditorAsync (derivedObj)).ReturnsAsync (derivedEditorMock.Object); - var vm = new PanelViewModel (providerMock.Object); + var vm = new PanelViewModel (providerMock.Object, TargetPlatform.Default); vm.SelectedObjects.AddItems (new[] { baseObj, derivedObj }); Assume.That (vm.Properties.Count, Is.EqualTo (1)); - Assume.That (vm.Properties.Select (v => v.Property), Contains.Item (baseProperty.Object)); + Assume.That (vm.Properties.Cast<PropertyViewModel>().Select (v => v.Property), Contains.Item (baseProperty.Object)); Assume.That (vm.SelectedObjects, Is.TypeOf<ObservableCollectionEx<object>> ()); ((ObservableCollectionEx<object>)vm.SelectedObjects).Reset (new[] { baseObj }); @@ -385,7 +386,7 @@ namespace Xamarin.PropertyEditing.Tests }); provider.Setup (ep => ep.GetObjectEditorAsync (obj2)).ReturnsAsync (editor2.Object); - var vm = new PanelViewModel (provider.Object); + var vm = new PanelViewModel (provider.Object, TargetPlatform.Default); vm.SelectedObjects.Add (obj1); Assume.That (returnObject, Is.Not.Null); @@ -409,15 +410,15 @@ namespace Xamarin.PropertyEditing.Tests var editor = await provider.GetObjectEditorAsync (obj); Assume.That (editor.Properties.Count, Is.EqualTo (2)); - var vm = new PanelViewModel (provider); + var vm = new PanelViewModel (provider, TargetPlatform.Default); Assume.That (vm.ArrangeMode, Is.EqualTo (PropertyArrangeMode.Name)); vm.SelectedObjects.Add (obj); - Assume.That (vm.ArrangedProperties, Is.Not.Empty); - Assume.That (vm.ArrangedProperties[0].Count, Is.EqualTo (2)); + Assume.That (vm.ArrangedEditors, Is.Not.Empty); + Assume.That (vm.ArrangedEditors[0].Count, Is.EqualTo (2)); vm.FilterText = "sub"; - Assert.That (vm.ArrangedProperties[0].Count, Is.EqualTo (1)); + Assert.That (vm.ArrangedEditors[0].Count, Is.EqualTo (1)); } [Test] @@ -429,18 +430,18 @@ namespace Xamarin.PropertyEditing.Tests var editor = await provider.GetObjectEditorAsync (obj); Assume.That (editor.Properties.Count, Is.EqualTo (2)); - var vm = new PanelViewModel (provider); + var vm = new PanelViewModel (provider, TargetPlatform.Default); Assume.That (vm.ArrangeMode, Is.EqualTo (PropertyArrangeMode.Name)); vm.SelectedObjects.Add (obj); - Assume.That (vm.ArrangedProperties, Is.Not.Empty); - Assume.That (vm.ArrangedProperties[0].Count, Is.EqualTo (2)); + Assume.That (vm.ArrangedEditors, Is.Not.Empty); + Assume.That (vm.ArrangedEditors[0].Count, Is.EqualTo (2)); vm.FilterText = "sub"; - Assume.That (vm.ArrangedProperties[0].Count, Is.EqualTo (1)); + Assume.That (vm.ArrangedEditors[0].Count, Is.EqualTo (1)); vm.FilterText = String.Empty; - Assert.That (vm.ArrangedProperties[0].Count, Is.EqualTo (2)); + Assert.That (vm.ArrangedEditors[0].Count, Is.EqualTo (2)); } [Test] @@ -451,11 +452,44 @@ namespace Xamarin.PropertyEditing.Tests var editor = await provider.GetObjectEditorAsync (obj); Assume.That (editor.Properties.Count, Is.EqualTo (2)); - var vm = new PanelViewModel (provider) { ArrangeMode = PropertyArrangeMode.Category }; + var vm = new PanelViewModel (provider, TargetPlatform.Default) { ArrangeMode = PropertyArrangeMode.Category }; vm.SelectedObjects.Add (obj); - Assume.That (vm.ArrangedProperties, Is.Not.Empty); - Assert.That (vm.ArrangedProperties.FirstOrDefault (g => g.Key == "Sub"), Is.Not.Null); + Assume.That (vm.ArrangedEditors, Is.Not.Empty); + Assert.That (vm.ArrangedEditors.FirstOrDefault (g => g.Key == "Sub"), Is.Not.Null); + } + + [Test] + public void GroupedPropertiesArrange () + { + var intProvider = new IntegerPropertyViewModelTests (); + var stringProvider = new StringViewModelTests (); + var brushProvider = new SolidBrushPropertyViewModelTests(); + + var intProperty = intProvider.GetPropertyMock ("int", "A"); + var stringProperty1 = stringProvider.GetPropertyMock ("string1"); + var stringProperty2 = stringProvider.GetPropertyMock ("string2"); + var brushProperty = brushProvider.GetPropertyMock ("brush", "C"); + + var editor = new MockObjectEditor (intProperty.Object, stringProperty1.Object, stringProperty2.Object, brushProperty.Object); + + var provider = new Mock<IEditorProvider> (); + provider.Setup (p => p.GetObjectEditorAsync (editor.Target)).ReturnsAsync (editor); + + var platform = new TargetPlatform { + GroupedTypes = new Dictionary<Type, string> { + { typeof(string), "B" } + } + }; + + var vm = new PanelViewModel (provider.Object, platform); + Assume.That (vm.ArrangeMode, Is.EqualTo (PropertyArrangeMode.Name)); + + vm.ArrangeMode = PropertyArrangeMode.Category; + vm.SelectedObjects.Add (editor.Target); + Assert.That (vm.ArrangedEditors[0].Key, Is.EqualTo ("A")); + Assert.That (vm.ArrangedEditors[1].Key, Is.EqualTo ("B")); + Assert.That (vm.ArrangedEditors[2].Key, Is.EqualTo ("C")); } [Test] @@ -466,22 +500,22 @@ namespace Xamarin.PropertyEditing.Tests var editor = await provider.GetObjectEditorAsync (obj); Assume.That (editor.Properties.Count, Is.EqualTo (2)); - var vm = new PanelViewModel (provider) { ArrangeMode = PropertyArrangeMode.Category }; + var vm = new PanelViewModel (provider, TargetPlatform.Default) { ArrangeMode = PropertyArrangeMode.Category }; vm.SelectedObjects.Add (obj); - Assume.That (vm.ArrangedProperties, Is.Not.Empty); + Assume.That (vm.ArrangedEditors, Is.Not.Empty); vm.FilterText = "sub"; - Assert.That (vm.ArrangedProperties.Count, Is.EqualTo (1)); + Assert.That (vm.ArrangedEditors.Count, Is.EqualTo (1)); - var group = vm.ArrangedProperties.FirstOrDefault (g => g.Key == "Sub"); + var group = vm.ArrangedEditors.FirstOrDefault (g => g.Key == "Sub"); Assert.That (group, Is.Not.Null); Assert.That (group.Count, Is.EqualTo (1)); } internal override PropertiesViewModel CreateVm (IEditorProvider provider) { - return new PanelViewModel (provider); + return new PanelViewModel (provider, TargetPlatform.Default); } private TestContext context; diff --git a/Xamarin.PropertyEditing.Tests/PropertyGroupViewModelTests.cs b/Xamarin.PropertyEditing.Tests/PropertyGroupViewModelTests.cs new file mode 100644 index 0000000..468fe79 --- /dev/null +++ b/Xamarin.PropertyEditing.Tests/PropertyGroupViewModelTests.cs @@ -0,0 +1,187 @@ +using System.Collections.Specialized; +using Moq; +using NUnit.Framework; +using Xamarin.PropertyEditing.ViewModels; + +namespace Xamarin.PropertyEditing.Tests +{ + [TestFixture] + internal class PropertyGroupViewModelTests + { + [Test] + public void PropertyGroup () + { + IObjectEditor editor = null; + + var prop = new Mock<IPropertyInfo> (); + prop.SetupGet (p => p.Type).Returns (typeof(int)); + + var prop2 = new Mock<IPropertyInfo> (); + prop2.SetupGet (p => p.Type).Returns (typeof(int)); + + editor = new MockObjectEditor (prop.Object, prop2.Object); + var pvm = new PropertyViewModel<int> (prop.Object, new[] { editor }); + var pvm2 = new PropertyViewModel<int> (prop2.Object, new[] { editor }); + + var vm = new PropertyGroupViewModel ("category", new[] { pvm, pvm2 }, new [] { editor}); + Assert.That (vm.Properties, Contains.Item (pvm)); + Assert.That (vm.Properties, Contains.Item (pvm2)); + } + + [Test] + public void UnavailablePropertyNotInList () + { + IObjectEditor editor; + + var constraint = new Mock<IAvailabilityConstraint>(); + var prop = new Mock<IPropertyInfo> (); + prop.SetupGet (p => p.Type).Returns (typeof(int)); + prop.SetupGet (p => p.AvailabilityConstraints).Returns (new[] { constraint.Object }); + + var constraint2 = new Mock<IAvailabilityConstraint> (); + var prop2 = new Mock<IPropertyInfo> (); + prop2.SetupGet (p => p.Type).Returns (typeof(int)); + prop2.SetupGet (p => p.AvailabilityConstraints).Returns (new[] { constraint2.Object }); + + editor = new MockObjectEditor (prop.Object, prop2.Object); + constraint.Setup (c => c.GetIsAvailableAsync (editor)).ReturnsAsync (true); + constraint2.Setup (c => c.GetIsAvailableAsync (editor)).ReturnsAsync (false); + + var pvm = new PropertyViewModel<int> (prop.Object, new[] { editor }); + var pvm2 = new PropertyViewModel<int> (prop2.Object, new[] { editor }); + + var vm = new PropertyGroupViewModel ("category", new[] { pvm, pvm2 }, new [] { editor}); + Assert.That (vm.Properties, Contains.Item (pvm)); + Assert.That (vm.Properties, Does.Not.Contain (pvm2)); + } + + [Test] + public void AvailabilityUpdates () + { + IObjectEditor editor = null; + + var constraint = new Mock<IAvailabilityConstraint>(); + var prop = new Mock<IPropertyInfo> (); + prop.SetupGet (p => p.Type).Returns (typeof(int)); + prop.SetupGet (p => p.AvailabilityConstraints).Returns (new[] { constraint.Object }); + + bool isAvailable = false; + + var constraint2 = new Mock<IAvailabilityConstraint> (); + constraint2.SetupGet (a => a.ConstrainingProperties).Returns (new[] { prop.Object }); + var prop2 = new Mock<IPropertyInfo> (); + prop2.SetupGet (p => p.Type).Returns (typeof(int)); + prop2.SetupGet (p => p.AvailabilityConstraints).Returns (new[] { constraint2.Object }); + + editor = new MockObjectEditor (prop.Object, prop2.Object); + constraint.Setup (c => c.GetIsAvailableAsync (editor)).ReturnsAsync (true); + constraint2.Setup (c => c.GetIsAvailableAsync (editor)).ReturnsAsync (() => isAvailable); + + var pvm = new PropertyViewModel<int> (prop.Object, new[] { editor }); + var pvm2 = new PropertyViewModel<int> (prop2.Object, new[] { editor }); + + var vm = new PropertyGroupViewModel ("category", new[] { pvm, pvm2 }, new [] { editor}); + Assume.That (vm.Properties, Contains.Item (pvm)); + Assume.That (vm.Properties, Does.Not.Contain (pvm2)); + + INotifyCollectionChanged notify = vm.Properties as INotifyCollectionChanged; + Assume.That (notify, Is.Not.Null); + + bool changed = false; + notify.CollectionChanged += (sender, args) => { + if (args.Action == NotifyCollectionChangedAction.Add && args.NewItems[0] == pvm2) + changed = true; + }; + + isAvailable = true; + + // Bit of integration here, constrainting property changes will trigger availability requery + pvm.Value = 5; + + Assert.That (changed, Is.True); + Assert.That (vm.Properties, Contains.Item (pvm)); + Assert.That (vm.Properties, Contains.Item (pvm2)); + } + + [Test] + public void Filtered () + { + IObjectEditor editor = null; + + var prop = new Mock<IPropertyInfo> (); + prop.SetupGet (p => p.Type).Returns (typeof(int)); + prop.SetupGet (p => p.Name).Returns ("one"); + + var prop2 = new Mock<IPropertyInfo> (); + prop2.SetupGet (p => p.Type).Returns (typeof(int)); + prop2.SetupGet (p => p.Name).Returns ("two"); + + editor = new MockObjectEditor (prop.Object, prop2.Object); + var pvm = new PropertyViewModel<int> (prop.Object, new[] { editor }); + var pvm2 = new PropertyViewModel<int> (prop2.Object, new[] { editor }); + + var vm = new PropertyGroupViewModel ("category", new[] { pvm, pvm2 }, new [] { editor}); + Assume.That (vm.Properties, Contains.Item (pvm)); + Assume.That (vm.Properties, Contains.Item (pvm2)); + + INotifyCollectionChanged notify = vm.Properties as INotifyCollectionChanged; + Assume.That (notify, Is.Not.Null); + + bool changed = false; + notify.CollectionChanged += (sender, args) => { + if (args.Action == NotifyCollectionChangedAction.Remove && args.OldItems[0] == pvm) + changed = true; + }; + + vm.FilterText = "t"; + + Assert.That (changed, Is.True, "Collection changed event didn't trigger correctly"); + Assert.That (vm.Properties, Contains.Item (pvm2)); + Assert.That (vm.Properties, Does.Not.Contain (pvm)); + Assert.That (vm.HasChildElements, Is.True); + } + + [Test] + public void FilteredOutOfChildren () + { + IObjectEditor editor = null; + + var prop = new Mock<IPropertyInfo> (); + prop.SetupGet (p => p.Type).Returns (typeof(int)); + prop.SetupGet (p => p.Name).Returns ("one"); + + var prop2 = new Mock<IPropertyInfo> (); + prop2.SetupGet (p => p.Type).Returns (typeof(int)); + prop2.SetupGet (p => p.Name).Returns ("two"); + + editor = new MockObjectEditor (prop.Object, prop2.Object); + var pvm = new PropertyViewModel<int> (prop.Object, new[] { editor }); + var pvm2 = new PropertyViewModel<int> (prop2.Object, new[] { editor }); + + var vm = new PropertyGroupViewModel ("category", new[] { pvm, pvm2 }, new [] { editor}); + Assume.That (vm.Properties, Contains.Item (pvm)); + Assume.That (vm.Properties, Contains.Item (pvm2)); + + bool changed = false; + vm.PropertyChanged += (sender, args) => { + if (args.PropertyName == nameof(PropertyGroupViewModel.HasChildElements)) + changed = true; + }; + + vm.FilterText = "1"; + + Assert.That (changed, Is.True, "HasChildElements didn't change"); + Assert.That (vm.HasChildElements, Is.False); + Assert.That (vm.Properties, Does.Not.Contain (pvm2)); + Assert.That (vm.Properties, Does.Not.Contain (pvm)); + + changed = false; + vm.FilterText = null; + + Assert.That (changed, Is.True, "HasChildElements didn't change"); + Assert.That (vm.HasChildElements, Is.True); + Assert.That (vm.Properties, Contains.Item (pvm2)); + Assert.That (vm.Properties, Contains.Item (pvm)); + } + } +} diff --git a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs index cd82739..de5f787 100644 --- a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs +++ b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs @@ -491,10 +491,43 @@ namespace Xamarin.PropertyEditing.Tests } [Test] - public async Task PropertyChangeRequeriesAvailability () + public async Task ConstrainingPropertyChangeRequeriesAvailability () { + var otherProp = GetPropertyMock (); + var prop = GetPropertyMock (); + var constraint = new Mock<IAvailabilityConstraint> (); + constraint.SetupGet (c => c.ConstrainingProperties).Returns (new[] { otherProp.Object }); + prop.SetupGet (p => p.AvailabilityConstraints).Returns (new List<IAvailabilityConstraint> { constraint.Object }); + + bool isAvailable = true; + IObjectEditor editor = new MockObjectEditor (prop.Object, otherProp.Object); + constraint.Setup (c => c.GetIsAvailableAsync (editor)).ReturnsAsync (() => isAvailable); + + var vm = GetViewModel (prop.Object, new[] { editor }); + Assume.That (vm.IsAvailable, Is.True); + + bool changed = false; + vm.PropertyChanged += (o, e) => { + if (e.PropertyName == nameof(PropertyViewModel.IsAvailable)) + changed = true; + }; + + isAvailable = false; + await editor.SetValueAsync (otherProp.Object, new ValueInfo<TValue> { + Value = GetRandomTestValue(), + Source = ValueSource.Local + }); + + Assert.That (vm.IsAvailable, Is.False); + Assert.That (changed, Is.True); + } + + [Test] + public async Task PropertyChangeRequeriesAvailability () + { + var prop = GetPropertyMock (); var constraint = new Mock<IAvailabilityConstraint> (); constraint.SetupGet (c => c.ConstrainingProperties).Returns (new[] { prop.Object }); @@ -523,6 +556,82 @@ namespace Xamarin.PropertyEditing.Tests Assert.That (changed, Is.True); } + [Test] + public void ClearLocalValue () + { + var value = GetNonDefaultRandomTestValue (); + + var mockProperty = GetPropertyMock (); + mockProperty.SetupGet (pi => pi.ValueSources).Returns (ValueSources.Default | ValueSources.Local); + + var mockEditor = new MockObjectEditor { + SupportsDefault = true, + Properties = new [] { + mockProperty.Object + } + }; + + var vm = GetViewModel (mockProperty.Object, new[] { mockEditor }); + Assume.That (vm.Value, Is.EqualTo (default(TValue))); + + Assert.That (vm.ClearValueCommand.CanExecute (null), Is.False); + + bool changed = false; + vm.ClearValueCommand.CanExecuteChanged += (sender, args) => { + changed = true; + }; + + vm.Value = value; + Assume.That (vm.ValueSource, Is.EqualTo (ValueSource.Local)); + + Assert.That (changed, Is.True); + Assert.That (vm.ClearValueCommand.CanExecute (null), Is.True); + + vm.ClearValueCommand.Execute (null); + + Assert.That (vm.Value, Is.EqualTo (default(TValue))); + } + + [Test] + [Description ("If the target platform doesn't support distinguishing local/default, we can't clear the value")] + public void ClearValueDefaultNotSupported () + { + var value = GetNonDefaultRandomTestValue (); + + var mockProperty = GetPropertyMock (); + + var editorMock = new Mock<IObjectEditor> (); + editorMock.Setup (oe => oe.GetValueAsync<TValue> (mockProperty.Object, null)).ReturnsAsync (new ValueInfo<TValue> { + Value = value, + Source = ValueSource.Local + }); + mockProperty.SetupGet (pi => pi.ValueSources).Returns (ValueSources.Local); + + var vm = GetViewModel (mockProperty.Object, new[] { editorMock.Object }); + + Assert.That (vm.ClearValueCommand.CanExecute (null), Is.False); + } + + [Test] + [Description ("We only allow clearing local values")] + public void ClearValueNotLocalValue () + { + var value = GetNonDefaultRandomTestValue (); + + var mockProperty = GetPropertyMock (); + mockProperty.SetupGet (pi => pi.ValueSources).Returns (ValueSources.Default | ValueSources.Local | ValueSources.Resource); + + var editorMock = new Mock<IObjectEditor> (); + editorMock.Setup (oe => oe.GetValueAsync<TValue> (mockProperty.Object, null)).ReturnsAsync (new ValueInfo<TValue> { + Value = value, + Source = ValueSource.Resource + }); + + var vm = GetViewModel (mockProperty.Object, new[] { editorMock.Object }); + + Assert.That (vm.ClearValueCommand.CanExecute (null), Is.False); + } + protected TValue GetNonDefaultRandomTestValue () { TValue value = default (TValue); @@ -541,10 +650,12 @@ namespace Xamarin.PropertyEditing.Tests { } - protected Mock<IPropertyInfo> GetPropertyMock () + protected internal Mock<IPropertyInfo> GetPropertyMock (string name = null, string category = null) { var mock = new Mock<IPropertyInfo> (); mock.SetupGet (pi => pi.Type).Returns (typeof(TValue)); + mock.SetupGet (pi => pi.Name).Returns (name); + mock.SetupGet (pi => pi.Category).Returns (category); AugmentPropertyMock (mock); @@ -553,12 +664,12 @@ namespace Xamarin.PropertyEditing.Tests protected Random Random => this.rand; - protected TValue GetRandomTestValue () + protected internal TValue GetRandomTestValue () { return GetRandomTestValue (this.rand); } - protected TValue GetRandomTestValue (TValue notValue) + protected internal TValue GetRandomTestValue (TValue notValue) { TValue value = GetRandomTestValue (); while (Equals (value, notValue)) { @@ -568,7 +679,7 @@ namespace Xamarin.PropertyEditing.Tests return value; } - protected MockObjectEditor GetBasicEditor (IPropertyInfo property = null) + protected internal MockObjectEditor GetBasicEditor (IPropertyInfo property = null) { if (property == null) { var propertyMock = GetPropertyMock (); @@ -584,21 +695,21 @@ namespace Xamarin.PropertyEditing.Tests return editor; } - protected MockObjectEditor GetBasicEditor (TValue value, IPropertyInfo property = null) + protected internal MockObjectEditor GetBasicEditor (TValue value, IPropertyInfo property = null) { var editor = GetBasicEditor (property); editor.values[editor.Properties.First ()] = value; return editor; } - protected TViewModel GetBasicTestModel () + protected internal TViewModel GetBasicTestModel () { var editor = GetBasicEditor (); return GetViewModel (editor.Properties.First(), new[] { editor }); } - protected TViewModel GetBasicTestModel (TValue value) + protected internal TViewModel GetBasicTestModel (TValue value) { var editor = GetBasicEditor (value); diff --git a/Xamarin.PropertyEditing.Tests/Xamarin.PropertyEditing.Tests.csproj b/Xamarin.PropertyEditing.Tests/Xamarin.PropertyEditing.Tests.csproj index d86ffac..5e94400 100644 --- a/Xamarin.PropertyEditing.Tests/Xamarin.PropertyEditing.Tests.csproj +++ b/Xamarin.PropertyEditing.Tests/Xamarin.PropertyEditing.Tests.csproj @@ -62,7 +62,7 @@ <Compile Include="BytePropertyViewModelTests.cs" /> <Compile Include="CommonColorTests.cs" /> <Compile Include="Helpers.cs" /> - <Compile Include="IGetAndSet.cs" /> + <Compile Include="IPropertyConverter.cs" /> <Compile Include="MockControls\MockControl.cs" /> <Compile Include="MockControls\MockSampleControl.cs" /> <Compile Include="MockControls\MockWpfButton.cs" /> @@ -74,7 +74,9 @@ <Compile Include="MockPropertyInfo\MockEnumPropertyInfo.cs" /> <Compile Include="MockPropertyProviderTests.cs" /> <Compile Include="MultiAvailabilityConstraintTests.cs" /> + <Compile Include="ObservableLookupTests.cs" /> <Compile Include="PointViewModelTests.cs" /> + <Compile Include="PropertyGroupViewModelTests.cs" /> <Compile Include="RadialGradientBrushPropertyViewModelTests.cs" /> <Compile Include="LinearGradientBrushPropertyViewModelTests.cs" /> <Compile Include="ImageBrushPropertyViewModelTests.cs" /> diff --git a/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs b/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs index 292d8c2..9508b85 100644 --- a/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs +++ b/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs @@ -1,5 +1,6 @@ -using System.Windows; +using System.Windows; using System.Windows.Controls; +using Xamarin.PropertyEditing.Drawing; using Xamarin.PropertyEditing.Tests; namespace Xamarin.PropertyEditing.Windows.Standalone @@ -15,11 +16,21 @@ namespace Xamarin.PropertyEditing.Windows.Standalone this.panel.EditorProvider = new MockEditorProvider (); } - private void Button_Click (object sender, RoutedEventArgs e) + private async void Button_Click (object sender, RoutedEventArgs e) { - var mockedButton = sender as IMockedControl; - var inspectedObject = (mockedButton == null || mockedButton.MockedControl == null) - ? sender : mockedButton.MockedControl; + var mockedControl = sender as IMockedControl; + object inspectedObject; + if (mockedControl == null || mockedControl.MockedControl == null) { + inspectedObject = sender; + } else { + inspectedObject = mockedControl.MockedControl; + if (mockedControl is MockedSampleControlButton mockedButton) { + IObjectEditor editor = await this.panel.EditorProvider.GetObjectEditorAsync (inspectedObject); + await mockedButton.SetBrush (editor, new CommonSolidBrush (20, 120, 220, 240, "sRGB")); + await mockedButton.SetReadOnlyBrush (editor, new CommonSolidBrush (240, 220, 15, 190)); + } + } + if (this.panel.SelectedItems.Contains (inspectedObject)) this.panel.SelectedItems.Remove (inspectedObject); else diff --git a/Xamarin.PropertyEditing.Windows.Standalone/MockedSampleControlButton.cs b/Xamarin.PropertyEditing.Windows.Standalone/MockedSampleControlButton.cs index e2e30e8..9e26a3b 100644 --- a/Xamarin.PropertyEditing.Windows.Standalone/MockedSampleControlButton.cs +++ b/Xamarin.PropertyEditing.Windows.Standalone/MockedSampleControlButton.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Xamarin.PropertyEditing.Drawing; using Xamarin.PropertyEditing.Tests.MockControls; @@ -8,22 +9,31 @@ namespace Xamarin.PropertyEditing.Windows.Standalone public MockedSampleControlButton () : base (new MockSampleControl ()) { // TODO: Move the declaration of this property to MockSampleControl once SolidBrush is supported on both platforms. - var brushPropertyInfo = new BrushPropertyInfo ( + this.brushPropertyInfo = new BrushPropertyInfo ( name: "SolidBrush", category: "Windows Only", canWrite: true, colorSpaces: new[] { "RGB", "sRGB" }); - MockedControl.AddProperty<CommonBrush> (brushPropertyInfo); - MockedControl.SetValue<CommonBrush>(brushPropertyInfo, - new CommonSolidBrush(20, 120, 220, 240, "sRGB")); + MockedControl.AddProperty<CommonBrush> (this.brushPropertyInfo); - var readOnlyBrushPropertyInfo = new BrushPropertyInfo( + this.readOnlyBrushPropertyInfo = new BrushPropertyInfo ( name: "ReadOnlySolidBrush", category: "Windows Only", canWrite: false); - MockedControl.AddProperty<CommonBrush> (readOnlyBrushPropertyInfo); - MockedControl.SetValue<CommonBrush> (readOnlyBrushPropertyInfo, - new CommonSolidBrush (240, 220, 15, 190)); + MockedControl.AddProperty<CommonBrush> (this.readOnlyBrushPropertyInfo); } + + public async Task SetBrush (IObjectEditor editor, CommonBrush brush) + { + await editor.SetValueAsync (this.brushPropertyInfo, new ValueInfo<CommonBrush> { Value = brush }); + } + + public async Task SetReadOnlyBrush (IObjectEditor editor, CommonBrush brush) + { + await editor.SetValueAsync (this.readOnlyBrushPropertyInfo, new ValueInfo<CommonBrush> { Value = brush }); + } + + private IPropertyInfo brushPropertyInfo; + private IPropertyInfo readOnlyBrushPropertyInfo; } } diff --git a/Xamarin.PropertyEditing.Windows/EditorPropertySelector.cs b/Xamarin.PropertyEditing.Windows/EditorPropertySelector.cs index e9b8209..2b81047 100644 --- a/Xamarin.PropertyEditing.Windows/EditorPropertySelector.cs +++ b/Xamarin.PropertyEditing.Windows/EditorPropertySelector.cs @@ -41,9 +41,9 @@ namespace Xamarin.PropertyEditing.Windows public override DataTemplate SelectTemplate (object item, DependencyObject container) { - var vm = item as PropertyViewModel; + var vm = item as EditorViewModel; if (vm != null) { - if (!vm.CanDelve) + if (!(vm is PropertyViewModel) || !((PropertyViewModel)vm).CanDelve) return Options.EditorTemplate; else return Options.ParentTemplate; @@ -107,6 +107,7 @@ namespace Xamarin.PropertyEditing.Windows { typeof(PropertyViewModel<CommonThickness>), typeof(ThicknessEditorControl) }, { typeof(PredefinedValuesViewModel<>), typeof(EnumEditorControl) }, { typeof(BrushPropertyViewModel), typeof(BrushEditorControl) }, + { typeof(PropertyGroupViewModel), typeof(GroupEditorControl) } }; } } diff --git a/Xamarin.PropertyEditing.Windows/GroupEditorControl.cs b/Xamarin.PropertyEditing.Windows/GroupEditorControl.cs new file mode 100644 index 0000000..48b140c --- /dev/null +++ b/Xamarin.PropertyEditing.Windows/GroupEditorControl.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; + +namespace Xamarin.PropertyEditing.Windows +{ + [TemplatePart (Name = "List", Type = typeof(ItemsControl))] + [TemplatePart (Name = "EditorPresenter", Type = typeof(Selector))] + internal class GroupEditorControl + : Selector + { + public GroupEditorControl () + { + FocusableProperty.OverrideMetadata (typeof(GroupEditorControl), new FrameworkPropertyMetadata (false)); + + ItemContainerGenerator.StatusChanged += OnItemContainerGeneratorStatusChanged; + } + + public static readonly DependencyProperty ContentTemplateProperty = DependencyProperty.Register ( + "ContentTemplate", typeof(DataTemplate), typeof(GroupEditorControl), new PropertyMetadata (default(DataTemplate))); + + public DataTemplate ContentTemplate + { + get { return (DataTemplate) GetValue (ContentTemplateProperty); } + set { SetValue (ContentTemplateProperty, value); } + } + + public static readonly DependencyProperty ContentTemplateSelectorProperty = DependencyProperty.Register ( + "ContentTemplateSelector", typeof(DataTemplateSelector), typeof(GroupEditorControl), new PropertyMetadata (default(DataTemplateSelector))); + + public DataTemplateSelector ContentTemplateSelector + { + get { return (DataTemplateSelector) GetValue (ContentTemplateSelectorProperty); } + set { SetValue (ContentTemplateSelectorProperty, value); } + } + + public static readonly DependencyProperty SelectorStyleProperty = DependencyProperty.Register ( + "SelectorStyle", typeof(Style), typeof(GroupEditorControl), new PropertyMetadata (default(Style))); + + public Style SelectorStyle + { + get { return (Style) GetValue (SelectorStyleProperty); } + set { SetValue (SelectorStyleProperty, value); } + } + + protected override void OnSelectionChanged (SelectionChangedEventArgs e) + { + if (e.AddedItems.Count == 0) + return; + + DependencyObject container = ItemContainerGenerator.ContainerFromItem (e.AddedItems[0]); + if (container == null || VisualTreeHelper.GetChildrenCount (container) == 0) + return; + + var presenter = container as ContentPresenter; + if (presenter == null) + throw new InvalidOperationException ("Unexpected visual tree"); + + var toggle = VisualTreeHelper.GetChild (presenter, 0) as ToggleButton; + if (toggle == null) + throw new InvalidOperationException ("Children must be ToggleButton's"); + + toggle.IsChecked = true; + } + + protected override void OnItemsChanged (NotifyCollectionChangedEventArgs e) + { + base.OnItemsChanged (e); + + if (SelectedItem == null && Items.Count > 0) + SelectedIndex = 0; + } + + protected override void OnKeyDown (KeyEventArgs e) + { + base.OnKeyDown (e); + + if (e.Key == Key.Down && SelectedIndex < Items.Count - 1) + SetCurrentValue (SelectedIndexProperty, SelectedIndex + 1); + else if (e.Key == Key.Up && SelectedIndex >= 1) + SetCurrentValue (SelectedIndexProperty, SelectedIndex - 1); + } + + private void OnItemContainerGeneratorStatusChanged (object sender, EventArgs args) + { + if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated) + return; + + if (SelectedItem == null) + SetCurrentValue (SelectedItemProperty, Items[0]); + + for (int i = 0; i < Items.Count; i++) { + var container = ItemContainerGenerator.ContainerFromIndex (i) as ContentPresenter; + if (container == null) + throw new InvalidOperationException ("Unexpected visual tree"); + + container.ApplyTemplate (); + + var child = VisualTreeHelper.GetChild (container, 0); + var toggle = child as ToggleButton; + if (toggle == null) + throw new InvalidOperationException ("Children must be of ToggleButton"); + + if (Equals (SelectedItem, container.DataContext)) + toggle.IsChecked = true; + + toggle.Checked -= OnChoiceSelected; + toggle.Checked += OnChoiceSelected; + } + } + + private void OnChoiceSelected (object sender, RoutedEventArgs e) + { + object selected = ((FrameworkElement) sender).DataContext; + + SetCurrentValue (SelectedItemProperty, selected); + } + } +} diff --git a/Xamarin.PropertyEditing.Windows/HeaderedContextMenu.cs b/Xamarin.PropertyEditing.Windows/HeaderedContextMenu.cs new file mode 100644 index 0000000..21558b6 --- /dev/null +++ b/Xamarin.PropertyEditing.Windows/HeaderedContextMenu.cs @@ -0,0 +1,27 @@ +using System.Windows; +using System.Windows.Controls; + +namespace Xamarin.PropertyEditing.Windows +{ + internal class HeaderedContextMenu + : ContextMenu + { + public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register ( + "Header", typeof (object), typeof (HeaderedContextMenu), new PropertyMetadata (default (object))); + + public object Header + { + get { return (object)GetValue (HeaderProperty); } + set { SetValue (HeaderProperty, value); } + } + + public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register ( + "HeaderTemplate", typeof (DataTemplate), typeof (HeaderedContextMenu), new PropertyMetadata (default (DataTemplate))); + + public DataTemplate HeaderTemplate + { + get { return (DataTemplate)GetValue (HeaderTemplateProperty); } + set { SetValue (HeaderTemplateProperty, value); } + } + } +}
\ No newline at end of file diff --git a/Xamarin.PropertyEditing.Windows/MenuButton.cs b/Xamarin.PropertyEditing.Windows/MenuButton.cs index db7b196..01a9aae 100644 --- a/Xamarin.PropertyEditing.Windows/MenuButton.cs +++ b/Xamarin.PropertyEditing.Windows/MenuButton.cs @@ -26,17 +26,30 @@ namespace Xamarin.PropertyEditing.Windows set { SetValue (HeaderTemplateProperty, value); } } + protected override void OnClick () + { + base.OnClick (); + + if (ContextMenu != null) + OpenMenu(); + } + protected override void OnMouseDown (MouseButtonEventArgs e) { if (ContextMenu == null) return; - ContextMenu.PlacementTarget = this; - ContextMenu.Placement = PlacementMode.Bottom; - ContextMenu.IsOpen = true; + OpenMenu (); e.Handled = true; base.OnMouseDown (e); } + + private void OpenMenu () + { + ContextMenu.PlacementTarget = this; + ContextMenu.Placement = PlacementMode.Bottom; + ContextMenu.IsOpen = true; + } } }
\ No newline at end of file diff --git a/Xamarin.PropertyEditing.Windows/NumericUpDownControl.cs b/Xamarin.PropertyEditing.Windows/NumericUpDownControl.cs index adbda6c..28f7259 100644 --- a/Xamarin.PropertyEditing.Windows/NumericUpDownControl.cs +++ b/Xamarin.PropertyEditing.Windows/NumericUpDownControl.cs @@ -156,8 +156,8 @@ namespace Xamarin.PropertyEditing.Windows base.OnApplyTemplate (); this.textBox = (TextBox) GetTemplateChild ("TextBox"); - this.textBox.TextChanged += OnTextChanged; this.textBox.Text = Value.ToString (); + this.textBox.TextChanged += OnTextChanged; Button up = (Button) GetTemplateChild ("Up"); up.Click += (sender, args) => SetCurrentValue (ValueProperty, GetIncrementedValue (Value)); diff --git a/Xamarin.PropertyEditing.Windows/PropertyButton.cs b/Xamarin.PropertyEditing.Windows/PropertyButton.cs new file mode 100644 index 0000000..5e893c1 --- /dev/null +++ b/Xamarin.PropertyEditing.Windows/PropertyButton.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Shapes; + +namespace Xamarin.PropertyEditing.Windows +{ + [TemplatePart (Name = "Border", Type = typeof(UIElement))] + [TemplatePart (Name = "Indicator", Type = typeof(Rectangle))] + internal class PropertyButton + : Control + { + static PropertyButton () + { + FocusableProperty.OverrideMetadata (typeof (PropertyButton), new FrameworkPropertyMetadata (false)); + DefaultStyleKeyProperty.OverrideMetadata (typeof (PropertyButton), new FrameworkPropertyMetadata (typeof (PropertyButton))); + } + + public static readonly DependencyProperty CanSetCustomExpressionProperty = DependencyProperty.Register ( + "CanSetCustomExpression", typeof (bool), typeof (PropertyButton), new PropertyMetadata (default (bool))); + + public bool CanSetCustomExpression + { + get { return (bool)GetValue (CanSetCustomExpressionProperty); } + set { SetValue (CanSetCustomExpressionProperty, value); } + } + + public static readonly DependencyProperty SystemResourcesSourceProperty = DependencyProperty.Register ( + "SystemResourcesSource", typeof (IEnumerable), typeof (PropertyButton), new PropertyMetadata (default (IEnumerable))); + + public IEnumerable SystemResourcesSource + { + get { return (IEnumerable)GetValue (SystemResourcesSourceProperty); } + set { SetValue (SystemResourcesSourceProperty, value); } + } + + public static readonly DependencyProperty SystemResourceNamePathProperty = DependencyProperty.Register ( + "SystemResourceNamePath", typeof (string), typeof (PropertyButton), new PropertyMetadata ("Name")); + + public string SystemResourceNamePath + { + get { return (string)GetValue (SystemResourceNamePathProperty); } + set { SetValue (SystemResourceNamePathProperty, value); } + } + + public static readonly DependencyProperty ValueSourceProperty = DependencyProperty.Register ( + "ValueSource", typeof (ValueSource), typeof (PropertyButton), new PropertyMetadata (ValueSource.Default, (o, args) => ((PropertyButton)o).OnValueSourceChanged ((ValueSource)args.NewValue))); + + public ValueSource ValueSource + { + get { return (ValueSource)GetValue (ValueSourceProperty); } + set { SetValue (ValueSourceProperty, value); } + } + + public static readonly DependencyProperty MenuTemplateProperty = DependencyProperty.Register ( + "MenuTemplate", typeof (DataTemplate), typeof (PropertyButton), new PropertyMetadata (default (DataTemplate))); + + public DataTemplate MenuTemplate + { + get { return (DataTemplate)GetValue (MenuTemplateProperty); } + set { SetValue (MenuTemplateProperty, value); } + } + + public override void OnApplyTemplate () + { + base.OnApplyTemplate (); + + this.indicator = (Rectangle)GetTemplateChild ("Indicator"); + if (this.indicator == null) + throw new InvalidOperationException ("PropertyButton template Missing part Indicator"); + + var border = GetTemplateChild ("Border") as UIElement; + if (border == null) + throw new InvalidOperationException ("PropertyButton template Missing part Border"); + + border.MouseDown += OnBorderMouseDown; + + OnValueSourceChanged (ValueSource); + } + + private Rectangle indicator; + private ContextMenu menu; + + private MenuItem systemResources; + + private void OnBorderMouseDown (object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton != MouseButton.Left) + return; + + if (this.menu == null) { + this.menu = MenuTemplate?.LoadContent () as ContextMenu; + if (this.menu == null) + return; + + this.menu.PlacementTarget = this.indicator; + this.menu.DataContext = DataContext; + } + + this.menu.IsOpen = true; + e.Handled = true; + } + + private void OnValueSourceChanged (ValueSource source) + { + if (this.indicator == null) + return; + + string brush = null; + + switch (source) { + case ValueSource.Local: + brush = "PropertyLocalValueBrush"; + break; + case ValueSource.Binding: + brush = "PropertyBoundValueBrush"; + break; + case ValueSource.Inherited: + case ValueSource.DefaultStyle: + case ValueSource.Style: + case ValueSource.Resource: + brush = "PropertyResourceBrush"; + break; + + case ValueSource.Default: + this.indicator.ClearValue (Shape.FillProperty); + return; + } + + this.indicator.SetResourceReference (Shape.FillProperty, brush); + } + } +}
\ No newline at end of file diff --git a/Xamarin.PropertyEditing.Windows/PropertyEditorPanel.cs b/Xamarin.PropertyEditing.Windows/PropertyEditorPanel.cs index 3598e3d..d3b43ad 100644 --- a/Xamarin.PropertyEditing.Windows/PropertyEditorPanel.cs +++ b/Xamarin.PropertyEditing.Windows/PropertyEditorPanel.cs @@ -43,6 +43,15 @@ namespace Xamarin.PropertyEditing.Windows set { SetValue (EditorProviderProperty, value); } } + public static readonly DependencyProperty TargetPlatformProperty = DependencyProperty.Register ( + "TargetPlatform", typeof(TargetPlatform), typeof(PropertyEditorPanel), new PropertyMetadata (TargetPlatform.Default)); + + public TargetPlatform TargetPlatform + { + get { return (TargetPlatform) GetValue (TargetPlatformProperty); } + set { SetValue (TargetPlatformProperty, value); } + } + private static readonly DependencyPropertyKey SelectedItemsPropertyKey = DependencyProperty.RegisterReadOnly ( nameof(SelectedItems), typeof(IList), typeof(PropertyEditorPanel), new PropertyMetadata (default(IList))); @@ -149,7 +158,7 @@ namespace Xamarin.PropertyEditing.Windows if (this.vm != null) this.vm.PropertyChanged -= OnVmPropertyChanged; - this.root.DataContext = this.vm = (EditorProvider != null) ? new PanelViewModel (EditorProvider) : null; + this.root.DataContext = this.vm = (EditorProvider != null) ? new PanelViewModel (EditorProvider, TargetPlatform) : null; if (this.vm != null) this.vm.PropertyChanged += OnVmPropertyChanged; @@ -168,9 +177,9 @@ namespace Xamarin.PropertyEditing.Windows Binding itemsSource; if (newMode == PropertyArrangeMode.Name) - itemsSource = new Binding ("ArrangedProperties[0]"); + itemsSource = new Binding ("ArrangedEditors[0]"); else - itemsSource = new Binding ("ArrangedProperties"); + itemsSource = new Binding ("ArrangedEditors"); this.items.SetBinding (ItemsControl.ItemsSourceProperty, itemsSource); } diff --git a/Xamarin.PropertyEditing.Windows/PropertyMenuItemContainerStyleSelector.cs b/Xamarin.PropertyEditing.Windows/PropertyMenuItemContainerStyleSelector.cs new file mode 100644 index 0000000..6d8377c --- /dev/null +++ b/Xamarin.PropertyEditing.Windows/PropertyMenuItemContainerStyleSelector.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; + +namespace Xamarin.PropertyEditing.Windows +{ + internal class PropertyMenuItemContainerStyleSelector + : StyleSelector + { + public Style MenuItemStyle + { + get; + set; + } + + public Style SeparatorStyle + { + get; + set; + } + + public override Style SelectStyle (object item, DependencyObject container) + { + if (container is MenuItem) + return MenuItemStyle; + if (container is Separator) + return SeparatorStyle ?? base.SelectStyle (item, container); + + return base.SelectStyle (item, container); + } + } +} diff --git a/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml b/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml index 6e0b87c..49f912a 100644 --- a/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml +++ b/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml @@ -1,10 +1,11 @@ <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:local="clr-namespace:Xamarin.PropertyEditing.Windows" - xmlns:options="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" - xmlns:vm="clr-namespace:Xamarin.PropertyEditing.ViewModels;assembly=Xamarin.PropertyEditing" - xmlns:theme="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" - xmlns:prop="clr-namespace:Xamarin.PropertyEditing.Properties;assembly=Xamarin.PropertyEditing"> + xmlns:local="clr-namespace:Xamarin.PropertyEditing.Windows" + xmlns:options="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" + xmlns:vm="clr-namespace:Xamarin.PropertyEditing.ViewModels;assembly=Xamarin.PropertyEditing" + xmlns:theme="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" + xmlns:prop="clr-namespace:Xamarin.PropertyEditing.Properties;assembly=Xamarin.PropertyEditing" + xmlns:system="clr-namespace:System;assembly=mscorlib"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="MenuButtonStyle.xaml" /> @@ -34,6 +35,85 @@ </Setter> </Style> + <Style x:Key="GroupSelectionItem" TargetType="RadioButton"> + <Setter Property="OverridesDefaultStyle" Value="True" /> + <Setter Property="Background" Value="Transparent" /> + <Setter Property="BorderThickness" Value="1" /> + <Setter Property="Padding" Value="20,2,4,2" /> + <Setter Property="MinHeight" Value="20" /> + <Setter Property="Template"> + <Setter.Value> + <ControlTemplate TargetType="RadioButton"> + <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}"> + <ContentPresenter /> + </Border> + <ControlTemplate.Triggers> + <Trigger Property="IsMouseOver" Value="True"> + <Setter Property="Background" Value="{DynamicResource ListItemMouseOverBackgroundBrush}" /> + <Setter Property="BorderBrush" Value="{DynamicResource ListItemMouseOverBorderBrush}" /> + <Setter Property="Foreground" Value="{DynamicResource ListItemMouseOverForegroundBrush}"/> + </Trigger> + <Trigger Property="IsChecked" Value="True"> + <Setter Property="Background" Value="{DynamicResource ListItemSelectedBackgroundBrush}" /> + <Setter Property="BorderBrush" Value="{DynamicResource ListItemSelectedBorderBrush}" /> + <Setter Property="Foreground" Value="{DynamicResource ListItemSelectedForegroundBrush}" /> + </Trigger> + </ControlTemplate.Triggers> + </ControlTemplate> + </Setter.Value> + </Setter> + </Style> + <Style TargetType="{x:Type local:GroupEditorControl}"> + <Setter Property="Margin" Value="-19,0,0,0" /> + <Setter Property="SelectorStyle"> + <Setter.Value> + <Style TargetType="Border"> + <Setter Property="BorderBrush" Value="{DynamicResource ListBackgroundBrush}" /> + <Setter Property="Background" Value="{DynamicResource ListBackgroundBrush}" /> + </Style> + </Setter.Value> + </Setter> + <Setter Property="ItemsSource" Value="{Binding Properties}" /> + <Setter Property="ItemTemplate"> + <Setter.Value> + <DataTemplate DataType="vm:PropertyViewModel"> + <RadioButton GroupName="GroupSelection" Style="{StaticResource GroupSelectionItem}" Content="{Binding}"> + <RadioButton.ContentTemplate> + <DataTemplate> + <local:PropertyPresenter Label="{Binding Name,Mode=OneTime}" Content="{Binding Mode=OneTime}" ContentTemplateSelector="{StaticResource PropertyEditorSelector}" /> + </DataTemplate> + </RadioButton.ContentTemplate> + </RadioButton> + </DataTemplate> + </Setter.Value> + </Setter> + <Setter Property="ContentTemplateSelector" Value="{StaticResource PropertyEditorSelector}" /> + <Setter Property="ItemContainerStyle"> + <Setter.Value> + <Style TargetType="ContentPresenter"> + <Setter Property="Width" Value="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=ItemsPresenter}}" /> + </Style> + </Setter.Value> + </Setter> + <Setter Property="Template"> + <Setter.Value> + <ControlTemplate TargetType="local:GroupEditorControl"> + <Grid> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + </Grid.RowDefinitions> + + <Border Style="{TemplateBinding SelectorStyle}" Grid.Row="0"> + <ItemsPresenter Grid.IsSharedSizeScope="True" ScrollViewer.HorizontalScrollBarVisibility="Disabled" /> + </Border> + <ContentPresenter Grid.Row="1" Name="EditorPresenter" Content="{TemplateBinding SelectedItem}" ContentTemplate="{TemplateBinding ContentTemplate}" ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}" /> + </Grid> + </ControlTemplate> + </Setter.Value> + </Setter> + </Style> + <Style x:Key="ChoiceControlItem" BasedOn="{StaticResource {x:Type RadioButton}}" TargetType="RadioButton"> <Setter Property="Background" Value="{DynamicResource ToggleItemBackgroundBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource ToggleItemBorderBrush}" /> @@ -335,6 +415,57 @@ </Setter> </Style> + <system:Double x:Key="MenuOffset">3</system:Double> + + <Rectangle x:Shared="False" x:Key="LiteralMarker" Stroke="{DynamicResource PropertyButtonBorderBrush}" Fill="{DynamicResource LiteralMarkerBrush}" Height="12" Width="12" StrokeThickness="1" /> + + <Style TargetType="local:PropertyButton"> + <Setter Property="Background" Value="Transparent" /> + <Setter Property="BorderThickness" Value="0" /> + <Setter Property="Height" Value="17" /> + <Setter Property="Width" Value="11" /> + <Setter Property="SnapsToDevicePixels" Value="True" /> + <Setter Property="MenuTemplate"> + <Setter.Value> + <DataTemplate> + <local:HeaderedContextMenu Header="{Binding Property.Name,Mode=OneTime}" StaysOpen="True" Placement="Left" HorizontalOffset="{StaticResource MenuOffset}" VerticalOffset="{StaticResource MenuOffset}"> + <!--<MenuItem x:Name="CustomExpressionItem" Header="{x:Static prop:Resources.CustomExpressionEllipsis}" Icon="{StaticResource LiteralMarker}" /> + <Separator />!--> + <MenuItem Header="{x:Static prop:Resources.Reset}" Icon="{StaticResource LiteralMarker}" Command="{Binding ClearValueCommand,Mode=OneTime}" /> + <!--<MenuItem Header="{x:Static prop:Resources.ConvertToLocalValue}" Icon="{StaticResource LiteralMarker}" Command="{Binding ConvertToLocalValueCommand,Mode=OneTime}" /> + <Separator /> + <MenuItem Header="{x:Static p:Resources.LocalResource}" /> + <MenuItem Header="{x:Static p:Resources.SystemResource}" /> + <MenuItem Header="{x:Static p:Resources.EditResourceEllipse}" /> + <MenuItem Header="{x:Static p:Resources.ConvertToNewResourceEllipse}" /> + <Separator /> + <MenuItem Header="{x:Static p:Resources.CreateDataBindingEllipse}" /> + <MenuItem Header="{x:Static p:Resources.TemplateBinding}" /> + <Separator /> + <MenuItem Header="{x:Static p:Resources.RecordCurrentValue}" /> + <MenuItem Header="{x:Static p:Resources.GoToSource}" />!--> + </local:HeaderedContextMenu> + </DataTemplate> + </Setter.Value> + </Setter> + <Setter Property="Template"> + <Setter.Value> + <ControlTemplate TargetType="local:PropertyButton"> + <Border Name="Border" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}"> + <Rectangle Name="Indicator" Fill="{DynamicResource PanelBackgroundBrush}" Stroke="{DynamicResource PropertyButtonBorderBrush}" Height="7" Width="7" /> + </Border> + </ControlTemplate> + </Setter.Value> + </Setter> + <Style.Triggers> + <Trigger Property="IsMouseOver" Value="True"> + <Trigger.Setters> + <Setter Property="Background" Value="{DynamicResource ToggleItemMouseOverBackgroundBrush}" /> + </Trigger.Setters> + </Trigger> + </Style.Triggers> + </Style> + <Style TargetType="local:BrushEditorControl"> <Setter Property="Template"> <Setter.Value> @@ -938,15 +1069,23 @@ <Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <Grid> <Grid.ColumnDefinitions> - <ColumnDefinition MinWidth="100" MaxWidth="180" Width="0.4*" /> + <ColumnDefinition Name="labelColumn" MinWidth="100" MaxWidth="180" Width="0.4*" /> <ColumnDefinition MinWidth="134" Width="0.6*" /> - <ColumnDefinition Width="12" /> + <ColumnDefinition Name="propertyButtonColumn" Width="12" /> </Grid.ColumnDefinitions> <TextBlock Name="Label" Grid.Column="0" Text="{TemplateBinding Label}" ToolTip="{TemplateBinding Label}" Height="16" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" /> <ContentPresenter Grid.Column="1" Margin="4,2,0,2" IsEnabled="{Binding Property.CanWrite}" /> + <local:PropertyButton Grid.Column="2" ValueSource="{Binding ValueSource}" /> </Grid> </Border> + <ControlTemplate.Triggers> + <Trigger Property="Label" Value="{x:Null}"> + <Setter TargetName="labelColumn" Property="MinWidth" Value="0" /> + <Setter TargetName="labelColumn" Property="Width" Value="0" /> + <Setter TargetName="propertyButtonColumn" Property="Width" Value="0" /> + </Trigger> + </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> @@ -999,7 +1138,7 @@ <Setter Property="Foreground" Value="{DynamicResource LabelForegroundBrush}"/> </Style> - <Style TargetType="MenuItem"> + <Style x:Key="HeaderedMenuItem" TargetType="MenuItem"> <Setter Property="OverridesDefaultStyle" Value="True" /> <Setter Property="HorizontalContentAlignment" Value="{Binding Path=HorizontalContentAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type ItemsControl}}}"/> <Setter Property="VerticalContentAlignment" Value="{Binding Path=VerticalContentAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type ItemsControl}}}"/> @@ -1007,13 +1146,13 @@ <Setter Property="Background" Value="Transparent" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderBrush" Value="Transparent" /> - <Setter Property="Padding" Value="0,2,0,2" /> <Setter Property="Stylus.IsFlicksEnabled" Value="False" /> + <Setter Property="Padding" Value="21,0,0,0" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="MenuItem"> <Border x:Name="templateRoot" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True"> - <Grid> + <Grid Margin="{TemplateBinding Padding}"> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth="17" SharedSizeGroup="MenuItemIconColumnGroup" Width="Auto"/> <ColumnDefinition Width="*"/> @@ -1022,7 +1161,7 @@ </Grid.ColumnDefinitions> <ContentPresenter x:Name="Icon" Grid.Column="0" ContentSource="Icon" Content="{TemplateBinding Icon}" Margin="4,0,6,0" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center" /> <Path x:Name="GlyphPanel" Data="M0,2L0,4.8 2.5,7.4 7.1,2.8 7.1,0 2.5,4.6z" Grid.Column="0" Margin="4,0,6,0" Visibility="Hidden" Fill="{TemplateBinding Foreground}" FlowDirection="LeftToRight" VerticalAlignment="Center" /> - <ContentPresenter x:Name="menuHeaderContainer" Grid.Column="1" ContentSource="Header" Content="{TemplateBinding Header}" ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" ContentTemplate="{TemplateBinding HeaderTemplate}" ContentStringFormat="{TemplateBinding HeaderStringFormat}" HorizontalAlignment="Left" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/> + <ContentPresenter x:Name="menuHeaderContainer" Grid.Column="1" ContentSource="Header" Content="{TemplateBinding Header}" ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" ContentTemplate="{TemplateBinding HeaderTemplate}" Margin="0,2,0,2" ContentStringFormat="{TemplateBinding HeaderStringFormat}" HorizontalAlignment="Left" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/> <TextBlock x:Name="menuGestureText" Grid.Column="2" Margin="5,2,0,2" DockPanel.Dock="Right" Text="{TemplateBinding InputGestureText}" VerticalAlignment="Center"/> </Grid> </Border> @@ -1051,6 +1190,7 @@ </Style> <Style TargetType="{x:Type ContextMenu}"> + <Setter Property="Foreground" Value="{DynamicResource ListItemForegroundBrush}" /> <Setter Property="OverridesDefaultStyle" Value="True" /> <Setter Property="FontFamily" Value="{DynamicResource {x:Static SystemFonts.MessageFontFamilyKey}}" /> <Setter Property="FontSize" Value="{DynamicResource {x:Static SystemFonts.MessageFontSizeKey}}" /> @@ -1091,6 +1231,55 @@ </Setter> </Style> + <Style TargetType="{x:Type local:HeaderedContextMenu}" BasedOn="{StaticResource {x:Type ContextMenu}}"> + <Setter Property="Padding" Value="2,0,2,14" /> + <Setter Property="Template"> + <Setter.Value> + <ControlTemplate TargetType="{x:Type local:HeaderedContextMenu}"> + <theme:SystemDropShadowChrome x:Name="shadow" Color="Transparent"> + <Border x:Name="Border" Padding="0,2,0,4" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}"> + <StackPanel> + <Label Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}" Margin="13,3,12,2" Padding="0" Foreground="{TemplateBinding Foreground}" /> + <ItemsPresenter Margin="{TemplateBinding Padding}" HorizontalAlignment="Stretch" KeyboardNavigation.DirectionalNavigation="Cycle" /> + </StackPanel> + </Border> + </theme:SystemDropShadowChrome> + + <ControlTemplate.Triggers> + <MultiDataTrigger> + <MultiDataTrigger.Conditions> + <Condition Binding="{Binding HasDropShadow,RelativeSource={RelativeSource Self}}" Value="True" /> + <Condition Binding="{Binding AreGradientsAllowed,Source={x:Static local:HostEnvironment.Current}}" Value="True" /> + </MultiDataTrigger.Conditions> + + <Setter TargetName="shadow" Property="Margin" Value="0,0,5,5" /> + <Setter TargetName="shadow" Property="Color" Value="{DynamicResource DropShadowBackgroundColor}" /> + </MultiDataTrigger> + </ControlTemplate.Triggers> + </ControlTemplate> + </Setter.Value> + </Setter> + <Setter Property="ItemContainerStyleSelector"> + <Setter.Value> + <local:PropertyMenuItemContainerStyleSelector MenuItemStyle="{StaticResource HeaderedMenuItem}"> + <local:PropertyMenuItemContainerStyleSelector.SeparatorStyle> + <Style TargetType="Separator" BasedOn="{StaticResource {x:Type Separator}}"> + <Setter Property="Margin" Value="4,2,0,2" /> + <Setter Property="Background" Value="{DynamicResource MenuSeparatorBrush}" /> + <Setter Property="Template"> + <Setter.Value> + <ControlTemplate TargetType="Separator"> + <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" /> + </ControlTemplate> + </Setter.Value> + </Setter> + </Style> + </local:PropertyMenuItemContainerStyleSelector.SeparatorStyle> + </local:PropertyMenuItemContainerStyleSelector> + </Setter.Value> + </Setter> + </Style> + <Style x:Key="ArrangeMenuItem" TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}"> <Setter Property="Header" Value="{Binding ArrangeMode,Mode=OneTime}" /> <Setter Property="IsCheckable" Value="True" /> @@ -1109,6 +1298,20 @@ <Setter Property="Foreground" Value="{DynamicResource InputForegroundBrush}" /> </Style> + <Style x:Key="RepeatButtonTransparent" TargetType="{x:Type RepeatButton}"> + <Setter Property="OverridesDefaultStyle" Value="True" /> + <Setter Property="IsTabStop" Value="False" /> + <Setter Property="Focusable" Value="False" /> + <Setter Property="Background" Value="Transparent" /> + <Setter Property="Template"> + <Setter.Value> + <ControlTemplate TargetType="RepeatButton"> + <Rectangle Height="{TemplateBinding Height}" Width="{TemplateBinding Width}" Fill="{TemplateBinding Background}" /> + </ControlTemplate> + </Setter.Value> + </Setter> + </Style> + <Style x:Key="FocusVisual"> <Setter Property="Control.Template"> <Setter.Value> diff --git a/Xamarin.PropertyEditing.Windows/Themes/VS.Dark.xaml b/Xamarin.PropertyEditing.Windows/Themes/VS.Dark.xaml index e138c08..6622f4b 100644 --- a/Xamarin.PropertyEditing.Windows/Themes/VS.Dark.xaml +++ b/Xamarin.PropertyEditing.Windows/Themes/VS.Dark.xaml @@ -35,8 +35,22 @@ <SolidColorBrush x:Key="ListItemHighlightBorderBrush">#72555555</SolidColorBrush> <SolidColorBrush x:Key="ListItemHighlightBackgroundBrush">#3E3E40</SolidColorBrush> <SolidColorBrush x:Key="ListItemDisabledForegroundBrush">#656565</SolidColorBrush> + <SolidColorBrush x:Key="ListItemSelectedBackgroundBrush">#FF007ACC</SolidColorBrush> + <SolidColorBrush x:Key="ListItemSelectedBorderBrush">#FF3399FF</SolidColorBrush> + <SolidColorBrush x:Key="ListItemSelectedForegroundBrush">#FFFFFFFF</SolidColorBrush> + <SolidColorBrush x:Key="ListItemMouseOverBackgroundBrush">#FF3E3E40</SolidColorBrush> + <SolidColorBrush x:Key="ListItemMouseOverBorderBrush">#72555555</SolidColorBrush> + <SolidColorBrush x:Key="ListItemMouseOverForegroundBrush">#FFF1F1F1</SolidColorBrush> <SolidColorBrush x:Key="MenuSeparatorBrush">#333337</SolidColorBrush> + <SolidColorBrush x:Key="LiteralMarkerBrush">#55000000</SolidColorBrush> + <SolidColorBrush x:Key="PropertyMenuDotIconBorderBrush">#FF999999</SolidColorBrush> + <SolidColorBrush x:Key="PropertyButtonBackgroundBrush">#F2F2F6</SolidColorBrush> + <SolidColorBrush x:Key="PropertyButtonBorderBrush">#717171</SolidColorBrush> + <SolidColorBrush x:Key="PropertyLocalValueBrush">#F1F1F1</SolidColorBrush> + <SolidColorBrush x:Key="PropertyBoundValueBrush">#FFCF00</SolidColorBrush> + <SolidColorBrush x:Key="PropertyResourceBrush">#00FF00</SolidColorBrush> + <SolidColorBrush x:Key="SearchControlBackgroundBrush">#333337</SolidColorBrush> <SolidColorBrush x:Key="SearchControlBorderBrush">#3F3F46</SolidColorBrush> <SolidColorBrush x:Key="SearchControlForegroundBrush">#D0D2D3</SolidColorBrush> diff --git a/Xamarin.PropertyEditing.Windows/Themes/VS.Light.xaml b/Xamarin.PropertyEditing.Windows/Themes/VS.Light.xaml index b7665de..35b1e8e 100644 --- a/Xamarin.PropertyEditing.Windows/Themes/VS.Light.xaml +++ b/Xamarin.PropertyEditing.Windows/Themes/VS.Light.xaml @@ -40,8 +40,16 @@ <SolidColorBrush x:Key="ListItemHighlightBackgroundBrush">#C9DEF5</SolidColorBrush> <SolidColorBrush x:Key="ListItemHighlightBorderBrush">#C9DEF5</SolidColorBrush> <SolidColorBrush x:Key="ListItemDisabledForegroundBrush">#FFA2A4A5</SolidColorBrush> + <SolidColorBrush x:Key="ListItemSelectedBackgroundBrush">#FF007ACC</SolidColorBrush> + <SolidColorBrush x:Key="ListItemSelectedBorderBrush">#FFCCCED8</SolidColorBrush> + <SolidColorBrush x:Key="ListItemSelectedForegroundBrush">#FFFFFFFF</SolidColorBrush> + <SolidColorBrush x:Key="ListItemMouseOverBackgroundBrush">#FFC9DEF5</SolidColorBrush> + <SolidColorBrush x:Key="ListItemMouseOverBorderBrush">#FFC9DEF5</SolidColorBrush> + <SolidColorBrush x:Key="ListItemMouseOverForegroundBrush">#FF1E1E1E</SolidColorBrush> <SolidColorBrush x:Key="MenuSeparatorBrush">#E0E3E6</SolidColorBrush> + <SolidColorBrush x:Key="LiteralMarkerBrush">#55000000</SolidColorBrush> + <SolidColorBrush x:Key="PropertyMenuDotIconBorderBrush">#FF717171</SolidColorBrush> <SolidColorBrush x:Key="PropertyButtonBackgroundBrush">#F2F2F6</SolidColorBrush> <SolidColorBrush x:Key="PropertyButtonBorderBrush">#717171</SolidColorBrush> <SolidColorBrush x:Key="PropertyLocalValueBrush">#1E1E1E</SolidColorBrush> diff --git a/Xamarin.PropertyEditing.Windows/Xamarin.PropertyEditing.Windows.csproj b/Xamarin.PropertyEditing.Windows/Xamarin.PropertyEditing.Windows.csproj index 8fac80f..818159a 100644 --- a/Xamarin.PropertyEditing.Windows/Xamarin.PropertyEditing.Windows.csproj +++ b/Xamarin.PropertyEditing.Windows/Xamarin.PropertyEditing.Windows.csproj @@ -71,6 +71,8 @@ <Compile Include="EditorPropertySelector.cs" /> <Compile Include="EnumEditorControl.cs" /> <Compile Include="HexColorConverter.cs" /> + <Compile Include="HeaderedContextMenu.cs" /> + <Compile Include="GroupEditorControl.cs" /> <Compile Include="HostEnvironment.cs" /> <Compile Include="MenuButton.cs" /> <Compile Include="NumericEditorControl.cs" /> @@ -81,8 +83,10 @@ <Compile Include="PointEditorControl.cs" /> <Compile Include="PointHelper.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="PropertyButton.cs" /> <Compile Include="PropertyEditorControl.cs" /> <Compile Include="PropertyEditorPanel.cs" /> + <Compile Include="PropertyMenuItemContainerStyleSelector.cs" /> <Compile Include="PropertyPresenter.cs" /> <Compile Include="ShadeEditorControl.cs" /> <Compile Include="SizeEditorControl.cs" /> diff --git a/Xamarin.PropertyEditing/IGroupingList.cs b/Xamarin.PropertyEditing/IGroupingList.cs index 45d655b..ee7cdb4 100644 --- a/Xamarin.PropertyEditing/IGroupingList.cs +++ b/Xamarin.PropertyEditing/IGroupingList.cs @@ -28,10 +28,5 @@ namespace Xamarin.PropertyEditing { get; } - - public void QuietClear() - { - Items.Clear (); - } } } diff --git a/Xamarin.PropertyEditing/ObservableLookup.cs b/Xamarin.PropertyEditing/ObservableLookup.cs index 55a9778..5221989 100644 --- a/Xamarin.PropertyEditing/ObservableLookup.cs +++ b/Xamarin.PropertyEditing/ObservableLookup.cs @@ -87,7 +87,7 @@ namespace Xamarin.PropertyEditing if (key == null) { grouping = nullGrouping; if (grouping.Count == 0) - OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, grouping, this.groupings.Count)); + OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, (object)grouping, this.groupings.Count)); } else if (!this.groupings.TryGetValue (key, out grouping)) { if (!ReuseGroups || !this.oldGroups.TryRemove (key, out grouping)) grouping = new ObservableGrouping<TKey, TElement> (key); @@ -110,7 +110,7 @@ namespace Xamarin.PropertyEditing grouping = nullGrouping; grouping.AddRange (elements); if (wasEmpty) - OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, grouping, this.groupings.Count)); + OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, (object)grouping, this.groupings.Count)); } else if (!this.groupings.TryGetValue (key, out grouping)) { if (!ReuseGroups || !this.oldGroups.TryRemove (key, out grouping)) grouping = new ObservableGrouping<TKey, TElement> (key); @@ -128,7 +128,7 @@ namespace Xamarin.PropertyEditing bool wasEmpty = this.nullGrouping.Count == 0; this.nullGrouping.AddRange (grouping); if (wasEmpty) - OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, grouping, this.groupings.Count)); + OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, (object)grouping, this.groupings.Count)); return; } @@ -174,7 +174,8 @@ namespace Xamarin.PropertyEditing if (group.Remove (element)) { if (group.Count == 0) { - Remove (key); + if (!Remove (key) && key == null) + OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Remove, (object)this.nullGrouping, this.groupings.Count)); } return true; @@ -193,9 +194,9 @@ namespace Xamarin.PropertyEditing { if (key == null) { bool removed = (this.nullGrouping.Count > 0); - this.nullGrouping.QuietClear(); + this.nullGrouping.Clear(); if (removed) - OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Remove, this.nullGrouping, this.groupings.Count)); + OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Remove, (object)this.nullGrouping, this.groupings.Count)); return removed; } @@ -205,11 +206,11 @@ namespace Xamarin.PropertyEditing var g = this.groupings[index]; this.groupings.Remove (key); if (ReuseGroups) { - g.QuietClear (); + g.Clear (); this.oldGroups.Add (key, g); } - OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Remove, g, index)); + OnCollectionChanged (new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Remove, (object)g, index)); return true; } @@ -218,12 +219,12 @@ namespace Xamarin.PropertyEditing public void Clear () { - this.nullGrouping?.QuietClear(); + this.nullGrouping?.Clear(); if (ReuseGroups) { foreach (var g in this.groupings.Values) { this.oldGroups.Add (g.Key, g); - g.QuietClear (); + g.Clear (); } } @@ -260,7 +261,16 @@ namespace Xamarin.PropertyEditing get { return this.groupings.Count + ((this.nullGrouping != null && this.nullGrouping.Count > 0) ? 1 : 0); } } - IGroupingList<TKey, TElement> IReadOnlyList<IGroupingList<TKey, TElement>>.this[int index] => (IGroupingList<TKey, TElement>)this[index]; + IGroupingList<TKey, TElement> IReadOnlyList<IGroupingList<TKey, TElement>>.this [int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException (nameof(index)); + + return (IGroupingList<TKey, TElement>)this[index]; + } + } /// <summary> /// Gets the elements for <paramref name="key"/>. diff --git a/Xamarin.PropertyEditing/OrderedDictionary.cs b/Xamarin.PropertyEditing/OrderedDictionary.cs index 2c607a1..d5dec71 100644 --- a/Xamarin.PropertyEditing/OrderedDictionary.cs +++ b/Xamarin.PropertyEditing/OrderedDictionary.cs @@ -34,7 +34,7 @@ using System.Collections.ObjectModel; namespace Cadenza.Collections { internal class OrderedDictionary<TKey, TValue> - : IDictionary<TKey, TValue>, IList<KeyValuePair<TKey, TValue>> + : IDictionary<TKey, TValue>, IList<KeyValuePair<TKey, TValue>>, IReadOnlyDictionary<TKey, TValue> { public OrderedDictionary () : this (0) @@ -106,6 +106,16 @@ namespace Cadenza.Collections } } + IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys + { + get { return Keys; } + } + + IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values + { + get { return Values; } + } + /// <summary> /// Gets the value at the specified index. /// </summary> diff --git a/Xamarin.PropertyEditing/Properties/Resources.Designer.cs b/Xamarin.PropertyEditing/Properties/Resources.Designer.cs index 095c821..8d2faaa 100644 --- a/Xamarin.PropertyEditing/Properties/Resources.Designer.cs +++ b/Xamarin.PropertyEditing/Properties/Resources.Designer.cs @@ -187,6 +187,24 @@ namespace Xamarin.PropertyEditing.Properties { } /// <summary> + /// Looks up a localized string similar to Convert to Local Value. + /// </summary> + public static string ConvertToLocalValue { + get { + return ResourceManager.GetString("ConvertToLocalValue", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Custom Expression…. + /// </summary> + public static string CustomExpressionEllipsis { + get { + return ResourceManager.GetString("CustomExpressionEllipsis", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Event handlers for the selected element. /// </summary> public static string EventHandlersSelectedElement { @@ -482,6 +500,14 @@ namespace Xamarin.PropertyEditing.Properties { return ResourceManager.GetString("SolidBrush", resourceCulture); } } + /// <summary> + /// Looks up a localized string similar to Reset. + /// </summary> + public static string Reset { + get { + return ResourceManager.GetString("Reset", resourceCulture); + } + } /// <summary> /// Looks up a localized string similar to Type. diff --git a/Xamarin.PropertyEditing/Properties/Resources.resx b/Xamarin.PropertyEditing/Properties/Resources.resx index 1ecae03..7142701 100644 --- a/Xamarin.PropertyEditing/Properties/Resources.resx +++ b/Xamarin.PropertyEditing/Properties/Resources.resx @@ -173,6 +173,12 @@ <value>Cyan / Magenta / Yellow / Black</value> <comment>CMYK help text</comment> </data> + <data name="ConvertToLocalValue" xml:space="preserve"> + <value>Convert to Local Value</value> + </data> + <data name="CustomExpressionEllipsis" xml:space="preserve"> + <value>Custom Expression…</value> + </data> <data name="EventHandlersSelectedElement" xml:space="preserve"> <value>Event handlers for the selected element</value> </data> @@ -281,6 +287,9 @@ <value>S</value> <comment>Saturation initial used as saturation textbox label</comment> </data> + <data name="Reset" xml:space="preserve"> + <value>Reset</value> + </data> <data name="Type" xml:space="preserve"> <value>Type</value> </data> diff --git a/Xamarin.PropertyEditing/TargetPlatform.cs b/Xamarin.PropertyEditing/TargetPlatform.cs new file mode 100644 index 0000000..496acc4 --- /dev/null +++ b/Xamarin.PropertyEditing/TargetPlatform.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +using Xamarin.PropertyEditing.Drawing; + +namespace Xamarin.PropertyEditing +{ + /// <summary> + /// Represents a target platform by defining top level feature support and presentation preferences + /// </summary> + public sealed class TargetPlatform + { + /// <summary> + /// Gets a dictionary defining the property types will be grouped into a single editor and their groups resource name. + /// </summary> + public IReadOnlyDictionary<Type, string> GroupedTypes + { + get; + set; + } + + public static readonly TargetPlatform Default = new TargetPlatform { + GroupedTypes = new Dictionary<Type, string> { + { typeof(CommonBrush), "Brush" } + } + }; + } +} diff --git a/Xamarin.PropertyEditing/ValueSource.cs b/Xamarin.PropertyEditing/ValueSource.cs index 43b430b..eef7350 100644 --- a/Xamarin.PropertyEditing/ValueSource.cs +++ b/Xamarin.PropertyEditing/ValueSource.cs @@ -12,6 +12,8 @@ namespace Xamarin.PropertyEditing Binding = 2, Resource = 3, Style = 4, + Inherited = 5, + DefaultStyle = 6, } [Flags] diff --git a/Xamarin.PropertyEditing/ViewModels/EditorViewModel.cs b/Xamarin.PropertyEditing/ViewModels/EditorViewModel.cs index 84e5896..89bbc80 100644 --- a/Xamarin.PropertyEditing/ViewModels/EditorViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/EditorViewModel.cs @@ -1,11 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Xamarin.PropertyEditing.ViewModels { @@ -25,6 +21,16 @@ namespace Xamarin.PropertyEditing.ViewModels //TODO: Property is being set after the editors added trickle down since its in the subclass } + public abstract string Category + { + get; + } + + public abstract string Name + { + get; + } + /// <summary> /// Gets if the property's value can not be determined because multiple editors disagree. /// </summary> @@ -44,7 +50,6 @@ namespace Xamarin.PropertyEditing.ViewModels public ICollection<IObjectEditor> Editors { get; - private set; } protected static AsyncWorkQueue AsyncWork diff --git a/Xamarin.PropertyEditing/ViewModels/EventViewModel.cs b/Xamarin.PropertyEditing/ViewModels/EventViewModel.cs index c725619..179a7c4 100644 --- a/Xamarin.PropertyEditing/ViewModels/EventViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/EventViewModel.cs @@ -25,6 +25,10 @@ namespace Xamarin.PropertyEditing.ViewModels get; } + public override string Name => Event.Name; + + public override string Category => null; + public string MethodName { get { return this.methodName; } diff --git a/Xamarin.PropertyEditing/ViewModels/IFilterable.cs b/Xamarin.PropertyEditing/ViewModels/IFilterable.cs new file mode 100644 index 0000000..db6fe5b --- /dev/null +++ b/Xamarin.PropertyEditing/ViewModels/IFilterable.cs @@ -0,0 +1,9 @@ +namespace Xamarin.PropertyEditing.ViewModels +{ + internal interface IFilterable + { + string FilterText { get; set; } + + bool HasChildElements { get; } + } +}
\ No newline at end of file diff --git a/Xamarin.PropertyEditing/ViewModels/ObjectViewModel.cs b/Xamarin.PropertyEditing/ViewModels/ObjectViewModel.cs index 2c6c08d..fbd905a 100644 --- a/Xamarin.PropertyEditing/ViewModels/ObjectViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/ObjectViewModel.cs @@ -1,12 +1,12 @@ -namespace Xamarin.PropertyEditing.ViewModels +namespace Xamarin.PropertyEditing.ViewModels { internal class ObjectViewModel : PropertiesViewModel { private object value; - public ObjectViewModel (IEditorProvider provider) - : base (provider) + public ObjectViewModel (IEditorProvider provider, TargetPlatform targetPlatform) + : base (provider, targetPlatform) { } diff --git a/Xamarin.PropertyEditing/ViewModels/PanelViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PanelViewModel.cs index bffbcc0..fb5d4d8 100644 --- a/Xamarin.PropertyEditing/ViewModels/PanelViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/PanelViewModel.cs @@ -5,10 +5,10 @@ using System.Linq; namespace Xamarin.PropertyEditing.ViewModels { internal class PanelViewModel - : PropertiesViewModel + : PropertiesViewModel, IFilterable { - public PanelViewModel (IEditorProvider provider) - : base (provider) + public PanelViewModel (IEditorProvider provider, TargetPlatform targetPlatform) + : base (provider, targetPlatform) { ArrangeModes = new List<ArrangeModeViewModel> { new ArrangeModeViewModel (PropertyArrangeMode.Name, this), @@ -18,7 +18,9 @@ namespace Xamarin.PropertyEditing.ViewModels public event EventHandler ArrangedPropertiesChanged; - public IReadOnlyList<IGroupingList<string, PropertyViewModel>> ArrangedProperties => this.arranged; + public IReadOnlyList<IGroupingList<string, EditorViewModel>> ArrangedEditors => this.arranged; + + public bool HasChildElements => (this.arranged.Count > 0); public string FilterText { @@ -79,27 +81,69 @@ namespace Xamarin.PropertyEditing.ViewModels groups.Remove (group); } - protected override void OnAddProperties (IEnumerable<PropertyViewModel> properties) + protected override void OnAddEditors (IEnumerable<EditorViewModel> editors) { - IEnumerable<PropertyViewModel> props = Properties; + IEnumerable<EditorViewModel> props = Properties; if (!String.IsNullOrWhiteSpace (FilterText)) props = props.Where (MatchesFilter); - props = props.OrderBy (vm => vm.Property.Name); + props = props.OrderBy (vm => vm.Name); + + Dictionary<string, List<PropertyViewModel>> groupedTypeProperties = null; this.arranged.Clear (); foreach (var grouping in props.GroupBy (GetGroup).OrderBy (g => g.Key)) { - this.arranged.Add (grouping); + HashSet<EditorViewModel> remainingItems = null; + + if (ArrangeMode == PropertyArrangeMode.Category) { + foreach (EditorViewModel editorVm in grouping) { + var vm = editorVm as PropertyViewModel; + if (vm != null && TargetPlatform.GroupedTypes.TryGetValue (vm.Property.Type, out string category)) { + if (remainingItems == null) + remainingItems = new HashSet<EditorViewModel> (grouping); + + remainingItems.Remove (vm); + + if (groupedTypeProperties == null) + groupedTypeProperties = new Dictionary<string, List<PropertyViewModel>> (); + if (!groupedTypeProperties.TryGetValue (category, out List<PropertyViewModel> group)) + groupedTypeProperties[category] = group = new List<PropertyViewModel> (); + + group.Add (vm); + } + } + } + + if (remainingItems != null) + this.arranged.Add (grouping.Key, remainingItems); + else + this.arranged.Add (grouping); + } + + if (groupedTypeProperties != null) { // Insert type-grouped properties back in sorted. + int i = 0; + foreach (var kvp in groupedTypeProperties.OrderBy (kvp => kvp.Key)) { + for (; i < this.arranged.Count; i++) { + var g = (IGrouping<string, EditorViewModel>) this.arranged[i]; + // TODO: Are we translating categories? If so this needs to lookup the resource and be culture specific + if (String.Compare (g.Key, kvp.Key, StringComparison.Ordinal) > 0) { + this.arranged.Insert (i++, new ObservableGrouping<string, EditorViewModel> (kvp.Key) { + new PropertyGroupViewModel (kvp.Key, kvp.Value, ObjectEditors) + }); + break; + } + } + } } ArrangedPropertiesChanged?.Invoke (this, EventArgs.Empty); } - protected override void OnRemoveProperties (IEnumerable<PropertyViewModel> properties) + protected override void OnRemoveEditors (IEnumerable<EditorViewModel> editors) { - foreach (PropertyViewModel vm in properties) { + foreach (EditorViewModel vm in editors) { string g = GetGroup (vm); - var grouping = this.arranged[g] as ObservableGrouping<string, PropertyViewModel>; + var grouping = this.arranged[g] as ObservableGrouping<string, EditorViewModel>; if (grouping != null) { this.arranged.Remove (g, vm); } @@ -116,7 +160,7 @@ namespace Xamarin.PropertyEditing.ViewModels } private readonly Dictionary<PropertyArrangeMode, HashSet<string>> expandedGroups = new Dictionary<PropertyArrangeMode, HashSet<string>> (); - private readonly ObservableLookup<string, PropertyViewModel> arranged = new ObservableLookup<string, PropertyViewModel> { + private readonly ObservableLookup<string, EditorViewModel> arranged = new ObservableLookup<string, EditorViewModel> { ReuseGroups = true }; @@ -127,56 +171,52 @@ namespace Xamarin.PropertyEditing.ViewModels { this.arranged.Clear (); - OnAddProperties (Properties); - } - - private enum FilterState - { - Unknown, - Shorter, - Longer + OnAddEditors (Properties); } private void Filter (string oldFilter) { - FilterState state = FilterState.Unknown; - if (String.IsNullOrWhiteSpace (oldFilter) || FilterText.StartsWith (oldFilter, StringComparison.OrdinalIgnoreCase)) - state = FilterState.Longer; - else if (oldFilter.StartsWith (FilterText, StringComparison.OrdinalIgnoreCase)) - state = FilterState.Shorter; - - if (state != FilterState.Shorter) { - var toRemove = new List<PropertyViewModel> (); + bool hadChildren = HasChildElements; + + if (FilterText != null && (String.IsNullOrWhiteSpace (oldFilter) || FilterText.StartsWith (oldFilter, StringComparison.OrdinalIgnoreCase))) { + var toRemove = new List<EditorViewModel> (); foreach (var g in this.arranged) { foreach (var vm in g) { if (!MatchesFilter (vm)) toRemove.Add (vm); + else if (vm is IFilterable) { + var filterable = (IFilterable) vm; + filterable.FilterText = FilterText; + if (!filterable.HasChildElements) + toRemove.Add (vm); + } } } - OnRemoveProperties (toRemove); + OnRemoveEditors (toRemove); + } else { + OnAddEditors (Properties); } - if (state != FilterState.Longer) { - OnAddProperties (Properties); - } + if (hadChildren != HasChildElements) + OnPropertyChanged (nameof(HasChildElements)); } - private string GetGroup (PropertyViewModel vm) + private string GetGroup (EditorViewModel vm) { - return (ArrangeMode == PropertyArrangeMode.Name) ? "0" : vm.Property.Category; + return (ArrangeMode == PropertyArrangeMode.Name) ? "0" : vm.Category; } - private bool MatchesFilter (PropertyViewModel vm) + private bool MatchesFilter (EditorViewModel vm) { if (String.IsNullOrWhiteSpace (FilterText)) return true; - if (ArrangeMode == PropertyArrangeMode.Category && vm.Property.Category != null && vm.Property.Category.Contains (FilterText, StringComparison.OrdinalIgnoreCase)) { + if (ArrangeMode == PropertyArrangeMode.Category && vm.Category != null && vm.Category.Contains (FilterText, StringComparison.OrdinalIgnoreCase)) { return true; } - return vm.Property.Name.Contains (FilterText, StringComparison.OrdinalIgnoreCase); + return vm.Name.Contains (FilterText, StringComparison.OrdinalIgnoreCase); } } }
\ No newline at end of file diff --git a/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs index 7b647ec..e2fbbbb 100644 --- a/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs @@ -13,12 +13,15 @@ namespace Xamarin.PropertyEditing.ViewModels internal abstract class PropertiesViewModel : NotifyingObject, INotifyDataErrorInfo { - public PropertiesViewModel (IEditorProvider provider) + public PropertiesViewModel (IEditorProvider provider, TargetPlatform targetPlatform) { if (provider == null) throw new ArgumentNullException (nameof (provider)); + if (targetPlatform == null) + throw new ArgumentNullException (nameof(targetPlatform)); EditorProvider = provider; + TargetPlatform = targetPlatform; this.selectedObjects.CollectionChanged += OnSelectedObjectsChanged; } @@ -26,7 +29,7 @@ namespace Xamarin.PropertyEditing.ViewModels public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; /// <remarks>Consumers should check for <see cref="INotifyCollectionChanged"/> and hook appropriately.</remarks> - public IReadOnlyList<PropertyViewModel> Properties => this.properties; + public IReadOnlyList<EditorViewModel> Properties => this.editors; public IReadOnlyList<EventViewModel> Events => this.events; @@ -85,6 +88,11 @@ namespace Xamarin.PropertyEditing.ViewModels } } + public TargetPlatform TargetPlatform + { + get; + } + public bool HasErrors => this.errors.IsValueCreated && this.errors.Value.Count > 0; protected IEditorProvider EditorProvider @@ -92,6 +100,8 @@ namespace Xamarin.PropertyEditing.ViewModels get; } + protected IReadOnlyList<IObjectEditor> ObjectEditors => this.objEditors; + public IEnumerable GetErrors (string propertyName) { if (!this.errors.IsValueCreated) @@ -146,28 +156,28 @@ namespace Xamarin.PropertyEditing.ViewModels case NotifyCollectionChangedAction.Remove: removedEditors = new IObjectEditor[e.OldItems.Count]; for (int i = 0; i < e.OldItems.Count; i++) { - IObjectEditor editor = this.editors.First (oe => oe.Target == e.OldItems[i]); + IObjectEditor editor = this.objEditors.First (oe => oe.Target == e.OldItems[i]); INotifyCollectionChanged notifier = editor.Properties as INotifyCollectionChanged; if (notifier != null) notifier.CollectionChanged -= OnObjectEditorPropertiesChanged; removedEditors[i] = editor; - this.editors.Remove (editor); + this.objEditors.Remove (editor); } break; case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Move: case NotifyCollectionChangedAction.Reset: { - removedEditors = new IObjectEditor[this.editors.Count]; + removedEditors = new IObjectEditor[this.objEditors.Count]; for (int i = 0; i < removedEditors.Length; i++) { - removedEditors[i] = this.editors[i]; + removedEditors[i] = this.objEditors[i]; INotifyCollectionChanged notifier = removedEditors[i].Properties as INotifyCollectionChanged; if (notifier != null) notifier.CollectionChanged -= OnObjectEditorPropertiesChanged; } - this.editors.Clear (); + this.objEditors.Clear (); newEditors = await AddEditorsAsync (this.selectedObjects); break; @@ -178,11 +188,11 @@ namespace Xamarin.PropertyEditing.ViewModels tcs.SetResult (true); } - protected virtual void OnAddProperties (IEnumerable<PropertyViewModel> properties) + protected virtual void OnAddEditors (IEnumerable<EditorViewModel> editors) { } - protected virtual void OnRemoveProperties (IEnumerable<PropertyViewModel> properties) + protected virtual void OnRemoveEditors (IEnumerable<EditorViewModel> editors) { } @@ -194,8 +204,8 @@ namespace Xamarin.PropertyEditing.ViewModels private bool nameReadOnly; private bool eventsEnabled; private string typeName, objectName; - private readonly List<IObjectEditor> editors = new List<IObjectEditor> (); - private readonly ObservableCollectionEx<PropertyViewModel> properties = new ObservableCollectionEx<PropertyViewModel> (); + private readonly List<IObjectEditor> objEditors = new List<IObjectEditor> (); + private readonly ObservableCollectionEx<EditorViewModel> editors = new ObservableCollectionEx<EditorViewModel> (); private readonly ObservableCollectionEx<object> selectedObjects = new ObservableCollectionEx<object> (); private readonly ObservableCollectionEx<EventViewModel> events = new ObservableCollectionEx<EventViewModel> (); private readonly Lazy<Dictionary<string, string>> errors = new Lazy<Dictionary<string, string>>(); @@ -205,16 +215,16 @@ namespace Xamarin.PropertyEditing.ViewModels ErrorsChanged?.Invoke (this, e); } - private void AddProperties (IEnumerable<PropertyViewModel> properties) + private void AddProperties (IEnumerable<EditorViewModel> newEditors) { - this.properties.AddRange (properties); - OnAddProperties (properties); + this.editors.AddRange (newEditors); + OnAddEditors (newEditors); } - private void RemoveProperties (IEnumerable<PropertyViewModel> properties) + private void RemoveProperties (IEnumerable<EditorViewModel> oldEditors) { - this.properties.RemoveRange (properties); - OnRemoveProperties (properties); + this.editors.RemoveRange (oldEditors); + OnRemoveEditors (oldEditors); } private async void SetObjectName (string value) @@ -255,31 +265,31 @@ namespace Xamarin.PropertyEditing.ViewModels TypeName = null; SetNameable (null); SetCurrentObjectName (null, isReadonly: true); - this.properties.Clear (); + this.editors.Clear (); this.events.Clear (); OnClearProperties (); } private void UpdateMembers (IObjectEditor[] removedEditors = null, IObjectEditor[] newEditors = null) { - if (this.editors.Count == 0) { + if (this.objEditors.Count == 0) { ClearMembers (); return; } Task<string> nameQuery = null; - INameableObject firstNameable = this.editors[0] as INameableObject; - if (this.editors.Count == 1) { + INameableObject firstNameable = this.objEditors[0] as INameableObject; + if (this.objEditors.Count == 1) { nameQuery = firstNameable?.GetNameAsync (); } - IObjectEventEditor events = this.editors[0] as IObjectEventEditor; + IObjectEventEditor events = this.objEditors[0] as IObjectEventEditor; var newEventSet = new HashSet<IEventInfo> (events?.Events ?? Enumerable.Empty<IEventInfo> ()); - string newTypeName = this.editors[0].TypeName; - var newPropertySet = new HashSet<IPropertyInfo> (this.editors[0].Properties); - for (int i = 1; i < this.editors.Count; i++) { - IObjectEditor editor = this.editors[i]; + string newTypeName = this.objEditors[0].TypeName; + var newPropertySet = new HashSet<IPropertyInfo> (this.objEditors[0].Properties); + for (int i = 1; i < this.objEditors.Count; i++) { + IObjectEditor editor = this.objEditors[i]; newPropertySet.IntersectWith (editor.Properties); if (editor is IObjectEventEditor) { @@ -291,7 +301,7 @@ namespace Xamarin.PropertyEditing.ViewModels firstNameable = editor as INameableObject; if (newTypeName != editor.TypeName) - newTypeName = String.Format (PropertyEditing.Properties.Resources.MultipleTypesSelected, this.editors.Count); + newTypeName = String.Format (PropertyEditing.Properties.Resources.MultipleTypesSelected, this.objEditors.Count); } TypeName = newTypeName; @@ -301,20 +311,20 @@ namespace Xamarin.PropertyEditing.ViewModels EventsEnabled = events != null; UpdateEvents (newEventSet, removedEditors, newEditors); - string name = (this.editors.Count > 1) ? String.Format (PropertyEditing.Properties.Resources.MultipleObjectsSelected, this.editors.Count) : PropertyEditing.Properties.Resources.NoName; - if (this.editors.Count == 1) { + string name = (this.objEditors.Count > 1) ? String.Format (PropertyEditing.Properties.Resources.MultipleObjectsSelected, this.objEditors.Count) : PropertyEditing.Properties.Resources.NoName; + if (this.objEditors.Count == 1) { string tname = nameQuery?.Result; if (tname != null) name = tname; } SetNameable (firstNameable); - SetCurrentObjectName (name, this.editors.Count > 1); + SetCurrentObjectName (name, this.objEditors.Count > 1); } private void UpdateEvents (HashSet<IEventInfo> newSet, IObjectEditor[] removedEditors = null, IObjectEditor[] newEditors = null) { - if (this.editors.Count > 1) { + if (this.objEditors.Count > 1) { this.events.Clear (); return; } @@ -341,14 +351,14 @@ namespace Xamarin.PropertyEditing.ViewModels if (toRemove.Count > 0) this.events.RemoveRange (toRemove); if (newSet.Count > 0) { - this.events.Reset (this.events.Concat (newSet.Select (i => new EventViewModel (i, this.editors))).OrderBy (e => e.Event.Name).ToArray()); + this.events.Reset (this.events.Concat (newSet.Select (i => new EventViewModel (i, this.objEditors))).OrderBy (e => e.Event.Name).ToArray()); } } private void UpdateProperties (HashSet<IPropertyInfo> newSet, IObjectEditor[] removedEditors = null, IObjectEditor[] newEditors = null) { List<PropertyViewModel> toRemove = new List<PropertyViewModel> (); - foreach (PropertyViewModel vm in this.properties.ToArray ()) { + foreach (PropertyViewModel vm in this.editors.ToArray ()) { if (!newSet.Remove (vm.Property)) { toRemove.Add (vm); vm.Editors.Clear (); @@ -386,7 +396,7 @@ namespace Xamarin.PropertyEditing.ViewModels notifier.CollectionChanged += OnObjectEditorPropertiesChanged; } - this.editors.AddRange (newEditors); + this.objEditors.AddRange (newEditors); return newEditors; } @@ -402,17 +412,17 @@ namespace Xamarin.PropertyEditing.ViewModels Type hasPredefinedValues = interfaces.FirstOrDefault (t => t.IsGenericType && t.GetGenericTypeDefinition () == typeof(IHavePredefinedValues<>)); if (hasPredefinedValues != null) { Type type = typeof(PredefinedValuesViewModel<>).MakeGenericType (hasPredefinedValues.GenericTypeArguments[0]); - return (PropertyViewModel) Activator.CreateInstance (type, property, this.editors); + return (PropertyViewModel) Activator.CreateInstance (type, property, this.objEditors); } else if (property.Type.IsEnum) { Type type = typeof(EnumPropertyViewModel<>).MakeGenericType (property.Type); - return (PropertyViewModel) Activator.CreateInstance (type, property, this.editors); + return (PropertyViewModel) Activator.CreateInstance (type, property, this.objEditors); } Func<IPropertyInfo, IEnumerable<IObjectEditor>, PropertyViewModel> vmFactory; if (ViewModelMap.TryGetValue (property.Type, out vmFactory)) - return vmFactory (property, this.editors); + return vmFactory (property, this.objEditors); - return new StringPropertyViewModel (property, this.editors); + return new StringPropertyViewModel (property, this.objEditors); } private Task busyTask; diff --git a/Xamarin.PropertyEditing/ViewModels/PropertyGroupViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PropertyGroupViewModel.cs new file mode 100644 index 0000000..8e65574 --- /dev/null +++ b/Xamarin.PropertyEditing/ViewModels/PropertyGroupViewModel.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; + +namespace Xamarin.PropertyEditing.ViewModels +{ + internal class PropertyGroupViewModel + : EditorViewModel, IFilterable + { + public PropertyGroupViewModel (string category, IEnumerable<PropertyViewModel> properties, IEnumerable<IObjectEditor> objEditors) + : base (objEditors) + { + if (category == null) + throw new ArgumentNullException (nameof(category)); + if (properties == null) + throw new ArgumentNullException (nameof (properties)); + + Category = category; + + this.properties = properties.ToArray (); + foreach (var vm in this.properties) { + if (vm.IsAvailable) + this.propertiesView.Add (vm); + + vm.PropertyChanged += OnChildPropertyChanged; + } + } + + public IReadOnlyList<PropertyViewModel> Properties => this.propertiesView; + + public override string Name => null; + + public override string Category + { + get; + } + + public bool HasChildElements => (this.propertiesView.Count > 0); + + public string FilterText + { + get { return this.filterText; } + set + { + if (this.filterText == value) + return; + + string oldFilter = this.filterText; + this.filterText = value; + Filter (oldFilter); + OnPropertyChanged(); + } + } + + private readonly PropertyViewModel[] properties; + private readonly ObservableCollectionEx<PropertyViewModel> propertiesView = new ObservableCollectionEx<PropertyViewModel> (); + private string filterText; + + private void Filter (string oldFilter) + { + bool hadChildren = HasChildElements; + + if (FilterText != null && (String.IsNullOrWhiteSpace (oldFilter) || FilterText.StartsWith (oldFilter, StringComparison.OrdinalIgnoreCase))) { + var current = new List<PropertyViewModel> (this.propertiesView); + for (int i = 0; i < current.Count; i++) { + var vm = current[i]; + if (!MatchesFilter (vm)) + this.propertiesView.Remove (vm); + } + } else { + this.propertiesView.Reset (this.properties.Where (MatchesFilter).OrderBy (p => p.Property.Name)); + } + + if (hadChildren != HasChildElements) + OnPropertyChanged (nameof(HasChildElements)); + } + + private bool MatchesFilter (PropertyViewModel vm) + { + if (!vm.IsAvailable) + return false; + if (String.IsNullOrWhiteSpace (FilterText)) + return true; + + return (vm.Property.Name.Contains (FilterText, StringComparison.OrdinalIgnoreCase)); + } + + private void OnChildPropertyChanged (object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(PropertyViewModel.IsAvailable)) + return; + + var vm = (PropertyViewModel) sender; + if (MatchesFilter (vm)) + this.propertiesView.Add (vm); + else + this.propertiesView.Remove (vm); + } + } +}
\ No newline at end of file diff --git a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs index 3bee8d6..249a1ea 100644 --- a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs @@ -19,6 +19,7 @@ namespace Xamarin.PropertyEditing.ViewModels : base (property, editors) { SetValueResourceCommand = new RelayCommand<Resource> (OnSetValueToResource, CanSetValueToResource); + ClearValueCommand = new RelayCommand (OnClearValue, CanClearValue); UpdateCurrentValue (); } @@ -58,6 +59,11 @@ namespace Xamarin.PropertyEditing.ViewModels get; } + public ICommand ClearValueCommand + { + get; + } + protected virtual TValue ValidateValue (TValue validationValue) { return validationValue; @@ -110,6 +116,10 @@ namespace Xamarin.PropertyEditing.ViewModels this.value = newValue; OnValueChanged (); OnPropertyChanged (nameof (Value)); + OnPropertyChanged (nameof (ValueSource)); + + ((RelayCommand) ClearValueCommand)?.ChangeCanExecute (); + return true; } @@ -196,6 +206,19 @@ namespace Xamarin.PropertyEditing.ViewModels ((RelayCommand<Resource>)SetValueResourceCommand).ChangeCanExecute (); } + + private void OnClearValue () + { + SetValue (new ValueInfo<TValue> { + Source = ValueSource.Default, + Value = default(TValue) + }); + } + + private bool CanClearValue () + { + return (Property.ValueSources.HasFlag (ValueSources.Local) && Property.ValueSources.HasFlag (ValueSources.Default) && ValueSource == ValueSource.Local); + } } internal abstract class PropertyViewModel @@ -218,9 +241,12 @@ namespace Xamarin.PropertyEditing.ViewModels get; } + public override string Name => Property.Name; + public override string Category => Property.Category; + public bool IsAvailable { - get { return this.isAvailable == null ? false : this.isAvailable.Result; } + get { return this.isAvailable?.Result ?? true; } private set { if (this.isAvailable.Result == value) @@ -279,9 +305,6 @@ namespace Xamarin.PropertyEditing.ViewModels protected virtual async void OnEditorPropertyChanged (object sender, EditorPropertyChangedEventArgs e) { - if (e.Property != null && !Equals (e.Property, Property)) - return; - IDisposable work = null; if (this.constraintProperties != null && this.constraintProperties.Contains (e.Property)) { work = await AsyncWork.RequestAsyncWork (this); @@ -289,6 +312,9 @@ namespace Xamarin.PropertyEditing.ViewModels } try { + if (e.Property != null && !Equals (e.Property, Property)) + return; + // TODO: Smarter querying, can query the single editor and check against MultipleValues UpdateCurrentValue (); } finally { diff --git a/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj b/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj index ebacf32..e7a95a8 100644 --- a/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj +++ b/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj @@ -90,6 +90,7 @@ <Compile Include="Reflection\ReflectionEventInfo.cs" /> <Compile Include="Reflection\ReflectionObjectEditor.cs" /> <Compile Include="Reflection\ReflectionPropertyInfo.cs" /> + <Compile Include="TargetPlatform.cs" /> <Compile Include="ValueInfo.cs" /> <Compile Include="ValueSource.cs" /> <Compile Include="ViewModels\ArrangeModeViewModel.cs" /> @@ -99,12 +100,14 @@ <Compile Include="ViewModels\EnumPropertyViewModel.cs" /> <Compile Include="Extensions.cs" /> <Compile Include="ViewModels\EventViewModel.cs" /> + <Compile Include="ViewModels\IFilterable.cs" /> <Compile Include="ViewModels\NumericPropertyViewModel.cs" /> <Compile Include="ViewModels\ObjectViewModel.cs" /> <Compile Include="ViewModels\PanelViewModel.cs" /> <Compile Include="ViewModels\PointPropertyViewModel.cs" /> <Compile Include="ViewModels\PredefinedValuesViewModel.cs" /> <Compile Include="ViewModels\PropertiesViewModel.cs" /> + <Compile Include="ViewModels\PropertyGroupViewModel.cs" /> <Compile Include="ViewModels\PropertyViewModel.cs" /> <Compile Include="ViewModels\RelayCommand.cs" /> <Compile Include="ViewModels\SizePropertyViewModel.cs" /> |