Browse Source

Merge branch 'master' into features/DevTools/PropertyType

pull/6280/head
workgroupengineering 5 years ago
committed by GitHub
parent
commit
62db89d782
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      samples/ControlCatalog/Pages/ComboBoxPage.xaml
  2. 6
      src/Avalonia.Base/Collections/AvaloniaList.cs
  3. 34
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  4. 7
      src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
  5. 13
      src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
  6. 30
      src/Avalonia.Controls/ComboBox.cs
  7. 38
      src/Avalonia.Controls/Mixins/DisposableMixin.cs
  8. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  9. 6
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  10. 41
      src/Avalonia.Controls/Primitives/Popup.cs
  11. 99
      src/Avalonia.Controls/SystemDialog.cs
  12. 6
      src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs
  13. 73
      src/Avalonia.Controls/TextBox.cs
  14. 5
      src/Avalonia.Input/AccessKeyHandler.cs
  15. 11
      src/Avalonia.Input/Navigation/TabNavigation.cs
  16. 2
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  17. 1
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  18. 15
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  19. 1
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  20. 3
      src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs
  21. 1
      src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs
  22. 2
      src/Windows/Avalonia.Win32/FramebufferManager.cs
  23. 7
      src/Windows/Avalonia.Win32/PlatformConstants.cs
  24. 6
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  25. 4
      src/Windows/Avalonia.Win32/Win32GlManager.cs
  26. 2
      src/Windows/Avalonia.Win32/Win32Platform.cs
  27. 1
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
  28. 4
      src/Windows/Avalonia.Win32/WindowImpl.cs
  29. 86
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs
  30. 1
      tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
  31. 3
      tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
  32. 27
      tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
  33. 2
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  34. 106
      tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
  35. BIN
      tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png
  36. BIN
      tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png
  37. BIN
      tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png
  38. BIN
      tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png

32
samples/ControlCatalog/Pages/ComboBoxPage.xaml

@ -1,12 +1,20 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ComboBoxPage"
xmlns:sys="clr-namespace:System;assembly=netstandard">
xmlns:sys="using:System"
xmlns:col="using:System.Collections">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ComboBox</TextBlock>
<TextBlock Classes="h2">A drop-down list.</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 16 0 0" Spacing="8">
<WrapPanel HorizontalAlignment="Center" Margin="0 16 0 0"
MaxWidth="750">
<WrapPanel.Styles>
<Style Selector="ComboBox">
<Setter Property="Width" Value="250" />
<Setter Property="Margin" Value="10" />
</Style>
</WrapPanel.Styles>
<ComboBox PlaceholderText="Pick an Item">
<ComboBoxItem>Inline Items</ComboBoxItem>
<ComboBoxItem>Inline Item 2</ComboBoxItem>
@ -14,6 +22,24 @@
<ComboBoxItem>Inline Item 4</ComboBoxItem>
</ComboBox>
<ComboBox>
<ComboBox.Items>
<col:ArrayList>
<x:Null />
<sys:String>Hello</sys:String>
<sys:String>World</sys:String>
</col:ArrayList>
</ComboBox.Items>
<ComboBox.ItemTemplate>
<DataTemplate>
<Panel>
<TextBlock Text="{Binding}" />
<TextBlock Text="Null object" IsVisible="{Binding Converter={x:Static ObjectConverters.IsNull}}" />
</Panel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox SelectedIndex="0">
<ComboBoxItem>
<Panel>
@ -46,7 +72,7 @@
<sys:Exception />
</DataValidationErrors.Error>
</ComboBox>
</StackPanel>
</WrapPanel>
</StackPanel>
</UserControl>

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

@ -280,8 +280,8 @@ namespace Avalonia.Collections
/// <summary>
/// Gets a range of items from the collection.
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
/// <param name="index">The zero-based <see cref="AvaloniaList{T}"/> index at which the range starts.</param>
/// <param name="count">The number of elements in the range.</param>
public IEnumerable<T> GetRange(int index, int count)
{
return _inner.GetRange(index, count);
@ -455,7 +455,7 @@ namespace Avalonia.Collections
}
/// <summary>
/// Ensures that the capacity of the list is at least <see cref="capacity"/>.
/// Ensures that the capacity of the list is at least <see cref="Capacity"/>.
/// </summary>
/// <param name="capacity">The capacity.</param>
public void EnsureCapacity(int capacity)

