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" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ComboBoxPage" 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"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">ComboBox</TextBlock> <TextBlock Classes="h1">ComboBox</TextBlock>
<TextBlock Classes="h2">A drop-down list.</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"> <ComboBox PlaceholderText="Pick an Item">
<ComboBoxItem>Inline Items</ComboBoxItem> <ComboBoxItem>Inline Items</ComboBoxItem>
<ComboBoxItem>Inline Item 2</ComboBoxItem> <ComboBoxItem>Inline Item 2</ComboBoxItem>
@ -14,6 +22,24 @@
<ComboBoxItem>Inline Item 4</ComboBoxItem> <ComboBoxItem>Inline Item 4</ComboBoxItem>
</ComboBox> </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"> <ComboBox SelectedIndex="0">
<ComboBoxItem> <ComboBoxItem>
<Panel> <Panel>
@ -46,7 +72,7 @@
<sys:Exception /> <sys:Exception />
</DataValidationErrors.Error> </DataValidationErrors.Error>
</ComboBox> </ComboBox>
</StackPanel> </WrapPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

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

@ -280,8 +280,8 @@ namespace Avalonia.Collections
/// <summary> /// <summary>
/// Gets a range of items from the collection. /// Gets a range of items from the collection.
/// </summary> /// </summary>
/// <param name="index">The first index 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 items to remove.</param> /// <param name="count">The number of elements in the range.</param>
public IEnumerable<T> GetRange(int index, int count) public IEnumerable<T> GetRange(int index, int count)
{ {
return _inner.GetRange(index, count); return _inner.GetRange(index, count);
@ -455,7 +455,7 @@ namespace Avalonia.Collections
} }
/// <summary> /// <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> /// </summary>
/// <param name="capacity">The capacity.</param> /// <param name="capacity">The capacity.</param>
public void EnsureCapacity(int capacity) public void EnsureCapacity(int capacity)

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

