Browse Source

Merge branch 'master' into feature/fluent-datagrid

pull/4534/head
Max Katz 6 years ago
committed by GitHub
parent
commit
12e3d938d3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      samples/BindingDemo/MainWindow.xaml
  2. 12
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  3. 41
      src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs
  4. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  5. 230
      src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
  6. 2
      src/Avalonia.Base/Metadata/DependsOnAttribute.cs
  7. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  8. 2
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  9. 2
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  10. 2
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  11. 17
      src/Avalonia.Controls/AppBuilderBase.cs
  12. 126
      src/Avalonia.Controls/TextBox.cs
  13. 2
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  14. 16
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  15. 7
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  16. 9
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  17. 71
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  18. 5
      src/Avalonia.Native/Avalonia.Native.csproj
  19. 17
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  20. 4
      src/Avalonia.Native/WindowImplBase.cs
  21. 2
      src/Avalonia.Themes.Default/Expander.xaml
  22. 9
      src/Avalonia.Themes.Default/TextBox.xaml
  23. 4
      src/Avalonia.Themes.Fluent/Expander.xaml
  24. 6
      src/Avalonia.Themes.Fluent/TextBox.xaml
  25. 4
      src/Avalonia.Visuals/Media/GlyphRun.cs
  26. 16
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  27. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  28. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  29. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  30. 41
      tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs
  31. 35
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  32. 26
      tests/Avalonia.LeakTests/ControlTests.cs
  33. 144
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs
  34. 60
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

1
samples/BindingDemo/MainWindow.xaml

@ -116,6 +116,7 @@
<RadioButton Content="Radio Button" IsChecked="{Binding !!BooleanFlag, Mode=OneWay}" Command="{Binding StringValueCommand}" CommandParameter="RadioButton"/>
<TextBox Text="{Binding Path=StringValue}"/>
<Button Content="Nested View Model Button" Name="NestedTest" Command="{Binding NestedModel.Command}" />
<Button Content="Command Method Do" Command="{Binding Do}" x:Name="ToDo"/>
</StackPanel>
</TabItem>
</TabControl>

12
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System.Threading;
using ReactiveUI;
using Avalonia.Controls;
using Avalonia.Metadata;
namespace BindingDemo.ViewModels
{
@ -102,5 +103,16 @@ namespace BindingDemo.ViewModels
get { return _nested; }
private set { this.RaiseAndSetIfChanged(ref _nested, value); }
}
public void Do(object parameter)
{
}
[DependsOn(nameof(BooleanFlag))]
bool CanDo(object parameter)
{
return BooleanFlag;
}
}
}

41
src/Avalonia.Base/Data/Converters/AlwaysEnabledDelegateCommand.cs

@ -1,41 +0,0 @@
using System;
using System.Globalization;
using System.Reflection;
using System.Windows.Input;
using Avalonia.Utilities;
namespace Avalonia.Data.Converters
{
class AlwaysEnabledDelegateCommand : ICommand
{
private readonly Delegate action;
private ParameterInfo parameterInfo;
public AlwaysEnabledDelegateCommand(Delegate action)
{
this.action = action;
var parameters = action.Method.GetParameters();
parameterInfo = parameters.Length == 0 ? null : parameters[0];
}
#pragma warning disable 0067
public event EventHandler CanExecuteChanged;
#pragma warning restore 0067
public bool CanExecute(object parameter) => true;
public void Execute(object parameter)
{
if (parameterInfo == null)
{
action.DynamicInvoke();
}
else
{
TypeUtilities.TryConvert(parameterInfo.ParameterType, parameter, CultureInfo.CurrentCulture, out object convertedParameter);
action.DynamicInvoke(convertedParameter);
}
}
}
}

2
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -33,7 +33,7 @@ namespace Avalonia.Data.Converters
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)
{
return new AlwaysEnabledDelegateCommand(d);
return new MethodToCommandConverter(d);
}
if (TypeUtilities.TryConvert(targetType, value, culture, out object result))

230
src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs

@ -0,0 +1,230 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Input;
using Avalonia.Utilities;
namespace Avalonia.Data.Converters
{
class MethodToCommandConverter : ICommand
{
readonly static Func<object, bool> AlwaysEnabled = (_) => true;
readonly static MethodInfo tryConvert = typeof(TypeUtilities)
.GetMethod(nameof(TypeUtilities.TryConvert), BindingFlags.Public | BindingFlags.Static);
readonly static PropertyInfo currentCulture = typeof(CultureInfo)
.GetProperty(nameof(CultureInfo.CurrentCulture), BindingFlags.Public | BindingFlags.Static);
readonly Func<object, bool> canExecute;
readonly Action<object> execute;
readonly WeakPropertyChangedProxy weakPropertyChanged;
readonly PropertyChangedEventHandler propertyChangedEventHandler;
readonly string[] dependencyProperties;
public MethodToCommandConverter(Delegate action)
{
var target = action.Target;
var canExecuteMethodName = "Can" + action.Method.Name;
var parameters = action.Method.GetParameters();
var parameterInfo = parameters.Length == 0 ? null : parameters[0].ParameterType;
if (parameterInfo == null)
{
execute = CreateExecute(target, action.Method);
}
else
{
execute = CreateExecute(target, action.Method, parameterInfo);
}
var canExecuteMethod = action.Method.DeclaringType.GetRuntimeMethods()
.FirstOrDefault(m => m.Name == canExecuteMethodName
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(object));
if (canExecuteMethod == null)
{
canExecute = AlwaysEnabled;
}
else
{
canExecute = CreateCanExecute(target, canExecuteMethod);
dependencyProperties = canExecuteMethod
.GetCustomAttributes(typeof(Metadata.DependsOnAttribute), true)
.OfType<Metadata.DependsOnAttribute>()
.Select(x => x.Name)
.ToArray();
if (dependencyProperties.Any()
&& target is INotifyPropertyChanged inpc)
{
propertyChangedEventHandler = OnPropertyChanged;
weakPropertyChanged = new WeakPropertyChangedProxy(inpc, propertyChangedEventHandler);
}
}
}
void OnPropertyChanged(object sender,PropertyChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.PropertyName)
|| dependencyProperties?.Contains(args.PropertyName) == true)
{
Threading.Dispatcher.UIThread.Post(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty)
, Threading.DispatcherPriority.Input);
}
}
#pragma warning disable 0067
public event EventHandler CanExecuteChanged;
#pragma warning restore 0067
public bool CanExecute(object parameter) => canExecute(parameter);
public void Execute(object parameter) => execute(parameter);
static Action<object> CreateExecute(object target
, System.Reflection.MethodInfo method)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
var instance = Expression.Convert
(
Expression.Constant(target),
method.DeclaringType
);
var call = Expression.Call
(
instance,
method
);
return Expression
.Lambda<Action<object>>(call, parameter)
.Compile();
}
static Action<object> CreateExecute(object target
, System.Reflection.MethodInfo method
, Type parameterType)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
var instance = Expression.Convert
(
Expression.Constant(target),
method.DeclaringType
);
Expression body;
if (parameterType == typeof(object))
{
body = Expression.Call(instance,
method,
parameter
);
}
else
{
var arg0 = Expression.Variable(typeof(object), "argX");
var convertCall = Expression.Call(tryConvert,
Expression.Constant(parameterType),
parameter,
Expression.Property(null, currentCulture),
arg0
);
var call = Expression.Call(instance,
method,
Expression.Convert(arg0, parameterType)
);
body = Expression.Block(new[] { arg0 },
convertCall,
call
);
}
Action<object> action = null;
try
{
action = Expression
.Lambda<Action<object>>(body, parameter)
.Compile();
}
catch (Exception ex)
{
throw ex;
}
return action;
}
static Func<object, bool> CreateCanExecute(object target
, System.Reflection.MethodInfo method)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
var instance = Expression.Convert
(
Expression.Constant(target),
method.DeclaringType
);
var call = Expression.Call
(
instance,
method,
parameter
);
return Expression
.Lambda<Func<object, bool>>(call, parameter)
.Compile();
}
internal class WeakPropertyChangedProxy
{
readonly WeakReference<PropertyChangedEventHandler> _listener = new WeakReference<PropertyChangedEventHandler>(null);
readonly PropertyChangedEventHandler _handler;
internal WeakReference<INotifyPropertyChanged> Source { get; } = new WeakReference<INotifyPropertyChanged>(null);
public WeakPropertyChangedProxy()
{
_handler = new PropertyChangedEventHandler(OnPropertyChanged);
}
public WeakPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler listener) : this()
{
SubscribeTo(source, listener);
}
public void SubscribeTo(INotifyPropertyChanged source, PropertyChangedEventHandler listener)
{
source.PropertyChanged += _handler;
Source.SetTarget(source);
_listener.SetTarget(listener);
}
public void Unsubscribe()
{
if (Source.TryGetTarget(out INotifyPropertyChanged source) && source != null)
source.PropertyChanged -= _handler;
Source.SetTarget(null);
_listener.SetTarget(null);
}
void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_listener.TryGetTarget(out var handler) && handler != null)
handler(sender, e);
else
Unsubscribe();
}
}
}
}