34
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -93,13 +93,25 @@ namespace Avalonia.Utilities
return !type.IsValueType || IsNullableType(type);
}
/// <summary>
/// Returns a value indicating whether value can be casted to the specified type.
/// If value is null, checks if instances of that type can be null.
/// </summary>
/// <typeparam name="T">The type to cast to</typeparam>
/// <param name="value">The value to check if cast possible</param>
/// <returns>True if the cast is possible, otherwise false.</returns>
public static bool CanCast<T>(object value)
{
return value is T || (value is null && AcceptsNull(typeof(T)));
}
/// <summary>
/// Try to convert a value to a type by any means possible.
/// </summary>
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="to">The type to convert to.</param>
/// <param name="value">The value to convert.</param>
/// <param name="culture">The culture to use.</param>
/// <param name="result">If successful, contains the cast value.</param>
/// <param name="result">If successful, contains the convert value.</param>
/// <returns>True if the cast was successful, otherwise false.</returns>
public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
{
@ -216,10 +228,10 @@ namespace Avalonia.Utilities
/// Try to convert a value to a type using the implicit conversions allowed by the C#
/// language.
/// </summary>
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="result">If successful, contains the cast value.</param>
/// <returns>True if the cast was successful, otherwise false.</returns>
/// <param name="to">The type to convert to.</param>
/// <param name="value">The value to convert.</param>
/// <param name="result">If successful, contains the converted value.</param>
/// <returns>True if the convert was successful, otherwise false.</returns>
public static bool TryConvertImplicit(Type to, object value, out object result)
{
if (value == null)
@ -278,8 +290,8 @@ namespace Avalonia.Utilities
/// Convert a value to a type by any means possible, returning the default for that type
/// if the value could not be converted.
/// </summary>
/// <param name="value">The value to cast.</param>
/// <param name="type">The type to cast to..</param>
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to..</param>
/// <param name="culture">The culture to use.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
public static object ConvertOrDefault(object value, Type type, CultureInfo culture)
@ -291,8 +303,8 @@ namespace Avalonia.Utilities
/// Convert a value to a type using the implicit conversions allowed by the C# language or
/// return the default for the type if the value could not be converted.
/// </summary>
/// <param name="value">The value to cast.</param>
/// <param name="type">The type to cast to..</param>
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
public static object ConvertImplicitOrDefault(object value, Type type)
{

7
src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs

@ -83,8 +83,9 @@ namespace Avalonia.Collections
if (key == null)
key = item;
if (_valueConverter != null)
key = _valueConverter.Convert(key, typeof(object), level, culture);
var valueConverter = ValueConverter;
if (valueConverter != null)
key = valueConverter.Convert(key, typeof(object), level, culture);
return key;
}
@ -99,6 +100,8 @@ namespace Avalonia.Collections
}
public override string PropertyName => _propertyPath;
public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; }
private Type GetPropertyType(object o)
{
return o.GetType().GetNestedPropertyType(_propertyPath);

13
src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs

@ -1,10 +1,8 @@
using Avalonia.Data;
using Avalonia.Reactive;
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Text;
namespace Avalonia.Controls.Utils
{
@ -67,11 +65,14 @@ namespace Avalonia.Controls.Utils
private void SetSourceValue(object value)
{
_settingSourceValue = true;
if (!_settingSourceValue)
{
_settingSourceValue = true;
_sourceSubject.OnNext(value);
_sourceSubject.OnNext(value);
_settingSourceValue = false;
_settingSourceValue = false;
}
}
private void SetControlValue(object value)
{
@ -157,4 +158,4 @@ namespace Avalonia.Controls.Utils
}
}
}
}
}

30
src/Avalonia.Controls/ComboBox.cs

@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
@ -80,7 +82,7 @@ namespace Avalonia.Controls
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
private IDisposable _subscriptionsOnOpen;
private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
/// <summary>
/// Initializes static members of the <see cref="ComboBox"/> class.
@ -291,6 +293,7 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
}
internal void ItemFocused(ComboBoxItem dropDownItem)
@ -303,8 +306,7 @@ namespace Avalonia.Controls
private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
_subscriptionsOnOpen.Clear();
if (CanFocus(this))
{
@ -316,20 +318,34 @@ namespace Avalonia.Controls
{
TryFocusSelectedItem();
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
_subscriptionsOnOpen.Clear();
var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null)
{
_subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
{
//eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
{
ev.Handled = true;
}
}, Interactivity.RoutingStrategies.Tunnel);
}, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen);
}
this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
foreach (var parent in this.GetVisualAncestors().OfType<IControl>())
{
parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen);
}
}
private void IsVisibleChanged(bool isVisible)
{
if (!isVisible && IsDropDownOpen)
{
IsDropDownOpen = false;
}
}

