diff --git a/Avalonia.sln.DotSettings b/Avalonia.sln.DotSettings
index 2c0a6b9dc8..b0692905e7 100644
--- a/Avalonia.sln.DotSettings
+++ b/Avalonia.sln.DotSettings
@@ -1,5 +1,4 @@
- True
ExplicitlyExcluded
ExplicitlyExcluded
ExplicitlyExcluded
@@ -39,4 +38,4 @@
<Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" />
True
True
- True
\ No newline at end of file
+ True
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index fbd8507193..11ef36d43f 100644
--- a/azure-pipelines.yml
+++ b/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'
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 14fe60ab0b..7a6e7dc72f 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/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];
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml
index 1ac447ea69..f631c40eb1 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml
@@ -18,6 +18,7 @@
Watermark="Floating Watermark"
UseFloatingWatermark="True"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
+
diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs
index 2c7f34c5be..2f1cb2888e 100644
--- a/src/Avalonia.Base/Collections/AvaloniaList.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaList.cs
@@ -280,8 +280,8 @@ namespace Avalonia.Collections
///
/// Gets a range of items from the collection.
///
- /// The first index to remove.
- /// The number of items to remove.
+ /// The zero-based index at which the range starts.
+ /// The number of elements in the range.
public IEnumerable GetRange(int index, int count)
{
return _inner.GetRange(index, count);
@@ -455,7 +455,7 @@ namespace Avalonia.Collections
}
///
- /// Ensures that the capacity of the list is at least .
+ /// Ensures that the capacity of the list is at least .
///
/// The capacity.
public void EnsureCapacity(int capacity)
diff --git a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs
index 9ec600d2bc..2385d4981c 100644
--- a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs
+++ b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs
@@ -26,7 +26,7 @@ namespace Avalonia.Data.Converters
///
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
- if (value is TIn || (value == null && TypeUtilities.AcceptsNull(typeof(TIn))))
+ if (TypeUtilities.CanCast(value))
{
return _convert((TIn)value);
}
diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
index 1a78792173..326d1a3f53 100644
--- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
+++ b/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();
}
diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs
index 179ded3549..0978308ef6 100644
--- a/src/Avalonia.Base/Utilities/TypeUtilities.cs
+++ b/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);
}
+ ///
+ /// Returns a value indicating whether null can be assigned to the specified type.
+ ///
+ /// The type
+ /// True if the type accepts null values; otherwise false.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool AcceptsNull()
+ {
+ return default(T) is null;
+ }
+
///
/// 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
/// True if the cast is possible, otherwise false.
public static bool CanCast(object value)
{
- return value is T || (value is null && AcceptsNull(typeof(T)));
+ return value is T || (value is null && AcceptsNull());
}
///
diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
index 9d8ebbfac1..587dd228a3 100644
--- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs
+++ b/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);
diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
index 6ac77fbb99..1d1a595ccf 100644
--- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs
+++ b/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
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index 5a6e78f441..0e946126ea 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/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)
diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs
new file mode 100644
index 0000000000..a72c617f05
--- /dev/null
+++ b/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 AsciiOnlyProperty =
+ AvaloniaProperty.Register(nameof(AsciiOnly));
+
+ public static readonly DirectProperty CultureProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Culture), o => o.Culture,
+ (o, v) => o.Culture = v, CultureInfo.CurrentCulture);
+
+ public static readonly StyledProperty HidePromptOnLeaveProperty =
+ AvaloniaProperty.Register(nameof(HidePromptOnLeave));
+
+ public static readonly DirectProperty MaskCompletedProperty =
+ AvaloniaProperty.RegisterDirect(nameof(MaskCompleted), o => o.MaskCompleted);
+
+ public static readonly DirectProperty MaskFullProperty =
+ AvaloniaProperty.RegisterDirect(nameof(MaskFull), o => o.MaskFull);
+
+ public static readonly StyledProperty MaskProperty =
+ AvaloniaProperty.Register(nameof(Mask), string.Empty);
+
+ public static new readonly StyledProperty PasswordCharProperty =
+ AvaloniaProperty.Register(nameof(PasswordChar), '\0');
+
+ public static readonly StyledProperty PromptCharProperty =
+ AvaloniaProperty.Register(nameof(PromptChar), '_');
+
+ public static readonly DirectProperty ResetOnPromptProperty =
+ AvaloniaProperty.RegisterDirect(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v);
+
+ public static readonly DirectProperty ResetOnSpaceProperty =
+ AvaloniaProperty.RegisterDirect(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() { }
+
+ ///
+ /// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters.
+ /// Default value is false.
+ ///
+ public bool AsciiOnly
+ {
+ get => GetValue(AsciiOnlyProperty);
+ set => SetValue(AsciiOnlyProperty, value);
+ }
+
+ ///
+ /// Gets or sets the culture information associated with the masked text box.
+ ///
+ public CultureInfo? Culture
+ {
+ get => _culture;
+ set => SetAndRaise(CultureProperty, ref _culture, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus.
+ ///
+ public bool HidePromptOnLeave
+ {
+ get => GetValue(HidePromptOnLeaveProperty);
+ set => SetValue(HidePromptOnLeaveProperty, value);
+ }
+
+ ///
+ /// Gets or sets the mask to apply to the TextBox.
+ ///
+ public string? Mask
+ {
+ get => GetValue(MaskProperty);
+ set => SetValue(MaskProperty, value);
+ }
+
+ ///
+ /// Specifies whether the test string required input positions, as specified by the mask, have
+ /// all been assigned.
+ ///
+ public bool? MaskCompleted
+ {
+ get => MaskProvider?.MaskCompleted;
+ }
+
+ ///
+ /// Specifies whether all inputs (required and optional) have been provided into the mask successfully.
+ ///
+ public bool? MaskFull
+ {
+ get => MaskProvider?.MaskFull;
+ }
+
+ ///
+ /// Gets the MaskTextProvider for the specified Mask.
+ ///
+ public MaskedTextProvider? MaskProvider { get; private set; }
+
+ ///
+ /// Gets or sets the character to be displayed in substitute for user input.
+ ///
+ public new char PasswordChar
+ {
+ get => GetValue(PasswordCharProperty);
+ set => SetValue(PasswordCharProperty, value);
+ }
+
+ ///
+ /// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
+ ///
+ public char PromptChar
+ {
+ get => GetValue(PromptCharProperty);
+ set => SetValue(PromptCharProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed.
+ ///
+ public bool ResetOnPrompt
+ {
+ get => _resetOnPrompt;
+ set
+ {
+ SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
+ if (MaskProvider != null)
+ {
+ MaskProvider.ResetOnPrompt = value;
+ }
+
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating if selected characters should be reset when the space character is pressed.
+ ///
+ 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();
+
+ bool Match(List 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(AvaloniaPropertyChangedEventArgs 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;
+ }
+ }
+
+ }
+}
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index e3783febdd..b0b52812b9 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/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;
}
}
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 0eade8d6df..9eae928eeb 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -145,6 +145,18 @@ namespace Avalonia.Controls
(o, v) => o.UndoLimit = v,
unsetValue: -1);
+ public static readonly RoutedEvent CopyingToClipboardEvent =
+ RoutedEvent.Register(
+ "CopyingToClipboard", RoutingStrategies.Bubble);
+
+ public static readonly RoutedEvent CuttingToClipboardEvent =
+ RoutedEvent.Register(
+ "CuttingToClipboard", RoutingStrategies.Bubble);
+
+ public static readonly RoutedEvent PastingFromClipboardEvent =
+ RoutedEvent.Register(
+ "PastingFromClipboard", RoutingStrategies.Bubble);
+
readonly struct UndoRedoState : IEquatable
{
public string Text { get; }
@@ -500,6 +512,24 @@ namespace Avalonia.Controls
}
}
+ public event EventHandler CopyingToClipboard
+ {
+ add => AddHandler(CopyingToClipboardEvent, value);
+ remove => RemoveHandler(CopyingToClipboardEvent, value);
+ }
+
+ public event EventHandler CuttingToClipboard
+ {
+ add => AddHandler(CuttingToClipboardEvent, value);
+ remove => RemoveHandler(CuttingToClipboardEvent, value);
+ }
+
+ public event EventHandler PastingFromClipboard
+ {
+ add => AddHandler(PastingFromClipboardEvent, value);
+ remove => RemoveHandler(PastingFromClipboardEvent, value);
+ }
+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get("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);
diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs
index 5c4af68d79..5082265ea6 100644
--- a/src/Avalonia.Input/AccessKeyHandler.cs
+++ b/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)
{
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
index 9e90d34e97..f4ac681b91 160000
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
@@ -1 +1 @@
-Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a
+Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 3b7d3efa2f..8fc25f8cfa 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -263,10 +263,8 @@ namespace Avalonia.Win32
{
ShowWindow(value, true);
}
- else
- {
- _showWindowState = value;
- }
+
+ _showWindowState = value;
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
index b346fca330..c8bd289e54 100644
--- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
+++ b/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(() => { control.Text = "a"; });
+ });
+ }
+
[Fact]
public void Text_Completion_Via_Text_Property()
{
diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
new file mode 100644
index 0000000000..1a251a5cef
--- /dev/null
+++ b/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