diff options
author | Eric Maupin <ermaup@microsoft.com> | 2018-09-28 21:34:28 +0300 |
---|---|---|
committer | Eric Maupin <ermaup@microsoft.com> | 2018-09-28 22:28:48 +0300 |
commit | 2e52e0bff0fac4668d810f466fa9019bd330963d (patch) | |
tree | e468fefddbe9ff00a1b35447f2082850d976eee0 | |
parent | 46c79646e2cbf01526305628f6d83f37fe5a1627 (diff) |
[Core/Win] Add input modes
9 files changed, 250 insertions, 8 deletions
diff --git a/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs b/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs index 8004461..d11daf9 100644 --- a/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs +++ b/Xamarin.PropertyEditing.Tests/MockControls/MockControl.cs @@ -14,7 +14,8 @@ namespace Xamarin.PropertyEditing.Tests.MockControls public void AddProperty<T> (string name, string category = null, bool canWrite = true, bool flag = false, IEnumerable<Type> converterTypes = null, - string description = null, bool constrained = true, ValueSources valueSources = ValueSources.Local | ValueSources.Default | ValueSources.Binding) + string description = null, bool constrained = true, ValueSources valueSources = ValueSources.Local | ValueSources.Default | ValueSources.Binding, + IReadOnlyList<InputMode> inputModes = null) { IPropertyInfo propertyInfo; if (typeof(T).IsEnum) { @@ -22,6 +23,8 @@ namespace Xamarin.PropertyEditing.Tests.MockControls var enumPropertyInfoType = typeof (MockEnumPropertyInfo<,>) .MakeGenericType (underlyingType, typeof (T)); propertyInfo = (IPropertyInfo)Activator.CreateInstance (enumPropertyInfoType, name, description, category, canWrite, flag, converterTypes, constrained); + } else if (inputModes != null) { + propertyInfo = new MockPropertyInfoWithInputTypes<T> (name, inputModes, description, category, canWrite, converterTypes, valueSources); } else { propertyInfo = new MockPropertyInfo<T> (name, description, category, canWrite, converterTypes, valueSources); } diff --git a/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs b/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs index 3fe6e51..c4d88fb 100644 --- a/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs +++ b/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs @@ -16,6 +16,7 @@ namespace Xamarin.PropertyEditing.Tests.MockControls AddProperty<int> ("UnsetInteger", ReadWrite, valueSources: ValueSources.Local); AddProperty<float> ("FloatingPoint", ReadWrite); AddProperty<string> ("String", ReadWrite, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding); + AddProperty<int> ("Width", ReadWrite, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding, inputModes: new[] { new InputMode("Auto", true), new InputMode("Star"), new InputMode("Pixel"), }); AddProperty<Enumeration> ("Enumeration", ReadWrite, constrained: false); AddProperty<FlagsNoValues> ("FlagsNoValues", ReadWrite, canWrite: true, flag: true); AddProperty<FlagsWithValues> ("FlagsWithValues", ReadWrite, canWrite: true, flag: true); diff --git a/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs b/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs index 035b646..e21737e 100644 --- a/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs +++ b/Xamarin.PropertyEditing.Tests/MockPropertyInfo/MockPropertyInfo.cs @@ -6,6 +6,21 @@ using Xamarin.PropertyEditing.Tests.MockControls; namespace Xamarin.PropertyEditing.Tests.MockPropertyInfo { + public class MockPropertyInfoWithInputTypes<T> + : MockPropertyInfo<T>, IHaveInputModes + { + public MockPropertyInfoWithInputTypes (string name, IReadOnlyList<InputMode> inputModes, string description = null, string category = null, bool canWrite = true, IEnumerable<Type> converterTypes = null, ValueSources valueSources = ValueSources.Default | ValueSources.Local) + : base (name, description, category, canWrite, converterTypes, valueSources) + { + InputModes = inputModes.ToArray (); + } + + public IReadOnlyList<InputMode> InputModes + { + get; + } + } + public class MockPropertyInfo<T> : IPropertyInfo, IPropertyConverter, IEquatable<MockPropertyInfo<T>> { public MockPropertyInfo (string name, string description = null, string category = null, bool canWrite = true, IEnumerable<Type> converterTypes = null, ValueSources valueSources = ValueSources.Local | ValueSources.Default) diff --git a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs index c207b4f..ae2fa61 100644 --- a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs +++ b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs @@ -1175,6 +1175,124 @@ namespace Xamarin.PropertyEditing.Tests Assert.That (tcs.Task.IsCanceled, Is.True); } + [Test] + public void DoesntHaveInputModes () + { + var vm = GetBasicTestModel (); + Assert.That (vm.HasInputModes, Is.False); + } + + [Test] + public void HasInputModes () + { + var modes = new[] { new InputMode ("TestMode") }; + + var property = GetPropertyMock (); + var input = property.As<IHaveInputModes> (); + input.SetupGet (im => im.InputModes).Returns (modes); + + var vm = GetViewModel (property.Object, new MockObjectEditor (property.Object)); + + Assert.That (vm.HasInputModes, Is.True, "HasInputModes was false"); + Assert.That (vm.InputModes, Contains.Item (modes[0])); + Assert.That (vm.InputModes.Count, Is.EqualTo (1)); + Assert.That (vm.InputMode, Is.EqualTo (modes[0]), "InputMode not set to a default on no value set for it"); + } + + [Test] + public void InputModeCommits () + { + var modes = new[] { new InputMode ("TestMode"), new InputMode("TestMode2"), }; + + var property = GetPropertyMock (); + var input = property.As<IHaveInputModes> (); + input.SetupGet (im => im.InputModes).Returns (modes); + + var target = new object (); + var editor = new Mock<IObjectEditor> (); + editor.SetupGet (e => e.Target).Returns (target); + editor.SetupGet (e => e.Properties).Returns (new[] { property.Object }); + SetupPropertySetAndGet (editor, property.Object); + + var vm = GetViewModel (property.Object, editor.Object); + Assume.That (vm.InputMode, Is.EqualTo (modes[0])); + + vm.InputMode = modes[1]; + editor.Verify (oe => oe.SetValueAsync (property.Object, It.Is<ValueInfo<TValue>> (vi => vi.ValueDescriptor == modes[1]), It.IsAny<PropertyVariation> ())); + } + + [Test] + public async Task InputModeRestores () + { + var modes = new[] { new InputMode ("TestMode"), new InputMode ("TestMode2"), }; + + var property = GetPropertyMock (); + var input = property.As<IHaveInputModes> (); + input.SetupGet (im => im.InputModes).Returns (modes); + + var target = new object (); + var editor = new Mock<IObjectEditor> (); + editor.SetupGet (e => e.Target).Returns (target); + editor.SetupGet (e => e.Properties).Returns (new[] { property.Object }); + SetupPropertySetAndGet (editor, property.Object); + + TValue value = GetRandomTestValue (); + + await editor.Object.SetValueAsync (property.Object, new ValueInfo<TValue> { + Value = value, + ValueDescriptor = modes[1] + }); + + var vm = GetViewModel (property.Object, editor.Object); + Assert.That (vm.InputMode, Is.EqualTo (modes[1])); + } + + [TestCase (true)] + [TestCase (false)] + public void InputEnabled (bool writeEnabled) + { + var property = GetPropertyMock (); + property.SetupGet (pi => pi.CanWrite).Returns (writeEnabled); + + var vm = GetViewModel (property.Object, new MockObjectEditor (property.Object)); + Assert.That (vm.IsInputEnabled, Is.EqualTo (writeEnabled)); + } + + [TestCase (true, false, true)] + [TestCase (true, true, false)] + [TestCase (false, true, false)] + [TestCase (false, false, false)] + public void InputEnabledSingleValueInputMode (bool writeEnabled, bool singleValue, bool expectation) + { + var modes = new[] { new InputMode ("TestMode"), new InputMode ("TestMode2", singleValue), }; + + var property = GetPropertyMock (); + property.SetupGet (pi => pi.CanWrite).Returns (writeEnabled); + var input = property.As<IHaveInputModes> (); + input.SetupGet (im => im.InputModes).Returns (modes); + + var target = new object (); + var editor = new Mock<IObjectEditor> (); + editor.SetupGet (e => e.Target).Returns (target); + editor.SetupGet (e => e.Properties).Returns (new[] { property.Object }); + SetupPropertySetAndGet (editor, property.Object); + + var vm = GetViewModel (property.Object, editor.Object); + Assume.That (vm.InputMode, Is.EqualTo (modes[0])); + Assume.That (vm.IsInputEnabled, Is.EqualTo (writeEnabled), "Initial state didn't match property"); + + bool changed = false; + vm.PropertyChanged += (o, e) => { + if (e.PropertyName == nameof(PropertyViewModel<TValue>.IsInputEnabled)) + changed = true; + }; + + vm.InputMode = modes[1]; + + Assert.That (changed, Is.EqualTo (writeEnabled != expectation)); + Assert.That (vm.IsInputEnabled, Is.EqualTo (expectation)); + } + protected TViewModel GetViewModel (IPropertyInfo property, IObjectEditor editor) { return GetViewModel (property, new[] { editor }); diff --git a/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml b/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml index 3f61759..4ae48b6 100644 --- a/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml +++ b/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml @@ -277,7 +277,15 @@ <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:StringEditorControl"> - <local:TextBoxEx x:Name="TextBox" FocusSelectsAll="True" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" Text="{Binding Value,UpdateSourceTrigger=Explicit}" IsEnabled="{Binding Property.CanWrite,Mode=OneTime}" VerticalContentAlignment="Center" /> + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + + <local:TextBoxEx x:Name="TextBox" Grid.Column="0" FocusSelectsAll="True" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" Text="{Binding Value,UpdateSourceTrigger=Explicit}" VerticalContentAlignment="Center" IsEnabled="{Binding IsInputEnabled}" /> + <ComboBox Grid.Column="1" Visibility="{Binding HasInputModes,Mode=OneTime,Converter={StaticResource BoolToVisibilityConverter}}" IsEnabled="{Binding Property.CanWrite,Mode=OneTime}" ItemsSource="{Binding InputModes,Mode=OneTime}" SelectedItem="{Binding InputMode,Mode=TwoWay}" DisplayMemberPath="Identifier" /> + </Grid> </ControlTemplate> </Setter.Value> </Setter> @@ -2022,17 +2030,17 @@ <local:NumericTemplateSelector x:Key="NumericTemplateSelector"> <local:NumericTemplateSelector.FloatingTemplate> <DataTemplate DataType="vm:FloatingPropertyViewModel"> - <local:DoubleUpDownControl Value="{Binding Value}" IsEnabled="{Binding Property.CanWrite,Mode=OneTime}" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" /> + <local:DoubleUpDownControl Value="{Binding Value}" IsEnabled="{Binding IsInputEnabled}" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" /> </DataTemplate> </local:NumericTemplateSelector.FloatingTemplate> <local:NumericTemplateSelector.IntegerTemplate> <DataTemplate DataType="vm:IntegerPropertyViewModel"> - <local:IntegerUpDownControl Value="{Binding Value}" IsEnabled="{Binding Property.CanWrite,Mode=OneTime}" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" /> + <local:IntegerUpDownControl Value="{Binding Value}" IsEnabled="{Binding IsInputEnabled}" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" /> </DataTemplate> </local:NumericTemplateSelector.IntegerTemplate> <local:NumericTemplateSelector.ByteTemplate> <DataTemplate DataType="vm:BytePropertyViewModel"> - <local:ByteUpDownControl Value="{Binding Value}" IsEnabled="{Binding Property.CanWrite,Mode=OneTime}" AutomationProperties.LabeledBy="{TemplateBinding AutomationProperties.LabeledBy}" /> + <local:ByteUpDownControl Value="{Binding Value}" IsEnabled="{Binding IsInputEnabled}" AutomationProperties.LabeledBy="{TemplateBinding AutomationProperties.LabeledBy}" /> </DataTemplate> </local:NumericTemplateSelector.ByteTemplate> </local:NumericTemplateSelector> @@ -2042,7 +2050,16 @@ <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:NumericEditorControl"> - <ContentPresenter Content="{Binding}" ContentTemplateSelector="{StaticResource NumericTemplateSelector}" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" /> + <Grid> + <Grid.ColumnDefinitions> + <ColumnDefinition Width="*" /> + <ColumnDefinition Width="Auto" /> + </Grid.ColumnDefinitions> + + <ContentPresenter Grid.Column="0" Content="{Binding}" ContentTemplateSelector="{StaticResource NumericTemplateSelector}" AutomationProperties.Name="{Binding Property.Name,Mode=OneTime}" /> + <ComboBox Grid.Column="1" Visibility="{Binding HasInputModes,Mode=OneTime,Converter={StaticResource BoolToVisibilityConverter}}" IsEnabled="{Binding Property.CanWrite,Mode=OneTime}" ItemsSource="{Binding InputModes,Mode=OneTime}" SelectedItem="{Binding InputMode,Mode=TwoWay}" DisplayMemberPath="Identifier" /> + </Grid> + </ControlTemplate> </Setter.Value> </Setter> diff --git a/Xamarin.PropertyEditing/IHaveInputModes.cs b/Xamarin.PropertyEditing/IHaveInputModes.cs new file mode 100644 index 0000000..10ed26c --- /dev/null +++ b/Xamarin.PropertyEditing/IHaveInputModes.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace Xamarin.PropertyEditing +{ + public class InputMode + { + public InputMode (string identifier, bool isSingleValue = false) + { + if (identifier == null) + throw new ArgumentNullException (nameof(identifier)); + + Identifier = identifier; + IsSingleValue = isSingleValue; + } + + public string Identifier + { + get; + } + + public bool IsSingleValue + { + get; + } + } + + /// <summary> + /// Indicates a property has input modes. + /// </summary> + /// <remarks> + /// Implemented on <see cref="IPropertyInfo"/> instances. + /// </remarks> + public interface IHaveInputModes + { + IReadOnlyList<InputMode> InputModes { get; } + } +} diff --git a/Xamarin.PropertyEditing/ViewModels/NumericPropertyViewModel.cs b/Xamarin.PropertyEditing/ViewModels/NumericPropertyViewModel.cs index 2666421..710023f 100644 --- a/Xamarin.PropertyEditing/ViewModels/NumericPropertyViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/NumericPropertyViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Windows.Input; namespace Xamarin.PropertyEditing.ViewModels diff --git a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs index 605b8c5..20985a1 100644 --- a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs @@ -25,6 +25,10 @@ namespace Xamarin.PropertyEditing.ViewModels public PropertyViewModel (TargetPlatform platform, IPropertyInfo property, IEnumerable<IObjectEditor> editors) : base (platform, property, editors) { + if (property is IHaveInputModes inputModes) { + InputModes = inputModes.InputModes.ToArray (); + } + this.coerce = property as ICoerce<TValue>; this.validator = property as IValidator<TValue>; this.valueNavigator = property as ICanNavigateToSource; @@ -58,7 +62,8 @@ namespace Xamarin.PropertyEditing.ViewModels SetValue (new ValueInfo<TValue> { Source = ValueSource.Local, - Value = value + Value = value, + ValueDescriptor = InputMode }); } } @@ -121,8 +126,40 @@ namespace Xamarin.PropertyEditing.ViewModels } } + public override bool IsInputEnabled => base.IsInputEnabled && (InputMode == null || !InputMode.IsSingleValue); + public override bool SupportsValueSourceNavigation => this.valueNavigator != null; + public bool HasInputModes => InputModes != null && InputModes.Count > 0; + + public IReadOnlyList<InputMode> InputModes + { + get; + } + + public InputMode InputMode + { + get { return this.inputMode; } + set + { + if (this.inputMode == value) + return; + + bool enabled = IsInputEnabled; + + this.inputMode = value; + OnPropertyChanged (); + if (enabled != IsInputEnabled) + OnPropertyChanged (nameof (IsInputEnabled)); + + SetValue (new ValueInfo<TValue> { + Source = ValueSource.Local, + Value = (CurrentValue != null && !value.IsSingleValue) ? CurrentValue.Value : default(TValue), + ValueDescriptor = value + }); + } + } + protected ValueInfo<TValue> CurrentValue { get { return this.value; } @@ -276,6 +313,7 @@ namespace Xamarin.PropertyEditing.ViewModels private readonly IValidator<TValue> validator; private readonly ICanNavigateToSource valueNavigator; internal const string NullableName = "Nullable`1"; + private InputMode inputMode; private bool isNullable; private ValueInfo<TValue> value; @@ -302,7 +340,16 @@ namespace Xamarin.PropertyEditing.ViewModels return false; this.value = newValue; + if (newValue != null && newValue.ValueDescriptor is InputMode newMode) { + this.inputMode = newMode; + } else if (HasInputModes) { + this.inputMode = InputModes.FirstOrDefault (); + } + OnValueChanged (); + if (HasInputModes) + OnPropertyChanged (nameof(InputMode)); + SignalValueChange(); ((RelayCommand) ConvertToLocalValueCommand)?.ChangeCanExecute (); @@ -466,7 +513,6 @@ namespace Xamarin.PropertyEditing.ViewModels SetupConstraints (); this.requestResourceCommand = new RelayCommand (OnRequestResource, CanRequestResource); - } public event EventHandler<ResourceRequestedEventArgs> ResourceRequested; @@ -497,6 +543,8 @@ namespace Xamarin.PropertyEditing.ViewModels public virtual bool CanDelve => false; + public virtual bool IsInputEnabled => Property.CanWrite; + public bool SupportsResources { get { return TargetPlatform.ResourceProvider != null && Property.CanWrite && Property.ValueSources.HasFlag (ValueSources.Resource); } diff --git a/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj b/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj index 22785d8..3dfb115 100644 --- a/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj +++ b/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj @@ -83,6 +83,7 @@ <Compile Include="ICoerce.cs" /> <Compile Include="ICompleteValues.cs" /> <Compile Include="IEditorProvider.cs" /> + <Compile Include="IHaveInputModes.cs" /> <Compile Include="IObjectEventEditor.cs" /> <Compile Include="IEventInfo.cs" /> <Compile Include="INameableObject.cs" /> |