Browse Source

Merge branch 'master' into feature/readable-cli-avalonia-build-task

pull/5390/head
Nikita Tsukanov 5 years ago
committed by GitHub
parent
commit
6c91de5843
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .github/ISSUE_TEMPLATE/config.yml
  2. 2
      CONTRIBUTING.md
  3. 2
      global.json
  4. 9
      samples/ControlCatalog/Pages/DataGridPage.xaml
  5. 15
      samples/ControlCatalog/Pages/DialogsPage.xaml
  6. 47
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  7. 6
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  8. 80
      src/Avalonia.Base/Collections/AvaloniaList.cs
  9. 2
      src/Avalonia.Base/Collections/AvaloniaListConverter.cs
  10. 4
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  11. 2
      src/Avalonia.Base/Collections/IAvaloniaList.cs
  12. 2
      src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs
  13. 12
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  14. 2
      src/Avalonia.Base/Collections/Pooled/ClearMode.cs
  15. 2
      src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs
  16. 2
      src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs
  17. 49
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  18. 22
      src/Avalonia.Base/Collections/Pooled/PooledStack.cs
  19. 2
      src/Avalonia.Base/Collections/Pooled/StackDebugView.cs
  20. 66
      src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs
  21. 34
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  22. 3
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  23. 3
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  24. 74
      src/Avalonia.Controls/Border.cs
  25. 6
      src/Avalonia.Controls/Platform/ISystemDialogImpl.cs
  26. 55
      src/Avalonia.Controls/TextBox.cs
  27. 52
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  28. 3
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  29. 3
      src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
  30. 1
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  31. 268
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  32. 269
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  33. 191
      src/Windows/Avalonia.Win32/Win32Com/win32.idl
  34. 4
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  35. 2
      src/tools/MicroComGenerator/ParseException.cs
  36. 152
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  37. 48
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

4
.github/ISSUE_TEMPLATE/config.yml

@ -3,6 +3,6 @@ contact_links:
- name: Questions, Discussions, Ideas
url: https://github.com/AvaloniaUI/Avalonia/discussions/new
about: Please ask and answer questions here.
- name: Avalonia Community Support on Gitter
url: https://gitter.im/AvaloniaUI/Avalonia
- name: Avalonia Community Support on Telegram
url: https://t.me/Avalonia
about: Please ask and answer questions here.

2
CONTRIBUTING.md

@ -2,7 +2,7 @@
## Before You Start
Drop into our [gitter chat room](https://gitter.im/AvaloniaUI/Avalonia) and let us know what you're thinking of doing. We might be able to give you guidance or let you know if someone else is already working on the feature.
Drop into our [telegram group](https://t.me/Avalonia) or [gitter chat room](https://gitter.im/AvaloniaUI/Avalonia) and let us know what you're thinking of doing. We might be able to give you guidance or let you know if someone else is already working on the feature.
## Style

2
global.json

@ -4,7 +4,7 @@
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",
"MSBuild.Sdk.Extras": "2.0.54",
"MSBuild.Sdk.Extras": "3.0.22",
"AggregatePackage.NuGet.Sdk" : "0.1.12"
}
}

9
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -4,6 +4,12 @@
x:Class="ControlCatalog.Pages.DataGridPage">
<UserControl.Resources>
<local:GDPValueConverter x:Key="GDPConverter" />
<DataTemplate x:Key="Demo.DataTemplates.CountryHeader">
<StackPanel Orientation="Horizontal" Spacing="5">
<PathIcon Height="12" Data="M 255 116 A 1 1 0 0 0 254 117 L 254 130 A 1 1 0 0 0 255 131 A 1 1 0 0 0 256 130 L 256 123.87109 C 256.1125 123.90694 256.2187 123.94195 256.33984 123.97852 C 257.18636 124.23404 258.19155 124.5 259 124.5 C 259.80845 124.5 260.52133 124.2168 261.17773 123.9668 C 261.83414 123.7168 262.43408 123.5 263 123.5 C 263.56592 123.5 264.5612 123.73404 265.37109 123.97852 C 266.18098 124.22299 266.82227 124.4668 266.82227 124.4668 A 0.50005 0.50005 0 0 0 267.5 124 L 267.5 118 A 0.50005 0.50005 0 0 0 267.17773 117.5332 C 267.17773 117.5332 266.50667 117.27701 265.66016 117.02148 C 264.81364 116.76596 263.80845 116.5 263 116.5 C 262.19155 116.5 261.47867 116.7832 260.82227 117.0332 C 260.16586 117.2832 259.56592 117.5 259 117.5 C 258.43408 117.5 257.4388 117.26596 256.62891 117.02148 C 256.39123 116.94974 256.17716 116.87994 255.98047 116.81445 A 1 1 0 0 0 255 116 z M 263 117.5 C 263.56592 117.5 264.5612 117.73404 265.37109 117.97852 C 266.00097 118.16865 266.29646 118.28239 266.5 118.35742 L 266.5 120.29297 C 266.25708 120.21012 265.97978 120.11797 265.66016 120.02148 C 264.81364 119.76596 263.80845 119.5 263 119.5 C 262.19155 119.5 261.47867 119.7832 260.82227 120.0332 C 260.16586 120.2832 259.56592 120.5 259 120.5 C 258.43408 120.5 257.4388 120.26596 256.62891 120.02148 C 256.39971 119.9523 256.19148 119.88388 256 119.82031 L 256 117.87109 C 256.1125 117.90694 256.2187 117.94195 256.33984 117.97852 C 257.18636 118.23404 258.19155 118.5 259 118.5 C 259.80845 118.5 260.52133 118.2168 261.17773 117.9668 C 261.83414 117.7168 262.43408 117.5 263 117.5 z M 263 120.5 C 263.56592 120.5 264.5612 120.73404 265.37109 120.97852 C 265.8714 121.12954 266.2398 121.25641 266.5 121.34961 L 266.5 123.30469 C 266.22286 123.20649 266.12863 123.1629 265.66016 123.02148 C 264.81364 122.76596 263.80845 122.5 263 122.5 C 262.19155 122.5 261.47867 122.7832 260.82227 123.0332 C 260.16586 123.2832 259.56592 123.5 259 123.5 C 258.43408 123.5 257.4388 123.26596 256.62891 123.02148 C 256.39971 122.9523 256.19148 122.88388 256 122.82031 L 256 120.87109 C 256.1125 120.90694 256.2187 120.94195 256.33984 120.97852 C 257.18636 121.23404 258.19155 121.5 259 121.5 C 259.80845 121.5 260.52133 121.2168 261.17773 120.9668 C 261.83414 120.7168 262.43408 120.5 263 120.5 z" />
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="DataGridCell.gdp">
@ -28,7 +34,8 @@
DockPanel.Dock="Top"/>
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
<DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" MinWidth="400" />
<!-- Using HeaderTemplate -->
<DataGridTextColumn Header="Country" HeaderTemplate="{StaticResource Demo.DataTemplates.CountryHeader}" Binding="{Binding Name}" Width="6*" MinWidth="400" />
<!-- CompiledBinding example of usage. -->
<DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />

15
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -2,16 +2,31 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DialogsPage">
<StackPanel Orientation="Vertical" Spacing="4" Margin="4">
<TextBlock Classes="h1"
Text="Picker dialogs" />
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Button Name="OpenFile">_Open File</Button>
<Button Name="OpenMultipleFiles">Open _Multiple File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
<TextBlock x:Name="PickerLastResultsVisible"
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsPresenter x:Name="PickerLastResults" />
<TextBlock Margin="0, 8, 0, 0"
Classes="h1"
Text="Window dialogs" />
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
</StackPanel>
</UserControl>

