Browse Source

Merge branch 'master' into fixes/itemssourceview-fixes

pull/6485/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
02b7ca4d74
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Avalonia.sln.DotSettings
  2. 2
      azure-pipelines.yml
  3. 10
      native/Avalonia.Native/src/OSX/window.mm
  4. 1
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  5. 6
      src/Avalonia.Base/Collections/AvaloniaList.cs
  6. 2
      src/Avalonia.Base/Data/Converters/FuncValueConverter.cs
  7. 2
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  8. 14
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  9. 7
      src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
  10. 13
      src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
  11. 16
      src/Avalonia.Controls/AutoCompleteBox.cs
  12. 433
      src/Avalonia.Controls/MaskedTextBox.cs
  13. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  14. 73
      src/Avalonia.Controls/TextBox.cs
  15. 5
      src/Avalonia.Input/AccessKeyHandler.cs
  16. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  17. 6
      src/Windows/Avalonia.Win32/WindowImpl.cs
  18. 10
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  19. 990
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  20. 1
      tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
  21. 3
      tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs
  22. 2
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

3
Avalonia.sln.DotSettings

@ -1,5 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Examl/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=3E53A01A_002DB331_002D47F3_002DB828_002D4A5717E77A24_002Fd_003Aglass/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6417B24E_002D49C2_002D4985_002D8DB2_002D3AB9D898EC91/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=E3A1060B_002D50D0_002D44E8_002D88B6_002DF44EF2E5BD72_002Ff_003Ahtml_002Ehtm/@EntryIndexedValue">ExplicitlyExcluded</s:String>
@ -39,4 +38,4 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcitx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcitx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

2
azure-pipelines.yml

@ -1,7 +1,7 @@
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
vmImage: 'ubuntu-20.04'
steps:
- task: CmdLine@2
displayName: 'Install Nuke'

10
native/Avalonia.Native/src/OSX/window.mm

@ -641,6 +641,7 @@ private:
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
[Window setTabbingMode:NSWindowTabbingModeDisallowed];
[Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
}
void HideOrShowTrafficLights ()
@ -1091,14 +1092,7 @@ private:
{
_fullScreenActive = true;
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable;
Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView;
[Window toggleFullScreen:nullptr];
}
@ -1672,6 +1666,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
switch(event.buttonNumber)
{
case 2:
case 3:
_isMiddlePressed = true;
[self mouseEvent:event withType:MiddleButtonDown];
@ -1704,6 +1699,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
switch(event.buttonNumber)
{
case 2:
case 3:
_isMiddlePressed = false;
[self mouseEvent:event withType:MiddleButtonUp];

1
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -18,6 +18,7 @@
Watermark="Floating Watermark"
UseFloatingWatermark="True"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
<MaskedTextBox Width="200" ResetOnSpace="False" Mask="(LLL) 999-0000"/>
<TextBox Width="200" Text="Validation Error">
<DataValidationErrors.Error>

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

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

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

@ -26,7 +26,7 @@ namespace Avalonia.Data.Converters
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TIn || (value == null && TypeUtilities.AcceptsNull(typeof(TIn))))
if (TypeUtilities.CanCast<TIn>(value))
{
return _convert((TIn)value);
}

2
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@ -39,7 +39,7 @@ namespace Avalonia.Threading
if (Dispatcher.UIThread.CheckAccess())
d(state);
else
Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait();
Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).GetAwaiter().GetResult();
}

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

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
@ -93,6 +94,17 @@ namespace Avalonia.Utilities
return !type.IsValueType || IsNullableType(type);
}
/// <summary>
/// Returns a value indicating whether null can be assigned to the specified type.
/// </summary>
/// <typeparam name="T">The type</typeparam>
/// <returns>True if the type accepts null values; otherwise false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool AcceptsNull<T>()
{
return default(T) is null;
}
/// <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.
@ -102,7 +114,7 @@ namespace Avalonia.Utilities
/// <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)));
return value is T || (value is null && AcceptsNull<T>());
}
/// <summary>

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

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

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

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

16
src/Avalonia.Controls/AutoCompleteBox.cs