2
src/Avalonia.Base/Metadata/DependsOnAttribute.cs

@ -5,7 +5,7 @@ namespace Avalonia.Metadata
/// <summary>
/// Indicates that the property depends on the value of another property in markup.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
{
/// <summary>

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -9,3 +9,4 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]

2
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -458,7 +458,7 @@ namespace Avalonia.Controls
private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (OwningColumn == null || e.Handled || !IsEnabled || e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (OwningColumn == null || e.Handled || !IsEnabled || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

2
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -789,7 +789,7 @@ namespace Avalonia.Controls
private void DataGridRow_PointerPressed(PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

2
src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs

@ -177,7 +177,7 @@ namespace Avalonia.Controls.Primitives
//TODO TabStop
private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

17
src/Avalonia.Controls/AppBuilderBase.cs

@ -88,6 +88,23 @@ namespace Avalonia.Controls
};
}
/// <summary>
/// Begin configuring an <see cref="Application"/>.
/// </summary>
/// <param name="appFactory">Factory function for <typeparamref name="TApp"/>.</param>
/// <typeparam name="TApp">The subclass of <see cref="Application"/> to configure.</typeparam>
/// <remarks><paramref name="appFactory"/> is useful for passing of dependencies to <typeparamref name="TApp"/>.</remarks>
/// <returns>An <typeparamref name="TAppBuilder"/> instance.</returns>
public static TAppBuilder Configure<TApp>(Func<TApp> appFactory)
where TApp : Application
{
return new TAppBuilder()
{
ApplicationType = typeof(TApp),
_appFactory = appFactory
};
}
protected TAppBuilder Self => (TAppBuilder)this;
public TAppBuilder AfterSetup(Action<TAppBuilder> callback)

126
src/Avalonia.Controls/TextBox.cs

@ -18,6 +18,15 @@ namespace Avalonia.Controls
{
public class TextBox : TemplatedControl, UndoRedoHelper<TextBox.UndoRedoState>.IUndoRedoHost
{
public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Cut.FirstOrDefault();
public static KeyGesture CopyGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Copy.FirstOrDefault();
public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Paste.FirstOrDefault();
public static readonly StyledProperty<bool> AcceptsReturnProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsReturn));
@ -103,6 +112,21 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut),
o => o.CanCut);
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCopy),
o => o.CanCopy);
public static readonly DirectProperty<TextBox, bool> CanPasteProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanPaste),
o => o.CanPaste);
struct UndoRedoState : IEquatable<UndoRedoState>
{
@ -126,6 +150,9 @@ namespace Avalonia.Controls
private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
private bool _isUndoingRedoing;
private bool _ignoreTextChanges;
private bool _canCut;
private bool _canCopy;
private bool _canPaste;
private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
@ -369,6 +396,41 @@ namespace Avalonia.Controls
get { return _newLine; }
set { SetAndRaise(NewLineProperty, ref _newLine, value); }
}
/// <summary>
/// Clears the current selection, maintaining the <see cref="CaretIndex"/>
/// </summary>
public void ClearSelection()
{
SelectionStart = SelectionEnd = CaretIndex;
}
/// <summary>
/// Property for determining if the Cut command can be executed.
/// </summary>
public bool CanCut
{
get { return _canCut; }
private set { SetAndRaise(CanCutProperty, ref _canCut, value); }
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
public bool CanCopy
{
get { return _canCopy; }
private set { SetAndRaise(CanCopyProperty, ref _canCopy, value); }
}
/// <summary>
/// Property for determining if the Paste command can be executed.
/// </summary>
public bool CanPaste
{
get { return _canPaste; }
private set { SetAndRaise(CanPasteProperty, ref _canPaste, value); }
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
@ -387,9 +449,19 @@ namespace Avalonia.Controls
if (change.Property == TextProperty)
{
UpdatePseudoclasses();
UpdateCommandStates();
}
}
private void UpdateCommandStates()
{
var text = GetSelection();
var isSelectionNullOrEmpty = string.IsNullOrEmpty(text);
CanCopy = !IsPasswordBox && !isSelectionNullOrEmpty;
CanCut = !IsPasswordBox && !isSelectionNullOrEmpty && !IsReadOnly;
CanPaste = !IsReadOnly;
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
@ -404,6 +476,8 @@ namespace Avalonia.Controls
SelectAll();
}
UpdateCommandStates();
_presenter?.ShowCaret();
}
@ -413,11 +487,12 @@ namespace Avalonia.Controls
if (ContextMenu == null || !ContextMenu.IsOpen)
{
SelectionStart = 0;
SelectionEnd = 0;
ClearSelection();
RevealPassword = false;
}
UpdateCommandStates();
_presenter?.HideCaret();
}
@ -444,7 +519,7 @@ namespace Avalonia.Controls
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
SelectionStart = SelectionEnd = CaretIndex;
ClearSelection();
_undoRedoHelper.DiscardRedo();
}
}
@ -460,19 +535,31 @@ namespace Avalonia.Controls
return text;
}
private async void Copy()
public async void Cut()
{
var text = GetSelection();
if (text is null) return;
_undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
}
public async void Copy()
{
var text = GetSelection();
if (text is null) return;
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(GetSelection());
.SetTextAsync(text);
}
private async void Paste()
public async void Paste()
{
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync();
if (text == null)
{
return;
}
if (text is null) return;
_undoRedoHelper.Snapshot();
HandleTextInput(text);
@ -511,23 +598,18 @@ namespace Avalonia.Controls
{
if (!IsPasswordBox)
{
_undoRedoHelper.Snapshot();
Copy();
DeleteSelection();
_undoRedoHelper.Snapshot();
Cut();
}
handled = true;
}
else if (Match(keymap.Paste))
{
Paste();
handled = true;
}
else if (Match(keymap.Undo))
{
try
{
_isUndoingRedoing = true;
@ -662,7 +744,7 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex - removedCharacters) +
text.Substring(caretIndex));
CaretIndex -= removedCharacters;
SelectionStart = SelectionEnd = CaretIndex;
ClearSelection();
}
_undoRedoHelper.Snapshot();
@ -735,7 +817,7 @@ namespace Avalonia.Controls
}
else if (movement)
{
SelectionStart = SelectionEnd = CaretIndex;
ClearSelection();
}
if (handled || movement)
@ -1042,7 +1124,8 @@ namespace Avalonia.Controls
var end = Math.Max(selectionStart, selectionEnd);
var text = Text;
SetTextInternal(text.Substring(0, start) + text.Substring(end));
SelectionStart = SelectionEnd = CaretIndex = start;
CaretIndex = start;
ClearSelection();
return true;
}
else
@ -1131,7 +1214,8 @@ namespace Avalonia.Controls
set
{
Text = value.Text;
SelectionStart = SelectionEnd = CaretIndex = value.CaretPosition;
CaretIndex = value.CaretPosition;
ClearSelection();
}
}
}

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