38
src/Avalonia.Controls/Mixins/DisposableMixin.cs

@ -0,0 +1,38 @@
using System;
using System.Reactive.Disposables;
namespace Avalonia.Controls.Mixins
{
/// <summary>
/// Extension methods associated with the IDisposable interface.
/// </summary>
public static class DisposableMixin
{
/// <summary>
/// Ensures the provided disposable is disposed with the specified <see cref="CompositeDisposable"/>.
/// </summary>
/// <typeparam name="T">
/// The type of the disposable.
/// </typeparam>
/// <param name="item">
/// The disposable we are going to want to be disposed by the CompositeDisposable.
/// </param>
/// <param name="compositeDisposable">
/// The <see cref="CompositeDisposable"/> to which <paramref name="item"/> will be added.
/// </param>
/// <returns>
/// The disposable.
/// </returns>
public static T DisposeWith<T>(this T item, CompositeDisposable compositeDisposable)
where T : IDisposable
{
if (compositeDisposable is null)
{
throw new ArgumentNullException(nameof(compositeDisposable));
}
compositeDisposable.Add(item);
return item;
}
}
}

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -115,7 +115,7 @@ namespace Avalonia.Controls.Platform
protected IMenu? Menu { get; private set; }
protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400);
public static TimeSpan MenuShowDelay { get; set; } = TimeSpan.FromMilliseconds(400);
protected internal virtual void GotFocus(object sender, GotFocusEventArgs e)
{

6
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -327,7 +327,11 @@ namespace Avalonia.Controls.Presenters
var oldChild = Child;
var newChild = content as IControl;
if (content != null && newChild == null)
// We want to allow creating Child from the Template, if Content is null.
// But it's important to not use DataTemplates, otherwise we will break content presenters in many places,
// otherwise it will blow up every ContentPresenter without Content set.
if (newChild == null
&& (content != null || ContentTemplate != null))
{
var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ??
(

41
src/Avalonia.Controls/Primitives/Popup.cs

@ -2,6 +2,7 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
@ -393,18 +394,8 @@ namespace Avalonia.Controls.Primitives
var handlerCleanup = new CompositeDisposable(5);
void DeferCleanup(IDisposable? disposable)
{
if (disposable is null)
{
return;
}
handlerCleanup.Add(disposable);
}
DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup);
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
@ -418,19 +409,19 @@ namespace Avalonia.Controls.Primitives
PlacementConstraintAdjustment,
PlacementRect);
DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
(x, handler) => x.TemplateApplied -= handler));
(x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup);
if (topLevel is Window window)
{
DeferCleanup(SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
(x, handler) => x.Deactivated += handler,
(x, handler) => x.Deactivated -= handler));
(x, handler) => x.Deactivated -= handler).DisposeWith(handlerCleanup);
DeferCleanup(SubscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, WindowLostFocus,
SubscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, WindowLostFocus,
(x, handler) => x.LostFocus += handler,
(x, handler) => x.LostFocus -= handler));
(x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
}
else
{
@ -438,13 +429,13 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
(x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup);
}
}
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
InputManager.Instance?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup);
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{
@ -466,17 +457,17 @@ namespace Avalonia.Controls.Primitives
dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
DeferCleanup(Disposable.Create(() =>
Disposable.Create(() =>
{
dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null;
}));
}).DisposeWith(handlerCleanup);
DeferCleanup(SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
dismissLayer,
PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler,
(x, handler) => x.PointerPressed -= handler));
(x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup);
}
}

99
src/Avalonia.Controls/SystemDialog.cs