47
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Dialogs;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
@ -16,6 +17,11 @@ namespace ControlCatalog.Pages
{
this.InitializeComponent();
var results = this.FindControl<ItemsPresenter>("PickerLastResults");
var resultsVisible = this.FindControl<TextBlock>("PickerLastResultsVisible");
string lastSelectedDirectory = null;
List<FileDialogFilter> GetFilters()
{
if (this.FindControl<CheckBox>("UseFilters").IsChecked != true)
@ -34,44 +40,67 @@ namespace ControlCatalog.Pages
};
}
this.FindControl<Button>("OpenFile").Click += delegate
this.FindControl<Button>("OpenFile").Click += async delegate
{
new OpenFileDialog()
var result = await new OpenFileDialog()
{
Title = "Open file",
Filters = GetFilters(),
Directory = lastSelectedDirectory,
// Almost guaranteed to exist
InitialFileName = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName
}.ShowAsync(GetWindow());
results.Items = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.FindControl<Button>("OpenMultipleFiles").Click += async delegate
{
var result = await new OpenFileDialog()
{
Title = "Open multiple files",
Filters = GetFilters(),
Directory = lastSelectedDirectory,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.Items = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.FindControl<Button>("SaveFile").Click += delegate
this.FindControl<Button>("SaveFile").Click += async delegate
{
new SaveFileDialog()
var result = await new SaveFileDialog()
{
Title = "Save file",
Filters = GetFilters(),
Directory = lastSelectedDirectory,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
results.Items = new[] { result };
resultsVisible.IsVisible = result != null;
};
this.FindControl<Button>("SelectFolder").Click += delegate
this.FindControl<Button>("SelectFolder").Click += async delegate
{
new OpenFolderDialog()
var result = await new OpenFolderDialog()
{
Title = "Select folder",
Directory = lastSelectedDirectory,
}.ShowAsync(GetWindow());
lastSelectedDirectory = result;
results.Items = new [] { result };
resultsVisible.IsVisible = result != null;
};
this.FindControl<Button>("OpenBoth").Click += async delegate
{
var res = await new OpenFileDialog()
var result = await new OpenFileDialog()
{
Title = "Select both",
Directory = lastSelectedDirectory,
AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{
AllowDirectorySelection = true
});
if (res != null)
Console.WriteLine("Selected: \n" + string.Join("\n", res));
results.Items = result;
resultsVisible.IsVisible = result?.Any() == true;
};
this.FindControl<Button>("DecoratedWindow").Click += delegate
{

6
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@ -6,6 +6,8 @@ using System.ComponentModel;
using System.Linq;
using Avalonia.Data.Core;
#nullable enable
namespace Avalonia.Collections
{
/// <summary>
@ -31,12 +33,12 @@ namespace Avalonia.Collections
/// <summary>
/// Occurs when the collection changes.
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event NotifyCollectionChangedEventHandler? CollectionChanged;
/// <summary>
/// Raised when a property on the collection changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
public int Count => _inner.Count;

80
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -6,6 +6,8 @@ using System.ComponentModel;
using System.Linq;
using Avalonia.Diagnostics;
#nullable enable
namespace Avalonia.Collections
{
/// <summary>
@ -53,7 +55,7 @@ namespace Avalonia.Collections
public class AvaloniaList<T> : IAvaloniaList<T>, IList, INotifyCollectionChangedDebug
{
private readonly List<T> _inner;
private NotifyCollectionChangedEventHandler _collectionChanged;
private NotifyCollectionChangedEventHandler? _collectionChanged;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaList{T}"/> class.
@ -102,7 +104,7 @@ namespace Avalonia.Collections
/// <summary>
/// Raised when a property on the collection changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Gets the number of items in the collection.
@ -118,7 +120,7 @@ namespace Avalonia.Collections
/// Gets or sets a validation routine that can be used to validate items before they are
/// added.
/// </summary>
public Action<T> Validate { get; set; }
public Action<T>? Validate { get; set; }
/// <inheritdoc/>
bool IList.IsFixedSize => false;
@ -133,7 +135,7 @@ namespace Avalonia.Collections
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot => null;
object? ICollection.SyncRoot => null;
/// <inheritdoc/>
bool ICollection<T>.IsReadOnly => false;
@ -178,10 +180,10 @@ namespace Avalonia.Collections
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
object IList.this[int index]
object? IList.this[int index]
{
get { return this[index]; }
set { this[index] = (T)value; }
set { this[index] = (T)value!; }
}
/// <summary>
@ -318,7 +320,7 @@ namespace Avalonia.Collections
/// <param name="items">The items.</param>
public virtual void InsertRange(int index, IEnumerable<T> items)
{
Contract.Requires<ArgumentNullException>(items != null);
_ = items ?? throw new ArgumentNullException(nameof(items));
bool willRaiseCollectionChanged = _collectionChanged != null;
bool hasValidation = Validate != null;
@ -333,7 +335,7 @@ namespace Avalonia.Collections
{
foreach (T item in collection)
{
Validate(item);
Validate!(item);
}
}
@ -354,7 +356,7 @@ namespace Avalonia.Collections
if (hasValidation)
{
Validate(item);
Validate!(item);
}
_inner.Insert(insertIndex++, item);
@ -372,7 +374,7 @@ namespace Avalonia.Collections
if (en.MoveNext())
{
// Avoid allocating list for collection notification if there is no event subscriptions.
List<T> notificationItems = willRaiseCollectionChanged ?
List<T>? notificationItems = willRaiseCollectionChanged ?
new List<T>() :
null;
@ -384,19 +386,17 @@ namespace Avalonia.Collections
if (hasValidation)
{
Validate(item);
Validate!(item);
}
_inner.Insert(insertIndex++, item);
if (willRaiseCollectionChanged)
{
notificationItems.Add(item);
}
notificationItems?.Add(item);
} while (en.MoveNext());
NotifyAdd(notificationItems, index);
if (notificationItems is not null)
NotifyAdd(notificationItems, index);
}
}
}
@ -501,13 +501,26 @@ namespace Avalonia.Collections
/// <param name="items">The items.</param>
public virtual void RemoveAll(IEnumerable<T> items)
{
Contract.Requires<ArgumentNullException>(items != null);
_ = items ?? throw new ArgumentNullException(nameof(items));
foreach (var i in items)
var hItems = new HashSet<T>(items);
int counter = 0;
for (int i = _inner.Count - 1; i >= 0; --i)
{
// TODO: Optimize to only send as many notifications as necessary.
Remove(i);
if (hItems.Contains(_inner[i]))
{
counter += 1;
}
else if(counter > 0)
{
RemoveRange(i + 1, counter);
counter = 0;
}
}
if (counter > 0)
RemoveRange(0, counter);
}
/// <summary>
@ -537,17 +550,17 @@ namespace Avalonia.Collections
}
/// <inheritdoc/>
int IList.Add(object value)
int IList.Add(object? value)
{
int index = Count;
Add((T)value);
Add((T)value!);
return index;
}
/// <inheritdoc/>
bool IList.Contains(object value)
bool IList.Contains(object? value)
{
return Contains((T)value);
return Contains((T)value!);
}
/// <inheritdoc/>
@ -557,21 +570,21 @@ namespace Avalonia.Collections
}
/// <inheritdoc/>
int IList.IndexOf(object value)
int IList.IndexOf(object? value)
{
return IndexOf((T)value);
return IndexOf((T)value!);
}
/// <inheritdoc/>
void IList.Insert(int index, object value)
void IList.Insert(int index, object? value)
{
Insert(index, (T)value);
Insert(index, (T)value!);
}
/// <inheritdoc/>
void IList.Remove(object value)
void IList.Remove(object? value)
{
Remove((T)value);
Remove((T)value!);
}
/// <inheritdoc/>
@ -631,8 +644,7 @@ namespace Avalonia.Collections
// We can't cast array of value type to object[], so we don't support
// widening of primitive types here.
//
object[] objects = array as object[];
if (objects == null)
if (array is not object?[] objects)
{
throw new ArgumentException("Invalid array type");
}
@ -653,7 +665,7 @@ namespace Avalonia.Collections
}
/// <inheritdoc/>
Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList();
Delegate[]? INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList();
/// <summary>
/// Raises the <see cref="CollectionChanged"/> event with an add action.
@ -752,7 +764,7 @@ namespace Avalonia.Collections
public T Current => _innerEnumerator.Current;
object IEnumerator.Current => Current;
object? IEnumerator.Current => Current;
public void Dispose()
{

2
src/Avalonia.Base/Collections/AvaloniaListConverter.cs

@ -3,6 +3,8 @@ using System.ComponentModel;
using System.Globalization;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Collections
{
/// <summary>

4
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@ -5,6 +5,8 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Reactive.Disposables;
#nullable enable
namespace Avalonia.Collections
{
/// <summary>
@ -194,7 +196,7 @@ namespace Avalonia.Collections
tracked.Remove(inpc);
}
},
null);
() => throw new NotSupportedException("Collection reset not supported."));
return Disposable.Create(() =>
{

2
src/Avalonia.Base/Collections/IAvaloniaList.cs

@ -1,5 +1,7 @@
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Collections
{
/// <summary>

2
src/Avalonia.Base/Collections/IAvaloniaReadOnlyList.cs

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
#nullable enable
namespace Avalonia.Collections
{
/// <summary>

12
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -4,6 +4,8 @@ using System.Reactive.Linq;
using Avalonia.Reactive;
using Avalonia.Utilities;
#nullable enable
namespace Avalonia.Collections
{
public static class NotifyCollectionChangedExtensions
@ -16,7 +18,7 @@ namespace Avalonia.Collections
public static IObservable<NotifyCollectionChangedEventArgs> GetWeakCollectionChangedObservable(
this INotifyCollectionChanged collection)
{
Contract.Requires<ArgumentNullException>(collection != null);
_ = collection ?? throw new ArgumentNullException(nameof(collection));
return new WeakCollectionChangedObservable(new WeakReference<INotifyCollectionChanged>(collection));
}
@ -33,8 +35,8 @@ namespace Avalonia.Collections
this INotifyCollectionChanged collection,
NotifyCollectionChangedEventHandler handler)
{
Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null);
_ = collection ?? throw new ArgumentNullException(nameof(collection));
_ = handler ?? throw new ArgumentNullException(nameof(handler));
return collection.GetWeakCollectionChangedObservable()
.Subscribe(e => handler(collection, e));
@ -52,8 +54,8 @@ namespace Avalonia.Collections
this INotifyCollectionChanged collection,
Action<NotifyCollectionChangedEventArgs> handler)
{
Contract.Requires<ArgumentNullException>(collection != null);
Contract.Requires<ArgumentNullException>(handler != null);
_ = collection ?? throw new ArgumentNullException(nameof(collection));
_ = handler ?? throw new ArgumentNullException(nameof(handler));
return collection.GetWeakCollectionChangedObservable().Subscribe(handler);
}

2
src/Avalonia.Base/Collections/Pooled/ClearMode.cs

@ -1,6 +1,8 @@
// This source file is adapted from the Collections.Pooled.
// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/)
#nullable enable
namespace Avalonia.Collections.Pooled
{
/// <summary>

2
src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs

@ -6,6 +6,8 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
#nullable enable
namespace Avalonia.Collections.Pooled
{
internal sealed class ICollectionDebugView<T>

2
src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs

@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Collections.Pooled
{
/// <summary>

49
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@ -8,10 +8,13 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
#nullable enable
namespace Avalonia.Collections.Pooled
{
/// <summary>
@ -38,7 +41,7 @@ namespace Avalonia.Collections.Pooled
[NonSerialized]
private ArrayPool<T> _pool;
[NonSerialized]
private object _syncRoot;
private object? _syncRoot;
private T[] _items; // Do not rename (binary serialization)
private int _size; // Do not rename (binary serialization)
@ -375,7 +378,7 @@ namespace Avalonia.Collections.Pooled
{
if (_syncRoot == null)
{
Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null);
Interlocked.CompareExchange<object?>(ref _syncRoot, new object(), null);
}
return _syncRoot;
}
@ -407,14 +410,14 @@ namespace Avalonia.Collections.Pooled
}
}
private static bool IsCompatibleObject(object value)
private static bool IsCompatibleObject(object? value)
{
// Non-null values are fine. Only accept nulls if T is a class or Nullable<U>.
// Note that default(T) is not equal to null for value types except when T is Nullable<U>.
return ((value is T) || (value == null && default(T) == null));
}
object IList.this[int index]
object? IList.this[int index]
{
get
{
@ -426,7 +429,7 @@ namespace Avalonia.Collections.Pooled
try
{
this[index] = (T)value;
this[index] = (T)value!;
}
catch (InvalidCastException)
{
@ -466,13 +469,13 @@ namespace Avalonia.Collections.Pooled
_items[size] = item;
}
int IList.Add(object item)
int IList.Add(object? item)
{
ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(item, ExceptionArgument.item);
try
{
Add((T)item);
Add((T)item!);
}
catch (InvalidCastException)
{
@ -545,7 +548,7 @@ namespace Avalonia.Collections.Pooled
/// the search value should be inserted into the list in order for the list
/// to remain sorted.
/// </para></remarks>
public int BinarySearch(int index, int count, T item, IComparer<T> comparer)
public int BinarySearch(int index, int count, T item, IComparer<T>? comparer)
{
if (index < 0)
ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
@ -608,11 +611,11 @@ namespace Avalonia.Collections.Pooled
return _size != 0 && IndexOf(item) != -1;
}
bool IList.Contains(object item)
bool IList.Contains(object? item)
{
if (IsCompatibleObject(item))
{
return Contains((T)item);
return Contains((T)item!);
}
return false;
}
@ -693,7 +696,7 @@ namespace Avalonia.Collections.Pooled
public bool Exists(Func<T, bool> match)
=> FindIndex(match) != -1;
public bool TryFind(Func<T, bool> match, out T result)
public bool TryFind(Func<T, bool> match, [MaybeNullWhen(false)] out T result)
{
if (match == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
@ -753,7 +756,7 @@ namespace Avalonia.Collections.Pooled
return -1;
}
public bool TryFindLast(Func<T, bool> match, out T result)
public bool TryFindLast(Func<T, bool> match, [MaybeNullWhen(false)] out T result)
{
if (match is null)
{
@ -886,11 +889,11 @@ namespace Avalonia.Collections.Pooled
public int IndexOf(T item)
=> Array.IndexOf(_items, item, 0, _size);
int IList.IndexOf(object item)
int IList.IndexOf(object? item)
{
if (IsCompatibleObject(item))
{
return IndexOf((T)item);
return IndexOf((T)item!);
}
return -1;
}
@ -947,13 +950,13 @@ namespace Avalonia.Collections.Pooled
_version++;
}
void IList.Insert(int index, object item)
void IList.Insert(int index, object? item)
{
ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(item, ExceptionArgument.item);
try
{
Insert(index, (T)item);
Insert(index, (T)item!);
}
catch (InvalidCastException)
{
@ -1155,11 +1158,11 @@ namespace Avalonia.Collections.Pooled
return false;
}
void IList.Remove(object item)
void IList.Remove(object? item)
{
if (IsCompatibleObject(item))
{
Remove((T)item);
Remove((T)item!);
}
}
@ -1225,7 +1228,7 @@ namespace Avalonia.Collections.Pooled
if (_clearOnFree)
{
// Clear the removed element so that the gc can reclaim the reference.
_items[_size] = default;
_items[_size] = default!;
}
}
@ -1315,7 +1318,7 @@ namespace Avalonia.Collections.Pooled
///
/// This method uses the Array.Sort method to sort the elements.
/// </summary>
public void Sort(int index, int count, IComparer<T> comparer)
public void Sort(int index, int count, IComparer<T>? comparer)
{
if (index < 0)
ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException();
@ -1452,7 +1455,7 @@ namespace Avalonia.Collections.Pooled
private readonly PooledList<T> _list;
private int _index;
private readonly int _version;
private T _current;
private T? _current;
internal Enumerator(PooledList<T> list)
{
@ -1491,9 +1494,9 @@ namespace Avalonia.Collections.Pooled
return false;
}
public T Current => _current;
public T Current => _current!;
object IEnumerator.Current
object? IEnumerator.Current
{
get
{

22
src/Avalonia.Base/Collections/Pooled/PooledStack.cs

@ -20,6 +20,8 @@ using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
#nullable enable
namespace Avalonia.Collections.Pooled
{
/// <summary>
@ -34,7 +36,7 @@ namespace Avalonia.Collections.Pooled
[NonSerialized]
private ArrayPool<T> _pool;
[NonSerialized]
private object _syncRoot;
private object? _syncRoot;
private T[] _array; // Storage for stack elements. Do not rename (binary serialization)
private int _size; // Number of items in the stack. Do not rename (binary serialization)
@ -237,7 +239,7 @@ namespace Avalonia.Collections.Pooled
{
if (_syncRoot == null)
{
Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null);
Interlocked.CompareExchange<object?>(ref _syncRoot, new object(), null);
}
return _syncRoot;
}
@ -456,7 +458,7 @@ namespace Avalonia.Collections.Pooled
return array[size];
}
public bool TryPeek(out T result)
public bool TryPeek([MaybeNullWhen(false)] out T result)
{
int size = _size - 1;
T[] array = _array;
@ -492,12 +494,12 @@ namespace Avalonia.Collections.Pooled
T item = array[size];
if (_clearOnFree)
{
array[size] = default; // Free memory quicker.
array[size] = default!; // Free memory quicker.
}
return item;
}
public bool TryPop(out T result)
public bool TryPop([MaybeNullWhen(false)] out T result)
{
int size = _size - 1;
T[] array = _array;
@ -513,7 +515,7 @@ namespace Avalonia.Collections.Pooled
result = array[size];
if (_clearOnFree)
{
array[size] = default; // Free memory quicker.
array[size] = default!; // Free memory quicker.
}
return true;
}
@ -574,7 +576,7 @@ namespace Avalonia.Collections.Pooled
throw new InvalidOperationException("Stack was empty.");
}
private void ReturnArray(T[] replaceWith = null)
private void ReturnArray(T[]? replaceWith = null)
{
if (_array?.Length > 0)
{
@ -625,7 +627,7 @@ namespace Avalonia.Collections.Pooled
private readonly PooledStack<T> _stack;
private readonly int _version;
private int _index;
private T _currentElement;
private T? _currentElement;
internal Enumerator(PooledStack<T> stack)
{
@ -672,7 +674,7 @@ namespace Avalonia.Collections.Pooled
{
if (_index < 0)
ThrowEnumerationNotStartedOrEnded();
return _currentElement;
return _currentElement!;
}
}
@ -682,7 +684,7 @@ namespace Avalonia.Collections.Pooled
throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended.");
}
object IEnumerator.Current
object? IEnumerator.Current
{
get { return Current; }
}

2
src/Avalonia.Base/Collections/Pooled/StackDebugView.cs

@ -5,6 +5,8 @@
using System;
using System.Diagnostics;
#nullable enable
namespace Avalonia.Collections.Pooled
{
internal sealed class StackDebugView<T>

66
src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs

@ -38,108 +38,128 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
#nullable enable
namespace Avalonia.Collections.Pooled
{
internal static class ThrowHelper
{
[DoesNotReturn]
internal static void ThrowArrayTypeMismatchException()
{
throw new ArrayTypeMismatchException();
}
[DoesNotReturn]
internal static void ThrowIndexOutOfRangeException()
{
throw new IndexOutOfRangeException();
}
[DoesNotReturn]
internal static void ThrowArgumentOutOfRangeException()
{
throw new ArgumentOutOfRangeException();
}
[DoesNotReturn]
internal static void ThrowArgumentException_DestinationTooShort()
{
throw new ArgumentException("Destination too short.");
}
[DoesNotReturn]
internal static void ThrowArgumentException_OverlapAlignmentMismatch()
{
throw new ArgumentException("Overlap alignment mismatch.");
}
[DoesNotReturn]
internal static void ThrowArgumentOutOfRange_IndexException()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.index,
ExceptionResource.ArgumentOutOfRange_Index);
}
[DoesNotReturn]
internal static void ThrowIndexArgumentOutOfRange_NeedNonNegNumException()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.index,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
[DoesNotReturn]
internal static void ThrowValueArgumentOutOfRange_NeedNonNegNumException()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.value,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
[DoesNotReturn]
internal static void ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.length,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
[DoesNotReturn]
internal static void ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.startIndex,
ExceptionResource.ArgumentOutOfRange_Index);
}
[DoesNotReturn]
internal static void ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.count,
ExceptionResource.ArgumentOutOfRange_Count);
}
[DoesNotReturn]
internal static void ThrowWrongKeyTypeArgumentException<T>(T key, Type targetType)
{
// Generic key to move the boxing to the right hand side of throw
throw GetWrongKeyTypeArgumentException((object)key, targetType);
throw GetWrongKeyTypeArgumentException((object?)key, targetType);
}
[DoesNotReturn]
internal static void ThrowWrongValueTypeArgumentException<T>(T value, Type targetType)
{
// Generic key to move the boxing to the right hand side of throw
throw GetWrongValueTypeArgumentException((object)value, targetType);
throw GetWrongValueTypeArgumentException((object?)value, targetType);
}
private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object key)
private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object? key)
{
return new ArgumentException($"Error adding duplicate with key: {key}.");
}
[DoesNotReturn]
internal static void ThrowAddingDuplicateWithKeyArgumentException<T>(T key)
{
// Generic key to move the boxing to the right hand side of throw
throw GetAddingDuplicateWithKeyArgumentException((object)key);
throw GetAddingDuplicateWithKeyArgumentException((object?)key);
}
[DoesNotReturn]
internal static void ThrowKeyNotFoundException<T>(T key)
{
// Generic key to move the boxing to the right hand side of throw
throw GetKeyNotFoundException((object)key);
throw GetKeyNotFoundException((object?)key);
}
[DoesNotReturn]
internal static void ThrowArgumentException(ExceptionResource resource)
{
throw GetArgumentException(resource);
}
[DoesNotReturn]
internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument)
{
throw GetArgumentException(resource, argument);
@ -150,141 +170,169 @@ namespace Avalonia.Collections.Pooled
return new ArgumentNullException(GetArgumentName(argument));
}
[DoesNotReturn]
internal static void ThrowArgumentNullException(ExceptionArgument argument)
{
throw GetArgumentNullException(argument);
}
[DoesNotReturn]
internal static void ThrowArgumentNullException(ExceptionResource resource)
{
throw new ArgumentNullException(GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowArgumentNullException(ExceptionArgument argument, ExceptionResource resource)
{
throw new ArgumentNullException(GetArgumentName(argument), GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument)
{
throw new ArgumentOutOfRangeException(GetArgumentName(argument));
}
[DoesNotReturn]
internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
{
throw GetArgumentOutOfRangeException(argument, resource);
}
[DoesNotReturn]
internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource)
{
throw GetArgumentOutOfRangeException(argument, paramNumber, resource);
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException(ExceptionResource resource)
{
throw GetInvalidOperationException(resource);
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException(ExceptionResource resource, Exception e)
{
throw new InvalidOperationException(GetResourceString(resource), e);
}
[DoesNotReturn]
internal static void ThrowSerializationException(ExceptionResource resource)
{
throw new SerializationException(GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowSecurityException(ExceptionResource resource)
{
throw new System.Security.SecurityException(GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowRankException(ExceptionResource resource)
{
throw new RankException(GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowNotSupportedException(ExceptionResource resource)
{
throw new NotSupportedException(GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowUnauthorizedAccessException(ExceptionResource resource)
{
throw new UnauthorizedAccessException(GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource)
{
throw new ObjectDisposedException(objectName, GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowObjectDisposedException(ExceptionResource resource)
{
throw new ObjectDisposedException(null, GetResourceString(resource));
}
[DoesNotReturn]
internal static void ThrowNotSupportedException()
{
throw new NotSupportedException();
}
[DoesNotReturn]
internal static void ThrowAggregateException(List<Exception> exceptions)
{
throw new AggregateException(exceptions);
}
[DoesNotReturn]
internal static void ThrowOutOfMemoryException()
{
throw new OutOfMemoryException();
}
[DoesNotReturn]
internal static void ThrowArgumentException_Argument_InvalidArrayType()
{
throw new ArgumentException("Invalid array type.");
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted()
{
throw new InvalidOperationException("Enumeration has not started.");
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_InvalidOperation_EnumEnded()
{
throw new InvalidOperationException("Enumeration has ended.");
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_EnumCurrent(int index)
{
throw GetInvalidOperationException_EnumCurrent(index);
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
{
throw new InvalidOperationException("Collection was modified during enumeration.");
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen()
{
throw new InvalidOperationException("Invalid enumerator state: enumeration cannot proceed.");
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_InvalidOperation_NoValue()
{
throw new InvalidOperationException("No value provided.");
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported()
{
throw new InvalidOperationException("Concurrent operations are not supported.");
}
[DoesNotReturn]
internal static void ThrowInvalidOperationException_HandleIsNotInitialized()
{
throw new InvalidOperationException("Handle is not initialized.");
}
[DoesNotReturn]
internal static void ThrowFormatException_BadFormatSpecifier()
{
throw new FormatException("Bad format specifier.");
@ -300,17 +348,17 @@ namespace Avalonia.Collections.Pooled
return new InvalidOperationException(GetResourceString(resource));
}
private static ArgumentException GetWrongKeyTypeArgumentException(object key, Type targetType)
private static ArgumentException GetWrongKeyTypeArgumentException(object? key, Type targetType)
{
return new ArgumentException($"Wrong key type. Expected {targetType}, got: '{key}'.", nameof(key));
}
private static ArgumentException GetWrongValueTypeArgumentException(object value, Type targetType)
private static ArgumentException GetWrongValueTypeArgumentException(object? value, Type targetType)
{
return new ArgumentException($"Wrong value type. Expected {targetType}, got: '{value}'.", nameof(value));
}
private static KeyNotFoundException GetKeyNotFoundException(object key)
private static KeyNotFoundException GetKeyNotFoundException(object? key)
{
return new KeyNotFoundException($"Key not found: {key}");
}
@ -342,7 +390,7 @@ namespace Avalonia.Collections.Pooled
// Aggressively inline so the jit evaluates the if in place and either drops the call altogether
// Or just leaves null test and call to the Non-returning ThrowHelper.ThrowArgumentNullException
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void IfNullAndNullsAreIllegalThenThrow<T>(object value, ExceptionArgument argName)
internal static void IfNullAndNullsAreIllegalThenThrow<T>(object? value, ExceptionArgument argName)
{
// Note that default(T) is not equal to null for value types except when T is Nullable<U>.
if (!(default(T) == null) && value == null)

34
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -12,7 +12,9 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Diagnostics;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls
@ -29,6 +31,7 @@ namespace Avalonia.Controls
private bool _settingWidthInternally;
private int _displayIndexWithFiller;
private object _header;
private IDataTemplate _headerTemplate;
private DataGridColumnHeader _headerCell;
private IControl _editingElement;
private ICellEditBinding _editBinding;
@ -416,6 +419,28 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Gets or sets an <see cref="IDataTemplate"/> for the <see cref="Header"/>
/// </summary>
public IDataTemplate HeaderTemplate
{
get
{
return _headerTemplate;
}
set
{
if (_headerTemplate != value)
{
_headerTemplate = value;
if (_headerCell != null)
{
_headerCell.ContentTemplate = value;
}
}
}
}
public bool IsAutoGenerated
{
get;
@ -834,9 +859,9 @@ namespace Avalonia.Controls
{
if (OwningGrid != null && OwningGrid.UseLayoutRounding)
{
double roundedLeftEdge = Math.Floor(leftEdge + 0.5);
double roundedRightEdge = Math.Floor(leftEdge + ActualWidth + 0.5);
LayoutRoundedWidth = roundedRightEdge - roundedLeftEdge;
var scale = LayoutHelper.GetLayoutScale(HeaderCell);
var roundSize = LayoutHelper.RoundLayoutSize(new Size(leftEdge + ActualWidth, 1), scale, scale);
LayoutRoundedWidth = roundSize.Width - leftEdge;
}
else
{
@ -850,7 +875,8 @@ namespace Avalonia.Controls
var result = new DataGridColumnHeader
{
OwningColumn = this,
Content = _header
Content = _header,
ContentTemplate = _headerTemplate
};
//result.EnsureStyle(null);

3
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -47,7 +47,8 @@
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}"/>
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"
Grid.Column="1"

3
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -193,7 +193,8 @@
Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}" />
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"
Grid.Column="1"

74
src/Avalonia.Controls/Border.cs

@ -1,3 +1,5 @@
using Avalonia.Collections;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Media;
@ -41,7 +43,31 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
/// <summary>
/// Defines the <see cref="BorderDashOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> BorderDashOffsetProperty =
AvaloniaProperty.Register<Border, double>(nameof(BorderDashOffset));
/// <summary>
/// Defines the <see cref="BorderDashArray"/> property.
/// </summary>
public static readonly StyledProperty<AvaloniaList<double>?> BorderDashArrayProperty =
AvaloniaProperty.Register<Border, AvaloniaList<double>?>(nameof(BorderDashArray));
/// <summary>
/// Defines the <see cref="BorderLineCap"/> property.
/// </summary>
public static readonly StyledProperty<PenLineCap> BorderLineCapProperty =
AvaloniaProperty.Register<Border, PenLineCap>(nameof(BorderLineCap), PenLineCap.Flat);
/// <summary>
/// Defines the <see cref="BorderLineJoin"/> property.
/// </summary>
public static readonly StyledProperty<PenLineJoin> BorderLineJoinProperty =
AvaloniaProperty.Register<Border, PenLineJoin>(nameof(BorderLineJoin), PenLineJoin.Miter);
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary>
@ -54,6 +80,10 @@ namespace Avalonia.Controls
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty,
BorderDashArrayProperty,
BorderLineCapProperty,
BorderLineJoinProperty,
BorderDashOffsetProperty,
BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
@ -76,6 +106,15 @@ namespace Avalonia.Controls
set { SetValue(BorderBrushProperty, value); }
}
/// <summary>
/// Gets or sets a collection of <see cref="double"/> values that indicate the pattern of dashes and gaps that is used to outline shapes.
/// </summary>
public AvaloniaList<double>? BorderDashArray
{
get { return GetValue(BorderDashArrayProperty); }
set { SetValue(BorderDashArrayProperty, value); }
}
/// <summary>
/// Gets or sets the thickness of the border.
/// </summary>
@ -85,6 +124,33 @@ namespace Avalonia.Controls
set { SetValue(BorderThicknessProperty, value); }
}
/// <summary>
/// Gets or sets a value that specifies the distance within the dash pattern where a dash begins.
/// </summary>
public double BorderDashOffset
{
get { return GetValue(BorderDashOffsetProperty); }
set { SetValue(BorderDashOffsetProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineCap"/> enumeration value that describes the shape at the ends of a line.
/// </summary>
public PenLineCap BorderLineCap
{
get { return GetValue(BorderLineCapProperty); }
set { SetValue(BorderLineCapProperty, value); }
}
/// <summary>
/// Gets or sets a <see cref="PenLineJoin"/> enumeration value that specifies the type of join that is used at the vertices of a Shape.
/// </summary>
public PenLineJoin BorderLineJoin
{
get { return GetValue(BorderLineJoinProperty); }
set { SetValue(BorderLineJoinProperty, value); }
}
/// <summary>
/// Gets or sets the radius of the border rounded corners.
/// </summary>
@ -93,7 +159,7 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
@ -102,7 +168,7 @@ namespace Avalonia.Controls
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Renders the control.
/// </summary>
@ -110,7 +176,7 @@ namespace Avalonia.Controls
public override void Render(DrawingContext context)
{
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray);
}
/// <summary>

6
src/Avalonia.Controls/Platform/ISystemDialogImpl.cs

@ -1,5 +1,7 @@
using System.Threading.Tasks;
#nullable enable
namespace Avalonia.Controls.Platform
{
/// <summary>
@ -13,8 +15,8 @@ namespace Avalonia.Controls.Platform
/// <param name="dialog">The details of the file dialog to show.</param>
/// <param name="parent">The parent window.</param>
/// <returns>A task returning the selected filenames.</returns>
Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent);
Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent);
Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent);
Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent);
}
}

55
src/Avalonia.Controls/TextBox.cs

@ -642,15 +642,31 @@ namespace Avalonia.Controls
if (!string.IsNullOrEmpty(input))
{
DeleteSelection();
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
if (IsUndoEnabled)
var oldText = _text;
_ignoreTextChanges = true;
try
{
_undoRedoHelper.DiscardRedo();
DeleteSelection(false);
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
}
if (_text != oldText)
{
RaisePropertyChanged(TextProperty, oldText, _text);
}
}
finally
{
_ignoreTextChanges = false;
}
}
}
@ -1285,7 +1301,7 @@ namespace Avalonia.Controls
CaretIndex = SelectionEnd;
}
private bool DeleteSelection()
private bool DeleteSelection(bool raiseTextChanged = true)
{
if (!IsReadOnly)
{
@ -1297,7 +1313,7 @@ namespace Avalonia.Controls
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
var text = Text;
SetTextInternal(text.Substring(0, start) + text.Substring(end));
SetTextInternal(text.Substring(0, start) + text.Substring(end), raiseTextChanged);
CaretIndex = start;
ClearSelection();
return true;
@ -1348,16 +1364,23 @@ namespace Avalonia.Controls
return i;
}
private void SetTextInternal(string value)
private void SetTextInternal(string value, bool raiseTextChanged = true)
{
try
if (raiseTextChanged)
{
_ignoreTextChanges = true;
SetAndRaise(TextProperty, ref _text, value);
try
{
_ignoreTextChanges = true;
SetAndRaise(TextProperty, ref _text, value);
}
finally
{
_ignoreTextChanges = false;
}
}
finally
else
{
_ignoreTextChanges = false;
_text = value;
}
}

52
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -1,8 +1,10 @@
using System;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Utilities;
using JetBrains.Annotations;
namespace Avalonia.Controls.Utils
{
@ -15,8 +17,13 @@ namespace Avalonia.Controls.Utils
private Size _size;
private Thickness _borderThickness;
private CornerRadius _cornerRadius;
private AvaloniaList<double> _borderDashArray;
private double _borderDashOffset;
private PenLineCap _borderLineCap;
private PenLineJoin _borderJoin;
private bool _initialized;
void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
{
_backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
@ -60,7 +67,8 @@ namespace Avalonia.Controls.Utils
if (boundRect.Width != 0 && innerRect.Height != 0)
{
var borderGeometryKeypoints = new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false);
var borderGeometryKeypoints =
new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false);
var borderGeometry = new StreamGeometry();
using (var ctx = borderGeometry.Open())
@ -84,17 +92,22 @@ namespace Avalonia.Controls.Utils
public void Render(DrawingContext context,
Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
IBrush background, IBrush borderBrush, BoxShadows boxShadows)
IBrush background, IBrush borderBrush, BoxShadows boxShadows, double borderDashOffset = 0,
PenLineCap borderLineCap = PenLineCap.Flat, PenLineJoin borderLineJoin = PenLineJoin.Miter,
AvaloniaList<double> borderDashArray = null)
{
if (_size != finalSize
|| _borderThickness != borderThickness
|| _cornerRadius != cornerRadius
|| !_initialized)
Update(finalSize, borderThickness, cornerRadius);
RenderCore(context, background, borderBrush, boxShadows);
RenderCore(context, background, borderBrush, boxShadows, borderDashOffset, borderLineCap, borderLineJoin,
borderDashArray);
}
void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows)
void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows,
double borderDashOffset, PenLineCap borderLineCap, PenLineJoin borderLineJoin,
AvaloniaList<double> borderDashArray)
{
if (_useComplexRendering)
{
@ -115,11 +128,25 @@ namespace Avalonia.Controls.Utils
var borderThickness = _borderThickness.Top;
IPen pen = null;
ImmutableDashStyle? dashStyle = null;
if (borderDashArray != null && borderDashArray.Count > 0)
{
dashStyle = new ImmutableDashStyle(borderDashArray, borderDashOffset);
}
if (borderBrush != null && borderThickness > 0)
{
pen = new ImmutablePen(borderBrush.ToImmutable(), borderThickness);
pen = new ImmutablePen(
borderBrush.ToImmutable(),
borderThickness,
dashStyle,
borderLineCap,
borderLineJoin);
}
var rect = new Rect(_size);
if (!MathUtilities.IsZero(borderThickness))
rect = rect.Deflate(borderThickness * 0.5);
@ -130,7 +157,8 @@ namespace Avalonia.Controls.Utils
}
}
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderGeometryKeypoints keypoints)
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect,
BorderGeometryKeypoints keypoints)
{
context.BeginFigure(keypoints.TopLeft, true);
@ -184,7 +212,8 @@ namespace Avalonia.Controls.Utils
private class BorderGeometryKeypoints
{
internal BorderGeometryKeypoints(Rect boundRect, Thickness borderThickness, CornerRadius cornerRadius, bool inner)
internal BorderGeometryKeypoints(Rect boundRect, Thickness borderThickness, CornerRadius cornerRadius,
bool inner)
{
var left = 0.5 * borderThickness.Left;
var top = 0.5 * borderThickness.Top;
@ -206,10 +235,13 @@ namespace Avalonia.Controls.Utils
topLeftX = Math.Max(0, cornerRadius.TopLeft - left) + boundRect.TopLeft.X;
topRightX = boundRect.Width - Math.Max(0, cornerRadius.TopRight - top) + boundRect.TopLeft.X;
rightTopY = Math.Max(0, cornerRadius.TopRight - right) + boundRect.TopLeft.Y;
rightBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomRight - bottom) + boundRect.TopLeft.Y;
bottomRightX = boundRect.Width - Math.Max(0, cornerRadius.BottomRight - right) + boundRect.TopLeft.X;
rightBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomRight - bottom) +
boundRect.TopLeft.Y;
bottomRightX = boundRect.Width - Math.Max(0, cornerRadius.BottomRight - right) +
boundRect.TopLeft.X;
bottomLeftX = Math.Max(0, cornerRadius.BottomLeft - left) + boundRect.TopLeft.X;
leftBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomLeft - bottom) + boundRect.TopLeft.Y;
leftBottomY = boundRect.Height - Math.Max(0, cornerRadius.BottomLeft - bottom) +
boundRect.TopLeft.Y;
}
else
{

3
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@ -68,7 +68,8 @@ namespace Avalonia.Controls
{
x.RemoveOwner(Owner);
}
}, null);
},
() => throw new NotSupportedException("Dictionary reset not supported"));
}
return _mergedDictionaries;

3
src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj

@ -1,8 +1,9 @@
<Project Sdk="MSBuild.Sdk.Extras">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<UseWpf>true</UseWpf>
<UseWindowsForms>true</UseWindowsForms>
<ExtrasEnableImplicitWinFormsReferences>true</ExtrasEnableImplicitWinFormsReferences>
<UseDirect3D9>true</UseDirect3D9>
<PackageId>Avalonia.Win32.Interoperability</PackageId>

1
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -9,6 +9,7 @@
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
<AvnComIdl Include="WinRT\winrt.idl" OutputFile="WinRT\WinRT.Generated.cs" />
<AvnComIdl Include="Win32Com\win32.idl" OutputFile="Win32Com\Win32.Generated.cs" />
</ItemGroup>
<Import Project="../../../build/MicroCom.targets" />
<Import Project="$(MSBuildThisFileDirectory)\..\..\..\build\System.Drawing.Common.props" />

268
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -6,6 +6,9 @@ using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using Avalonia.MicroCom;
using Avalonia.Win32.Win32Com;
// ReSharper disable InconsistentNaming
#pragma warning disable 169, 649
@ -1244,8 +1247,19 @@ namespace Avalonia.Win32.Interop
internal static extern int CoCreateInstance(ref Guid clsid,
IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter);
internal unsafe static T CreateInstance<T>(ref Guid clsid, ref Guid iid) where T : IUnknown
{
var hresult = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out IntPtr pUnk);
if (hresult != 0)
{
throw new COMException("CreateInstance", hresult);
}
using var unk = MicroComRuntime.CreateProxyFor<IUnknown>(pUnk, true);
return MicroComRuntime.QueryInterface<T>(unk);
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv);
internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, out IntPtr ppv);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool OpenClipboard(IntPtr hWndOwner);
@ -1888,7 +1902,8 @@ namespace Avalonia.Win32.Interop
E_INVALIDARG = 0x80070057,
E_OUTOFMEMORY = 0x8007000E,
E_NOTIMPL = 0x80004001,
E_UNEXPECTED = 0x8000FFFF
E_UNEXPECTED = 0x8000FFFF,
E_CANCELLED = 0x800704C7,
}
public enum Icons
@ -1897,33 +1912,6 @@ namespace Avalonia.Win32.Interop
ICON_BIG = 1
}
public const uint SIGDN_FILESYSPATH = 0x80058000;
[Flags]
public enum FOS : uint
{
FOS_OVERWRITEPROMPT = 0x00000002,
FOS_STRICTFILETYPES = 0x00000004,
FOS_NOCHANGEDIR = 0x00000008,
FOS_PICKFOLDERS = 0x00000020,
FOS_FORCEFILESYSTEM = 0x00000040, // Ensure that items returned are filesystem items.
FOS_ALLNONSTORAGEITEMS = 0x00000080, // Allow choosing items that have no storage.
FOS_NOVALIDATE = 0x00000100,
FOS_ALLOWMULTISELECT = 0x00000200,
FOS_PATHMUSTEXIST = 0x00000800,
FOS_FILEMUSTEXIST = 0x00001000,
FOS_CREATEPROMPT = 0x00002000,
FOS_SHAREAWARE = 0x00004000,
FOS_NOREADONLYRETURN = 0x00008000,
FOS_NOTESTFILECREATE = 0x00010000,
FOS_HIDEMRUPLACES = 0x00020000,
FOS_HIDEPINNEDPLACES = 0x00040000,
FOS_NODEREFERENCELINKS = 0x00100000,
FOS_DONTADDTORECENT = 0x02000000,
FOS_FORCESHOWHIDDEN = 0x10000000,
FOS_DEFAULTNOMINIMODE = 0x20000000
}
public static class ShellIds
{
public static readonly Guid OpenFileDialog = Guid.Parse("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7");
@ -1934,228 +1922,6 @@ namespace Avalonia.Win32.Interop
public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf");
}
[ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IFileDialog
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
[PreserveSig()]
uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileTypes(uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] COMDLG_FILTERSPEC[] rgFilterSpec);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileTypeIndex([In] uint iFileType);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetFileTypeIndex(out uint piFileType);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Unadvise([In] uint dwCookie);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetOptions([In] uint fos);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetOptions(out uint fos);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Close([MarshalAs(UnmanagedType.Error)] uint hr);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetClientGuid([In] ref Guid guid);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint ClearClientData();
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
}
[ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IFileOpenDialog
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
[PreserveSig()]
uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetFileTypeIndex([In] uint iFileType);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetFileTypeIndex(out uint piFileType);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void Unadvise([In] uint dwCookie);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetOptions([In] uint fos);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetOptions(out uint fos);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void Close([MarshalAs(UnmanagedType.Error)] int hr);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetClientGuid([In] ref Guid guid);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void ClearClientData();
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai);
}
[ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellItemArray
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid,
[In] ref Guid riid, out IntPtr ppvOut);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetPropertyDescriptionList([In] ref PROPERTYKEY keyType, [In] ref Guid riid, out IntPtr ppv);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetAttributes([In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetCount(out uint pdwNumItems);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems);
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct PROPERTYKEY
{
public Guid fmtid;
public uint pid;
}
public enum SIATTRIBFLAGS
{
SIATTRIBFLAGS_AND = 1,
SIATTRIBFLAGS_APPCOMPAT = 3,
SIATTRIBFLAGS_OR = 2
}
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IShellItem
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct COMDLG_FILTERSPEC
{

269
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@ -1,170 +1,229 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.MicroCom;
using Avalonia.Win32.Interop;
using Avalonia.Win32.Win32Com;
namespace Avalonia.Win32
{
class SystemDialogImpl : ISystemDialogImpl
internal class SystemDialogImpl : ISystemDialogImpl
{
private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE |
UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT;
private const uint SIGDN_FILESYSPATH = 0x80058000;
public unsafe Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
private const FILEOPENDIALOGOPTIONS DefaultDialogOptions = FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM | FILEOPENDIALOGOPTIONS.FOS_NOVALIDATE |
FILEOPENDIALOGOPTIONS.FOS_NOTESTFILECREATE | FILEOPENDIALOGOPTIONS.FOS_DONTADDTORECENT;
public unsafe Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
string[] result = default;
Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk);
var frm = (UnmanagedMethods.IFileDialog)unk;
string[]? result = default;
try
{
var clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
var iid = UnmanagedMethods.ShellIds.IFileDialog;
var frm = UnmanagedMethods.CreateInstance<IFileDialog>(ref clsid, ref iid);
var openDialog = dialog as OpenFileDialog;
var openDialog = dialog as OpenFileDialog;
uint options;
frm.GetOptions(out options);
options |= (uint)(DefaultDialogOptions);
if (openDialog?.AllowMultiple == true)
options |= (uint)UnmanagedMethods.FOS.FOS_ALLOWMULTISELECT;
frm.SetOptions(options);
var options = frm.Options;
options |= DefaultDialogOptions;
if (openDialog?.AllowMultiple == true)
{
options |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT;
}
frm.SetOptions(options);
var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? "";
frm.SetDefaultExtension(defaultExtension);
frm.SetFileName(dialog.InitialFileName ?? "");
frm.SetTitle(dialog.Title ?? "");
var defaultExtension = (dialog as SaveFileDialog)?.DefaultExtension ?? "";
fixed (char* pExt = defaultExtension)
{
frm.SetDefaultExtension(pExt);
}
var filters = new List<UnmanagedMethods.COMDLG_FILTERSPEC>();
if (dialog.Filters != null)
{
foreach (var filter in dialog.Filters)
var initialFileName = dialog.InitialFileName ?? "";
fixed (char* fExt = initialFileName)
{
var extMask = string.Join(";", filter.Extensions.Select(e => "*." + e));
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = filter.Name, pszSpec = extMask });
frm.SetFileName(fExt);
}
}
if (filters.Count == 0)
filters.Add(new UnmanagedMethods.COMDLG_FILTERSPEC { pszName = "All files", pszSpec = "*.*" });
frm.SetFileTypes((uint)filters.Count, filters.ToArray());
frm.SetFileTypeIndex(0);
var title = dialog.Title ?? "";
fixed (char* tExt = title)
{
frm.SetTitle(tExt);
}
if (dialog.Directory != null)
{
UnmanagedMethods.IShellItem directoryShellItem;
Guid riid = UnmanagedMethods.ShellIds.IShellItem;
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
fixed (void* pFilters = FiltersToPointer(dialog.Filters, out var count))
{
frm.SetFolder(directoryShellItem);
frm.SetDefaultFolder(directoryShellItem);
frm.SetFileTypes((ushort)count, pFilters);
}
}
if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK)
{
if (openDialog?.AllowMultiple == true)
frm.SetFileTypeIndex(0);
if (dialog.Directory != null)
{
UnmanagedMethods.IShellItemArray shellItemArray;
((UnmanagedMethods.IFileOpenDialog)frm).GetResults(out shellItemArray);
uint count;
shellItemArray.GetCount(out count);
result = new string[count];
for (uint i = 0; i < count; i++)
var riid = UnmanagedMethods.ShellIds.IShellItem;
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out var directoryShellItem)
== (uint)UnmanagedMethods.HRESULT.S_OK)
{
UnmanagedMethods.IShellItem shellItem;
shellItemArray.GetItemAt(i, out shellItem);
result[i] = GetAbsoluteFilePath(shellItem);
var proxy = MicroComRuntime.CreateProxyFor<IShellItem>(directoryShellItem, true);
frm.SetFolder(proxy);
frm.SetDefaultFolder(proxy);
}
}
else
frm.Show(hWnd);
if (openDialog?.AllowMultiple == true)
{
UnmanagedMethods.IShellItem shellItem;
if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
using var fileOpenDialog = frm.QueryInterface<IFileOpenDialog>();
var shellItemArray = fileOpenDialog.Results;
var count = shellItemArray.Count;
var results = new List<string>();
for (int i = 0; i < count; i++)
{
result = new string[] { GetAbsoluteFilePath(shellItem) };
var shellItem = shellItemArray.GetItemAt(i);
if (GetAbsoluteFilePath(shellItem) is { } selected)
{
results.Add(selected);
}
}
result = results.ToArray();
}
else if (frm.Result is { } shellItem
&& GetAbsoluteFilePath(shellItem) is { } singleResult)
{
result = new[] { singleResult };
}
}
return result;
});
return result;
}
catch (COMException ex)
{
if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED)
{
return result;
}
throw new Win32Exception(ex.HResult);
}
})!;
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
public unsafe Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
return Task.Factory.StartNew(() =>
return Task.Run(() =>
{
string result = default;
string? result = default;
try
{
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
var clsid = UnmanagedMethods.ShellIds.OpenFileDialog;
var iid = UnmanagedMethods.ShellIds.IFileDialog;
var frm = UnmanagedMethods.CreateInstance<IFileDialog>(ref clsid, ref iid);
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog;
Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
var options = frm.Options;
options = FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS | DefaultDialogOptions;
frm.SetOptions(options);
UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk);
var frm = (UnmanagedMethods.IFileDialog)unk;
uint options;
frm.GetOptions(out options);
options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | DefaultDialogOptions);
frm.SetOptions(options);
frm.SetTitle(dialog.Title ?? "");
var title = dialog.Title ?? "";
fixed (char* tExt = title)
{
frm.SetTitle(tExt);
}
if (dialog.Directory != null)
{
UnmanagedMethods.IShellItem directoryShellItem;
Guid riid = UnmanagedMethods.ShellIds.IShellItem;
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
if (dialog.Directory != null)
{
frm.SetFolder(directoryShellItem);
var riid = UnmanagedMethods.ShellIds.IShellItem;
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out var directoryShellItem)
== (uint)UnmanagedMethods.HRESULT.S_OK)
{
var proxy = MicroComRuntime.CreateProxyFor<IShellItem>(directoryShellItem, true);
frm.SetFolder(proxy);
frm.SetDefaultFolder(proxy);
}
}
}
if (dialog.Directory != null)
{
UnmanagedMethods.IShellItem directoryShellItem;
Guid riid = UnmanagedMethods.ShellIds.IShellItem;
if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.Directory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
frm.Show(hWnd);
if (frm.Result is not null)
{
frm.SetDefaultFolder(directoryShellItem);
result = GetAbsoluteFilePath(frm.Result);
}
}
if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK)
return result;
}
catch (COMException ex)
{
UnmanagedMethods.IShellItem shellItem;
if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK)
if ((uint)ex.HResult == (uint)UnmanagedMethods.HRESULT.E_CANCELLED)
{
result = GetAbsoluteFilePath(shellItem);
return result;
}
throw new Win32Exception(ex.HResult);
}
return result;
});
}
private string GetAbsoluteFilePath(UnmanagedMethods.IShellItem shellItem)
private unsafe string? GetAbsoluteFilePath(IShellItem shellItem)
{
IntPtr pszString;
if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK)
var pszString = new IntPtr(shellItem.GetDisplayName(SIGDN_FILESYSPATH));
if (pszString != IntPtr.Zero)
{
if (pszString != IntPtr.Zero)
try
{
try
{
return Marshal.PtrToStringAuto(pszString);
}
finally
{
Marshal.FreeCoTaskMem(pszString);
}
return Marshal.PtrToStringUni(pszString);
}
finally
{
Marshal.FreeCoTaskMem(pszString);
}
}
return default;
}
private unsafe byte[] FiltersToPointer(List<FileDialogFilter>? filters, out int lenght)
{
if (filters == null || filters.Count == 0)
{
filters = new List<FileDialogFilter>
{
new FileDialogFilter { Name = "All files", Extensions = new List<string> { "*" } }
};
}
var size = Marshal.SizeOf<UnmanagedMethods.COMDLG_FILTERSPEC>();
var arr = new byte[size];
var resultArr = new byte[size * filters.Count];
for (int i = 0; i < filters.Count; i++)
{
var filter = filters[i];
var filterPtr = Marshal.AllocHGlobal(size);
try
{
var filterStr = new UnmanagedMethods.COMDLG_FILTERSPEC
{
pszName = filter.Name ?? string.Empty,
pszSpec = string.Join(";", filter.Extensions.Select(e => "*." + e))
};
Marshal.StructureToPtr(filterStr, filterPtr, false);
Marshal.Copy(filterPtr, resultArr, i * size, size);
}
finally
{
Marshal.FreeHGlobal(filterPtr);
}
}
lenght = filters.Count;
return resultArr;
}
}
}

