Browse Source

Merge branch 'master' into revert-6066-fixes/5947-gradient-drawingcontext

pull/6522/head
Steven Kirk 5 years ago
committed by GitHub
parent
commit
7a21df6aff
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. 2
      src/Avalonia.Base/Data/Converters/FuncValueConverter.cs
  6. 2
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  7. 14
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  8. 51
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  9. 49
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  10. 11
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  11. 36
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  12. 15
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  13. 17
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  14. 16
      src/Avalonia.Controls/AutoCompleteBox.cs
  15. 433
      src/Avalonia.Controls/MaskedTextBox.cs
  16. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  17. 22
      src/Avalonia.Controls/Primitives/Popup.cs
  18. 4
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  19. 7
      src/Avalonia.Visuals/Visual.cs
  20. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  21. 23
      src/Markup/Avalonia.Markup.Xaml/Extensions.cs
  22. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  23. 23
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs
  24. 6
      src/Windows/Avalonia.Win32/WindowImpl.cs
  25. 1
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  26. 73
      tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs
  27. 10
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  28. 990
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.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>

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>

51
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -3039,6 +3039,12 @@ namespace Avalonia.Controls
}
}
//TODO: Ensure right button is checked for
internal bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
{
KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift);
return UpdateStateOnMouseRightButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl);
}
//TODO: Ensure left button is checked for
internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
{
@ -4489,17 +4495,27 @@ namespace Avalonia.Controls
element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext);
if (element != null)
{
// Subscribe to the new element's events
element.Initialized += EditingElement_Initialized;
dataGridCell.Content = element;
if (element.IsInitialized)
{
PreparingCellForEditPrivate(element as Control);
}
else
{
// Subscribe to the new element's events
element.Initialized += EditingElement_Initialized;
}
}
}
else
{
// Generate Element and apply column style if available
element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext);
dataGridCell.Content = element;
}
dataGridCell.Content = element;
}
private void PreparingCellForEditPrivate(Control editingElement)
@ -5711,6 +5727,35 @@ namespace Avalonia.Controls
VerticalScroll?.Invoke(sender, e);
}
//TODO: Ensure right button is checked for
private bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl)
{
Debug.Assert(slot >= 0);
if (shift || ctrl)
{
return true;
}
if (IsSlotOutOfBounds(slot))
{
return true;
}
if (GetRowSelection(slot))
{
return true;
}
// Unselect everything except the row that was clicked on
try
{
UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
}
finally
{
NoSelectionChangeCount--;
}
return true;
}
//TODO: Ensure left button is checked for
private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl)
{

49
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -161,29 +161,42 @@ namespace Avalonia.Controls
private void DataGridCell_PointerPressed(PointerPressedEventArgs e)
{
// OwningGrid is null for TopLeftHeaderCell and TopRightHeaderCell because they have no OwningRow
if (OwningGrid != null)
if (OwningGrid == null)
{
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
}
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
if (OwningRow != null)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
// Do not handle PointerPressed with touch,
// so we can start scroll gesture on the same event.
if (e.Pointer.Type != PointerType.Touch)
{
OwningGrid.Focus();
e.Handled = handled;
}
if (OwningRow != null)
{
var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
// Do not handle PointerPressed with touch,
// so we can start scroll gesture on the same event.
if (e.Pointer.Type != PointerType.Touch)
{
e.Handled = handled;
}
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
if (OwningRow != null)
{
e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
}
}
}

11
src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs

@ -233,7 +233,7 @@ namespace Avalonia.Controls
else
{
editableCollectionView.EditItem(dataItem);
return editableCollectionView.IsEditingItem;
return editableCollectionView.IsEditingItem || editableCollectionView.IsAddingNew;
}
}
@ -314,7 +314,14 @@ namespace Avalonia.Controls
CommittingEdit = true;
try
{
editableCollectionView.CommitEdit();
if (editableCollectionView.IsAddingNew)
{
editableCollectionView.CommitNew();
}
else
{
editableCollectionView.CommitEdit();
}
}
finally
{

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

@ -378,13 +378,13 @@ namespace Avalonia.Controls
}
}
}
}
}
internal Panel RootElement
{
get;
private set;
}
}
internal int Slot
{
@ -638,7 +638,7 @@ namespace Avalonia.Controls
PseudoClasses.Set(":editing", IsEditing);
PseudoClasses.Set(":invalid", !IsValid);
ApplyHeaderStatus();
}
}
}
//TODO Animation
@ -896,7 +896,7 @@ namespace Avalonia.Controls
_detailsElement.ContentHeight = _detailsDesiredHeight;
}
}
}
}
// Makes sure the _detailsDesiredHeight is initialized. We need to measure it to know what
// height we want to animate to. Subsequently, we just update that height in response to SizeChanged
@ -919,7 +919,7 @@ namespace Avalonia.Controls
//TODO Cleanup
double? _previousDetailsHeight = null;
//TODO Animation
private void DetailsContent_HeightChanged(double newValue)
{
@ -1022,7 +1022,7 @@ namespace Avalonia.Controls
}
}
}
internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight)
{
if (_detailsElement != null && AreDetailsVisible)
@ -1066,7 +1066,7 @@ namespace Avalonia.Controls
.Subscribe(DetailsContent_MarginChanged);
}
_detailsElement.Children.Add(_detailsContent);
}
}
@ -1090,6 +1090,28 @@ namespace Avalonia.Controls
}
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (change.Property == DataContextProperty)
{
var owner = OwningGrid;
if (owner != null && this.IsRecycled)
{
var columns = owner.ColumnsItemsInternal;
var nc = columns.Count;
for (int ci = 0; ci < nc; ci++)
{
if (columns[ci] is DataGridTemplateColumn column)
{
column.RefreshCellContent((Control)this.Cells[column.Index].Content, nameof(DataGridTemplateColumn.CellTemplate));
}
}
}
}
base.OnPropertyChanged(change);
}
}