@ -4,30 +4,65 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Base class for system file dialogs.
/// </summary>
public abstract class FileDialog : FileSystemDialog
{
/// <summary>
/// Gets or sets a collection of filters which determine the types of files displayed in an
/// <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary>
public List<FileDialogFilter> Filters { get; set; } = new List<FileDialogFilter>();
public string InitialFileName { get; set; }
/// <summary>
/// Gets or sets initial file name that is displayed when the dialog is opened.
/// </summary>
public string? InitialFileName { get; set; }
}
/// <summary>
/// Base class for system file and directory dialogs.
/// </summary>
public abstract class FileSystemDialog : SystemDialog
{
[Obsolete("Use Directory")]
public string InitialDirectory
public string? InitialDirectory
{
get => Directory;
set => Directory = value;
}
public string Directory { get; set; }
/// <summary>
/// Gets or sets the initial directory that will be displayed when the file system dialog
/// is opened.
/// </summary>
public string? Directory { get; set; }
}
/// <summary>
/// Represents a system dialog that prompts the user to select a location for saving a file.
/// </summary>
public class SaveFileDialog : FileDialog
{
public string DefaultExtension { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file (including the period ".").
/// </summary>
public string? DefaultExtension { get; set; }
public async Task<string> ShowAsync(Window parent)
/// <summary>
/// Shows the save file dialog.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <returns>
/// A task that on completion contains the full path of the save location, or null if the
/// dialog was canceled.
/// </returns>
public async Task<string?> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@ -37,11 +72,25 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Represents a system dialog that allows the user to select one or more files to open.
/// </summary>
public class OpenFileDialog : FileDialog
{
/// <summary>
/// Gets or sets a value indicating whether the user can select multiple files.
/// </summary>
public bool AllowMultiple { get; set; }
public Task<string[]> ShowAsync(Window parent)
/// <summary>
/// Shows the open file dialog.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <returns>
/// A task that on completion returns an array containing the full path to the selected
/// files, or null if the dialog was canceled.
/// </returns>
public Task<string[]?> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@ -49,15 +98,27 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Represents a system dialog that allows the user to select a directory.
/// </summary>
public class OpenFolderDialog : FileSystemDialog
{
[Obsolete("Use Directory")]
public string DefaultDirectory
public string? DefaultDirectory
{
get => Directory;
set => Directory = value;
}
public Task<string> ShowAsync(Window parent)
/// <summary>
/// Shows the open folder dialog.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <returns>
/// A task that on completion returns the full path of the selected directory, or null if the
/// dialog was canceled.
/// </returns>
public Task<string?> ShowAsync(Window parent)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
@ -65,14 +126,32 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Base class for system dialogs.
/// </summary>
public abstract class SystemDialog
{
public string Title { get; set; }
/// <summary>
/// Gets or sets the dialog title.
/// </summary>
public string? Title { get; set; }
}
/// <summary>
/// Represents a filter in an <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary>
public class FileDialogFilter
{
public string Name { get; set; }
/// <summary>
/// Gets or sets the name of the filter, e.g. ("Text files (.txt)").
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets a list of file extensions matched by the filter (e.g. "txt" or "*" for all
/// files).
/// </summary>
public List<string> Extensions { get; set; } = new List<string>();
}
}

6
src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs

@ -1,5 +1,7 @@
using System;
using Avalonia.Utilities;
namespace Avalonia.Controls.Templates
{
/// <summary>
@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates
/// </param>
/// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(Func<T, INameScope, IControl> build, bool supportsRecycling = false)
: base(typeof(T), CastBuild(build), supportsRecycling)
: base(o => TypeUtilities.CanCast<T>(o), CastBuild(build), supportsRecycling)
{
}
@ -63,7 +65,7 @@ namespace Avalonia.Controls.Templates
/// <returns>The weakly typed function.</returns>
private static Func<object, bool> CastMatch(Func<T, bool> f)
{
return o => (o is T) && f((T)o);
return o => TypeUtilities.CanCast<T>(o) && f((T)o);
}
/// <summary>

73
src/Avalonia.Controls/TextBox.cs

@ -145,6 +145,18 @@ namespace Avalonia.Controls
(o, v) => o.UndoLimit = v,
unsetValue: -1);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<TextBox, RoutedEventArgs>(
"CopyingToClipboard", RoutingStrategies.Bubble);
public static readonly RoutedEvent<RoutedEventArgs> CuttingToClipboardEvent =
RoutedEvent.Register<TextBox, RoutedEventArgs>(
"CuttingToClipboard", RoutingStrategies.Bubble);
public static readonly RoutedEvent<RoutedEventArgs> PastingFromClipboardEvent =
RoutedEvent.Register<TextBox, RoutedEventArgs>(
"PastingFromClipboard", RoutingStrategies.Bubble);
readonly struct UndoRedoState : IEquatable<UndoRedoState>
{
public string Text { get; }
@ -500,6 +512,24 @@ namespace Avalonia.Controls
}
}
public event EventHandler<RoutedEventArgs> CopyingToClipboard
{
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
public event EventHandler<RoutedEventArgs> CuttingToClipboard
{
add => AddHandler(CuttingToClipboardEvent, value);
remove => RemoveHandler(CuttingToClipboardEvent, value);
}
public event EventHandler<RoutedEventArgs> PastingFromClipboard
{
add => AddHandler(PastingFromClipboardEvent, value);
remove => RemoveHandler(PastingFromClipboardEvent, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
@ -638,27 +668,54 @@ namespace Avalonia.Controls
public async void Cut()
{
var text = GetSelection();
if (text is null) return;
if (string.IsNullOrEmpty(text))
{
return;
}
SnapshotUndoRedo();
Copy();
DeleteSelection();
var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
SnapshotUndoRedo();
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
DeleteSelection();
}
}
public async void Copy()
{
var text = GetSelection();
if (text is null) return;
if (string.IsNullOrEmpty(text))
{
return;
}
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
}
}
public async void Paste()
{
var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent);
RaiseEvent(eventArgs);
if (eventArgs.Handled)
{
return;
}
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync();
if (text is null) return;
if (string.IsNullOrEmpty(text))
{
return;
}
SnapshotUndoRedo();
HandleTextInput(text);

5
src/Avalonia.Input/AccessKeyHandler.cs

@ -157,10 +157,9 @@ namespace Avalonia.Input
_restoreFocusElement?.Focus();
_restoreFocusElement = null;
e.Handled = true;
}
// We always handle the Alt key.
e.Handled = true;
}
else if (_altIsDown)
{

11
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -234,7 +234,7 @@ namespace Avalonia.Input.Navigation
// Return the first visible element.
var uiElement = e as InputElement;
if (uiElement is null || uiElement.IsVisible)
if (uiElement is null || IsVisibleAndEnabled(uiElement))
{
if (e is IVisual elementAsVisual)
{
@ -245,7 +245,7 @@ namespace Avalonia.Input.Navigation
{
if (children[i] is InputElement ie)
{
if (ie.IsVisible)
if (IsVisibleAndEnabled(ie))
return ie;
else
{
@ -270,7 +270,7 @@ namespace Avalonia.Input.Navigation
// Return the last visible element.
var uiElement = e as InputElement;
if (uiElement == null || uiElement.IsVisible)
if (uiElement == null || IsVisibleAndEnabled(uiElement))
{
var elementAsVisual = e as IVisual;
@ -283,7 +283,7 @@ namespace Avalonia.Input.Navigation
{
if (children[i] is InputElement ie)
{
if (ie.IsVisible)
if (IsVisibleAndEnabled(ie))
return ie;
else
{
@ -600,7 +600,7 @@ namespace Avalonia.Input.Navigation
var vchild = children[i];
if (vchild == elementAsVisual)
break;
if (vchild.IsVisible == true && vchild is IInputElement ie)
if (vchild is IInputElement ie && IsVisibleAndEnabled(ie))
prev = ie;
}
return prev;
@ -668,5 +668,6 @@ namespace Avalonia.Input.Navigation
}
private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e);
private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled;
}
}

2
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin)
{
using (drawingContext.PushPostTransform(Matrix.CreateTranslation(origin)))
using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
{
if (GlyphRun.GlyphIndices.Length == 0)
{

1
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@ -9,7 +9,6 @@ namespace Avalonia.Markup.Xaml.Templates
{
public Type DataType { get; set; }
//we need content to be object otherwise portable.xaml is crashing
[Content]
[TemplateContent]
public object Content { get; set; }

15
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -534,11 +534,7 @@ namespace Avalonia.Skia
public void PushOpacityMask(IBrush mask, Rect bounds)
{
// TODO: This should be disposed
var paint = new SKPaint()
{
IsAntialias = true,
Style = SKPaintStyle.StrokeAndFill
};
var paint = new SKPaint();
Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(paint, mask, bounds, true));
@ -547,14 +543,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void PopOpacityMask()
{
using (var paint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.StrokeAndFill,
BlendMode = SKBlendMode.DstIn,
Color = new SKColor(0, 0, 0, 255),
ColorFilter = SKColorFilter.CreateLumaColor()
})
using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn })
{
Canvas.SaveLayer(paint);
using (var paintWrapper = _maskStack.Pop())

1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@ -12,6 +12,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\Avalonia.Win32\Avalonia.Win32.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\SharpDX.props" />

3
src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs

@ -1,4 +1,5 @@
using Avalonia.Platform;
using Avalonia.Win32;
using Avalonia.Win32.Interop;
using SharpDX;
using SharpDX.DXGI;
@ -21,7 +22,7 @@ namespace Avalonia.Direct2D1
protected override Size2F GetWindowDpi()
{
if (UnmanagedMethods.ShCoreAvailable)
if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{
uint dpix, dpiy;

1
src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs

@ -150,6 +150,7 @@ namespace Avalonia.Win32.Interop.Wpf
if (_image == null || _oldDpi.X != dpi.X || _oldDpi.Y != dpi.Y)
{
_image = new D3DImage(dpi.X, dpi.Y);
_oldDpi = dpi;
}
_impl.ImageSource = _image;

2
src/Windows/Avalonia.Win32/FramebufferManager.cs

@ -87,7 +87,7 @@ namespace Avalonia.Win32
private Vector GetCurrentDpi()
{
if (UnmanagedMethods.ShCoreAvailable)
if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{
var monitor =
UnmanagedMethods.MonitorFromWindow(_hwnd, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);

7
src/Windows/Avalonia.Win32/PlatformConstants.cs

@ -1,8 +1,13 @@
using System;
namespace Avalonia.Win32
{
static class PlatformConstants
public static class PlatformConstants
{
public const string WindowHandleType = "HWND";
public const string CursorHandleType = "HCURSOR";
public static readonly Version Windows8 = new Version(6, 2);
public static readonly Version Windows7 = new Version(6, 1);
}
}

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

@ -19,7 +19,7 @@ namespace Avalonia.Win32
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
return Task.Factory.StartNew(() =>
{
var result = Array.Empty<string>();
string[] result = default;
Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
@ -100,7 +100,7 @@ namespace Avalonia.Win32
{
return Task.Factory.StartNew(() =>
{
string result = string.Empty;
string result = default;
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog;
@ -164,7 +164,7 @@ namespace Avalonia.Win32
}
}
}
return "";
return default;
}
}
}

4
src/Windows/Avalonia.Win32/Win32GlManager.cs

@ -1,4 +1,3 @@
using System;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Angle;
using Avalonia.OpenGL.Egl;
@ -9,7 +8,6 @@ namespace Avalonia.Win32
{
static class Win32GlManager
{
private static readonly Version Windows7 = new Version(6, 1);
public static void Initialize()
{
@ -22,7 +20,7 @@ namespace Avalonia.Win32
return wgl;
}
if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > Windows7)
if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7)
{
var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay());

2
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -94,7 +94,7 @@ namespace Avalonia
namespace Avalonia.Win32
{
class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl
public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl
{
private static readonly Win32Platform s_instance = new Win32Platform();
private static Thread _uiThread;

1
src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs

@ -144,6 +144,7 @@ namespace Avalonia.Win32.WinRT.Composition
try
{
TryCreateAndRegisterCore(angle);
return;
}
catch (Exception e)
{

4
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -764,8 +764,8 @@ namespace Avalonia.Win32
RegisterTouchWindow(_hwnd, 0);
}
if (ShCoreAvailable)
{
if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{
var monitor = MonitorFromWindow(
_hwnd,
MONITOR.MONITOR_DEFAULTTONEAREST);

86
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs

@ -264,5 +264,91 @@ namespace Avalonia.Controls.UnitTests.Presenters
// InheritanceParent is exposed via StylingParent.
Assert.Same(logicalParent, ((IStyledElement)child).StylingParent);
}
[Fact]
public void Should_Create_Child_Even_With_Null_Content_When_ContentTemplate_Is_Set()
{
var target = new ContentPresenter
{
ContentTemplate = new FuncDataTemplate<object>(_ => true, (_, __) => new TextBlock
{
Text = "Hello World"
}),
Content = null
};
target.UpdateChild();
var textBlock = Assert.IsType<TextBlock>(target.Child);
Assert.Equal("Hello World", textBlock.Text);
}
[Fact]
public void Should_Not_Create_Child_Even_With_Null_Content_And_DataTemplates_InsteadOf_ContentTemplate()
{
var target = new ContentPresenter
{
DataTemplates =
{
new FuncDataTemplate<object>(_ => true, (_, __) => new TextBlock
{
Text = "Hello World"
})
},
Content = null
};
target.UpdateChild();
Assert.Null(target.Child);
}
[Fact]
public void Should_Not_Create_Child_When_Content_And_Template_Are_Null()
{
var target = new ContentPresenter
{
ContentTemplate = null,
Content = null
};
target.UpdateChild();
Assert.Null(target.Child);
}
[Fact]
public void Should_Not_Create_When_Child_Content_Is_Null_But_Expected_ValueType_With_FuncDataTemplate()
{
var target = new ContentPresenter
{
ContentTemplate = new FuncDataTemplate<int>(_ => true, (_, __) => new TextBlock
{
Text = "Hello World"
}),
Content = null
};
target.UpdateChild();
Assert.Null(target.Child);
}
[Fact]
public void Should_Create_Child_When_Content_Is_Null_And_Expected_NullableValueType_With_FuncDataTemplate()
{
var target = new ContentPresenter
{
ContentTemplate = new FuncDataTemplate<int?>(_ => true, (_, __) => new TextBlock
{
Text = "Hello World"
}),
Content = null
};
target.UpdateChild();
Assert.NotNull(target.Child);
}
}
}

1
tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs

@ -10,7 +10,6 @@ using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
using Xunit;
using System;
using Avalonia.Input.Raw;
using Factory = System.Func<int, System.Action<object>, Avalonia.Controls.Window, Avalonia.AvaloniaObject>;

3
tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs

@ -2,7 +2,6 @@
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -126,7 +125,7 @@ namespace Avalonia.Input.UnitTests
{
private readonly Action _action;
public DelegateCommand(Action action) => _action = action;
public event EventHandler CanExecuteChanged;
public event EventHandler CanExecuteChanged { add { } remove { } }
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => _action();
}

27
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs

@ -1225,5 +1225,32 @@ namespace Avalonia.Input.UnitTests
"Button2", "Button3", "Button5", "Button1", "Button6", "Button4"
}, result);
}
[Fact]
public void Cannot_Focus_Child_Of_Disabled_Control()
{
Button start;
Button expected;
var top = new StackPanel
{
[KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(start = new Button { Name = "Button1" }),
new Border
{
IsEnabled = false,
Child = new Button { Name = "Button2" },
},
(expected = new Button { Name = "Button3" }),
}
};
var current = (IInputElement)start;
var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next);
Assert.Same(expected, result);
}
}
}

2
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -923,7 +923,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public bool HasResources => true;
public List<object> RequestedResources { get; } = new List<object>();
public event EventHandler OwnerChanged;
public event EventHandler OwnerChanged { add { } remove { } }
public void AddOwner(IResourceHost owner) => Owner = owner;
public void RemoveOwner(IResourceHost owner) => Owner = null;

106
tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs

@ -0,0 +1,106 @@
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using System.Runtime.InteropServices;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
using Avalonia.Direct2D1.RenderTests;
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class TextLayoutTests : TestBase
{
public TextLayoutTests()
: base(@"Media\TextFormatting\TextLayout")
{
}
[Fact]
public async Task TextLayout_Basic()
{
// Skip test on OSX: text rendering is subtly different.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return;
var t = new TextLayout(
"Avalonia!",
new Typeface(TestFontFamily),
24,
Brushes.Black);
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = new Rect(t.Size);
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
c.DrawRectangle(Brushes.Yellow, null, rect);
t.Draw(c, rect.Position);
}),
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task TextLayout_Rotated()
{
// Skip test on OSX: text rendering is subtly different.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return;
var t = new TextLayout(
"Avalonia!",
new Typeface(TestFontFamily),
24,
Brushes.Black);
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new DrawnControl(c =>
{
var textRect = new Rect(t.Size);
var bounds = new Rect(0, 0, 200, 200);
var rect = bounds.CenterRect(textRect);
var rotate = Matrix.CreateTranslation(-100, -100) *
Matrix.CreateRotation(MathUtilities.Deg2Rad(90)) *
Matrix.CreateTranslation(100, 100);
using var transform = c.PushPreTransform(rotate);
c.DrawRectangle(Brushes.Yellow, null, rect);
t.Draw(c, rect.Position);
}),
};
await RenderToFile(target);
CompareImages();
}
private class DrawnControl : Control
{
private readonly Action<DrawingContext> _render;
public DrawnControl(Action<DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(context);
}
}
}

BIN
tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Loading…
Cancel
Save