@ -141,7 +141,7 @@ namespace Avalonia.Controls.Utils
var radiusY = keypoints.RightTop.Y - boundRect.TopRight.Y;
if (radiusX != 0 || radiusY != 0)
{
context.ArcTo(keypoints.RightTop, new Size(radiusY, radiusY), 0, false, SweepDirection.Clockwise);
context.ArcTo(keypoints.RightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise);
}
// Right

16
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -18,12 +18,13 @@ namespace Avalonia.Diagnostics.ViewModels
private int _selectedTab;
private string _focusedControl;
private string _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
public MainViewModel(IControl root)
{
_root = root;
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
_events = new EventsPageViewModel(root);
UpdateFocusedControl();
@ -34,6 +35,17 @@ namespace Avalonia.Diagnostics.ViewModels
Console = new ConsoleViewModel(UpdateConsoleContext);
}
public bool ShouldVisualizeMarginPadding
{
get => _shouldVisualizeMarginPadding;
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
}
public void ToggleVisualizeMarginPadding()
{
ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
}
public ConsoleViewModel Console { get; }
public ViewModelBase Content

7
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@ -10,8 +10,9 @@ namespace Avalonia.Diagnostics.ViewModels
private ControlDetailsViewModel _details;
private string _propertyFilter;
public TreePageViewModel(TreeNode[] nodes)
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{
MainView = mainView;
Nodes = nodes;
Selection = new SelectionModel
{
@ -23,7 +24,9 @@ namespace Avalonia.Diagnostics.ViewModels
{
SelectedNode = (TreeNode)Selection.SelectedItem;
};
}
}
public MainViewModel MainView { get; }
public TreeNode[] Nodes { get; protected set; }

9
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -16,6 +16,15 @@
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Options">
<MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeMarginPadding}"
IsEnabled="False"/>
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">