191
src/Windows/Avalonia.Win32/Win32Com/win32.idl

@ -0,0 +1,191 @@
@clr-namespace Avalonia.Win32.Win32Com
@clr-access internal
@clr-map FLOAT float
@clr-map HSTRING IntPtr
@clr-map Vector2 System.Numerics.Vector2
@clr-map Vector3 System.Numerics.Vector3
@clr-map Quaternion System.Numerics.Quaternion
@clr-map Matrix4x4 System.Numerics.Matrix4x4
@clr-map RECT Avalonia.Win32.Interop.UnmanagedMethods.RECT
@clr-map SIZE Avalonia.Win32.Interop.UnmanagedMethods.SIZE
@clr-map POINT Avalonia.Win32.Interop.UnmanagedMethods.POINT
@clr-map HWND IntPtr
@clr-map BOOL int
@clr-map DWORD int
@clr-map boolean int
@clr-map BYTE byte
@clr-map INT16 short
@clr-map INT32 int
@clr-map INT64 long
@clr-map UINT ushort
@clr-map UINT16 ushort
@clr-map ULONG uint
@clr-map UINT32 uint
@clr-map UINT64 ulong
@clr-map DOUBLE double
@clr-map GUID System.Guid
@clr-map REFGUID System.Guid*
@clr-map REFIID System.Guid*
@clr-map WCHAR System.Char
[flags]
enum FILEOPENDIALOGOPTIONS
{
FOS_OVERWRITEPROMPT = 0x00000002,
FOS_STRICTFILETYPES = 0x00000004,
FOS_NOCHANGEDIR = 0x00000008,
FOS_PICKFOLDERS = 0x00000020,
FOS_FORCEFILESYSTEM = 0x00000040, // Ensure that items returned are filesystem items.
FOS_ALLNONSTORAGEITEMS = 0x00000080, // Allow choosing items that have no storage.
FOS_NOVALIDATE = 0x00000100,
FOS_ALLOWMULTISELECT = 0x00000200,
FOS_PATHMUSTEXIST = 0x00000800,
FOS_FILEMUSTEXIST = 0x00001000,
FOS_CREATEPROMPT = 0x00002000,
FOS_SHAREAWARE = 0x00004000,
FOS_NOREADONLYRETURN = 0x00008000,
FOS_NOTESTFILECREATE = 0x00010000,
FOS_HIDEMRUPLACES = 0x00020000,
FOS_HIDEPINNEDPLACES = 0x00040000,
FOS_NODEREFERENCELINKS = 0x00100000,
FOS_DONTADDTORECENT = 0x02000000,
FOS_FORCESHOWHIDDEN = 0x10000000,
FOS_DEFAULTNOMINIMODE = 0x20000000
}
[
object,
uuid(43826d1e-e718-42ee-bc55-a1e261c37bfe),
pointer_default(unique)
]
interface IShellItem : IUnknown
{
HRESULT BindToHandler(
[in, unique] void* pbc,
[in] REFGUID bhid,
[in] REFIID riid,
[out, iid_is(riid)] void** ppv);
HRESULT GetParent([out] IShellItem** ppsi);
HRESULT GetDisplayName(
[in] uint sigdnName,
[out, string, annotation("_Outptr_result_nullonfailure_")] WCHAR** ppszName);
HRESULT GetAttributes(
[in] ULONG sfgaoMask,
[out] ULONG* psfgaoAttribs);
HRESULT Compare(
[in] IShellItem* psi,
[in] ULONG hint,
[out] int* piOrder);
}
[
object,
uuid(B63EA76D-1F85-456F-A19C-48159EFA858B),
pointer_default(unique)
]
interface IShellItemArray : IUnknown
{
HRESULT BindToHandler([in, unique] void* pbc, [in] REFGUID bhid, [in] REFIID riid, [out, iid_is(riid)] void** ppvOut);
HRESULT GetPropertyStore([in] UINT flags, [in] REFIID riid, [out, iid_is(riid)] void** ppv);
HRESULT GetPropertyDescriptionList([in] void* keyType, [in] REFIID riid, [out, iid_is(riid)] void** ppv);
HRESULT GetAttributes([in] int AttribFlags, [in] UINT sfgaoMask, [out] UINT* psfgaoAttribs);
HRESULT GetCount([out] DWORD* pdwNumItems);
HRESULT GetItemAt([in] DWORD dwIndex, [out] IShellItem** ppsi);
HRESULT EnumItems([out] void** ppenumShellItems);
}
[
object,
uuid(B4DB1657-70D7-485E-8E3E-6FCB5A5C1802),
pointer_default(unique)
]
interface IModalWindow : IUnknown
{
[local]
HRESULT Show(
[in, unique] HWND hwndOwner);
}
[
object,
uuid(42F85136-DB7E-439C-85F1-E4075D135FC8),
pointer_default(unique)
]
interface IFileDialog : IModalWindow
{
HRESULT SetFileTypes(
[in] UINT cFileTypes,
[in, size_is(cFileTypes)] void* rgFilterSpec);
HRESULT SetFileTypeIndex([in] UINT iFileType);
HRESULT GetFileTypeIndex([out] UINT* piFileType);
HRESULT Advise(
[in] void* pfde,
[out] DWORD* pdwCookie);
HRESULT Unadvise([in] DWORD dwCookie);
HRESULT SetOptions([in] FILEOPENDIALOGOPTIONS fos);
HRESULT GetOptions([out] FILEOPENDIALOGOPTIONS* pfos);
HRESULT SetDefaultFolder([in] IShellItem* psi);
HRESULT SetFolder([in] IShellItem* psi);
HRESULT GetFolder([out] IShellItem** ppsi);
HRESULT GetCurrentSelection([out] IShellItem** ppsi);
HRESULT SetFileName([in, string] WCHAR* pszName);
HRESULT GetFileName([out, string] WCHAR** pszName);
HRESULT SetTitle([in, string] WCHAR* pszTitle);
HRESULT SetOkButtonLabel([in, string] WCHAR* pszText);
HRESULT SetFileNameLabel([in, string] WCHAR* pszLabel);
HRESULT GetResult([out] IShellItem** ppsi);
HRESULT AddPlace(
[in] IShellItem* psi,
[in] INT32 fdap);
HRESULT SetDefaultExtension([in, string] WCHAR* pszDefaultExtension);
HRESULT Close([in] HRESULT hr);
HRESULT SetClientGuid([in] REFGUID guid);
HRESULT ClearClientData();
HRESULT SetFilter([in] void* pFilter);
}
[
object,
uuid(D57C7288-D4AD-4768-BE02-9D969532D960),
pointer_default(unique)
]
interface IFileOpenDialog : IFileDialog
{
HRESULT GetResults(
[out] IShellItemArray** ppenum);
HRESULT GetSelectedItems(
[out] IShellItemArray** ppsai);
}