15
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -283,7 +283,11 @@ namespace Avalonia.Controls
//TODO TabStop
private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e)
{
if (OwningGrid != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (OwningGrid == null)
{
return;
}
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled)
{
@ -300,6 +304,15 @@ namespace Avalonia.Controls
e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false);
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
{
OwningGrid.Focus();
}
e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false);
}
}
private void EnsureChildClip(Visual child, double frozenLeftEdge)

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

@ -179,12 +179,12 @@ namespace Avalonia.Controls.Primitives
//TODO TabStop
private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (OwningGrid == null)
{
return;
}
if (OwningGrid != null)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
@ -199,6 +199,19 @@ namespace Avalonia.Controls.Primitives
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
{
OwningGrid.Focus();
}
if (OwningRow != null)
{
Debug.Assert(sender is DataGridRowHeader);
Debug.Assert(sender == this);
e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, -1, Slot, false);
}
}
}
}

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

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

@ -145,7 +145,9 @@ namespace Avalonia.Controls.Primitives
{
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e));
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
VerticalOffsetProperty.Changed.AddClassHandler<Popup>((x, _) => x.HandlePositionChange());
HorizontalOffsetProperty.Changed.AddClassHandler<Popup>((x, _) => x.HandlePositionChange());
}
/// <summary>
@ -519,6 +521,24 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromLogicalTree(e);
Close();
}
private void HandlePositionChange()
{
if (_openState != null)
{
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType<IControl>();
if (placementTarget == null)
return;
_openState.PopupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect);
}
}
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{

4
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -75,8 +75,6 @@
</Grid.ColumnDefinitions>
<ContentPresenter Name="PART_IconPresenter"
Content="{TemplateBinding Icon}"
Width="16"
Height="16"
Margin="{DynamicResource MenuIconPresenterMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
@ -199,6 +197,8 @@
</Style>
<Style Selector="MenuItem /template/ ContentPresenter#PART_IconPresenter">
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="MenuItem:icon /template/ ContentPresenter#PART_IconPresenter">

7
src/Avalonia.Visuals/Visual.cs

@ -489,11 +489,8 @@ namespace Avalonia
protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e)
{
// Don't log a binding error unless the control is attached to a logical or visual tree.
// In theory this should only need to check for logical tree attachment, but in practise
// due to ContentControlMixin only taking effect when the template has finished being
// applied, some controls are attached to the visual tree before the logical tree.
if (((ILogical)this).IsAttachedToLogicalTree || ((IVisual)this).IsAttachedToVisualTree)
// Don't log a binding error unless the control is attached to a logical tree.
if (((ILogical)this).IsAttachedToLogicalTree)
{
if (e is BindingChainException b &&
string.IsNullOrEmpty(b.ExpressionErrorPoint) &&

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

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

23
src/Markup/Avalonia.Markup.Xaml/Extensions.cs

@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.XamlIl.Runtime;
using Avalonia.Styling;
namespace Avalonia.Markup.Xaml
{
@ -32,5 +34,26 @@ namespace Avalonia.Markup.Xaml
string name = string.IsNullOrEmpty(namespacePrefix) ? type : $"{namespacePrefix}:{type}";
return tr?.Resolve(name);
}
public static object GetDefaultAnchor(this IServiceProvider provider)
{
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
object anchor = provider.GetFirstParent<IControl>();
if (anchor is null)
{
// Try to find IDataContextProvider, this was added to allow us to find
// a datacontext for Application class when using NativeMenuItems.
anchor = provider.GetFirstParent<IDataContextProvider>();
}
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??
provider.GetService<IRootObjectProvider>()?.RootObject as IStyle ??
provider.GetLastParent<IStyle>();
}
}
}

18
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@ -1,7 +1,5 @@
using System;
using Avalonia.Data;
using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
@ -33,24 +31,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Priority = Priority,
StringFormat = StringFormat,
Source = Source,
DefaultAnchor = new WeakReference(GetDefaultAnchor(provider))
DefaultAnchor = new WeakReference(provider.GetDefaultAnchor())
};
}
private static object GetDefaultAnchor(IServiceProvider provider)
{
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
object anchor = provider.GetFirstParent<IControl>();
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??
provider.GetService<IRootObjectProvider>()?.RootObject as IStyle ??
provider.GetLastParent<IStyle>();
}
protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation)
{
if (Source != null)

23
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs

@ -37,33 +37,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Source = Source,
StringFormat = StringFormat,
RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)),
DefaultAnchor = new WeakReference(descriptorContext.GetDefaultAnchor()),
TargetNullValue = TargetNullValue,
NameScope = new WeakReference<INameScope>(serviceProvider.GetService<INameScope>())
};
}
private static object GetDefaultAnchor(IServiceProvider context)
{
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
object anchor = context.GetFirstParent<IControl>();
if(anchor is null)
{
// Try to find IDataContextProvider, this was added to allow us to find
// a datacontext for Application class when using NativeMenuItems.
anchor = context.GetFirstParent<IDataContextProvider>();
}
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??
context.GetService<IRootObjectProvider>()?.RootObject as IStyle ??
context.GetLastParent<IStyle>();
}
public IValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }

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

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

