Browse Source

Merge branch 'master' into features/Binding_Method_CanExecute

pull/4095/head
Jeremy Koritzinsky 6 years ago
committed by GitHub
parent
commit
45144855a9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      src/Avalonia.Controls/AppBuilderBase.cs
  2. 126
      src/Avalonia.Controls/TextBox.cs
  3. 2
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  4. 16
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  5. 7
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  6. 9
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  7. 71
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  8. 17
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  9. 4
      src/Avalonia.Native/WindowImplBase.cs
  10. 9
      src/Avalonia.Themes.Default/TextBox.xaml
  11. 6
      src/Avalonia.Themes.Fluent/TextBox.xaml
  12. 4
      src/Avalonia.Visuals/Media/GlyphRun.cs
  13. 16
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  14. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs
  15. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  16. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  17. 41
      tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs
  18. 35
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  19. 26
      tests/Avalonia.LeakTests/ControlTests.cs
  20. 60
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

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 isNullOrEmpty = string.IsNullOrEmpty(text);
CanCopy = !isNullOrEmpty;
CanCut = !isNullOrEmpty && !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()

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()

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"

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));
}
}

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