71
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -1,7 +1,7 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
@ -11,45 +11,78 @@ namespace Avalonia.Diagnostics.Views
{
internal class TreePageView : UserControl
{
private Control _adorner;
private readonly Panel _adorner;
private AdornerLayer _currentLayer;
private TreeView _tree;
public TreePageView()
{
this.InitializeComponent();
InitializeComponent();
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
_adorner = new Panel
{
ClipToBounds = false,
Children =
{
//Padding frame
new Border { BorderBrush = new SolidColorBrush(Colors.Green, 0.5) },
//Content frame
new Border { Background = new SolidColorBrush(Color.FromRgb(160, 197, 232), 0.5) },
//Margin frame
new Border { BorderBrush = new SolidColorBrush(Colors.Yellow, 0.5) }
},
};
}
protected void AddAdorner(object sender, PointerEventArgs e)
{
var node = (TreeNode)((Control)sender).DataContext;
var layer = AdornerLayer.GetAdornerLayer(node.Visual);
var visual = (Visual)node.Visual;
_currentLayer = AdornerLayer.GetAdornerLayer(visual);
if (layer != null)
if (_currentLayer == null ||
_currentLayer.Children.Contains(_adorner))
{
if (_adorner != null)
{
((Panel)_adorner.Parent).Children.Remove(_adorner);
_adorner = null;
}
return;
}
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
[AdornerLayer.AdornedElementProperty] = node.Visual,
};
_currentLayer.Children.Add(_adorner);
AdornerLayer.SetAdornedElement(_adorner, visual);
var vm = (TreePageViewModel) DataContext;
layer.Children.Add(_adorner);
if (vm.MainView.ShouldVisualizeMarginPadding)
{
var paddingBorder = (Border)_adorner.Children[0];
paddingBorder.BorderThickness = visual.GetValue(PaddingProperty);
var contentBorder = (Border)_adorner.Children[1];
contentBorder.Margin = visual.GetValue(PaddingProperty);
var marginBorder = (Border)_adorner.Children[2];
marginBorder.BorderThickness = visual.GetValue(MarginProperty);
marginBorder.Margin = InvertThickness(visual.GetValue(MarginProperty));
}
}
private static Thickness InvertThickness(Thickness input)
{
return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom);
}
protected void RemoveAdorner(object sender, PointerEventArgs e)
{
if (_adorner != null)
foreach (var border in _adorner.Children.OfType<Border>())
{
((Panel)_adorner.Parent).Children.Remove(_adorner);
_adorner = null;
border.Margin = default;
border.Padding = default;
border.BorderThickness = default;
}
_currentLayer?.Children.Remove(_adorner);
_currentLayer = null;
}
private void InitializeComponent()

5
src/Avalonia.Native/Avalonia.Native.csproj

@ -7,6 +7,7 @@
<CastXmlPath Condition="Exists('/usr/bin/castxml')">/usr/bin/castxml</CastXmlPath>
<CastXmlPath Condition="Exists('/usr/local/bin/castxml')">/usr/local/bin/castxml</CastXmlPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<SharpGenGenerateConsumerBindMapping>false</SharpGenGenerateConsumerBindMapping>
</PropertyGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release' AND '$([MSBuild]::IsOSPlatform(OSX))' == 'true'">
@ -18,8 +19,8 @@
</ItemGroup>
<ItemGroup>
<SharpGenMapping Include="Mappings.xml" />
<!--<PackageReference Include="SharpGenTools.Sdk" Version="1.2.1" PrivateAssets="all" />-->
<!--<SharpGenMapping Include="Mappings.xml" />-->
<PackageReference Include="SharpGenTools.Sdk" Version="1.2.1" PrivateAssets="all" />
<PackageReference Include="SharpGen.Runtime" Version="1.2.1" />
<PackageReference Include="SharpGen.Runtime.COM" Version="1.2.0" />
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />

17
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -110,11 +110,20 @@ namespace Avalonia.Native
.Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
;
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory));
if (_options.UseGpu)
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>()
.ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay()));
{
try
{
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>()
.ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay()));
}
catch (Exception)
{
// ignored
}
}
}
public IWindowImpl CreateWindow()

4
src/Avalonia.Native/WindowImplBase.cs

@ -351,12 +351,12 @@ namespace Avalonia.Native
public Point PointToClient(PixelPoint point)
{
return _native.PointToClient(point.ToAvnPoint()).ToAvaloniaPoint();
return _native?.PointToClient(point.ToAvnPoint()).ToAvaloniaPoint() ?? default;
}
public PixelPoint PointToScreen(Point point)
{
return _native.PointToScreen(point.ToAvnPoint()).ToAvaloniaPixelPoint();
return _native?.PointToScreen(point.ToAvnPoint()).ToAvaloniaPixelPoint() ?? default;
}
public void Hide()

