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/Documentation/build.md b/Documentation/build.md
index a7d68eb599..9f5436e68e 100644
--- a/Documentation/build.md
+++ b/Documentation/build.md
@@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on
```
git clone https://github.com/AvaloniaUI/Avalonia.git
+cd Avalonia
git submodule update --init
```
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/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets
index 45a7f1aa44..de3830ffea 100644
--- a/packages/Avalonia/AvaloniaBuildTasks.targets
+++ b/packages/Avalonia/AvaloniaBuildTasks.targets
@@ -42,12 +42,24 @@
- $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences
+ $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache
+
+
+
+
+
+
+
+
+
+
+
+
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/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/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index ab1aff9220..fea02dabf4 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/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)
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
index 7dda936317..e3f150f5c4 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs
+++ b/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);
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
index a94acdec57..fade597ca1 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
+++ b/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
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index 7546970498..1efce7c0b8 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/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(AvaloniaPropertyChangedEventArgs 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);
+ }
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
index 1e03b134b1..49ca23d34c 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
+++ b/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)
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
index 0cd3589a57..510072174f 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
+++ b/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);
+ }
+ }
}
}
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/Design.cs b/src/Avalonia.Controls/Design.cs
index 0d05e19e53..07d2918a88 100644
--- a/src/Avalonia.Controls/Design.cs
+++ b/src/Avalonia.Controls/Design.cs
@@ -60,6 +60,19 @@ namespace Avalonia.Controls
return target.GetValue(PreviewWithProperty);
}
+ public static readonly AttachedProperty DesignStyleProperty = AvaloniaProperty
+ .RegisterAttached("DesignStyle", typeof(Design));
+
+ public static void SetDesignStyle(Control control, IStyle value)
+ {
+ control.SetValue(DesignStyleProperty, value);
+ }
+
+ public static IStyle GetDesignStyle(Control control)
+ {
+ return control.GetValue(DesignStyleProperty);
+ }
+
public static void ApplyDesignModeProperties(Control target, Control source)
{
if (source.IsSet(WidthProperty))
@@ -68,6 +81,8 @@ namespace Avalonia.Controls
target.Height = source.GetValue(HeightProperty);
if (source.IsSet(DataContextProperty))
target.DataContext = source.GetValue(DataContextProperty);
+ if (source.IsSet(DesignStyleProperty))
+ target.Styles.Add(source.GetValue(DesignStyleProperty));
}
}
}
diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs
index 230b4954fe..4b903d056c 100644
--- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs
+++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs
@@ -215,11 +215,6 @@ namespace Avalonia.Controls.Primitives
}
}
- if (CancelOpening())
- {
- return false;
- }
-
if (Popup.Parent != null && Popup.Parent != placementTarget)
{
((ISetLogicalParent)Popup).SetParent(null);
@@ -236,6 +231,11 @@ namespace Avalonia.Controls.Primitives
Popup.Child = CreatePresenter();
}
+ if (CancelOpening())
+ {
+ return false;
+ }
+
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
OnOpened();
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/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index 209feb351c..e361e7b736 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -275,7 +275,7 @@ namespace Avalonia.Controls.Platform
return;
}
- if (item.HasSubMenu)
+ if (item.HasSubMenu && item.IsEffectivelyEnabled)
{
Open(item, true);
}
@@ -303,7 +303,8 @@ namespace Avalonia.Controls.Platform
{
item.Parent.SelectedItem.Close();
SelectItemAndAncestors(item);
- Open(item, false);
+ if (item.HasSubMenu)
+ Open(item, false);
}
else
{
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/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index e804c4b4a9..a5cdeefb0e 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -53,6 +53,7 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register(
nameof(PlacementConstraintAdjustment),
PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
+ PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY |
PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
///
@@ -145,7 +146,9 @@ namespace Avalonia.Controls.Primitives
{
IsHitTestVisibleProperty.OverrideDefaultValue(false);
ChildProperty.Changed.AddClassHandler((x, e) => x.ChildChanged(e));
- IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e));
+ IsOpenProperty.Changed.AddClassHandler((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs)e));
+ VerticalOffsetProperty.Changed.AddClassHandler((x, _) => x.HandlePositionChange());
+ HorizontalOffsetProperty.Changed.AddClassHandler((x, _) => x.HandlePositionChange());
}
///
@@ -519,6 +522,24 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromLogicalTree(e);
Close();
}
+
+ private void HandlePositionChange()
+ {
+ if (_openState != null)
+ {
+ var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType();
+ if (placementTarget == null)
+ return;
+ _openState.PopupHost.ConfigurePosition(
+ placementTarget,
+ PlacementMode,
+ new Point(HorizontalOffset, VerticalOffset),
+ PlacementAnchor,
+ PlacementGravity,
+ PlacementConstraintAdjustment,
+ PlacementRect);
+ }
+ }
private static IDisposable SubscribeToEventHandler(T target, TEventHandler handler, Action subscribe, Action unsubscribe)
{
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.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
index 55e30396e1..5d7619d184 100644
--- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
+++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
@@ -30,13 +30,13 @@ namespace Avalonia.Dialogs
}
else
{
- using (Process process = Process.Start(new ProcessStartInfo
+ using Process process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- }));
+ });
}
}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs
index f9f38ac474..9058c405a3 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooser.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs
@@ -1,13 +1,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
-using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
-using Avalonia.Markup.Xaml;
namespace Avalonia.Dialogs
{
@@ -35,7 +33,9 @@ namespace Avalonia.Dialogs
if (_quickLinksRoot != null)
{
var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
+#pragma warning disable CS0618 // Type or member is obsolete
if (e.ClickCount == 2 || isQuickLink)
+#pragma warning restore CS0618 // Type or member is obsolete
{
if (model.ItemType == ManagedFileChooserItemType.File)
{
diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
index 050d618ce1..a217a67bc6 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
@@ -67,7 +67,7 @@ namespace Avalonia.Dialogs
{
Directory.GetFiles(x.VolumePath);
}
- catch (Exception _)
+ catch (Exception)
{
return null;
}
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 268171d467..63cbfb2dbe 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -47,6 +47,8 @@ namespace Avalonia.Headless
}
public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub();
+ public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException();
+ public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException();
public IRenderTarget CreateRenderTarget(IEnumerable