diff --git a/readme.md b/readme.md
index eeee39dabe..2b26cbdd1a 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 beta 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.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs
index b151cabf43..a2048005a4 100644
--- a/samples/ControlCatalog.Desktop/Program.cs
+++ b/samples/ControlCatalog.Desktop/Program.cs
@@ -10,6 +10,7 @@ namespace ControlCatalog
{
internal class Program
{
+ [STAThread]
static void Main(string[] args)
{
// TODO: Make this work with GTK/Skia/Cairo depending on command-line args
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 346535d39d..b45a93455e 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -9,8 +9,10 @@ namespace ControlCatalog.NetCore
{
static class Program
{
+
static void Main(string[] args)
{
+ Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA);
if (args.Contains("--wait-for-attach"))
{
Console.WriteLine("Attach debugger and use 'Set next statement'");
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index a3d7a0cdce..b8a8479a49 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -1,189 +1,17 @@
- netstandard2.0
- False
- false
+ netstandard2.0
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
-
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
-
-
- App.xaml
-
-
- MainView.xaml
-
-
- DecoratedWindow.xaml
-
-
- MainWindow.xaml
-
-
- DialogsPage.xaml
-
-
- BorderPage.xaml
-
-
- ButtonPage.xaml
-
-
- CalendarPage.xaml
-
-
- CanvasPage.xaml
-
-
- CarouselPage.xaml
-
-
- ContextMenuPage.xaml
-
-
- CheckBoxPage.xaml
-
-
- DropDownPage.xaml
-
-
- DatePickerPage.xaml
-
-
- ExpanderPage.xaml
-
-
- ImagePage.xaml
-
-
- LayoutTransformControlPage.xaml
+
+ %(Filename)
-
- MenuPage.xaml
-
-
- ProgressBarPage.xaml
-
-
- RadioButtonPage.xaml
-
-
- SliderPage.xaml
-
-
- TreeViewPage.xaml
-
-
- TextBoxPage.xaml
-
-
- ToolTipPage.xaml
-
-
- ButtonSpinnerPage.xaml
-
-
-
-
-
-
-
-
-
-
-
-
+
Designer
+
+
@@ -200,20 +28,6 @@
-
-
-
-
-
- Designer
-
-
-
-
- MSBuild:Compile
-
-
-
-
+
\ No newline at end of file
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 142d0d42b1..1107d34b3e 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -5,20 +5,23 @@
+
-
+
+
+
@@ -26,4 +29,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/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
new file mode 100644
index 0000000000..af679d2f9a
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
@@ -0,0 +1,19 @@
+
+
+ Drag+Drop
+ Example of Drag+Drop capabilities
+
+
+
+ Drag Me
+
+
+ Drop some text or files here
+
+
+
+
\ No newline at end of file
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
new file mode 100644
index 0000000000..718f21314e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
@@ -0,0 +1,71 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace ControlCatalog.Pages
+{
+ public class DragAndDropPage : UserControl
+ {
+ private TextBlock _DropState;
+ private TextBlock _DragState;
+ private Border _DragMe;
+ private int DragCount = 0;
+
+ public DragAndDropPage()
+ {
+ this.InitializeComponent();
+
+ _DragMe.PointerPressed += DoDrag;
+
+ AddHandler(DragDrop.DropEvent, Drop);
+ AddHandler(DragDrop.DragOverEvent, DragOver);
+ }
+
+ private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
+ {
+ DataObject dragData = new DataObject();
+ dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
+
+ var result = await DragDrop.DoDragDrop(dragData, DragDropEffects.Copy);
+ switch(result)
+ {
+ case DragDropEffects.Copy:
+ _DragState.Text = "The text was copied"; break;
+ case DragDropEffects.Link:
+ _DragState.Text = "The text was linked"; break;
+ case DragDropEffects.None:
+ _DragState.Text = "The drag operation was canceled"; break;
+ }
+ }
+
+ private void DragOver(object sender, DragEventArgs e)
+ {
+ // Only allow Copy or Link as Drop Operations.
+ e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
+
+ // Only allow if the dragged data contains text or filenames.
+ if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames))
+ e.DragEffects = DragDropEffects.None;
+ }
+
+ private void Drop(object sender, DragEventArgs e)
+ {
+ if (e.Data.Contains(DataFormats.Text))
+ _DropState.Text = e.Data.GetText();
+ else if (e.Data.Contains(DataFormats.FileNames))
+ _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ _DropState = this.Find("DropState");
+ _DragState = this.Find("DragState");
+ _DragMe = this.Find("DragMe");
+ }
+ }
+}
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/ControlCatalog/Properties/AssemblyInfo.cs b/samples/ControlCatalog/Properties/AssemblyInfo.cs
deleted file mode 100644
index 30c069d7d8..0000000000
--- a/samples/ControlCatalog/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("ControlCatalog")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("ControlCatalog")]
-[assembly: AssemblyCopyright("Copyright © 2015")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("61bec86c-f307-4295-b5b8-9428610d7d55")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
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.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs
new file mode 100644
index 0000000000..2559e52932
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Globalization;
+using static System.Char;
+
+namespace Avalonia.Utilities
+{
+ public struct StringTokenizer : IDisposable
+ {
+ private const char DefaultSeparatorChar = ',';
+
+ private readonly string _s;
+ private readonly int _length;
+ private readonly char _separator;
+ private readonly string _exceptionMessage;
+ private readonly IFormatProvider _formatProvider;
+ private int _index;
+ private int _tokenIndex;
+ private int _tokenLength;
+
+ public StringTokenizer(string s, IFormatProvider formatProvider, string exceptionMessage = null)
+ : this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage)
+ {
+ _formatProvider = formatProvider;
+ }
+
+ public StringTokenizer(string s, char separator = DefaultSeparatorChar, string exceptionMessage = null)
+ {
+ _s = s ?? throw new ArgumentNullException(nameof(s));
+ _length = s?.Length ?? 0;
+ _separator = separator;
+ _exceptionMessage = exceptionMessage;
+ _formatProvider = CultureInfo.InvariantCulture;
+ _index = 0;
+ _tokenIndex = -1;
+ _tokenLength = 0;
+
+ while (_index < _length && IsWhiteSpace(_s, _index))
+ {
+ _index++;
+ }
+ }
+
+ public string CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength);
+
+ public void Dispose()
+ {
+ if (_index != _length)
+ {
+ throw GetFormatException();
+ }
+ }
+
+ public bool TryReadInt32(out Int32 result, char? separator = null)
+ {
+ var success = TryReadString(out var stringResult, separator);
+ result = success ? int.Parse(stringResult, _formatProvider) : 0;
+ return success;
+ }
+
+ public int ReadInt32(char? separator = null)
+ {
+ if (!TryReadInt32(out var result, separator))
+ {
+ throw GetFormatException();
+ }
+
+ return result;
+ }
+
+ public bool TryReadDouble(out double result, char? separator = null)
+ {
+ var success = TryReadString(out var stringResult, separator);
+ result = success ? double.Parse(stringResult, _formatProvider) : 0;
+ return success;
+ }
+
+ public double ReadDouble(char? separator = null)
+ {
+ if (!TryReadDouble(out var result, separator))
+ {
+ throw GetFormatException();
+ }
+
+ return result;
+ }
+
+ public bool TryReadString(out string result, char? separator = null)
+ {
+ var success = TryReadToken(separator ?? _separator);
+ result = CurrentToken;
+ return success;
+ }
+
+ public string ReadString(char? separator = null)
+ {
+ if (!TryReadString(out var result, separator))
+ {
+ throw GetFormatException();
+ }
+
+ return result;
+ }
+
+ private bool TryReadToken(char separator)
+ {
+ _tokenIndex = -1;
+
+ if (_index >= _length)
+ {
+ return false;
+ }
+
+ var c = _s[_index];
+
+ var index = _index;
+ var length = 0;
+
+ while (_index < _length)
+ {
+ c = _s[_index];
+
+ if (IsWhiteSpace(c) || c == separator)
+ {
+ break;
+ }
+
+ _index++;
+ length++;
+ }
+
+ SkipToNextToken(separator);
+
+ _tokenIndex = index;
+ _tokenLength = length;
+
+ if (_tokenLength < 1)
+ {
+ throw GetFormatException();
+ }
+
+ return true;
+ }
+
+ private void SkipToNextToken(char separator)
+ {
+ if (_index < _length)
+ {
+ var c = _s[_index];
+
+ if (c != separator && !IsWhiteSpace(c))
+ {
+ throw GetFormatException();
+ }
+
+ var length = 0;
+
+ while (_index < _length)
+ {
+ c = _s[_index];
+
+ if (c == separator)
+ {
+ length++;
+ _index++;
+
+ if (length > 1)
+ {
+ throw GetFormatException();
+ }
+ }
+ else
+ {
+ if (!IsWhiteSpace(c))
+ {
+ break;
+ }
+
+ _index++;
+ }
+ }
+
+ if (length > 0 && _index >= _length)
+ {
+ throw GetFormatException();
+ }
+ }
+ }
+
+ private FormatException GetFormatException() =>
+ _exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException();
+
+ private static char GetSeparatorFromFormatProvider(IFormatProvider provider)
+ {
+ var c = DefaultSeparatorChar;
+
+ var formatInfo = NumberFormatInfo.GetInstance(provider);
+ if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0])
+ {
+ c = ';';
+ }
+
+ return c;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs
index 06c1a8b4cc..6fdca557eb 100644
--- a/src/Avalonia.Controls/Application.cs
+++ b/src/Avalonia.Controls/Application.cs
@@ -2,16 +2,17 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Reactive.Concurrency;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
+using Avalonia.Input.Raw;
using Avalonia.Layout;
-using Avalonia.Rendering;
+using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
-using System.Reactive.Concurrency;
namespace Avalonia
{
@@ -234,7 +235,9 @@ namespace Avalonia
.Bind().ToConstant(_styler)
.Bind().ToSingleton()
.Bind().ToConstant(this)
- .Bind().ToConstant(AvaloniaScheduler.Instance);
+ .Bind().ToConstant(AvaloniaScheduler.Instance)
+ .Bind().ToConstant(DragDropDevice.Instance)
+ .Bind().ToTransient();
}
}
}
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
-
+
\ No newline at end of file
diff --git a/src/OSX/Avalonia.MonoMac/Cursor.cs b/src/OSX/Avalonia.MonoMac/Cursor.cs
index 10445e62e2..d9370e527b 100644
--- a/src/OSX/Avalonia.MonoMac/Cursor.cs
+++ b/src/OSX/Avalonia.MonoMac/Cursor.cs
@@ -51,6 +51,10 @@ namespace Avalonia.MonoMac
[StandardCursorType.TopSide] = NSCursor.ResizeUpCursor,
[StandardCursorType.UpArrow] = NSCursor.ResizeUpCursor,
[StandardCursorType.Wait] = NSCursor.ArrowCursor, //TODO
+ [StandardCursorType.DragMove] = NSCursor.DragCopyCursor, // TODO
+ [StandardCursorType.DragCopy] = NSCursor.DragCopyCursor,
+ [StandardCursorType.DragLink] = NSCursor.DragLinkCursor,
+
};
}
diff --git a/src/OSX/Avalonia.MonoMac/DragSource.cs b/src/OSX/Avalonia.MonoMac/DragSource.cs
new file mode 100644
index 0000000000..41a206b580
--- /dev/null
+++ b/src/OSX/Avalonia.MonoMac/DragSource.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Runtime.InteropServices;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Input.Raw;
+using MonoMac;
+using MonoMac.AppKit;
+using MonoMac.CoreGraphics;
+using MonoMac.Foundation;
+using MonoMac.OpenGL;
+
+namespace Avalonia.MonoMac
+{
+ public class DragSource : NSDraggingSource, IPlatformDragSource
+ {
+ private const string NSPasteboardTypeString = "public.utf8-plain-text";
+ private const string NSPasteboardTypeFileUrl = "public.file-url";
+
+ private readonly Subject _result = new Subject();
+ private readonly IInputManager _inputManager;
+ private DragDropEffects _allowedEffects;
+
+ public override bool IgnoreModifierKeysWhileDragging => false;
+
+ public DragSource()
+ {
+ _inputManager = AvaloniaLocator.Current.GetService();
+ }
+
+ private string DataFormatToUTI(string s)
+ {
+ if (s == DataFormats.FileNames)
+ return NSPasteboardTypeFileUrl;
+ if (s == DataFormats.Text)
+ return NSPasteboardTypeString;
+ return s;
+ }
+
+ private NSDraggingItem CreateDraggingItem(string format, object data)
+ {
+ var pasteboardItem = new NSPasteboardItem();
+ NSData nsData;
+ if (data is string s)
+ {
+ if (format == DataFormats.FileNames)
+ s = new Uri(s).AbsoluteUri; // Ensure file uris...
+ nsData = NSData.FromString(s);
+ }
+ else if (data is Stream strm)
+ nsData = NSData.FromStream(strm);
+ else if (data is byte[] bytes)
+ nsData = NSData.FromArray(bytes);
+ else
+ {
+ BinaryFormatter bf = new BinaryFormatter();
+ using (var ms = new MemoryStream())
+ {
+ bf.Serialize(ms, data);
+ ms.Position = 0;
+ nsData = NSData.FromStream(ms);
+ }
+ }
+ pasteboardItem.SetDataForType(nsData, DataFormatToUTI(format));
+
+ NSPasteboardWriting writing = new NSPasteboardWriting(pasteboardItem.Handle);
+
+ return new NSDraggingItem(writing);
+ }
+
+ public IEnumerable CreateDraggingItems(string format, object data)
+ {
+ if (format == DataFormats.FileNames && data is IEnumerable files)
+ {
+ foreach (var file in files)
+ yield return CreateDraggingItem(format, file);
+
+ yield break;
+ }
+
+ yield return CreateDraggingItem(format, data);
+ }
+
+
+ public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ {
+ // We need the TopLevelImpl + a mouse location so we just wait for the next event.
+ var mouseEv = await _inputManager.PreProcess.OfType().FirstAsync();
+ var view = ((mouseEv.Root as TopLevel)?.PlatformImpl as TopLevelImpl)?.View;
+ if (view == null)
+ return DragDropEffects.None;
+
+ // Prepare the source event:
+ var pt = view.TranslateLocalPoint(mouseEv.Position).ToMonoMacPoint();
+ var ev = NSEvent.MouseEvent(NSEventType.LeftMouseDown, pt, 0, 0, 0, null, 0, 0, 0);
+
+ _allowedEffects = allowedEffects;
+ var items = data.GetDataFormats().SelectMany(fmt => CreateDraggingItems(fmt, data.Get(fmt))).ToArray();
+ view.BeginDraggingSession(items ,ev, this);
+
+ return await _result;
+ }
+
+ public override NSDragOperation DraggingSourceOperationMaskForLocal(bool flag)
+ {
+ return DraggingInfo.ConvertDragOperation(_allowedEffects);
+ }
+
+ public override void DraggedImageEndedAtOperation(NSImage image, CGPoint screenPoint, NSDragOperation operation)
+ {
+ _result.OnNext(DraggingInfo.ConvertDragOperation(operation));
+ _result.OnCompleted();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs
new file mode 100644
index 0000000000..fc5f52713e
--- /dev/null
+++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Input;
+using MonoMac.AppKit;
+using MonoMac.Foundation;
+
+namespace Avalonia.MonoMac
+{
+ class DraggingInfo : IDataObject
+ {
+ private readonly NSDraggingInfo _info;
+
+ public DraggingInfo(NSDraggingInfo info)
+ {
+ _info = info;
+ }
+
+ internal static NSDragOperation ConvertDragOperation(DragDropEffects d)
+ {
+ NSDragOperation result = NSDragOperation.None;
+ if (d.HasFlag(DragDropEffects.Copy))
+ result |= NSDragOperation.Copy;
+ if (d.HasFlag(DragDropEffects.Link))
+ result |= NSDragOperation.Link;
+ if (d.HasFlag(DragDropEffects.Move))
+ result |= NSDragOperation.Move;
+ return result;
+ }
+
+ internal static DragDropEffects ConvertDragOperation(NSDragOperation d)
+ {
+ DragDropEffects result = DragDropEffects.None;
+ if (d.HasFlag(NSDragOperation.Copy))
+ result |= DragDropEffects.Copy;
+ if (d.HasFlag(NSDragOperation.Link))
+ result |= DragDropEffects.Link;
+ if (d.HasFlag(NSDragOperation.Move))
+ result |= DragDropEffects.Move;
+ return result;
+ }
+
+ public Point Location => new Point(_info.DraggingLocation.X, _info.DraggingLocation.Y);
+
+ public IEnumerable GetDataFormats()
+ {
+ return _info.DraggingPasteboard.Types.Select(NSTypeToWellknownType);
+ }
+
+ private string NSTypeToWellknownType(string type)
+ {
+ if (type == NSPasteboard.NSStringType)
+ return DataFormats.Text;
+ if (type == NSPasteboard.NSFilenamesType)
+ return DataFormats.FileNames;
+ return type;
+ }
+
+ public string GetText()
+ {
+ return _info.DraggingPasteboard.GetStringForType(NSPasteboard.NSStringType);
+ }
+
+ public IEnumerable GetFileNames()
+ {
+ using(var fileNames = (NSArray)_info.DraggingPasteboard.GetPropertyListForType(NSPasteboard.NSFilenamesType))
+ {
+ if (fileNames != null)
+ return NSArray.StringArrayFromHandle(fileNames.Handle);
+ }
+
+ return Enumerable.Empty();
+ }
+
+ public bool Contains(string dataFormat)
+ {
+ return GetDataFormats().Any(f => f == dataFormat);
+ }
+
+ public object Get(string dataFormat)
+ {
+ if (dataFormat == DataFormats.Text)
+ return GetText();
+ if (dataFormat == DataFormats.FileNames)
+ return GetFileNames();
+
+ return _info.DraggingPasteboard.GetDataForType(dataFormat).ToArray();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
index 5907459459..ba45ad8403 100644
--- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
+++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs
@@ -35,7 +35,8 @@ namespace Avalonia.MonoMac
.Bind().ToSingleton()
.Bind().ToSingleton()
.Bind().ToConstant(s_renderLoop)
- .Bind().ToConstant(PlatformThreadingInterface.Instance);
+ .Bind().ToConstant(PlatformThreadingInterface.Instance)
+ /*.Bind().ToTransient()*/;
}
public static void Initialize()
diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
index a655bc1ec5..db7f29f05b 100644
--- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
+++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs
@@ -18,6 +18,7 @@ namespace Avalonia.MonoMac
{
public TopLevelView View { get; }
private readonly IMouseDevice _mouse = AvaloniaLocator.Current.GetService();
+ private readonly IDragDropDevice _dragDevice = AvaloniaLocator.Current.GetService();
protected TopLevelImpl()
{
View = new TopLevelView(this);
@@ -53,6 +54,10 @@ namespace Avalonia.MonoMac
_tl = tl;
_mouse = AvaloniaLocator.Current.GetService();
_keyboard = AvaloniaLocator.Current.GetService();
+
+ RegisterForDraggedTypes(new string[] {
+ "public.data" // register for any kind of data.
+ });
}
protected override void Dispose(bool disposing)
@@ -149,6 +154,48 @@ namespace Avalonia.MonoMac
UpdateCursor();
}
+ private NSDragOperation SendRawDragEvent(NSDraggingInfo sender, RawDragEventType type)
+ {
+ Action input = _tl.Input;
+ IDragDropDevice dragDevice = _tl._dragDevice;
+ IInputRoot root = _tl?.InputRoot;
+ if (root == null || dragDevice == null || input == null)
+ return NSDragOperation.None;
+
+ var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask);
+ DraggingInfo info = new DraggingInfo(sender);
+ var pt = TranslateLocalPoint(info.Location);
+ var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp);
+ input(args);
+ return DraggingInfo.ConvertDragOperation(args.Effects);
+ }
+
+ public override NSDragOperation DraggingEntered(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.DragEnter);
+ }
+
+ public override NSDragOperation DraggingUpdated(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.DragOver);
+ }
+
+ public override void DraggingExited(NSDraggingInfo sender)
+ {
+ SendRawDragEvent(sender, RawDragEventType.DragLeave);
+ }
+
+ public override bool PrepareForDragOperation(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.DragOver) != NSDragOperation.None;
+ }
+
+ public override bool PerformDragOperation(NSDraggingInfo sender)
+ {
+ return SendRawDragEvent(sender, RawDragEventType.Drop) != NSDragOperation.None;
+ }
+
+
public override void SetFrameSize(CGSize newSize)
{
lock (SyncRoot)
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/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
index c76a5b5da5..124e33c5a3 100644
--- a/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
+++ b/src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
@@ -103,7 +103,7 @@ namespace Avalonia.Direct2D1
/// Converts a pen to a Direct2D stroke style.
///
/// The pen to convert.
- /// The render target.
+ /// The render target.
/// The Direct2D brush.
public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget)
{
@@ -114,7 +114,7 @@ namespace Avalonia.Direct2D1
/// Converts a pen to a Direct2D stroke style.
///
/// The pen to convert.
- /// The render target.
+ /// The factory associated with this resource.
/// The Direct2D brush.
public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory)
{
@@ -127,13 +127,16 @@ namespace Avalonia.Direct2D1
EndCap = pen.EndLineCap.ToDirect2D(),
DashCap = pen.DashCap.ToDirect2D()
};
- var dashes = new float[0];
+ float[] dashes = null;
if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
{
properties.DashStyle = DashStyle.Custom;
properties.DashOffset = (float)pen.DashStyle.Offset;
- dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray();
+ dashes = pen.DashStyle.Dashes.Select(x => (float)x).ToArray();
}
+
+ dashes = dashes ?? Array.Empty();
+
return new StrokeStyle(factory, properties, dashes);
}
diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs
new file mode 100644
index 0000000000..5e0bbab975
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using Avalonia.Input;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ static class ClipboardFormats
+ {
+ private const int MAX_FORMAT_NAME_LENGTH = 260;
+
+ class ClipboardFormat
+ {
+ public short Format { get; private set; }
+ public string Name { get; private set; }
+ public short[] Synthesized { get; private set; }
+
+ public ClipboardFormat(string name, short format, params short[] synthesized)
+ {
+ Format = format;
+ Name = name;
+ Synthesized = synthesized;
+ }
+ }
+
+ private static readonly List FormatList = new List()
+ {
+ new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (short)UnmanagedMethods.ClipboardFormat.CF_TEXT),
+ new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP),
+ };
+
+
+ private static string QueryFormatName(short format)
+ {
+ StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH);
+ if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0)
+ return sb.ToString();
+ return null;
+ }
+
+ public static string GetFormat(short format)
+ {
+ lock (FormatList)
+ {
+ var pd = FormatList.FirstOrDefault(f => f.Format == format || Array.IndexOf(f.Synthesized, format) >= 0);
+ if (pd == null)
+ {
+ string name = QueryFormatName(format);
+ if (string.IsNullOrEmpty(name))
+ name = string.Format("Unknown_Format_{0}", format);
+ pd = new ClipboardFormat(name, format);
+ FormatList.Add(pd);
+ }
+ return pd.Name;
+ }
+ }
+
+ public static short GetFormat(string format)
+ {
+ lock (FormatList)
+ {
+ var pd = FormatList.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Name, format));
+ if (pd == null)
+ {
+ int id = UnmanagedMethods.RegisterClipboardFormat(format);
+ if (id == 0)
+ throw new Win32Exception();
+ pd = new ClipboardFormat(format, (short)id);
+ FormatList.Add(pd);
+ }
+ return pd.Format;
+ }
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs
index 0d529d6b91..fa2fbe4810 100644
--- a/src/Windows/Avalonia.Win32/CursorFactory.cs
+++ b/src/Windows/Avalonia.Win32/CursorFactory.cs
@@ -9,6 +9,7 @@ using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Platform;
+using System.Runtime.InteropServices;
namespace Avalonia.Win32
{
@@ -20,6 +21,27 @@ namespace Avalonia.Win32
{
}
+ static CursorFactory()
+ {
+ LoadModuleCursor(StandardCursorType.DragMove, "ole32.dll", 2);
+ LoadModuleCursor(StandardCursorType.DragCopy, "ole32.dll", 3);
+ LoadModuleCursor(StandardCursorType.DragLink, "ole32.dll", 4);
+ }
+
+ private static void LoadModuleCursor(StandardCursorType cursorType, string module, int id)
+ {
+ IntPtr mh = UnmanagedMethods.GetModuleHandle(module);
+ if (mh != IntPtr.Zero)
+ {
+ IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id));
+ if (cursor != IntPtr.Zero)
+ {
+ PlatformHandle phCursor = new PlatformHandle(cursor, PlatformConstants.CursorHandleType);
+ Cache.Add(cursorType, phCursor);
+ }
+ }
+ }
+
private static readonly Dictionary CursorTypeMapping = new Dictionary
{
@@ -47,6 +69,11 @@ namespace Avalonia.Win32
//Using SizeNorthEastSouthWest
{StandardCursorType.TopRightCorner, 32643},
{StandardCursorType.BottomLeftCorner, 32643},
+
+ // Fallback, should have been loaded from ole32.dll
+ {StandardCursorType.DragMove, 32516},
+ {StandardCursorType.DragCopy, 32516},
+ {StandardCursorType.DragLink, 32516},
};
private static readonly Dictionary Cache =
diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs
new file mode 100644
index 0000000000..34867765e5
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/DataObject.cs
@@ -0,0 +1,361 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+using Avalonia.Input;
+using Avalonia.Win32.Interop;
+using IDataObject = Avalonia.Input.IDataObject;
+using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+
+namespace Avalonia.Win32
+{
+ class DataObject : IDataObject, IOleDataObject
+ {
+ // Compatibility with WinForms + WPF...
+ internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray();
+
+ class FormatEnumerator : IEnumFORMATETC
+ {
+ private FORMATETC[] _formats;
+ private int _current;
+
+ private FormatEnumerator(FORMATETC[] formats, int current)
+ {
+ _formats = formats;
+ _current = current;
+ }
+
+ public FormatEnumerator(IDataObject dataobj)
+ {
+ _formats = dataobj.GetDataFormats().Select(ConvertToFormatEtc).ToArray();
+ _current = 0;
+ }
+
+ private FORMATETC ConvertToFormatEtc(string aFormatName)
+ {
+ FORMATETC result = default(FORMATETC);
+ result.cfFormat = ClipboardFormats.GetFormat(aFormatName);
+ result.dwAspect = DVASPECT.DVASPECT_CONTENT;
+ result.ptd = IntPtr.Zero;
+ result.lindex = -1;
+ result.tymed = TYMED.TYMED_HGLOBAL;
+ return result;
+ }
+
+ public void Clone(out IEnumFORMATETC newEnum)
+ {
+ newEnum = new FormatEnumerator(_formats, _current);
+ }
+
+ public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched)
+ {
+ if (rgelt == null)
+ return unchecked((int)UnmanagedMethods.HRESULT.E_INVALIDARG);
+
+ int i = 0;
+ while (i < celt && _current < _formats.Length)
+ {
+ rgelt[i] = _formats[_current];
+ _current++;
+ i++;
+ }
+ if (pceltFetched != null)
+ pceltFetched[0] = i;
+
+ if (i != celt)
+ return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+
+ public int Reset()
+ {
+ _current = 0;
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+
+ public int Skip(int celt)
+ {
+ _current += Math.Min(celt, int.MaxValue - _current);
+ if (_current >= _formats.Length)
+ return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ }
+
+ private const int DV_E_TYMED = unchecked((int)0x80040069);
+ private const int DV_E_DVASPECT = unchecked((int)0x8004006B);
+ private const int DV_E_FORMATETC = unchecked((int)0x80040064);
+ private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003);
+ private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070);
+ private const int GMEM_ZEROINIT = 0x0040;
+ private const int GMEM_MOVEABLE = 0x0002;
+
+
+ IDataObject _wrapped;
+
+ public DataObject(IDataObject wrapped)
+ {
+ _wrapped = wrapped;
+ }
+
+ #region IDataObject
+ bool IDataObject.Contains(string dataFormat)
+ {
+ return _wrapped.Contains(dataFormat);
+ }
+
+ IEnumerable IDataObject.GetDataFormats()
+ {
+ return _wrapped.GetDataFormats();
+ }
+
+ IEnumerable IDataObject.GetFileNames()
+ {
+ return _wrapped.GetFileNames();
+ }
+
+ string IDataObject.GetText()
+ {
+ return _wrapped.GetText();
+ }
+
+ object IDataObject.Get(string dataFormat)
+ {
+ return _wrapped.Get(dataFormat);
+ }
+ #endregion
+
+ #region IOleDataObject
+
+ int IOleDataObject.DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.DAdvise(ref pFormatetc, advf, adviseSink, out connection);
+ connection = 0;
+ return OLE_E_ADVISENOTSUPPORTED;
+ }
+
+ void IOleDataObject.DUnadvise(int connection)
+ {
+ if (_wrapped is IOleDataObject ole)
+ ole.DUnadvise(connection);
+ Marshal.ThrowExceptionForHR(OLE_E_ADVISENOTSUPPORTED);
+ }
+
+ int IOleDataObject.EnumDAdvise(out IEnumSTATDATA enumAdvise)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.EnumDAdvise(out enumAdvise);
+
+ enumAdvise = null;
+ return OLE_E_ADVISENOTSUPPORTED;
+ }
+
+ IEnumFORMATETC IOleDataObject.EnumFormatEtc(DATADIR direction)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.EnumFormatEtc(direction);
+ if (direction == DATADIR.DATADIR_GET)
+ return new FormatEnumerator(_wrapped);
+ throw new NotSupportedException();
+ }
+
+ int IOleDataObject.GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.GetCanonicalFormatEtc(ref formatIn, out formatOut);
+
+ formatOut = new FORMATETC();
+ formatOut.ptd = IntPtr.Zero;
+ return unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL);
+ }
+
+ void IOleDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium)
+ {
+ if (_wrapped is IOleDataObject ole)
+ {
+ ole.GetData(ref format, out medium);
+ return;
+ }
+ if(!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL))
+ Marshal.ThrowExceptionForHR(DV_E_TYMED);
+
+ if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
+ Marshal.ThrowExceptionForHR(DV_E_DVASPECT);
+
+ string fmt = ClipboardFormats.GetFormat(format.cfFormat);
+ if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt))
+ Marshal.ThrowExceptionForHR(DV_E_FORMATETC);
+
+ medium = default(STGMEDIUM);
+ medium.tymed = TYMED.TYMED_HGLOBAL;
+ int result = WriteDataToHGlobal(fmt, ref medium.unionmember);
+ Marshal.ThrowExceptionForHR(result);
+ }
+
+ void IOleDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium)
+ {
+ if (_wrapped is IOleDataObject ole)
+ {
+ ole.GetDataHere(ref format, ref medium);
+ return;
+ }
+
+ if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlag(TYMED.TYMED_HGLOBAL))
+ Marshal.ThrowExceptionForHR(DV_E_TYMED);
+
+ if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
+ Marshal.ThrowExceptionForHR(DV_E_DVASPECT);
+
+ string fmt = ClipboardFormats.GetFormat(format.cfFormat);
+ if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt))
+ Marshal.ThrowExceptionForHR(DV_E_FORMATETC);
+
+ if (medium.unionmember == IntPtr.Zero)
+ Marshal.ThrowExceptionForHR(STG_E_MEDIUMFULL);
+
+ int result = WriteDataToHGlobal(fmt, ref medium.unionmember);
+ Marshal.ThrowExceptionForHR(result);
+ }
+
+ int IOleDataObject.QueryGetData(ref FORMATETC format)
+ {
+ if (_wrapped is IOleDataObject ole)
+ return ole.QueryGetData(ref format);
+ if (format.dwAspect != DVASPECT.DVASPECT_CONTENT)
+ return DV_E_DVASPECT;
+ if (!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL))
+ return DV_E_TYMED;
+
+ string dataFormat = ClipboardFormats.GetFormat(format.cfFormat);
+ if (!string.IsNullOrEmpty(dataFormat) && _wrapped.Contains(dataFormat))
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ return DV_E_FORMATETC;
+ }
+
+ void IOleDataObject.SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release)
+ {
+ if (_wrapped is IOleDataObject ole)
+ {
+ ole.SetData(ref formatIn, ref medium, release);
+ return;
+ }
+ Marshal.ThrowExceptionForHR(unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL));
+ }
+
+ private int WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal)
+ {
+ object data = _wrapped.Get(dataFormat);
+ if (dataFormat == DataFormats.Text || data is string)
+ return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data));
+ if (dataFormat == DataFormats.FileNames && data is IEnumerable files)
+ return WriteFileListToHGlobal(ref hGlobal, files);
+ if (data is Stream stream)
+ {
+ byte[] buffer = new byte[stream.Length - stream.Position];
+ stream.Read(buffer, 0, buffer.Length);
+ return WriteBytesToHGlobal(ref hGlobal, buffer);
+ }
+ if (data is IEnumerable bytes)
+ {
+ var byteArr = bytes is byte[] ? (byte[])bytes : bytes.ToArray();
+ return WriteBytesToHGlobal(ref hGlobal, byteArr);
+ }
+ return WriteBytesToHGlobal(ref hGlobal, SerializeObject(data));
+ }
+
+ private byte[] SerializeObject(object data)
+ {
+ using (var ms = new MemoryStream())
+ {
+ ms.Write(SerializedObjectGUID, 0, SerializedObjectGUID.Length);
+ BinaryFormatter binaryFormatter = new BinaryFormatter();
+ binaryFormatter.Serialize(ms, data);
+ return ms.ToArray();
+ }
+ }
+
+ private int WriteBytesToHGlobal(ref IntPtr hGlobal, byte[] data)
+ {
+ int required = data.Length;
+ if (hGlobal == IntPtr.Zero)
+ hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required);
+
+ long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ if (required > available)
+ return STG_E_MEDIUMFULL;
+
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ Marshal.Copy(data, 0, ptr, data.Length);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files)
+ {
+ if (!files?.Any() ?? false)
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+
+ char[] filesStr = (string.Join("\0", files) + "\0\0").ToCharArray();
+ _DROPFILES df = new _DROPFILES();
+ df.pFiles = Marshal.SizeOf<_DROPFILES>();
+ df.fWide = true;
+
+ int required = (filesStr.Length * sizeof(char)) + Marshal.SizeOf<_DROPFILES>();
+ if (hGlobal == IntPtr.Zero)
+ hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required);
+
+ long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ if (required > available)
+ return STG_E_MEDIUMFULL;
+
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ Marshal.StructureToPtr(df, ptr, false);
+
+ Marshal.Copy(filesStr, 0, ptr + Marshal.SizeOf<_DROPFILES>(), filesStr.Length);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private int WriteStringToHGlobal(ref IntPtr hGlobal, string data)
+ {
+ int required = (data.Length + 1) * sizeof(char);
+ if (hGlobal == IntPtr.Zero)
+ hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, required);
+
+ long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ if (required > available)
+ return STG_E_MEDIUMFULL;
+
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ char[] chars = (data + '\0').ToCharArray();
+ Marshal.Copy(chars, 0, ptr, chars.Length);
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs
new file mode 100644
index 0000000000..ea124e5f29
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/DragSource.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Threading;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ class DragSource : IPlatformDragSource
+ {
+ public Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+
+ OleDragSource src = new OleDragSource();
+ DataObject dataObject = new DataObject(data);
+ int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects);
+
+ int[] finalEffect = new int[1];
+ UnmanagedMethods.DoDragDrop(dataObject, src, allowed, finalEffect);
+
+ return Task.FromResult(OleDropTarget.ConvertDropEffect((DropEffect)finalEffect[0]));}
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index f13dd3272c..aa86ab0f8d 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -5,6 +5,7 @@ using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
using System.Text;
// ReSharper disable InconsistentNaming
@@ -951,6 +952,32 @@ namespace Avalonia.Win32.Interop
[DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)]
public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count);
+ [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
+ public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target);
+
+ [DllImport("ole32.dll", EntryPoint = "OleInitialize")]
+ public static extern HRESULT OleInitialize(IntPtr val);
+
+ [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
+ internal static extern void ReleaseStgMedium(ref STGMEDIUM medium);
+
+ [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)]
+ public static extern int GetClipboardFormatName(int format, StringBuilder lpString, int cchMax);
+
+ [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)]
+ public static extern int RegisterClipboardFormat(string format);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true, SetLastError = true)]
+ public static extern IntPtr GlobalSize(IntPtr hGlobal);
+
+ [DllImport("shell32.dll", BestFitMapping = false, CharSet = CharSet.Auto)]
+ public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch);
+
+ [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)]
+ public static extern void DoDragDrop(IDataObject dataObject, IDropSource dropSource, int allowedEffects, int[] finalEffect);
+
+
+
public enum MONITOR
{
MONITOR_DEFAULTTONULL = 0x00000000,
@@ -990,10 +1017,28 @@ namespace Avalonia.Win32.Interop
MDT_DEFAULT = MDT_EFFECTIVE_DPI
}
- public enum ClipboardFormat
+ public enum ClipboardFormat
{
+ ///
+ /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text.
+ ///
CF_TEXT = 1,
- CF_UNICODETEXT = 13
+ ///
+ /// A handle to a bitmap
+ ///
+ CF_BITMAP = 2,
+ ///
+ /// A memory object containing a BITMAPINFO structure followed by the bitmap bits.
+ ///
+ CF_DIB = 3,
+ ///
+ /// Unicode text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data.
+ ///
+ CF_UNICODETEXT = 13,
+ ///
+ /// A handle to type HDROP that identifies a list of files.
+ ///
+ CF_HDROP = 15,
}
public struct MSG
@@ -1136,7 +1181,9 @@ namespace Avalonia.Win32.Interop
S_FALSE = 0x0001,
S_OK = 0x0000,
E_INVALIDARG = 0x80070057,
- E_OUTOFMEMORY = 0x8007000E
+ E_OUTOFMEMORY = 0x8007000E,
+ E_NOTIMPL = 0x80004001,
+ E_UNEXPECTED = 0x8000FFFF,
}
public enum Icons
@@ -1300,4 +1347,53 @@ namespace Avalonia.Win32.Interop
uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
}
+
+ [Flags]
+ internal enum DropEffect : int
+ {
+ None = 0,
+ Copy = 1,
+ Move = 2,
+ Link = 4,
+ Scroll = -2147483648,
+ }
+
+
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("00000122-0000-0000-C000-000000000046")]
+ internal interface IDropTarget
+ {
+ [PreserveSig]
+ UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect);
+ [PreserveSig]
+ UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect);
+ [PreserveSig]
+ UnmanagedMethods.HRESULT DragLeave();
+ [PreserveSig]
+ UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect);
+ }
+
+ [ComImport]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("00000121-0000-0000-C000-000000000046")]
+ internal interface IDropSource
+ {
+ [PreserveSig]
+ int QueryContinueDrag(int fEscapePressed, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState);
+ [PreserveSig]
+ int GiveFeedback([MarshalAs(UnmanagedType.U4)] [In] int dwEffect);
+ }
+
+
+ [StructLayoutAttribute(LayoutKind.Sequential)]
+ internal struct _DROPFILES
+ {
+ public Int32 pFiles;
+ public Int32 X;
+ public Int32 Y;
+ public bool fNC;
+ public bool fWide;
+ }
}
diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs
new file mode 100644
index 0000000000..085c0f8ea9
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleContext.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Threading;
+using Avalonia.Platform;
+using Avalonia.Threading;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ class OleContext
+ {
+ private static OleContext fCurrent;
+
+ internal static OleContext Current
+ {
+ get
+ {
+ if (!IsValidOleThread())
+ return null;
+
+ if (fCurrent == null)
+ fCurrent = new OleContext();
+ return fCurrent;
+ }
+ }
+
+
+ private OleContext()
+ {
+ if (UnmanagedMethods.OleInitialize(IntPtr.Zero) != UnmanagedMethods.HRESULT.S_OK)
+ throw new SystemException("Failed to initialize OLE");
+ }
+
+ private static bool IsValidOleThread()
+ {
+ return Dispatcher.UIThread.CheckAccess() &&
+ Thread.CurrentThread.GetApartmentState() == ApartmentState.STA;
+ }
+
+ internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target)
+ {
+ if (hwnd?.HandleDescriptor != "HWND" || target == null)
+ return false;
+
+ return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK;
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs
new file mode 100644
index 0000000000..85d1daadeb
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleDataObject.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Text;
+using Avalonia.Input;
+using Avalonia.Win32.Interop;
+using IDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
+
+namespace Avalonia.Win32
+{
+ class OleDataObject : Avalonia.Input.IDataObject
+ {
+ private IDataObject _wrapped;
+
+ public OleDataObject(IDataObject wrapped)
+ {
+ _wrapped = wrapped;
+ }
+
+ public bool Contains(string dataFormat)
+ {
+ return GetDataFormatsCore().Any(df => StringComparer.OrdinalIgnoreCase.Equals(df, dataFormat));
+ }
+
+ public IEnumerable GetDataFormats()
+ {
+ return GetDataFormatsCore().Distinct();
+ }
+
+ public string GetText()
+ {
+ return GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT) as string;
+ }
+
+ public IEnumerable GetFileNames()
+ {
+ return GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT) as IEnumerable;
+ }
+
+ public object Get(string dataFormat)
+ {
+ return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT);
+ }
+
+ private object GetDataFromOleHGLOBAL(string format, DVASPECT aspect)
+ {
+ FORMATETC formatEtc = new FORMATETC();
+ formatEtc.cfFormat = ClipboardFormats.GetFormat(format);
+ formatEtc.dwAspect = aspect;
+ formatEtc.lindex = -1;
+ formatEtc.tymed = TYMED.TYMED_HGLOBAL;
+ if (_wrapped.QueryGetData(ref formatEtc) == 0)
+ {
+ _wrapped.GetData(ref formatEtc, out STGMEDIUM medium);
+ try
+ {
+ if (medium.unionmember != IntPtr.Zero && medium.tymed == TYMED.TYMED_HGLOBAL)
+ {
+ if (format == DataFormats.Text)
+ return ReadStringFromHGlobal(medium.unionmember);
+ if (format == DataFormats.FileNames)
+ return ReadFileNamesFromHGlobal(medium.unionmember);
+
+ byte[] data = ReadBytesFromHGlobal(medium.unionmember);
+
+ if (IsSerializedObject(data))
+ {
+ using (var ms = new MemoryStream(data))
+ {
+ ms.Position = DataObject.SerializedObjectGUID.Length;
+ BinaryFormatter binaryFormatter = new BinaryFormatter();
+ return binaryFormatter.Deserialize(ms);
+ }
+ }
+ return data;
+ }
+ }
+ finally
+ {
+ UnmanagedMethods.ReleaseStgMedium(ref medium);
+ }
+ }
+ return null;
+ }
+
+ private bool IsSerializedObject(byte[] data)
+ {
+ if (data.Length < DataObject.SerializedObjectGUID.Length)
+ return false;
+ for (int i = 0; i < DataObject.SerializedObjectGUID.Length; i++)
+ if (data[i] != DataObject.SerializedObjectGUID[i])
+ return false;
+ return true;
+ }
+
+ private static IEnumerable ReadFileNamesFromHGlobal(IntPtr hGlobal)
+ {
+ List files = new List();
+ int fileCount = UnmanagedMethods.DragQueryFile(hGlobal, -1, null, 0);
+ if (fileCount > 0)
+ {
+ for (int i = 0; i < fileCount; i++)
+ {
+ int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0);
+ StringBuilder sb = new StringBuilder(pathLen+1);
+
+ if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen)
+ {
+ files.Add(sb.ToString());
+ }
+ }
+ }
+ return files;
+ }
+
+ private static string ReadStringFromHGlobal(IntPtr hGlobal)
+ {
+ IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ return Marshal.PtrToStringAuto(ptr);
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private static byte[] ReadBytesFromHGlobal(IntPtr hGlobal)
+ {
+ IntPtr source = UnmanagedMethods.GlobalLock(hGlobal);
+ try
+ {
+ int size = (int)UnmanagedMethods.GlobalSize(hGlobal).ToInt64();
+ byte[] data = new byte[size];
+ Marshal.Copy(source, data, 0, size);
+ return data;
+ }
+ finally
+ {
+ UnmanagedMethods.GlobalUnlock(hGlobal);
+ }
+ }
+
+ private IEnumerable GetDataFormatsCore()
+ {
+ var enumFormat = _wrapped.EnumFormatEtc(DATADIR.DATADIR_GET);
+ if (enumFormat != null)
+ {
+ enumFormat.Reset();
+ FORMATETC[] formats = new FORMATETC[1];
+ int[] fetched = { 1 };
+ while (fetched[0] > 0)
+ {
+ fetched[0] = 0;
+ if (enumFormat.Next(1, formats, fetched) == 0 && fetched[0] > 0)
+ {
+ if (formats[0].ptd != IntPtr.Zero)
+ Marshal.FreeCoTaskMem(formats[0].ptd);
+
+ yield return ClipboardFormats.GetFormat(formats[0].cfFormat);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs
new file mode 100644
index 0000000000..522014abc0
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleDragSource.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32
+{
+ class OleDragSource : IDropSource
+ {
+ private const int DRAGDROP_S_USEDEFAULTCURSORS = 0x00040102;
+ private const int DRAGDROP_S_DROP = 0x00040100;
+ private const int DRAGDROP_S_CANCEL = 0x00040101;
+
+ private const int KEYSTATE_LEFTMB = 1;
+ private const int KEYSTATE_MIDDLEMB = 16;
+ private const int KEYSTATE_RIGHTMB = 2;
+ private static readonly int[] MOUSE_BUTTONS = new int[] { KEYSTATE_LEFTMB, KEYSTATE_MIDDLEMB, KEYSTATE_RIGHTMB };
+
+ public int QueryContinueDrag(int fEscapePressed, int grfKeyState)
+ {
+ if (fEscapePressed != 0)
+ return DRAGDROP_S_CANCEL;
+
+ int pressedMouseButtons = MOUSE_BUTTONS.Where(mb => (grfKeyState & mb) == mb).Count();
+
+ if (pressedMouseButtons >= 2)
+ return DRAGDROP_S_CANCEL;
+ if (pressedMouseButtons == 0)
+ return DRAGDROP_S_DROP;
+
+ return unchecked((int)UnmanagedMethods.HRESULT.S_OK);
+ }
+
+ public int GiveFeedback(int dwEffect)
+ {
+ return DRAGDROP_S_USEDEFAULTCURSORS;
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs
new file mode 100644
index 0000000000..500c03e317
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs
@@ -0,0 +1,160 @@
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Platform;
+using Avalonia.Win32.Interop;
+using IDataObject = Avalonia.Input.IDataObject;
+using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
+
+namespace Avalonia.Win32
+{
+ class OleDropTarget : IDropTarget
+ {
+ private readonly IInputElement _target;
+ private readonly ITopLevelImpl _tl;
+ private readonly IDragDropDevice _dragDevice;
+
+ private IDataObject _currentDrag = null;
+
+ public OleDropTarget(ITopLevelImpl tl, IInputElement target)
+ {
+ _dragDevice = AvaloniaLocator.Current.GetService();
+ _tl = tl;
+ _target = target;
+ }
+
+ public static DropEffect ConvertDropEffect(DragDropEffects operation)
+ {
+ DropEffect result = DropEffect.None;
+ if (operation.HasFlag(DragDropEffects.Copy))
+ result |= DropEffect.Copy;
+ if (operation.HasFlag(DragDropEffects.Move))
+ result |= DropEffect.Move;
+ if (operation.HasFlag(DragDropEffects.Link))
+ result |= DropEffect.Link;
+ return result;
+ }
+
+ public static DragDropEffects ConvertDropEffect(DropEffect effect)
+ {
+ DragDropEffects result = DragDropEffects.None;
+ if (effect.HasFlag(DropEffect.Copy))
+ result |= DragDropEffects.Copy;
+ if (effect.HasFlag(DropEffect.Move))
+ result |= DragDropEffects.Move;
+ if (effect.HasFlag(DropEffect.Link))
+ result |= DragDropEffects.Link;
+ return result;
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect)
+ {
+ var dispatch = _tl?.Input;
+ if (dispatch == null)
+ {
+ pdwEffect = DropEffect.None;
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+ _currentDrag = pDataObj as IDataObject;
+ if (_currentDrag == null)
+ _currentDrag = new OleDataObject(pDataObj);
+ var args = new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.DragEnter,
+ _target,
+ GetDragLocation(pt),
+ _currentDrag,
+ ConvertDropEffect(pdwEffect)
+ );
+ dispatch(args);
+ pdwEffect = ConvertDropEffect(args.Effects);
+
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.DragOver(int grfKeyState, long pt, ref DropEffect pdwEffect)
+ {
+ var dispatch = _tl?.Input;
+ if (dispatch == null)
+ {
+ pdwEffect = DropEffect.None;
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ var args = new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.DragOver,
+ _target,
+ GetDragLocation(pt),
+ _currentDrag,
+ ConvertDropEffect(pdwEffect)
+ );
+ dispatch(args);
+ pdwEffect = ConvertDropEffect(args.Effects);
+
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.DragLeave()
+ {
+ try
+ {
+ _tl?.Input(new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.DragLeave,
+ _target,
+ default(Point),
+ null,
+ DragDropEffects.None
+ ));
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+ finally
+ {
+ _currentDrag = null;
+ }
+ }
+
+ UnmanagedMethods.HRESULT IDropTarget.Drop(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect)
+ {
+ try
+ {
+ var dispatch = _tl?.Input;
+ if (dispatch == null)
+ {
+ pdwEffect = DropEffect.None;
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+
+ _currentDrag = pDataObj as IDataObject;
+ if (_currentDrag == null)
+ _currentDrag= new OleDataObject(pDataObj);
+
+ var args = new RawDragEvent(
+ _dragDevice,
+ RawDragEventType.Drop,
+ _target,
+ GetDragLocation(pt),
+ _currentDrag,
+ ConvertDropEffect(pdwEffect)
+ );
+ dispatch(args);
+ pdwEffect = ConvertDropEffect(args.Effects);
+
+ return UnmanagedMethods.HRESULT.S_OK;
+ }
+ finally
+ {
+ _currentDrag = null;
+ }
+ }
+
+ private Point GetDragLocation(long dragPoint)
+ {
+ int x = (int)dragPoint;
+ int y = (int)(dragPoint >> 32);
+
+ Point screenPt = new Point(x, y);
+ return _target.PointToClient(screenPt);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 4e1ba618a8..902abaf65b 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -86,6 +86,9 @@ namespace Avalonia.Win32
.Bind().ToConstant(s_instance)
.Bind().ToConstant(s_instance);
+ if (OleContext.Current != null)
+ AvaloniaLocator.CurrentMutable.Bind().ToSingleton();
+
UseDeferredRendering = deferredRendering;
_uiThread = UnmanagedMethods.GetCurrentThreadId();
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index a67362d59f..bb3c4cf6e6 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -34,6 +34,7 @@ namespace Avalonia.Win32
private double _scaling = 1;
private WindowState _showWindowState;
private FramebufferManager _framebuffer;
+ private OleDropTarget _dropTarget;
#if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag;
#endif
@@ -310,6 +311,7 @@ namespace Avalonia.Win32
public void SetInputRoot(IInputRoot inputRoot)
{
_owner = inputRoot;
+ CreateDropTarget();
}
public void SetTitle(string title)
@@ -699,6 +701,13 @@ namespace Avalonia.Win32
}
}
+ private void CreateDropTarget()
+ {
+ OleDropTarget odt = new OleDropTarget(this, _owner);
+ if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false)
+ _dropTarget = odt;
+ }
+
private Point DipFromLParam(IntPtr lParam)
{
return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling;
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.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
index fb432c30d4..ba68838382 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
@@ -8,6 +8,7 @@ using Avalonia.Markup.Xaml.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
+using Avalonia.Media.Immutable;
using Avalonia.Styling;
using Avalonia.UnitTests;
using System.Collections;
@@ -359,8 +360,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
var control = AvaloniaXamlLoader.Parse(xaml);
var bk = control.Background;
- Assert.IsType(bk);
- Assert.Equal(Colors.White, (bk as SolidColorBrush).Color);
+ Assert.IsType(bk);
+ Assert.Equal(Colors.White, (bk as ISolidColorBrush).Color);
}
[Fact]
@@ -496,7 +497,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.NotNull(brush);
- Assert.Equal(Colors.White, ((SolidColorBrush)brush).Color);
+ Assert.Equal(Colors.White, ((ISolidColorBrush)brush).Color);
style.TryGetResource("Double", out var d);
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/Media/BrushTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs
index ae88a94073..a6015c52e5 100644
--- a/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs
@@ -12,7 +12,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Parse_Parses_RGB_Hash_Brush()
{
- var result = (SolidColorBrush)Brush.Parse("#ff8844");
+ var result = (ISolidColorBrush)Brush.Parse("#ff8844");
Assert.Equal(0xff, result.Color.R);
Assert.Equal(0x88, result.Color.G);
@@ -23,7 +23,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Parse_Parses_ARGB_Hash_Brush()
{
- var result = (SolidColorBrush)Brush.Parse("#40ff8844");
+ var result = (ISolidColorBrush)Brush.Parse("#40ff8844");
Assert.Equal(0xff, result.Color.R);
Assert.Equal(0x88, result.Color.G);
@@ -34,7 +34,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Parse_Parses_Named_Brush_Lowercase()
{
- var result = (SolidColorBrush)Brush.Parse("red");
+ var result = (ISolidColorBrush)Brush.Parse("red");
Assert.Equal(0xff, result.Color.R);
Assert.Equal(0x00, result.Color.G);
@@ -45,7 +45,7 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Parse_Parses_Named_Brush_Uppercase()
{
- var result = (SolidColorBrush)Brush.Parse("RED");
+ var result = (ISolidColorBrush)Brush.Parse("RED");
Assert.Equal(0xff, result.Color.R);
Assert.Equal(0x00, result.Color.G);
@@ -53,6 +53,16 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(0xff, result.Color.A);
}
+ [Fact]
+ public void Parse_ToString_Named_Brush_Roundtrip()
+ {
+ const string expectedName = "Red";
+ var brush = (ISolidColorBrush)Brush.Parse(expectedName);
+ var name = brush.ToString();
+
+ Assert.Equal(expectedName, name);
+ }
+
[Fact]
public void Parse_Hex_Value_Doesnt_Accept_Too_Few_Chars()
{
diff --git a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs
index 8ba4f3b739..9f25dcd413 100644
--- a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs
+++ b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs
@@ -1,6 +1,7 @@
// 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 Xunit;
@@ -25,5 +26,12 @@ namespace Avalonia.Visuals.UnitTests
Assert.Equal(new RelativeRect(0.1, 0.2, 0.4, 0.7, RelativeUnit.Relative), result, Compare);
}
+
+ [Fact]
+ public void Parse_Should_Throw_Mixed_Values()
+ {
+ Assert.Throws(() =>
+ RelativeRect.Parse("10%, 20%, 40, 70%", CultureInfo.InvariantCulture));
+ }
}
}
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