2
src/Avalonia.Themes.Default/Expander.xaml

@ -86,7 +86,7 @@
<Style Selector="Expander /template/ ToggleButton#PART_toggle">
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="1">
<Border BorderThickness="1" Background="Transparent">
<Grid ColumnDefinitions="Auto,Auto">
<Border Grid.Column="0" Width="20" Height="20" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Fill="{DynamicResource ThemeForegroundBrush}"

9
src/Avalonia.Themes.Default/TextBox.xaml

@ -1,11 +1,20 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
<MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
<MenuItem x:Name="TextBoxContextMenuPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
</ContextMenu>
</Styles.Resources>
<Style Selector="TextBox">
<Setter Property="CaretBrush" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="SelectionBrush" Value="{DynamicResource HighlightBrush}"/>
<Setter Property="SelectionForegroundBrush" Value="{DynamicResource HighlightForegroundBrush}"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="ContextMenu" Value="{StaticResource DefaultTextBoxContextMenu}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"

4
src/Avalonia.Themes.Fluent/Expander.xaml

@ -86,10 +86,10 @@
<Style Selector="Expander /template/ ToggleButton#PART_toggle">
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="1">
<Border BorderThickness="1" Background="Transparent">
<Grid ColumnDefinitions="Auto,Auto">
<Border Grid.Column="0" Width="20" Height="20" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Fill="{DynamicResource SystemControlForegroundAltMediumHighBrush}"
<Path Fill="{DynamicResource SystemControlForegroundBaseHighBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 2 L 4 6 L 0 10 Z" />

6
src/Avalonia.Themes.Fluent/TextBox.xaml

@ -14,6 +14,11 @@
<StreamGeometry x:Key="TextBoxClearButtonData">M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z</StreamGeometry>
<StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
<StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
<ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
<MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
<MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
<MenuItem x:Name="TextBoxContextMenuPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
</ContextMenu>
</Styles.Resources>
<Style Selector="TextBox">
@ -28,6 +33,7 @@
<Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
<Setter Property="Padding" Value="{DynamicResource TextControlThemePadding}" />
<Setter Property="FocusAdorner" Value="{x:Null}" />
<Setter Property="ContextMenu" Value="{StaticResource DefaultTextBoxContextMenu}" />
<Setter Property="Template">
<ControlTemplate>
<DockPanel>

4
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -555,7 +555,7 @@ namespace Avalonia.Media
}
}
return new Rect(0, 0, width, height);
return new Rect(0, GlyphTypeface.Ascent * Scale, width, height);
}
private void Set<T>(ref T field, T value)
@ -595,8 +595,6 @@ namespace Avalonia.Media
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width);
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
_bounds = new Rect(0, 0, width, height);
}
void IDisposable.Dispose()

16
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@ -181,6 +181,17 @@ namespace Avalonia.Media.TextFormatting
return nextCharacterHit;
}
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength <= TextRange.Start + TextRange.Length)
{
return characterHit; // Can't move, we're after the last character
}
var runIndex = GetRunIndexAtCodepointIndex(TextRange.End);
var textRun = _textRuns[runIndex];
characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
return characterHit; // Can't move, we're after the last character
}
@ -192,6 +203,11 @@ namespace Avalonia.Media.TextFormatting
return previousCharacterHit;
}
if (characterHit.FirstCharacterIndex < TextRange.Start)
{
characterHit = new CharacterHit(TextRange.Start);
}
return characterHit; // Can't move, we're before the first character
}

2
src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs

