Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/xamarin/Xamarin.PropertyEditing.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Maupin <me@ermau.com>2018-09-05 17:59:08 +0300
committerGitHub <noreply@github.com>2018-09-05 17:59:08 +0300
commit27a74c5e58217b9e47eb26b89260c00742243a0e (patch)
tree5c166793b53dddcdfed1ec602bb272b089dfa532
parentf4e834c60b3b3bc9780e4064b72cd9ccf9a48a74 (diff)
parentc9b474b066ee6b9e8e74a8fbcb5b625a113b627c (diff)
Merge pull request #379 from xamarin/ermau-autocomplete
Basic autocomplete support
-rw-r--r--Xamarin.PropertyEditing.Tests/MockEditorProvider.cs8
-rw-r--r--Xamarin.PropertyEditing.Tests/MockObjectEditor.cs27
-rw-r--r--Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs90
-rw-r--r--Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs4
-rw-r--r--Xamarin.PropertyEditing.Windows/EntryPopup.cs46
-rw-r--r--Xamarin.PropertyEditing.Windows/TextBoxEx.cs144
-rw-r--r--Xamarin.PropertyEditing.Windows/Themes/Resources.xaml38
-rw-r--r--Xamarin.PropertyEditing/ICompleteValues.cs44
-rw-r--r--Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs91
-rw-r--r--Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj1
10 files changed, 451 insertions, 42 deletions
diff --git a/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs b/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs
index 2225bca..dee6c71 100644
--- a/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs
+++ b/Xamarin.PropertyEditing.Tests/MockEditorProvider.cs
@@ -13,8 +13,9 @@ namespace Xamarin.PropertyEditing.Tests
{
public static readonly TargetPlatform MockPlatform = new TargetPlatform (new MockEditorProvider ());
- public MockEditorProvider ()
+ public MockEditorProvider (IResourceProvider resources = null)
{
+ this.resources = resources;
}
public MockEditorProvider (IObjectEditor editor)
@@ -70,9 +71,9 @@ namespace Xamarin.PropertyEditing.Tests
{
switch (item) {
case MockWpfControl msc:
- return new MockObjectEditor (msc);
+ return new MockObjectEditor (msc) { Resources = this.resources };
case MockControl mc:
- return new MockNameableEditor (mc);
+ return new MockNameableEditor (mc) { Resources = this.resources };
case MockBinding mb:
return new MockBindingEditor (mb);
default:
@@ -99,6 +100,7 @@ namespace Xamarin.PropertyEditing.Tests
return Task.FromResult<IReadOnlyDictionary<Type, ITypeInfo>> (new Dictionary<Type, ITypeInfo> ());
}
+ private readonly IResourceProvider resources;
private readonly 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 80f11c0..3adb37e 100644
--- a/Xamarin.PropertyEditing.Tests/MockObjectEditor.cs
+++ b/Xamarin.PropertyEditing.Tests/MockObjectEditor.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
+using System.Threading;
using System.Threading.Tasks;
using Xamarin.PropertyEditing.Reflection;
using Xamarin.PropertyEditing.Tests.MockControls;
@@ -29,7 +30,7 @@ namespace Xamarin.PropertyEditing.Tests
}
internal class MockObjectEditor
- : IObjectEditor, IObjectEventEditor
+ : IObjectEditor, IObjectEventEditor, ICompleteValues
{
public MockObjectEditor ()
{
@@ -111,6 +112,12 @@ namespace Xamarin.PropertyEditing.Tests
set;
}
+ public IResourceProvider Resources
+ {
+ get;
+ set;
+ }
+
public void ChangeAllProperties ()
{
PropertyChanged?.Invoke (this, new EditorPropertyChangedEventArgs (null));
@@ -332,6 +339,24 @@ namespace Xamarin.PropertyEditing.Tests
return Task.FromResult<ITypeInfo> (new TypeInfo (asm, type.Namespace, type.Name));
}
+ public bool CanAutocomplete (string input)
+ {
+ return (input != null && input.Trim ().StartsWith ("@"));
+ }
+
+ public async Task<IReadOnlyList<string>> GetCompletionsAsync (IPropertyInfo property, string input, CancellationToken cancellationToken)
+ {
+ if (Resources == null)
+ return Array.Empty<string> ();
+
+ input = input.Trim ().TrimStart('@');
+ var resources = await Resources.GetResourcesAsync (Target, property, cancellationToken);
+ return resources.Where (r =>
+ r.Name.IndexOf (input, StringComparison.OrdinalIgnoreCase) != -1
+ && r.Name.Length > input.Length) // Skip exact matches
+ .Select (r => "@" + r.Name).ToList ();
+ }
+
internal readonly IDictionary<IPropertyInfo, object> values = new Dictionary<IPropertyInfo, object> ();
internal readonly IDictionary<IEventInfo, string> events = new Dictionary<IEventInfo, string> ();
internal readonly IReadOnlyDictionary<IPropertyInfo, IReadOnlyList<ITypeInfo>> assignableTypes;
diff --git a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs
index 00d7e3b..c207b4f 100644
--- a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs
+++ b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs
@@ -1085,6 +1085,96 @@ namespace Xamarin.PropertyEditing.Tests
Assert.That (requested, Is.True, "CreateResourceRequested did not fire");
}
+ [TestCase (true, true, true)]
+ [TestCase (false, true, false)]
+ [TestCase (true, false, false)]
+ public void AutocompleteEnabled (bool customExpressions, bool hasInterface, bool expected)
+ {
+ var target = new object ();
+ var property = GetPropertyMock ();
+ var editor = new Mock<IObjectEditor> ();
+ editor.SetupGet (e => e.Target).Returns (target);
+ SetupPropertySetAndGet (editor, property.Object);
+
+ if (hasInterface) {
+ string[] results = { "Foo", "Bar", "Baz" };
+ var complete = editor.As<ICompleteValues> ();
+ complete.Setup (c => c.GetCompletionsAsync (property.Object, It.IsAny<string> (), It.IsAny<CancellationToken> ()))
+ .ReturnsAsync (results);
+ }
+
+ var mockProvider = new MockEditorProvider (editor.Object);
+ var resources = new MockResourceProvider ();
+ var platform = new TargetPlatform (mockProvider, resources) {
+ SupportsCustomExpressions = customExpressions
+ };
+
+ var vm = GetViewModel (platform, property.Object, new[] { editor.Object });
+ Assert.That (vm.SupportsAutocomplete, Is.EqualTo (expected));
+ }
+
+ [Test]
+ public void AutocompleteResults ()
+ {
+ var target = new object ();
+ var property = GetPropertyMock ();
+ var editor = new Mock<IObjectEditor> ();
+ editor.SetupGet (e => e.Target).Returns (target);
+ SetupPropertySetAndGet (editor, property.Object);
+
+ string[] results = new[] { "Foo", "Bar", "Baz" };
+ var complete = editor.As<ICompleteValues> ();
+ complete.Setup (c => c.GetCompletionsAsync (property.Object, It.IsAny<string> (), It.IsAny<CancellationToken> ())).ReturnsAsync (results);
+
+ var mockProvider = new MockEditorProvider (editor.Object);
+ var resources = new MockResourceProvider();
+ var platform = new TargetPlatform (mockProvider, resources) {
+ SupportsCustomExpressions = true
+ };
+
+ var vm = GetViewModel (platform, property.Object, new[] { editor.Object });
+ Assume.That (vm.SupportsAutocomplete, Is.True);
+ vm.PreviewCustomExpression = "preview";
+
+ CollectionAssert.AreEqual (vm.AutocompleteItems, results);
+ complete.Verify (c => c.GetCompletionsAsync (property.Object, "preview", It.IsAny<CancellationToken> ()));
+ }
+
+ [Test]
+ public void AutocompleteCancels ()
+ {
+ var target = new object ();
+ var property = GetPropertyMock ();
+ var editor = new Mock<IObjectEditor> ();
+ editor.SetupGet (e => e.Target).Returns (target);
+ SetupPropertySetAndGet (editor, property.Object);
+
+ string[] results = new[] { "Foo", "Bar", "Baz" };
+ var tcs = new TaskCompletionSource<IReadOnlyList<string>> ();
+
+ var complete = editor.As<ICompleteValues> ();
+ complete.Setup (c => c.GetCompletionsAsync (property.Object, It.IsAny<string> (), It.IsAny<CancellationToken> ()))
+ .Returns<IPropertyInfo,string,CancellationToken> ((a,b,c) => {
+ c.Register (() => {
+ tcs.TrySetCanceled ();
+ });
+ return tcs.Task;
+ });
+
+ var mockProvider = new MockEditorProvider (editor.Object);
+ var resources = new MockResourceProvider ();
+ var platform = new TargetPlatform (mockProvider, resources) {
+ SupportsCustomExpressions = true
+ };
+
+ var vm = GetViewModel (platform, property.Object, new[] { editor.Object });
+ Assume.That (vm.SupportsAutocomplete, Is.True);
+ vm.PreviewCustomExpression = "preview";
+
+ vm.PreviewCustomExpression = "attempt2";
+ Assert.That (tcs.Task.IsCanceled, Is.True);
+ }
+
protected TViewModel GetViewModel (IPropertyInfo property, IObjectEditor editor)
{
return GetViewModel (property, new[] { editor });
diff --git a/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs b/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs
index 28f69b7..d543269 100644
--- a/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs
+++ b/Xamarin.PropertyEditing.Windows.Standalone/MainWindow.xaml.cs
@@ -15,7 +15,8 @@ namespace Xamarin.PropertyEditing.Windows.Standalone
public MainWindow ()
{
InitializeComponent ();
- this.panel.TargetPlatform = new TargetPlatform (new MockEditorProvider(), new MockResourceProvider(), new MockBindingProvider()) {
+ var resources = new MockResourceProvider();
+ this.panel.TargetPlatform = new TargetPlatform (new MockEditorProvider (resources), resources, new MockBindingProvider()) {
SupportsCustomExpressions = true,
SupportsMaterialDesign = true,
SupportsBrushOpacity = false,
@@ -24,7 +25,6 @@ namespace Xamarin.PropertyEditing.Windows.Standalone
}
};
- this.panel.ResourceProvider = new MockResourceProvider ();
#if USE_VS_ICONS
this.panel.Resources.MergedDictionaries.Add (new ResourceDictionary {
Source = new Uri ("pack://application:,,,/ProppyIcons.xaml", UriKind.RelativeOrAbsolute)
diff --git a/Xamarin.PropertyEditing.Windows/EntryPopup.cs b/Xamarin.PropertyEditing.Windows/EntryPopup.cs
index fbc3a05..08db76b 100644
--- a/Xamarin.PropertyEditing.Windows/EntryPopup.cs
+++ b/Xamarin.PropertyEditing.Windows/EntryPopup.cs
@@ -1,6 +1,5 @@
using System;
using System.Windows;
-using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
@@ -9,6 +8,20 @@ namespace Xamarin.PropertyEditing.Windows
internal class EntryPopup
: Popup
{
+ static EntryPopup ()
+ {
+ // Autocomplete is a popup in a popup and when you go to click the other popup, this one might close
+ // so we need to hack around this otherwise uncontrollable behavior (StaysOpen has no effect).
+ var existing = IsOpenProperty.GetMetadata (typeof(Popup));
+ PropertyChangedCallback callback = (o,e) => {
+ if ((bool) e.NewValue || ((EntryPopup) o).CanClose ()) {
+ existing.PropertyChangedCallback (o, e);
+ }
+ };
+
+ IsOpenProperty.OverrideMetadata(typeof(EntryPopup), new FrameworkPropertyMetadata (existing.DefaultValue, callback, existing.CoerceValueCallback));
+ }
+
public static readonly DependencyProperty ContentTemplateProperty = DependencyProperty.Register (
nameof(ContentTemplate), typeof(DataTemplate), typeof(EntryPopup), new PropertyMetadata ((s,e) => ((EntryPopup)s).UpdateContentTemplate()));
@@ -18,6 +31,15 @@ namespace Xamarin.PropertyEditing.Windows
set { SetValue (ContentTemplateProperty, value); }
}
+ public static readonly DependencyProperty ValueProperty = DependencyProperty.Register (
+ "Value", typeof(string), typeof(EntryPopup), new PropertyMetadata (default(string)));
+
+ public string Value
+ {
+ get { return (string) GetValue (ValueProperty); }
+ set { SetValue (ValueProperty, value); }
+ }
+
public override void OnApplyTemplate ()
{
base.OnApplyTemplate ();
@@ -33,15 +55,20 @@ namespace Xamarin.PropertyEditing.Windows
protected override void OnClosed (EventArgs e)
{
if (!this.closingFromEscape) {
- this.textBox.GetBindingExpression (TextBox.TextProperty)?.UpdateSource();
+ GetBindingExpression (ValueProperty)?.UpdateSource ();
} else
this.closingFromEscape = false;
base.OnClosed (e);
}
- protected override void OnPreviewKeyDown (KeyEventArgs e)
+ protected override void OnKeyDown (KeyEventArgs e)
{
+ base.OnKeyDown (e);
+
+ if (e.Handled)
+ return;
+
if (e.Key == Key.Escape) {
this.closingFromEscape = true;
IsOpen = false;
@@ -50,22 +77,25 @@ namespace Xamarin.PropertyEditing.Windows
IsOpen = false;
e.Handled = true;
}
-
- base.OnPreviewKeyDown (e);
}
- private TextBox textBox;
+ private TextBoxEx textBox;
private bool closingFromEscape;
+ private bool CanClose ()
+ {
+ return this.textBox.CanCloseParent ();
+ }
+
private void UpdateContentTemplate()
{
Child = ContentTemplate?.LoadContent() as UIElement;
if (Child == null)
return;
- this.textBox = ((FrameworkElement)Child)?.FindName ("entry") as TextBox;
+ this.textBox = ((FrameworkElement)Child)?.FindName ("entry") as TextBoxEx;
if (this.textBox == null)
- throw new InvalidOperationException ("Need an entry TextBox for EntryPopup");
+ throw new InvalidOperationException ("Need an entry TextBoxEx for EntryPopup");
}
}
} \ No newline at end of file
diff --git a/Xamarin.PropertyEditing.Windows/TextBoxEx.cs b/Xamarin.PropertyEditing.Windows/TextBoxEx.cs
index 295ea32..74863fe 100644
--- a/Xamarin.PropertyEditing.Windows/TextBoxEx.cs
+++ b/Xamarin.PropertyEditing.Windows/TextBoxEx.cs
@@ -1,19 +1,19 @@
using System;
+using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace Xamarin.PropertyEditing.Windows
{
[TemplatePart (Name ="PART_Clear", Type = typeof (Button))]
+ [TemplatePart (Name = "PART_CompletePopup", Type = typeof (Popup))]
+ [TemplatePart (Name = "PART_CompleteList", Type = typeof (ListBox))]
internal class TextBoxEx
: TextBox
{
- public TextBoxEx()
- {
- PreviewKeyDown += OnPreviewKeyDown;
- }
public static readonly DependencyProperty HintProperty = DependencyProperty.Register (
"Hint", typeof(object), typeof(TextBoxEx), new PropertyMetadata (default(object)));
@@ -42,6 +42,15 @@ namespace Xamarin.PropertyEditing.Windows
set { SetValue (FocusSelectsAllProperty, value); }
}
+ public static readonly DependencyProperty EnableClearProperty = DependencyProperty.Register (
+ "EnableClear", typeof(bool), typeof(TextBoxEx), new PropertyMetadata (true));
+
+ public bool EnableClear
+ {
+ get { return (bool) GetValue (EnableClearProperty); }
+ set { SetValue (EnableClearProperty, value); }
+ }
+
public static readonly DependencyProperty ShowClearButtonProperty = DependencyProperty.Register (
"ShowClearButton", typeof(bool), typeof(TextBoxEx), new PropertyMetadata (default(bool)));
@@ -60,6 +69,15 @@ namespace Xamarin.PropertyEditing.Windows
set { SetValue (ClearButtonStyleProperty, value); }
}
+ public static readonly DependencyProperty EnableSubmitProperty = DependencyProperty.Register (
+ "EnableSubmit", typeof(bool), typeof(TextBoxEx), new PropertyMetadata (true));
+
+ public bool EnableSubmit
+ {
+ get { return (bool) GetValue (EnableSubmitProperty); }
+ set { SetValue (EnableSubmitProperty, value); }
+ }
+
public static readonly DependencyProperty SubmitButtonStyleProperty = DependencyProperty.Register (
"SubmitButtonStyle", typeof(Style), typeof(TextBoxEx), new PropertyMetadata (default(Style)));
@@ -69,6 +87,24 @@ namespace Xamarin.PropertyEditing.Windows
set { SetValue (SubmitButtonStyleProperty, value); }
}
+ public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register (
+ "ItemsSource", typeof (IEnumerable), typeof (TextBoxEx), new PropertyMetadata (default (IEnumerable)));
+
+ public IEnumerable ItemsSource
+ {
+ get { return (IEnumerable)GetValue (ItemsSourceProperty); }
+ set { SetValue (ItemsSourceProperty, value); }
+ }
+
+ public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register (
+ "ItemTemplate", typeof (DataTemplate), typeof (TextBoxEx), new PropertyMetadata (default (DataTemplate)));
+
+ public DataTemplate ItemTemplate
+ {
+ get { return (DataTemplate)GetValue (ItemTemplateProperty); }
+ set { SetValue (ItemTemplateProperty, value); }
+ }
+
public override void OnApplyTemplate ()
{
base.OnApplyTemplate ();
@@ -81,6 +117,14 @@ namespace Xamarin.PropertyEditing.Windows
return;
}
+ this.popup = (Popup)GetTemplateChild ("PART_CompletePopup");
+ this.list = (ListBox)GetTemplateChild ("PART_CompleteList");
+ if (this.popup == null || this.list == null)
+ throw new InvalidOperationException ("PART_CompletePopup and PART_CompleteList must be present");
+
+ this.list.ItemContainerGenerator.ItemsChanged += OnItemsChanged;
+ this.list.PreviewMouseLeftButtonDown += OnListMouseDown;
+
clear.Click += (sender, e) => {
Clear();
};
@@ -114,8 +158,14 @@ namespace Xamarin.PropertyEditing.Windows
protected override void OnLostKeyboardFocus (KeyboardFocusChangedEventArgs e)
{
+ if (this.defocusFromList)
+ return;
+
base.OnLostKeyboardFocus (e);
- OnSubmit();
+ this.popup.IsOpen = false;
+
+ if (EnableSubmit)
+ OnSubmit ();
}
protected virtual void OnSubmit()
@@ -124,6 +174,64 @@ namespace Xamarin.PropertyEditing.Windows
expression?.UpdateSource ();
}
+ protected override void OnPreviewKeyDown (KeyEventArgs e)
+ {
+ e.Handled = true;
+ if (e.Key == Key.Down) {
+ if (this.list.SelectedIndex == -1 || this.list.SelectedIndex + 1 == this.list.Items.Count)
+ this.list.SelectedIndex = 0;
+ else
+ this.list.SelectedIndex++;
+
+ this.list.ScrollIntoView (this.list.SelectedItem);
+ } else if (e.Key == Key.Up) {
+ if (this.list.SelectedIndex == -1 || this.list.SelectedIndex == 0)
+ this.list.SelectedIndex = this.list.Items.Count - 1;
+ else
+ this.list.SelectedIndex--;
+
+ this.list.ScrollIntoView (this.list.SelectedItem);
+ } else if (e.Key == Key.Enter || e.Key == Key.Tab) {
+ if (this.list.SelectedValue != null) {
+ SelectCompleteItem (this.list.SelectedItem);
+ } else if (!this.popup.IsOpen) {
+ if (EnableSubmit)
+ OnSubmit ();
+ else
+ e.Handled = false;
+ }
+ } else if (e.Key == Key.Escape) {
+ if (this.popup.IsOpen)
+ this.popup.IsOpen = false;
+ else if (EnableClear)
+ Clear ();
+ else
+ e.Handled = false;
+ } else {
+ e.Handled = false;
+ }
+
+ base.OnPreviewKeyDown (e);
+ }
+
+ protected internal bool CanCloseParent ()
+ {
+ bool can = !this.defocusFromList;
+ this.defocusFromList = false;
+ return can;
+ }
+
+ private bool defocusFromList;
+ private Popup popup;
+ private ListBox list;
+
+ private void SelectCompleteItem (object item)
+ {
+ Text = item.ToString ();
+ CaretIndex = Text.Length;
+ this.popup.IsOpen = false;
+ }
+
private void FocusSelect()
{
if (!FocusSelectsAll)
@@ -133,13 +241,27 @@ namespace Xamarin.PropertyEditing.Windows
SelectAll ();
}
- private void OnPreviewKeyDown (object sender, System.Windows.Input.KeyEventArgs e)
+ private void OnItemsChanged (object sender, ItemsChangedEventArgs e)
{
- if (e.Key == Key.Enter) {
- OnSubmit();
- } else if (e.Key == Key.Escape) {
- Clear();
- }
+ if (!HasEffectiveKeyboardFocus)
+ return;
+
+ this.popup.IsOpen = (this.list.Items.Count > 0);
+ if (this.list.SelectedIndex == -1)
+ this.list.SelectedIndex = 0;
+ }
+
+ private void OnListMouseDown (object sender, MouseButtonEventArgs e)
+ {
+ Point pos = e.GetPosition (this.list);
+ var element = this.list.InputHitTest (pos) as FrameworkElement;
+ var item = element?.FindParentOrSelf<ListBoxItem> ();
+ if (item == null)
+ return;
+
+ SelectCompleteItem (item.DataContext);
+ this.defocusFromList = true;
+ e.Handled = true;
}
}
}
diff --git a/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml b/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml
index 0772b60..75b5b92 100644
--- a/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml
+++ b/Xamarin.PropertyEditing.Windows/Themes/Resources.xaml
@@ -497,7 +497,8 @@
<Style x:Key="CustomExpressionPopup" TargetType="local:EntryPopup">
<Setter Property="Placement" Value="Bottom" />
<Setter Property="Width" Value="400" />
- <Setter Property="StaysOpen" Value="False" />
+ <Setter Property="StaysOpen" Value="True" />
+ <Setter Property="Value" Value="{Binding CustomExpression,Mode=TwoWay,UpdateSourceTrigger=Explicit}" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
@@ -509,7 +510,7 @@
</Grid.RowDefinitions>
<TextBlock Name="label" Grid.Row="0" Margin="5,5,0,5" FontWeight="Bold" Text="{x:Static prop:Resources.CustomExpression}" />
- <local:TextBoxEx x:Name="entry" Grid.Row="1" Margin="5,0,5,5" AutomationProperties.LabeledBy="{Binding Mode=OneTime,ElementName=label}" FocusSelectsAll="True" Text="{Binding CustomExpression,Mode=TwoWay,UpdateSourceTrigger=Explicit}" />
+ <local:TextBoxEx x:Name="entry" Grid.Row="1" Margin="5,0,5,5" AutomationProperties.LabeledBy="{Binding Mode=OneTime,ElementName=label}" FocusSelectsAll="True" EnableClear="False" EnableSubmit="False" Text="{Binding PreviewCustomExpression,Mode=OneWayToSource,UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding AutocompleteItems}" />
</Grid>
</Border>
</DataTemplate>
@@ -1631,8 +1632,6 @@
<Style TargetType="ListBoxItem">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Padding" Value="4,1"/>
- <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
- <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Foreground" Value="{DynamicResource ListItemForegroundBrush}" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
@@ -1942,21 +1941,26 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TextBoxEx}">
- <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
- <Grid>
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="*" />
- <ColumnDefinition Width="Auto" />
- </Grid.ColumnDefinitions>
+ <Grid>
+ <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*" />
+ <ColumnDefinition Width="Auto" />
+ </Grid.ColumnDefinitions>
- <ContentPresenter Grid.Column="0" Name="hintContent" Margin="2,0,2,0" Visibility="Collapsed" ContentSource="Hint" />
- <ScrollViewer x:Name="PART_ContentHost" Grid.Column="0" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
+ <ContentPresenter Grid.Column="0" Name="hintContent" Margin="2,0,2,0" Visibility="Collapsed" ContentSource="Hint" />
+ <ScrollViewer x:Name="PART_ContentHost" Grid.Column="0" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
- <Button Name="PART_Clear" Grid.Column="1" Style="{TemplateBinding ClearButtonStyle}" Visibility="{TemplateBinding ShowClearButton,Converter={StaticResource BoolToVisibilityConverter}}" />
- <Button Name="PART_Submit" Grid.Column="1" Style="{TemplateBinding SubmitButtonStyle}" Visibility="Collapsed" />
- </Grid>
- </Border>
- <ControlTemplate.Triggers>
+ <Button Name="PART_Clear" Grid.Column="1" Style="{TemplateBinding ClearButtonStyle}" Visibility="{TemplateBinding ShowClearButton,Converter={StaticResource BoolToVisibilityConverter}}" />
+ <Button Name="PART_Submit" Grid.Column="1" Style="{TemplateBinding SubmitButtonStyle}" Visibility="Collapsed" />
+ </Grid>
+ </Border>
+ <Popup Name="PART_CompletePopup" Placement="Bottom" StaysOpen="True" Focusable="False" Width="{Binding ElementName=border,Path=ActualWidth}">
+ <ListBox Name="PART_CompleteList" Focusable="False" MaxHeight="200" ItemsSource="{TemplateBinding ItemsSource}" ItemTemplate="{TemplateBinding ItemTemplate}" />
+ </Popup>
+ </Grid>
+ <ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Opacity" TargetName="border" Value="0.56"/>
</Trigger>
diff --git a/Xamarin.PropertyEditing/ICompleteValues.cs b/Xamarin.PropertyEditing/ICompleteValues.cs
new file mode 100644
index 0000000..c9e7a88
--- /dev/null
+++ b/Xamarin.PropertyEditing/ICompleteValues.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Xamarin.PropertyEditing
+{
+ /// <summary>
+ /// Light-up interface to handle auto-completion of values.
+ /// </summary>
+ /// <remarks>
+ /// To be implemented on <see cref="IObjectEditor"/> implementations. Requires <see cref="TargetPlatform.SupportsCustomExpressions"/>.
+ /// </remarks>
+ public interface ICompleteValues
+ {
+ /// <summary>
+ /// Gets whether or not the string can be looked at for auto-completion.
+ /// </summary>
+ /// <returns>
+ /// <c>true</c> if <paramref name="input"/> contains auto-completable markers, <c>false</c> otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>
+ /// This is not an indicator of whether a value to autocomplete the <paramref name="input"/> exists, but
+ /// instead to indicate whether it is a special string that has auto-completion implications. Examples
+ /// of this are '@' in Android, or '{' in XAML languages.
+ /// </para>
+ /// <para>
+ /// Additionally this is not the only gating mechanic for auto-completion. UI elements that would always
+ /// auto-complete may bypass this check altogether. Instead this is for inputs that may not auto-complete,
+ /// but could be enabled to given the correct input.
+ /// </para>
+ /// </remarks>
+ bool CanAutocomplete (string input);
+
+ /// <returns>
+ /// A list of strings to be used to set the value of the <paramref name="property"/> if selected via <see cref="ValueInfo.CustomExpression"/>.
+ /// </returns>
+ /// <remarks>
+ /// Returned strings are not post-sorted nor count-trimmed. Across multiple editors, only the common elements will be present
+ /// in order of the first editor. The general assumption is that order would likely be the same across editors.
+ /// </remarks>
+ Task<IReadOnlyList<string>> GetCompletionsAsync (IPropertyInfo property, string input, CancellationToken cancellationToken);
+ }
+}
diff --git a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs
index 4e353c8..605b8c5 100644
--- a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs
+++ b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
@@ -77,6 +78,32 @@ namespace Xamarin.PropertyEditing.ViewModels
}
}
+ public bool SupportsAutocomplete
+ {
+ get { return this.supportsAutocomplete; }
+ private set
+ {
+ if (this.supportsAutocomplete == value)
+ return;
+
+ this.supportsAutocomplete = value;
+ OnPropertyChanged();
+
+ if (!value) {
+ this.autocomplete = null;
+ this.autocompleteCancel?.Cancel();
+ this.autocompleteCancel = null;
+ }
+ }
+ }
+
+ public IReadOnlyList<string> AutocompleteItems => this.autocomplete;
+
+ public string PreviewCustomExpression
+ {
+ set { UpdateAutocomplete (value); }
+ }
+
public string CustomExpression
{
get { return this.value?.CustomExpression; }
@@ -228,6 +255,23 @@ namespace Xamarin.PropertyEditing.ViewModels
}
}
+ protected override void OnEditorsChanged (object sender, NotifyCollectionChangedEventArgs e)
+ {
+ base.OnEditorsChanged (sender, e);
+
+ if (e.Action == NotifyCollectionChangedAction.Add && SupportsAutocomplete)
+ return;
+
+ if (TargetPlatform.SupportsCustomExpressions) {
+ foreach (IObjectEditor editor in Editors) {
+ if (editor is ICompleteValues) {
+ SupportsAutocomplete = true;
+ break;
+ }
+ }
+ }
+ }
+
private readonly ICoerce<TValue> coerce;
private readonly IValidator<TValue> validator;
private readonly ICanNavigateToSource valueNavigator;
@@ -235,6 +279,10 @@ namespace Xamarin.PropertyEditing.ViewModels
private bool isNullable;
private ValueInfo<TValue> value;
+ private bool supportsAutocomplete;
+ private ObservableCollectionEx<string> autocomplete;
+ private CancellationTokenSource autocompleteCancel;
+
private void SignalValueChange ()
{
OnPropertyChanged (nameof (Value));
@@ -359,6 +407,49 @@ namespace Xamarin.PropertyEditing.ViewModels
});
}
+ private async void UpdateAutocomplete (string value)
+ {
+ if (!SupportsAutocomplete)
+ return;
+
+ if (this.autocomplete == null) {
+ this.autocomplete = new ObservableCollectionEx<string> ();
+ OnPropertyChanged (nameof(AutocompleteItems));
+ } else {
+ this.autocompleteCancel.Cancel();
+ }
+
+ this.autocompleteCancel = new CancellationTokenSource ();
+ CancellationToken cancel = this.autocompleteCancel.Token;
+
+ try {
+ HashSet<string> common = null;
+ List<Task<IReadOnlyList<string>>> tasks = new List<Task<IReadOnlyList<string>>> ();
+
+ foreach (IObjectEditor editor in Editors) {
+ if (!(editor is ICompleteValues complete))
+ continue;
+
+ tasks.Add (complete.GetCompletionsAsync (Property, value, cancel));
+ }
+
+ IReadOnlyList<string> list = null;
+ do {
+ Task<IReadOnlyList<string>> results = await Task.WhenAny (tasks);
+ tasks.Remove (results);
+
+ if (list == null) {
+ list = results.Result;
+ common = new HashSet<string> (list);
+ } else
+ common.IntersectWith (results.Result);
+ } while (tasks.Count > 0 && !cancel.IsCancellationRequested);
+
+ this.autocomplete.Reset (list.Where (common.Contains));
+ } catch (OperationCanceledException) {
+ }
+ }
+
private static TValue DefaultValue;
}
diff --git a/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj b/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj
index 45486a8..22785d8 100644
--- a/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj
+++ b/Xamarin.PropertyEditing/Xamarin.PropertyEditing.csproj
@@ -81,6 +81,7 @@
<Compile Include="IClampedPropertyInfo.cs" />
<Compile Include="IColorSpaced.cs" />
<Compile Include="ICoerce.cs" />
+ <Compile Include="ICompleteValues.cs" />
<Compile Include="IEditorProvider.cs" />
<Compile Include="IObjectEventEditor.cs" />
<Compile Include="IEventInfo.cs" />