4
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -1,5 +1,4 @@
<Project>
<Import Project="Sdk.props" Sdk="MSBuild.Sdk.Extras" />
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFramework>xamarin.ios10</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -20,5 +19,4 @@
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="MSBuild.Sdk.Extras" />
</Project>

2
src/tools/MicroComGenerator/ParseException.cs

@ -7,7 +7,7 @@ namespace MicroComGenerator
public int Line { get; }
public int Position { get; }
public ParseException(string message, int line, int position) : base(message)
public ParseException(string message, int line, int position) : base($"({line}, {position}) {message}")
{
Line = line;
Position = position;

152
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -357,5 +357,157 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Equal(target, result);
}
[Fact]
public void RemoveAll_Should_Send_Single_Notification_For_Sequential_Range()
{
var target = new AvaloniaList<string>(Enumerable.Range(0, 10).Select(x => $"Item {x}"));
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove, e.OldItems);
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(1, raised);
}
[Fact]
public void RemoveAll_Should_Send_Single_Notification_For_Sequential_Range_With_Duplicate_Source_Items()
{
var items = Enumerable.Range(0, 20).Select(x => $"Item {x / 2}");
var target = new AvaloniaList<string>(items);
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
Assert.Equal(10, e.OldStartingIndex);
Assert.Equal(new[]
{
"Item 5",
"Item 5",
"Item 6",
"Item 6",
"Item 7",
"Item 7",
}, e.OldItems);
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(1, raised);
}
[Fact]
public void RemoveAll_Should_Send_Multiple_Notifications_For_Non_Sequential_Range()
{
var target = new AvaloniaList<string>(Enumerable.Range(0, 10).Select(x => $"Item {x}"));
var raised = 0;
var toRemove = new[]
{
new[] { "Item 2", "Item 3" },
new[] { "Item 5", "Item 6" }
};
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
if (raised == 0)
{
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove[1], e.OldItems);
}
else
{
Assert.Equal(2, e.OldStartingIndex);
Assert.Equal(toRemove[0], e.OldItems);
}
++raised;
};
target.RemoveAll(toRemove[0].Concat(toRemove[1]));
Assert.Equal(2, raised);
}
[Fact]
public void RemoveAll_Should_Send_Multiple_Notifications_For_Sequential_Range_With_Nonsequential_Duplicate_Source_Items()
{
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}");
var target = new AvaloniaList<string>(items.Concat(items));
var raised = 0;
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
if (raised == 0)
{
Assert.Equal(15, e.OldStartingIndex);
Assert.Equal(toRemove, e.OldItems);
}
else
{
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove, e.OldItems);
}
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(2, raised);
}
[Fact]
public void RemoveAll_Should_Not_Send_Notification_For_Items_Not_Present()
{
var target = new AvaloniaList<string>(Enumerable.Range(0, 10).Select(x => $"Item {x}"));
var toRemove = new[] { "Item 5", "Item 6", "Item 7", "Not present" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Remove, e.Action);
Assert.Equal(5, e.OldStartingIndex);
Assert.Equal(toRemove.Take(3).ToArray(), e.OldItems);
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(1, raised);
}
[Fact]
public void RemoveAll_Should_Handle_Empty_List()
{
var target = new AvaloniaList<string>();
var toRemove = new[] { "Item 5", "Item 6", "Item 7" };
var raised = 0;
target.CollectionChanged += (s, e) =>
{
++raised;
};
target.RemoveAll(toRemove);
Assert.Equal(0, raised);
}
}
}

48
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -784,6 +784,54 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Setting_SelectedText_Should_Fire_Single_Text_Changed_Notification()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123",
AcceptsReturn = true,
AcceptsTab = true,
SelectionStart = 1,
SelectionEnd = 3,
};
var values = new List<string>();
target.GetObservable(TextBox.TextProperty).Subscribe(x => values.Add(x));
target.SelectedText = "A";
Assert.Equal(new[] { "0123", "0A3" }, values);
}
}
[Fact]
public void Entering_Text_With_SelectedText_Should_Fire_Single_Text_Changed_Notification()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123",
AcceptsReturn = true,
AcceptsTab = true,
SelectionStart = 1,
SelectionEnd = 3,
};
var values = new List<string>();
target.GetObservable(TextBox.TextProperty).Subscribe(x => values.Add(x));
RaiseTextEvent(target, "A");
Assert.Equal(new[] { "0123", "0A3" }, values);
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),

Loading…
Cancel
Save