@ -2094,7 +2094,21 @@ namespace Avalonia.Controls
bool inResults = !(stringFiltering || objectFiltering);
if (!inResults)
{
inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item);
if (stringFiltering)
{
inResults = TextFilter(text, FormatValue(item));
}
else
{
if (ItemFilter is null)
{
throw new Exception("ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom");
}
else
{
inResults = ItemFilter(text, item);
}
}
}
if (view_count > view_index && inResults && _view[view_index] == item)

433
src/Avalonia.Controls/MaskedTextBox.cs

@ -0,0 +1,433 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls
{
public class MaskedTextBox : TextBox, IStyleable
{
public static readonly StyledProperty<bool> AsciiOnlyProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture,
(o, v) => o.Culture = v, CultureInfo.CurrentCulture);
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave));
public static readonly DirectProperty<MaskedTextBox, bool?> MaskCompletedProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskCompleted), o => o.MaskCompleted);
public static readonly DirectProperty<MaskedTextBox, bool?> MaskFullProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskFull), o => o.MaskFull);
public static readonly StyledProperty<string?> MaskProperty =
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
public static new readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar), '\0');
public static readonly StyledProperty<char> PromptCharProperty =
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_');
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v);
private CultureInfo? _culture;
private bool _resetOnPrompt = true;
private bool _ignoreTextChanges;
private bool _resetOnSpace = true;
public MaskedTextBox() { }
/// <summary>
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
/// </summary>
public MaskedTextBox(MaskedTextProvider maskedTextProvider)
{
if (maskedTextProvider == null)
{
throw new ArgumentNullException(nameof(maskedTextProvider));
}
AsciiOnly = maskedTextProvider.AsciiOnly;
Culture = maskedTextProvider.Culture;
Mask = maskedTextProvider.Mask;
PasswordChar = maskedTextProvider.PasswordChar;
PromptChar = maskedTextProvider.PromptChar;
}
/// <summary>
/// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters.
/// Default value is false.
/// </summary>
public bool AsciiOnly
{
get => GetValue(AsciiOnlyProperty);
set => SetValue(AsciiOnlyProperty, value);
}
/// <summary>
/// Gets or sets the culture information associated with the masked text box.
/// </summary>
public CultureInfo? Culture
{
get => _culture;
set => SetAndRaise(CultureProperty, ref _culture, value);
}
/// <summary>
/// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus.
/// </summary>
public bool HidePromptOnLeave
{
get => GetValue(HidePromptOnLeaveProperty);
set => SetValue(HidePromptOnLeaveProperty, value);
}
/// <summary>
/// Gets or sets the mask to apply to the TextBox.
/// </summary>
public string? Mask
{
get => GetValue(MaskProperty);
set => SetValue(MaskProperty, value);
}
/// <summary>
/// Specifies whether the test string required input positions, as specified by the mask, have
/// all been assigned.
/// </summary>
public bool? MaskCompleted
{
get => MaskProvider?.MaskCompleted;
}
/// <summary>
/// Specifies whether all inputs (required and optional) have been provided into the mask successfully.
/// </summary>
public bool? MaskFull
{
get => MaskProvider?.MaskFull;
}
/// <summary>
/// Gets the MaskTextProvider for the specified Mask.
/// </summary>
public MaskedTextProvider? MaskProvider { get; private set; }
/// <summary>
/// Gets or sets the character to be displayed in substitute for user input.
/// </summary>
public new char PasswordChar
{
get => GetValue(PasswordCharProperty);
set => SetValue(PasswordCharProperty, value);
}
/// <summary>
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
/// </summary>
public char PromptChar
{
get => GetValue(PromptCharProperty);
set => SetValue(PromptCharProperty, value);
}
/// <summary>
/// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed.
/// </summary>
public bool ResetOnPrompt
{
get => _resetOnPrompt;
set
{
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnPrompt = value;
}
}
}
/// <summary>
/// Gets or sets a value indicating if selected characters should be reset when the space character is pressed.
/// </summary>
public bool ResetOnSpace
{
get => _resetOnSpace;
set
{
SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnSpace = value;
}
}
}
Type IStyleable.StyleKey => typeof(TextBox);
protected override void OnGotFocus(GotFocusEventArgs e)
{
if (HidePromptOnLeave == true && MaskProvider != null)
{
Text = MaskProvider.ToDisplayString();
}
base.OnGotFocus(e);
}
protected override async void OnKeyDown(KeyEventArgs e)
{
if (MaskProvider == null)
{
base.OnKeyDown(e);
return;
}
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
if (Match(keymap.Paste))
{
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync();
if (text == null)
return;
foreach (var item in text)
{
var index = GetNextCharacterPosition(CaretIndex);
if (MaskProvider.InsertAt(item, index))
{
CaretIndex = ++index;
}
}
Text = MaskProvider.ToDisplayString();
e.Handled = true;
return;
}
if (e.Key != Key.Back)
{
base.OnKeyDown(e);
}
switch (e.Key)
{
case Key.Delete:
if (CaretIndex < Text.Length)
{
if (MaskProvider.RemoveAt(CaretIndex))
{
RefreshText(MaskProvider, CaretIndex);
}
e.Handled = true;
}
break;
case Key.Space:
if (!MaskProvider.ResetOnSpace || string.IsNullOrEmpty(SelectedText))
{
if (MaskProvider.InsertAt(" ", CaretIndex))
{
RefreshText(MaskProvider, CaretIndex);
}
}
e.Handled = true;
break;
case Key.Back:
if (CaretIndex > 0)
{
MaskProvider.RemoveAt(CaretIndex - 1);
}
RefreshText(MaskProvider, CaretIndex - 1);
e.Handled = true;
break;
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
if (HidePromptOnLeave == true && MaskProvider != null)
{
Text = MaskProvider.ToString(!HidePromptOnLeave, true);
}
base.OnLostFocus(e);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
void UpdateMaskProvider()
{
MaskProvider = new MaskedTextProvider(Mask, Culture, true, PromptChar, PasswordChar, AsciiOnly) { ResetOnSpace = ResetOnSpace, ResetOnPrompt = ResetOnPrompt };
if (Text != null)
{
MaskProvider.Set(Text);
}
RefreshText(MaskProvider, 0);
}
if (change.Property == TextProperty && MaskProvider != null && _ignoreTextChanges == false)
{
if (string.IsNullOrEmpty(Text))
{
MaskProvider.Clear();
RefreshText(MaskProvider, CaretIndex);
base.OnPropertyChanged(change);
return;
}
MaskProvider.Set(Text);
RefreshText(MaskProvider, CaretIndex);
}
else if (change.Property == MaskProperty)
{
UpdateMaskProvider();
if (!string.IsNullOrEmpty(Mask))
{
foreach (var c in Mask!)
{
if (!MaskedTextProvider.IsValidMaskChar(c))
{
throw new ArgumentException("Specified mask contains characters that are not valid.");
}
}
}
}
else if (change.Property == PasswordCharProperty)
{
if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar))
{
throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar));
}
if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar)
{
// Prompt and password chars must be different.
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar)
{
UpdateMaskProvider();
}
}
else if (change.Property == PromptCharProperty)
{
if (!MaskedTextProvider.IsValidInputChar(PromptChar))
{
throw new ArgumentException("Specified character value is not allowed for this property.");
}
if (PromptChar == PasswordChar)
{
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
{
UpdateMaskProvider();
}
}
else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly
|| change.Property == CultureProperty && MaskProvider != null && !MaskProvider.Culture.Equals(Culture))
{
UpdateMaskProvider();
}
base.OnPropertyChanged(change);
}
protected override void OnTextInput(TextInputEventArgs e)
{
_ignoreTextChanges = true;
try
{
if (IsReadOnly)
{
e.Handled = true;
base.OnTextInput(e);
return;
}
if (MaskProvider == null)
{
base.OnTextInput(e);
return;
}
if ((MaskProvider.ResetOnSpace && e.Text == " " || MaskProvider.ResetOnPrompt && e.Text == MaskProvider.PromptChar.ToString()) && !string.IsNullOrEmpty(SelectedText))
{
if (SelectionStart > SelectionEnd ? MaskProvider.RemoveAt(SelectionEnd, SelectionStart - 1) : MaskProvider.RemoveAt(SelectionStart, SelectionEnd - 1))
{
SelectedText = string.Empty;
}
}
if (CaretIndex < Text.Length)
{
CaretIndex = GetNextCharacterPosition(CaretIndex);
if (MaskProvider.InsertAt(e.Text, CaretIndex))
{
CaretIndex++;
}
var nextPos = GetNextCharacterPosition(CaretIndex);
if (nextPos != 0 && CaretIndex != Text.Length)
{
CaretIndex = nextPos;
}
}
RefreshText(MaskProvider, CaretIndex);
e.Handled = true;
base.OnTextInput(e);
}
finally
{
_ignoreTextChanges = false;
}
}
private int GetNextCharacterPosition(int startPosition)
{
if (MaskProvider != null)
{
var position = MaskProvider.FindEditPositionFrom(startPosition, true);
if (CaretIndex != -1)
{
return position;
}
}
return startPosition;
}
private void RefreshText(MaskedTextProvider provider, int position)
{
if (provider != null)
{
Text = provider.ToDisplayString();
CaretIndex = position;
}
}
}
}

