diff --git a/readme.md b/readme.md
index eeee39dabe..96cfde3eb2 100644
--- a/readme.md
+++ b/readme.md
@@ -35,7 +35,7 @@ https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts
## Documentation
-As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/guides/quickstart) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
+As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index a3d7a0cdce..862de9d320 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -41,6 +41,9 @@
Designer
+
+ Designer
+
Designer
@@ -78,6 +81,9 @@
Designer
+
+ Designer
+
Designer
@@ -113,6 +119,9 @@
BorderPage.xaml
+
+ AutoCompleteBoxPage.xaml
+
ButtonPage.xaml
@@ -169,6 +178,9 @@
ButtonSpinnerPage.xaml
+
+
+ NumericUpDownPage.xaml
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 142d0d42b1..a0e0df450b 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -5,10 +5,11 @@
+
-
+
@@ -19,6 +20,7 @@
+
@@ -26,4 +28,4 @@
-
\ No newline at end of file
+
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
new file mode 100644
index 0000000000..943fadf100
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
@@ -0,0 +1,59 @@
+
+
+ AutoCompleteBox
+ A control into which the user can input text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
new file mode 100644
index 0000000000..6f3b8361cd
--- /dev/null
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs
@@ -0,0 +1,143 @@
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.Markup;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace ControlCatalog.Pages
+{
+ public class AutoCompleteBoxPage : UserControl
+ {
+ public class StateData
+ {
+ public string Name { get; private set; }
+ public string Abbreviation { get; private set; }
+ public string Capital { get; private set; }
+
+ public StateData(string name, string abbreviatoin, string capital)
+ {
+ Name = name;
+ Abbreviation = abbreviatoin;
+ Capital = capital;
+ }
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+
+ private StateData[] BuildAllStates()
+ {
+ return new StateData[]
+ {
+ new StateData("Alabama","AL","Montgomery"),
+ new StateData("Alaska","AK","Juneau"),
+ new StateData("Arizona","AZ","Phoenix"),
+ new StateData("Arkansas","AR","Little Rock"),
+ new StateData("California","CA","Sacramento"),
+ new StateData("Colorado","CO","Denver"),
+ new StateData("Connecticut","CT","Hartford"),
+ new StateData("Delaware","DE","Dover"),
+ new StateData("Florida","FL","Tallahassee"),
+ new StateData("Georgia","GA","Atlanta"),
+ new StateData("Hawaii","HI","Honolulu"),
+ new StateData("Idaho","ID","Boise"),
+ new StateData("Illinois","IL","Springfield"),
+ new StateData("Indiana","IN","Indianapolis"),
+ new StateData("Iowa","IA","Des Moines"),
+ new StateData("Kansas","KS","Topeka"),
+ new StateData("Kentucky","KY","Frankfort"),
+ new StateData("Louisiana","LA","Baton Rouge"),
+ new StateData("Maine","ME","Augusta"),
+ new StateData("Maryland","MD","Annapolis"),
+ new StateData("Massachusetts","MA","Boston"),
+ new StateData("Michigan","MI","Lansing"),
+ new StateData("Minnesota","MN","St. Paul"),
+ new StateData("Mississippi","MS","Jackson"),
+ new StateData("Missouri","MO","Jefferson City"),
+ new StateData("Montana","MT","Helena"),
+ new StateData("Nebraska","NE","Lincoln"),
+ new StateData("Nevada","NV","Carson City"),
+ new StateData("New Hampshire","NH","Concord"),
+ new StateData("New Jersey","NJ","Trenton"),
+ new StateData("New Mexico","NM","Santa Fe"),
+ new StateData("New York","NY","Albany"),
+ new StateData("North Carolina","NC","Raleigh"),
+ new StateData("North Dakota","ND","Bismarck"),
+ new StateData("Ohio","OH","Columbus"),
+ new StateData("Oklahoma","OK","Oklahoma City"),
+ new StateData("Oregon","OR","Salem"),
+ new StateData("Pennsylvania","PA","Harrisburg"),
+ new StateData("Rhode Island","RI","Providence"),
+ new StateData("South Carolina","SC","Columbia"),
+ new StateData("South Dakota","SD","Pierre"),
+ new StateData("Tennessee","TN","Nashville"),
+ new StateData("Texas","TX","Austin"),
+ new StateData("Utah","UT","Salt Lake City"),
+ new StateData("Vermont","VT","Montpelier"),
+ new StateData("Virginia","VA","Richmond"),
+ new StateData("Washington","WA","Olympia"),
+ new StateData("West Virginia","WV","Charleston"),
+ new StateData("Wisconsin","WI","Madison"),
+ new StateData("Wyoming","WY","Cheyenne"),
+ };
+ }
+ public StateData[] States { get; private set; }
+
+ public AutoCompleteBoxPage()
+ {
+ this.InitializeComponent();
+
+ States = BuildAllStates();
+
+ foreach (AutoCompleteBox box in GetAllAutoCompleteBox())
+ {
+ box.Items = States;
+ }
+
+ var converter = new FuncMultiValueConverter(parts =>
+ {
+ return String.Format("{0} ({1})", parts.ToArray());
+ });
+ var binding = new MultiBinding { Converter = converter };
+ binding.Bindings.Add(new Binding("Name"));
+ binding.Bindings.Add(new Binding("Abbreviation"));
+
+ var multibindingBox = this.FindControl("MultiBindingBox");
+ multibindingBox.ValueMemberBinding = binding;
+
+ var asyncBox = this.FindControl("AsyncBox");
+ asyncBox.AsyncPopulator = PopulateAsync;
+ }
+ private IEnumerable GetAllAutoCompleteBox()
+ {
+ return
+ this.GetLogicalDescendants()
+ .OfType();
+ }
+
+ private bool StringContains(string str, string query)
+ {
+ return str.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+ private async Task> PopulateAsync(string searchText, CancellationToken cancellationToken)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(1.5), cancellationToken);
+
+ return
+ States.Where(data => StringContains(data.Name, searchText) || StringContains(data.Capital, searchText))
+ .ToList();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
new file mode 100644
index 0000000000..a5c911f47d
--- /dev/null
+++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
@@ -0,0 +1,80 @@
+
+
+ Numeric up-down control
+ Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.
+
+ Features:
+
+
+ ShowButtonSpinner:
+
+
+ IsReadOnly:
+
+
+ AllowSpin:
+
+
+ ClipValueToMinMax:
+
+
+
+
+ FormatString:
+
+
+
+
+
+
+
+
+
+
+
+
+ ButtonSpinnerLocation:
+
+
+ CultureInfo:
+
+
+ Watermark:
+
+
+ Text:
+
+
+
+ Minimum:
+
+
+ Maximum:
+
+
+ Increment:
+
+
+ Value:
+
+
+
+
+
+
+ Usage of NumericUpDown:
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
new file mode 100644
index 0000000000..92da64d87e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Markup.Xaml;
+using ReactiveUI;
+
+namespace ControlCatalog.Pages
+{
+ public class NumericUpDownPage : UserControl
+ {
+ public NumericUpDownPage()
+ {
+ this.InitializeComponent();
+ var viewModel = new NumbersPageViewModel();
+ DataContext = viewModel;
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ }
+
+ public class NumbersPageViewModel : ReactiveObject
+ {
+ private IList _formats;
+ private FormatObject _selectedFormat;
+ private IList _spinnerLocations;
+
+ public NumbersPageViewModel()
+ {
+ SelectedFormat = Formats.FirstOrDefault();
+ }
+
+ public IList Formats
+ {
+ get
+ {
+ return _formats ?? (_formats = new List()
+ {
+ new FormatObject() {Name = "Currency", Value = "C2"},
+ new FormatObject() {Name = "Fixed point", Value = "F2"},
+ new FormatObject() {Name = "General", Value = "G"},
+ new FormatObject() {Name = "Number", Value = "N"},
+ new FormatObject() {Name = "Percent", Value = "P"},
+ new FormatObject() {Name = "Degrees", Value = "{0:N2} °"},
+ });
+ }
+ }
+
+ public IList SpinnerLocations
+ {
+ get
+ {
+ if (_spinnerLocations == null)
+ {
+ _spinnerLocations = new List();
+ foreach (Location value in Enum.GetValues(typeof(Location)))
+ {
+ _spinnerLocations.Add(value);
+ }
+ }
+ return _spinnerLocations ;
+ }
+ }
+
+ public IList Cultures { get; } = new List()
+ {
+ new CultureInfo("en-US"),
+ new CultureInfo("en-GB"),
+ new CultureInfo("fr-FR"),
+ new CultureInfo("ar-DZ"),
+ new CultureInfo("zh-CN"),
+ new CultureInfo("cs-CZ")
+ };
+
+ public FormatObject SelectedFormat
+ {
+ get { return _selectedFormat; }
+ set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); }
+ }
+ }
+
+ public class FormatObject
+ {
+ public string Value { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
index 4271d05f91..e0f3e92c74 100644
--- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
+++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
@@ -17,7 +17,9 @@
-
+
+ PreserveNewest
+
diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
index b90dccf74e..84ac85d3db 100644
--- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs
@@ -117,7 +117,7 @@ namespace Avalonia.Collections
_inner = new Dictionary();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[]"));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]"));
if (CollectionChanged != null)
@@ -222,4 +222,4 @@ namespace Avalonia.Collections
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
new file mode 100644
index 0000000000..8e801d606b
--- /dev/null
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -0,0 +1,2726 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Collections;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data for the
+ ///
+ /// event.
+ ///
+ public class PopulatedEventArgs : EventArgs
+ {
+ ///
+ /// Gets the list of possible matches added to the drop-down portion of
+ /// the
+ /// control.
+ ///
+ /// The list of possible matches added to the
+ /// .
+ public IEnumerable Data { get; private set; }
+
+ ///
+ /// Initializes a new instance of the
+ /// .
+ ///
+ /// The list of possible matches added to the
+ /// drop-down portion of the
+ /// control.
+ public PopulatedEventArgs(IEnumerable data)
+ {
+ Data = data;
+ }
+ }
+
+ ///
+ /// Provides data for the
+ ///
+ /// event.
+ ///
+ /// Stable
+ public class PopulatingEventArgs : CancelEventArgs
+ {
+ ///
+ /// Gets the text that is used to determine which items to display in
+ /// the
+ /// control.
+ ///
+ /// The text that is used to determine which items to display in
+ /// the .
+ public string Parameter { get; private set; }
+
+ ///
+ /// Initializes a new instance of the
+ /// .
+ ///
+ /// The value of the
+ ///
+ /// property, which is used to filter items for the
+ /// control.
+ public PopulatingEventArgs(string parameter)
+ {
+ Parameter = parameter;
+ }
+ }
+
+ ///
+ /// Represents the filter used by the
+ /// control to
+ /// determine whether an item is a possible match for the specified text.
+ ///
+ /// true to indicate is a possible match
+ /// for ; otherwise false.
+ /// The string used as the basis for filtering.
+ /// The item that is compared with the
+ /// parameter.
+ /// The type used for filtering the
+ /// . This type can
+ /// be either a string or an object.
+ /// Stable
+ public delegate bool AutoCompleteFilterPredicate(string search, T item);
+
+ ///
+ /// Specifies how text in the text box portion of the
+ /// control is used
+ /// to filter items specified by the
+ ///
+ /// property for display in the drop-down.
+ ///
+ /// Stable
+ public enum AutoCompleteFilterMode
+ {
+ ///
+ /// Specifies that no filter is used. All items are returned.
+ ///
+ None = 0,
+
+ ///
+ /// Specifies a culture-sensitive, case-insensitive filter where the
+ /// returned items start with the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as
+ /// the string comparison criteria.
+ ///
+ StartsWith = 1,
+
+ ///
+ /// Specifies a culture-sensitive, case-sensitive filter where the
+ /// returned items start with the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the string
+ /// comparison criteria.
+ ///
+ StartsWithCaseSensitive = 2,
+
+ ///
+ /// Specifies an ordinal, case-insensitive filter where the returned
+ /// items start with the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the
+ /// string comparison criteria.
+ ///
+ StartsWithOrdinal = 3,
+
+ ///
+ /// Specifies an ordinal, case-sensitive filter where the returned items
+ /// start with the specified text. The filter uses the
+ ///
+ /// method, specifying as
+ /// the string comparison criteria.
+ ///
+ StartsWithOrdinalCaseSensitive = 4,
+
+ ///
+ /// Specifies a culture-sensitive, case-insensitive filter where the
+ /// returned items contain the specified text.
+ ///
+ Contains = 5,
+
+ ///
+ /// Specifies a culture-sensitive, case-sensitive filter where the
+ /// returned items contain the specified text.
+ ///
+ ContainsCaseSensitive = 6,
+
+ ///
+ /// Specifies an ordinal, case-insensitive filter where the returned
+ /// items contain the specified text.
+ ///
+ ContainsOrdinal = 7,
+
+ ///
+ /// Specifies an ordinal, case-sensitive filter where the returned items
+ /// contain the specified text.
+ ///
+ ContainsOrdinalCaseSensitive = 8,
+
+ ///
+ /// Specifies a culture-sensitive, case-insensitive filter where the
+ /// returned items equal the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as
+ /// the search comparison criteria.
+ ///
+ Equals = 9,
+
+ ///
+ /// Specifies a culture-sensitive, case-sensitive filter where the
+ /// returned items equal the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the string
+ /// comparison criteria.
+ ///
+ EqualsCaseSensitive = 10,
+
+ ///
+ /// Specifies an ordinal, case-insensitive filter where the returned
+ /// items equal the specified text. The filter uses the
+ ///
+ /// method, specifying
+ /// as the
+ /// string comparison criteria.
+ ///
+ EqualsOrdinal = 11,
+
+ ///
+ /// Specifies an ordinal, case-sensitive filter where the returned items
+ /// equal the specified text. The filter uses the
+ ///
+ /// method, specifying as
+ /// the string comparison criteria.
+ ///
+ EqualsOrdinalCaseSensitive = 12,
+
+ ///
+ /// Specifies that a custom filter is used. This mode is used when the
+ ///
+ /// or
+ ///
+ /// properties are set.
+ ///
+ Custom = 13,
+ }
+
+ ///
+ /// Represents a control that provides a text box for user input and a
+ /// drop-down that contains possible matches based on the input in the text
+ /// box.
+ ///
+ public class AutoCompleteBox : TemplatedControl
+ {
+ ///
+ /// Specifies the name of the selection adapter TemplatePart.
+ ///
+ private const string ElementSelectionAdapter = "PART_SelectionAdapter";
+
+ ///
+ /// Specifies the name of the Selector TemplatePart.
+ ///
+ private const string ElementSelector = "PART_SelectingItemsControl";
+
+ ///
+ /// Specifies the name of the Popup TemplatePart.
+ ///
+ private const string ElementPopup = "PART_Popup";
+
+ ///
+ /// The name for the text box part.
+ ///
+ private const string ElementTextBox = "PART_TextBox";
+
+ private IEnumerable _itemsEnumerable;
+
+ ///
+ /// Gets or sets a local cached copy of the items data.
+ ///
+ private List _items;
+
+ ///
+ /// Gets or sets the observable collection that contains references to
+ /// all of the items in the generated view of data that is provided to
+ /// the selection-style control adapter.
+ ///
+ private AvaloniaList _view;
+
+ ///
+ /// Gets or sets a value to ignore a number of pending change handlers.
+ /// The value is decremented after each use. This is used to reset the
+ /// value of properties without performing any of the actions in their
+ /// change handlers.
+ ///
+ /// The int is important as a value because the TextBox
+ /// TextChanged event does not immediately fire, and this will allow for
+ /// nested property changes to be ignored.
+ private int _ignoreTextPropertyChange;
+
+ ///
+ /// Gets or sets a value indicating whether to ignore calling a pending
+ /// change handlers.
+ ///
+ private bool _ignorePropertyChange;
+
+ ///
+ /// Gets or sets a value indicating whether to ignore the selection
+ /// changed event.
+ ///
+ private bool _ignoreTextSelectionChange;
+
+ ///
+ /// Gets or sets a value indicating whether to skip the text update
+ /// processing when the selected item is updated.
+ ///
+ private bool _skipSelectedItemTextUpdate;
+
+ ///
+ /// Gets or sets the last observed text box selection start location.
+ ///
+ private int _textSelectionStart;
+
+ ///
+ /// Gets or sets a value indicating whether the user initiated the
+ /// current populate call.
+ ///
+ private bool _userCalledPopulate;
+
+ ///
+ /// A value indicating whether the popup has been opened at least once.
+ ///
+ private bool _popupHasOpened;
+
+ ///
+ /// Gets or sets the DispatcherTimer used for the MinimumPopulateDelay
+ /// condition for auto completion.
+ ///
+ private DispatcherTimer _delayTimer;
+
+ ///
+ /// Gets or sets a value indicating whether a read-only dependency
+ /// property change handler should allow the value to be set. This is
+ /// used to ensure that read-only properties cannot be changed via
+ /// SetValue, etc.
+ ///
+ private bool _allowWrite;
+
+ ///
+ /// The TextBox template part.
+ ///
+ private TextBox _textBox;
+ private IDisposable _textBoxSubscriptions;
+
+ ///
+ /// The SelectionAdapter.
+ ///
+ private ISelectionAdapter _adapter;
+
+ ///
+ /// A control that can provide updated string values from a binding.
+ ///
+ private BindingEvaluator _valueBindingEvaluator;
+
+ ///
+ /// A weak subscription for the collection changed event.
+ ///
+ private IDisposable _collectionChangeSubscription;
+
+ private IMemberSelector _valueMemberSelector;
+ private Func>> _asyncPopulator;
+ private CancellationTokenSource _populationCancellationTokenSource;
+
+ private bool _itemTemplateIsFromValueMemeberBinding = true;
+ private bool _settingItemTemplateFromValueMemeberBinding;
+
+ private object _selectedItem;
+ private bool _isDropDownOpen;
+ private bool _isFocused = false;
+
+ private string _text = string.Empty;
+ private string _searchText = string.Empty;
+
+ private AutoCompleteFilterPredicate _itemFilter;
+ private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith);
+
+ public static readonly RoutedEvent SelectionChangedEvent =
+ RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox));
+
+ public static readonly StyledProperty WatermarkProperty =
+ TextBox.WatermarkProperty.AddOwner();
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty MinimumPrefixLengthProperty =
+ AvaloniaProperty.Register(
+ nameof(MinimumPrefixLength), 1,
+ validate: ValidateMinimumPrefixLength);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty MinimumPopulateDelayProperty =
+ AvaloniaProperty.Register(
+ nameof(MinimumPopulateDelay),
+ TimeSpan.Zero,
+ validate: ValidateMinimumPopulateDelay);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty MaxDropDownHeightProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxDropDownHeight),
+ double.PositiveInfinity,
+ validate: ValidateMaxDropDownHeight);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty IsTextCompletionEnabledProperty =
+ AvaloniaProperty.Register(nameof(IsTextCompletionEnabled));
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly StyledProperty ItemTemplateProperty =
+ AvaloniaProperty.Register(nameof(ItemTemplate));
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty IsDropDownOpenProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(IsDropDownOpen),
+ o => o.IsDropDownOpen,
+ (o, v) => o.IsDropDownOpen = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty SelectedItemProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SelectedItem),
+ o => o.SelectedItem,
+ (o, v) => o.SelectedItem = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty TextProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Text),
+ o => o.Text,
+ (o, v) => o.Text = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty SearchTextProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SearchText),
+ o => o.SearchText,
+ unsetValue: string.Empty);
+
+ ///
+ /// Gets the identifier for the
+ ///
+ /// dependency property.
+ ///
+ public static readonly StyledProperty FilterModeProperty =
+ AvaloniaProperty.Register(
+ nameof(FilterMode),
+ defaultValue: AutoCompleteFilterMode.StartsWith,
+ validate: ValidateFilterMode);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty> ItemFilterProperty =
+ AvaloniaProperty.RegisterDirect>(
+ nameof(ItemFilter),
+ o => o.ItemFilter,
+ (o, v) => o.ItemFilter = v);
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty> TextFilterProperty =
+ AvaloniaProperty.RegisterDirect>(
+ nameof(TextFilter),
+ o => o.TextFilter,
+ (o, v) => o.TextFilter = v,
+ unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+ ///
+ /// Identifies the
+ ///
+ /// dependency property.
+ ///
+ /// The identifier for the
+ ///
+ /// dependency property.
+ public static readonly DirectProperty ItemsProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Items),
+ o => o.Items,
+ (o, v) => o.Items = v);
+
+ public static readonly DirectProperty ValueMemberSelectorProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ValueMemberSelector),
+ o => o.ValueMemberSelector,
+ (o, v) => o.ValueMemberSelector = v);
+
+ public static readonly DirectProperty>>> AsyncPopulatorProperty =
+ AvaloniaProperty.RegisterDirect>>>(
+ nameof(AsyncPopulator),
+ o => o.AsyncPopulator,
+ (o, v) => o.AsyncPopulator = v);
+
+ private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value)
+ {
+ Contract.Requires(value >= -1);
+
+ return value;
+ }
+
+ private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value)
+ {
+ Contract.Requires(value.TotalMilliseconds >= 0.0);
+
+ return value;
+ }
+
+ private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value)
+ {
+ Contract.Requires(value >= 0.0);
+
+ return value;
+ }
+
+ private static bool IsValidFilterMode(AutoCompleteFilterMode mode)
+ {
+ switch (mode)
+ {
+ case AutoCompleteFilterMode.None:
+ case AutoCompleteFilterMode.StartsWith:
+ case AutoCompleteFilterMode.StartsWithCaseSensitive:
+ case AutoCompleteFilterMode.StartsWithOrdinal:
+ case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+ case AutoCompleteFilterMode.Contains:
+ case AutoCompleteFilterMode.ContainsCaseSensitive:
+ case AutoCompleteFilterMode.ContainsOrdinal:
+ case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+ case AutoCompleteFilterMode.Equals:
+ case AutoCompleteFilterMode.EqualsCaseSensitive:
+ case AutoCompleteFilterMode.EqualsOrdinal:
+ case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+ case AutoCompleteFilterMode.Custom:
+ return true;
+ default:
+ return false;
+ }
+ }
+ private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value)
+ {
+ Contract.Requires(IsValidFilterMode(value));
+
+ return value;
+ }
+
+ ///
+ /// Handle the change of the IsEnabled property.
+ ///
+ /// The event data.
+ private void OnControlIsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ bool isEnabled = (bool)e.NewValue;
+ if (!isEnabled)
+ {
+ IsDropDownOpen = false;
+ }
+ }
+
+ ///
+ /// MinimumPopulateDelayProperty property changed handler. Any current
+ /// dispatcher timer will be stopped. The timer will not be restarted
+ /// until the next TextUpdate call by the user.
+ ///
+ /// Event arguments.
+ private void OnMinimumPopulateDelayChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var newValue = (TimeSpan)e.NewValue;
+
+ // Stop any existing timer
+ if (_delayTimer != null)
+ {
+ _delayTimer.Stop();
+
+ if (newValue == TimeSpan.Zero)
+ {
+ _delayTimer = null;
+ }
+ }
+
+ if (newValue > TimeSpan.Zero)
+ {
+ // Create or clear a dispatcher timer instance
+ if (_delayTimer == null)
+ {
+ _delayTimer = new DispatcherTimer();
+ _delayTimer.Tick += PopulateDropDown;
+ }
+
+ // Set the new tick interval
+ _delayTimer.Interval = newValue;
+ }
+ }
+
+ ///
+ /// IsDropDownOpenProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ // Ignore the change if requested
+ if (_ignorePropertyChange)
+ {
+ _ignorePropertyChange = false;
+ return;
+ }
+
+ bool oldValue = (bool)e.OldValue;
+ bool newValue = (bool)e.NewValue;
+
+ if (newValue)
+ {
+ TextUpdated(Text, true);
+ }
+ else
+ {
+ ClosingDropDown(oldValue);
+ }
+
+ UpdatePseudoClasses();
+ }
+
+ private void OnSelectedItemPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (_ignorePropertyChange)
+ {
+ _ignorePropertyChange = false;
+ return;
+ }
+
+ // Update the text display
+ if (_skipSelectedItemTextUpdate)
+ {
+ _skipSelectedItemTextUpdate = false;
+ }
+ else
+ {
+ OnSelectedItemChanged(e.NewValue);
+ }
+
+ // Fire the SelectionChanged event
+ List removed = new List();
+ if (e.OldValue != null)
+ {
+ removed.Add(e.OldValue);
+ }
+
+ List added = new List();
+ if (e.NewValue != null)
+ {
+ added.Add(e.NewValue);
+ }
+
+ OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
+ }
+
+ ///
+ /// TextProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ TextUpdated((string)e.NewValue, false);
+ }
+
+ private void OnSearchTextPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (_ignorePropertyChange)
+ {
+ _ignorePropertyChange = false;
+ return;
+ }
+
+ // Ensure the property is only written when expected
+ if (!_allowWrite)
+ {
+ // Reset the old value before it was incorrectly written
+ _ignorePropertyChange = true;
+ SetValue(e.Property, e.OldValue);
+
+ throw new InvalidOperationException("Cannot set read-only property SearchText.");
+ }
+ }
+
+ ///
+ /// FilterModeProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnFilterModePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue;
+
+ // Sets the filter predicate for the new value
+ TextFilter = AutoCompleteSearch.GetFilter(mode);
+ }
+
+ ///
+ /// ItemFilterProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnItemFilterPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ AutoCompleteFilterPredicate value = e.NewValue as AutoCompleteFilterPredicate;
+
+ // If null, revert to the "None" predicate
+ if (value == null)
+ {
+ FilterMode = AutoCompleteFilterMode.None;
+ }
+ else
+ {
+ FilterMode = AutoCompleteFilterMode.Custom;
+ TextFilter = null;
+ }
+ }
+
+ ///
+ /// ItemsSourceProperty property changed handler.
+ ///
+ /// Event arguments.
+ private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ OnItemsChanged((IEnumerable)e.NewValue);
+ }
+
+ private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (!_settingItemTemplateFromValueMemeberBinding)
+ _itemTemplateIsFromValueMemeberBinding = false;
+ }
+ private void OnValueMemberBindingChanged(IBinding value)
+ {
+ if(_itemTemplateIsFromValueMemeberBinding)
+ {
+ var template =
+ new FuncDataTemplate(
+ typeof(object),
+ o =>
+ {
+ var control = new ContentControl();
+ control.Bind(ContentControl.ContentProperty, value);
+ return control;
+ });
+
+ _settingItemTemplateFromValueMemeberBinding = true;
+ ItemTemplate = template;
+ _settingItemTemplateFromValueMemeberBinding = false;
+ }
+ }
+
+ static AutoCompleteBox()
+ {
+ FocusableProperty.OverrideDefaultValue(true);
+
+ MinimumPopulateDelayProperty.Changed.AddClassHandler(x => x.OnMinimumPopulateDelayChanged);
+ IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged);
+ SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemPropertyChanged);
+ TextProperty.Changed.AddClassHandler(x => x.OnTextPropertyChanged);
+ SearchTextProperty.Changed.AddClassHandler(x => x.OnSearchTextPropertyChanged);
+ FilterModeProperty.Changed.AddClassHandler(x => x.OnFilterModePropertyChanged);
+ ItemFilterProperty.Changed.AddClassHandler(x => x.OnItemFilterPropertyChanged);
+ ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged);
+ IsEnabledProperty.Changed.AddClassHandler(x => x.OnControlIsEnabledChanged);
+ }
+
+ ///
+ /// Initializes a new instance of the
+ /// class.
+ ///
+ public AutoCompleteBox()
+ {
+ ClearView();
+ }
+
+ ///
+ /// Gets or sets the minimum number of characters required to be entered
+ /// in the text box before the
+ /// displays
+ /// possible matches.
+ /// matches.
+ ///
+ ///
+ /// The minimum number of characters to be entered in the text box
+ /// before the
+ /// displays possible matches. The default is 1.
+ ///
+ ///
+ /// If you set MinimumPrefixLength to -1, the AutoCompleteBox will
+ /// not provide possible matches. There is no maximum value, but
+ /// setting MinimumPrefixLength to value that is too large will
+ /// prevent the AutoCompleteBox from providing possible matches as well.
+ ///
+ public int MinimumPrefixLength
+ {
+ get { return GetValue(MinimumPrefixLengthProperty); }
+ set { SetValue(MinimumPrefixLengthProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the first possible match
+ /// found during the filtering process will be displayed automatically
+ /// in the text box.
+ ///
+ ///
+ /// True if the first possible match found will be displayed
+ /// automatically in the text box; otherwise, false. The default is
+ /// false.
+ ///
+ public bool IsTextCompletionEnabled
+ {
+ get { return GetValue(IsTextCompletionEnabledProperty); }
+ set { SetValue(IsTextCompletionEnabledProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the used
+ /// to display each item in the drop-down portion of the control.
+ ///
+ /// The used to
+ /// display each item in the drop-down. The default is null.
+ ///
+ /// You use the ItemTemplate property to specify the visualization
+ /// of the data objects in the drop-down portion of the AutoCompleteBox
+ /// control. If your AutoCompleteBox is bound to a collection and you
+ /// do not provide specific display instructions by using a
+ /// DataTemplate, the resulting UI of each item is a string
+ /// representation of each object in the underlying collection.
+ ///
+ public IDataTemplate ItemTemplate
+ {
+ get { return GetValue(ItemTemplateProperty); }
+ set { SetValue(ItemTemplateProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the minimum delay, after text is typed
+ /// in the text box before the
+ /// control
+ /// populates the list of possible matches in the drop-down.
+ ///
+ /// The minimum delay, after text is typed in
+ /// the text box, but before the
+ /// populates
+ /// the list of possible matches in the drop-down. The default is 0.
+ public TimeSpan MinimumPopulateDelay
+ {
+ get { return GetValue(MinimumPopulateDelayProperty); }
+ set { SetValue(MinimumPopulateDelayProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the maximum height of the drop-down portion of the
+ /// control.
+ ///
+ /// The maximum height of the drop-down portion of the
+ /// control.
+ /// The default is .
+ /// The specified value is less than 0.
+ public double MaxDropDownHeight
+ {
+ get { return GetValue(MaxDropDownHeightProperty); }
+ set { SetValue(MaxDropDownHeightProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the drop-down portion of
+ /// the control is open.
+ ///
+ ///
+ /// True if the drop-down is open; otherwise, false. The default is
+ /// false.
+ ///
+ public bool IsDropDownOpen
+ {
+ get { return _isDropDownOpen; }
+ set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); }
+ }
+
+ ///
+ /// Gets or sets the that
+ /// is used to get the values for display in the text portion of
+ /// the
+ /// control.
+ ///
+ /// The object used
+ /// when binding to a collection property.
+ [AssignBinding]
+ public IBinding ValueMemberBinding
+ {
+ get { return _valueBindingEvaluator?.ValueBinding; }
+ set
+ {
+ if (ValueMemberBinding != value)
+ {
+ _valueBindingEvaluator = new BindingEvaluator(value);
+ OnValueMemberBindingChanged(value);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the MemberSelector that is used to get values for
+ /// display in the text portion of the
+ /// control.
+ ///
+ /// The MemberSelector that is used to get values for display in
+ /// the text portion of the
+ /// control.
+ public IMemberSelector ValueMemberSelector
+ {
+ get { return _valueMemberSelector; }
+ set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
+ }
+
+ ///
+ /// Gets or sets the selected item in the drop-down.
+ ///
+ /// The selected item in the drop-down.
+ ///
+ /// If the IsTextCompletionEnabled property is true and text typed by
+ /// the user matches an item in the ItemsSource collection, which is
+ /// then displayed in the text box, the SelectedItem property will be
+ /// a null reference.
+ ///
+ public object SelectedItem
+ {
+ get { return _selectedItem; }
+ set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); }
+ }
+
+ ///
+ /// Gets or sets the text in the text box portion of the
+ /// control.
+ ///
+ /// The text in the text box portion of the
+ /// control.
+ public string Text
+ {
+ get { return _text; }
+ set { SetAndRaise(TextProperty, ref _text, value); }
+ }
+
+ ///
+ /// Gets the text that is used to filter items in the
+ ///
+ /// item collection.
+ ///
+ /// The text that is used to filter items in the
+ ///
+ /// item collection.
+ ///
+ /// The SearchText value is typically the same as the
+ /// Text property, but is set after the TextChanged event occurs
+ /// and before the Populating event.
+ ///
+ public string SearchText
+ {
+ get { return _searchText; }
+ private set
+ {
+ try
+ {
+ _allowWrite = true;
+ SetAndRaise(SearchTextProperty, ref _searchText, value);
+ }
+ finally
+ {
+ _allowWrite = false;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets how the text in the text box is used to filter items
+ /// specified by the
+ ///
+ /// property for display in the drop-down.
+ ///
+ /// One of the
+ ///
+ /// values The default is
+ /// .
+ /// The specified value is
+ /// not a valid
+ /// .
+ ///
+ /// Use the FilterMode property to specify how possible matches are
+ /// filtered. For example, possible matches can be filtered in a
+ /// predefined or custom way. The search mode is automatically set to
+ /// Custom if you set the ItemFilter property.
+ ///
+ public AutoCompleteFilterMode FilterMode
+ {
+ get { return GetValue(FilterModeProperty); }
+ set { SetValue(FilterModeProperty, value); }
+ }
+
+ public string Watermark
+ {
+ get { return GetValue(WatermarkProperty); }
+ set { SetValue(WatermarkProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the custom method that uses user-entered text to filter
+ /// the items specified by the
+ ///
+ /// property for display in the drop-down.
+ ///
+ /// The custom method that uses the user-entered text to filter
+ /// the items specified by the
+ ///
+ /// property. The default is null.
+ ///
+ /// The filter mode is automatically set to Custom if you set the
+ /// ItemFilter property.
+ ///
+ public AutoCompleteFilterPredicate ItemFilter
+ {
+ get { return _itemFilter; }
+ set { SetAndRaise(ItemFilterProperty, ref _itemFilter, value); }
+ }
+
+ ///
+ /// Gets or sets the custom method that uses the user-entered text to
+ /// filter items specified by the
+ ///
+ /// property in a text-based way for display in the drop-down.
+ ///
+ /// The custom method that uses the user-entered text to filter
+ /// items specified by the
+ ///
+ /// property in a text-based way for display in the drop-down.
+ ///
+ /// The search mode is automatically set to Custom if you set the
+ /// TextFilter property.
+ ///
+ public AutoCompleteFilterPredicate TextFilter
+ {
+ get { return _textFilter; }
+ set { SetAndRaise(TextFilterProperty, ref _textFilter, value); }
+ }
+
+ public Func>> AsyncPopulator
+ {
+ get { return _asyncPopulator; }
+ set { SetAndRaise(AsyncPopulatorProperty, ref _asyncPopulator, value); }
+ }
+
+ ///
+ /// Gets or sets a collection that is used to generate the items for the
+ /// drop-down portion of the
+ /// control.
+ ///
+ /// The collection that is used to generate the items of the
+ /// drop-down portion of the
+ /// control.
+ public IEnumerable Items
+ {
+ get { return _itemsEnumerable; }
+ set { SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); }
+ }
+
+ ///
+ /// Gets or sets the drop down popup control.
+ ///
+ private Popup DropDownPopup { get; set; }
+
+ ///
+ /// Gets or sets the Text template part.
+ ///
+ private TextBox TextBox
+ {
+ get { return _textBox; }
+ set
+ {
+ _textBoxSubscriptions?.Dispose();
+ _textBox = value;
+
+ // Attach handlers
+ if (_textBox != null)
+ {
+ _textBoxSubscriptions =
+ _textBox.GetObservable(TextBox.TextProperty)
+ .Subscribe(_ => OnTextBoxTextChanged());
+
+ if (Text != null)
+ {
+ UpdateTextValue(Text);
+ }
+ }
+ }
+ }
+
+ private int TextBoxSelectionStart
+ {
+ get
+ {
+ if (TextBox != null)
+ {
+ return Math.Min(TextBox.SelectionStart, TextBox.SelectionEnd);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+ private int TextBoxSelectionLength
+ {
+ get
+ {
+ if (TextBox != null)
+ {
+ return Math.Abs(TextBox.SelectionEnd - TextBox.SelectionStart);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the selection adapter used to populate the drop-down
+ /// with a list of selectable items.
+ ///
+ /// The selection adapter used to populate the drop-down with a
+ /// list of selectable items.
+ ///
+ /// You can use this property when you create an automation peer to
+ /// use with AutoCompleteBox or deriving from AutoCompleteBox to
+ /// create a custom control.
+ ///
+ protected ISelectionAdapter SelectionAdapter
+ {
+ get { return _adapter; }
+ set
+ {
+ if (_adapter != null)
+ {
+ _adapter.SelectionChanged -= OnAdapterSelectionChanged;
+ _adapter.Commit -= OnAdapterSelectionComplete;
+ _adapter.Cancel -= OnAdapterSelectionCanceled;
+ _adapter.Cancel -= OnAdapterSelectionComplete;
+ _adapter.Items = null;
+ }
+
+ _adapter = value;
+
+ if (_adapter != null)
+ {
+ _adapter.SelectionChanged += OnAdapterSelectionChanged;
+ _adapter.Commit += OnAdapterSelectionComplete;
+ _adapter.Cancel += OnAdapterSelectionCanceled;
+ _adapter.Cancel += OnAdapterSelectionComplete;
+ _adapter.Items = _view;
+ }
+ }
+ }
+
+ ///
+ /// Returns the
+ /// part, if
+ /// possible.
+ ///
+ ///
+ /// A object,
+ /// if possible. Otherwise, null.
+ ///
+ protected virtual ISelectionAdapter GetSelectionAdapterPart(INameScope nameScope)
+ {
+ ISelectionAdapter adapter = null;
+ SelectingItemsControl selector = nameScope.Find(ElementSelector);
+ if (selector != null)
+ {
+ // Check if it is already an IItemsSelector
+ adapter = selector as ISelectionAdapter;
+ if (adapter == null)
+ {
+ // Built in support for wrapping a Selector control
+ adapter = new SelectingItemsControlSelectionAdapter(selector);
+ }
+ }
+ if (adapter == null)
+ {
+ adapter = nameScope.Find(ElementSelectionAdapter);
+ }
+ return adapter;
+ }
+
+ ///
+ /// Builds the visual tree for the
+ /// control
+ /// when a new template is applied.
+ ///
+ protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+ {
+
+ if (DropDownPopup != null)
+ {
+ DropDownPopup.Closed -= DropDownPopup_Closed;
+ DropDownPopup = null;
+ }
+
+ // Set the template parts. Individual part setters remove and add
+ // any event handlers.
+ Popup popup = e.NameScope.Find(ElementPopup);
+ if (popup != null)
+ {
+ DropDownPopup = popup;
+ DropDownPopup.Closed += DropDownPopup_Closed;
+ }
+
+ SelectionAdapter = GetSelectionAdapterPart(e.NameScope);
+ TextBox = e.NameScope.Find(ElementTextBox);
+
+ // If the drop down property indicates that the popup is open,
+ // flip its value to invoke the changed handler.
+ if (IsDropDownOpen && DropDownPopup != null && !DropDownPopup.IsOpen)
+ {
+ OpeningDropDown(false);
+ }
+
+ base.OnTemplateApplied(e);
+ }
+
+ ///
+ /// Provides handling for the
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ Contract.Requires(e != null);
+
+ base.OnKeyDown(e);
+
+ if (e.Handled || !IsEnabled)
+ {
+ return;
+ }
+
+ // The drop down is open, pass along the key event arguments to the
+ // selection adapter. If it isn't handled by the adapter's logic,
+ // then we handle some simple navigation scenarios for controlling
+ // the drop down.
+ if (IsDropDownOpen)
+ {
+ if (SelectionAdapter != null)
+ {
+ SelectionAdapter.HandleKeyDown(e);
+ if (e.Handled)
+ {
+ return;
+ }
+ }
+
+ if (e.Key == Key.Escape)
+ {
+ OnAdapterSelectionCanceled(this, new RoutedEventArgs());
+ e.Handled = true;
+ }
+ }
+ else
+ {
+ // The drop down is not open, the Down key will toggle it open.
+ if (e.Key == Key.Down)
+ {
+ IsDropDownOpen = true;
+ e.Handled = true;
+ }
+ }
+
+ // Standard drop down navigation
+ switch (e.Key)
+ {
+ case Key.F4:
+ IsDropDownOpen = !IsDropDownOpen;
+ e.Handled = true;
+ break;
+
+ case Key.Enter:
+ OnAdapterSelectionComplete(this, new RoutedEventArgs());
+ e.Handled = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Provides handling for the
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected override void OnGotFocus(GotFocusEventArgs e)
+ {
+ base.OnGotFocus(e);
+ FocusChanged(HasFocus());
+ }
+
+ ///
+ /// Provides handling for the
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected override void OnLostFocus(RoutedEventArgs e)
+ {
+ base.OnLostFocus(e);
+ FocusChanged(HasFocus());
+ }
+
+ ///
+ /// Determines whether the text box or drop-down portion of the
+ /// control has
+ /// focus.
+ ///
+ /// true to indicate the
+ /// has focus;
+ /// otherwise, false.
+ protected bool HasFocus()
+ {
+ IVisual focused = FocusManager.Instance.Current;
+
+ while (focused != null)
+ {
+ if (object.ReferenceEquals(focused, this))
+ {
+ return true;
+ }
+
+ // This helps deal with popups that may not be in the same
+ // visual tree
+ IVisual parent = focused.GetVisualParent();
+ if (parent == null)
+ {
+ // Try the logical parent.
+ IControl element = focused as IControl;
+ if (element != null)
+ {
+ parent = element.Parent;
+ }
+ }
+ focused = parent;
+ }
+ return false;
+ }
+
+ ///
+ /// Handles the FocusChanged event.
+ ///
+ /// A value indicating whether the control
+ /// currently has the focus.
+ private void FocusChanged(bool hasFocus)
+ {
+ // The OnGotFocus & OnLostFocus are asynchronously and cannot
+ // reliably tell you that have the focus. All they do is let you
+ // know that the focus changed sometime in the past. To determine
+ // if you currently have the focus you need to do consult the
+ // FocusManager (see HasFocus()).
+
+ bool wasFocused = _isFocused;
+ _isFocused = hasFocus;
+
+ if (hasFocus)
+ {
+
+ if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0)
+ {
+ TextBox.Focus();
+ TextBox.SelectionStart = 0;
+ TextBox.SelectionEnd = TextBox.Text?.Length ?? 0;
+ }
+ }
+ else
+ {
+ IsDropDownOpen = false;
+ _userCalledPopulate = false;
+ ClearTextBoxSelection();
+ }
+
+ _isFocused = hasFocus;
+ }
+
+ ///
+ /// Occurs when the text in the text box portion of the
+ /// changes.
+ ///
+ public event EventHandler TextChanged;
+
+ ///
+ /// Occurs when the
+ /// is
+ /// populating the drop-down with possible matches based on the
+ ///
+ /// property.
+ ///
+ ///
+ /// If the event is canceled, by setting the PopulatingEventArgs.Cancel
+ /// property to true, the AutoCompleteBox will not automatically
+ /// populate the selection adapter contained in the drop-down.
+ /// In this case, if you want possible matches to appear, you must
+ /// provide the logic for populating the selection adapter.
+ ///
+ public event EventHandler Populating;
+
+ ///
+ /// Occurs when the
+ /// has
+ /// populated the drop-down with possible matches based on the
+ ///
+ /// property.
+ ///
+ public event EventHandler Populated;
+
+ ///
+ /// Occurs when the value of the
+ ///
+ /// property is changing from false to true.
+ ///
+ public event EventHandler DropDownOpening;
+
+ ///
+ /// Occurs when the value of the
+ ///
+ /// property has changed from false to true and the drop-down is open.
+ ///
+ public event EventHandler DropDownOpened;
+
+ ///
+ /// Occurs when the
+ ///
+ /// property is changing from true to false.
+ ///
+ public event EventHandler DropDownClosing;
+
+ ///
+ /// Occurs when the
+ ///
+ /// property was changed from true to false and the drop-down is open.
+ ///
+ public event EventHandler DropDownClosed;
+
+ ///
+ /// Occurs when the selected item in the drop-down portion of the
+ /// has
+ /// changed.
+ ///
+ public event EventHandler SelectionChanged
+ {
+ add { AddHandler(SelectionChangedEvent, value); }
+ remove { RemoveHandler(SelectionChangedEvent, value); }
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ /// that
+ /// contains the event data.
+ protected virtual void OnPopulating(PopulatingEventArgs e)
+ {
+ Populating?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnPopulated(PopulatedEventArgs e)
+ {
+ Populated?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnSelectionChanged(SelectionChangedEventArgs e)
+ {
+ RaiseEvent(e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnDropDownOpening(CancelEventArgs e)
+ {
+ DropDownOpening?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnDropDownOpened(EventArgs e)
+ {
+ DropDownOpened?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// that contains the event data.
+ protected virtual void OnDropDownClosing(CancelEventArgs e)
+ {
+ DropDownClosing?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ ///
+ /// which contains the event data.
+ protected virtual void OnDropDownClosed(EventArgs e)
+ {
+ DropDownClosed?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ /// A
+ /// that contains the event data.
+ protected virtual void OnTextChanged(RoutedEventArgs e)
+ {
+ TextChanged?.Invoke(this, e);
+ }
+
+ ///
+ /// Begin closing the drop-down.
+ ///
+ /// The original value.
+ private void ClosingDropDown(bool oldValue)
+ {
+ var args = new CancelEventArgs();
+ OnDropDownClosing(args);
+
+ if (args.Cancel)
+ {
+ _ignorePropertyChange = true;
+ SetValue(IsDropDownOpenProperty, oldValue);
+ }
+ else
+ {
+ CloseDropDown();
+ }
+
+ UpdatePseudoClasses();
+ }
+
+ ///
+ /// Begin opening the drop down by firing cancelable events, opening the
+ /// drop-down or reverting, depending on the event argument values.
+ ///
+ /// The original value, if needed for a revert.
+ private void OpeningDropDown(bool oldValue)
+ {
+ var args = new CancelEventArgs();
+
+ // Opening
+ OnDropDownOpening(args);
+
+ if (args.Cancel)
+ {
+ _ignorePropertyChange = true;
+ SetValue(IsDropDownOpenProperty, oldValue);
+ }
+ else
+ {
+ OpenDropDown();
+ }
+
+ UpdatePseudoClasses();
+ }
+
+ ///
+ /// Connects to the DropDownPopup Closed event.
+ ///
+ /// The source object.
+ /// The event data.
+ private void DropDownPopup_Closed(object sender, EventArgs e)
+ {
+ // Force the drop down dependency property to be false.
+ if (IsDropDownOpen)
+ {
+ IsDropDownOpen = false;
+ }
+
+ // Fire the DropDownClosed event
+ if (_popupHasOpened)
+ {
+ OnDropDownClosed(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Handles the timer tick when using a populate delay.
+ ///
+ /// The source object.
+ /// The event arguments.
+ private void PopulateDropDown(object sender, EventArgs e)
+ {
+ if (_delayTimer != null)
+ {
+ _delayTimer.Stop();
+ }
+
+ // Update the prefix/search text.
+ SearchText = Text;
+
+ if(TryPopulateAsync(SearchText))
+ {
+ return;
+ }
+
+ // The Populated event enables advanced, custom filtering. The
+ // client needs to directly update the ItemsSource collection or
+ // call the Populate method on the control to continue the
+ // display process if Cancel is set to true.
+ PopulatingEventArgs populating = new PopulatingEventArgs(SearchText);
+ OnPopulating(populating);
+ if (!populating.Cancel)
+ {
+ PopulateComplete();
+ }
+ }
+ private bool TryPopulateAsync(string searchText)
+ {
+ _populationCancellationTokenSource?.Cancel(false);
+ _populationCancellationTokenSource?.Dispose();
+ _populationCancellationTokenSource = null;
+
+ if(_asyncPopulator == null)
+ {
+ return false;
+ }
+
+ _populationCancellationTokenSource = new CancellationTokenSource();
+ var task = PopulateAsync(searchText, _populationCancellationTokenSource.Token);
+ if (task.Status == TaskStatus.Created)
+ task.Start();
+
+ return true;
+ }
+ private async Task PopulateAsync(string searchText, CancellationToken cancellationToken)
+ {
+
+ try
+ {
+ IEnumerable result = await _asyncPopulator.Invoke(searchText, cancellationToken);
+ var resultList = result.ToList();
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ await Dispatcher.UIThread.InvokeAsync(() =>
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ Items = resultList;
+ PopulateComplete();
+ }
+ });
+ }
+ catch (TaskCanceledException)
+ { }
+ finally
+ {
+ _populationCancellationTokenSource?.Dispose();
+ _populationCancellationTokenSource = null;
+ }
+
+ }
+
+ ///
+ /// Private method that directly opens the popup, checks the expander
+ /// button, and then fires the Opened event.
+ ///
+ private void OpenDropDown()
+ {
+ if (DropDownPopup != null)
+ {
+ DropDownPopup.IsOpen = true;
+ }
+ _popupHasOpened = true;
+ OnDropDownOpened(EventArgs.Empty);
+ }
+
+ ///
+ /// Private method that directly closes the popup, flips the Checked
+ /// value, and then fires the Closed event.
+ ///
+ private void CloseDropDown()
+ {
+ if (_popupHasOpened)
+ {
+ if (SelectionAdapter != null)
+ {
+ SelectionAdapter.SelectedItem = null;
+ }
+ if (DropDownPopup != null)
+ {
+ DropDownPopup.IsOpen = false;
+ }
+ OnDropDownClosed(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Formats an Item for text comparisons based on Converter
+ /// and ConverterCulture properties.
+ ///
+ /// The object to format.
+ /// A value indicating whether to clear
+ /// the data context after the lookup is performed.
+ /// Formatted Value.
+ private string FormatValue(object value, bool clearDataContext)
+ {
+ string result = FormatValue(value);
+ if(clearDataContext && _valueBindingEvaluator != null)
+ {
+ _valueBindingEvaluator.ClearDataContext();
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts the specified object to a string by using the
+ /// and
+ /// values
+ /// of the binding object specified by the
+ ///
+ /// property.
+ ///
+ /// The object to format as a string.
+ /// The string representation of the specified object.
+ ///
+ /// Override this method to provide a custom string conversion.
+ ///
+ protected virtual string FormatValue(object value)
+ {
+ if (_valueBindingEvaluator != null)
+ {
+ return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
+ }
+
+ if (_valueMemberSelector != null)
+ {
+ value = _valueMemberSelector.Select(value);
+ }
+
+ return value == null ? String.Empty : value.ToString();
+ }
+
+ ///
+ /// Handle the TextChanged event that is directly attached to the
+ /// TextBox part. This ensures that only user initiated actions will
+ /// result in an AutoCompleteBox suggestion and operation.
+ ///
+ private void OnTextBoxTextChanged()
+ {
+ //Uses Dispatcher.Post to allow the TextBox selection to update before processing
+ Dispatcher.UIThread.Post(() =>
+ {
+ // Call the central updated text method as a user-initiated action
+ TextUpdated(_textBox.Text, true);
+ });
+ }
+
+ ///
+ /// Updates both the text box value and underlying text dependency
+ /// property value if and when they change. Automatically fires the
+ /// text changed events when there is a change.
+ ///
+ /// The new string value.
+ private void UpdateTextValue(string value)
+ {
+ UpdateTextValue(value, null);
+ }
+
+ ///
+ /// Updates both the text box value and underlying text dependency
+ /// property value if and when they change. Automatically fires the
+ /// text changed events when there is a change.
+ ///
+ /// The new string value.
+ /// A nullable bool value indicating whether
+ /// the action was user initiated. In a user initiated mode, the
+ /// underlying text dependency property is updated. In a non-user
+ /// interaction, the text box value is updated. When user initiated is
+ /// null, all values are updated.
+ private void UpdateTextValue(string value, bool? userInitiated)
+ {
+ bool callTextChanged = false;
+ // Update the Text dependency property
+ if ((userInitiated == null || userInitiated == true) && Text != value)
+ {
+ _ignoreTextPropertyChange++;
+ Text = value;
+ callTextChanged = true;
+ }
+
+ // Update the TextBox's Text dependency property
+ if ((userInitiated == null || userInitiated == false) && TextBox != null && TextBox.Text != value)
+ {
+ _ignoreTextPropertyChange++;
+ TextBox.Text = value ?? string.Empty;
+
+ // Text dependency property value was set, fire event
+ if (!callTextChanged && (Text == value || Text == null))
+ {
+ callTextChanged = true;
+ }
+ }
+
+ if (callTextChanged)
+ {
+ OnTextChanged(new RoutedEventArgs());
+ }
+ }
+
+ ///
+ /// Handle the update of the text for the control from any source,
+ /// including the TextBox part and the Text dependency property.
+ ///
+ /// The new text.
+ /// A value indicating whether the update
+ /// is a user-initiated action. This should be a True value when the
+ /// TextUpdated method is called from a TextBox event handler.
+ private void TextUpdated(string newText, bool userInitiated)
+ {
+ // Only process this event if it is coming from someone outside
+ // setting the Text dependency property directly.
+ if (_ignoreTextPropertyChange > 0)
+ {
+ _ignoreTextPropertyChange--;
+ return;
+ }
+
+ if (newText == null)
+ {
+ newText = string.Empty;
+ }
+
+ // The TextBox.TextChanged event was not firing immediately and
+ // was causing an immediate update, even with wrapping. If there is
+ // a selection currently, no update should happen.
+ if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length)
+ {
+ return;
+ }
+
+ // Evaluate the conditions needed for completion.
+ // 1. Minimum prefix length
+ // 2. If a delay timer is in use, use it
+ bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0;
+ _userCalledPopulate = populateReady ? userInitiated : false;
+
+ // Update the interface and values only as necessary
+ UpdateTextValue(newText, userInitiated);
+
+ if (populateReady)
+ {
+ _ignoreTextSelectionChange = true;
+
+ if (_delayTimer != null)
+ {
+ _delayTimer.Start();
+ }
+ else
+ {
+ PopulateDropDown(this, EventArgs.Empty);
+ }
+ }
+ else
+ {
+ SearchText = string.Empty;
+ if (SelectedItem != null)
+ {
+ _skipSelectedItemTextUpdate = true;
+ }
+ SelectedItem = null;
+ if (IsDropDownOpen)
+ {
+ IsDropDownOpen = false;
+ }
+ }
+ }
+
+ ///
+ /// A simple helper method to clear the view and ensure that a view
+ /// object is always present and not null.
+ ///
+ private void ClearView()
+ {
+ if (_view == null)
+ {
+ _view = new AvaloniaList();
+ }
+ else
+ {
+ _view.Clear();
+ }
+ }
+
+ ///
+ /// Walks through the items enumeration. Performance is not going to be
+ /// perfect with the current implementation.
+ ///
+ private void RefreshView()
+ {
+ if (_items == null)
+ {
+ ClearView();
+ return;
+ }
+
+ // Cache the current text value
+ string text = Text ?? string.Empty;
+
+ // Determine if any filtering mode is on
+ bool stringFiltering = TextFilter != null;
+ bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null;
+
+ int view_index = 0;
+ int view_count = _view.Count;
+ List items = _items;
+ foreach (object item in items)
+ {
+ bool inResults = !(stringFiltering || objectFiltering);
+ if (!inResults)
+ {
+ inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item);
+ }
+
+ if (view_count > view_index && inResults && _view[view_index] == item)
+ {
+ // Item is still in the view
+ view_index++;
+ }
+ else if (inResults)
+ {
+ // Insert the item
+ if (view_count > view_index && _view[view_index] != item)
+ {
+ // Replace item
+ // Unfortunately replacing via index throws a fatal
+ // exception: View[view_index] = item;
+ // Cost: O(n) vs O(1)
+ _view.RemoveAt(view_index);
+ _view.Insert(view_index, item);
+ view_index++;
+ }
+ else
+ {
+ // Add the item
+ if (view_index == view_count)
+ {
+ // Constant time is preferred (Add).
+ _view.Add(item);
+ }
+ else
+ {
+ _view.Insert(view_index, item);
+ }
+ view_index++;
+ view_count++;
+ }
+ }
+ else if (view_count > view_index && _view[view_index] == item)
+ {
+ // Remove the item
+ _view.RemoveAt(view_index);
+ view_count--;
+ }
+ }
+
+ // Clear the evaluator to discard a reference to the last item
+ if (_valueBindingEvaluator != null)
+ {
+ _valueBindingEvaluator.ClearDataContext();
+ }
+ }
+
+ ///
+ /// Handle any change to the ItemsSource dependency property, update
+ /// the underlying ObservableCollection view, and set the selection
+ /// adapter's ItemsSource to the view if appropriate.
+ ///
+ /// The new enumerable reference.
+ private void OnItemsChanged(IEnumerable newValue)
+ {
+ // Remove handler for oldValue.CollectionChanged (if present)
+ _collectionChangeSubscription?.Dispose();
+ _collectionChangeSubscription = null;
+
+ // Add handler for newValue.CollectionChanged (if possible)
+ if (newValue is INotifyCollectionChanged newValueINotifyCollectionChanged)
+ {
+ _collectionChangeSubscription = newValueINotifyCollectionChanged.WeakSubscribe(ItemsCollectionChanged);
+ }
+
+ // Store a local cached copy of the data
+ _items = newValue == null ? null : new List(newValue.Cast().ToList());
+
+ // Clear and set the view on the selection adapter
+ ClearView();
+ if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+ {
+ SelectionAdapter.Items = _view;
+ }
+ if (IsDropDownOpen)
+ {
+ RefreshView();
+ }
+ }
+
+ ///
+ /// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property.
+ ///
+ /// The object that raised the event.
+ /// The event data.
+ private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ // Update the cache
+ if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
+ {
+ for (int index = 0; index < e.OldItems.Count; index++)
+ {
+ _items.RemoveAt(e.OldStartingIndex);
+ }
+ }
+ if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null && _items.Count >= e.NewStartingIndex)
+ {
+ for (int index = 0; index < e.NewItems.Count; index++)
+ {
+ _items.Insert(e.NewStartingIndex + index, e.NewItems[index]);
+ }
+ }
+ if (e.Action == NotifyCollectionChangedAction.Replace && e.NewItems != null && e.OldItems != null)
+ {
+ for (int index = 0; index < e.NewItems.Count; index++)
+ {
+ _items[e.NewStartingIndex] = e.NewItems[index];
+ }
+ }
+
+ // Update the view
+ if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace)
+ {
+ for (int index = 0; index < e.OldItems.Count; index++)
+ {
+ _view.Remove(e.OldItems[index]);
+ }
+ }
+
+ if (e.Action == NotifyCollectionChangedAction.Reset)
+ {
+ // Significant changes to the underlying data.
+ ClearView();
+ if (Items != null)
+ {
+ _items = new List(Items.Cast().ToList());
+ }
+ }
+
+ // Refresh the observable collection used in the selection adapter.
+ RefreshView();
+ }
+
+ ///
+ /// Notifies the
+ /// that the
+ ///
+ /// property has been set and the data can be filtered to provide
+ /// possible matches in the drop-down.
+ ///
+ ///
+ /// Call this method when you are providing custom population of
+ /// the drop-down portion of the AutoCompleteBox, to signal the control
+ /// that you are done with the population process.
+ /// Typically, you use PopulateComplete when the population process
+ /// is a long-running process and you want to cancel built-in filtering
+ /// of the ItemsSource items. In this case, you can handle the
+ /// Populated event and set PopulatingEventArgs.Cancel to true.
+ /// When the long-running process has completed you call
+ /// PopulateComplete to indicate the drop-down is populated.
+ ///
+ public void PopulateComplete()
+ {
+ // Apply the search filter
+ RefreshView();
+
+ // Fire the Populated event containing the read-only view data.
+ PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view));
+ OnPopulated(populated);
+
+ if (SelectionAdapter != null && SelectionAdapter.Items != _view)
+ {
+ SelectionAdapter.Items = _view;
+ }
+
+ bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0);
+ if (isDropDownOpen != IsDropDownOpen)
+ {
+ _ignorePropertyChange = true;
+ IsDropDownOpen = isDropDownOpen;
+ }
+ if (IsDropDownOpen)
+ {
+ OpeningDropDown(false);
+ }
+ else
+ {
+ ClosingDropDown(true);
+ }
+
+ UpdateTextCompletion(_userCalledPopulate);
+ }
+
+ ///
+ /// Performs text completion, if enabled, and a lookup on the underlying
+ /// item values for an exact match. Will update the SelectedItem value.
+ ///
+ /// A value indicating whether the operation
+ /// was user initiated. Text completion will not be performed when not
+ /// directly initiated by the user.
+ private void UpdateTextCompletion(bool userInitiated)
+ {
+ // By default this method will clear the selected value
+ object newSelectedItem = null;
+ string text = Text;
+
+ // Text search is StartsWith explicit and only when enabled, in
+ // line with WPF's ComboBox lookup. When in use it will associate
+ // a Value with the Text if it is found in ItemsSource. This is
+ // only valid when there is data and the user initiated the action.
+ if (_view.Count > 0)
+ {
+ if (IsTextCompletionEnabled && TextBox != null && userInitiated)
+ {
+ int currentLength = TextBox.Text.Length;
+ int selectionStart = TextBoxSelectionStart;
+ if (selectionStart == text.Length && selectionStart > _textSelectionStart)
+ {
+ // When the FilterMode dependency property is set to
+ // either StartsWith or StartsWithCaseSensitive, the
+ // first item in the view is used. This will improve
+ // performance on the lookup. It assumes that the
+ // FilterMode the user has selected is an acceptable
+ // case sensitive matching function for their scenario.
+ object top = FilterMode == AutoCompleteFilterMode.StartsWith || FilterMode == AutoCompleteFilterMode.StartsWithCaseSensitive
+ ? _view[0]
+ : TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
+
+ // If the search was successful, update SelectedItem
+ if (top != null)
+ {
+ newSelectedItem = top;
+ string topString = FormatValue(top, true);
+
+ // Only replace partially when the two words being the same
+ int minLength = Math.Min(topString.Length, Text.Length);
+ if (AutoCompleteSearch.Equals(Text.Substring(0, minLength), topString.Substring(0, minLength)))
+ {
+ // Update the text
+ UpdateTextValue(topString);
+
+ // Select the text past the user's caret
+ TextBox.SelectionStart = currentLength;
+ TextBox.SelectionEnd = topString.Length;
+ }
+ }
+ }
+ }
+ else
+ {
+ // Perform an exact string lookup for the text. This is a
+ // design change from the original Toolkit release when the
+ // IsTextCompletionEnabled property behaved just like the
+ // WPF ComboBox's IsTextSearchEnabled property.
+ //
+ // This change provides the behavior that most people expect
+ // to find: a lookup for the value is always performed.
+ newSelectedItem = TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive));
+ }
+ }
+
+ // Update the selected item property
+
+ if (SelectedItem != newSelectedItem)
+ {
+ _skipSelectedItemTextUpdate = true;
+ }
+ SelectedItem = newSelectedItem;
+
+ // Restore updates for TextSelection
+ if (_ignoreTextSelectionChange)
+ {
+ _ignoreTextSelectionChange = false;
+ if (TextBox != null)
+ {
+ _textSelectionStart = TextBoxSelectionStart;
+ }
+ }
+ }
+
+ ///
+ /// Attempts to look through the view and locate the specific exact
+ /// text match.
+ ///
+ /// The search text.
+ /// The view reference.
+ /// The predicate to use for the partial or
+ /// exact match.
+ /// Returns the object or null.
+ private object TryGetMatch(string searchText, AvaloniaList view, AutoCompleteFilterPredicate predicate)
+ {
+ if (view != null && view.Count > 0)
+ {
+ foreach (object o in view)
+ {
+ if (predicate(searchText, FormatValue(o)))
+ {
+ return o;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void UpdatePseudoClasses()
+ {
+ PseudoClasses.Set(":dropdownopen", IsDropDownOpen);
+ }
+
+ private void ClearTextBoxSelection()
+ {
+ if (TextBox != null)
+ {
+ int length = TextBox.Text?.Length ?? 0;
+ TextBox.SelectionStart = length;
+ TextBox.SelectionEnd = length;
+ }
+ }
+
+ ///
+ /// Called when the selected item is changed, updates the text value
+ /// that is displayed in the text box part.
+ ///
+ /// The new item.
+ private void OnSelectedItemChanged(object newItem)
+ {
+ string text;
+
+ if (newItem == null)
+ {
+ text = SearchText;
+ }
+ else
+ {
+ text = FormatValue(newItem, true);
+ }
+
+ // Update the Text property and the TextBox values
+ UpdateTextValue(text);
+
+ // Move the caret to the end of the text box
+ ClearTextBoxSelection();
+ }
+
+ ///
+ /// Handles the SelectionChanged event of the selection adapter.
+ ///
+ /// The source object.
+ /// The selection changed event data.
+ private void OnAdapterSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ SelectedItem = _adapter.SelectedItem;
+ }
+
+ //TODO Check UpdateTextCompletion
+ ///
+ /// Handles the Commit event on the selection adapter.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnAdapterSelectionComplete(object sender, RoutedEventArgs e)
+ {
+ IsDropDownOpen = false;
+
+ // Completion will update the selected value
+ //UpdateTextCompletion(false);
+
+ // Text should not be selected
+ ClearTextBoxSelection();
+
+ TextBox.Focus();
+ }
+
+ ///
+ /// Handles the Cancel event on the selection adapter.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnAdapterSelectionCanceled(object sender, RoutedEventArgs e)
+ {
+ UpdateTextValue(SearchText);
+
+ // Completion will update the selected value
+ UpdateTextCompletion(false);
+ }
+
+ ///
+ /// A predefined set of filter functions for the known, built-in
+ /// AutoCompleteFilterMode enumeration values.
+ ///
+ private static class AutoCompleteSearch
+ {
+ ///
+ /// Index function that retrieves the filter for the provided
+ /// AutoCompleteFilterMode.
+ ///
+ /// The built-in search mode.
+ /// Returns the string-based comparison function.
+ public static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode FilterMode)
+ {
+ switch (FilterMode)
+ {
+ case AutoCompleteFilterMode.Contains:
+ return Contains;
+
+ case AutoCompleteFilterMode.ContainsCaseSensitive:
+ return ContainsCaseSensitive;
+
+ case AutoCompleteFilterMode.ContainsOrdinal:
+ return ContainsOrdinal;
+
+ case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive:
+ return ContainsOrdinalCaseSensitive;
+
+ case AutoCompleteFilterMode.Equals:
+ return Equals;
+
+ case AutoCompleteFilterMode.EqualsCaseSensitive:
+ return EqualsCaseSensitive;
+
+ case AutoCompleteFilterMode.EqualsOrdinal:
+ return EqualsOrdinal;
+
+ case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive:
+ return EqualsOrdinalCaseSensitive;
+
+ case AutoCompleteFilterMode.StartsWith:
+ return StartsWith;
+
+ case AutoCompleteFilterMode.StartsWithCaseSensitive:
+ return StartsWithCaseSensitive;
+
+ case AutoCompleteFilterMode.StartsWithOrdinal:
+ return StartsWithOrdinal;
+
+ case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive:
+ return StartsWithOrdinalCaseSensitive;
+
+ case AutoCompleteFilterMode.None:
+ case AutoCompleteFilterMode.Custom:
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// An implementation of the Contains member of string that takes in a
+ /// string comparison. The traditional .NET string Contains member uses
+ /// StringComparison.Ordinal.
+ ///
+ /// The string.
+ /// The string value to search for.
+ /// The string comparison type.
+ /// Returns true when the substring is found.
+ private static bool Contains(string s, string value, StringComparison comparison)
+ {
+ return s.IndexOf(value, comparison) >= 0;
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWith(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWithCaseSensitive(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.CurrentCulture);
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWithOrdinal(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Check if the string value begins with the text.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool StartsWithOrdinalCaseSensitive(string text, string value)
+ {
+ return value.StartsWith(text, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value. The current
+ /// culture's case insensitive string comparison operator is used.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool Contains(string text, string value)
+ {
+ return Contains(value, text, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool ContainsCaseSensitive(string text, string value)
+ {
+ return Contains(value, text, StringComparison.CurrentCulture);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool ContainsOrdinal(string text, string value)
+ {
+ return Contains(value, text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Check if the prefix is contained in the string value.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool ContainsOrdinalCaseSensitive(string text, string value)
+ {
+ return Contains(value, text, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool Equals(string text, string value)
+ {
+ return value.Equals(text, StringComparison.CurrentCultureIgnoreCase);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool EqualsCaseSensitive(string text, string value)
+ {
+ return value.Equals(text, StringComparison.CurrentCulture);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool EqualsOrdinal(string text, string value)
+ {
+ return value.Equals(text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Check if the string values are equal.
+ ///
+ /// The AutoCompleteBox prefix text.
+ /// The item's string value.
+ /// Returns true if the condition is met.
+ public static bool EqualsOrdinalCaseSensitive(string text, string value)
+ {
+ return value.Equals(text, StringComparison.Ordinal);
+ }
+ }
+
+ ///
+ /// A framework element that permits a binding to be evaluated in a new data
+ /// context leaf node.
+ ///
+ /// The type of dynamic binding to return.
+ public class BindingEvaluator : Control
+ {
+ ///
+ /// Gets or sets the string value binding used by the control.
+ ///
+ private IBinding _binding;
+
+ #region public T Value
+
+ ///
+ /// Identifies the Value dependency property.
+ ///
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register, T>(nameof(Value));
+
+ ///
+ /// Gets or sets the data item value.
+ ///
+ public T Value
+ {
+ get { return GetValue(ValueProperty); }
+ set { SetValue(ValueProperty, value); }
+ }
+
+ #endregion public string Value
+
+ ///
+ /// Gets or sets the value binding.
+ ///
+ public IBinding ValueBinding
+ {
+ get { return _binding; }
+ set
+ {
+ _binding = value;
+ AvaloniaObjectExtensions.Bind(this, ValueProperty, value);
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the BindingEvaluator class.
+ ///
+ public BindingEvaluator()
+ { }
+
+ ///
+ /// Initializes a new instance of the BindingEvaluator class,
+ /// setting the initial binding to the provided parameter.
+ ///
+ /// The initial string value binding.
+ public BindingEvaluator(IBinding binding)
+ : this()
+ {
+ ValueBinding = binding;
+ }
+
+ ///
+ /// Clears the data context so that the control does not keep a
+ /// reference to the last-looked up item.
+ ///
+ public void ClearDataContext()
+ {
+ DataContext = null;
+ }
+
+ ///
+ /// Updates the data context of the framework element and returns the
+ /// updated binding value.
+ ///
+ /// The object to use as the data context.
+ /// If set to true, this parameter will
+ /// clear the data context immediately after retrieving the value.
+ /// Returns the evaluated T value of the bound dependency
+ /// property.
+ public T GetDynamicValue(object o, bool clearDataContext)
+ {
+ DataContext = o;
+ T value = Value;
+ if (clearDataContext)
+ {
+ DataContext = null;
+ }
+ return value;
+ }
+
+ ///
+ /// Updates the data context of the framework element and returns the
+ /// updated binding value.
+ ///
+ /// The object to use as the data context.
+ /// Returns the evaluated T value of the bound dependency
+ /// property.
+ public T GetDynamicValue(object o)
+ {
+ DataContext = o;
+ return Value;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs
index 002c5ea3f2..8acb3603c9 100644
--- a/src/Avalonia.Controls/Border.cs
+++ b/src/Avalonia.Controls/Border.cs
@@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia;
+using Avalonia.Controls.Utils;
using Avalonia.Media;
namespace Avalonia.Controls
@@ -8,7 +10,7 @@ namespace Avalonia.Controls
///
/// A control which decorates a child with a border and background.
///
- public class Border : Decorator
+ public partial class Border : Decorator
{
///
/// Defines the property.
@@ -25,21 +27,24 @@ namespace Avalonia.Controls
///
/// Defines the property.
///
- public static readonly StyledProperty BorderThicknessProperty =
- AvaloniaProperty.Register(nameof(BorderThickness));
+ public static readonly StyledProperty BorderThicknessProperty =
+ AvaloniaProperty.Register(nameof(BorderThickness));
///
/// Defines the property.
///
- public static readonly StyledProperty CornerRadiusProperty =
- AvaloniaProperty.Register(nameof(CornerRadius));
+ public static readonly StyledProperty CornerRadiusProperty =
+ AvaloniaProperty.Register(nameof(CornerRadius));
+
+ private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
///
/// Initializes static members of the class.
///
static Border()
{
- AffectsRender(BackgroundProperty, BorderBrushProperty);
+ AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
+ AffectsMeasure(BorderThicknessProperty);
}
///
@@ -63,7 +68,7 @@ namespace Avalonia.Controls
///
/// Gets or sets the thickness of the border.
///
- public double BorderThickness
+ public Thickness BorderThickness
{
get { return GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); }
@@ -72,7 +77,7 @@ namespace Avalonia.Controls
///
/// Gets or sets the radius of the border rounded corners.
///
- public float CornerRadius
+ public CornerRadius CornerRadius
{
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
@@ -84,21 +89,7 @@ namespace Avalonia.Controls
/// The drawing context.
public override void Render(DrawingContext context)
{
- var background = Background;
- var borderBrush = BorderBrush;
- var borderThickness = BorderThickness;
- var cornerRadius = CornerRadius;
- var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
- if (background != null)
- {
- context.FillRectangle(background, rect, cornerRadius);
- }
-
- if (borderBrush != null && borderThickness > 0)
- {
- context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
- }
+ _borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
}
///
@@ -120,10 +111,12 @@ namespace Avalonia.Controls
{
if (Child != null)
{
- var padding = Padding + new Thickness(BorderThickness);
+ var padding = Padding + BorderThickness;
Child.Arrange(new Rect(finalSize).Deflate(padding));
}
+ _borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
+
return finalSize;
}
@@ -131,19 +124,17 @@ namespace Avalonia.Controls
Size availableSize,
IControl child,
Thickness padding,
- double borderThickness)
+ Thickness borderThickness)
{
- padding += new Thickness(borderThickness);
+ padding += borderThickness;
if (child != null)
{
child.Measure(availableSize.Deflate(padding));
return child.DesiredSize.Inflate(padding);
}
- else
- {
- return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
- }
+
+ return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
}
}
}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs
index 4922761a84..fa69d72d67 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -245,7 +245,7 @@ namespace Avalonia.Controls
{
base.OnPointerReleased(e);
- if (e.MouseButton == MouseButton.Left)
+ if (IsPressed && e.MouseButton == MouseButton.Left)
{
e.Device.Capture(null);
IsPressed = false;
diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs
index 3ce81e0b12..866237ecce 100644
--- a/src/Avalonia.Controls/ButtonSpinner.cs
+++ b/src/Avalonia.Controls/ButtonSpinner.cs
@@ -201,6 +201,11 @@ namespace Avalonia.Controls
}
}
+ protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
+ {
+ SetButtonUsage();
+ }
+
///
/// Called when the property value changed.
///
diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs
index a7ea2da4a4..932179028e 100644
--- a/src/Avalonia.Controls/DropDown.cs
+++ b/src/Avalonia.Controls/DropDown.cs
@@ -164,6 +164,7 @@ namespace Avalonia.Controls
else
{
IsDropDownOpen = !IsDropDownOpen;
+ e.Handled = true;
}
}
base.OnPointerPressed(e);
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index dadd3b910b..96f6fb59b0 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -93,6 +93,7 @@ namespace Avalonia.Controls
static MenuItem()
{
SelectableMixin.Attach(IsSelectedProperty);
+ CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue(true);
IconProperty.Changed.AddClassHandler(x => x.IconChanged);
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
@@ -424,6 +425,40 @@ namespace Avalonia.Controls
}
}
+ ///
+ /// Called when the property changes.
+ ///
+ /// The event args.
+ private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is MenuItem menuItem)
+ {
+ if (e.OldValue is ICommand oldCommand)
+ {
+ oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
+ }
+
+ if (e.NewValue is ICommand newCommand)
+ {
+ newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
+ }
+
+ menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Called when the event fires.
+ ///
+ /// The event sender.
+ /// The event args.
+ private void CanExecuteChanged(object sender, EventArgs e)
+ {
+ // HACK: Just set the IsEnabled property for the moment. This needs to be changed to
+ // use IsEnabledCore etc. but it will do for now.
+ IsEnabled = Command == null || Command.CanExecute(CommandParameter);
+ }
+
///
/// Called when the property changes.
///
diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
new file mode 100644
index 0000000000..59d2949b81
--- /dev/null
+++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
@@ -0,0 +1,998 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
+ ///
+ public class NumericUpDown : TemplatedControl
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty AllowSpinProperty =
+ ButtonSpinner.AllowSpinProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ButtonSpinnerLocationProperty =
+ ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ShowButtonSpinnerProperty =
+ ButtonSpinner.ShowButtonSpinnerProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ClipValueToMinMaxProperty =
+ AvaloniaProperty.RegisterDirect(nameof(ClipValueToMinMax),
+ updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty CultureInfoProperty =
+ AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo,
+ (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty FormatStringProperty =
+ AvaloniaProperty.Register(nameof(FormatString), string.Empty);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IncrementProperty =
+ AvaloniaProperty.Register(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsReadOnlyProperty =
+ AvaloniaProperty.Register(nameof(IsReadOnly));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaximumProperty =
+ AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinimumProperty =
+ AvaloniaProperty.Register(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ParsingNumberStyleProperty =
+ AvaloniaProperty.RegisterDirect(nameof(ParsingNumberStyle),
+ updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty TextProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ValueProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value,
+ (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty WatermarkProperty =
+ AvaloniaProperty.Register(nameof(Watermark));
+
+ private IDisposable _textBoxTextChangedSubscription;
+
+ private double _value;
+ private string _text;
+ private bool _internalValueSet;
+ private bool _clipValueToMinMax;
+ private bool _isSyncingTextAndValueProperties;
+ private bool _isTextChangedFromUI;
+ private CultureInfo _cultureInfo;
+ private NumberStyles _parsingNumberStyle = NumberStyles.Any;
+
+ ///
+ /// Gets the Spinner template part.
+ ///
+ private Spinner Spinner { get; set; }
+
+ ///
+ /// Gets the TextBox template part.
+ ///
+ private TextBox TextBox { get; set; }
+
+ ///
+ /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
+ ///
+ public bool AllowSpin
+ {
+ get { return GetValue(AllowSpinProperty); }
+ set { SetValue(AllowSpinProperty, value); }
+ }
+
+ ///
+ /// Gets or sets current location of the .
+ ///
+ public Location ButtonSpinnerLocation
+ {
+ get { return GetValue(ButtonSpinnerLocationProperty); }
+ set { SetValue(ButtonSpinnerLocationProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the spin buttons should be shown.
+ ///
+ public bool ShowButtonSpinner
+ {
+ get { return GetValue(ShowButtonSpinnerProperty); }
+ set { SetValue(ShowButtonSpinnerProperty, value); }
+ }
+
+ ///
+ /// Gets or sets if the value should be clipped when minimum/maximum is reached.
+ ///
+ public bool ClipValueToMinMax
+ {
+ get { return _clipValueToMinMax; }
+ set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); }
+ }
+
+ ///
+ /// Gets or sets the current CultureInfo.
+ ///
+ public CultureInfo CultureInfo
+ {
+ get { return _cultureInfo; }
+ set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); }
+ }
+
+ ///
+ /// Gets or sets the display format of the .
+ ///
+ public string FormatString
+ {
+ get { return GetValue(FormatStringProperty); }
+ set { SetValue(FormatStringProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the amount in which to increment the .
+ ///
+ public double Increment
+ {
+ get { return GetValue(IncrementProperty); }
+ set { SetValue(IncrementProperty, value); }
+ }
+
+ ///
+ /// Gets or sets if the control is read only.
+ ///
+ public bool IsReadOnly
+ {
+ get { return GetValue(IsReadOnlyProperty); }
+ set { SetValue(IsReadOnlyProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the maximum allowed value.
+ ///
+ public double Maximum
+ {
+ get { return GetValue(MaximumProperty); }
+ set { SetValue(MaximumProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the minimum allowed value.
+ ///
+ public double Minimum
+ {
+ get { return GetValue(MinimumProperty); }
+ set { SetValue(MinimumProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
+ ///
+ public NumberStyles ParsingNumberStyle
+ {
+ get { return _parsingNumberStyle; }
+ set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); }
+ }
+
+ ///
+ /// Gets or sets the formatted string representation of the value.
+ ///
+ public string Text
+ {
+ get { return _text; }
+ set { SetAndRaise(TextProperty, ref _text, value); }
+ }
+
+ ///
+ /// Gets or sets the value.
+ ///
+ public double Value
+ {
+ get { return _value; }
+ set
+ {
+ value = OnCoerceValue(value);
+ SetAndRaise(ValueProperty, ref _value, value);
+ }
+ }
+
+ ///
+ /// Gets or sets the object to use as a watermark if the is null.
+ ///
+ public string Watermark
+ {
+ get { return GetValue(WatermarkProperty); }
+ set { SetValue(WatermarkProperty, value); }
+ }
+
+ ///
+ /// Initializes new instance of class.
+ ///
+ public NumericUpDown()
+ {
+ Initialized += (sender, e) =>
+ {
+ if (!_internalValueSet && IsInitialized)
+ {
+ SyncTextAndValueProperties(false, null, true);
+ }
+
+ SetValidSpinDirection();
+ };
+ }
+
+ ///
+ /// Initializes static members of the class.
+ ///
+ static NumericUpDown()
+ {
+ CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
+ FormatStringProperty.Changed.Subscribe(FormatStringChanged);
+ IncrementProperty.Changed.Subscribe(IncrementChanged);
+ IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged);
+ MaximumProperty.Changed.Subscribe(OnMaximumChanged);
+ MinimumProperty.Changed.Subscribe(OnMinimumChanged);
+ TextProperty.Changed.Subscribe(OnTextChanged);
+ ValueProperty.Changed.Subscribe(OnValueChanged);
+ }
+
+ ///
+ protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
+ {
+ if (TextBox != null)
+ {
+ TextBox.PointerPressed -= TextBoxOnPointerPressed;
+ _textBoxTextChangedSubscription?.Dispose();
+ }
+ TextBox = e.NameScope.Find("PART_TextBox");
+ if (TextBox != null)
+ {
+ TextBox.Text = Text;
+ TextBox.PointerPressed += TextBoxOnPointerPressed;
+ _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged());
+ }
+
+ if (Spinner != null)
+ {
+ Spinner.Spin -= OnSpinnerSpin;
+ }
+
+ Spinner = e.NameScope.Find("PART_Spinner");
+
+ if (Spinner != null)
+ {
+ Spinner.Spin += OnSpinnerSpin;
+ }
+
+ SetValidSpinDirection();
+ }
+
+ ///
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Enter:
+ var commitSuccess = CommitInput();
+ e.Handled = !commitSuccess;
+ break;
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue)
+ {
+ if (IsInitialized)
+ {
+ SyncTextAndValueProperties(false, null);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnFormatStringChanged(string oldValue, string newValue)
+ {
+ if (IsInitialized)
+ {
+ SyncTextAndValueProperties(false, null);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnIncrementChanged(double oldValue, double newValue)
+ {
+ if (IsInitialized)
+ {
+ SetValidSpinDirection();
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
+ {
+ SetValidSpinDirection();
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnMaximumChanged(double oldValue, double newValue)
+ {
+ if (IsInitialized)
+ {
+ SetValidSpinDirection();
+ }
+ if (ClipValueToMinMax)
+ {
+ Value = MathUtilities.Clamp(Value, Minimum, Maximum);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnMinimumChanged(double oldValue, double newValue)
+ {
+ if (IsInitialized)
+ {
+ SetValidSpinDirection();
+ }
+ if (ClipValueToMinMax)
+ {
+ Value = MathUtilities.Clamp(Value, Minimum, Maximum);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnTextChanged(string oldValue, string newValue)
+ {
+ if (IsInitialized)
+ {
+ SyncTextAndValueProperties(true, Text);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void OnValueChanged(double oldValue, double newValue)
+ {
+ if (!_internalValueSet && IsInitialized)
+ {
+ SyncTextAndValueProperties(false, null, true);
+ }
+
+ SetValidSpinDirection();
+
+ RaiseValueChangedEvent(oldValue, newValue);
+ }
+
+ ///
+ /// Called when the property has to be coerced.
+ ///
+ /// The value.
+ protected virtual double OnCoerceIncrement(double baseValue)
+ {
+ return baseValue;
+ }
+
+ ///
+ /// Called when the property has to be coerced.
+ ///
+ /// The value.
+ protected virtual double OnCoerceMaximum(double baseValue)
+ {
+ return Math.Max(baseValue, Minimum);
+ }
+
+ ///
+ /// Called when the property has to be coerced.
+ ///
+ /// The value.
+ protected virtual double OnCoerceMinimum(double baseValue)
+ {
+ return Math.Min(baseValue, Maximum);
+ }
+
+ ///
+ /// Called when the property has to be coerced.
+ ///
+ /// The value.
+ protected virtual double OnCoerceValue(double baseValue)
+ {
+ return baseValue;
+ }
+
+ ///
+ /// Raises the OnSpin event when spinning is initiated by the end-user.
+ ///
+ /// The event args.
+ protected virtual void OnSpin(SpinEventArgs e)
+ {
+ if (e == null)
+ {
+ throw new ArgumentNullException("e");
+ }
+
+ var handler = Spinned;
+ handler?.Invoke(this, e);
+
+ if (e.Direction == SpinDirection.Increase)
+ {
+ DoIncrement();
+ }
+ else
+ {
+ DoDecrement();
+ }
+ }
+
+ ///
+ /// Raises the event.
+ ///
+ /// The old value.
+ /// The new value.
+ protected virtual void RaiseValueChangedEvent(double oldValue, double newValue)
+ {
+ var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue);
+ RaiseEvent(e);
+ }
+
+ ///
+ /// Converts the formatted text to a value.
+ ///
+ private double ConvertTextToValue(string text)
+ {
+ double result = 0;
+
+ if (string.IsNullOrEmpty(text))
+ {
+ return result;
+ }
+
+ // Since the conversion from Value to text using a FormartString may not be parsable,
+ // we verify that the already existing text is not the exact same value.
+ var currentValueText = ConvertValueToText();
+ if (Equals(currentValueText, text))
+ {
+ return Value;
+ }
+
+ result = ConvertTextToValueCore(currentValueText, text);
+
+ if (ClipValueToMinMax)
+ {
+ return MathUtilities.Clamp(result, Minimum, Maximum);
+ }
+
+ ValidateMinMax(result);
+
+ return result;
+ }
+
+ ///
+ /// Converts the value to formatted text.
+ ///
+ ///
+ private string ConvertValueToText()
+ {
+ //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
+ if (FormatString.Contains("{0"))
+ {
+ return string.Format(CultureInfo, FormatString, Value);
+ }
+
+ return Value.ToString(FormatString, CultureInfo);
+ }
+
+ ///
+ /// Called by OnSpin when the spin direction is SpinDirection.Increase.
+ ///
+ private void OnIncrement()
+ {
+ var result = Value + Increment;
+ Value = MathUtilities.Clamp(result, Minimum, Maximum);
+ }
+
+ ///
+ /// Called by OnSpin when the spin direction is SpinDirection.Descrease.
+ ///
+ private void OnDecrement()
+ {
+ var result = Value - Increment;
+ Value = MathUtilities.Clamp(result, Minimum, Maximum);
+ }
+
+ ///
+ /// Sets the valid spin directions.
+ ///
+ private void SetValidSpinDirection()
+ {
+ var validDirections = ValidSpinDirections.None;
+
+ // Zero increment always prevents spin.
+ if (Increment != 0 && !IsReadOnly)
+ {
+ if (Value < Maximum)
+ {
+ validDirections = validDirections | ValidSpinDirections.Increase;
+ }
+
+ if (Value > Minimum)
+ {
+ validDirections = validDirections | ValidSpinDirections.Decrease;
+ }
+ }
+
+ if (Spinner != null)
+ {
+ Spinner.ValidSpinDirection = validDirections;
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (CultureInfo)e.OldValue;
+ var newValue = (CultureInfo)e.NewValue;
+ upDown.OnCultureInfoChanged(oldValue, newValue);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (double)e.OldValue;
+ var newValue = (double)e.NewValue;
+ upDown.OnIncrementChanged(oldValue, newValue);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (string)e.OldValue;
+ var newValue = (string)e.NewValue;
+ upDown.OnFormatStringChanged(oldValue, newValue);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (bool)e.OldValue;
+ var newValue = (bool)e.NewValue;
+ upDown.OnIsReadOnlyChanged(oldValue, newValue);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (double)e.OldValue;
+ var newValue = (double)e.NewValue;
+ upDown.OnMaximumChanged(oldValue, newValue);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (double)e.OldValue;
+ var newValue = (double)e.NewValue;
+ upDown.OnMinimumChanged(oldValue, newValue);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (string)e.OldValue;
+ var newValue = (string)e.NewValue;
+ upDown.OnTextChanged(oldValue, newValue);
+ }
+ }
+
+ ///
+ /// Called when the property value changed.
+ ///
+ /// The event args.
+ private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Sender is NumericUpDown upDown)
+ {
+ var oldValue = (double)e.OldValue;
+ var newValue = (double)e.NewValue;
+ upDown.OnValueChanged(oldValue, newValue);
+ }
+ }
+
+ private void SetValueInternal(double value)
+ {
+ _internalValueSet = true;
+ try
+ {
+ Value = value;
+ }
+ finally
+ {
+ _internalValueSet = false;
+ }
+ }
+
+ private static double OnCoerceMaximum(NumericUpDown upDown, double value)
+ {
+ return upDown.OnCoerceMaximum(value);
+ }
+
+ private static double OnCoerceMinimum(NumericUpDown upDown, double value)
+ {
+ return upDown.OnCoerceMinimum(value);
+ }
+
+ private static double OnCoerceIncrement(NumericUpDown upDown, double value)
+ {
+ return upDown.OnCoerceIncrement(value);
+ }
+
+ private void TextBoxOnTextChanged()
+ {
+ try
+ {
+ _isTextChangedFromUI = true;
+ if (TextBox != null)
+ {
+ Text = TextBox.Text;
+ }
+ }
+ finally
+ {
+ _isTextChangedFromUI = false;
+ }
+ }
+
+ private void OnSpinnerSpin(object sender, SpinEventArgs e)
+ {
+ if (AllowSpin && !IsReadOnly)
+ {
+ var spin = !e.UsingMouseWheel;
+ spin |= ((TextBox != null) && TextBox.IsFocused);
+
+ if (spin)
+ {
+ e.Handled = true;
+ OnSpin(e);
+ }
+ }
+ }
+
+ private void DoDecrement()
+ {
+ if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease)
+ {
+ OnDecrement();
+ }
+ }
+
+ private void DoIncrement()
+ {
+ if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase)
+ {
+ OnIncrement();
+ }
+ }
+
+ public event EventHandler Spinned;
+
+ private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
+ {
+ if (e.Device.Captured != Spinner)
+ {
+ Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input);
+ }
+ }
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent ValueChangedEvent =
+ RoutedEvent.Register(nameof(ValueChanged), RoutingStrategies.Bubble);
+
+ ///
+ /// Raised when the changes.
+ ///
+ public event EventHandler ValueChanged
+ {
+ add { AddHandler(ValueChangedEvent, value); }
+ remove { RemoveHandler(ValueChangedEvent, value); }
+ }
+
+ private bool CommitInput()
+ {
+ return SyncTextAndValueProperties(true, Text);
+ }
+
+ ///
+ /// Synchronize and properties.
+ ///
+ /// If value should be updated from text.
+ /// The text.
+ private bool SyncTextAndValueProperties(bool updateValueFromText, string text)
+ {
+ return SyncTextAndValueProperties(updateValueFromText, text, false);
+ }
+
+ ///
+ /// Synchronize and properties.
+ ///
+ /// If value should be updated from text.
+ /// The text.
+ /// Force text update.
+ private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate)
+ {
+ if (_isSyncingTextAndValueProperties)
+ return true;
+
+ _isSyncingTextAndValueProperties = true;
+ var parsedTextIsValid = true;
+ try
+ {
+ if (updateValueFromText)
+ {
+ if (!string.IsNullOrEmpty(text))
+ {
+ try
+ {
+ var newValue = ConvertTextToValue(text);
+ if (!Equals(newValue, Value))
+ {
+ SetValueInternal(newValue);
+ }
+ }
+ catch
+ {
+ parsedTextIsValid = false;
+ }
+ }
+ }
+
+ // Do not touch the ongoing text input from user.
+ if (!_isTextChangedFromUI)
+ {
+ var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text);
+ if (!keepEmpty)
+ {
+ var newText = ConvertValueToText();
+ if (!Equals(Text, newText))
+ {
+ Text = newText;
+ }
+ }
+
+ // Sync Text and textBox
+ if (TextBox != null)
+ {
+ TextBox.Text = Text;
+ }
+ }
+
+ if (_isTextChangedFromUI && !parsedTextIsValid)
+ {
+ // Text input was made from the user and the text
+ // repesents an invalid value. Disable the spinner in this case.
+ if (Spinner != null)
+ {
+ Spinner.ValidSpinDirection = ValidSpinDirections.None;
+ }
+ }
+ else
+ {
+ SetValidSpinDirection();
+ }
+ }
+ finally
+ {
+ _isSyncingTextAndValueProperties = false;
+ }
+ return parsedTextIsValid;
+ }
+
+ private double ConvertTextToValueCore(string currentValueText, string text)
+ {
+ double result;
+
+ if (IsPercent(FormatString))
+ {
+ result = decimal.ToDouble(ParsePercent(text, CultureInfo));
+ }
+ else
+ {
+ // Problem while converting new text
+ if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue))
+ {
+ var shouldThrow = true;
+
+ // Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
+ if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _))
+ {
+ // extract non-digit characters
+ var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c));
+ var textSpecialCharacters = text.Where(c => !char.IsDigit(c));
+ // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
+ if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0)
+ {
+ foreach (var character in textSpecialCharacters)
+ {
+ text = text.Replace(character.ToString(), string.Empty);
+ }
+ // if without the special characters, parsing is good, do not throw
+ if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue))
+ {
+ shouldThrow = false;
+ }
+ }
+ }
+
+ if (shouldThrow)
+ {
+ throw new InvalidDataException("Input string was not in a correct format.");
+ }
+ }
+ result = outputValue;
+ }
+ return result;
+ }
+
+ private void ValidateMinMax(double value)
+ {
+ if (value < Minimum)
+ {
+ throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum));
+ }
+ else if (value > Maximum)
+ {
+ throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum));
+ }
+ }
+
+ ///
+ /// Parse percent format text
+ ///
+ /// Text to parse.
+ /// The culture info.
+ private static decimal ParsePercent(string text, IFormatProvider cultureInfo)
+ {
+ var info = NumberFormatInfo.GetInstance(cultureInfo);
+ text = text.Replace(info.PercentSymbol, null);
+ var result = decimal.Parse(text, NumberStyles.Any, info);
+ result = result / 100;
+ return result;
+ }
+
+
+ private bool IsPercent(string stringToTest)
+ {
+ var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal);
+ if (PIndex >= 0)
+ {
+ //stringToTest contains a "P" between 2 "'", it's considered as text, not percent
+ var isText = stringToTest.Substring(0, PIndex).Contains("'")
+ && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'");
+
+ return !isText;
+ }
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs
new file mode 100644
index 0000000000..e994ffdd15
--- /dev/null
+++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs
@@ -0,0 +1,16 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+ public class NumericUpDownValueChangedEventArgs : RoutedEventArgs
+ {
+ public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent)
+ {
+ OldValue = oldValue;
+ NewValue = newValue;
+ }
+
+ public double OldValue { get; }
+ public double NewValue { get; }
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index d0a438cc2b..3cc750e20d 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -4,6 +4,7 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
+using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
@@ -31,7 +32,7 @@ namespace Avalonia.Controls.Presenters
///
/// Defines the property.
///
- public static readonly StyledProperty BorderThicknessProperty =
+ public static readonly StyledProperty BorderThicknessProperty =
Border.BorderThicknessProperty.AddOwner();
///
@@ -57,7 +58,7 @@ namespace Avalonia.Controls.Presenters
///
/// Defines the property.
///
- public static readonly StyledProperty CornerRadiusProperty =
+ public static readonly StyledProperty CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner();
///
@@ -76,17 +77,20 @@ namespace Avalonia.Controls.Presenters
/// Defines the property.
///
public static readonly StyledProperty PaddingProperty =
- Border.PaddingProperty.AddOwner();
+ Decorator.PaddingProperty.AddOwner();
private IControl _child;
private bool _createdChild;
private IDataTemplate _dataTemplate;
+ private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
///
/// Initializes static members of the class.
///
static ContentPresenter()
{
+ AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
+ AffectsMeasure(BorderThicknessProperty);
ContentProperty.Changed.AddClassHandler(x => x.ContentChanged);
ContentTemplateProperty.Changed.AddClassHandler(x => x.ContentChanged);
TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged);
@@ -120,7 +124,7 @@ namespace Avalonia.Controls.Presenters
///
/// Gets or sets the thickness of the border.
///
- public double BorderThickness
+ public Thickness BorderThickness
{
get { return GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); }
@@ -157,7 +161,7 @@ namespace Avalonia.Controls.Presenters
///
/// Gets or sets the radius of the border rounded corners.
///
- public float CornerRadius
+ public CornerRadius CornerRadius
{
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
@@ -221,7 +225,7 @@ namespace Avalonia.Controls.Presenters
{
var content = Content;
var oldChild = Child;
- var newChild = CreateChild();
+ var newChild = CreateChild();
// Remove the old child if we're not recycling it.
if (oldChild != null && newChild != oldChild)
@@ -277,21 +281,7 @@ namespace Avalonia.Controls.Presenters
///
public override void Render(DrawingContext context)
{
- var background = Background;
- var borderBrush = BorderBrush;
- var borderThickness = BorderThickness;
- var cornerRadius = CornerRadius;
- var rect = new Rect(Bounds.Size).Deflate(BorderThickness);
-
- if (background != null)
- {
- context.FillRectangle(background, rect, cornerRadius);
- }
-
- if (borderBrush != null && borderThickness > 0)
- {
- context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius);
- }
+ _borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
}
///
@@ -344,7 +334,11 @@ namespace Avalonia.Controls.Presenters
///
protected override Size ArrangeOverride(Size finalSize)
{
- return ArrangeOverrideImpl(finalSize, new Vector());
+ finalSize = ArrangeOverrideImpl(finalSize, new Vector());
+
+ _borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
+
+ return finalSize;
}
///
@@ -372,74 +366,69 @@ namespace Avalonia.Controls.Presenters
internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
{
- if (Child != null)
- {
- var padding = Padding;
- var borderThickness = BorderThickness;
- var horizontalContentAlignment = HorizontalContentAlignment;
- var verticalContentAlignment = VerticalContentAlignment;
- var useLayoutRounding = UseLayoutRounding;
- var availableSizeMinusMargins = new Size(
- Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness),
- Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness));
- var size = availableSizeMinusMargins;
- var scale = GetLayoutScale();
- var originX = offset.X + padding.Left + borderThickness;
- var originY = offset.Y + padding.Top + borderThickness;
-
- if (horizontalContentAlignment != HorizontalAlignment.Stretch)
- {
- size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
- }
+ if (Child == null) return finalSize;
- if (verticalContentAlignment != VerticalAlignment.Stretch)
- {
- size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
- }
-
- size = LayoutHelper.ApplyLayoutConstraints(Child, size);
+ var padding = Padding;
+ var borderThickness = BorderThickness;
+ var horizontalContentAlignment = HorizontalContentAlignment;
+ var verticalContentAlignment = VerticalContentAlignment;
+ var useLayoutRounding = UseLayoutRounding;
+ var availableSizeMinusMargins = new Size(
+ Math.Max(0, finalSize.Width - padding.Left - padding.Right - borderThickness.Left - borderThickness.Right),
+ Math.Max(0, finalSize.Height - padding.Top - padding.Bottom - borderThickness.Top - borderThickness.Bottom));
+ var size = availableSizeMinusMargins;
+ var scale = GetLayoutScale();
+ var originX = offset.X + padding.Left + borderThickness.Left;
+ var originY = offset.Y + padding.Top + borderThickness.Top;
+
+ if (horizontalContentAlignment != HorizontalAlignment.Stretch)
+ {
+ size = size.WithWidth(Math.Min(size.Width, DesiredSize.Width - padding.Left - padding.Right));
+ }
- if (useLayoutRounding)
- {
- size = new Size(
- Math.Ceiling(size.Width * scale) / scale,
- Math.Ceiling(size.Height * scale) / scale);
- availableSizeMinusMargins = new Size(
- Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
- Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
- }
+ if (verticalContentAlignment != VerticalAlignment.Stretch)
+ {
+ size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - padding.Top - padding.Bottom));
+ }
- switch (horizontalContentAlignment)
- {
- case HorizontalAlignment.Center:
- case HorizontalAlignment.Stretch:
- originX += (availableSizeMinusMargins.Width - size.Width) / 2;
- break;
- case HorizontalAlignment.Right:
- originX += availableSizeMinusMargins.Width - size.Width;
- break;
- }
+ if (useLayoutRounding)
+ {
+ size = new Size(
+ Math.Ceiling(size.Width * scale) / scale,
+ Math.Ceiling(size.Height * scale) / scale);
+ availableSizeMinusMargins = new Size(
+ Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale,
+ Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale);
+ }
- switch (verticalContentAlignment)
- {
- case VerticalAlignment.Center:
- case VerticalAlignment.Stretch:
- originY += (availableSizeMinusMargins.Height - size.Height) / 2;
- break;
- case VerticalAlignment.Bottom:
- originY += availableSizeMinusMargins.Height - size.Height;
- break;
- }
+ switch (horizontalContentAlignment)
+ {
+ case HorizontalAlignment.Center:
+ originX += (availableSizeMinusMargins.Width - size.Width) / 2;
+ break;
+ case HorizontalAlignment.Right:
+ originX += availableSizeMinusMargins.Width - size.Width;
+ break;
+ }
- if (useLayoutRounding)
- {
- originX = Math.Floor(originX * scale) / scale;
- originY = Math.Floor(originY * scale) / scale;
- }
+ switch (verticalContentAlignment)
+ {
+ case VerticalAlignment.Center:
+ originY += (availableSizeMinusMargins.Height - size.Height) / 2;
+ break;
+ case VerticalAlignment.Bottom:
+ originY += availableSizeMinusMargins.Height - size.Height;
+ break;
+ }
- Child.Arrange(new Rect(originX, originY, size.Width, size.Height));
+ if (useLayoutRounding)
+ {
+ originX = Math.Floor(originX * scale) / scale;
+ originY = Math.Floor(originY * scale) / scale;
}
+ Child.Arrange(new Rect(originX, originY, Math.Max(0, size.Width), Math.Max(0, size.Height)));
+
return finalSize;
}
diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
index 6deef7c7b9..77735f3f12 100644
--- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs
+++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
@@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives
///
/// Defines the property.
///
- public static readonly StyledProperty BorderThicknessProperty =
+ public static readonly StyledProperty BorderThicknessProperty =
Border.BorderThicknessProperty.AddOwner();
///
@@ -132,7 +132,7 @@ namespace Avalonia.Controls.Primitives
///
/// Gets or sets the thickness of the control's border.
///
- public double BorderThickness
+ public Thickness BorderThickness
{
get { return GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); }
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index f73725f95a..34c6b1cfd6 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -85,6 +85,7 @@ namespace Avalonia.Controls
private int _selectionEnd;
private TextPresenter _presenter;
private UndoRedoHelper _undoRedoHelper;
+ private bool _isUndoingRedoing;
private bool _ignoreTextChanges;
private static readonly string[] invalidCharacters = new String[1]{"\u007f"};
@@ -198,7 +199,11 @@ namespace Avalonia.Controls
if (!_ignoreTextChanges)
{
CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0);
- SetAndRaise(TextProperty, ref _text, value);
+
+ if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
+ {
+ _undoRedoHelper.Clear();
+ }
}
}
}
@@ -367,14 +372,30 @@ namespace Avalonia.Controls
case Key.Z:
if (modifiers == InputModifiers.Control)
{
- _undoRedoHelper.Undo();
+ try
+ {
+ _isUndoingRedoing = true;
+ _undoRedoHelper.Undo();
+ }
+ finally
+ {
+ _isUndoingRedoing = false;
+ }
handled = true;
}
break;
case Key.Y:
if (modifiers == InputModifiers.Control)
{
- _undoRedoHelper.Redo();
+ try
+ {
+ _isUndoingRedoing = true;
+ _undoRedoHelper.Redo();
+ }
+ finally
+ {
+ _isUndoingRedoing = false;
+ }
handled = true;
}
break;
@@ -791,7 +812,7 @@ namespace Avalonia.Controls
int pos = 0;
int i;
- for (i = 0; i < lines.Count; ++i)
+ for (i = 0; i < lines.Count - 1; ++i)
{
var line = lines[i];
pos += line.Length;
diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs
new file mode 100644
index 0000000000..d9169e51f3
--- /dev/null
+++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs
@@ -0,0 +1,279 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Utils
+{
+ internal class BorderRenderHelper
+ {
+ private bool _useComplexRendering;
+ private StreamGeometry _backgroundGeometryCache;
+ private StreamGeometry _borderGeometryCache;
+
+ public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
+ {
+ if (borderThickness.IsUniform && cornerRadius.IsUniform)
+ {
+ _backgroundGeometryCache = null;
+ _borderGeometryCache = null;
+ _useComplexRendering = false;
+ }
+ else
+ {
+ _useComplexRendering = true;
+
+ var boundRect = new Rect(finalSize);
+ var innerRect = boundRect.Deflate(borderThickness);
+ var innerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, false);
+
+ StreamGeometry backgroundGeometry = null;
+
+ if (innerRect.Width != 0 && innerRect.Height != 0)
+ {
+ backgroundGeometry = new StreamGeometry();
+
+ using (var ctx = backgroundGeometry.Open())
+ {
+ CreateGeometry(ctx, innerRect, innerCoordinates);
+ }
+
+ _backgroundGeometryCache = backgroundGeometry;
+ }
+ else
+ {
+ _backgroundGeometryCache = null;
+ }
+
+ if (boundRect.Width != 0 && innerRect.Height != 0)
+ {
+ var outerCoordinates = new BorderCoordinates(cornerRadius, borderThickness, true);
+ var borderGeometry = new StreamGeometry();
+
+ using (var ctx = borderGeometry.Open())
+ {
+ CreateGeometry(ctx, boundRect, outerCoordinates);
+
+ if (backgroundGeometry != null)
+ {
+ CreateGeometry(ctx, innerRect, innerCoordinates);
+ }
+ }
+
+ _borderGeometryCache = borderGeometry;
+ }
+ else
+ {
+ _borderGeometryCache = null;
+ }
+ }
+ }
+
+ public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
+ {
+ if (_useComplexRendering)
+ {
+ var backgroundGeometry = _backgroundGeometryCache;
+ if (backgroundGeometry != null)
+ {
+ context.DrawGeometry(background, null, backgroundGeometry);
+ }
+
+ var borderGeometry = _borderGeometryCache;
+ if (borderGeometry != null)
+ {
+ context.DrawGeometry(borderBrush, null, borderGeometry);
+ }
+ }
+ else
+ {
+ var borderThickness = borders.Left;
+ var cornerRadius = (float)radii.TopLeft;
+ var rect = new Rect(size);
+
+ if (background != null)
+ {
+ context.FillRectangle(background, rect.Deflate(borders), cornerRadius);
+ }
+
+ if (borderBrush != null && borderThickness > 0)
+ {
+ context.DrawRectangle(new Pen(borderBrush, borderThickness), rect.Deflate(borderThickness), cornerRadius);
+ }
+ }
+ }
+
+ private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderCoordinates borderCoordinates)
+ {
+ var topLeft = new Point(borderCoordinates.LeftTop, 0);
+ var topRight = new Point(boundRect.Width - borderCoordinates.RightTop, 0);
+ var rightTop = new Point(boundRect.Width, borderCoordinates.TopRight);
+ var rightBottom = new Point(boundRect.Width, boundRect.Height - borderCoordinates.BottomRight);
+ var bottomRight = new Point(boundRect.Width - borderCoordinates.RightBottom, boundRect.Height);
+ var bottomLeft = new Point(borderCoordinates.LeftBottom, boundRect.Height);
+ var leftBottom = new Point(0, boundRect.Height - borderCoordinates.BottomLeft);
+ var leftTop = new Point(0, borderCoordinates.TopLeft);
+
+
+ if (topLeft.X > topRight.X)
+ {
+ var scaledX = borderCoordinates.LeftTop / (borderCoordinates.LeftTop + borderCoordinates.RightTop) * boundRect.Width;
+ topLeft = new Point(scaledX, topLeft.Y);
+ topRight = new Point(scaledX, topRight.Y);
+ }
+
+ if (rightTop.Y > rightBottom.Y)
+ {
+ var scaledY = borderCoordinates.TopRight / (borderCoordinates.TopRight + borderCoordinates.BottomRight) * boundRect.Height;
+ rightTop = new Point(rightTop.X, scaledY);
+ rightBottom = new Point(rightBottom.X, scaledY);
+ }
+
+ if (bottomRight.X < bottomLeft.X)
+ {
+ var scaledX = borderCoordinates.LeftBottom / (borderCoordinates.LeftBottom + borderCoordinates.RightBottom) * boundRect.Width;
+ bottomRight = new Point(scaledX, bottomRight.Y);
+ bottomLeft = new Point(scaledX, bottomLeft.Y);
+ }
+
+ if (leftBottom.Y < leftTop.Y)
+ {
+ var scaledY = borderCoordinates.TopLeft / (borderCoordinates.TopLeft + borderCoordinates.BottomLeft) * boundRect.Height;
+ leftBottom = new Point(leftBottom.X, scaledY);
+ leftTop = new Point(leftTop.X, scaledY);
+ }
+
+ var offset = new Vector(boundRect.TopLeft.X, boundRect.TopLeft.Y);
+ topLeft += offset;
+ topRight += offset;
+ rightTop += offset;
+ rightBottom += offset;
+ bottomRight += offset;
+ bottomLeft += offset;
+ leftBottom += offset;
+ leftTop += offset;
+
+ context.BeginFigure(topLeft, true);
+
+ //Top
+ context.LineTo(topRight);
+
+ //TopRight corner
+ var radiusX = boundRect.TopRight.X - topRight.X;
+ var radiusY = rightTop.Y - boundRect.TopRight.Y;
+ if (radiusX != 0 || radiusY != 0)
+ {
+ context.ArcTo(rightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise);
+ }
+
+ //Right
+ context.LineTo(rightBottom);
+
+ //BottomRight corner
+ radiusX = boundRect.BottomRight.X - bottomRight.X;
+ radiusY = boundRect.BottomRight.Y - rightBottom.Y;
+ if (radiusX != 0 || radiusY != 0)
+ {
+ context.ArcTo(bottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+ }
+
+ //Bottom
+ context.LineTo(bottomLeft);
+
+ //BottomLeft corner
+ radiusX = bottomLeft.X - boundRect.BottomLeft.X;
+ radiusY = boundRect.BottomLeft.Y - leftBottom.Y;
+ if (radiusX != 0 || radiusY != 0)
+ {
+ context.ArcTo(leftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+ }
+
+ //Left
+ context.LineTo(leftTop);
+
+ //TopLeft corner
+ radiusX = topLeft.X - boundRect.TopLeft.X;
+ radiusY = leftTop.Y - boundRect.TopLeft.Y;
+
+ if (radiusX != 0 || radiusY != 0)
+ {
+ context.ArcTo(topLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
+ }
+
+ context.EndFigure(true);
+ }
+
+ private struct BorderCoordinates
+ {
+ internal BorderCoordinates(CornerRadius cornerRadius, Thickness borderThickness, bool isOuter)
+ {
+ var left = 0.5 * borderThickness.Left;
+ var top = 0.5 * borderThickness.Top;
+ var right = 0.5 * borderThickness.Right;
+ var bottom = 0.5 * borderThickness.Bottom;
+
+ if (isOuter)
+ {
+ if (cornerRadius.TopLeft == 0)
+ {
+ LeftTop = TopLeft = 0.0;
+ }
+ else
+ {
+ LeftTop = cornerRadius.TopLeft + left;
+ TopLeft = cornerRadius.TopLeft + top;
+ }
+ if (cornerRadius.TopRight == 0)
+ {
+ TopRight = RightTop = 0;
+ }
+ else
+ {
+ TopRight = cornerRadius.TopRight + top;
+ RightTop = cornerRadius.TopRight + right;
+ }
+ if (cornerRadius.BottomRight == 0)
+ {
+ RightBottom = BottomRight = 0;
+ }
+ else
+ {
+ RightBottom = cornerRadius.BottomRight + right;
+ BottomRight = cornerRadius.BottomRight + bottom;
+ }
+ if (cornerRadius.BottomLeft == 0)
+ {
+ BottomLeft = LeftBottom = 0;
+ }
+ else
+ {
+ BottomLeft = cornerRadius.BottomLeft + bottom;
+ LeftBottom = cornerRadius.BottomLeft + left;
+ }
+ }
+ else
+ {
+ LeftTop = Math.Max(0, cornerRadius.TopLeft - left);
+ TopLeft = Math.Max(0, cornerRadius.TopLeft - top);
+ TopRight = Math.Max(0, cornerRadius.TopRight - top);
+ RightTop = Math.Max(0, cornerRadius.TopRight - right);
+ RightBottom = Math.Max(0, cornerRadius.BottomRight - right);
+ BottomRight = Math.Max(0, cornerRadius.BottomRight - bottom);
+ BottomLeft = Math.Max(0, cornerRadius.BottomLeft - bottom);
+ LeftBottom = Math.Max(0, cornerRadius.BottomLeft - left);
+ }
+ }
+
+ internal readonly double LeftTop;
+ internal readonly double TopLeft;
+ internal readonly double TopRight;
+ internal readonly double RightTop;
+ internal readonly double RightBottom;
+ internal readonly double BottomRight;
+ internal readonly double BottomLeft;
+ internal readonly double LeftBottom;
+ }
+
+ }
+}
diff --git a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs
new file mode 100644
index 0000000000..3c1006a12e
--- /dev/null
+++ b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs
@@ -0,0 +1,64 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Collections;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+
+namespace Avalonia.Controls.Utils
+{
+ ///
+ /// Defines an item collection, selection members, and key handling for the
+ /// selection adapter contained in the drop-down portion of an
+ /// control.
+ ///
+ public interface ISelectionAdapter
+ {
+ ///
+ /// Gets or sets the selected item.
+ ///
+ /// The currently selected item.
+ object SelectedItem { get; set; }
+
+ ///
+ /// Occurs when the
+ ///
+ /// property value changes.
+ ///
+ event EventHandler SelectionChanged;
+
+ ///
+ /// Gets or sets a collection that is used to generate content for the
+ /// selection adapter.
+ ///
+ /// The collection that is used to generate content for the
+ /// selection adapter.
+ IEnumerable Items { get; set; }
+
+ ///
+ /// Occurs when a selected item is not cancelled and is committed as the
+ /// selected item.
+ ///
+ event EventHandler Commit;
+
+ ///
+ /// Occurs when a selection has been canceled.
+ ///
+ event EventHandler Cancel;
+
+ ///
+ /// Provides handling for the
+ /// event that occurs
+ /// when a key is pressed while the drop-down portion of the
+ /// has focus.
+ ///
+ /// A
+ /// that contains data about the
+ /// event.
+ void HandleKeyDown(KeyEventArgs e);
+ }
+
+}
diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
new file mode 100644
index 0000000000..43c8a5aa6c
--- /dev/null
+++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs
@@ -0,0 +1,342 @@
+// (c) Copyright Microsoft Corporation.
+// This source is subject to the Microsoft Public License (Ms-PL).
+// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
+// All other rights reserved.
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+using System.Collections;
+using System.Diagnostics;
+
+namespace Avalonia.Controls.Utils
+{
+ ///
+ /// Represents the selection adapter contained in the drop-down portion of
+ /// an control.
+ ///
+ public class SelectingItemsControlSelectionAdapter : ISelectionAdapter
+ {
+ ///
+ /// The SelectingItemsControl instance.
+ ///
+ private SelectingItemsControl _selector;
+
+ ///
+ /// Gets or sets a value indicating whether the selection change event
+ /// should not be fired.
+ ///
+ private bool IgnoringSelectionChanged { get; set; }
+
+ ///
+ /// Gets or sets the underlying
+ ///
+ /// control.
+ ///
+ /// The underlying
+ ///
+ /// control.
+ public SelectingItemsControl SelectorControl
+ {
+ get { return _selector; }
+
+ set
+ {
+ if (_selector != null)
+ {
+ _selector.SelectionChanged -= OnSelectionChanged;
+ _selector.PointerReleased -= OnSelectorPointerReleased;
+ }
+
+ _selector = value;
+
+ if (_selector != null)
+ {
+ _selector.SelectionChanged += OnSelectionChanged;
+ _selector.PointerReleased += OnSelectorPointerReleased;
+ }
+ }
+ }
+
+ ///
+ /// Occurs when the
+ ///
+ /// property value changes.
+ ///
+ public event EventHandler SelectionChanged;
+
+ ///
+ /// Occurs when an item is selected and is committed to the underlying
+ ///
+ /// control.
+ ///
+ public event EventHandler Commit;
+
+ ///
+ /// Occurs when a selection is canceled before it is committed.
+ ///
+ public event EventHandler Cancel;
+
+ ///
+ /// Initializes a new instance of the
+ ///
+ /// class.
+ ///
+ public SelectingItemsControlSelectionAdapter()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the
+ ///
+ /// class with the specified
+ ///
+ /// control.
+ ///
+ /// The
+ /// control
+ /// to wrap as a
+ /// .
+ public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector)
+ {
+ SelectorControl = selector;
+ }
+
+ ///
+ /// Gets or sets the selected item of the selection adapter.
+ ///
+ /// The selected item of the underlying selection adapter.
+ public object SelectedItem
+ {
+ get
+ {
+ return SelectorControl?.SelectedItem;
+ }
+
+ set
+ {
+ IgnoringSelectionChanged = true;
+ if (SelectorControl != null)
+ {
+ SelectorControl.SelectedItem = value;
+ }
+
+ // Attempt to reset the scroll viewer's position
+ if (value == null)
+ {
+ ResetScrollViewer();
+ }
+
+ IgnoringSelectionChanged = false;
+ }
+ }
+
+ ///
+ /// Gets or sets a collection that is used to generate the content of
+ /// the selection adapter.
+ ///
+ /// The collection used to generate content for the selection
+ /// adapter.
+ public IEnumerable Items
+ {
+ get
+ {
+ return SelectorControl?.Items;
+ }
+ set
+ {
+ if (SelectorControl != null)
+ {
+ SelectorControl.Items = value;
+ }
+ }
+ }
+
+ ///
+ /// If the control contains a ScrollViewer, this will reset the viewer
+ /// to be scrolled to the top.
+ ///
+ private void ResetScrollViewer()
+ {
+ if (SelectorControl != null)
+ {
+ ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType().FirstOrDefault();
+ if (sv != null)
+ {
+ sv.Offset = new Vector(0, 0);
+ }
+ }
+ }
+
+ ///
+ /// Handles the mouse left button up event on the selector control.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e)
+ {
+ if (e.MouseButton == MouseButton.Left)
+ {
+ OnCommit();
+ }
+ }
+
+ ///
+ /// Handles the SelectionChanged event on the SelectingItemsControl control.
+ ///
+ /// The source object.
+ /// The selection changed event data.
+ private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (IgnoringSelectionChanged)
+ {
+ return;
+ }
+
+ SelectionChanged?.Invoke(sender, e);
+ }
+
+ ///
+ /// Increments the
+ ///
+ /// property of the underlying
+ ///
+ /// control.
+ ///
+ protected void SelectedIndexIncrement()
+ {
+ if (SelectorControl != null)
+ {
+ SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1;
+ }
+ }
+
+ ///
+ /// Decrements the
+ ///
+ /// property of the underlying
+ ///
+ /// control.
+ ///
+ protected void SelectedIndexDecrement()
+ {
+ if (SelectorControl != null)
+ {
+ int index = SelectorControl.SelectedIndex;
+ if (index >= 0)
+ {
+ SelectorControl.SelectedIndex--;
+ }
+ else if (index == -1)
+ {
+ SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1;
+ }
+ }
+ }
+
+ ///
+ /// Provides handling for the
+ /// event that occurs
+ /// when a key is pressed while the drop-down portion of the
+ /// has focus.
+ ///
+ /// A
+ /// that contains data about the
+ /// event.
+ public void HandleKeyDown(KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Enter:
+ OnCommit();
+ e.Handled = true;
+ break;
+
+ case Key.Up:
+ SelectedIndexDecrement();
+ e.Handled = true;
+ break;
+
+ case Key.Down:
+ if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None)
+ {
+ SelectedIndexIncrement();
+ e.Handled = true;
+ }
+ break;
+
+ case Key.Escape:
+ OnCancel();
+ e.Handled = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ protected virtual void OnCommit()
+ {
+ OnCommit(this, new RoutedEventArgs());
+ }
+
+ ///
+ /// Fires the Commit event.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnCommit(object sender, RoutedEventArgs e)
+ {
+ Commit?.Invoke(sender, e);
+
+ AfterAdapterAction();
+ }
+
+ ///
+ /// Raises the
+ ///
+ /// event.
+ ///
+ protected virtual void OnCancel()
+ {
+ OnCancel(this, new RoutedEventArgs());
+ }
+
+ ///
+ /// Fires the Cancel event.
+ ///
+ /// The source object.
+ /// The event data.
+ private void OnCancel(object sender, RoutedEventArgs e)
+ {
+ Cancel?.Invoke(sender, e);
+
+ AfterAdapterAction();
+ }
+
+ ///
+ /// Change the selection after the actions are complete.
+ ///
+ private void AfterAdapterAction()
+ {
+ IgnoringSelectionChanged = true;
+ if (SelectorControl != null)
+ {
+ SelectorControl.SelectedItem = null;
+ SelectorControl.SelectedIndex = -1;
+ }
+ IgnoringSelectionChanged = false;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs
index be4c1aa6c4..c76555e554 100644
--- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs
+++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs
@@ -91,6 +91,11 @@ namespace Avalonia.Controls.Utils
}
}
+ public void Clear()
+ {
+ _states.Clear();
+ }
+
bool WeakTimer.IWeakTimerSubscriber.Tick()
{
Snapshot();
diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs
index d815f8082b..2a1cdec1c0 100644
--- a/src/Avalonia.Input/KeyboardDevice.cs
+++ b/src/Avalonia.Input/KeyboardDevice.cs
@@ -46,13 +46,13 @@ namespace Avalonia.Input
if (element != FocusedElement)
{
var interactive = FocusedElement as IInteractive;
+ FocusedElement = element;
interactive?.RaiseEvent(new RoutedEventArgs
{
RoutedEvent = InputElement.LostFocusEvent,
});
- FocusedElement = element;
interactive = element as IInteractive;
interactive?.RaiseEvent(new GotFocusEventArgs
diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml
index cb86598a42..4c85e172ff 100644
--- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml
+++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml
@@ -20,7 +20,7 @@
Red
#10ff0000
- 2
+ 2
0.5
10
diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml
new file mode 100644
index 0000000000..82dbf6064b
--- /dev/null
+++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml
index 7c567c1835..2b9132ee56 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -23,7 +23,7 @@
-
+
@@ -43,4 +43,6 @@
+
+
diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml
index 1cd9587297..66f226d2f6 100644
--- a/src/Avalonia.Themes.Default/MenuItem.xaml
+++ b/src/Avalonia.Themes.Default/MenuItem.xaml
@@ -133,4 +133,8 @@
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Default/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml
new file mode 100644
index 0000000000..e6325d07dc
--- /dev/null
+++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml
@@ -0,0 +1,41 @@
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/CornerRadius.cs b/src/Avalonia.Visuals/CornerRadius.cs
new file mode 100644
index 0000000000..db0ac97d49
--- /dev/null
+++ b/src/Avalonia.Visuals/CornerRadius.cs
@@ -0,0 +1,97 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Globalization;
+using System.Linq;
+
+namespace Avalonia
+{
+ public struct CornerRadius
+ {
+ public CornerRadius(double uniformRadius)
+ {
+ TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius;
+
+ }
+ public CornerRadius(double top, double bottom)
+ {
+ TopLeft = TopRight = top;
+ BottomLeft = BottomRight = bottom;
+ }
+ public CornerRadius(double topLeft, double topRight, double bottomRight, double bottomLeft)
+ {
+ TopLeft = topLeft;
+ TopRight = topRight;
+ BottomRight = bottomRight;
+ BottomLeft = bottomLeft;
+ }
+
+ public double TopLeft { get; }
+ public double TopRight { get; }
+ public double BottomRight { get; }
+ public double BottomLeft { get; }
+ public bool IsEmpty => TopLeft.Equals(0) && IsUniform;
+ public bool IsUniform => TopLeft.Equals(TopRight) && BottomLeft.Equals(BottomRight) && TopRight.Equals(BottomRight);
+
+ public override bool Equals(object obj)
+ {
+ if (obj is CornerRadius)
+ {
+ return this == (CornerRadius)obj;
+ }
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return TopLeft.GetHashCode() ^ TopRight.GetHashCode() ^ BottomLeft.GetHashCode() ^ BottomRight.GetHashCode();
+ }
+
+ public override string ToString()
+ {
+ return $"{TopLeft},{TopRight},{BottomRight},{BottomLeft}";
+ }
+
+ public static CornerRadius Parse(string s, CultureInfo culture)
+ {
+ var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Trim())
+ .ToList();
+
+ switch (parts.Count)
+ {
+ case 1:
+ var uniform = double.Parse(parts[0], culture);
+ return new CornerRadius(uniform);
+ case 2:
+ var top = double.Parse(parts[0], culture);
+ var bottom = double.Parse(parts[1], culture);
+ return new CornerRadius(top, bottom);
+ case 4:
+ var topLeft = double.Parse(parts[0], culture);
+ var topRight = double.Parse(parts[1], culture);
+ var bottomRight = double.Parse(parts[2], culture);
+ var bottomLeft = double.Parse(parts[3], culture);
+ return new CornerRadius(topLeft, topRight, bottomRight, bottomLeft);
+ default:
+ {
+ throw new FormatException("Invalid CornerRadius.");
+ }
+ }
+ }
+
+ public static bool operator ==(CornerRadius cr1, CornerRadius cr2)
+ {
+ return cr1.TopLeft.Equals(cr2.TopLeft)
+ && cr1.TopRight.Equals(cr2.TopRight)
+ && cr1.BottomRight.Equals(cr2.BottomRight)
+ && cr1.BottomLeft.Equals(cr2.BottomLeft);
+ }
+
+ public static bool operator !=(CornerRadius cr1, CornerRadius cr2)
+ {
+ return !(cr1 == cr2);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Media/GradientBrush.cs b/src/Avalonia.Visuals/Media/GradientBrush.cs
index 8c2c9a2c01..fb29f68373 100644
--- a/src/Avalonia.Visuals/Media/GradientBrush.cs
+++ b/src/Avalonia.Visuals/Media/GradientBrush.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Media
/// Defines the property.
///
public static readonly StyledProperty> GradientStopsProperty =
- AvaloniaProperty.Register>(nameof(Opacity));
+ AvaloniaProperty.Register>(nameof(GradientStops));
///
/// Initializes a new instance of the class.
diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
index 87347d64b1..900746d05a 100644
--- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs
@@ -9,6 +9,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
\ No newline at end of file
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
index 6005ee8b8f..799380cb85 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
@@ -167,7 +167,9 @@ namespace Avalonia.Rendering.SceneGraph
using (context.PushPostTransform(m))
using (context.PushTransformContainer())
{
- var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip);
+ var clipBounds = clipToBounds ?
+ bounds.TransformToAABB(contextImpl.Transform).Intersect(clip) :
+ clip;
forceRecurse = forceRecurse ||
node.ClipBounds != clipBounds ||
diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs
index dc9be7341d..ead9fd004a 100644
--- a/src/Avalonia.Visuals/Thickness.cs
+++ b/src/Avalonia.Visuals/Thickness.cs
@@ -90,7 +90,12 @@ namespace Avalonia
///
/// Gets a value indicating whether all sides are set to 0.
///
- public bool IsEmpty => Left == 0 && Top == 0 && Right == 0 && Bottom == 0;
+ public bool IsEmpty => Left.Equals(0) && IsUniform;
+
+ ///
+ /// Gets a value indicating whether all sides are equal.
+ ///
+ public bool IsUniform => Left.Equals(Right) && Top.Equals(Bottom) && Right.Equals(Bottom);
///
/// Compares two Thicknesses.
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 0ce2a1a992..bd6acfdad1 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -31,6 +31,7 @@
+
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
new file mode 100644
index 0000000000..5da0efae1b
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml/Converters/CornerRadiusTypeConverter.cs
@@ -0,0 +1,19 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+ public class CornerRadiusTypeConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return sourceType == typeof(string);
+ }
+
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ {
+ return CornerRadius.Parse((string)value, culture);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
index c6705cbb4b..6e8fe1e4c0 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
@@ -43,6 +43,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Mode = Mode,
Path = pathInfo.Path,
Priority = Priority,
+ Source = Source,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
};
diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
index 1cf5b6a58e..1ae24c8a34 100644
--- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaDefaultTypeConverters.cs
@@ -43,6 +43,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml
{ typeof(Selector), typeof(SelectorTypeConverter)},
{ typeof(SolidColorBrush), typeof(BrushTypeConverter) },
{ typeof(Thickness), typeof(ThicknessTypeConverter) },
+ { typeof(CornerRadius), typeof(CornerRadiusTypeConverter) },
{ typeof(TimeSpan), typeof(TimeSpanTypeConverter) },
//{ typeof(Uri), typeof(Converters.UriTypeConverter) },
{ typeof(Cursor), typeof(CursorTypeConverter) },
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
index 8f11d1463b..120ab71ead 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
@@ -1,7 +1,6 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
-using System;
using Avalonia.Platform;
using SharpDX.Direct2D1;
@@ -20,14 +19,12 @@ namespace Avalonia.Direct2D1.Media
///
public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia();
- ///
public Geometry Geometry { get; }
///
public Rect GetRenderBounds(Avalonia.Media.Pen pen)
{
- var factory = AvaloniaLocator.Current.GetService();
- return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
+ return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
}
///
@@ -51,7 +48,7 @@ namespace Avalonia.Direct2D1.Media
///
public bool StrokeContains(Avalonia.Media.Pen pen, Point point)
{
- return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness);
+ return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)(pen?.Thickness ?? 0));
}
public ITransformedGeometryImpl WithTransform(Matrix transform)
diff --git a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
index 33af55fdf9..d24a646f74 100644
--- a/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
+++ b/tests/Avalonia.Benchmarks/Styling/ApplyStyling.cs
@@ -33,7 +33,7 @@ namespace Avalonia.Benchmarks.Styling
var border = (Border)textBox.GetVisualChildren().Single();
- if (border.BorderThickness != 2)
+ if (border.BorderThickness != new Thickness(2))
{
throw new Exception("Styles not applied.");
}
diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
new file mode 100644
index 0000000000..f9da2ab6f3
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
@@ -0,0 +1,1042 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+using System.Collections.ObjectModel;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class AutoCompleteBoxTests
+ {
+ [Fact]
+ public void Search_Filters()
+ {
+ Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("am", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.Contains)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("hello", "name"));
+
+ Assert.Null(GetFilter(AutoCompleteFilterMode.Custom));
+ Assert.Null(GetFilter(AutoCompleteFilterMode.None));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "na"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.Equals)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "na"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("na", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWith)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("hello", "name"));
+ }
+
+ [Fact]
+ public void Ordinal_Search_Filters()
+ {
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("am", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("AME", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "na"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "na"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "NA"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("na", "name"));
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("hello", "name"));
+
+ Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("na", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("NAM", "name"));
+ Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("hello", "name"));
+ }
+
+ [Fact]
+ public void Fires_DropDown_Events()
+ {
+ RunTest((control, textbox) =>
+ {
+ bool openEvent = false;
+ bool closeEvent = false;
+ control.DropDownOpened += (s, e) => openEvent = true;
+ control.DropDownClosed += (s, e) => closeEvent = true;
+ control.Items = CreateSimpleStringArray();
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.SearchText == "a");
+ Assert.True(control.IsDropDownOpen);
+ Assert.True(openEvent);
+
+ textbox.Text = String.Empty;
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.SearchText == String.Empty);
+ Assert.False(control.IsDropDownOpen);
+ Assert.True(closeEvent);
+ });
+ }
+
+ [Fact]
+ public void Text_Completion_Via_Text_Property()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.IsTextCompletionEnabled = true;
+
+ Assert.Equal(String.Empty, control.Text);
+ control.Text = "close";
+ Assert.NotNull(control.SelectedItem);
+ });
+ }
+
+ [Fact]
+ public void Text_Completion_Selects_Text()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.IsTextCompletionEnabled = true;
+
+ textbox.Text = "ac";
+ textbox.SelectionEnd = textbox.SelectionStart = 2;
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(control.IsDropDownOpen);
+ Assert.True(Math.Abs(textbox.SelectionEnd - textbox.SelectionStart) > 2);
+ });
+ }
+
+ [Fact]
+ public void TextChanged_Event_Fires()
+ {
+ RunTest((control, textbox) =>
+ {
+ bool textChanged = false;
+ control.TextChanged += (s, e) => textChanged = true;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(textChanged);
+
+ textChanged = false;
+ control.Text = "conversati";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(textChanged);
+
+ textChanged = false;
+ control.Text = null;
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(textChanged);
+ });
+ }
+
+ [Fact]
+ public void MinimumPrefixLength_Works()
+ {
+ RunTest((control, textbox) =>
+ {
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.IsDropDownOpen);
+
+
+ textbox.Text = String.Empty;
+ Dispatcher.UIThread.RunJobs();
+ Assert.False(control.IsDropDownOpen);
+
+ control.MinimumPrefixLength = 3;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.False(control.IsDropDownOpen);
+
+ textbox.Text = "acc";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.IsDropDownOpen);
+ });
+ }
+
+ [Fact]
+ public void Can_Cancel_DropDown_Opening()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.DropDownOpening += (s, e) => e.Cancel = true;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.False(control.IsDropDownOpen);
+ });
+ }
+
+ [Fact]
+ public void Can_Cancel_DropDown_Closing()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.DropDownClosing += (s, e) => e.Cancel = true;
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.True(control.IsDropDownOpen);
+
+ control.IsDropDownOpen = false;
+ Assert.True(control.IsDropDownOpen);
+ });
+ }
+
+ [Fact]
+ public void Can_Cancel_Population()
+ {
+ RunTest((control, textbox) =>
+ {
+ bool populating = false;
+ bool populated = false;
+ control.FilterMode = AutoCompleteFilterMode.None;
+ control.Populating += (s, e) =>
+ {
+ e.Cancel = true;
+ populating = true;
+ };
+ control.Populated += (s, e) => populated = true;
+
+ textbox.Text = "accounti";
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(populating);
+ Assert.False(populated);
+ });
+ }
+
+ [Fact]
+ public void Custom_Population_Supported()
+ {
+ RunTest((control, textbox) =>
+ {
+ string custom = "Custom!";
+ string search = "accounti";
+ bool populated = false;
+ bool populatedOk = false;
+ control.FilterMode = AutoCompleteFilterMode.None;
+ control.Populating += (s, e) =>
+ {
+ control.Items = new string[] { custom };
+ Assert.Equal(search, e.Parameter);
+ };
+ control.Populated += (s, e) =>
+ {
+ populated = true;
+ ReadOnlyCollection collection = e.Data as ReadOnlyCollection;
+ populatedOk = collection != null && collection.Count == 1;
+ };
+
+ textbox.Text = search;
+ Dispatcher.UIThread.RunJobs();
+
+ Assert.True(populated);
+ Assert.True(populatedOk);
+ });
+ }
+
+ [Fact]
+ public void Text_Completion()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.IsTextCompletionEnabled = true;
+ textbox.Text = "accounti";
+ textbox.SelectionStart = textbox.SelectionEnd = textbox.Text.Length;
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal("accounti", control.SearchText);
+ Assert.Equal("accounting", textbox.Text);
+ });
+ }
+
+ [Fact]
+ public void String_Search()
+ {
+ RunTest((control, textbox) =>
+ {
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "acc";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "accept";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+ });
+ }
+
+ [Fact]
+ public void Item_Search()
+ {
+ RunTest((control, textbox) =>
+ {
+ control.FilterMode = AutoCompleteFilterMode.Custom;
+ control.ItemFilter = (search, item) =>
+ {
+ string s = item as string;
+ return s == null ? false : true;
+ };
+
+ // Just set to null briefly to exercise that code path
+ AutoCompleteFilterPredicate filter = control.ItemFilter;
+ Assert.NotNull(filter);
+ control.ItemFilter = null;
+ Assert.Null(control.ItemFilter);
+ control.ItemFilter = filter;
+ Assert.NotNull(control.ItemFilter);
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "acc";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "a";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "accept";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+
+ textbox.Text = "cook";
+ Dispatcher.UIThread.RunJobs();
+ Assert.Equal(textbox.Text, control.Text);
+ });
+ }
+
+ ///
+ /// Retrieves a defined predicate filter through a new AutoCompleteBox
+ /// control instance.
+ ///
+ /// The FilterMode of interest.
+ /// Returns the predicate instance.
+ private static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode mode)
+ {
+ return new AutoCompleteBox { FilterMode = mode }
+ .TextFilter;
+ }
+
+ ///
+ /// Creates a large list of strings for AutoCompleteBox testing.
+ ///
+ /// Returns a new List of string values.
+ private IList CreateSimpleStringArray()
+ {
+ return new List
+ {
+ "a",
+ "abide",
+ "able",
+ "about",
+ "above",
+ "absence",
+ "absurd",
+ "accept",
+ "acceptance",
+ "accepted",
+ "accepting",
+ "access",
+ "accessed",
+ "accessible",
+ "accident",
+ "accidentally",
+ "accordance",
+ "account",
+ "accounting",
+ "accounts",
+ "accusation",
+ "accustomed",
+ "ache",
+ "across",
+ "act",
+ "active",
+ "actual",
+ "actually",
+ "ada",
+ "added",
+ "adding",
+ "addition",
+ "additional",
+ "additions",
+ "address",
+ "addressed",
+ "addresses",
+ "addressing",
+ "adjourn",
+ "adoption",
+ "advance",
+ "advantage",
+ "adventures",
+ "advice",
+ "advisable",
+ "advise",
+ "affair",
+ "affectionately",
+ "afford",
+ "afore",
+ "afraid",
+ "after",
+ "afterwards",
+ "again",
+ "against",
+ "age",
+ "aged",
+ "agent",
+ "ago",
+ "agony",
+ "agree",
+ "agreed",
+ "agreement",
+ "ah",
+ "ahem",
+ "air",
+ "airs",
+ "ak",
+ "alarm",
+ "alarmed",
+ "alas",
+ "alice",
+ "alive",
+ "all",
+ "allow",
+ "almost",
+ "alone",
+ "along",
+ "aloud",
+ "already",
+ "also",
+ "alteration",
+ "altered",
+ "alternate",
+ "alternately",
+ "altogether",
+ "always",
+ "am",
+ "ambition",
+ "among",
+ "an",
+ "ancient",
+ "and",
+ "anger",
+ "angrily",
+ "angry",
+ "animal",
+ "animals",
+ "ann",
+ "annoy",
+ "annoyed",
+ "another",
+ "answer",
+ "answered",
+ "answers",
+ "antipathies",
+ "anxious",
+ "anxiously",
+ "any",
+ "anyone",
+ "anything",
+ "anywhere",
+ "appealed",
+ "appear",
+ "appearance",
+ "appeared",
+ "appearing",
+ "appears",
+ "applause",
+ "apple",
+ "apples",
+ "applicable",
+ "apply",
+ "approach",
+ "arch",
+ "archbishop",
+ "arches",
+ "archive",
+ "are",
+ "argue",
+ "argued",
+ "argument",
+ "arguments",
+ "arise",
+ "arithmetic",
+ "arm",
+ "arms",
+ "around",
+ "arranged",
+ "array",
+ "arrived",
+ "arrow",
+ "arrum",
+ "as",
+ "ascii",
+ "ashamed",
+ "ask",
+ "askance",
+ "asked",
+ "asking",
+ "asleep",
+ "assembled",
+ "assistance",
+ "associated",
+ "at",
+ "ate",
+ "atheling",
+ "atom",
+ "attached",
+ "attempt",
+ "attempted",
+ "attempts",
+ "attended",
+ "attending",
+ "attends",
+ "audibly",
+ "australia",
+ "author",
+ "authority",
+ "available",
+ "avoid",
+ "away",
+ "awfully",
+ "axes",
+ "axis",
+ "b",
+ "baby",
+ "back",
+ "backs",
+ "bad",
+ "bag",
+ "baked",
+ "balanced",
+ "bank",
+ "banks",
+ "banquet",
+ "bark",
+ "barking",
+ "barley",
+ "barrowful",
+ "based",
+ "bat",
+ "bathing",
+ "bats",
+ "bawled",
+ "be",
+ "beak",
+ "bear",
+ "beast",
+ "beasts",
+ "beat",
+ "beating",
+ "beau",
+ "beauti",
+ "beautiful",
+ "beautifully",
+ "beautify",
+ "became",
+ "because",
+ "become",
+ "becoming",
+ "bed",
+ "beds",
+ "bee",
+ "been",
+ "before",
+ "beg",
+ "began",
+ "begged",
+ "begin",
+ "beginning",
+ "begins",
+ "begun",
+ "behead",
+ "beheaded",
+ "beheading",
+ "behind",
+ "being",
+ "believe",
+ "believed",
+ "bells",
+ "belong",
+ "belongs",
+ "beloved",
+ "below",
+ "belt",
+ "bend",
+ "bent",
+ "besides",
+ "best",
+ "better",
+ "between",
+ "bill",
+ "binary",
+ "bird",
+ "birds",
+ "birthday",
+ "bit",
+ "bite",
+ "bitter",
+ "blacking",
+ "blades",
+ "blame",
+ "blasts",
+ "bleeds",
+ "blew",
+ "blow",
+ "blown",
+ "blows",
+ "body",
+ "boldly",
+ "bone",
+ "bones",
+ "book",
+ "books",
+ "boon",
+ "boots",
+ "bore",
+ "both",
+ "bother",
+ "bottle",
+ "bottom",
+ "bough",
+ "bound",
+ "bowed",
+ "bowing",
+ "box",
+ "boxed",
+ "boy",
+ "brain",
+ "branch",
+ "branches",
+ "brandy",
+ "brass",
+ "brave",
+ "breach",
+ "bread",
+ "break",
+ "breath",
+ "breathe",
+ "breeze",
+ "bright",
+ "brightened",
+ "bring",
+ "bringing",
+ "bristling",
+ "broke",
+ "broken",
+ "brother",
+ "brought",
+ "brown",
+ "brush",
+ "brushing",
+ "burn",
+ "burning",
+ "burnt",
+ "burst",
+ "bursting",
+ "busily",
+ "business",
+ "business@pglaf",
+ "busy",
+ "but",
+ "butter",
+ "buttercup",
+ "buttered",
+ "butterfly",
+ "buttons",
+ "by",
+ "bye",
+ "c",
+ "cackled",
+ "cake",
+ "cakes",
+ "calculate",
+ "calculated",
+ "call",
+ "called",
+ "calling",
+ "calmly",
+ "came",
+ "camomile",
+ "can",
+ "canary",
+ "candle",
+ "cannot",
+ "canterbury",
+ "canvas",
+ "capering",
+ "capital",
+ "card",
+ "cardboard",
+ "cards",
+ "care",
+ "carefully",
+ "cares",
+ "carried",
+ "carrier",
+ "carroll",
+ "carry",
+ "carrying",
+ "cart",
+ "cartwheels",
+ "case",
+ "cat",
+ "catch",
+ "catching",
+ "caterpillar",
+ "cats",
+ "cattle",
+ "caucus",
+ "caught",
+ "cauldron",
+ "cause",
+ "caused",
+ "cautiously",
+ "cease",
+ "ceiling",
+ "centre",
+ "certain",
+ "certainly",
+ "chain",
+ "chains",
+ "chair",
+ "chance",
+ "chanced",
+ "change",
+ "changed",
+ "changes",
+ "changing",
+ "chapter",
+ "character",
+ "charge",
+ "charges",
+ "charitable",
+ "charities",
+ "chatte",
+ "cheap",
+ "cheated",
+ "check",
+ "checked",
+ "checks",
+ "cheeks",
+ "cheered",
+ "cheerfully",
+ "cherry",
+ "cheshire",
+ "chief",
+ "child",
+ "childhood",
+ "children",
+ "chimney",
+ "chimneys",
+ "chin",
+ "choice",
+ "choke",
+ "choked",
+ "choking",
+ "choose",
+ "choosing",
+ "chop",
+ "chorus",
+ "chose",
+ "christmas",
+ "chrysalis",
+ "chuckled",
+ "circle",
+ "circumstances",
+ "city",
+ "civil",
+ "claim",
+ "clamour",
+ "clapping",
+ "clasped",
+ "classics",
+ "claws",
+ "clean",
+ "clear",
+ "cleared",
+ "clearer",
+ "clearly",
+ "clever",
+ "climb",
+ "clinging",
+ "clock",
+ "close",
+ "closed",
+ "closely",
+ "closer",
+ "clubs",
+ "coast",
+ "coaxing",
+ "codes",
+ "coils",
+ "cold",
+ "collar",
+ "collected",
+ "collection",
+ "come",
+ "comes",
+ "comfits",
+ "comfort",
+ "comfortable",
+ "comfortably",
+ "coming",
+ "commercial",
+ "committed",
+ "common",
+ "commotion",
+ "company",
+ "compilation",
+ "complained",
+ "complaining",
+ "completely",
+ "compliance",
+ "comply",
+ "complying",
+ "compressed",
+ "computer",
+ "computers",
+ "concept",
+ "concerning",
+ "concert",
+ "concluded",
+ "conclusion",
+ "condemn",
+ "conduct",
+ "confirmation",
+ "confirmed",
+ "confused",
+ "confusing",
+ "confusion",
+ "conger",
+ "conqueror",
+ "conquest",
+ "consented",
+ "consequential",
+ "consider",
+ "considerable",
+ "considered",
+ "considering",
+ "constant",
+ "consultation",
+ "contact",
+ "contain",
+ "containing",
+ "contempt",
+ "contemptuous",
+ "contemptuously",
+ "content",
+ "continued",
+ "contract",
+ "contradicted",
+ "contributions",
+ "conversation",
+ "conversations",
+ "convert",
+ "cook",
+ "cool",
+ "copied",
+ "copies",
+ "copy",
+ "copying",
+ "copyright",
+ "corner",
+ "corners",
+ "corporation",
+ "corrupt",
+ "cost",
+ "costs",
+ "could",
+ "couldn",
+ "counting",
+ "countries",
+ "country",
+ "couple",
+ "couples",
+ "courage",
+ "course",
+ "court",
+ "courtiers",
+ "coward",
+ "crab",
+ "crash",
+ "crashed",
+ "crawled",
+ "crawling",
+ "crazy",
+ "created",
+ "creating",
+ "creation",
+ "creature",
+ "creatures",
+ "credit",
+ "creep",
+ "crept",
+ "cried",
+ "cries",
+ "crimson",
+ "critical",
+ "crocodile",
+ "croquet",
+ "croqueted",
+ "croqueting",
+ "cross",
+ "crossed",
+ "crossly",
+ "crouched",
+ "crowd",
+ "crowded",
+ "crown",
+ "crumbs",
+ "crust",
+ "cry",
+ "crying",
+ "cucumber",
+ "cunning",
+ "cup",
+ "cupboards",
+ "cur",
+ "curiosity",
+ "curious",
+ "curiouser",
+ "curled",
+ "curls",
+ "curly",
+ "currants",
+ "current",
+ "curtain",
+ "curtsey",
+ "curtseying",
+ "curving",
+ "cushion",
+ "custard",
+ "custody",
+ "cut",
+ "cutting",
+ };
+ }
+ private void RunTest(Action test)
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ AutoCompleteBox control = CreateControl();
+ control.Items = CreateSimpleStringArray();
+ TextBox textBox = GetTextBox(control);
+ Dispatcher.UIThread.RunJobs();
+ test.Invoke(control, textBox);
+ }
+ }
+
+ private static TestServices Services => TestServices.StyledWindow;
+
+ /*private static TestServices Services => TestServices.MockThreadingInterface.With(
+ standardCursorFactory: Mock.Of(),
+ windowingPlatform: new MockWindowingPlatform());*/
+
+ private AutoCompleteBox CreateControl()
+ {
+ var datePicker =
+ new AutoCompleteBox
+ {
+ Template = CreateTemplate()
+ };
+
+ datePicker.ApplyTemplate();
+ return datePicker;
+ }
+ private TextBox GetTextBox(AutoCompleteBox control)
+ {
+ return control.GetTemplateChildren()
+ .OfType()
+ .First();
+ }
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate(control =>
+ {
+ var textBox =
+ new TextBox
+ {
+ Name = "PART_TextBox"
+ };
+ var listbox =
+ new ListBox
+ {
+ Name = "PART_SelectingItemsControl"
+ };
+ var popup =
+ new Popup
+ {
+ Name = "PART_Popup"
+ };
+
+ var panel = new Panel();
+ panel.Children.Add(textBox);
+ panel.Children.Add(popup);
+ panel.Children.Add(listbox);
+
+ return panel;
+ });
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/BorderTests.cs b/tests/Avalonia.Controls.UnitTests/BorderTests.cs
index c0d2a39ab2..9a6a041ec7 100644
--- a/tests/Avalonia.Controls.UnitTests/BorderTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/BorderTests.cs
@@ -13,12 +13,34 @@ namespace Avalonia.Controls.UnitTests
var target = new Border
{
Padding = new Thickness(6),
- BorderThickness = 4,
+ BorderThickness = new Thickness(4)
};
target.Measure(new Size(100, 100));
Assert.Equal(new Size(20, 20), target.DesiredSize);
}
+
+ [Fact]
+ public void Child_Should_Arrange_With_Zero_Height_Width_If_Padding_Greater_Than_Child_Size()
+ {
+ Border content;
+
+ var target = new Border
+ {
+ Padding = new Thickness(6),
+ MaxHeight = 12,
+ MaxWidth = 12,
+ Child = content = new Border
+ {
+ Height = 0,
+ Width = 0
+ }
+ };
+
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.Equal(new Rect(6, 6, 0, 0), content.Bounds);
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
index 2ab02a0418..2c1074aa9a 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
@@ -80,6 +80,31 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), content.Bounds);
}
+ [Fact]
+ public void Should_Correctly_Align_Child_With_Fixed_Size()
+ {
+ Border content;
+ var target = new ContentPresenter
+ {
+ HorizontalContentAlignment = HorizontalAlignment.Stretch,
+ VerticalContentAlignment = VerticalAlignment.Stretch,
+ Content = content = new Border
+ {
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Bottom,
+ Width = 16,
+ Height = 16,
+ },
+ };
+
+ target.UpdateChild();
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ // Check correct result for Issue #1447.
+ Assert.Equal(new Rect(0, 84, 16, 16), content.Bounds);
+ }
+
[Fact]
public void Content_Can_Be_Stretched()
{
@@ -185,5 +210,30 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
}
+
+ [Fact]
+ public void Child_Arrange_With_Zero_Height_When_Padding_Height_Greater_Than_Child_Height()
+ {
+ Border content;
+ var target = new ContentPresenter
+ {
+ Padding = new Thickness(32),
+ MaxHeight = 32,
+ MaxWidth = 32,
+ HorizontalContentAlignment = HorizontalAlignment.Center,
+ VerticalContentAlignment = VerticalAlignment.Center,
+ Content = content = new Border
+ {
+ Height = 0,
+ Width = 0,
+ },
+ };
+
+ target.UpdateChild();
+
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.Equal(new Rect(48, 48, 0, 0), content.Bounds);
+ }
}
}
\ No newline at end of file
diff --git a/tests/Avalonia.RenderTests/Controls/BorderTests.cs b/tests/Avalonia.RenderTests/Controls/BorderTests.cs
index 3bd5a6e1cb..7d2e40c3b4 100644
--- a/tests/Avalonia.RenderTests/Controls/BorderTests.cs
+++ b/tests/Avalonia.RenderTests/Controls/BorderTests.cs
@@ -31,7 +31,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 1,
+ BorderThickness = new Thickness(1),
}
};
@@ -50,7 +50,47 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task Border_Uniform_CornerRadius()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ BorderBrush = Brushes.Black,
+ BorderThickness = new Thickness(2),
+ CornerRadius = new CornerRadius(16),
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
+
+ [Fact]
+ public async Task Border_NonUniform_CornerRadius()
+ {
+ Decorator target = new Decorator
+ {
+ Padding = new Thickness(8),
+ Width = 200,
+ Height = 200,
+ Child = new Border
+ {
+ BorderBrush = Brushes.Black,
+ BorderThickness = new Thickness(2),
+ CornerRadius = new CornerRadius(16, 4, 7, 10),
}
};
@@ -87,7 +127,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new Border
{
Background = Brushes.Red,
@@ -110,7 +150,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Padding = new Thickness(2),
Child = new Border
{
@@ -134,7 +174,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new Border
{
Background = Brushes.Red,
@@ -159,7 +199,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
@@ -186,7 +226,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
@@ -213,7 +253,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
@@ -240,7 +280,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
@@ -267,7 +307,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
@@ -294,7 +334,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
@@ -321,7 +361,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
@@ -348,7 +388,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
Child = new Border
{
BorderBrush = Brushes.Black,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
Child = new TextBlock
{
Text = "Foo",
diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
index 099b022862..cfa15ae304 100644
--- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
+++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs
@@ -42,7 +42,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
new Border
{
BorderBrush = Brushes.Blue,
- BorderThickness = 2,
+ BorderThickness = new Thickness(2),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new TextBlock
diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs
index fab867f428..4703daca25 100644
--- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs
+++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs
@@ -316,7 +316,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
Child = new Border
{
BorderBrush = Brushes.Red,
- BorderThickness = 1,
+ BorderThickness = new Thickness(1),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Child = new Path
diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs
index a7c559668b..5ef559b887 100644
--- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs
+++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs
@@ -151,7 +151,7 @@ namespace Avalonia.Styling.UnitTests
{
Setters = new[]
{
- new Setter(Border.BorderThicknessProperty, 4),
+ new Setter(Border.BorderThicknessProperty, new Thickness(4)),
}
};
@@ -162,9 +162,9 @@ namespace Avalonia.Styling.UnitTests
style.Attach(border, null);
- Assert.Equal(4, border.BorderThickness);
+ Assert.Equal(new Thickness(4), border.BorderThickness);
root.Child = null;
- Assert.Equal(0, border.BorderThickness);
+ Assert.Equal(new Thickness(0), border.BorderThickness);
}
private class Class1 : Control
diff --git a/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs b/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs
new file mode 100644
index 0000000000..bc0bbdc867
--- /dev/null
+++ b/tests/Avalonia.Visuals.UnitTests/CornerRadiusTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Globalization;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests
+{
+ public class CornerRadiusTests
+ {
+ [Fact]
+ public void Parse_Parses_Single_Uniform_Radius()
+ {
+ var result = CornerRadius.Parse("3.4", CultureInfo.InvariantCulture);
+
+ Assert.Equal(new CornerRadius(3.4), result);
+ }
+
+ [Fact]
+ public void Parse_Parses_Top_Bottom()
+ {
+ var result = CornerRadius.Parse("1.1,2.2", CultureInfo.InvariantCulture);
+
+ Assert.Equal(new CornerRadius(1.1, 2.2), result);
+ }
+
+ [Fact]
+ public void Parse_Parses_TopLeft_TopRight_BottomRight_BottomLeft()
+ {
+ var result = CornerRadius.Parse("1.1,2.2,3.3,4.4", CultureInfo.InvariantCulture);
+
+ Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
+ }
+
+ [Fact]
+ public void Parse_Accepts_Spaces()
+ {
+ var result = CornerRadius.Parse("1.1 2.2 3.3 4.4", CultureInfo.InvariantCulture);
+
+ Assert.Equal(new CornerRadius(1.1, 2.2, 3.3, 4.4), result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
index 2ada7bdbba..df4584518e 100644
--- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
@@ -83,6 +83,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Margin = new Thickness(10, 20, 30, 40),
Child = canvas = new Canvas
{
+ ClipToBounds = true,
Background = Brushes.AliceBlue,
}
}
@@ -129,6 +130,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
(border = new Border
{
Background = Brushes.AliceBlue,
+ ClipToBounds = true,
Width = 100,
Height = 100,
[Canvas.LeftProperty] = 50,
@@ -173,6 +175,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
(border = new Border
{
Background = Brushes.AliceBlue,
+ ClipToBounds = true,
Width = 100,
Height = 100,
[Canvas.LeftProperty] = 50,
@@ -254,6 +257,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Margin = new Thickness(24, 26),
Child = target = new Border
{
+ ClipToBounds = true,
Margin = new Thickness(26, 24),
Width = 100,
Height = 100,
@@ -515,6 +519,50 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
}
}
+ [Fact]
+ public void Should_Update_ClipBounds_For_Negative_Margin()
+ {
+ using (TestApplication())
+ {
+ Decorator decorator;
+ Border border;
+ var tree = new TestRoot
+ {
+ Width = 100,
+ Height = 100,
+ Child = decorator = new Decorator
+ {
+ Margin = new Thickness(0, 10, 0, 0),
+ Child = border = new Border
+ {
+ Background = Brushes.Red,
+ ClipToBounds = true,
+ Margin = new Thickness(0, -5, 0, 0),
+ }
+ }
+ };
+
+ var layout = AvaloniaLocator.Current.GetService();
+ layout.ExecuteInitialLayoutPass(tree);
+
+ var scene = new Scene(tree);
+ var sceneBuilder = new SceneBuilder();
+ sceneBuilder.UpdateAll(scene);
+
+ var borderNode = scene.FindNode(border);
+ Assert.Equal(new Rect(0, 5, 100, 95), borderNode.ClipBounds);
+
+ border.Margin = new Thickness(0, -8, 0, 0);
+ layout.ExecuteLayoutPass();
+
+ scene = scene.CloneScene();
+ sceneBuilder.Update(scene, border);
+
+ borderNode = scene.FindNode(border);
+ Assert.Equal(new Rect(0, 2, 100, 98), borderNode.ClipBounds);
+ }
+ }
+
[Fact]
public void Should_Update_Descendent_Tranform_When_Margin_Changed()
{
diff --git a/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs b/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs
index bd694d073a..03bf395d1e 100644
--- a/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/ThicknessTests.cs
@@ -4,7 +4,7 @@
using System.Globalization;
using Xunit;
-namespace Avalonia.Visuals.UnitTests.Media
+namespace Avalonia.Visuals.UnitTests
{
public class ThicknessTests
{
@@ -40,4 +40,4 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(new Thickness(1.2, 3.4, 5, 6), result);
}
}
-}
+}
\ No newline at end of file
diff --git a/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png b/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png
new file mode 100644
index 0000000000..9deb45aaeb
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/Border/Border_NonUniform_CornerRadius.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png b/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png
new file mode 100644
index 0000000000..a4bfa75eb8
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/Border/Border_Uniform_CornerRadius.expected.png differ
diff --git a/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png b/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png
new file mode 100644
index 0000000000..9deb45aaeb
Binary files /dev/null and b/tests/TestFiles/Skia/Controls/Border/Border_NonUniform_CornerRadius.expected.png differ
diff --git a/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png b/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png
new file mode 100644
index 0000000000..a4bfa75eb8
Binary files /dev/null and b/tests/TestFiles/Skia/Controls/Border/Border_Uniform_CornerRadius.expected.png differ