1
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -14,6 +14,7 @@
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>

73
tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs

@ -0,0 +1,73 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Markup.Xaml;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Logging
{
public class LoggingTests
{
[Fact]
public void Control_Should_Not_Log_Binding_Errors_When_Detached_From_Visual_Tree()
{
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.Base.UnitTests.Logging;assembly=Avalonia.UnitTests'>
<Panel Name='panel'>
<Rectangle Name='rect' Fill='{Binding $parent[Window].Background}'/>
</Panel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var calledTimes = 0;
using var logSink = TestLogSink.Start((l, a, s, m, d) =>
{
if (l >= Avalonia.Logging.LogEventLevel.Warning)
{
calledTimes++;
}
});
var panel = window.FindControl<Panel>("panel");
var rect = window.FindControl<Rectangle>("rect");
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
panel.Children.Remove(rect);
Assert.Equal(0, calledTimes);
}
}
[Fact]
public void Control_Should_Log_Binding_Errors_When_No_Ancestor_With_Such_Name()
{
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.Base.UnitTests.Logging;assembly=Avalonia.UnitTests'>
<Panel>
<Rectangle Fill='{Binding $parent[Grid].Background}'/>
</Panel>
</Window>";
var calledTimes = 0;
using var logSink = TestLogSink.Start((l, a, s, m, d) =>
{
if (l >= Avalonia.Logging.LogEventLevel.Warning && s is Rectangle)
{
calledTimes++;
}
});
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.Equal(1, calledTimes);
}
}
}
}

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;
}
}
}
}
Loading…
Cancel
Save