From 397267d1b5b39a11c700003039f3da67025e177f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 9 Aug 2021 23:46:20 +0300 Subject: [PATCH 01/75] [Menu] [Interaction] Allow end user to change menu show delay globally --- src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 984faa4d60..209feb351c 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -115,7 +115,7 @@ namespace Avalonia.Controls.Platform protected IMenu? Menu { get; private set; } - protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400); + public static TimeSpan MenuShowDelay { get; set; } = TimeSpan.FromMilliseconds(400); protected internal virtual void GotFocus(object sender, GotFocusEventArgs e) { From 8526d98d28d90912bd4ea8e5712d733d0ebc0448 Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Tue, 10 Aug 2021 03:35:43 +0200 Subject: [PATCH 02/75] Fix datagrid right click selection --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 35 +++++++++++++++++ .../DataGridCell.cs | 39 ++++++++++++------- .../DataGridRowGroupHeader.cs | 15 ++++++- .../DataGridRowHeader.cs | 17 +++++++- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 2f170c617d..00ae1b0e7d 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3002,6 +3002,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) { @@ -5674,6 +5680,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 0de4612958..1029d8ce25 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -161,21 +161,34 @@ 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) { - if (!e.Handled) - //if (!e.Handled && OwningGrid.IsTabStop) - { - OwningGrid.Focus(); - } - if (OwningRow != null) - { - e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); - OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; - } + OwningGrid.Focus(); + } + if (OwningRow != null) + { + e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + 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/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); + } + } } } From d082ad91d8c87e0972b5d340df8b4f5d07bb7015 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Wed, 25 Aug 2021 19:34:54 +0300 Subject: [PATCH 03/75] Update AvaloniaSynchronizationContext to don't wrap exceptions to AggregateException when invoking from non-UI thread The code suggested by the user makes sense to me. Reproed issue with ` try { var ctx = SynchronizationContext.Current; await Task.Run(() => ctx.Send(state => throw new ArgumentException("hello"), null)); } catch (ArgumentException ex) { Console.WriteLine(ex.Message); } catch (AggregateException ex) { Console.WriteLine(ex.Message); }` this code --- src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); } From 9682f014b2e71ded52b82d0e602a56506147b883 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 28 Aug 2021 15:41:52 +0300 Subject: [PATCH 04/75] [OSX] fix middle button #5784 --- native/Avalonia.Native/src/OSX/window.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 14fe60ab0b..e15f4cc311 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1672,6 +1672,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = true; [self mouseEvent:event withType:MiddleButtonDown]; @@ -1704,6 +1705,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = false; [self mouseEvent:event withType:MiddleButtonUp]; From 56709c9982d78063984c4865278a7afd913e571b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 Aug 2021 14:59:29 -0400 Subject: [PATCH 05/75] Add text box clipboard events --- src/Avalonia.Controls/TextBox.cs | 47 +++++++++++++++---- .../TextBoxClipboardEventArgs.cs | 18 +++++++ 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Controls/TextBoxClipboardEventArgs.cs diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0eade8d6df..657d8fae8f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -500,6 +500,10 @@ namespace Avalonia.Controls } } + public event EventHandler CopyingToClipboard; + public event EventHandler CuttingToClipboard; + public event EventHandler PastingFromClipboard; + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -638,27 +642,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 TextBoxClipboardEventArgs(); + CuttingToClipboard?.Invoke(this, 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 TextBoxClipboardEventArgs(); + CopyingToClipboard?.Invoke(this, eventArgs); + if (!eventArgs.Handled) + { + await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) + .SetTextAsync(text); + } } public async void Paste() { + var eventArgs = new TextBoxClipboardEventArgs(); + PastingFromClipboard?.Invoke(this, 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.Controls/TextBoxClipboardEventArgs.cs b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs new file mode 100644 index 0000000000..0fe4cadf39 --- /dev/null +++ b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Provides event data for the , and events. + /// + /// + /// If you perform any action in the handler for a clipboard event, set the Handled property to true; otherwise, the default action is performed. + /// + public class TextBoxClipboardEventArgs : EventArgs + { + /// + /// Gets or sets a value that marks the event as handled. A true value for Handled prevents most handlers along the event from handling the same event again. + /// + public bool Handled { get; set; } + } +} From 825ddc9ccaa2e3d7e002564f96f22c3b1d23a2d8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 Aug 2021 18:29:56 -0400 Subject: [PATCH 06/75] Use routed events --- src/Avalonia.Controls/TextBox.cs | 44 +++++++++++++++---- .../TextBoxClipboardEventArgs.cs | 18 -------- 2 files changed, 35 insertions(+), 27 deletions(-) delete mode 100644 src/Avalonia.Controls/TextBoxClipboardEventArgs.cs diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 657d8fae8f..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,9 +512,23 @@ namespace Avalonia.Controls } } - public event EventHandler CopyingToClipboard; - public event EventHandler CuttingToClipboard; - public event EventHandler PastingFromClipboard; + 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) { @@ -647,8 +673,8 @@ namespace Avalonia.Controls return; } - var eventArgs = new TextBoxClipboardEventArgs(); - CuttingToClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent); + RaiseEvent(eventArgs); if (!eventArgs.Handled) { SnapshotUndoRedo(); @@ -666,8 +692,8 @@ namespace Avalonia.Controls return; } - var eventArgs = new TextBoxClipboardEventArgs(); - CopyingToClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent); + RaiseEvent(eventArgs); if (!eventArgs.Handled) { await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) @@ -677,8 +703,8 @@ namespace Avalonia.Controls public async void Paste() { - var eventArgs = new TextBoxClipboardEventArgs(); - PastingFromClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent); + RaiseEvent(eventArgs); if (eventArgs.Handled) { return; diff --git a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs deleted file mode 100644 index 0fe4cadf39..0000000000 --- a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - /// - /// Provides event data for the , and events. - /// - /// - /// If you perform any action in the handler for a clipboard event, set the Handled property to true; otherwise, the default action is performed. - /// - public class TextBoxClipboardEventArgs : EventArgs - { - /// - /// Gets or sets a value that marks the event as handled. A true value for Handled prevents most handlers along the event from handling the same event again. - /// - public bool Handled { get; set; } - } -} From 7a027792cbfc4426bea431ea4b24268ef68c68a3 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 30 Aug 2021 18:39:21 +0300 Subject: [PATCH 07/75] add failing test --- .../Avalonia.Base.UnitTests.csproj | 1 + .../Logging/LoggingTests.cs | 214 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 32f90d5cbf..c4c1f49346 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs b/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs new file mode 100644 index 0000000000..193d5e3a45 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Logging; +using Avalonia.Markup.Xaml; +using Avalonia.UnitTests; +using Avalonia.Utilities; +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 = @" + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + using var logSink = new StubLogSink(LogEventLevel.Warning); + var panel = window.FindControl("panel"); + var rect = window.FindControl("rect"); + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + panel.Children.Remove(rect); + Assert.Equal(0, logSink.Results.Count); + } + } + } + + class StubLogSink : ILogSink, IDisposable + { + LogEventLevel _level; + public StubLogSink(LogEventLevel level) + { + _level = level; + Logger.Sink = this; + } + public void Dispose() + { + Logger.Sink = null; + } + public List Results { get; set; } = new List(); + + public bool IsEnabled(LogEventLevel level, string area) + { + return true; + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate) + { + if (level >= _level) + { + Results.Add(Format(area, messageTemplate, source)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) + { + if (level >= _level) + { + Results.Add(Format(area, messageTemplate, source, propertyValue0)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) + { + if (level >= _level) + { + Results.Add(Format(area, messageTemplate, source, propertyValue0, propertyValue1)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) + { + if (level >= _level) + { + Results.Add(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2)); + } + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + { + if (level >= _level) + { + Results.Add(Format(area, messageTemplate, source, propertyValues)); + } + } + #region Copy-Pasta + private static string Format( + string area, + string template, + object source, + T0 v0 = default, + T1 v1 = default, + T2 v2 = default) + { + var result = new StringBuilder(template.Length); + var r = new CharacterReader(template.AsSpan()); + var i = 0; + + result.Append('['); + result.Append(area); + result.Append("] "); + + while (!r.End) + { + var c = r.Take(); + + if (c != '{') + { + result.Append(c); + } + else + { + if (r.Peek != '{') + { + result.Append('\''); + result.Append(i++ switch + { + 0 => v0, + 1 => v1, + 2 => v2, + _ => null + }); + result.Append('\''); + r.TakeUntil('}'); + r.Take(); + } + else + { + result.Append('{'); + r.Take(); + } + } + } + + if (source is object) + { + result.Append(" ("); + result.Append(source.GetType().Name); + result.Append(" #"); + result.Append(source.GetHashCode()); + result.Append(')'); + } + + return result.ToString(); + } + + private static string Format( + string area, + string template, + object source, + object[] v) + { + var result = new StringBuilder(template.Length); + var r = new CharacterReader(template.AsSpan()); + var i = 0; + + result.Append('['); + result.Append(area); + result.Append(']'); + + while (!r.End) + { + var c = r.Take(); + + if (c != '{') + { + result.Append(c); + } + else + { + if (r.Peek != '{') + { + result.Append('\''); + result.Append(i < v.Length ? v[i++] : null); + result.Append('\''); + r.TakeUntil('}'); + r.Take(); + } + else + { + result.Append('{'); + r.Take(); + } + } + } + + if (source is object) + { + result.Append('('); + result.Append(source.GetType().Name); + result.Append(" #"); + result.Append(source.GetHashCode()); + result.Append(')'); + } + + return result.ToString(); + } + #endregion + } +} From 59ebe6e0c8edbaecf24d0edc5325b43502ad2e8c Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 10:33:34 +0200 Subject: [PATCH 08/75] fixes(Dialogs): Warning CS0642 Possible mistaken empty statement --- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) - })); + }); } } From 98a0f43f9ec5ccff309813ba36ad21987fc03bf6 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 10:36:20 +0200 Subject: [PATCH 09/75] fixes(Dialogs): Suppress warning CS0618 'PointerPressedEventArgs.ClickCount' is obsolete: 'Use DoubleTapped event or Gestures.DoubleRightTapped attached event' --- src/Avalonia.Dialogs/ManagedFileChooser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) { From 17229f80a733964f68fc360faecbd3a150ab7b08 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 10:41:20 +0200 Subject: [PATCH 10/75] fixes(Dialogs): Warning CS0168 The variable '_' is declared but never used --- src/Avalonia.Dialogs/ManagedFileChooserSources.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From 6f4f5d9bed25b9c043a507ae2daa1d40862b43b2 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Tue, 31 Aug 2021 12:55:32 +0300 Subject: [PATCH 11/75] [CI] Update Ubuntu to Ubuntu 20.04 LTS because current one would be deprecated on September 20, 2021 --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fbd8507193..a987fa6f77 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,7 @@ jobs: - job: Linux pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-18.04' steps: - task: CmdLine@2 displayName: 'Install Nuke' From 8cdabdda8f54eca19140f59a9019e87e73d9756e Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Tue, 31 Aug 2021 13:10:02 +0300 Subject: [PATCH 12/75] fix --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a987fa6f77..11ef36d43f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,7 @@ jobs: - job: Linux pool: - vmImage: 'ubuntu-18.04' + vmImage: 'ubuntu-20.04' steps: - task: CmdLine@2 displayName: 'Install Nuke' From 22432a002dc9ba86429d582c08103e17f3e50433 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 31 Aug 2021 13:14:59 +0300 Subject: [PATCH 13/75] test --- src/Avalonia.Styling/LogicalTree/ControlLocator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 6d0302ace4..e89bf55404 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -57,7 +57,7 @@ namespace Avalonia.LogicalTree private void Detached(object sender, LogicalTreeAttachmentEventArgs e) { _value = null; - PublishNext(null); + // PublishNext(null); } private void Update() From 68370e37a5d5340613369e49f8aa9f9e5b7f31d1 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 16:14:15 +0200 Subject: [PATCH 14/75] fixes(DataGrid): Issue #6508 does not re-enter the edit mode after pressing ESC --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index ab1aff9220..cb1ce0c731 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -4489,8 +4489,19 @@ namespace Avalonia.Controls element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext); if (element != null) { - // Subscribe to the new element's events - element.Initialized += EditingElement_Initialized; + + if (element.IsInitialized) + { + Threading.Dispatcher.UIThread.Post(() => + { + PreparingCellForEditPrivate(element as Control); + }); + } + else + { + // Subscribe to the new element's events + element.Initialized += EditingElement_Initialized; + } } } else From 3aa38398ff28012d7d296966a6df7d86e42f1424 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Tue, 31 Aug 2021 23:47:25 +0300 Subject: [PATCH 15/75] Fix alt down shortcuts and allow alt down handling for end users (#6491) * [Menu] [Interaction] Allow end user to change menu show delay globally * Fix all alt down handle = true by AccessKeyHandler Co-authored-by: Max Katz --- src/Avalonia.Input/AccessKeyHandler.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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) { From 063e5e7be3b94d4a3f929acbd2ac552534877e41 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 11:42:16 +0300 Subject: [PATCH 16/75] test --- src/Avalonia.Styling/LogicalTree/ControlLocator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index e89bf55404..501a26c848 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -57,7 +57,10 @@ namespace Avalonia.LogicalTree private void Detached(object sender, LogicalTreeAttachmentEventArgs e) { _value = null; - // PublishNext(null); + if (_relativeTo.IsAttachedToLogicalTree) + { + PublishNext(null); + } } private void Update() From 01518b37e78b44dff6f7839672b6fe5575771a08 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 12:30:23 +0300 Subject: [PATCH 17/75] test --- src/Avalonia.Styling/LogicalTree/ControlLocator.cs | 6 ++---- src/Avalonia.Visuals/Visual.cs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 501a26c848..3f5b1ee0b5 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -57,10 +57,8 @@ namespace Avalonia.LogicalTree private void Detached(object sender, LogicalTreeAttachmentEventArgs e) { _value = null; - if (_relativeTo.IsAttachedToLogicalTree) - { - PublishNext(null); - } + PublishNext(null); + } private void Update() diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 2c96d87bb6..163c3db5fd 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -493,7 +493,7 @@ namespace Avalonia // 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) + if (((ILogical)this).IsAttachedToLogicalTree) { if (e is BindingChainException b && string.IsNullOrEmpty(b.ExpressionErrorPoint) && From 5c1fd27d1f0f31cf2c5c5a60aa634c7d3fc58914 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 15:00:08 +0300 Subject: [PATCH 18/75] wip --- src/Avalonia.Visuals/Visual.cs | 5 +- .../Logging/LoggingTests.cs | 191 +----------------- 2 files changed, 12 insertions(+), 184 deletions(-) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 163c3db5fd..322b630e83 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -489,10 +489,7 @@ 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. + // Don't log a binding error unless the control is attached to a logical tree. if (((ILogical)this).IsAttachedToLogicalTree) { if (e is BindingChainException b && diff --git a/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs b/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs index 193d5e3a45..9e1885463d 100644 --- a/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs +++ b/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Controls.Shapes; -using Avalonia.Logging; using Avalonia.Markup.Xaml; using Avalonia.UnitTests; -using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests.Logging @@ -28,187 +23,23 @@ namespace Avalonia.Base.UnitTests.Logging "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); - using var logSink = new StubLogSink(LogEventLevel.Warning); + 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"); var rect = window.FindControl("rect"); window.ApplyTemplate(); window.Presenter.ApplyTemplate(); panel.Children.Remove(rect); - Assert.Equal(0, logSink.Results.Count); - } - } - } - - class StubLogSink : ILogSink, IDisposable - { - LogEventLevel _level; - public StubLogSink(LogEventLevel level) - { - _level = level; - Logger.Sink = this; - } - public void Dispose() - { - Logger.Sink = null; - } - public List Results { get; set; } = new List(); - - public bool IsEnabled(LogEventLevel level, string area) - { - return true; - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate) - { - if (level >= _level) - { - Results.Add(Format(area, messageTemplate, source)); - } - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0) - { - if (level >= _level) - { - Results.Add(Format(area, messageTemplate, source, propertyValue0)); - } - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1) - { - if (level >= _level) - { - Results.Add(Format(area, messageTemplate, source, propertyValue0, propertyValue1)); - } - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2) - { - if (level >= _level) - { - Results.Add(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2)); - } - } - - public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) - { - if (level >= _level) - { - Results.Add(Format(area, messageTemplate, source, propertyValues)); - } - } - #region Copy-Pasta - private static string Format( - string area, - string template, - object source, - T0 v0 = default, - T1 v1 = default, - T2 v2 = default) - { - var result = new StringBuilder(template.Length); - var r = new CharacterReader(template.AsSpan()); - var i = 0; - - result.Append('['); - result.Append(area); - result.Append("] "); - - while (!r.End) - { - var c = r.Take(); - - if (c != '{') - { - result.Append(c); - } - else - { - if (r.Peek != '{') - { - result.Append('\''); - result.Append(i++ switch - { - 0 => v0, - 1 => v1, - 2 => v2, - _ => null - }); - result.Append('\''); - r.TakeUntil('}'); - r.Take(); - } - else - { - result.Append('{'); - r.Take(); - } - } + Assert.Equal(0, calledTimes); } - - if (source is object) - { - result.Append(" ("); - result.Append(source.GetType().Name); - result.Append(" #"); - result.Append(source.GetHashCode()); - result.Append(')'); - } - - return result.ToString(); } + } - private static string Format( - string area, - string template, - object source, - object[] v) - { - var result = new StringBuilder(template.Length); - var r = new CharacterReader(template.AsSpan()); - var i = 0; - - result.Append('['); - result.Append(area); - result.Append(']'); - - while (!r.End) - { - var c = r.Take(); - - if (c != '{') - { - result.Append(c); - } - else - { - if (r.Peek != '{') - { - result.Append('\''); - result.Append(i < v.Length ? v[i++] : null); - result.Append('\''); - r.TakeUntil('}'); - r.Take(); - } - else - { - result.Append('{'); - r.Take(); - } - } - } - - if (source is object) - { - result.Append('('); - result.Append(source.GetType().Name); - result.Append(" #"); - result.Append(source.GetHashCode()); - result.Append(')'); - } - return result.ToString(); - } - #endregion - } } From 77f4a6e808a37664c5a4aa23e171129f02dd9971 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 15:17:16 +0300 Subject: [PATCH 19/75] more tests --- .../Logging/LoggingTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs b/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs index 9e1885463d..d160713023 100644 --- a/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs +++ b/tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs @@ -39,6 +39,34 @@ namespace Avalonia.Base.UnitTests.Logging 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 = @" + + + + +"; + 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); + } + } } From df5a9c7afc1726beb5a1338c7175ee56243afea2 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 15:18:17 +0300 Subject: [PATCH 20/75] wip --- src/Avalonia.Styling/LogicalTree/ControlLocator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 3f5b1ee0b5..6d0302ace4 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -58,7 +58,6 @@ namespace Avalonia.LogicalTree { _value = null; PublishNext(null); - } private void Update() From 2270b0926687bac941f55ac5361ab55f8e4c21f5 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 17:30:19 +0300 Subject: [PATCH 21/75] revert --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } } From 2a09574ba70b0bebdfb9339279fc38ef2a525c11 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Sep 2021 09:52:34 +0200 Subject: [PATCH 22/75] Revert "Fixed gradient brush target rectangles." Reverts #6066. Keeps the render tests to demonstrate that the WPF behavior can be reproduced by pushing a translation. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 47 +++++++++---------- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 4 +- .../Media/AvaloniaTextRenderer.cs | 4 +- .../Media/DrawingContextImpl.cs | 32 +++++++------ .../Media/LinearGradientBrushImpl.cs | 7 ++- .../Media/RadialGradientBrushImpl.cs | 11 ++--- .../Media/ConicGradientBrushTests.cs | 3 +- .../Media/LinearGradientBrushTests.cs | 12 +++-- .../Media/RadialGradientBrushTests.cs | 3 +- 9 files changed, 63 insertions(+), 60 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 1f186396ab..d812f6a059 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -164,7 +164,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { - using (var paint = CreatePaint(_strokePaint, pen, new Rect(p1, p2).Normalize())) + using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { if (paint.Paint is object) { @@ -177,10 +177,10 @@ namespace Avalonia.Skia public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { var impl = (GeometryImpl) geometry; - var rect = geometry.Bounds; + var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(_fillPaint, brush, rect) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, rect) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -354,7 +354,7 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Rect)) + using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) { if (isRounded) { @@ -397,7 +397,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Rect)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size)) { if (paint.Paint is object) { @@ -417,7 +417,7 @@ namespace Avalonia.Skia /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds)) + using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size)) { var textImpl = (FormattedTextImpl) text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); @@ -427,7 +427,7 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var paintWrapper = CreatePaint(_fillPaint, foreground, new Rect(glyphRun.Size))) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -537,7 +537,7 @@ namespace Avalonia.Skia var paint = new SKPaint(); Canvas.SaveLayer(paint); - _maskStack.Push(CreatePaint(paint, mask, bounds, true)); + _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true)); } /// @@ -593,19 +593,18 @@ namespace Avalonia.Skia /// Paint wrapper. /// Target bound rect. /// Gradient brush. - private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush) + private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) { var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode(); var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray(); var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray(); - var position = targetRect.Position.ToSKPoint(); switch (gradientBrush) { case ILinearGradientBrush linearGradient: { - var start = position + linearGradient.StartPoint.ToPixels(targetRect.Size).ToSKPoint(); - var end = position + linearGradient.EndPoint.ToPixels(targetRect.Size).ToSKPoint(); + var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint(); + var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? using (var shader = @@ -618,10 +617,10 @@ namespace Avalonia.Skia } case IRadialGradientBrush radialGradient: { - var center = position + radialGradient.Center.ToPixels(targetRect.Size).ToSKPoint(); - var radius = (float)(radialGradient.Radius * targetRect.Width); + var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); + var radius = (float)(radialGradient.Radius * targetSize.Width); - var origin = position + radialGradient.GradientOrigin.ToPixels(targetRect.Size).ToSKPoint(); + var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint(); if (origin.Equals(center)) { @@ -666,7 +665,7 @@ namespace Avalonia.Skia } case IConicGradientBrush conicGradient: { - var center = position + conicGradient.Center.ToPixels(targetRect.Size).ToSKPoint(); + var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint(); // Skia's default is that angle 0 is from the right hand side of the center point // but we are matching CSS where the vertical point above the center is 0. @@ -868,10 +867,10 @@ namespace Avalonia.Skia /// /// The paint to wrap. /// Source brush. - /// Target rect. + /// Target size. /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Rect targetRect, bool disposePaint = false) + internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false) { var paintWrapper = new PaintWrapper(paint, disposePaint); @@ -890,7 +889,7 @@ namespace Avalonia.Skia if (brush is IGradientBrush gradient) { - ConfigureGradientBrush(ref paintWrapper, targetRect, gradient); + ConfigureGradientBrush(ref paintWrapper, targetSize, gradient); return paintWrapper; } @@ -910,7 +909,7 @@ namespace Avalonia.Skia if (tileBrush != null && tileBrushImage != null) { - ConfigureTileBrush(ref paintWrapper, targetRect.Size, tileBrush, tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage); } else { @@ -925,10 +924,10 @@ namespace Avalonia.Skia /// /// The paint to wrap. /// Source pen. - /// Target rect. + /// Target size. /// Optional dispose of the supplied paint. /// - private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Rect targetRect, bool disposePaint = false) + private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. @@ -937,7 +936,7 @@ namespace Avalonia.Skia return default; } - var rv = CreatePaint(paint, pen.Brush, targetRect, disposePaint); + var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 3eca42faa9..5f4980e461 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -278,9 +278,9 @@ namespace Avalonia.Skia if (fb != null) { - //TODO: figure out how to get the brush rect + //TODO: figure out how to get the brush size currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, - default); + new Size()); } else { diff --git a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs index e4b2405290..22c998df93 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs @@ -34,10 +34,10 @@ namespace Avalonia.Direct2D1.Media { var wrapper = clientDrawingEffect as BrushWrapper; - // TODO: Work out how to get the rect below rather than passing default. + // TODO: Work out how to get the size below rather than passing new Size(). var brush = (wrapper == null) ? _foreground : - _context.CreateBrush(wrapper.Brush, default).PlatformBrush; + _context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush; _renderTarget.DrawGlyphRun( new RawVector2 { X = baselineOriginX, Y = baselineOriginY }, diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 09e5b7c71a..622f47f953 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -192,7 +192,7 @@ namespace Avalonia.Direct2D1.Media { using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value)) - using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect)) + using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D())) { if (d2dOpacityMask.PlatformBrush != null) @@ -217,7 +217,9 @@ namespace Avalonia.Direct2D1.Media { if (pen != null) { - using (var d2dBrush = CreateBrush(pen.Brush, new Rect(p1, p2).Normalize())) + var size = new Rect(p1, p2).Size; + + using (var d2dBrush = CreateBrush(pen.Brush, size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (d2dBrush.PlatformBrush != null) @@ -243,7 +245,7 @@ namespace Avalonia.Direct2D1.Media { if (brush != null) { - using (var d2dBrush = CreateBrush(brush, geometry.Bounds)) + using (var d2dBrush = CreateBrush(brush, geometry.Bounds.Size)) { if (d2dBrush.PlatformBrush != null) { @@ -255,7 +257,7 @@ namespace Avalonia.Direct2D1.Media if (pen != null) { - using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen))) + using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (d2dBrush.PlatformBrush != null) @@ -280,7 +282,7 @@ namespace Avalonia.Direct2D1.Media if (brush != null) { - using (var b = CreateBrush(brush, rect)) + using (var b = CreateBrush(brush, rect.Size)) { if (b.PlatformBrush != null) { @@ -309,7 +311,7 @@ namespace Avalonia.Direct2D1.Media if (pen?.Brush != null) { - using (var wrapper = CreateBrush(pen.Brush, rect)) + using (var wrapper = CreateBrush(pen.Brush, rect.Size)) using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext)) { if (wrapper.PlatformBrush != null) @@ -347,7 +349,7 @@ namespace Avalonia.Direct2D1.Media { var impl = (FormattedTextImpl)text; - using (var brush = CreateBrush(foreground, impl.Bounds)) + using (var brush = CreateBrush(foreground, impl.Bounds.Size)) using (var renderer = new AvaloniaTextRenderer(this, _deviceContext, brush.PlatformBrush)) { if (brush.PlatformBrush != null) @@ -365,7 +367,7 @@ namespace Avalonia.Direct2D1.Media /// The glyph run. public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { - using (var brush = CreateBrush(foreground, new Rect(glyphRun.Size))) + using (var brush = CreateBrush(foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -456,9 +458,9 @@ namespace Avalonia.Direct2D1.Media /// Creates a Direct2D brush wrapper for a Avalonia brush. /// /// The avalonia brush. - /// The brush's target area. + /// The size of the brush's target area. /// The Direct2D brush wrapper. - public BrushImpl CreateBrush(IBrush brush, Rect destinationRect) + public BrushImpl CreateBrush(IBrush brush, Size destinationSize) { var solidColorBrush = brush as ISolidColorBrush; var linearGradientBrush = brush as ILinearGradientBrush; @@ -473,11 +475,11 @@ namespace Avalonia.Direct2D1.Media } else if (linearGradientBrush != null) { - return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationRect); + return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationSize); } else if (radialGradientBrush != null) { - return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationRect); + return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize); } else if (conicGradientBrush != null) { @@ -490,7 +492,7 @@ namespace Avalonia.Direct2D1.Media imageBrush, _deviceContext, (BitmapImpl)imageBrush.Source.PlatformImpl.Item, - destinationRect.Size); + destinationSize); } else if (visualBrush != null) { @@ -521,7 +523,7 @@ namespace Avalonia.Direct2D1.Media visualBrush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), - destinationRect.Size); + destinationSize); } } } @@ -573,7 +575,7 @@ namespace Avalonia.Direct2D1.Media ContentBounds = PrimitiveExtensions.RectangleInfinite, MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = 1, - OpacityBrush = CreateBrush(mask, bounds).PlatformBrush + OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush }; var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext); _deviceContext.PushLayer(ref parameters, layer); diff --git a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs index 69b45455ac..0e63d4cc03 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media public LinearGradientBrushImpl( ILinearGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, - Rect destinationRect) + Size destinationSize) { if (brush.GradientStops.Count == 0) { @@ -21,9 +21,8 @@ namespace Avalonia.Direct2D1.Media Position = (float)s.Offset }).ToArray(); - var position = destinationRect.Position; - var startPoint = position + brush.StartPoint.ToPixels(destinationRect.Size); - var endPoint = position + brush.EndPoint.ToPixels(destinationRect.Size); + var startPoint = brush.StartPoint.ToPixels(destinationSize); + var endPoint = brush.EndPoint.ToPixels(destinationSize); using (var stops = new SharpDX.Direct2D1.GradientStopCollection( target, diff --git a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs index 7dcfd7e1e0..1fca6d4e33 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media public RadialGradientBrushImpl( IRadialGradientBrush brush, SharpDX.Direct2D1.RenderTarget target, - Rect destinationRect) + Size destinationSize) { if (brush.GradientStops.Count == 0) { @@ -21,13 +21,12 @@ namespace Avalonia.Direct2D1.Media Position = (float)s.Offset }).ToArray(); - var position = destinationRect.Position; - var centerPoint = position + brush.Center.ToPixels(destinationRect.Size); - var gradientOrigin = position + brush.GradientOrigin.ToPixels(destinationRect.Size) - centerPoint; + var centerPoint = brush.Center.ToPixels(destinationSize); + var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint; // Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property - var radiusX = brush.Radius * destinationRect.Width; - var radiusY = brush.Radius * destinationRect.Height; + var radiusX = brush.Radius * destinationSize.Width; + var radiusY = brush.Radius * destinationSize.Height; using (var stops = new SharpDX.Direct2D1.GradientStopCollection( target, diff --git a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs index 3d2f09e2a8..ef400410a4 100644 --- a/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs @@ -200,7 +200,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media Child = new DrawnControl(c => { c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); - c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100)); + using (c.PushPreTransform(Matrix.CreateTranslation(100, 100))) + c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); }), }; diff --git a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs index 28701f2f97..dd9d2f9b39 100644 --- a/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs @@ -81,10 +81,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative), GradientStops = - { - new GradientStop { Color = Colors.Red, Offset = 0 }, - new GradientStop { Color = Colors.Blue, Offset = 1 } - } + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + } }; Decorator target = new Decorator @@ -94,7 +94,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media Child = new DrawnControl(c => { c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); - c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100)); + + using (c.PushPreTransform(Matrix.CreateTranslation(100, 100))) + c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); }), }; diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index 95fae7f2fa..e52f844359 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -185,7 +185,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media Child = new DrawnControl(c => { c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); - c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100)); + using (c.PushPreTransform(Matrix.CreateTranslation(100, 100))) + c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100)); }), }; From f14816144b6e680de653632a8c2ce02d2f4773c2 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Thu, 2 Sep 2021 13:32:37 +0300 Subject: [PATCH 23/75] Compiler intrinsic based type nullability check --- .../Data/Converters/FuncValueConverter.cs | 2 +- src/Avalonia.Base/Utilities/TypeUtilities.cs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs index 9ec600d2bc..c0a3c6ad0f 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 (value is TIn || (value == null && TypeUtilities.AcceptsNull())) { return _convert((TIn)value); } 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()); } /// From 0c20e122ba877db3ad702ce235858aef6ec590be Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Thu, 2 Sep 2021 13:36:47 +0300 Subject: [PATCH 24/75] Code deduplication --- src/Avalonia.Base/Data/Converters/FuncValueConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncValueConverter.cs index c0a3c6ad0f..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())) + if (TypeUtilities.CanCast(value)) { return _convert((TIn)value); } From 60b3e028b5eca793b146f6bb7fa5c27b79bbb9b6 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 2 Sep 2021 16:24:52 +0300 Subject: [PATCH 25/75] fix --- src/Avalonia.Controls/AutoCompleteBox.cs | 16 +++++++++++++++- .../AutoCompleteBoxTests.cs | 10 ++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 5a6e78f441..805431eeea 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 == null) + { + throw new Exception("ItemFilter property can not be unassigned when FilterMode has value AutoCompleteFilterMode.Custom"); + } + else + { + inResults = ItemFilter(text, item); + } + } } if (view_count > view_index && inResults && _view[view_index] == item) 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() { From 3c33ee41b1266a70bb624a71d71b9ed33dd921b3 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 2 Sep 2021 16:39:48 +0300 Subject: [PATCH 26/75] fix --- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 805431eeea..265a354af6 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2102,7 +2102,7 @@ namespace Avalonia.Controls { if (ItemFilter == null) { - throw new Exception("ItemFilter property can not be unassigned when FilterMode has value AutoCompleteFilterMode.Custom"); + throw new Exception("ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom"); } else { From b6650a8e4b0acceae33de13a9918760daaaafa84 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 3 Sep 2021 18:23:54 +0800 Subject: [PATCH 27/75] Update src/Avalonia.Controls/AutoCompleteBox.cs --- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 265a354af6..0e946126ea 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2100,7 +2100,7 @@ namespace Avalonia.Controls } else { - if (ItemFilter == null) + if (ItemFilter is null) { throw new Exception("ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom"); } From 846cdb411243d817a451023dbf8c6272b1509b9a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 6 Sep 2021 15:33:54 +0100 Subject: [PATCH 28/75] correctly implement fullscreen mode so that app ca be started in fullscreen. --- native/Avalonia.Native/src/OSX/window.mm | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 14fe60ab0b..35c97f1701 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]; } From 69a39974514848da6187672e74e43b5dbe6f58ab Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 7 Sep 2021 12:11:38 +0300 Subject: [PATCH 29/75] upd --- src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 35bc310fcdd053f43e754d6d34110cf19989f00c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 7 Sep 2021 11:17:43 +0100 Subject: [PATCH 30/75] win32 - dont loose window state when hide and show are called programatically. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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; } } From fcece1a215098a0ed66ee11e94cbd091abebd4f3 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Tue, 7 Sep 2021 23:53:43 +0700 Subject: [PATCH 31/75] DotSettings: enable XAML highlighting --- Avalonia.sln.DotSettings | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From c6f9c6a82015677559580b99844aac044d84f262 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Wed, 8 Sep 2021 13:05:26 +0300 Subject: [PATCH 32/75] Implement MaskedTextBox (#6453) --- samples/ControlCatalog/Pages/TextBoxPage.xaml | 1 + src/Avalonia.Controls/MaskedTextBox.cs | 433 ++++++++ .../MaskedTextBoxTests.cs | 990 ++++++++++++++++++ 3 files changed, 1424 insertions(+) create mode 100644 src/Avalonia.Controls/MaskedTextBox.cs create mode 100644 tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs 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.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/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 + { + 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().ToSingleton(); + + var clipboard = AvaloniaLocator.CurrentMutable.GetService(); + 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().ToSingleton(); + + 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()); + + private static TestServices Services => TestServices.MockThreadingInterface.With( + standardCursorFactory: Mock.Of()); + + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate((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 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 GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult((object)null); + } + + private class TestContextMenu : ContextMenu + { + public TestContextMenu() + { + IsOpen = true; + } + } + } +} + From 907b55cb7e6c3ea93e78a1ed097b3b0441d196ab Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 16:02:58 +0100 Subject: [PATCH 33/75] CompiledBinding correctly locates IDataContextProvider as anchor. (implementation was in ReflectionBinding but missing from CompiledBinding) --- .../MarkupExtensions/CompiledBindingExtension.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 17d2ea7ae9..5c4d9315d5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -44,6 +44,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // the context. object anchor = provider.GetFirstParent(); + 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(); + } + // 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 ?? From 8f833dbb2fba5d9a4df0946eef1122bc4f097d87 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 9 Sep 2021 19:16:32 +0300 Subject: [PATCH 34/75] it works --- packages/Avalonia/AvaloniaBuildTasks.targets | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 45a7f1aa44..a18930527f 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -42,12 +42,20 @@ - $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences + $(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;ReGenerateAvaloniaResourcesOnResourceDeletion + + + + + + + + From aa5ae2360748056304b643d806e46fc061c607c2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 18:17:32 +0100 Subject: [PATCH 35/75] move duplicated code to an extension method. --- .../Avalonia.Markup.Xaml.csproj | 1 + .../CompiledBindingExtension.cs | 25 +--------------- .../IServiceProviderExtensions.cs | 30 +++++++++++++++++++ .../ReflectionBindingExtension.cs | 23 +------------- 4 files changed, 33 insertions(+), 46 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 217da2d50d..8f118c7b2f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -26,6 +26,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 5c4d9315d5..41de2355aa 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/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,31 +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(); - - 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(); - } - - // 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()?.RootObject as IStyle ?? - provider.GetLastParent(); - } - protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { if (Source != null) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs new file mode 100644 index 0000000000..a2bc19bbce --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + internal static class IServiceProviderExtensions + { + 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(); + + 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(); + } + + // 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()?.RootObject as IStyle ?? + provider.GetLastParent(); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs index 10770365a3..d373ed852a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs +++ b/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(serviceProvider.GetService()) }; } - 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(); - - 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(); - } - - // 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()?.RootObject as IStyle ?? - context.GetLastParent(); - } - public IValueConverter Converter { get; set; } public object ConverterParameter { get; set; } From 42763792f44ec9317b621bf3633deeacf770dfb9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 18:37:44 +0100 Subject: [PATCH 36/75] use existing extensions class. --- src/Markup/Avalonia.Markup.Xaml/Extensions.cs | 23 ++++++++++++++ .../IServiceProviderExtensions.cs | 30 ------------------- 2 files changed, 23 insertions(+), 30 deletions(-) delete mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs b/src/Markup/Avalonia.Markup.Xaml/Extensions.cs index fe3fd44c1c..263750c316 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Extensions.cs +++ b/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(); + + 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(); + } + + // 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()?.RootObject as IStyle ?? + provider.GetLastParent(); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs deleted file mode 100644 index a2bc19bbce..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/IServiceProviderExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Styling; - -namespace Avalonia.Markup.Xaml.MarkupExtensions -{ - internal static class IServiceProviderExtensions - { - 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(); - - 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(); - } - - // 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()?.RootObject as IStyle ?? - provider.GetLastParent(); - } - } -} From 5b28a2d94cf52e573f63a0164237010d50058113 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 19:42:14 +0100 Subject: [PATCH 37/75] fix build. --- src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 8f118c7b2f..217da2d50d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -26,7 +26,6 @@ - From 890eabbd1fb2e211aaadb3a8bb8b3cb1cf6b4e68 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 10 Sep 2021 11:27:17 +0300 Subject: [PATCH 38/75] upd --- packages/Avalonia/AvaloniaBuildTasks.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index a18930527f..7fbe939390 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -50,12 +50,12 @@ - + From c919109be60d96900961cb1917314903eada4bfb Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 10 Sep 2021 11:41:07 +0200 Subject: [PATCH 39/75] fixes(DataGrid): Issue #6567 New line in Editable DataGrid, Not editable --- .../DataGridDataConnection.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 { From 89cb07677824ed70584b5379b314e63ec0e18fb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Sep 2021 15:16:47 +0200 Subject: [PATCH 40/75] Implement GeometryGroup. --- .../HeadlessPlatformRenderInterface.cs | 1 + .../Media/GeometryCollection.cs | 37 +++++ src/Avalonia.Visuals/Media/GeometryGroup.cs | 80 +++++++++ .../Platform/IPlatformRenderInterface.cs | 8 + src/Skia/Avalonia.Skia/GeometryGroupImpl.cs | 36 ++++ .../Avalonia.Skia/PlatformRenderInterface.cs | 5 + .../Avalonia.Direct2D1/Direct2D1Platform.cs | 1 + .../Media/GeometryGroupImpl.cs | 33 ++++ .../NullRenderingPlatform.cs | 5 + .../Media/GeometryGroupTests.cs | 154 ++++++++++++++++++ .../MockPlatformRenderInterface.cs | 5 + .../Media/GeometryGroupTests.cs | 26 +++ .../VisualTree/MockRenderInterface.cs | 5 + .../FillRule_EvenOdd.expected.png | Bin 0 -> 2349 bytes .../FillRule_EvenOdd_Stroke.expected.png | Bin 0 -> 4047 bytes .../FillRule_NonZero.expected.png | Bin 0 -> 1966 bytes .../FillRule_NonZero_Stroke.expected.png | Bin 0 -> 3880 bytes .../FillRule_EvenOdd.expected.png | Bin 0 -> 2607 bytes .../FillRule_EvenOdd_Stroke.expected.png | Bin 0 -> 3814 bytes .../FillRule_NonZero.expected.png | Bin 0 -> 2137 bytes .../FillRule_NonZero_Stroke.expected.png | Bin 0 -> 3693 bytes 21 files changed, 396 insertions(+) create mode 100644 src/Avalonia.Visuals/Media/GeometryCollection.cs create mode 100644 src/Avalonia.Visuals/Media/GeometryGroup.cs create mode 100644 src/Skia/Avalonia.Skia/GeometryGroupImpl.cs create mode 100644 src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs create mode 100644 tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_NonZero.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png create mode 100644 tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd.expected.png create mode 100644 tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png create mode 100644 tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero.expected.png create mode 100644 tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 268171d467..abc07d0e71 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,6 +47,7 @@ namespace Avalonia.Headless } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); diff --git a/src/Avalonia.Visuals/Media/GeometryCollection.cs b/src/Avalonia.Visuals/Media/GeometryCollection.cs new file mode 100644 index 0000000000..0bd02d5438 --- /dev/null +++ b/src/Avalonia.Visuals/Media/GeometryCollection.cs @@ -0,0 +1,37 @@ +using System.Collections; +using System.Collections.Generic; +using Avalonia.Animation; + +#nullable enable + +namespace Avalonia.Media +{ + public class GeometryCollection : Animatable, IList, IReadOnlyList + { + private List _inner; + + public GeometryCollection() => _inner = new List(); + public GeometryCollection(IEnumerable collection) => _inner = new List(collection); + public GeometryCollection(int capacity) => _inner = new List(capacity); + + public Geometry this[int index] + { + get => _inner[index]; + set => _inner[index] = value; + } + + public int Count => _inner.Count; + public bool IsReadOnly => false; + + public void Add(Geometry item) => _inner.Add(item); + public void Clear() => _inner.Clear(); + public bool Contains(Geometry item) => _inner.Contains(item); + public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex); + public IEnumerator GetEnumerator() => _inner.GetEnumerator(); + public int IndexOf(Geometry item) => _inner.IndexOf(item); + public void Insert(int index, Geometry item) => _inner.Insert(index, item); + public bool Remove(Geometry item) => _inner.Remove(item); + public void RemoveAt(int index) => _inner.RemoveAt(index); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Avalonia.Visuals/Media/GeometryGroup.cs b/src/Avalonia.Visuals/Media/GeometryGroup.cs new file mode 100644 index 0000000000..a3ce6a9dcd --- /dev/null +++ b/src/Avalonia.Visuals/Media/GeometryGroup.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Metadata; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Media +{ + /// + /// Represents a composite geometry, composed of other objects. + /// + public class GeometryGroup : Geometry + { + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect ( + nameof(Children), + o => o.Children, + (o, v) => o.Children = v); + + public static readonly StyledProperty FillRuleProperty = + AvaloniaProperty.Register(nameof(FillRule)); + + private GeometryCollection? _children; + private bool _childrenSet; + + /// + /// Gets or sets the collection that contains the child geometries. + /// + [Content] + public GeometryCollection? Children + { + get => _children ??= (!_childrenSet ? new GeometryCollection() : null); + set + { + SetAndRaise(ChildrenProperty, ref _children, value); + _childrenSet = true; + } + } + + /// + /// Gets or sets how the intersecting areas of the objects contained in this + /// are combined. The default is . + /// + public FillRule FillRule + { + get => GetValue(FillRuleProperty); + set => SetValue(FillRuleProperty, value); + } + + public override Geometry Clone() + { + var result = new GeometryGroup { FillRule = FillRule }; + if (_children?.Count > 0) + result.Children = new GeometryCollection(_children); + return result; + } + + protected override IGeometryImpl? CreateDefiningGeometry() + { + if (_children?.Count > 0) + { + var factory = AvaloniaLocator.Current.GetService(); + return factory.CreateGeometryGroup(FillRule, _children); + } + + return null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) + { + InvalidateGeometry(); + } + } + } +} diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index de67aca5a8..2cc44681d5 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -59,6 +59,14 @@ namespace Avalonia.Platform /// An . IStreamGeometryImpl CreateStreamGeometry(); + /// + /// Creates a geometry group implementation. + /// + /// The fill rule. + /// The geometries to group. + /// A combined geometry. + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + /// /// Creates a renderer. /// diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs new file mode 100644 index 0000000000..d6f19612c1 --- /dev/null +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Avalonia.Media; +using SkiaSharp; + +#nullable enable + +namespace Avalonia.Skia +{ + /// + /// A Skia implementation of a . + /// + internal class GeometryGroupImpl : GeometryImpl + { + public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) + { + var path = new SKPath + { + FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, + }; + + var count = children.Count; + + for (var i = 0; i < count; ++i) + { + if (children[i]?.PlatformImpl is GeometryImpl child) + path.AddPath(child.EffectivePath); + } + + EffectivePath = path; + Bounds = path.Bounds.ToAvaloniaRect(); + } + + public override Rect Bounds { get; } + public override SKPath EffectivePath { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 7bc83ec85b..447d683a2c 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,6 +62,11 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + return new GeometryGroupImpl(fillRule, children); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index f50167b39a..91f81223a0 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -175,6 +175,7 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); /// public IBitmapImpl LoadBitmap(string fileName) diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs new file mode 100644 index 0000000000..352708bf03 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using SharpDX.Direct2D1; +using AM = Avalonia.Media; + +namespace Avalonia.Direct2D1.Media +{ + /// + /// A Direct2D implementation of a . + /// + internal class GeometryGroupImpl : GeometryImpl + { + /// + /// Initializes a new instance of the class. + /// + public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) + : base(CreateGeometry(fillRule, geometry)) + { + } + + private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) + { + var count = children.Count; + var c = new Geometry[count]; + + for (var i = 0; i < count; ++i) + { + c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry; + } + + return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c); + } + } +} diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 876a0de643..fd8dd3ff94 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -36,6 +36,11 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + throw new NotImplementedException(); + } + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { throw new NotImplementedException(); diff --git a/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs b/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs new file mode 100644 index 0000000000..6201c2c55e --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class GeometryGroupTests : TestBase + { + public GeometryGroupTests() + : base(@"Media\GeometryGroup") + { + } + + [Fact] + public async Task FillRule_EvenOdd() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new GeometryGroup + { + FillRule = FillRule.EvenOdd, + Children = + { + new RectangleGeometry(new Rect(25, 25, 100, 100)), + new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + }, + } + }, + Fill = Brushes.Blue, + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task FillRule_NonZero() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new GeometryGroup + { + FillRule = FillRule.NonZero, + Children = + { + new RectangleGeometry(new Rect(25, 25, 100, 100)), + new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + }, + } + }, + Fill = Brushes.Blue, + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task FillRule_EvenOdd_Stroke() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new GeometryGroup + { + FillRule = FillRule.EvenOdd, + Children = + { + new RectangleGeometry(new Rect(25, 25, 100, 100)), + new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + }, + } + }, + Fill = Brushes.Blue, + Stroke = Brushes.Red, + StrokeThickness = 1, + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task FillRule_NonZero_Stroke() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Data = new GeometryGroup + { + FillRule = FillRule.NonZero, + Children = + { + new RectangleGeometry(new Rect(25, 25, 100, 100)), + new EllipseGeometry + { + Center = new Point(125, 125), + RadiusX = 50, + RadiusY = 50, + }, + } + }, + Fill = Brushes.Blue, + Stroke = Brushes.Red, + StrokeThickness = 1, + } + }; + + await RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 74366f9e26..34697b8616 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -52,6 +52,11 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + return Mock.Of(); + } + public IWriteableBitmapImpl CreateWriteableBitmap( PixelSize size, Vector dpi, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs new file mode 100644 index 0000000000..8f80238903 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs @@ -0,0 +1,26 @@ +using Avalonia.Media; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Media +{ + public class GeometryGroupTests + { + [Fact] + public void Children_Should_Have_Initial_Collection() + { + var target = new GeometryGroup(); + + Assert.NotNull(target.Children); + } + + [Fact] + public void Children_Can_Be_Set_To_Null() + { + var target = new GeometryGroup(); + + target.Children = null; + + Assert.Null(target.Children); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 51ea1e893f..dded3caa88 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -37,6 +37,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree return new MockStreamGeometry(); } + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + { + throw new NotImplementedException(); + } + public IBitmapImpl LoadBitmap(Stream stream) { throw new NotImplementedException(); diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8eb361584591b82d6d2206bd8d4caa881672a5 GIT binary patch literal 2349 zcmd5;dpHwp8{cNM&!G{GmUk#Qgv=aDquSZWD5De11h2wPeLSz z>r6nE$IcrbB^HTxA&iIX--U0)vb~;mJq|Y-Zsdheb(fhhm-N?pU8=fNa;a*oeiI(w zgtkRVKZFVgbNMv^fo&Q2U-^ogpAC#X5%CsHmEJxtY+sz?%F|y@dv1QVVY7qE&#ike zdmuysq)Fw?h5pDRv~fy4}PC3&nMVE zd*a^egVmB%*6J`4tci(`4Di}L(Zef}r>9S2VkfThmr2ORzFWp`F&o4ge4Sdvm7~U> zthB}R?7Tcq26kodF>>779;pc*6COc37tj_bRFJFT=MD0&O!G7IE^%N>`|w#Fa9Bb4uXw<77Dr z-w4k9ha>E{fo7UDx8*9X*eo>TgyR*tG^ReZVA=IW`4s~QXh)hEs|?97Rs2|`+PT+l zt6ZN!P*62HD_rnGjJ3CZf5x9K<8f2TPW?i)4XT_j;kXb5bpi zvK?72<;N^@*V79uIWc^xU`IZt9BWHWcvdrUirsNGN zbl9|J5#v-tXM_2|$43iB9g@J1T$(`)&3wve^UhmLn^T*??YDRcE-6a2W(c=1ac{h2 zJhxf$#xfetWH+Z1R!$5=IvuCoY=Ph~ecw{a9q|JeWPHqrlf`K~K*6`TEz3{YUR&<= zJ_mq8lVsk1dNvsoCC(Nj>MBSJn0wjfQEVSDbU@g$snY>iQ*d+Zf@*mnkYxw0#d;X{(2^k#oKyU)UT^&3o?o#jHKtU7#g_0ug;Isg#J#B0 zMb+O!>2P~Q!z`n0&5Q29BSsKCz z{cCI<@4w_|?+HwFP%8=*I{Hy?;rsVpFpp#6Sc-LFTB=ON)P;cAu-(7CGeB@3%+zaK zdNq4hrSOdSs9y|+P50MyCQ|!QYMN?)2QPI_S^D3U1hOs*)YnIEyp}z$W%18S9brAy z4F+aPRZenjEB;VdM-r`Y|B$I)lBj2Kt0+Nf-Lk8c(Sofq2(QRyFqPS|;>^3Q-SqA{ zIO^ZZAQg8njQB~~8LkAsaWy3MzwfEZAlpa)XD;8>Pd@CgmcSrbaMR%%t0_CNtr8^Z zB9K{>W`#QZ7s24|4S70ZF^9E=SB)04M4L@;&Q&D&Wzo11H@PS81B+Fi*gr!kc``zU zGoj))pRkI*R_j#PR&pX_5?mxTlO&z)!K9eIVQF=VQFf@pij6;K*(4a>j}YIj_RbH> zBm#O|-^*Tk`#q@;sz`kslySfyMK1V?e%QdFo|DJxv2wkDougfj1att&@gxmXiiVSx zn0$7SpCsi-Oy~UKcy*RMFobS4UtGL+*Q>{^(PMSwX)Prq=5P(13F6g%d>r8?5&FI+ zVa+gu2myu+ii^m6H~}3|ADQ=yrRu4JD3k|r^BKP`%ug~uR$Me%u!Ih<_)``f2@p=Z z5H8QPq?M5w!{9M6(GN%vbyf5LvV6SPw66ey%b_z`;l#X-Fent*3wnP}q8pgEq zrrIr3tx4B>bIfPOq4DC^ zTa}OSoV@qDmN@s*ZqoG~Ts+T2ELHZpH4L@wXx0F08p&;0QamWlu`~R=L&{&XMZ5Pw z&nwa$gGmm*zhfzSE(r!>lCp*9joxI1qayWPy_9_>=NKc;y}EO(rD=g`4Hp_bwQc@` zdt(o=cTcLhA#Moo?^wq98k+m6XcxFLPUF z*ShkI%gf#9hXutVquGH}k<6@BaEqhb1!T6Xo-EWInS9aTBJ@a*SOO3c|5gV4ua4Mk apZC!jx5}Ri(~A>J9)O+AacrfvZ{lA-+8WIO literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png b/tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..4dd547c4bf5b01343a211277795b2201e639c16b GIT binary patch literal 4047 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..80b91d12094545a721c032a4dd3dd569e745c212 GIT binary patch literal 3814 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png b/tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a101525cb30d8c7dcc344174bf9ad8bae6490e0b GIT binary patch literal 3693 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 PBaMNssZNE~pHcq-(MI2G literal 0 HcmV?d00001 From 7a8650ae06867b16c716740b117bf61aef87653a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 10 Sep 2021 18:05:37 +0200 Subject: [PATCH 41/75] fixes(Datagrid): Avoid using Threading.Dispatcher.UIThread.Post( --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index cb1ce0c731..9d231ff8b2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -4489,13 +4489,11 @@ namespace Avalonia.Controls element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext); if (element != null) { - + + dataGridCell.Content = element; if (element.IsInitialized) { - Threading.Dispatcher.UIThread.Post(() => - { - PreparingCellForEditPrivate(element as Control); - }); + PreparingCellForEditPrivate(element as Control); } else { @@ -4508,9 +4506,10 @@ namespace Avalonia.Controls { // 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) From 4eff3852ee640953fd48dcd770b2a2cc17434336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 10 Sep 2021 20:43:47 +0200 Subject: [PATCH 42/75] Set MenuItem icon presenter size from style --- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index 3903096933..72c25cea37 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -75,8 +75,6 @@ @@ -199,6 +197,8 @@