@ -93,13 +93,25 @@ namespace Avalonia.Utilities
return !type.IsValueType || IsNullableType(type); 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> /// <summary>
/// Try to convert a value to a type by any means possible. /// Try to convert a value to a type by any means possible.
/// </summary> /// </summary>
/// <param name="to">The type to cast to.</param> /// <param name="to">The type to convert to.</param>
/// <param name="value">The value to cast.</param> /// <param name="value">The value to convert.</param>
/// <param name="culture">The culture to use.</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> /// <returns>True if the cast was successful, otherwise false.</returns>
public static bool TryConvert(Type to, object value, CultureInfo culture, out object result) 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# /// Try to convert a value to a type using the implicit conversions allowed by the C#
/// language. /// language.
/// </summary> /// </summary>
/// <param name="to">The type to cast to.</param> /// <param name="to">The type to convert to.</param>
/// <param name="value">The value to cast.</param> /// <param name="value">The value to convert.</param>
/// <param name="result">If successful, contains the cast value.</param> /// <param name="result">If successful, contains the converted value.</param>
/// <returns>True if the cast was successful, otherwise false.</returns> /// <returns>True if the convert was successful, otherwise false.</returns>
public static bool TryConvertImplicit(Type to, object value, out object result) public static bool TryConvertImplicit(Type to, object value, out object result)
{ {
if (value == null) 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 /// Convert a value to a type by any means possible, returning the default for that type
/// if the value could not be converted. /// if the value could not be converted.
/// </summary> /// </summary>
/// <param name="value">The value to cast.</param> /// <param name="value">The value to convert.</param>
/// <param name="type">The type to cast to..</param> /// <param name="type">The type to convert to..</param>
/// <param name="culture">The culture to use.</param> /// <param name="culture">The culture to use.</param>
/// <returns>A value of <paramref name="type"/>.</returns> /// <returns>A value of <paramref name="type"/>.</returns>
public static object ConvertOrDefault(object value, Type type, CultureInfo culture) 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 /// 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. /// return the default for the type if the value could not be converted.
/// </summary> /// </summary>
/// <param name="value">The value to cast.</param> /// <param name="value">The value to convert.</param>
/// <param name="type">The type to cast to..</param> /// <param name="type">The type to convert to.</param>
/// <returns>A value of <paramref name="type"/>.</returns> /// <returns>A value of <paramref name="type"/>.</returns>
public static object ConvertImplicitOrDefault(object value, Type type) 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) if (key == null)
key = item; key = item;
if (_valueConverter != null) var valueConverter = ValueConverter;
key = _valueConverter.Convert(key, typeof(object), level, culture); if (valueConverter != null)
key = valueConverter.Convert(key, typeof(object), level, culture);
return key; return key;
} }
@ -99,6 +100,8 @@ namespace Avalonia.Collections
} }
public override string PropertyName => _propertyPath; public override string PropertyName => _propertyPath;
public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; }
private Type GetPropertyType(object o) private Type GetPropertyType(object o)
{ {
return o.GetType().GetNestedPropertyType(_propertyPath); return o.GetType().GetNestedPropertyType(_propertyPath);

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

@ -1,10 +1,8 @@
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Reactive; using Avalonia.Reactive;
using System; using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text;
namespace Avalonia.Controls.Utils namespace Avalonia.Controls.Utils
{ {
@ -67,11 +65,14 @@ namespace Avalonia.Controls.Utils
private void SetSourceValue(object value) 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) 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;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
@ -80,7 +82,7 @@ namespace Avalonia.Controls
private bool _isDropDownOpen; private bool _isDropDownOpen;
private Popup _popup; private Popup _popup;
private object _selectionBoxItem; private object _selectionBoxItem;
private IDisposable _subscriptionsOnOpen; private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable();
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ComboBox"/> class. /// Initializes static members of the <see cref="ComboBox"/> class.
@ -291,6 +293,7 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup"); _popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened; _popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
} }
internal void ItemFocused(ComboBoxItem dropDownItem) internal void ItemFocused(ComboBoxItem dropDownItem)
@ -303,8 +306,7 @@ namespace Avalonia.Controls
private void PopupClosed(object sender, EventArgs e) private void PopupClosed(object sender, EventArgs e)
{ {
_subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen.Clear();
_subscriptionsOnOpen = null;
if (CanFocus(this)) if (CanFocus(this))
{ {
@ -316,20 +318,34 @@ namespace Avalonia.Controls
{ {
TryFocusSelectedItem(); TryFocusSelectedItem();
_subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen.Clear();
_subscriptionsOnOpen = null;
var toplevel = this.GetVisualRoot() as TopLevel; var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null) 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 //eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
{ {
ev.Handled = true; 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 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) 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 oldChild = Child;
var newChild = content as IControl; 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) ?? var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ??
( (

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

@ -2,6 +2,7 @@ using System;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
@ -393,18 +394,8 @@ namespace Avalonia.Controls.Primitives
var handlerCleanup = new CompositeDisposable(5); var handlerCleanup = new CompositeDisposable(5);
void DeferCleanup(IDisposable? disposable) popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
{ HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup);
if (disposable is null)
{
return;
}
handlerCleanup.Add(disposable);
}
DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
popupHost.SetChild(Child); popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this); ((ISetLogicalParent)popupHost).SetParent(this);
@ -418,19 +409,19 @@ namespace Avalonia.Controls.Primitives
PlacementConstraintAdjustment, PlacementConstraintAdjustment,
PlacementRect); 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)); (x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup);
if (topLevel is Window window) 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)); (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)); (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
} }
else else
{ {
@ -438,13 +429,13 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup) 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)); (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 => var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
{ {
@ -466,17 +457,17 @@ namespace Avalonia.Controls.Primitives
dismissLayer.IsVisible = true; dismissLayer.IsVisible = true;
dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement;
DeferCleanup(Disposable.Create(() => Disposable.Create(() =>
{ {
dismissLayer.IsVisible = false; dismissLayer.IsVisible = false;
dismissLayer.InputPassThroughElement = null; dismissLayer.InputPassThroughElement = null;
})); }).DisposeWith(handlerCleanup);
DeferCleanup(SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>( SubscribeToEventHandler<LightDismissOverlayLayer, EventHandler<PointerPressedEventArgs>>(
dismissLayer, dismissLayer,
PointerPressedDismissOverlay, PointerPressedDismissOverlay,
(x, handler) => x.PointerPressed += handler, (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 System.Threading.Tasks;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary>
/// Base class for system file dialogs.
/// </summary>
public abstract class FileDialog : FileSystemDialog 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 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 public abstract class FileSystemDialog : SystemDialog
{ {
[Obsolete("Use Directory")] [Obsolete("Use Directory")]
public string InitialDirectory public string? InitialDirectory
{ {
get => Directory; get => Directory;
set => Directory = value; 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 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) if(parent == null)
throw new ArgumentNullException(nameof(parent)); 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 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 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) if(parent == null)
throw new ArgumentNullException(nameof(parent)); 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 public class OpenFolderDialog : FileSystemDialog
{ {
[Obsolete("Use Directory")] [Obsolete("Use Directory")]
public string DefaultDirectory public string? DefaultDirectory
{ {
get => Directory; get => Directory;
set => Directory = value; 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) if(parent == null)
throw new ArgumentNullException(nameof(parent)); throw new ArgumentNullException(nameof(parent));
@ -65,14 +126,32 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Base class for system dialogs.
/// </summary>
public abstract class SystemDialog 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 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>(); public List<string> Extensions { get; set; } = new List<string>();
} }
} }

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

@ -1,5 +1,7 @@
using System; using System;
using Avalonia.Utilities;
namespace Avalonia.Controls.Templates namespace Avalonia.Controls.Templates
{ {
/// <summary> /// <summary>
@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates
/// </param> /// </param>
/// <param name="supportsRecycling">Whether the control can be recycled.</param> /// <param name="supportsRecycling">Whether the control can be recycled.</param>
public FuncDataTemplate(Func<T, INameScope, IControl> build, bool supportsRecycling = false) 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> /// <returns>The weakly typed function.</returns>
private static Func<object, bool> CastMatch(Func<T, bool> f) 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> /// <summary>

73
src/Avalonia.Controls/TextBox.cs

@ -145,6 +145,18 @@ namespace Avalonia.Controls
(o, v) => o.UndoLimit = v, (o, v) => o.UndoLimit = v,
unsetValue: -1); 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> readonly struct UndoRedoState : IEquatable<UndoRedoState>
{ {
public string Text { get; } 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) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
@ -638,27 +668,54 @@ namespace Avalonia.Controls
public async void Cut() public async void Cut()
{ {
var text = GetSelection(); var text = GetSelection();
if (text is null) return; if (string.IsNullOrEmpty(text))
{
return;
}
SnapshotUndoRedo(); var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent);
Copy(); RaiseEvent(eventArgs);
DeleteSelection(); if (!eventArgs.Handled)
{
SnapshotUndoRedo();
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
DeleteSelection();
}
} }
public async void Copy() public async void Copy()
{ {
var text = GetSelection(); var text = GetSelection();
if (text is null) return; if (string.IsNullOrEmpty(text))
{
return;
}
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
.SetTextAsync(text); RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
}
} }
public async void Paste() 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(); var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync();
if (text is null) return; if (string.IsNullOrEmpty(text))
{
return;
}
SnapshotUndoRedo(); SnapshotUndoRedo();
HandleTextInput(text); HandleTextInput(text);

5
src/Avalonia.Input/AccessKeyHandler.cs

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

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

@ -234,7 +234,7 @@ namespace Avalonia.Input.Navigation
// Return the first visible element. // Return the first visible element.
var uiElement = e as InputElement; var uiElement = e as InputElement;
if (uiElement is null || uiElement.IsVisible) if (uiElement is null || IsVisibleAndEnabled(uiElement))
{ {
if (e is IVisual elementAsVisual) if (e is IVisual elementAsVisual)
{ {
@ -245,7 +245,7 @@ namespace Avalonia.Input.Navigation
{ {
if (children[i] is InputElement ie) if (children[i] is InputElement ie)
{ {
if (ie.IsVisible) if (IsVisibleAndEnabled(ie))
return ie; return ie;
else else
{ {
@ -270,7 +270,7 @@ namespace Avalonia.Input.Navigation
// Return the last visible element. // Return the last visible element.
var uiElement = e as InputElement; var uiElement = e as InputElement;
if (uiElement == null || uiElement.IsVisible) if (uiElement == null || IsVisibleAndEnabled(uiElement))
{ {
var elementAsVisual = e as IVisual; var elementAsVisual = e as IVisual;
@ -283,7 +283,7 @@ namespace Avalonia.Input.Navigation
{ {
if (children[i] is InputElement ie) if (children[i] is InputElement ie)
{ {
if (ie.IsVisible) if (IsVisibleAndEnabled(ie))
return ie; return ie;
else else
{ {
@ -600,7 +600,7 @@ namespace Avalonia.Input.Navigation
var vchild = children[i]; var vchild = children[i];
if (vchild == elementAsVisual) if (vchild == elementAsVisual)
break; break;
if (vchild.IsVisible == true && vchild is IInputElement ie) if (vchild is IInputElement ie && IsVisibleAndEnabled(ie))
prev = ie; prev = ie;
} }
return prev; return prev;
@ -668,5 +668,6 @@ namespace Avalonia.Input.Navigation
} }
private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); 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/> /// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin) 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) 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; } public Type DataType { get; set; }
//we need content to be object otherwise portable.xaml is crashing
[Content] [Content]
[TemplateContent] [TemplateContent]
public object Content { get; set; } 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) public void PushOpacityMask(IBrush mask, Rect bounds)
{ {
// TODO: This should be disposed // TODO: This should be disposed
var paint = new SKPaint() var paint = new SKPaint();
{
IsAntialias = true,
Style = SKPaintStyle.StrokeAndFill
};
Canvas.SaveLayer(paint); Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(paint, mask, bounds, true)); _maskStack.Push(CreatePaint(paint, mask, bounds, true));
@ -547,14 +543,7 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void PopOpacityMask() public void PopOpacityMask()
{ {
using (var paint = new SKPaint using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn })
{
IsAntialias = true,
Style = SKPaintStyle.StrokeAndFill,
BlendMode = SKBlendMode.DstIn,
Color = new SKColor(0, 0, 0, 255),
ColorFilter = SKColorFilter.CreateLumaColor()
})
{ {
Canvas.SaveLayer(paint); Canvas.SaveLayer(paint);
using (var paintWrapper = _maskStack.Pop()) using (var paintWrapper = _maskStack.Pop())

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

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

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

@ -1,4 +1,5 @@
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Win32;
using Avalonia.Win32.Interop; using Avalonia.Win32.Interop;
using SharpDX; using SharpDX;
using SharpDX.DXGI; using SharpDX.DXGI;
@ -21,7 +22,7 @@ namespace Avalonia.Direct2D1
protected override Size2F GetWindowDpi() protected override Size2F GetWindowDpi()
{ {
if (UnmanagedMethods.ShCoreAvailable) if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{ {
uint dpix, dpiy; 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) if (_image == null || _oldDpi.X != dpi.X || _oldDpi.Y != dpi.Y)
{ {
_image = new D3DImage(dpi.X, dpi.Y); _image = new D3DImage(dpi.X, dpi.Y);
_oldDpi = dpi;
} }
_impl.ImageSource = _image; _impl.ImageSource = _image;

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

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

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

@ -1,8 +1,13 @@
using System;
namespace Avalonia.Win32 namespace Avalonia.Win32
{ {
static class PlatformConstants public static class PlatformConstants
{ {
public const string WindowHandleType = "HWND"; public const string WindowHandleType = "HWND";
public const string CursorHandleType = "HCURSOR"; 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; var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
return Task.Factory.StartNew(() => return Task.Factory.StartNew(() =>
{ {
var result = Array.Empty<string>(); string[] result = default;
Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
Guid iid = UnmanagedMethods.ShellIds.IFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
@ -100,7 +100,7 @@ namespace Avalonia.Win32
{ {
return Task.Factory.StartNew(() => return Task.Factory.StartNew(() =>
{ {
string result = string.Empty; string result = default;
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; 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;
using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Angle;
using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Egl;
@ -9,7 +8,6 @@ namespace Avalonia.Win32
{ {
static class Win32GlManager static class Win32GlManager
{ {
private static readonly Version Windows7 = new Version(6, 1);
public static void Initialize() public static void Initialize()
{ {
@ -22,7 +20,7 @@ namespace Avalonia.Win32
return wgl; return wgl;
} }
if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > Windows7) if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7)
{ {
var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay());

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

@ -94,7 +94,7 @@ namespace Avalonia
namespace Avalonia.Win32 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 readonly Win32Platform s_instance = new Win32Platform();
private static Thread _uiThread; private static Thread _uiThread;

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

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

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

@ -764,8 +764,8 @@ namespace Avalonia.Win32
RegisterTouchWindow(_hwnd, 0); RegisterTouchWindow(_hwnd, 0);
} }
if (ShCoreAvailable) if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8)
{ {
var monitor = MonitorFromWindow( var monitor = MonitorFromWindow(
_hwnd, _hwnd,
MONITOR.MONITOR_DEFAULTTONEAREST); 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. // InheritanceParent is exposed via StylingParent.
Assert.Same(logicalParent, ((IStyledElement)child).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 Avalonia.UnitTests;
using Moq; using Moq;
using Xunit; using Xunit;
using System;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Factory = System.Func<int, System.Action<object>, Avalonia.Controls.Window, Avalonia.AvaloniaObject>; 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 System.Windows.Input;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq; using Moq;
using Xunit; using Xunit;
@ -126,7 +125,7 @@ namespace Avalonia.Input.UnitTests
{ {
private readonly Action _action; private readonly Action _action;
public DelegateCommand(Action action) => _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 bool CanExecute(object parameter) => true;
public void Execute(object parameter) => _action(); 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" "Button2", "Button3", "Button5", "Button1", "Button6", "Button4"
}, result); }, 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 bool HasResources => true;
public List<object> RequestedResources { get; } = new List<object>(); 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 AddOwner(IResourceHost owner) => Owner = owner;
public void RemoveOwner(IResourceHost owner) => Owner = null; 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