2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -511,8 +511,8 @@ namespace Avalonia.Controls.Presenters
else if (scrollable.IsLogicalScrollEnabled)
{
Viewport = scrollable.Viewport;
Offset = scrollable.Offset;
Extent = scrollable.Extent;
Offset = scrollable.Offset;
}
}

73
src/Avalonia.Controls/TextBox.cs

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

5
src/Avalonia.Input/AccessKeyHandler.cs

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

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a
Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72

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

@ -263,10 +263,8 @@ namespace Avalonia.Win32
{
ShowWindow(value, true);
}
else
{
_showWindowState = value;
}
_showWindowState = value;
}
}

10
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -105,6 +105,16 @@ namespace Avalonia.Controls.UnitTests
});
}
[Fact]
public void Custom_FilterMode_Without_ItemFilter_Setting_Throws_Exception()
{
RunTest((control, textbox) =>
{
control.FilterMode = AutoCompleteFilterMode.Custom;
Assert.Throws<Exception>(() => { control.Text = "a"; });
});
}
[Fact]
public void Text_Completion_Via_Text_Property()
{

990
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -0,0 +1,990 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class MaskedTextBoxTests
{
[Fact]
public void Opening_Context_Menu_Does_not_Lose_Selection()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
ContextMenu = new TestContextMenu()
};
var target2 = new MaskedTextBox
{
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 };
target1.SelectionStart = 0;
target1.SelectionEnd = 3;
target1.Focus();
Assert.False(target2.IsFocused);
Assert.True(target1.IsFocused);
target2.Focus();
Assert.Equal("123", target1.SelectedText);
}
}
[Fact]
public void Opening_Context_Flyout_Does_not_Lose_Selection()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
ContextFlyout = new MenuFlyout
{
Items = new List<MenuItem>
{
new MenuItem { Header = "Item 1" },
new MenuItem {Header = "Item 2" },
new MenuItem {Header = "Item 3" }
}
}
};
target1.ApplyTemplate();
var root = new TestRoot() { Child = target1 };
target1.SelectionStart = 0;
target1.SelectionEnd = 3;
target1.Focus();
Assert.True(target1.IsFocused);
target1.ContextFlyout.ShowAt(target1);
Assert.Equal("123", target1.SelectedText);
}
}
[Fact]
public void DefaultBindingMode_Should_Be_TwoWay()
{
Assert.Equal(
BindingMode.TwoWay,
TextBox.TextProperty.GetMetadata(typeof(MaskedTextBox)).DefaultBindingMode);
}
[Fact]
public void CaretIndex_Can_Moved_To_Position_After_The_End_Of_Text_With_Arrow_Key()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
target.CaretIndex = 3;
RaiseKeyEvent(target, Key.Right, 0);
Assert.Equal(4, target.CaretIndex);
}
}
[Fact]
public void Press_Ctrl_A_Select_All_Text()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
RaiseKeyEvent(target, Key.A, KeyModifiers.Control);
Assert.Equal(0, target.SelectionStart);
Assert.Equal(4, target.SelectionEnd);
}
}
[Fact]
public void Press_Ctrl_A_Select_All_Null_Text()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate()
};
RaiseKeyEvent(target, Key.A, KeyModifiers.Control);
Assert.Equal(0, target.SelectionStart);
Assert.Equal(0, target.SelectionEnd);
}
}
[Fact]
public void Press_Ctrl_Z_Will_Not_Modify_Text()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
RaiseKeyEvent(target, Key.Z, KeyModifiers.Control);
Assert.Equal("1234", target.Text);
}
}
[Fact]
public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
{
using (Start())
{
var source = new Class1();
var target = new MaskedTextBox
{
DataContext = source,
Template = CreateTemplate(),
};
target.ApplyTemplate();
target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
Assert.Equal("0", target.Text);
target.CaretIndex = 1;
target.RaiseEvent(new TextInputEventArgs
{
RoutedEvent = InputElement.TextInputEvent,
Text = "2",
});
Assert.Equal("02", target.Text);
}
}
[Fact]
public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
{
using (Start())
{
MaskedTextBox textBox = new MaskedTextBox
{
Text = "First Second Third Fourth",
CaretIndex = 5
};
// (First| Second Third Fourth)
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" Second Third Fourth", textBox.Text);
// ( Second |Third Fourth)
textBox.CaretIndex = 8;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" Third Fourth", textBox.Text);
// ( Thi|rd Fourth)
textBox.CaretIndex = 4;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" rd Fourth", textBox.Text);
// ( rd F[ou]rth)
textBox.SelectionStart = 5;
textBox.SelectionEnd = 7;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" rd Frth", textBox.Text);
// ( |rd Frth)
textBox.CaretIndex = 1;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal("rd Frth", textBox.Text);
}
}
[Fact]
public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No_Selection()
{
using (Start())
{
var textBox = new MaskedTextBox
{
Text = "First Second Third Fourth",
CaretIndex = 19
};
// (First Second Third |Fourth)
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("First Second Third ", textBox.Text);
// (First Second |Third )
textBox.CaretIndex = 13;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("First Second ", textBox.Text);
// (First Sec|ond )
textBox.CaretIndex = 9;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("First Sec", textBox.Text);
// (Fi[rs]t Sec )
textBox.SelectionStart = 2;
textBox.SelectionEnd = 4;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("Fit Sec", textBox.Text);
// (Fit Sec| )
textBox.Text += " ";
textBox.CaretIndex = 7;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("Fit Sec", textBox.Text);
}
}
[Fact]
public void Setting_SelectionStart_To_SelectionEnd_Sets_CaretPosition_To_SelectionStart()
{
using (Start())
{
var textBox = new MaskedTextBox
{
Text = "0123456789"
};
textBox.SelectionStart = 2;
textBox.SelectionEnd = 2;
Assert.Equal(2, textBox.CaretIndex);
}
}
[Fact]
public void Setting_Text_Updates_CaretPosition()
{
using (Start())
{
var target = new MaskedTextBox
{
Text = "Initial Text",
CaretIndex = 11
};
var invoked = false;
target.GetObservable(TextBox.TextProperty).Skip(1).Subscribe(_ =>
{
// Caret index should be set before Text changed notification, as we don't want
// to notify with an invalid CaretIndex.
Assert.Equal(7, target.CaretIndex);
invoked = true;
});
target.Text = "Changed";
Assert.True(invoked);
}
}
[Fact]
public void Press_Enter_Does_Not_Accept_Return()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
AcceptsReturn = false,
Text = "1234"
};
RaiseKeyEvent(target, Key.Enter, 0);
Assert.Equal("1234", target.Text);
}
}
[Fact]
public void Press_Enter_Add_Default_Newline()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
AcceptsReturn = true
};
RaiseKeyEvent(target, Key.Enter, 0);
Assert.Equal(Environment.NewLine, target.Text);
}
}
[Theory]
[InlineData("00/00/0000", "12102000", "12/10/2000")]
[InlineData("LLLL", "дбs", "____")]
[InlineData("AA", "Ü1", "__")]
public void AsciiOnly_Should_Not_Accept_Non_Ascii(string mask, string textEventArg, string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = mask,
AsciiOnly = true
};
RaiseTextEvent(target, textEventArg);
Assert.Equal(expected, target.Text);
}
}
[Fact]
public void Programmatically_Set_Text_Should_Not_Be_Removed_On_Key_Press()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = "00:00:00.000",
Text = "12:34:56.000"
};
target.CaretIndex = target.Text.Length;
RaiseKeyEvent(target, Key.Back, 0);
Assert.Equal("12:34:56.00_", target.Text);
}
}
[Fact]
public void Invalid_Programmatically_Set_Text_Should_Be_Rejected()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = "00:00:00.000",
Text = "12:34:560000"
};
Assert.Equal("__:__:__.___", target.Text);
}
}
[Theory]
[InlineData("00/00/0000", "12102000", "**/**/****")]
[InlineData("LLLL", "дбs", "***_")]
[InlineData("AA#00", "S2 33", "**_**")]
public void PasswordChar_Should_Hide_User_Input(string mask, string textEventArg, string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = mask,
PasswordChar = '*'
};
RaiseTextEvent(target, textEventArg);
Assert.Equal(expected, target.Text);
}
}
[Theory]
[InlineData("00/00/0000", "12102000", "12/10/2000")]
[InlineData("LLLL", "дбs", "дбs_")]
[InlineData("AA#00", "S2 33", "S2_33")]
public void Mask_Should_Work_Correctly(string mask, string textEventArg, string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = mask
};
RaiseTextEvent(target, textEventArg);
Assert.Equal(expected, target.Text);
}
}
[Fact]
public void Press_Enter_Add_Custom_Newline()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
AcceptsReturn = true,
NewLine = "Test"
};
RaiseKeyEvent(target, Key.Enter, 0);
Assert.Equal("Test", target.Text);
}
}
[Theory]
[InlineData(new object[] { false, TextWrapping.NoWrap, ScrollBarVisibility.Hidden })]
[InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
[InlineData(new object[] { true, TextWrapping.NoWrap, ScrollBarVisibility.Auto })]
[InlineData(new object[] { true, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
public void Has_Correct_Horizontal_ScrollBar_Visibility(
bool acceptsReturn,
TextWrapping wrapping,
ScrollBarVisibility expected)
{
using (Start())
{
var target = new MaskedTextBox
{
AcceptsReturn = acceptsReturn,
TextWrapping = wrapping,
};
Assert.Equal(expected, ScrollViewer.GetHorizontalScrollBarVisibility(target));
}
}
[Fact]
public void SelectionEnd_Doesnt_Cause_Exception()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
target.SelectionStart = 0;
target.SelectionEnd = 9;
target.Text = "123";
RaiseTextEvent(target, "456");
Assert.True(true);
}
}
[Fact]
public void SelectionStart_Doesnt_Cause_Exception()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
target.SelectionStart = 8;
target.SelectionEnd = 9;
target.Text = "123";
RaiseTextEvent(target, "456");
Assert.True(true);
}
}
[Fact]
public void SelectionStartEnd_Are_Valid_AterTextChange()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
target.SelectionStart = 8;
target.SelectionEnd = 9;
target.Text = "123";
Assert.True(target.SelectionStart <= "123".Length);
Assert.True(target.SelectionEnd <= "123".Length);
}
}
[Fact]
public void SelectedText_Changes_OnSelectionChange()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
Assert.True(target.SelectedText == "");
target.SelectionStart = 2;
target.SelectionEnd = 4;
Assert.True(target.SelectedText == "23");
}
}
[Fact]
public void SelectedText_EditsText()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectedText = "AA";
Assert.True(target.Text == "AA0123");
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = "BB";
Assert.True(target.Text == "ABB123");
}
}
[Fact]
public void SelectedText_CanClearText()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = "";
Assert.True(target.Text == "03");
}
}
[Fact]
public void SelectedText_NullClearsText()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = null;
Assert.True(target.Text == "03");
}
}
[Fact]
public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789\r"
};
target.CaretIndex = 11;
Assert.True(true);
}
}
[Theory]
[InlineData(Key.Up)]
[InlineData(Key.Down)]
[InlineData(Key.Home)]
[InlineData(Key.End)]
public void Textbox_doesnt_crash_when_Receives_input_and_template_not_applied(Key key)
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
IsVisible = false
};
var root = new TestRoot { Child = target1 };
target1.Focus();
Assert.True(target1.IsFocused);
RaiseKeyEvent(target1, key, KeyModifiers.None);
}
}
[Fact]
public void TextBox_GotFocus_And_LostFocus_Work_Properly()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
var target2 = new MaskedTextBox
{
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 };
var gfcount = 0;
var lfcount = 0;
target1.GotFocus += (s, e) => gfcount++;
target2.LostFocus += (s, e) => lfcount++;
target2.Focus();
Assert.False(target1.IsFocused);
Assert.True(target2.IsFocused);
target1.Focus();
Assert.False(target2.IsFocused);
Assert.True(target1.IsFocused);
Assert.Equal(1, gfcount);
Assert.Equal(1, lfcount);
}
}
[Fact]
public void TextBox_CaretIndex_Persists_When_Focus_Lost()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
var target2 = new MaskedTextBox
{
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()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
PasswordChar = '*'
};
var target2 = new MaskedTextBox
{
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 };
target1.Focus();
target1.RevealPassword = true;
target2.Focus();
Assert.False(target1.RevealPassword);
}
}
[Fact]
public void Setting_Bound_Text_To_Null_Works()
{
using (Start())
{
var source = new Class1 { Bar = "bar" };
var target = new MaskedTextBox { DataContext = source };
target.Bind(TextBox.TextProperty, new Binding("Bar"));
Assert.Equal("bar", target.Text);
source.Bar = null;
Assert.Null(target.Text);
}
}
[Theory]
[InlineData("abc", "d", 3, 0, 0, false, "abc")]
[InlineData("abc", "dd", 4, 3, 3, false, "abcd")]
[InlineData("abc", "ddd", 3, 0, 2, true, "ddc")]
[InlineData("abc", "dddd", 4, 1, 3, true, "addd")]
[InlineData("abc", "ddddd", 5, 3, 3, true, "abcdd")]
public void MaxLength_Works_Properly(
string initalText,
string textInput,
int maxLength,
int selectionStart,
int selectionEnd,
bool fromClipboard,
string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = initalText,
MaxLength = maxLength,
SelectionStart = selectionStart,
SelectionEnd = selectionEnd
};
if (fromClipboard)
{
AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
var clipboard = AvaloniaLocator.CurrentMutable.GetService<IClipboard>();
clipboard.SetTextAsync(textInput).GetAwaiter().GetResult();
RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
clipboard.ClearAsync().GetAwaiter().GetResult();
}
else
{
RaiseTextEvent(target, textInput);
}
Assert.Equal(expected, target.Text);
}
}
[Theory]
[InlineData(Key.X, KeyModifiers.Control)]
[InlineData(Key.Back, KeyModifiers.None)]
[InlineData(Key.Delete, KeyModifiers.None)]
[InlineData(Key.Tab, KeyModifiers.None)]
[InlineData(Key.Enter, KeyModifiers.None)]
public void Keys_Allow_Undo(Key key, KeyModifiers modifiers)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123",
AcceptsReturn = true,
AcceptsTab = true
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
AvaloniaLocator.CurrentMutable
.Bind<Input.Platform.IClipboard>().ToSingleton<ClipboardStub>();
RaiseKeyEvent(target, key, modifiers);
RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
Assert.True(target.Text == "0123");
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl(),
standardCursorFactory: Mock.Of<ICursorFactory>());
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<ICursorFactory>());
private IControlTemplate CreateTemplate()
{
return new FuncControlTemplate<MaskedTextBox>((control, scope) =>
new TextPresenter
{
Name = "PART_TextPresenter",
[!!TextPresenter.TextProperty] = new Binding
{
Path = "Text",
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
},
}.RegisterInNameScope(scope));
}
private void RaiseKeyEvent(MaskedTextBox textBox, Key key, KeyModifiers inputModifiers)
{
textBox.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
KeyModifiers = inputModifiers,
Key = key
});
}
private void RaiseTextEvent(MaskedTextBox textBox, string text)
{
textBox.RaiseEvent(new TextInputEventArgs
{
RoutedEvent = InputElement.TextInputEvent,
Text = text
});
}
private static IDisposable Start(TestServices services = null)
{
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
return UnitTestApplication.Start(services ?? Services);
}
private class Class1 : NotifyingBase
{
private int _foo;
private string _bar;
public int Foo
{
get { return _foo; }
set { _foo = value; RaisePropertyChanged(); }
}
public string Bar
{
get { return _bar; }
set { _bar = value; RaisePropertyChanged(); }
}
}
private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
{
private string _text;
public Task<string> GetTextAsync() => Task.FromResult(_text);
public Task SetTextAsync(string text)
{
_text = text;
return Task.CompletedTask;
}
public Task ClearAsync()
{
_text = null;
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
}
private class TestContextMenu : ContextMenu
{
public TestContextMenu()
{
IsOpen = true;
}
}
}
}

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

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

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

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

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

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

Loading…
Cancel
Save