@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
GlyphRun glyphRun,
Point baselineOrigin,
IDictionary<IVisual, Scene> childScenes = null)
: base(glyphRun.Bounds, transform)
: base(glyphRun.Bounds.Translate(baselineOrigin), transform)
{
Transform = transform;
Foreground = foreground?.ToImmutable();

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -24,7 +24,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
AvaloniaXamlIlDataContextTypeMetadataNode inferredDataContextTypeNode = null;
AvaloniaXamlIlDataContextTypeMetadataNode directiveDataContextTypeNode = null;
bool isDataTemplate = on.Type.GetClrType().Equals(context.GetAvaloniaTypes().DataTemplate);
for (int i = 0; i < on.Children.Count; ++i)
{
@ -57,7 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
inferredDataContextTypeNode = ParseDataContext(context, on, obj);
}
else if(isDataTemplate
else if(context.GetAvaloniaTypes().DataTemplate.IsAssignableFrom(on.Type.GetClrType())
&& pa.Property.Name == "DataType"
&& pa.Values[0] is XamlTypeExtensionNode dataTypeNode)
{
@ -70,7 +69,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
// do more specialized inference
if (directiveDataContextTypeNode is null)
{
if (isDataTemplate && inferredDataContextTypeNode is null)
if (context.GetAvaloniaTypes().IDataTemplate.IsAssignableFrom(on.Type.GetClrType())
&& inferredDataContextTypeNode is null)
{
// Infer data type from collection binding on a control that displays items.
var parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault();

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -41,6 +41,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ResolveByNameExtension { get; }
public IXamlType DataTemplate { get; }
public IXamlType IDataTemplate { get; }
public IXamlType IItemsPresenterHost { get; }
public IXamlType ItemsRepeater { get; }
public IXamlType ReflectionBindingExtension { get; }
@ -98,6 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension");
ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension");
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");
IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate");
IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost");
ItemsRepeater = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsRepeater");
ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension");

41
tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.UnitTests;
using System;
using Avalonia.Controls.UnitTests;
using Avalonia.Platform;
using Xunit;
@ -18,6 +19,19 @@ namespace Avalonia.Controls.UnitTests
{
}
public class AppWithDependencies : Application
{
public AppWithDependencies(object dependencyA, object dependencyB)
{
DependencyA = dependencyA;
DependencyB = dependencyB;
}
public object DependencyA { get; }
public object DependencyB { get; }
}
public class DefaultModule
{
public static bool IsLoaded = false;
@ -53,7 +67,30 @@ namespace Avalonia.Controls.UnitTests
IsLoaded = true;
}
}
[Fact]
public void UseAppFactory()
{
using (AvaloniaLocator.EnterScope())
{
ResetModuleLoadStates();
Func<AppWithDependencies> appFactory = () => new AppWithDependencies(dependencyA: new object(), dependencyB: new object());
var builder = AppBuilder.Configure<AppWithDependencies>(appFactory)
.UseWindowingSubsystem(() => { })
.UseRenderingSubsystem(() => { })
.UseAvaloniaModules()
.SetupWithoutStarting();
AppWithDependencies app = (AppWithDependencies)builder.Instance;
Assert.NotNull(app.DependencyA);
Assert.NotNull(app.DependencyB);
Assert.True(DefaultModule.IsLoaded);
}
}
[Fact]
public void LoadsDefaultModule()
{

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

@ -562,6 +562,41 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void TextBox_CaretIndex_Persists_When_Focus_Lost()
{
using (UnitTestApplication.Start(FocusServices))
{
var target1 = new TextBox
{
Template = CreateTemplate(),
Text = "1234"
};
var target2 = new TextBox
{
Template = CreateTemplate(),
Text = "5678"
};
var sp = new StackPanel();
sp.Children.Add(target1);
sp.Children.Add(target2);
target1.ApplyTemplate();
target2.ApplyTemplate();
var root = new TestRoot { Child = sp };
target2.Focus();
target2.CaretIndex = 2;
Assert.False(target1.IsFocused);
Assert.True(target2.IsFocused);
target1.Focus();
Assert.Equal(2, target2.CaretIndex);
}
}
[Fact]
public void TextBox_Reveal_Password_Reset_When_Lost_Focus()
{

26
tests/Avalonia.LeakTests/ControlTests.cs

@ -449,13 +449,22 @@ namespace Avalonia.LeakTests
Assert.Same(window, FocusManager.Instance.Current);
// Context menu in resources means the baseline may not be 0.
var initialMenuCount = 0;
var initialMenuItemCount = 0;
dotMemory.Check(memory =>
{
initialMenuCount = memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount;
initialMenuItemCount = memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount;
});
AttachShowAndDetachContextMenu(window);
Mock.Get(window.PlatformImpl).ResetCalls();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
}
}
@ -484,14 +493,23 @@ namespace Avalonia.LeakTests
Assert.Same(window, FocusManager.Instance.Current);
// Context menu in resources means the baseline may not be 0.
var initialMenuCount = 0;
var initialMenuItemCount = 0;
dotMemory.Check(memory =>
{
initialMenuCount = memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount;
initialMenuItemCount = memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount;
});
BuildAndShowContextMenu(window);
BuildAndShowContextMenu(window);
Mock.Get(window.PlatformImpl).ResetCalls();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
}
}

144
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs

@ -1,5 +1,5 @@
using System.Reactive.Subjects;
using System.Windows.Input;
using System;
using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.UnitTests;
@ -56,7 +56,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
Assert.Equal("Called 5", vm.Value);
}
}
[Fact]
public void Binding_Method_To_TextBlock_Text_Works()
{
@ -79,6 +79,111 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
}
}
[Theory]
[InlineData(null, "Not called")]
[InlineData("A", "Do A")]
public void Binding_Method_With_Parameter_To_Command_CanExecute(object commandParameter, string result)
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button' Command='{Binding Do}' CommandParameter='{Binding Parameter, Mode=OneTime}'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.FindControl<Button>("button");
var vm = new ViewModel()
{
Parameter = commandParameter
};
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
PerformClick(button);
Assert.Equal(vm.Value, result);
}
}
[Fact]
public void Binding_Method_With_Parameter_To_Command_CanExecute_DependsOn()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button' Command='{Binding Do}' CommandParameter='{Binding Parameter, Mode=OneWay}'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.FindControl<Button>("button");
var vm = new ViewModel()
{
Parameter = null,
};
button.DataContext = vm;
window.ApplyTemplate();
Assert.NotNull(button.Command);
Assert.Equal(button.IsEffectivelyEnabled, false);
vm.Parameter = true;
Threading.Dispatcher.UIThread.RunJobs();
Assert.Equal(button.IsEffectivelyEnabled, true);
}
}
[Fact]
public void Binding_Method_To_Command_Collected()
{
WeakReference<ViewModel> MakeRef()
{
var weakVm = new WeakReference<ViewModel>(null);
{
var vm = new ViewModel()
{
Parameter = null,
};
weakVm.SetTarget(vm);
var canExecuteCount = 0;
var action = new Action<object>(vm.Do);
var command = new Avalonia.Data.Converters.MethodToCommandConverter(action);
command.CanExecuteChanged += (s, e) => canExecuteCount++;
vm.Parameter = 0;
Threading.Dispatcher.UIThread.RunJobs();
vm.Parameter = null;
Threading.Dispatcher.UIThread.RunJobs();
Assert.Equal(2, canExecuteCount);
}
return weakVm;
}
bool IsAlive(WeakReference<ViewModel> @ref)
{
return @ref.TryGetTarget(out var instance)
&& instance is null == false;
}
var vmref = MakeRef();
var beforeCollect = IsAlive(vmref);
GC.Collect();
GC.WaitForPendingFinalizers();
var afterCollect = IsAlive(vmref);
Assert.True(beforeCollect, "Invalid ViewModel instance, it is already collected.");
Assert.False(afterCollect, "ViewModel instance was not collected");
}
static void PerformClick(Button button)
{
button.RaiseEvent(new KeyEventArgs
@ -88,12 +193,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
});
}
private class ViewModel
private class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Method() => Value = "Called";
public string Method1(int i) => Value = $"Called {i}";
public string Method2(int i, int j) => Value = $"Called {i},{j}";
public string Value { get; private set; } = "Not called";
object _parameter;
public object Parameter
{
get
{
return _parameter;
}
set
{
if (_parameter == value)
{
return;
}
_parameter = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Parameter)));
}
}
public void Do(object parameter)
{
Value = $"Do {parameter}";
}
[Metadata.DependsOn(nameof(Parameter))]
public bool CanDo(object parameter)
{
return ReferenceEquals(null, parameter) == false;
}
}
}
}

60
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@ -10,6 +9,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
public class TextLineTests
{
private static readonly string s_multiLineText = "012345678\r\r0123456789";
[Fact]
public void Should_Get_First_CharacterHit()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties);
var formatter = new TextFormatterImpl();
var currentIndex = 0;
while (currentIndex < s_multiLineText.Length)
{
var textLine =
formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var firstCharacterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(int.MinValue));
Assert.Equal(textLine.TextRange.Start, firstCharacterHit.FirstCharacterIndex);
currentIndex += textLine.TextRange.Length;
}
}
}
[Fact]
public void Should_Get_Last_CharacterHit()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties);
var formatter = new TextFormatterImpl();
var currentIndex = 0;
while (currentIndex < s_multiLineText.Length)
{
var textLine =
formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var lastCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(int.MaxValue));
Assert.Equal(textLine.TextRange.Start + textLine.TextRange.Length,
lastCharacterHit.FirstCharacterIndex + lastCharacterHit.TrailingLength);
currentIndex += textLine.TextRange.Length;
}
}
}
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
[InlineData("𐐷1234")]
[Theory]

Loading…
Cancel
Save