From 397267d1b5b39a11c700003039f3da67025e177f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 9 Aug 2021 23:46:20 +0300 Subject: [PATCH 001/311] [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 002/311] 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 003/311] 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 158ebe7710f9cdc8ec69a113200a6b8a66566695 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 26 Aug 2021 22:52:49 +0200 Subject: [PATCH 004/311] Lazy subscribe to collection changed. Subscribing to the inner collection's `CollectionChanged` event when an `ItemsSourceView` was created means the only way to unsubscribe is to dispose the `ItemsSourceView` meaning that the instance can't be easily shared. --- src/Avalonia.Controls/ItemsSourceView.cs | 48 ++++++++++++++++-------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index b2663f3213..2884295386 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); private protected readonly IList _inner; - private INotifyCollectionChanged? _notifyCollectionChanged; + private NotifyCollectionChangedEventHandler? _collectionChanged; /// /// Initializes a new instance of the ItemsSourceView class for the specified data source. @@ -55,8 +55,6 @@ namespace Avalonia.Controls { _inner = new List(source.Cast()); } - - ListenToCollectionChanges(); } /// @@ -82,14 +80,41 @@ namespace Avalonia.Controls /// /// Occurs when the collection has changed to indicate the reason for the change and which items changed. /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event NotifyCollectionChangedEventHandler? CollectionChanged + { + add + { + if (_collectionChanged is null) + { + if (_inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnCollectionChanged; + } + } + + _collectionChanged += value; + } + + remove + { + _collectionChanged -= value; + + if (_collectionChanged is null) + { + if (_inner is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= OnCollectionChanged; + } + } + } + } /// public void Dispose() { - if (_notifyCollectionChanged != null) + if (_inner is INotifyCollectionChanged incc) { - _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged; + incc.CollectionChanged -= OnCollectionChanged; } } @@ -162,16 +187,7 @@ namespace Avalonia.Controls protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) { - CollectionChanged?.Invoke(this, args); - } - - private void ListenToCollectionChanges() - { - if (_inner is INotifyCollectionChanged incc) - { - incc.CollectionChanged += OnCollectionChanged; - _notifyCollectionChanged = incc; - } + _collectionChanged?.Invoke(this, args); } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) From 5ae9a2d60c902a1b5fb0067b65df9ef5ea0c398c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 26 Aug 2021 23:15:07 +0200 Subject: [PATCH 005/311] Validate source collection, and add tests. --- src/Avalonia.Controls/ItemsSourceView.cs | 21 +++---- .../ItemsSourceViewTests.cs | 63 +++++++++++++++++++ 2 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 2884295386..d306939e4b 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -42,19 +42,16 @@ namespace Avalonia.Controls public ItemsSourceView(IEnumerable source) { source = source ?? throw new ArgumentNullException(nameof(source)); - - if (source is IList list) - { - _inner = list; - } - else if (source is IEnumerable objectEnumerable) + _inner = source switch { - _inner = new List(objectEnumerable); - } - else - { - _inner = new List(source.Cast()); - } + ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), + IList list => list, + INotifyCollectionChanged => throw new ArgumentException( + "Collection implements INotifyCollectionChanged by not IList.", + nameof(source)), + IEnumerable iObj => new List(iObj), + _ => new List(source.Cast()) + }; } /// diff --git a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs new file mode 100644 index 0000000000..529b3b1aa8 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using Avalonia.Collections; +using Avalonia.Diagnostics; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ItemsSourceViewTests + { + [Fact] + public void Only_Subscribes_To_Source_CollectionChanged_When_CollectionChanged_Subscribed() + { + var source = new AvaloniaList(); + var target = new ItemsSourceView(source); + var debug = (INotifyCollectionChangedDebug)source; + + Assert.Null(debug.GetCollectionChangedSubscribers()); + + void Handler(object sender, NotifyCollectionChangedEventArgs e) { } + target.CollectionChanged += Handler; + + Assert.NotNull(debug.GetCollectionChangedSubscribers()); + Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); + + target.CollectionChanged -= Handler; + + Assert.Null(debug.GetCollectionChangedSubscribers()); + } + + [Fact] + public void Cannot_Wrap_An_ItemsSourceView_In_Another() + { + var source = new ItemsSourceView(new string[0]); + Assert.Throws(() => new ItemsSourceView(source)); + } + + [Fact] + public void Cannot_Create_ItemsSourceView_With_Collection_That_Implements_INCC_But_Not_List() + { + var source = new InvalidCollection(); + Assert.Throws(() => new ItemsSourceView(source)); + } + + private class InvalidCollection : INotifyCollectionChanged, IEnumerable + { + public event NotifyCollectionChangedEventHandler CollectionChanged; + + public IEnumerator GetEnumerator() + { + yield break; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield break; + } + } + } +} From 2517a70994f6630d80c96e90b324e39262416db9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 26 Aug 2021 23:25:54 +0200 Subject: [PATCH 006/311] Expose inner list, and throw if disposed. --- src/Avalonia.Controls/ItemsSourceView.cs | 41 ++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index d306939e4b..e8869d6d0c 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls /// public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private protected readonly IList _inner; + private IList? _inner; private NotifyCollectionChangedEventHandler? _collectionChanged; /// @@ -57,7 +57,7 @@ namespace Avalonia.Controls /// /// Gets the number of items in the collection. /// - public int Count => _inner.Count; + public int Count => Inner.Count; /// /// Gets a value that indicates whether the items source can provide a unique key for each item. @@ -67,6 +67,19 @@ namespace Avalonia.Controls /// public bool HasKeyIndexMapping => false; + /// + /// Gets the inner collection. + /// + public IList Inner + { + get + { + if (_inner is null) + ThrowDisposed(); + return _inner!; + } + } + /// /// Retrieves the item at the specified index. /// @@ -81,6 +94,9 @@ namespace Avalonia.Controls { add { + if (_inner is null) + ThrowDisposed(); + if (_collectionChanged is null) { if (_inner is INotifyCollectionChanged incc) @@ -94,6 +110,9 @@ namespace Avalonia.Controls remove { + if (_inner is null) + ThrowDisposed(); + _collectionChanged -= value; if (_collectionChanged is null) @@ -113,6 +132,8 @@ namespace Avalonia.Controls { incc.CollectionChanged -= OnCollectionChanged; } + + _inner = null; } /// @@ -120,9 +141,9 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public object? GetAt(int index) => _inner[index]; + public object? GetAt(int index) => Inner[index]; - public int IndexOf(object? item) => _inner.IndexOf(item); + public int IndexOf(object? item) => Inner.IndexOf(item); public static ItemsSourceView GetOrCreate(IEnumerable? items) { @@ -168,7 +189,7 @@ namespace Avalonia.Controls internal void AddListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.AddListener(incc, listener); } @@ -176,7 +197,7 @@ namespace Avalonia.Controls internal void RemoveListener(ICollectionChangedListener listener) { - if (_inner is INotifyCollectionChanged incc) + if (Inner is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.RemoveListener(incc, listener); } @@ -191,6 +212,8 @@ namespace Avalonia.Controls { OnItemsSourceChanged(e); } + + private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView)); } public class ItemsSourceView : ItemsSourceView, IReadOnlyList @@ -229,10 +252,10 @@ namespace Avalonia.Controls /// The index. /// The item. [return: MaybeNull] - public new T GetAt(int index) => (T)_inner[index]; + public new T GetAt(int index) => (T)Inner[index]; - public IEnumerator GetEnumerator() => _inner.Cast().GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + public IEnumerator GetEnumerator() => Inner.Cast().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); public static new ItemsSourceView GetOrCreate(IEnumerable? items) { From 9682f014b2e71ded52b82d0e602a56506147b883 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 28 Aug 2021 15:41:52 +0300 Subject: [PATCH 007/311] [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 7a027792cbfc4426bea431ea4b24268ef68c68a3 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 30 Aug 2021 18:39:21 +0300 Subject: [PATCH 008/311] 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 009/311] 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 010/311] 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 011/311] 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 012/311] [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 dc71df166ce493b00c8e521adbb0b00bd5e1bd93 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 12:05:28 +0200 Subject: [PATCH 013/311] fixes(Visuals): Suppress warning CS0618 'IVisualWithRoundRectClip' is obsolete: 'Internal API, will be removed in future versions, you've been warned' --- src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs | 7 +++++-- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 85feb06c44..52427c4ae6 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -289,11 +289,14 @@ namespace Avalonia.Rendering using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (clipToBounds - ? visual is IVisualWithRoundRectClip roundClipVisual + using (clipToBounds +#pragma warning disable CS0618 // Type or member is obsolete + ? visual is IVisualWithRoundRectClip roundClipVisual ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius)) : context.PushClip(bounds) : default(DrawingContext.PushedState)) +#pragma warning restore CS0618 // Type or member is obsolete + using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState)) using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index c6cdf474bb..b9131c26f4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -164,10 +164,12 @@ namespace Avalonia.Rendering.SceneGraph var visual = node.Visual; var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; +#pragma warning disable CS0618 // Type or member is obsolete var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? roundRectClip.ClipToBoundsRadius : default; - +#pragma warning restore CS0618 // Type or member is obsolete + var bounds = new Rect(visual.Bounds.Size); var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; 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 014/311] 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 5d3796092178f90fa625407ca875c99e0a14381c Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 12:11:55 +0200 Subject: [PATCH 015/311] fixes(Input): Suppress warning CS0618 'MouseDevice.Position' is obsolete --- src/Avalonia.Input/MouseDevice.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index cfa3690daf..401c6cb2ac 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -75,7 +75,9 @@ namespace Avalonia.Input throw new InvalidOperationException("Control is not attached to visual tree."); } +#pragma warning disable CS0618 // Type or member is obsolete var rootPoint = relativeTo.VisualRoot.PointToClient(Position); +#pragma warning restore CS0618 // Type or member is obsolete var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo); return rootPoint * transform!.Value; } From 22432a002dc9ba86429d582c08103e17f3e50433 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 31 Aug 2021 13:14:59 +0300 Subject: [PATCH 016/311] 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 91c9281511ad7697d322173edb7ce19ea4dbd0d7 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 12:15:03 +0200 Subject: [PATCH 017/311] fixes(Input): Suppress warning CS0618 'PointerPressedEventArgs.ClickCount' is obsolete --- src/Avalonia.Input/Gestures.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index f2cc9e9072..8d74001309 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -81,17 +81,21 @@ namespace Avalonia.Input var e = (PointerPressedEventArgs)ev; var visual = (IVisual)ev.Source; - if (e.ClickCount <= 1) +#pragma warning disable CS0618 // Type or member is obsolete + var clickCount = e.ClickCount; +#pragma warning restore CS0618 // Type or member is obsolete + if (clickCount <= 1) { s_lastPress = new WeakReference(ev.Source); } - else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) + else if (s_lastPress != null && clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed) { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); } } + } } From 68370e37a5d5340613369e49f8aa9f9e5b7f31d1 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 31 Aug 2021 16:14:15 +0200 Subject: [PATCH 018/311] 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 063e5e7be3b94d4a3f929acbd2ac552534877e41 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 11:42:16 +0300 Subject: [PATCH 019/311] 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 020/311] 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 3b65579b1abc74b289f355037a0e36f2ebb853ce Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 1 Sep 2021 12:50:02 +0200 Subject: [PATCH 021/311] fixes(DevTools): Warning CS8604 Possible null reference argument for parameter 'topLevel' --- .../Diagnostics/Views/MainWindow.xaml.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index ea06c33e4d..73d867bf10 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -160,13 +160,19 @@ namespace Avalonia.Diagnostics.Views return; } + var root = Root; + if (root is null) + { + return; + } + switch (e.Modifiers) { case RawInputModifiers.Control | RawInputModifiers.Shift: { IControl? control = null; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { control = GetHoveredControl(popupRoot); @@ -176,7 +182,7 @@ namespace Avalonia.Diagnostics.Views } } - control ??= GetHoveredControl(Root); + control ??= GetHoveredControl(root); if (control != null) { @@ -190,7 +196,7 @@ namespace Avalonia.Diagnostics.Views { vm.FreezePopups = !vm.FreezePopups; - foreach (var popupRoot in GetPopupRoots(Root)) + foreach (var popupRoot in GetPopupRoots(root)) { if (popupRoot.Parent is Popup popup) { From 5c1fd27d1f0f31cf2c5c5a60aa634c7d3fc58914 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 15:00:08 +0300 Subject: [PATCH 022/311] 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 023/311] 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 024/311] 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 025/311] 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 6e0edab57f4b8d51338fee9db788a1dfd46aa301 Mon Sep 17 00:00:00 2001 From: evan-choi Date: Thu, 2 Sep 2021 15:09:48 +0900 Subject: [PATCH 026/311] Fix OpenBrowser on Linux --- src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs index 55e30396e1..bd7e7c9c3e 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using Avalonia.Controls; using Avalonia.Markup.Xaml; @@ -42,7 +43,8 @@ namespace Avalonia.Dialogs private static void ShellExec(string cmd, bool waitForExit = true) { - var escapedArgs = cmd.Replace("\"", "\\\""); + var escapedArgs = Regex.Replace(cmd, "(?=[`~!#&*()|;'<>])", "\\") + .Replace("\"", "\\\\\\\""); using (var process = Process.Start( new ProcessStartInfo From 2a09574ba70b0bebdfb9339279fc38ef2a525c11 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Sep 2021 09:52:34 +0200 Subject: [PATCH 027/311] 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 028/311] 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 029/311] 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 030/311] 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 031/311] 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 032/311] 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 033/311] 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 034/311] 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 035/311] 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 036/311] 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 037/311] 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 3438ac149befdb21161d434f9f88af7c1b09b0eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 11:42:16 +0100 Subject: [PATCH 038/311] initial implementation of tray icon. --- samples/ControlCatalog/App.xaml.cs | 9 + src/Avalonia.Controls/ApiCompatBaseline.txt | 3 +- .../Platform/ITrayIconImpl.cs | 22 ++ .../Platform/IWindowingPlatform.cs | 3 + .../Platform/PlatformManager.cs | 13 + src/Avalonia.Controls/TrayIcon.cs | 107 ++++++++ .../Remote/PreviewerWindowingPlatform.cs | 4 +- .../Remote/TrayIconStub.cs | 24 ++ .../AvaloniaHeadlessPlatform.cs | 5 + src/Avalonia.Native/AvaloniaNativePlatform.cs | 5 + src/Avalonia.X11/X11Platform.cs | 6 + .../Interop/UnmanagedMethods.cs | 60 +++++ src/Windows/Avalonia.Win32/TrayIconImpl.cs | 252 ++++++++++++++++++ src/Windows/Avalonia.Win32/Win32Platform.cs | 5 + 14 files changed, 516 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/Platform/ITrayIconImpl.cs create mode 100644 src/Avalonia.Controls/TrayIcon.cs create mode 100644 src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs create mode 100644 src/Windows/Avalonia.Win32/TrayIconImpl.cs diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index f3ec7b48aa..008ef6570b 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,5 +1,6 @@ using System; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; @@ -92,12 +93,20 @@ namespace ControlCatalog Styles.Insert(0, FluentLight); AvaloniaXamlLoader.Load(this); + + } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { desktopLifetime.MainWindow = new MainWindow(); + + var trayIcon = new TrayIcon(); + + trayIcon.Icon = desktopLifetime.MainWindow.Icon; + } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index fac5923db5..4046900a3a 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -55,4 +55,5 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. -Total Issues: 56 +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. +Total Issues: 57 diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs new file mode 100644 index 0000000000..9690c26296 --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Platform +{ + public interface ITrayIconImpl + { + /// + /// Sets the icon of this tray icon. + /// + void SetIcon(IWindowIconImpl icon); + + /// + /// Sets the icon of this tray icon. + /// + void SetToolTipText(string? text); + + /// + /// Sets if the tray icon is visible or not. + /// + void SetIsVisible (bool visible); + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index be8939e19a..4efa92cc6b 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -3,6 +3,9 @@ namespace Avalonia.Platform public interface IWindowingPlatform { IWindowImpl CreateWindow(); + IWindowImpl CreateEmbeddableWindow(); + + ITrayIconImpl CreateTrayIcon(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index 19d034b4e2..fe83e37909 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -22,6 +22,19 @@ namespace Avalonia.Controls.Platform { } + public static ITrayIconImpl CreateTrayIcon () + { + var platform = AvaloniaLocator.Current.GetService(); + + if (platform == null) + { + throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered."); + } + + return s_designerMode ? null : platform.CreateTrayIcon(); + } + + public static IWindowImpl CreateWindow() { var platform = AvaloniaLocator.Current.GetService(); diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs new file mode 100644 index 0000000000..8cb1951c43 --- /dev/null +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -0,0 +1,107 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Controls +{ + public class TrayIcon : AvaloniaObject, IDataContextProvider + { + private readonly ITrayIconImpl _impl; + + private TrayIcon(ITrayIconImpl impl) + { + _impl = impl; + } + + public TrayIcon () : this(PlatformManager.CreateTrayIcon()) + { + + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty DataContextProperty = + StyledElement.DataContextProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + Window.IconProperty.AddOwner(); + + + public static readonly StyledProperty ToolTipTextProperty = + AvaloniaProperty.Register(nameof(ToolTipText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsVisibleProperty = + Visual.IsVisibleProperty.AddOwner(); + + /// + /// Removes the notify icon from the taskbar notification area. + /// + public void Remove() + { + + } + + + public new ITrayIconImpl PlatformImpl => _impl; + + + /// + /// Gets or sets the Applications's data context. + /// + /// + /// The data context property specifies the default object that will + /// be used for data binding. + /// + public object? DataContext + { + get => GetValue(DataContextProperty); + set => SetValue(DataContextProperty, value); + } + + /// + /// Gets or sets the icon of the TrayIcon. + /// + public WindowIcon Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets the tooltip text of the TrayIcon. + /// + public string? ToolTipText + { + get => GetValue(ToolTipTextProperty); + set => SetValue(ToolTipTextProperty, value); + } + + /// + /// Gets or sets the visibility of the TrayIcon. + /// + public bool IsVisible + { + get => GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if(change.Property == IconProperty) + { + _impl.SetIcon(Icon.PlatformImpl); + } + } + } +} diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 67b832318a..caca15b3a3 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -16,7 +16,9 @@ namespace Avalonia.DesignerSupport.Remote private static DetachableTransportConnection s_lastWindowTransport; private static PreviewerWindowImpl s_lastWindow; public static List PreFlightMessages = new List(); - + + public ITrayIconImpl CreateTrayIcon() => new TrayIconStub(); + public IWindowImpl CreateWindow() => new WindowStub(); public IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs new file mode 100644 index 0000000000..939cf16824 --- /dev/null +++ b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.DesignerSupport.Remote +{ + class TrayIconStub : ITrayIconImpl + { + public Action Clicked { get; set; } + public Action DoubleClicked { get; set; } + public Action RightClicked { get; set; } + + public void SetIcon(IWindowIconImpl icon) + { + } + + public void SetIsVisible(bool visible) + { + } + + public void SetToolTipText(string text) + { + } + } +} diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index fca2a1336f..afaec3a8a0 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -51,6 +51,11 @@ namespace Avalonia.Headless public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); + + public ITrayIconImpl CreateTrayIcon() + { + throw new NotImplementedException(); + } } internal static void Initialize() diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index a7d05e416f..c98c56fcb1 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -134,6 +134,11 @@ namespace Avalonia.Native } } + public ITrayIconImpl CreateTrayIcon () + { + throw new NotImplementedException(); + } + public IWindowImpl CreateWindow() { return new WindowImpl(_factory, _options, _platformGl); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 3a919c8814..c6cea60efd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -100,6 +100,12 @@ namespace Avalonia.X11 public IntPtr DeferredDisplay { get; set; } public IntPtr Display { get; set; } + + public ITrayIconImpl CreateTrayIcon () + { + throw new NotImplementedException(); + } + public IWindowImpl CreateWindow() { return new X11Window(this, null); diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ad409810b8..22f46ae5cb 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1172,6 +1172,9 @@ namespace Avalonia.Win32.Interop GCW_ATOM = -32 } + [DllImport("shell32", CharSet = CharSet.Auto)] + public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData); + [DllImport("user32.dll", EntryPoint = "SetClassLongPtr")] private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); @@ -2271,4 +2274,61 @@ namespace Avalonia.Win32.Interop public uint VisibleMask; public uint DamageMask; } + + public enum NIM : uint + { + ADD = 0x00000000, + MODIFY = 0x00000001, + DELETE = 0x00000002, + SETFOCUS = 0x00000003, + SETVERSION = 0x00000004 + } + + [Flags] + public enum NIF : uint + { + MESSAGE = 0x00000001, + ICON = 0x00000002, + TIP = 0x00000004, + STATE = 0x00000008, + INFO = 0x00000010, + GUID = 0x00000020, + REALTIME = 0x00000040, + SHOWTIP = 0x00000080 + } + + [Flags] + public enum NIIF : uint + { + NONE = 0x00000000, + INFO = 0x00000001, + WARNING = 0x00000002, + ERROR = 0x00000003, + USER = 0x00000004, + ICON_MASK = 0x0000000F, + NOSOUND = 0x00000010, + LARGE_ICON = 0x00000020, + RESPECT_QUIET_TIME = 0x00000080 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class NOTIFYICONDATA + { + public int cbSize = Marshal.SizeOf(); + public IntPtr hWnd; + public int uID; + public NIF uFlags; + public int uCallbackMessage; + public IntPtr hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip; + public int dwState = 0; + public int dwStateMask = 0; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo; + public int uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle; + public NIIF dwInfoFlags; + } } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs new file mode 100644 index 0000000000..576dcd9d33 --- /dev/null +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Platform; +using Avalonia.Win32.Interop; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + /// + /// Custom Win32 window messages for the NotifyIcon + /// + public enum CustomWindowsMessage : uint + { + WM_TRAYICON = (uint)WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = (uint)WindowsMessage.WM_USER + 1024 + } + + public class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); + private readonly MoveResizeDelegate _moveResize; + private Window _hiddenWindow; + + public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) + { + _moveResize = moveResize; + _hiddenWindow = new Window(); + } + + public IReadOnlyList Screens => + _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; + var size = _hiddenWindow.Screens.Primary.Bounds.Size; + return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity); + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); + } + + public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; + } + + public class TrayPopupRoot : Window + { + private ManagedPopupPositioner _positioner; + + public TrayPopupRoot() + { + _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); + Topmost = true; + + LostFocus += TrayPopupRoot_LostFocus; + } + + private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) + { + Close(); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + PlatformImpl.Move(position); + PlatformImpl.Resize(size, PlatformResizeReason.Layout); + } + + protected override void ArrangeCore(Rect finalRect) + { + base.ArrangeCore(finalRect); + + _positioner.Update(new PopupPositionerParameters + { + Anchor = PopupAnchor.TopLeft, + Gravity = PopupGravity.BottomRight, + AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), + Size = finalRect.Size, + ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY, + }); + } + } + + public class TrayIconImpl : ITrayIconImpl + { + private readonly int _uniqueId = 0; + private static int _nextUniqueId = 0; + private WndProc _wndProcDelegate; + private IntPtr _hwnd; + private bool _iconAdded; + private IconImpl _icon; + + public TrayIconImpl() + { + _uniqueId = ++_nextUniqueId; + + CreateMessageWindow(); + + UpdateIcon(); + } + + + ~TrayIconImpl() + { + UpdateIcon(false); + } + + private void CreateMessageWindow() + { + // Ensure that the delegate doesn't get garbage collected by storing it as a field. + _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); + + UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + lpfnWndProc = _wndProcDelegate, + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(), + }; + + ushort atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + + if (atom == 0) + { + throw new Win32Exception(); + } + + _hwnd = UnmanagedMethods.CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + + if (_hwnd == IntPtr.Zero) + { + throw new Win32Exception(); + } + } + + public void SetIcon(IWindowIconImpl icon) + { + _icon = icon as IconImpl; + UpdateIcon(); + } + + public void SetIsVisible(bool visible) + { + if (visible) + { + + } + } + + public void SetToolTipText(string text) + { + throw new NotImplementedException(); + } + + private void UpdateIcon(bool remove = false) + { + var iconData = new NOTIFYICONDATA() + { + hWnd = _hwnd, + uID = _uniqueId, + uFlags = NIF.TIP | NIF.MESSAGE, + uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, + hIcon = _icon?.HIcon ?? new IconImpl(new System.Drawing.Bitmap(32, 32)).HIcon, + szTip = "Tool tip text here." + }; + + if (!remove) + { + iconData.uFlags |= NIF.ICON; + + if (!_iconAdded) + { + UnmanagedMethods.Shell_NotifyIcon(NIM.ADD, iconData); + _iconAdded = true; + } + else + { + UnmanagedMethods.Shell_NotifyIcon(NIM.MODIFY, iconData); + } + } + else + { + UnmanagedMethods.Shell_NotifyIcon(NIM.DELETE, iconData); + _iconAdded = false; + } + } + + private void OnRightClicked() + { + UnmanagedMethods.GetCursorPos(out UnmanagedMethods.POINT pt); + var cursor = new PixelPoint(pt.X, pt.Y); + + var trayMenu = new TrayPopupRoot() + { + Position = cursor, + SystemDecorations = SystemDecorations.None, + SizeToContent = SizeToContent.WidthAndHeight, + Background = null, + TransparencyLevelHint = WindowTransparencyLevel.Transparent, + Content = new MenuFlyoutPresenter() + { + Items = new List + { + new MenuItem { Header = "Item 1"}, + new MenuItem { Header = "Item 2"}, + new MenuItem { Header = "Item 3"}, + new MenuItem { Header = "Item 4"}, + new MenuItem { Header = "Item 5"} + } + } + }; + + trayMenu.Show(); + } + + private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) + { + // Determine the type of message and call the matching event handlers + switch (lParam.ToInt32()) + { + case (int)WindowsMessage.WM_LBUTTONUP: + break; + + case (int)WindowsMessage.WM_LBUTTONDBLCLK: + break; + + case (int)WindowsMessage.WM_RBUTTONUP: + OnRightClicked(); + break; + + default: + break; + } + } + + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c011a458c3..9316c9805c 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -293,6 +293,11 @@ namespace Avalonia.Win32 } } + public ITrayIconImpl CreateTrayIcon () + { + return new TrayIconImpl(); + } + public IWindowImpl CreateWindow() { return new WindowImpl(); From 0b9601dddb601cae4d471f73eeaec8c2d3218575 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 11:52:34 +0100 Subject: [PATCH 039/311] tidy win32 tray menu impl. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 102 +++++++++++---------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 576dcd9d33..1219e5eba5 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -16,8 +16,9 @@ namespace Avalonia.Win32 /// public enum CustomWindowsMessage : uint { - WM_TRAYICON = (uint)WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = (uint)WindowsMessage.WM_USER + 1024 + WM_TRAYICON = WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + } public class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup @@ -66,17 +67,17 @@ namespace Avalonia.Win32 LostFocus += TrayPopupRoot_LostFocus; } - private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) - { - Close(); - } - private void MoveResize(PixelPoint position, Size size, double scaling) { PlatformImpl.Move(position); PlatformImpl.Resize(size, PlatformResizeReason.Layout); } + private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) + { + Close(); + } + protected override void ArrangeCore(Rect finalRect) { base.ArrangeCore(finalRect); @@ -87,7 +88,7 @@ namespace Avalonia.Win32 Gravity = PopupGravity.BottomRight, AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), Size = finalRect.Size, - ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY, + ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, }); } } @@ -119,24 +120,24 @@ namespace Avalonia.Win32 private void CreateMessageWindow() { // Ensure that the delegate doesn't get garbage collected by storing it as a field. - _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); + _wndProcDelegate = new WndProc(WndProc); - UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX + WNDCLASSEX wndClassEx = new WNDCLASSEX { - cbSize = Marshal.SizeOf(), + cbSize = Marshal.SizeOf(), lpfnWndProc = _wndProcDelegate, - hInstance = UnmanagedMethods.GetModuleHandle(null), + hInstance = GetModuleHandle(null), lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(), }; - ushort atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + ushort atom = RegisterClassEx(ref wndClassEx); if (atom == 0) { throw new Win32Exception(); } - _hwnd = UnmanagedMethods.CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + _hwnd = CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (_hwnd == IntPtr.Zero) { @@ -181,49 +182,21 @@ namespace Avalonia.Win32 if (!_iconAdded) { - UnmanagedMethods.Shell_NotifyIcon(NIM.ADD, iconData); + Shell_NotifyIcon(NIM.ADD, iconData); _iconAdded = true; } else { - UnmanagedMethods.Shell_NotifyIcon(NIM.MODIFY, iconData); + Shell_NotifyIcon(NIM.MODIFY, iconData); } } else { - UnmanagedMethods.Shell_NotifyIcon(NIM.DELETE, iconData); + Shell_NotifyIcon(NIM.DELETE, iconData); _iconAdded = false; } } - private void OnRightClicked() - { - UnmanagedMethods.GetCursorPos(out UnmanagedMethods.POINT pt); - var cursor = new PixelPoint(pt.X, pt.Y); - - var trayMenu = new TrayPopupRoot() - { - Position = cursor, - SystemDecorations = SystemDecorations.None, - SizeToContent = SizeToContent.WidthAndHeight, - Background = null, - TransparencyLevelHint = WindowTransparencyLevel.Transparent, - Content = new MenuFlyoutPresenter() - { - Items = new List - { - new MenuItem { Header = "Item 1"}, - new MenuItem { Header = "Item 2"}, - new MenuItem { Header = "Item 3"}, - new MenuItem { Header = "Item 4"}, - new MenuItem { Header = "Item 5"} - } - } - }; - - trayMenu.Show(); - } - private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) @@ -232,9 +205,18 @@ namespace Avalonia.Win32 switch (lParam.ToInt32()) { case (int)WindowsMessage.WM_LBUTTONUP: + //if (!_doubleClick) + //{ + // Click?.Invoke(this, new EventArgs()); + //} + //_doubleClick = false; + + Debug.WriteLine($"Clicked {lParam:X}"); break; case (int)WindowsMessage.WM_LBUTTONDBLCLK: + //DoubleClick?.Invoke(this, new EventArgs()); + //_doubleClick = true; break; case (int)WindowsMessage.WM_RBUTTONUP: @@ -246,7 +228,35 @@ namespace Avalonia.Win32 } } - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + return DefWindowProc(hWnd, msg, wParam, lParam); + } + + private static void OnRightClicked() + { + var _trayMenu = new TrayPopupRoot() + { + SystemDecorations = SystemDecorations.None, + SizeToContent = SizeToContent.WidthAndHeight, + Background = null, + TransparencyLevelHint = WindowTransparencyLevel.Transparent, + Content = new MenuFlyoutPresenter() + { + Items = new List + { + new MenuItem { Header = "Item 1"}, + new MenuItem { Header = "Item 2"}, + new MenuItem { Header = "Item 3"}, + new MenuItem { Header = "Item 4"}, + new MenuItem { Header = "Item 5"} + } + } + }; + + GetCursorPos(out UnmanagedMethods.POINT pt); + + _trayMenu.Position = new PixelPoint(pt.X, pt.Y); + + _trayMenu.Show(); } } } From 2d2d8fa5a76a0fde45aaf5b93f3b0ae3da93b730 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 13:02:29 +0100 Subject: [PATCH 040/311] add example win32 tray icon. --- samples/ControlCatalog/App.xaml.cs | 8 +- samples/ControlCatalog/MainWindow.xaml.cs | 2 + src/Windows/Avalonia.Win32/TrayIconImpl.cs | 182 +++++++++++---------- 3 files changed, 103 insertions(+), 89 deletions(-) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 008ef6570b..663451e9c1 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -103,9 +103,13 @@ namespace ControlCatalog { desktopLifetime.MainWindow = new MainWindow(); - var trayIcon = new TrayIcon(); + var trayIcon1 = new TrayIcon(); - trayIcon.Icon = desktopLifetime.MainWindow.Icon; + trayIcon1.Icon = desktopLifetime.MainWindow.Icon; + + var trayIcon2 = new TrayIcon(); + + trayIcon2.Icon = desktopLifetime.MainWindow.Icon; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 2446c0e1c9..a9900471c5 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -35,6 +35,8 @@ namespace ControlCatalog var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; + + ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar; } public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 1219e5eba5..a27a913974 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -6,93 +6,12 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; +using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 { - /// - /// Custom Win32 window messages for the NotifyIcon - /// - public enum CustomWindowsMessage : uint - { - WM_TRAYICON = WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 - - } - - public class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup - { - public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); - private readonly MoveResizeDelegate _moveResize; - private Window _hiddenWindow; - - public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) - { - _moveResize = moveResize; - _hiddenWindow = new Window(); - } - - public IReadOnlyList Screens => - _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( - s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); - - public Rect ParentClientAreaScreenGeometry - { - get - { - var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; - var size = _hiddenWindow.Screens.Primary.Bounds.Size; - return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity); - } - } - - public void MoveAndResize(Point devicePoint, Size virtualSize) - { - _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); - } - - public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; - } - - public class TrayPopupRoot : Window - { - private ManagedPopupPositioner _positioner; - - public TrayPopupRoot() - { - _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); - Topmost = true; - - LostFocus += TrayPopupRoot_LostFocus; - } - - private void MoveResize(PixelPoint position, Size size, double scaling) - { - PlatformImpl.Move(position); - PlatformImpl.Resize(size, PlatformResizeReason.Layout); - } - - private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) - { - Close(); - } - - protected override void ArrangeCore(Rect finalRect) - { - base.ArrangeCore(finalRect); - - _positioner.Update(new PopupPositionerParameters - { - Anchor = PopupAnchor.TopLeft, - Gravity = PopupGravity.BottomRight, - AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), - Size = finalRect.Size, - ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, - }); - } - } - public class TrayIconImpl : ITrayIconImpl { private readonly int _uniqueId = 0; @@ -163,7 +82,14 @@ namespace Avalonia.Win32 { throw new NotImplementedException(); } - + /// + /// Custom Win32 window messages for the NotifyIcon + /// + public enum CustomWindowsMessage : uint + { + WM_TRAYICON = WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + } private void UpdateIcon(bool remove = false) { var iconData = new NOTIFYICONDATA() @@ -210,8 +136,6 @@ namespace Avalonia.Win32 // Click?.Invoke(this, new EventArgs()); //} //_doubleClick = false; - - Debug.WriteLine($"Clicked {lParam:X}"); break; case (int)WindowsMessage.WM_LBUTTONDBLCLK: @@ -226,9 +150,13 @@ namespace Avalonia.Win32 default: break; } - } - return DefWindowProc(hWnd, msg, wParam, lParam); + return IntPtr.Zero; + } + else + { + return DefWindowProc(hWnd, msg, wParam, lParam); + } } private static void OnRightClicked() @@ -258,5 +186,85 @@ namespace Avalonia.Win32 _trayMenu.Show(); } + + class TrayPopupRoot : Window + { + private ManagedPopupPositioner _positioner; + + public TrayPopupRoot() + { + _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); + Topmost = true; + + Deactivated += TrayPopupRoot_Deactivated; + } + + private void TrayPopupRoot_Deactivated(object sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => + { + Close(); + }); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + PlatformImpl.Move(position); + PlatformImpl.Resize(size, PlatformResizeReason.Layout); + } + + private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) + { + Close(); + } + + protected override void ArrangeCore(Rect finalRect) + { + base.ArrangeCore(finalRect); + + _positioner.Update(new PopupPositionerParameters + { + Anchor = PopupAnchor.TopLeft, + Gravity = PopupGravity.BottomRight, + AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), + Size = finalRect.Size, + ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, + }); + } + + class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); + private readonly MoveResizeDelegate _moveResize; + private Window _hiddenWindow; + + public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) + { + _moveResize = moveResize; + _hiddenWindow = new Window(); + } + + public IReadOnlyList Screens => + _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; + var size = _hiddenWindow.Screens.Primary.Bounds.Size; + return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity); + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); + } + + public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; + } + } } } From bc772cce40204da51167e5e15256231ee7e93cfe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 13:45:36 +0100 Subject: [PATCH 041/311] dont create multiple message queues for tray icons. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 50 +++++++-------------- src/Windows/Avalonia.Win32/Win32Platform.cs | 6 +++ 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index a27a913974..f058759279 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Controls; @@ -16,16 +17,24 @@ namespace Avalonia.Win32 { private readonly int _uniqueId = 0; private static int _nextUniqueId = 0; - private WndProc _wndProcDelegate; - private IntPtr _hwnd; private bool _iconAdded; private IconImpl _icon; + private static Dictionary s_trayIcons = new Dictionary(); + + internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if(msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) + { + s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); + } + } + public TrayIconImpl() { _uniqueId = ++_nextUniqueId; - CreateMessageWindow(); + s_trayIcons.Add(_uniqueId, this); UpdateIcon(); } @@ -36,34 +45,6 @@ namespace Avalonia.Win32 UpdateIcon(false); } - private void CreateMessageWindow() - { - // Ensure that the delegate doesn't get garbage collected by storing it as a field. - _wndProcDelegate = new WndProc(WndProc); - - WNDCLASSEX wndClassEx = new WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - lpfnWndProc = _wndProcDelegate, - hInstance = GetModuleHandle(null), - lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(), - }; - - ushort atom = RegisterClassEx(ref wndClassEx); - - if (atom == 0) - { - throw new Win32Exception(); - } - - _hwnd = CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - - if (_hwnd == IntPtr.Zero) - { - throw new Win32Exception(); - } - } - public void SetIcon(IWindowIconImpl icon) { _icon = icon as IconImpl; @@ -94,7 +75,7 @@ namespace Avalonia.Win32 { var iconData = new NOTIFYICONDATA() { - hWnd = _hwnd, + hWnd = Win32Platform.Instance.Handle, uID = _uniqueId, uFlags = NIF.TIP | NIF.MESSAGE, uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, @@ -125,6 +106,7 @@ namespace Avalonia.Win32 private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { + Debug.WriteLine(wParam); if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) { // Determine the type of message and call the matching event handlers @@ -180,7 +162,7 @@ namespace Avalonia.Win32 } }; - GetCursorPos(out UnmanagedMethods.POINT pt); + GetCursorPos(out POINT pt); _trayMenu.Position = new PixelPoint(pt.X, pt.Y); @@ -197,6 +179,8 @@ namespace Avalonia.Win32 Topmost = true; Deactivated += TrayPopupRoot_Deactivated; + + ShowInTaskbar = false; } private void TrayPopupRoot_Deactivated(object sender, EventArgs e) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 9316c9805c..45fa8f44ce 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -108,6 +108,10 @@ namespace Avalonia.Win32 CreateMessageWindow(); } + internal static Win32Platform Instance => s_instance; + + internal IntPtr Handle => _hwnd; + /// /// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion. /// @@ -261,6 +265,8 @@ namespace Avalonia.Win32 } } } + + TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam); return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } From 35dc6ced0395410ea919cf7382cba9ac11bcea5e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 13:49:12 +0100 Subject: [PATCH 042/311] remove unused code. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 32 ++++++++-------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index f058759279..54337b57be 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; @@ -24,7 +21,7 @@ namespace Avalonia.Win32 internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - if(msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) + if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) { s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); } @@ -63,14 +60,8 @@ namespace Avalonia.Win32 { throw new NotImplementedException(); } - /// - /// Custom Win32 window messages for the NotifyIcon - /// - public enum CustomWindowsMessage : uint - { - WM_TRAYICON = WindowsMessage.WM_APP + 1024, - WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 - } + + private void UpdateIcon(bool remove = false) { var iconData = new NOTIFYICONDATA() @@ -106,23 +97,15 @@ namespace Avalonia.Win32 private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - Debug.WriteLine(wParam); if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) { // Determine the type of message and call the matching event handlers switch (lParam.ToInt32()) { case (int)WindowsMessage.WM_LBUTTONUP: - //if (!_doubleClick) - //{ - // Click?.Invoke(this, new EventArgs()); - //} - //_doubleClick = false; break; case (int)WindowsMessage.WM_LBUTTONDBLCLK: - //DoubleClick?.Invoke(this, new EventArgs()); - //_doubleClick = true; break; case (int)WindowsMessage.WM_RBUTTONUP: @@ -169,6 +152,15 @@ namespace Avalonia.Win32 _trayMenu.Show(); } + /// + /// Custom Win32 window messages for the NotifyIcon + /// + enum CustomWindowsMessage : uint + { + WM_TRAYICON = WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + } + class TrayPopupRoot : Window { private ManagedPopupPositioner _positioner; From 26e221e7f5fd613d918b4dd4ca24110f56ac19d7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 14:38:45 +0100 Subject: [PATCH 043/311] Win32 trayicon, make menu close when item is clicked. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 54337b57be..7f1f8763c0 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.LogicalTree; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -132,7 +134,7 @@ namespace Avalonia.Win32 SizeToContent = SizeToContent.WidthAndHeight, Background = null, TransparencyLevelHint = WindowTransparencyLevel.Transparent, - Content = new MenuFlyoutPresenter() + Content = new TrayIconMenuFlyoutPresenter() { Items = new List { @@ -161,6 +163,22 @@ namespace Avalonia.Win32 WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 } + class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable + { + Type IStyleable.StyleKey => typeof(MenuFlyoutPresenter); + + public override void Close() + { + // DefaultMenuInteractionHandler calls this + var host = this.FindLogicalAncestorOfType(); + if (host != null) + { + SelectedIndex = -1; + host.Close(); + } + } + } + class TrayPopupRoot : Window { private ManagedPopupPositioner _positioner; From 30f6145d73cccacaaf2bd617352d00325f8e2326 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Sep 2021 15:52:42 +0100 Subject: [PATCH 044/311] implement visible changed and tooltip. --- samples/ControlCatalog/App.xaml | 11 ++++ samples/ControlCatalog/App.xaml.cs | 10 ---- .../Platform/ITrayIconImpl.cs | 5 +- src/Avalonia.Controls/TrayIcon.cs | 55 +++++++++++++++++++ src/Windows/Avalonia.Win32/TrayIconImpl.cs | 21 ++++--- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 6aad44c0d5..ec3734f4f5 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,6 +1,17 @@ + + + + + + + + + + + - - - - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 5f7dc248c0..36b6fc2dcd 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -5,11 +5,17 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; +using ControlCatalog.ViewModels; namespace ControlCatalog { public class App : Application { + public App() + { + DataContext = new ApplicationViewModel(); + } + private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs new file mode 100644 index 0000000000..c96872ef7f --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ApplicationViewModel : ViewModelBase + { + public ApplicationViewModel() + { + ExitCommand = MiniCommand.Create(() => + { + if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.Shutdown(); + } + }); + } + + public MiniCommand ExitCommand { get; } + } +} diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index dfa71dcb1e..bdfea40b5e 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls { } - public class TrayIcon : AvaloniaObject, IDataContextProvider, INativeMenuExporterProvider, IDisposable + public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { private readonly ITrayIconImpl _impl; @@ -79,12 +79,6 @@ namespace Avalonia.Controls public static readonly AttachedProperty TrayIconsProperty = AvaloniaProperty.RegisterAttached("TrayIcons"); - /// - /// Defines the property. - /// - public static readonly StyledProperty DataContextProperty = - StyledElement.DataContextProperty.AddOwner(); - /// /// Defines the property. /// @@ -117,20 +111,6 @@ namespace Avalonia.Controls public new ITrayIconImpl PlatformImpl => _impl; - - /// - /// Gets or sets the Applications's data context. - /// - /// - /// The data context property specifies the default object that will - /// be used for data binding. - /// - public object? DataContext - { - get => GetValue(DataContextProperty); - set => SetValue(DataContextProperty, value); - } - /// /// Gets or sets the icon of the TrayIcon. /// From 5e87cffc9cc9cdf25987b508fa76f0536dc164b3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 16:02:58 +0100 Subject: [PATCH 049/311] 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 907b55cb7e6c3ea93e78a1ed097b3b0441d196ab Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 16:02:58 +0100 Subject: [PATCH 050/311] 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 0c55449d8b5578678c287393ae16d88bcee86d9b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 16:15:50 +0100 Subject: [PATCH 051/311] restore removed code. --- samples/ControlCatalog/App.xaml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 7a5dc0d9ec..d12eef6b4c 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -4,7 +4,29 @@ x:DataType="vm:ApplicationViewModel" x:CompileBindings="True" x:Class="ControlCatalog.App"> - + + + + + + + + + From 8f833dbb2fba5d9a4df0946eef1122bc4f097d87 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 9 Sep 2021 19:16:32 +0300 Subject: [PATCH 052/311] 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 6ae59214a45eab407c7b9c4e1ef4a6994cb5a520 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 17:33:26 +0100 Subject: [PATCH 053/311] add initial implementation for osx tray icon support. --- .../project.pbxproj | 6 ++ native/Avalonia.Native/src/OSX/common.h | 1 + native/Avalonia.Native/src/OSX/main.mm | 11 +++ native/Avalonia.Native/src/OSX/trayicon.h | 30 ++++++++ native/Avalonia.Native/src/OSX/trayicon.mm | 59 +++++++++++++++ src/Avalonia.Native/AvaloniaNativePlatform.cs | 2 +- src/Avalonia.Native/TrayIconImpl.cs | 75 +++++++++++++++++++ src/Avalonia.Native/avn.idl | 15 ++++ .../Win32NativeToManagedMenuExporter.cs | 2 +- 9 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/trayicon.h create mode 100644 native/Avalonia.Native/src/OSX/trayicon.mm create mode 100644 src/Avalonia.Native/TrayIconImpl.cs diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index dba3ee6d31..85fcf20034 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; 522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; }; + 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; @@ -51,6 +52,8 @@ 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; 522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; + 523484C926EA688F00EA0C2C /* trayicon.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = trayicon.mm; sourceTree = ""; }; + 523484CB26EA68AA00EA0C2C /* trayicon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = trayicon.h; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -114,6 +117,8 @@ AB00E4F62147CA920032A60A /* main.mm */, 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, + 523484C926EA688F00EA0C2C /* trayicon.mm */, + 523484CB26EA68AA00EA0C2C /* trayicon.h */, 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, @@ -204,6 +209,7 @@ 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, + 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c082003ccf..5c174eb663 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -22,6 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); +extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 3e152a6125..f179d4f049 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -303,6 +303,17 @@ public: } } + virtual HRESULT CreateTrayIcon (IAvnTrayIconEvents*cb, IAvnTrayIcon** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateTrayIcon(cb); + return S_OK; + } + } + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { START_COM_CALL; diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h new file mode 100644 index 0000000000..4329668cbd --- /dev/null +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -0,0 +1,30 @@ +// +// trayicon.h +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 09/09/2021. +// Copyright © 2021 Avalonia. All rights reserved. +// + +#ifndef trayicon_h +#define trayicon_h + +#include "common.h" + +class AvnTrayIcon : public ComSingleObject +{ +private: + NSStatusItem* _native; + ComPtr _events; + +public: + FORWARD_IUNKNOWN() + + AvnTrayIcon(IAvnTrayIconEvents* events); + + virtual HRESULT SetIcon (void* data, size_t length) override; + + virtual HRESULT SetMenu (IAvnMenu* menu) override; +}; + +#endif /* trayicon_h */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm new file mode 100644 index 0000000000..959762a663 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -0,0 +1,59 @@ +#include "common.h" +#include "trayicon.h" + +extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* cb) +{ + @autoreleasepool + { + return new AvnTrayIcon(cb); + } +} + +AvnTrayIcon::AvnTrayIcon(IAvnTrayIconEvents* events) +{ + _events = events; + + _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; +} + +HRESULT AvnTrayIcon::SetIcon (void* data, size_t length) +{ + START_COM_CALL; + + @autoreleasepool + { + if(data != nullptr) + { + NSData *imageData = [NSData dataWithBytes:data length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + NSSize originalSize = [image size]; + + NSSize size; + size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333; + + auto scaleFactor = size.height / originalSize.height; + size.width = originalSize.width * scaleFactor; + + [image setSize: size]; + [_native setImage:image]; + } + else + { + [_native setImage:nullptr]; + } + return S_OK; + } +} + +HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) +{ + START_COM_CALL; + + @autoreleasepool + { + + } + + return S_OK; +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index c98c56fcb1..eaf4d0e2e4 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -136,7 +136,7 @@ namespace Avalonia.Native public ITrayIconImpl CreateTrayIcon () { - throw new NotImplementedException(); + return new TrayIconImpl(_factory); } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs new file mode 100644 index 0000000000..bbeb6c4452 --- /dev/null +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform; + +namespace Avalonia.Native +{ + class TrayIconEvents : CallbackBase, IAvnTrayIconEvents + { + private TrayIconImpl _parent; + + public TrayIconEvents (TrayIconImpl parent) + { + _parent = parent; + } + + public void Clicked() + { + } + + public void DoubleClicked() + { + } + } + + internal class TrayIconImpl : ITrayIconImpl + { + private readonly IAvnTrayIcon _native; + + public TrayIconImpl(IAvaloniaNativeFactory factory) + { + _native = factory.CreateTrayIcon(new TrayIconEvents(this)); + } + + public void Dispose() + { + + } + + public unsafe void SetIcon(IWindowIconImpl? icon) + { + if(icon is null) + { + _native.SetIcon(null, IntPtr.Zero); + } + else + { + using (var ms = new MemoryStream()) + { + icon.Save(ms); + + var imageData = ms.ToArray(); + + fixed(void* ptr = imageData) + { + _native.SetIcon(ptr, new IntPtr(imageData.Length)); + } + } + } + } + + public void SetToolTipText(string? text) + { + // NOP + } + + public void SetIsVisible(bool visible) + { + + } + + public INativeMenuExporter? MenuExporter { get; } + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 70d85dacdd..47ed7116a7 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -427,6 +427,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); + HRESULT CreateTrayIcon(IAvnTrayIconEvents* cb, IAvnTrayIcon** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -665,6 +666,20 @@ interface IAvnGlSurfaceRenderingSession : IUnknown HRESULT GetScaling(double* ret); } +[uuid(60992d19-38f0-4141-a0a9-76ac303801f3)] +interface IAvnTrayIcon : IUnknown +{ + HRESULT SetIcon(void* data, size_t length); + HRESULT SetMenu(IAvnMenu* menu); +} + +[uuid(a687a6d9-73aa-4fef-9b4a-61587d7285d3)] +interface IAvnTrayIconEvents : IUnknown +{ + void Clicked (); + void DoubleClicked (); +} + [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] interface IAvnMenu : IUnknown { diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 57fccad633..8663aec773 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -35,7 +35,7 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (_,_) => bridge.RaiseClicked(); + newItem.Click += (s, e) => bridge.RaiseClicked(); } items.Add(newItem); From 0e703c9209e4e12def5ade921e5bb743a35fb911 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 17:53:30 +0100 Subject: [PATCH 054/311] Add trayicon menu export support for osx. --- native/Avalonia.Native/src/OSX/trayicon.mm | 6 +++ .../AvaloniaNativeMenuExporter.cs | 52 ++++++++++++++++--- src/Avalonia.Native/TrayIconImpl.cs | 2 + 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 959762a663..67b6bd4874 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -1,5 +1,6 @@ #include "common.h" #include "trayicon.h" +#include "menu.h" extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* cb) { @@ -52,7 +53,12 @@ HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) @autoreleasepool { + auto appMenu = dynamic_cast(menu); + if(appMenu != nullptr) + { + [_native setMenu:appMenu->GetNative()]; + } } return S_OK; diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 89efa6af0c..dd52bd3544 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -17,6 +17,7 @@ namespace Avalonia.Native private IAvnWindow _nativeWindow; private NativeMenu _menu; private __MicroComIAvnMenuProxy _nativeMenu; + private IAvnTrayIcon _trayIcon; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -33,6 +34,14 @@ namespace Avalonia.Native DoLayoutReset(); } + public AvaloniaNativeMenuExporter(IAvnTrayIcon trayIcon, IAvaloniaNativeFactory factory) + { + _factory = factory; + _trayIcon = trayIcon; + + DoLayoutReset(); + } + public bool IsNativeMenuExported => _exported; public event EventHandler OnIsNativeMenuExportedChanged; @@ -82,15 +91,25 @@ namespace Avalonia.Native if (_nativeWindow is null) { - var appMenu = NativeMenu.GetMenu(Application.Current); + if (_trayIcon is null) + { + var appMenu = NativeMenu.GetMenu(Application.Current); + + if (appMenu == null) + { + appMenu = CreateDefaultAppMenu(); + NativeMenu.SetMenu(Application.Current, appMenu); + } - if (appMenu == null) + SetMenu(appMenu); + } + else { - appMenu = CreateDefaultAppMenu(); - NativeMenu.SetMenu(Application.Current, appMenu); + if (_menu != null) + { + SetMenu(_trayIcon, _menu); + } } - - SetMenu(appMenu); } else { @@ -171,5 +190,26 @@ namespace Avalonia.Native avnWindow.SetMainMenu(_nativeMenu); } } + + private void SetMenu(IAvnTrayIcon trayIcon, NativeMenu menu) + { + var setMenu = false; + + if (_nativeMenu is null) + { + _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); + + _nativeMenu.Initialize(this, menu, ""); + + setMenu = true; + } + + _nativeMenu.Update(_factory, menu); + + if(setMenu) + { + trayIcon.SetMenu(_nativeMenu); + } + } } } diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index bbeb6c4452..b5cb0d8c08 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -31,6 +31,8 @@ namespace Avalonia.Native public TrayIconImpl(IAvaloniaNativeFactory factory) { _native = factory.CreateTrayIcon(new TrayIconEvents(this)); + + MenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } public void Dispose() From aa5ae2360748056304b643d806e46fc061c607c2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 18:17:32 +0100 Subject: [PATCH 055/311] 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 056/311] 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 057/311] 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 f4031c67a28defa20ff7035524d2a250bc956188 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 19:56:15 +0100 Subject: [PATCH 058/311] more complex test menu for tray icons. --- samples/ControlCatalog/App.xaml | 11 ++++++++++- .../ControlCatalog/ViewModels/ApplicationViewModel.cs | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index d12eef6b4c..07737a087c 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -25,13 +25,22 @@ - + + + + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs index c96872ef7f..6cd44eecaf 100644 --- a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs @@ -15,8 +15,12 @@ namespace ControlCatalog.ViewModels lifetime.Shutdown(); } }); + + ToggleCommand = MiniCommand.Create(() => { }); } public MiniCommand ExitCommand { get; } + + public MiniCommand ToggleCommand { get; } } } From a8c435ebbb74c5221eb139bc31217a106f60ef34 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 19:59:59 +0100 Subject: [PATCH 059/311] show checks in tray menu. --- samples/ControlCatalog/App.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 07737a087c..3f8b768f6b 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -34,10 +34,10 @@ - - + + - + From 9c0c6f7efc2941d332e00d76b2c5e0d7e0f55387 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Sep 2021 20:40:41 +0100 Subject: [PATCH 060/311] fix sub-menus win32 trayicons --- src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 8663aec773..72a7a6ff35 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Win32 if(item.Menu != null) { - newItem.ContextMenu = new ContextMenu() { Items = Populate(item.Menu) }; + newItem.Items = Populate(item.Menu); } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { From 890eabbd1fb2e211aaadb3a8bb8b3cb1cf6b4e68 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 10 Sep 2021 11:27:17 +0300 Subject: [PATCH 061/311] 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 062/311] 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 59e66f72f2a681dcf5cd69118bf94215bc9fab82 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 11:39:22 +0100 Subject: [PATCH 063/311] add some documentation. --- src/Avalonia.Controls/TrayIcon.cs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index bdfea40b5e..73f1fcb006 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -71,11 +71,13 @@ namespace Avalonia.Controls { foreach(var icon in icons) { - icon.Remove(); + icon.Dispose(); } } - + /// + /// Defines the attached property. + /// public static readonly AttachedProperty TrayIconsProperty = AvaloniaProperty.RegisterAttached("TrayIcons"); @@ -85,29 +87,22 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = Window.IconProperty.AddOwner(); - + /// + /// Defines the property. + /// public static readonly StyledProperty ToolTipTextProperty = AvaloniaProperty.Register(nameof(ToolTipText)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty IsVisibleProperty = Visual.IsVisibleProperty.AddOwner(); - private bool _disposedValue; public static void SetTrayIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(TrayIconsProperty, trayIcons); public static TrayIcons GetTrayIcons(AvaloniaObject o) => o.GetValue(TrayIconsProperty); - /// - /// Removes the notify icon from the taskbar notification area. - /// - public void Remove() - { - - } - public new ITrayIconImpl PlatformImpl => _impl; @@ -158,6 +153,9 @@ namespace Avalonia.Controls } } + /// + /// Disposes the tray icon (removing it from the tray area). + /// public void Dispose() => _impl.Dispose(); } } From 59f3ce055eb71a7c0f11f50b248646467680092a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 11:51:50 +0100 Subject: [PATCH 064/311] remove unused property. --- src/Avalonia.Controls/TrayIcon.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 73f1fcb006..cfff568a4a 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -103,9 +103,6 @@ namespace Avalonia.Controls public static TrayIcons GetTrayIcons(AvaloniaObject o) => o.GetValue(TrayIconsProperty); - - public new ITrayIconImpl PlatformImpl => _impl; - /// /// Gets or sets the icon of the TrayIcon. /// From abf4242280b71160ec7845f42a051b87b0534927 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 12:08:54 +0100 Subject: [PATCH 065/311] support trayicon clicked on osx. --- .../Platform/ITrayIconImpl.cs | 8 +++ src/Avalonia.Controls/TrayIcon.cs | 60 ++++++++++++------- .../Remote/TrayIconStub.cs | 4 +- src/Avalonia.Native/TrayIconImpl.cs | 6 +- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 36 ++++++----- 5 files changed, 71 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs index 013aff13ee..12a32ec64b 100644 --- a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -23,6 +23,14 @@ namespace Avalonia.Platform /// void SetIsVisible (bool visible); + /// + /// Gets the MenuExporter to allow native menus to be exported to the TrayIcon. + /// INativeMenuExporter? MenuExporter { get; } + + /// + /// Gets or Sets the Action that is called when the TrayIcon is clicked. + /// + Action? OnClicked { get; set; } } } diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index cfff568a4a..4d86f9ddc1 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -22,6 +22,15 @@ namespace Avalonia.Controls _impl = impl; _impl.SetIsVisible(IsVisible); + + _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + + Clicked += TrayIcon_Clicked; + } + + private void TrayIcon_Clicked(object sender, EventArgs e) + { + } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) @@ -52,28 +61,12 @@ namespace Avalonia.Controls } } - private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) - { - var trayIcons = GetTrayIcons(Application.Current); - - foreach(var icon in trayIcons) - { - icon.Dispose(); - } - } - - private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - - } - - private static void RemoveIcons (IEnumerable icons) - { - foreach(var icon in icons) - { - icon.Dispose(); - } - } + /// + /// Raised when the TrayIcon is clicked. + /// Note, this is only supported on Win32. + /// Linux and OSX this event is not raised. + /// + public event EventHandler? Clicked; /// /// Defines the attached property. @@ -132,6 +125,29 @@ namespace Avalonia.Controls public INativeMenuExporter? NativeMenuExporter => _impl.MenuExporter; + private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) + { + var trayIcons = GetTrayIcons(Application.Current); + + foreach (var icon in trayIcons) + { + icon.Dispose(); + } + } + + private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + + } + + private static void RemoveIcons(IEnumerable icons) + { + foreach (var icon in icons) + { + icon.Dispose(); + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs index 6fd70f203c..88ca076f8a 100644 --- a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs +++ b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs @@ -11,7 +11,9 @@ namespace Avalonia.DesignerSupport.Remote public Action DoubleClicked { get; set; } public Action RightClicked { get; set; } - public INativeMenuExporter MenuExporter => throw new NotImplementedException(); + public INativeMenuExporter MenuExporter => null; + + public Action OnClicked { get; set; } public void Dispose() { diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index b5cb0d8c08..7e2ade901c 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -4,6 +4,8 @@ using Avalonia.Controls.Platform; using Avalonia.Native.Interop; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Native { class TrayIconEvents : CallbackBase, IAvnTrayIconEvents @@ -34,7 +36,9 @@ namespace Avalonia.Native MenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } - + + public Action? OnClicked { get; set; } + public void Dispose() { diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index c1286f8436..ba208a4b74 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -23,10 +23,20 @@ namespace Avalonia.Win32 private IconImpl? _icon; private string? _tooltipText; private readonly Win32NativeToManagedMenuExporter _exporter; - private static Dictionary s_trayIcons = new Dictionary(); private bool _disposedValue; + public TrayIconImpl() + { + _exporter = new Win32NativeToManagedMenuExporter(); + + _uniqueId = ++_nextUniqueId; + + s_trayIcons.Add(_uniqueId, this); + } + + public Action? OnClicked { get; set; } + public INativeMenuExporter MenuExporter => _exporter; internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) @@ -37,15 +47,6 @@ namespace Avalonia.Win32 } } - public TrayIconImpl() - { - _exporter = new Win32NativeToManagedMenuExporter(); - - _uniqueId = ++_nextUniqueId; - - s_trayIcons.Add(_uniqueId, this); - } - public void SetIcon(IWindowIconImpl? icon) { _icon = icon as IconImpl; @@ -63,7 +64,6 @@ namespace Avalonia.Win32 UpdateIcon(!_iconAdded); } - private void UpdateIcon(bool remove = false) { var iconData = new NOTIFYICONDATA() @@ -105,9 +105,7 @@ namespace Avalonia.Win32 switch (lParam.ToInt32()) { case (int)WindowsMessage.WM_LBUTTONUP: - break; - - case (int)WindowsMessage.WM_LBUTTONDBLCLK: + OnClicked?.Invoke(); break; case (int)WindowsMessage.WM_RBUTTONUP: @@ -264,11 +262,11 @@ namespace Avalonia.Win32 } } - ~TrayIconImpl() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } + ~TrayIconImpl() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } public void Dispose() { From 89cb07677824ed70584b5379b314e63ec0e18fb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Sep 2021 15:16:47 +0200 Subject: [PATCH 066/311] 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 zcmb`KX*3jW7r>2WEDZ+PcarQzgg;p_wiGRrt!RwwTPV9BnVDouimaiCv5oA;lEFx} zY*{k)nW9FR?Bku@FYl-K+yBG!+;f-bo_o)E&i$Q>zGiLC$sxkQz`($1X#u@KmxRBE z4M@K$?W^+X0uX$|{1QXekl1&+!t7yu)tG^yHucoeJr)KA?r=+}v2EDhpWnEzy&&bW zZdTWH(wHENit}A8d|$fsiavN3zY6pf$sIe*9}zKn;n8cfHSCD*YDm>k599myq53}W zs6f8pQQOMMeF>2&a=M&81r0d`VoPUax>VAM04u0VK{@^f)s@y}KR9?S z_#nlo@Z?g#z!746ys)`}Mde%uC%jbSIFHSCo$`d%K0>NHBUN92kV!NB!$&a*t=~(e z^cmG~|EtCPi+%l{RK=QkNAqG@T4j|J)jMifJL#EpBv%voeAN@vF#b5i-u?6-(eQA@ zz(njF`>#Z^6C}Wrv6&2I7|dGJMXE*429gFJKKto!P9%%%tO&};GBu^UAjWMww@2kP zwV5Qt4+2QUCiq+daV!OLF|X-D6`M9iIpGmZT7TCjG5y_~^S)uSKFES`OBVG&bJ2$; z5T!z8^QWvsH)srPZx19tlrfr`9#6$|90_)q|Bh+mG3A+g_DQzN3RfBwixG})Bz?Rk zfuOw6C@fl^?f9sN@oGi&)Q5J1eUl79z3*J|7YZ68Cl}p_VPg3; z62TpjYHgAIS{DTkf*^vOuQECE7wqg=%{^`GuWkNw^pi%ooX(#$Z$-ALV45_@+iwqYHXlP1`V zpq#95CA2#~C1c7nX{4at*pUlsw0`Y+ROKh~+N8EPNRQr|0X_IWR4wpFlf*X8+ljThY2X zD<=xBbj+%mNC4ELzv@wgiA-6MME<)4GpHF;hj z9w?`iw4MKcZ(tSBBuXt$$Ft7IZ@T%18PfKbh%z@c#C5`N%x4G*fs<}6sXHFm1s*Cn zrM2(t9WNJ}Ap&*{k~&%)k(-a};=Z-IS8PM11ZK$PUG1}WABi(;%Jx!D<|#m1*9T4f z9e3-6al;r^7$iJ3m>vdpE9GhygstKQ+|)Ap%{%hSIZe;~GKkx~Ymh_+;a=;sZ4V)` zt|TmrQ$U}~wq)1870X7zHz=s(v+h&twcEDrGCeoN#pxZuBj>DwrsiOKF>S&49c`ct zP|)tlg~gtVZSean%GW^^ARbyY{ClE*?5)Z}a0ffz9z7wMD`vm9aFsKAcs9ZJ+ zJ=nF1dZFjBtHz}949{-O_s&E1c;9>a((j0>T_?{r=V#=P=I%*2dTNd(kIEk*|8qHisRH;sTa|7W* zwypVnK4+t%HUh{OO+v~zaz1@>83Z+T&lG3U#69JT@2x{TIdd((ITs1kzfkYit z$x)Xw`7pYzKR2vw^YlC*Z;j)@wQq+usH#~hCrlp#6jvq@d_yE{YlmKPw_oH2`%Zm6 z^H6@K$|TGvv9PyJ*sFs2BOSq~e1-$UgAcxVf+9ym9>{ z^3Hkc`VW9o=$?z~(@)3y1my8;x6sxbf1=+6=ti`+-fFN%0)&|%B`cx|tP75`CSU0~ zv?AcW1+m$^>Mm0?--ud^NGz_SnZlc+|VI;REzYF?^SC zt?OBvxAmzyTG|514h+`WI?2t;5&XA~cpkpgc3k$cIvPYSWFdwNV7M&V>58-ohu&8XK!vGxw?%j|R; z3>9gX*k;_Yj#_opFjb&iisI)8Bs|Dky4T&I_8l7&FLK;=pL1F@>LxdeMcyX)OUPDa z@c-zHet#vFr5B9B;$y@;y^Q^Wg}ryGp=a}B>$4agP7XCP_5nO5jLIScu8POpR&CFq z+$~bTBjLw1Ui&TPGY%W`$y52hUGW6fX`_b`6Rn_=no!{}- zS?(Dv1up-RakEvMt`H)PMwV)kR@J3~Hm%_-L5#npGa+a|kl64C%X8HHmyHBfciLDW zQO|N-gO07=>y=2o zuRjC*lT~dYAmgbU(lNV^*`Z7-!f87D5{lid`h8rF0XER<%vX-K>3 zmZ^Z9`~j){*!p=U6W4^+5~>v{a_e2pUto9RJ+D!a<#0kLq=rGe@Ao-4M(Xk61LMOr z*|5(&im=R{DxA=*TV7r|%ywyA-T~&GOCc+!sx@`~8l$==U$rH&`$Sb-a-Q(RYdB7& zYm_qj+)*O{H3m4QxJoju>)aP*?9$$N`k3s#VBLH&Vwx`tp(EGi@H!JRU(cP@+sMr!)09rds@B$&9e2rJ^A(n0wbeq?BsZ^>d>2JM@V35oWbwF^{uSHvS3!Ef!ftn6bP{B?Kys}*;>8BEFUn<7qa@%P+~@E6d#5WMQQAc2 zwZz>40g-2thcB1bUA`OjIwjKwE=H=Gfb)>QztXtaypxQ5vP8A*<+45Q4}u@<6#CW* zDiH$L$wpycLiCTk#6_-2i+V}n`>&a6kYtxhl_?HavjC9p{tPxl9np;L%HG%WJNx+e zA4u5C>C}unM$N$wLjw5*Kx6N^4%M7SUO2dK49IKMx-Hw=@gGeL>nMsbDxuI31rDiy z;fP(QqPmW<349a7SOJ(>rleQvqfY`PR~E?igEh*6l?!7#g+8mv+qU^`tJ71$WbE9UnZuFytz)Gkz+s}R9$2u_!#?eW$ zGZ`_tW;?of2OPa^xhNf!N4`4D{xAn)F|UB~K3vk%k(aum&|%79EACd&@M1KnthoCg zm_I@JgU>60tu@c{qh+`Kuj9;zTS|VuLd78sw^Yt2rZ$Ok>#niw z8`W~dUbX3m&HMD2#ERq9qHYEmrVQvkM+#wS6`1$EohG}irM#+Qz~YoN+@Fn6nd#;k z;}e()Yr#>+U-EB(TI)HmdFY6R&gT@uuMrf6az|y(E7h;JJNs4oZDPY$EHus;+oUv%-l|+(+)_Ew`PsIz)Hu`V?DR}MiBz8V zk6c02*1}!oQl9|xo&8E7>I|T*z<|ZDse!H9E#GfD5Pj7UGSHG`5pj8^2_o)!&{FD~ zJwC_*vM56Rh<@IUrFyamJ{rCH;k@5x->}_&g~7WmZs3P2EtE=|RswD@*dI3Am{` z{xt$NecsKz^eKnc6sbm(={aa-2C;UZPA(*Q{~_F2C-uWM!I)(ge5ryi8MP{s17FPX zcLt?u(fY$E=9}+OljpXa^Mooxr47du4O|*(Ejs=SKwYrK=Vig}1c0SoSK(+Zk5w*Y2JTp5B=Z1HY&+-qy{I> W9VM=vSjNnZhOI16Q*KhCBJM+%_aJO^s?|1Hpd+smuJe`Vw zW8n}81mQt*^H#+Ee;c5x@DG`FrHY^w?M-!o)IGeDw_dsKpe#yau z2Q{unorlByaj6ARJ}(uE!(n-%4*Rk+3+el{i8v?cjRYr>XxU$MXJ=Oy?X*+!o`~;E zw1TMZne1b_lmJ*15|{^(U1Tshg#=OKLNuXDI2hFaPc|-S=d7&&y8Y0zDtdZ`{}0AL zzr%k=tn6<2N$b3*R4K`pUS{j?x1QRjaoLbg)7K`Ffmj3b!#X>4hgguh(u-ue$Cb6O$>L?URx4uY&>-T ze)X4LHTpXu?nfH}g0a5N%{iYbuN%Dpuaa|1LZMW9 z#ODC8fol;(vn_`>Ctbn*o?lp{N0hA8$c)m{HKJgFI+Vj zemFWh$k2e^c)lR_$bwTW7X@?AJe_69D<{NeWcjHTubd_n2HgvV@eEMFymSVxYh#E+uZ~wWT(E+Y z^S8HqC=vkXJ~ngd1X2Nd8z%ihhg(kDgZgOr6Wkhq{r$(U(6ky*-rl?W;}W_?iCAl! zbEYn6cKurz|}u$4BFiyvGIRsxW5;dfR%S-!2v+1Oq@Q7m(U@>}=SjD8uKg4aZ8MV)pXYje)Je+u=Zu;HZY=fKQD~%oBhI&CLdBGz}!RQBNa|;*MLlqNWQ&19j)iuHFLhWEsbEC%8cAVugU{)*RQvS`93zAec1MoLe9o-YE?|H zO6BULt=85a_3)y(HjKra3|F9XAU1X=G)%0C-QI1S{`=7&6J?lpieK}Po+IF3|M>lm zB=V)E!`&;I-uHCvhO{waO@H5dMpaddIg$?A%)GQ)+qClw)Uuc-?O`7pFA<;5+CQtl z5P3-Ps5JvRs4a;NWYA`OVt#oza}msPt@0sS@VZSIMy-f1LV6>D4ckXvqxqDa7rq%m zuuM0@fwo(rQ}yYXnIhxG;^;-abR^9!>9x`NKB~cK(0r9;R>jO58Ww()>suD{L|LWc zPI61Y@~$&XkmvMqqP=orb9Su52TP1@bxCBFus*B&**oTVUGy8q_iZ<}910IB=vaHZ zI}e1^XyFZiq`+K8B&3tCXqKEy{JfAF!`63)+f%- zveFxov>^G43jez+wszt>`749XrNb4Ltolc#a4y<&>RjPB5{9pY!6@#ONwwqfkc7}Honwiwy7Z$p^K5ZU)->|{&X8S9`DB8;++P?juZNrR9H zWlNaII*1}BkMIBRJm)+=eC|E(b3d^6vG&C&6NPWxmO#JUK z0MBE?Z9?gJq7Ah)(xoAcLD$a(otKW84h>CH4)dwYMH(7blCi#yRixwg0yAc6N|1i{ zYS76Wnb+P`9#T!-w(y!b01(qWeY@Fp7%yP@BtScfp@*OCa{BXhw|XVoIqff zsoIS)Z*|{yE&!y0pZA57TC@)HJnwdL=NV-YZ_5Sl_CL#sIicc4y8q1UQt*qVq5sxS z$Je>##VGJYEwlLV^RA_Bz?WUci@=;V6-3eSD0%FJo0(2{T zW3b%EW;*~ZY& zxsjiDKu9Q2SApqfmWxVyueV&HP{C*7wLok>QNA%W^Um_GhtPv{0H{5Z!`OK6Fxl{+ z^HXLEm41c9(y_}rYA`G#;{4)@)X1r_20MTwk@Q_ps1}z4;c{u?$N7o~hx54rD|*wg z_9vN*r-VSw)R9=&5JRkhTnB8ep{MPRc>P>x;<`BsPHsa16%cBS_ZdxzO9)L^UC8W;<9sH4E-}rKg zcgKO|no@0JrWPpehOvT|#wx^u&vhEM(!`4oV(<%oJJS|q!i;!H8!C5tdVij*9=G;I zyk9F!I^nYyfQ4nFZkZmijNr)3dXfd;%RudNg8G7`-$7cOmXt{2wou3Tt?$9tfBik& zJ;OkCIZc)&CC>XG>5s2SD<}X{xI9BqNM!W^bc(-b4Ev)kxoAb%tLvM_$HdHj0 z*~k2mTh9cGQ!+=_-gcycw4Wi5*m0W+sW9sZGiI4&;yHlv?~7B>&o=K)ypsi-^uh0_ zq?A0VfMGA8B?eH`)Fv-58&s9m+xOCF&Sg*NNK#c*zTVg4wYx6V@+F4VtdMXd zA6v#ySZ*|#|3M=~o|=`7`?IWb#?%$*DVgy8v;Ew>tG2XgEUW5M^-Di$lY0D`8o9#5 zwdaKKsAXMHngQ8+YT>AO;@8)~B0art#f&|IN6SB$Y+wuL7<>haP?NnynptZ{tUu!} zeE=vwW{#V+LvOHxkjnXDQ?eou1fWg)QbYP<=z=7pzn=3x$R?{@0lt7-wcdVr4bxW- zQyrT?cUmMp>N#PP4jW%`0#*tPu}i5Y*``>cbtj^m4=t=phxNP%G3aG!U}Z7!VYq5w zamhZRm|x<<`bM<#2oHK0dJYkX%n`}@e+Boh?|qWphyCld$^Hb$h=0t3-TJO$x6hUC zox`S4&#W&`yJ=U5s;dvJDsDZUL$MzBl-#DXva06Tl!sg7J&$qs5)T<9gdXp{MdNpD zb7--7*Baf01y%d+QR-?ZHU7JW>(%MlwX$0y=Jp3kVCKX$Ku9j~uB;z;joR6^$8Tj* z7c=duT>(GAjaZM6>)WV!_Gn7=8!s84C{uGp`__ojSe~bK^|#U}+jG)ATHR>8MEq@K zUiMrl2RDG@A|rzz+uD#9?t5U9hYuWBnJ5ZHs1ObK=a+6Z?qv+8tW!WpdJjDIb!x`F zDfQ2fxlw#L*1Rh_WSH5K^qt2N9@=V1UpCo~-(iTWBiR}Td~E5)@iz>$F)C5DHUnh= zcid(WRs~aKz~frD(vYYDJGfELF%h%FIYK@z80H`jju$+t2d9}bzVxw+d+zJ|v4DM( zmL1_BHeK~`0HwYB{)JiO;WvmWIKb*6xpMT|@zKhL%e$_q+5yN`qJBB*Qg|YCZQK7q zD=+JQ+Iv1+_VCzXT!#3RU`SDYH8|&R&QuOA=y8wF*my%pRd(((<{zmU0w7t~+8zZ< z`LVUNOOh^B+G=g5mK;dfZ{){K-dLNXuOV9amvTk+{3Gq-UIMXw9e$ysp}Is{L|wUz zV*g`E5-qmxy-wn&Z8y38`Xv4k7&A8FqFwP_a2PVcLN2f($pHt#EeApUDuE?$jLe5n zb$a_hT{cQze>KiA3{|-B1+3${`K31Q>8{7DwmYnr4tU2YD!l3|Ea{;C!YpLI#In`NP zO=!DRJ@~=m=mu=&$I|`crIJ9(m?OqD+)yR+=G%YmLlJQ|XlL}Tr?d(YbjD6kn?2Hp zly4`7rF=*vmP9NCfc@AzKkj368uPBSrs%x{uGbG59;Id*+}%}C-{i?3JLatcOtiH& zcY|F;rr{-ziZj0Dr#BIQ)l0(b<|($RNx^OyaH=y4iJOH*MQh*Mep9PAR9{+H%I$sL(zsZ9DMSu%E0VGbw0~K zLvqm(&m!r2I|F&;2EAQX;}nrL{I8MA-J!dd8yC>p3m*Q9myvJW(%qKW>UG{Ej;Ee4 zHB+?~cC>_5@+7xg5S!vUXYSEnaOm%ZTjx(reRZv}-obkpb$mmYXM=Gg`NM)1z9vXvS?|1pw5j3O0 zW_e|}#Ic1;-naR3lc(<}eiZof*8w2T>7DCGs&7`F=`2iH>x|>sN1J@s;qC=eBoT3>? zCcJMXp$Up<$#%T5y(a?M@UwZ@Bp0d)xoN{`T|ur#l!dW)x{l+IGmC5az$4VsYH_@m zWkD^+U&4=}5evC?enx>|G`904u&D?nyqeXQ;q}6`sqvxW&{=NK>H3=nr0P+2M`fOX zJp3*W$A!{!5R~g}>-P?xiy`IAA)l>G9dvk(E>YK%zMOvxtOq^6PvdOzgaSRjoKg+z z?x83f)m4r!#YQyN^}FOjk}$7=-3BP5nOxW=5XcT@{5baAQ{mkM^YM`uMV%#TOT zx_44fME=#ah@eCX?OR~W>y9uJ$(*FWo`%9Pe`=M5lwN|DAHEQyV-~O2w}N>BhgVvi zggL=yrD}^$HS_F`Q~V5x>glm;RqkFQKd97ps*UEUayVCjPD3>OKJnpU4#}vH6P%wT znBEce6UfPuw#BNhWT2yvt2RJe_cXe_;$!yi9`Eymu9O`b7f{_LUZ*`L>!+I&3F5`v% z*mF+)y;cX3Uhyyl8{Q~ z4!=`B58dF=%(%W`>yctx>PP-0bVy#I&vET=a1+r*H5xYBw5=o#~ReytLfY;TgMG4B|t9Zyo6)g17 zU>Y|Os9I>O)OZ|z=Y)rDRdVyzmXp+%01#4IEf!K;9e36MDXV4bRm$Oi7!lk5k@J=N z`{OZ!+STHN5l!;G7BQfi12n^VvsFVO;;y7@Yxo1!DtY)`SvhSr!SH7{o?%K^cs){s z<5cEovN)qL+N!a*g+C}6Z!YFpdG6x_DYAUc-^w5wcpK78jZoqh`` zx7o=`q>b)n1oXuXNIe_hdP{s9jPSi0Q}kr?)#~c+4<32W%@a9$k+XhuK9a_4%UwfU nGXL2;K=S{A!#YTeJEL_Quk*YeQx7|LyEMiI=K2I(=Y;etZK*mlSpIqPpWFfEL&?`H`-BL}9yhNA_ktNK;$V&h{H`lXg!1BOr0^ z&Z7YOEu(d85XSxC&RQdAz^C`tbBEO((r7mYa-&)!=WCDf5 z;gB0(Q*9eHoqd2cKLG)oLBQQQpfd=amsrG&1gzTH1hsX4@1k`Re5;A+x2O6+zmnC@ zrRo2_cz-{G|1QFHy^Y$p;uk&_L+=g@3=uDt9Y2jJutBHZQHVW{BfY%vckj*xe=sX4 zv?p=%{jiNZVzC~mYP-PV`F0vFQjhT!A;_oyzn8_xJ*qkK2+Y##&phv7x1W_ zK4zHHQ?SOe&d}oWkkNg`S_v%II4&omh!f9EF@n_3rSHEq6dBI-DlO}r-NV0)^?2Ci z+prQTdYs3)i$*E0$v(o-CtS83z~xys$~5&8979GNE=+riDjQiPo?_{9*%18Q<9ZqF zx-02-1T}pt7(nmQKY<$_4r{}WhUdHvG=LB+g=V3HBJ=d$`Qnx-*O05+;QdG zH~Cv)?tflcF03W4I*(&Ju&80}u%LECN zhi~C!@%Q)PI?|GuuiFj1TcBInH#Nqr@{=!6-`pEx9=@MyMq@8!GUGad z8UjMDA=kM~e@J2)#R;fFE^p7y7r5?c`sksWG?R_g0lV=PcMqf4LDcE!vy=F30A5k4 zsVFa;H89YV^)WyQ*V5u=8b|nqPUxW1BWIHB(k#A@XoyB=M~EZ8Cw}Jr(pO(cWZ9FJ zIt1LgfB+njp`oH0-2(C37Q=YMI!oo$y3jUENX6g#M|IE}8XDi}Ip3E^X?A68ntScV zS6P;67V;^y6oz>KLTW?}ZH!6e zO;^+@!qTx7c=2 z$Ze+D#G+K*ubmK7ShHUOJQ#lFWp^Y92?pwJ@Xi2j#2p499Tpc0Q7@yLY%gLFr*iig ztGwpxgo+Rvwi=6AK>W)qR=p*u#q3V1etU2a*q{1a_Oi_7g1Hnyw6(W$A+YUMf3i_W zQdb{qev=4Y~+%k z4$c6CgaLI8|IM?sY>XTd*z^ALgm_XnAt-dXp-1mwS*NfTgd|-0aEqyCD^19W5*si^ z{3~g<#>Il5a>OYWMcHQSZQ4Hf_KT$LaX`k+!7#ARtIuEL;oXMb+UqoiXU#{1pB@V5 zU4zwm_LOJjsgb_k6Y|s}Mj81LxnKN}n-9@W9)dzO#cfMj{a=4y1=svwzA9&~9wph7 z)e$F6iiWIJy`=oZCVxHfJ7~#H_Hxg>I_j9KXIzfZ+a1p2LlZ>MUjC0-EmTS>C z*!x~Q(MCV!*A}rt>_3)q(*E1I5LZ&e`|aj|qgK{z^Cl1#?yqVpV`o;Ud5-eCu>>jd zCQUKV8<>G?*3(W#_X>7#B_5L|{P2#ngQF?{-KYLHe~cy*z9^f_iV~>GWT$FkW8=Aj zKT13cQTQCfuFNJS$<%y<4bSf2YZvTUwq%4hH)F8ZlZ=j&xcfxl$`N#{cpp-|hZSX~ z1Q}6QnY}cdGVx0XHBcRYjF2Mf59T{Tl9))}-d+zPF+W1pw$GhXe30A1R>+|hoZ$5L z9}Hp`tEorKh)t`$Bra)rX|N~jwZAeJ;v5(_i4Q49FfI1u<H4MVZb+^SSLm2ss}s zV_%gH^G8|>QM~lCoh(a1#;!&)2~yAVMmJgXzTafK)n|d$%rrVJYd7Vw*7eAuwo>Qi73TD${I_#|U4e)P27ESQ vFmrmD?}}+FaREO>(DeT(g#LpJ`3mot#-)E}#4M{6pc>xksAHwWnZ$nra*VpB 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 zcmbtXS5yncQ6e3rH&F-)QcRE5qcfzBX*<KEuy2`3z#FNLUs+mv;&et3Q4lb= z{BmqYZ3<%7JQ&?&H$cD&^$+-&B7v!nI^)W5!GE|H**q*20dq!Mw^y(JuL1o3L;o)= z)4Xwa#jJ%}Mf5mb3lbhEXL7!~#5M3#M$(&x@$l*;!RoHh9{2kGYV{_ny~EAPutgRm5KzO`@vxY*#9usqtgfij zUFd8Jz3GS^GZa8OcRk>fAZvOdTUC6J*O<{z#bQch9pRx=KIPjdZ14~)N`H6jNypEv z$80QkMYujIC6g>2AbmW(kY%EhKP}ic|It?UP-_(yg8{g`;*(GPe3qcjOx;Da)+Ar=W$<^`hZASqV;aw#r8{b>T z=n(J{W~eJ8_eO65J08g^B~M)}rIx_c+llS*Mh_K4Jci)_K2ftxEn-Bm`%2|k!|_1z za8=mQ^QJl;JF}kd*uxua*qt+Neli^qJJ(1(UXkE|p6?*pyt$VKtG>b=rs{l?BwkO- z9o5v0zjtfubCr_>)h#S{PJY+UG%TASuhd+uB}qR95l%l5u1hF(IiVuCAyPzx%#Yx> zU?qXip&aP>d=%jR8kW{^ymS}VP2bP0ftEQ)^YAw)g|pV?XGou?;i7V52$&84;${u> zC&B)AkmMuO=jMC09>*OyL=kW2*Cd986c2y>=PilcZCGweBGfc+uB{_o+|0}cXMKNN zyqi;tTohJGqZ^xAgmjv5wKNRT%jL}>PoYnt$;(-=o*=6>NZ2w^V-G!((fG?f>D-+4 z$4A-@?Q22Fr7dNH^g+;ibhcNp!)bE@>}|MbSJ-j~bse zlAliIgjj+Ix;YLN(*hzWbkI+efawnbn8Tdum(*(LG66(jzDT{2OS9PL!>)SG zSU4_C{}tET4i=^lIlADI9s`i?Fo4ne<>Iq|kc}E>PnhBPe%nSVgE> zPbpNmQ)*xvPhcxQ-k54Uf7wPHIJ#L`1zq+57M_Rx%GIZR zTv@y@gNwO7KX>bDDCf=NEmgtehE6&Njt_nLwYjf;@mO5Zw9+MR5F$D2(Em{|V4t-8EW(-|CV}jTr`Gn*pCz>Q`RQbnf0jE#V;PP zT5!_dkfyD?!0_S|xgXWM^VD9`86Vnl7=8B3{(Wg=MEpIpx$ZOm7rhqy?F!|@N_gX! z)=3Vnt?gOSPliiR(3X=C@&)i~UM633P%A#MMdl1$UWen}IB|$f6HhvjG$Jbis|hdntsX1sY}5r$ zZx)}SL)U-F88hyu(5!`zzKI7}f_d$oON0Lyv5x4(PwL1!+a0pU&3mipBKDPmIJ(B2 zV!&$N1HAW0@7kK{K)|%&>%%7@+%+J?N#mi4HLGyF(3dhIs zerPlLE~Z8lBb~;jG1JBw9PCT^vF*m<6?H6nV+aMgpxExfoS(KX&~htNalNS^l~@G7 z25D1!QTSbGo2CUk0uo^u9aCm^~QWWZy(YksQs(oj=Cd%yy6b0a=k zx@4A4s0jIy+wq)xSbF2cF=XIrRmhx7ze8mE&ImJy;lt}#L?`7_NZKnfmrDE6pwC7y zni)lm_@svydK>u6%n@I&FI54b6$cgMwe-?~do^+Dtc&su4d`y}bd#EO@s^xigONqK z)aDv{d?-8`qQ~x}?|YiB=+citN+F{_@4w&UKGt(82l5rEIpgb14D$g&@rvfn_ki7D z2N*Cf#1m&wnPq%I0KR1Mq1Ss^po7(}9F>oQ4$s}7SVFSXwwBl@JfwI^oU-hUhd`K(hvo2V zuj-$5zHi}hsbi$3SIM^|xAN?eM%Sl*=;#S(%0?da(^GD_izqBUzcxk-*_N^9e;*mm z!Zr3O&R-%6@m%#CtX8;noQ+Hw2N#}p+p$9RaWGzHv!Um(V$(?fRAXG|rxUS+g70#_ z^uwKu67atk<5Nz5(5xTm-(%>^4}G2V&y=PMs6L%)h`d_?nR5$&M;LIcq$v$Gvo_V8 z8??SfX-Ms4c6jSI3@$v6mM2m~=TQs~#2d{;Ec}$ZMjd=;{A`;-K6ZG;v0ak^p>zD3 zXG?_nMX_^IWtxokxVrO8%X3+lfqn9`ISlXDqAg#F8Dy>p*B3aHjXkr2%Q*NxF0V+| zOulr5n7Y#GN-Ft*j11o|$I6Z_Ix~5onPCJq#z_zD^ zh{hSBq}|T@RFhG*bsy=OmC|V}gy>I3kvcqW)XvFEeps&xpGzt;&z(!4f{|S5OjCQ> z^wz)X4k$_$RNU)8XN?I&K_&;LFzeJIsV9b5@U$qG>u3$ zhCOS48@|_foUJ+UFlNqh$o6x>B9_ddepISZN0yoDEhX`X^|_F$HEjE*Bzr&DYj7t{ z#oG8|7=zMP%i472xwC`*%F*!w+)Bx21Boi#sM=rmxoRZe${vlPxw}e7*n-zI|AJ zhw%owCb}RH$bd-j^at?C9~H6*XzPOOD*yn;c@QZOpj?DRxzXzmIzE)KDzFGkS*OVU6;;<%#y@wAlAZ2oyMdYh+$eo?f0fPsi-^*y;4Bh>Q=p zbc%_N>1Hhp{~Y)kIvyL24Y+>%SUHUFs3iX6ha^dKW!OPy>3s9l%!4@PGhWx*w{L$1 z(E{?+AZ#B5ydMTRj)FbHqAsUlK^xORo3u9QgSB@2&4~cIjsLej!QX^GuG8wxNB*Cz z{{4IW#f~3TyFjXKy8hv}C^lJmT*h;S-QofvZkiyMOL+-CgF+^G_RfAMC#SMy^UT{+ zY}3$l0vk@Wre^gvM*1J)sVOQWh&bx8?o0umZ&kqw3GEQFGO&Kd0+JxGmOoT0X#QTw zXvCp@ePZB1khoe$kD~f3-eY_hjUY9@RCy;a9AUinW~k^&Mol)>??Nq=q{27|b=y4$i3&QDqMfwYOUsLcqTP&V{Ag+B-1G=Pn`WX04_v5VAsB zyTj^%t5zC&ixq!WS(3kNv7$$82B+COkXDz7Jf5PEw$m9x<2hN_hS_PBZm7{C-l!P$ zAbA2~m8zM-JRO37iH?bW$tw)yo_cQ-L42w*bNfH4u2{_we4^{_Gezem7)2o%oIZJ9 zM8fCuW1Dz58}yr!Fh2E1RfYL+ZTH5Nm9EF0WN>;!KhGNJYU9hBb=a64Oc;K2dHtwu zJPch|Z+GR@V!kKPjgIh4)?DaZ{N0_?E(>9t{jkemam4N|lwp}X&Jf^O!3X@4;?$}{7*rc>FReUrX%OU;w0(*2Ld~&@SJA+Jn?=z%s>xtwIdlc(?;yJM`GnPu7 z7-GW?z=*N77EwD5or9ISqjbzU=V63&nXKLFkwrtwcJM)H+;k%OGKsFSgG3-8Xe5}6 z`mg0@qKiU8+J#s=zFd8a1U~qaq1zm0Lr{IL2%eTZcy*g%3u;=4>WfabpUoJN;rz8YLeovXxT2S@HO(G_)iUc}*I-45}P ze0?78L@l1CmTz}}vao&P6G0buxy3;n`o43SoB!8R;nC`?>mIhwgi@J||E5x1LI0FE z@l7T5z}*bW%N`xZ@NHIHz*WjNUFpl45|&XsM|UixZDBNA5%9xBGG;1an1kedXf!m( zXCZSE@6OH6gv@Elo7|w{iX`zqC=1Eo>tj(}Ypsy4S+ZA>XC{dZv3{rVsC&)LroiOa z2Fg8_6$uJe9*(YEUB=!n@M3qV4(=U!Vf}twxxV2p*xw7EPe>TI)o4U&^z!%7A)j%F zn9XW5f*v^3o+PdERRm0qGnE3P>>enW1yC62+%9|~>@_WMUG8CC{Wk=?@y$Ty+1*y; zWHNVO!k8J-2`x{&rAC*;<`kvTkF306$HeP}4@%81Ek}mL66R2)PARmIR*F-cn903w zZ%=GX5YD&1_L)N&1I&J?32L2!D~Zyf=jivcqLYZtX%Y28E6=Wp;h~Ta{?88!TH89T zmQxXQuTS$5>8B~_H8IwRJv zzC%v_=&mGwaarIj$P)mKQ=`LisDh!9?0`o*9enHy-wMM5c6Th5EiAv8;q^6rMD!d9 z3u`$?+nYq@)-A;|R+nP>oPW0bZT#+nO{k@TK4D*{%YlSKfOMLbkC^ELKseCG+EBvT zVM4YclkAUVZzq{vLd!3dsL@5Gms3Tbszp?%#Qp6ij>CAwsfp{-hz}gahrDg znirdeJ$IABb^VM&3>rd{$y?6H`h)Y3^VWJ=daR9a=%z394IlRs1nUb0mCVD* zFVkAiGR-*UtubaU)#Ggr?N=g|KhBIc%*r=4`EjNPii&LtK6E&^!0oPA9j+S~k?3ia zV!ge)`l0!zfFv4Rn>aCZ&8@2qA>kFKtYf~s5R32b-pcmJ;adJVB1iu6c2-xJQ|>BP quM#%^NwfY>G5Cx0^FO89?$oUPN^)=bvoheX03v!F_N@0fp8g-5->o+Q 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 zcmb_fhdUeE`zJ;TS|g|$iBV#-iq@)#E%siws<)|8BUWlu)GjG4p%kUkYg5#W-GSJo zluP8=d(YbCoBR6nVhjSG*`t8bTqAlvo|L(zG;h*UAx;oJs%t+*p=hxQB}$o z=5o-ssEE2XnnM9b*<#0c69Gn=WXtS`coQ5q69Ee7^>Wv|WzQ!g*6^BzprK38f_?CZ z^JvpQ{(#<_cK*>d;p2}B3a&MNIC^fjch|qN653{x#s>${gcKzw7mo4A&>CLjT3L{QtH1 z-$-KXU>YKyM2mJU;_>WZTCC1jHkjH=ZDx+bfAuX#n||;z3%JQJ%qV8A9f9;-(5{&M z_7W1BYH1>0aD4GT4sR_rUd5M)q9EQ$#3Zq2vK5HMekw8iHH2H0P^6f}W%*;*@hp)v zVq)VLoaGh}i_$A`%;v^85%0x^@yt6sqGaMN-Y6(hUy%25anMass0}p2?RYRfUqZCA zjsGeWbNQ3q{ekrLF+a9?m~vt@e{71XI^rBgyE4??Bl}a8fm8RiDhr}Z4=v;>k`(_2 z*lTz@aO>P zJhMa$A{&0_*!o5pSF0GxkIF3g8SYYYegzqJBQH+WEC3nG$N+?GKdniS zU2O{rjzdNfmX%!v^Z2a4BxjhqTYHgF7}GpS_A4*&i!I&Os^$aB2eq&+Km2e)jPI>D zJ>%TaAs_{PLpM^{nZt_jn<-;u-yvwd;uCoweBc!3a?Pv;wpb0q(h0fIcCB`b%GAS% z_qIMeeh)8O?cU8~L7B-}LO2N6k`Ajb$DC>yF<`HB&!I6tYEM7R3)2hF*AQ7blaa{u zSRzDkTi+Y&FAY_^JO!m0*-qs68=PuBjyZ7SP1-?EqVKk~rb(YzIl;e>k5p3_k7{7c zB0w=fO0g?T{ZOO_#VS{Cy9QIt&w1@$8avnrGsqC~iktRqRU3Rr$>9hHi9158HYaWJ z@mfI2yX9>NGkjvqDt7!q*Zz2fa{2N(81d6K5vd*KJO z8(dq#=sbY?O>MH4nYt(Mc6og5YFf?7(_i>M&jKk{AcUu+vv|UB@+Kdu^Pv4w?_`0G z;0DG0eJ4I3^+&787njL*f~-i=Bh{ak-qE9izU z%i%kQb&jio04o?!`BvwpN=|w!+v19f2Dtylu#nsZpeRg~tuFJ-ypShNoB;C~3afBq z*THGm?6Eur$Ir``)O(j&o-T$m3qWLV$a&kN`C@l!wu4V(QYv zIzF&!8oYYqCWR9p9&N9fzDo~1OGC@nfDl}(7wr`yQP$=DTQ2Va?osp}4QWAwC*ul@ zwe@_a^>FXlSVsLpD~QR*bi1ue6*(H>U`xLx#Kcz=yvHMoB0W8tQpPCRP>xjn{}uOC z)=*g`xYc5~^ZgFnlLS?aBJV~fSsLz=bB4BF?x44xb|?RP>oGIOFvX~hWme*lg<(#$ zDXKx62`HSlw+*cxot+?HpTDk8eNHhWv5C5%b;LMm6|hQSed z%m1G|4M*L`1|x&Avvl^ai^}9 z7KYTGNjfjGSQW~zIuC2YWS>kmJ31X@upf(tn-y!^=-ck%wS{=kvPiQB?Vfb2*?C}I zp4+%a1N_oWKOlg1TxR8)H#eO`++MMaX@)FJJCtj@|0qw4yY^7mYo9AuY9JGDAc+$% zChg4Y9|;pqm@THMTA3OX0Dg3E;TAHmBY^X438(dv`o&ICi(-=*^W6@>d}d4UfyFQM z&G<}-JL2wig5Gb=uQAiA&HAQ(4Qex}s~eFh;s|$|?x|kQ?lLldAsF1s0XMS46(SHb z9rv^wqfH3uwGZ0Kh?Uc3fmd`O#M^>IHqH+66=>fji>1z*@^X$>NxHpBzyec&-EXdo zQ&H-9BMIbhXQ8R}ZnGgxD%AQAoxFSltsA^s_si)_E_$+BAc2ZhBlc#fPljKwDkuMn z{{NU`>;bc;5W*5-ih(p{9PAFN^E*V|)%U(l_Ul?5l%p6;ypdMc8HIdlP5>#;q6BMH&sN`s3qNmPYT1Kg{@cTT02CwU-^|+9v_{13Mf~ad0V4 z%XOUU&fN0Rr9~{|+2zs*mJbF8$yUut^C4?U9cRDPq!dP+#fy>NlcuOz^TnS{@j8~m z3d&hN%WJgMo%kp2D*rfDs%ndC38sbbFH!Ay>AXKn zf1;YfRtbu;{jrLtTR1$9qHXcP7-q?31T}8)bLYeEw5?rb7&dIW@K>)H=B4BexvEY2 zF<+$Zqe$kEaw3Le?j2UlOA5|AoBiAS&e;8gFs{$-3cUwZS~_a*G}d1dSAT+u_k^V%aCw8* z^CW}2F}NPpVdemU6#;|k3N?d<#_SSn5v)hJ;y{eq3B$@$shfIwh2W;_Q?Qdz^?Q6^cRfsE&Iyr89H#s|{Hu>Hcz*I1IybRQ;pXs%kdl*)W*l+8@868#&c*^IHoE;F z#L6UA-|=Rw4?4jP!XHJrIT}pzaUS-$8)~i!W zkPaJp8)kgputd>rD0y0Zj)q~*u;gvmTU$Ql`G$|h(q{kLDXnV1>8@Lj--TVDFZ)Xo zq@OG={HU6PO2V&uQj4o$qcW)Z#e!O!(W5zg6GqQ<0Eoj`b}&?LdU~zPo9eZgx%gd) zbdLRT=i#N=$u1#D+|z0wa2yXW*KH}0S!-l;L+ zLPmjbTrC?=%y1MeH9Ou%3|0gszxY$(>n8imSXz*k=?&p?-mp>JNb$66a?PW&uUh#Q zgVB=w4XvdaqGMCK%IspEmJtoox$FE_ZWU-uM8z^fZx?!xIY|YD_c!JVw@7H^r^XfU zckI6$Gb`yLRaN4(10MZm_$sfiLoLK*1w{58vB7J_#=p=NpTIr@{K&-^`xu-TK-;B$ z=jR&`;(?tl<6l3BUuQlg$(@~kK6uuA|DhqZd0L^g{C68EY76rHlJ@oczemUYn|G=G PBaMNssZNE~pHcq-(MI2G literal 0 HcmV?d00001 From 5d568d104aa53a95943f91ab4aa4dc8e3660eb52 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 14:41:20 +0100 Subject: [PATCH 067/311] handle items being programatically removed. --- src/Avalonia.Controls/TrayIcon.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 4d86f9ddc1..fdf30846f4 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -129,15 +129,12 @@ namespace Avalonia.Controls { var trayIcons = GetTrayIcons(Application.Current); - foreach (var icon in trayIcons) - { - icon.Dispose(); - } + RemoveIcons(trayIcons); } private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { - + RemoveIcons(e.OldItems.Cast()); } private static void RemoveIcons(IEnumerable icons) From 4e350e64d86e11d2a70ec5d94f7f3681811d46bb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 14:41:35 +0100 Subject: [PATCH 068/311] demo tray icon constantly shown and hidden. --- src/Avalonia.Controls/TrayIcon.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index fdf30846f4..e1c70afae9 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Platform; +using Avalonia.Threading; #nullable enable @@ -25,12 +27,16 @@ namespace Avalonia.Controls _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); - Clicked += TrayIcon_Clicked; + var timer = new DispatcherTimer(); + timer.Interval = TimeSpan.FromSeconds(1); + timer.Tick += Timer_Tick; + + timer.Start(); } - private void TrayIcon_Clicked(object sender, EventArgs e) + private void Timer_Tick(object sender, EventArgs e) { - + IsVisible = !IsVisible; } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) From a3c8396cf5a221edacefa63b6b743ffc00253615 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Sep 2021 15:03:07 +0100 Subject: [PATCH 069/311] Tray icon osx, implement visibility toggle and lifetime management. --- native/Avalonia.Native/src/OSX/trayicon.h | 4 ++++ native/Avalonia.Native/src/OSX/trayicon.mm | 26 ++++++++++++++++++++-- src/Avalonia.Controls/TrayIcon.cs | 11 --------- src/Avalonia.Native/TrayIconImpl.cs | 4 ++-- src/Avalonia.Native/avn.idl | 1 + 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h index 4329668cbd..11ad71756a 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.h +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -22,9 +22,13 @@ public: AvnTrayIcon(IAvnTrayIconEvents* events); + ~AvnTrayIcon (); + virtual HRESULT SetIcon (void* data, size_t length) override; virtual HRESULT SetMenu (IAvnMenu* menu) override; + + virtual HRESULT SetIsVisible (bool isVisible) override; }; #endif /* trayicon_h */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 67b6bd4874..79b16f82c6 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -14,7 +14,17 @@ AvnTrayIcon::AvnTrayIcon(IAvnTrayIconEvents* events) { _events = events; - _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; + _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; + +} + +AvnTrayIcon::~AvnTrayIcon() +{ + if(_native != nullptr) + { + [[_native statusBar] removeStatusItem:_native]; + _native = nullptr; + } } HRESULT AvnTrayIcon::SetIcon (void* data, size_t length) @@ -57,9 +67,21 @@ HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) if(appMenu != nullptr) { - [_native setMenu:appMenu->GetNative()]; + [_native setMenu:appMenu->GetNative()]; } } return S_OK; } + +HRESULT AvnTrayIcon::SetIsVisible(bool isVisible) +{ + START_COM_CALL; + + @autoreleasepool + { + [_native setVisible:isVisible]; + } + + return S_OK; +} diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index e1c70afae9..bd346c1e5d 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -26,17 +26,6 @@ namespace Avalonia.Controls _impl.SetIsVisible(IsVisible); _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); - - var timer = new DispatcherTimer(); - timer.Interval = TimeSpan.FromSeconds(1); - timer.Tick += Timer_Tick; - - timer.Start(); - } - - private void Timer_Tick(object sender, EventArgs e) - { - IsVisible = !IsVisible; } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index 7e2ade901c..b8b81214f1 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Native public void Dispose() { - + _native.Dispose(); } public unsafe void SetIcon(IWindowIconImpl? icon) @@ -73,7 +73,7 @@ namespace Avalonia.Native public void SetIsVisible(bool visible) { - + _native.SetIsVisible(visible.AsComBool()); } public INativeMenuExporter? MenuExporter { get; } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 47ed7116a7..c6fd3850c5 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -671,6 +671,7 @@ interface IAvnTrayIcon : IUnknown { HRESULT SetIcon(void* data, size_t length); HRESULT SetMenu(IAvnMenu* menu); + HRESULT SetIsVisible(bool isVisible); } [uuid(a687a6d9-73aa-4fef-9b4a-61587d7285d3)] From 7a8650ae06867b16c716740b117bf61aef87653a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 10 Sep 2021 18:05:37 +0200 Subject: [PATCH 070/311] 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 071/311] 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 @@ - + @@ -46,5 +46,5 @@ - + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index e044987a27..36b6fc2dcd 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -106,7 +106,6 @@ namespace ControlCatalog if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) { desktopLifetime.MainWindow = new MainWindow(); - desktopLifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown; } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 73a0856359..7b400d3600 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls static TrayIcon () { - TrayIconsProperty.Changed.Subscribe(args => + IconsProperty.Changed.Subscribe(args => { if (args.Sender is Application application) { @@ -65,8 +65,8 @@ namespace Avalonia.Controls /// /// Defines the attached property. /// - public static readonly AttachedProperty TrayIconsProperty - = AvaloniaProperty.RegisterAttached("TrayIcons"); + public static readonly AttachedProperty IconsProperty + = AvaloniaProperty.RegisterAttached("Icons"); /// /// Defines the property. @@ -86,9 +86,9 @@ namespace Avalonia.Controls public static readonly StyledProperty IsVisibleProperty = Visual.IsVisibleProperty.AddOwner(); - public static void SetTrayIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(TrayIconsProperty, trayIcons); + public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons); - public static TrayIcons GetTrayIcons(AvaloniaObject o) => o.GetValue(TrayIconsProperty); + public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); /// /// Gets or sets the icon of the TrayIcon. @@ -121,7 +121,7 @@ namespace Avalonia.Controls private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) { - var trayIcons = GetTrayIcons(Application.Current); + var trayIcons = GetIcons(Application.Current); RemoveIcons(trayIcons); } From 40fef4976cbc7e5396c8a441d8ce50f874e73625 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 16:19:22 +0100 Subject: [PATCH 156/311] fix comment. --- src/Avalonia.Base/Logging/LogArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 08ed8669ef..c049f9e763 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -41,7 +41,7 @@ namespace Avalonia.Logging public const string Win32Platform = nameof(Win32Platform); /// - /// The log event comes from Win32Platform. + /// The log event comes from X11Platform. /// public const string X11Platform = nameof(X11Platform); } From 34b96f45f30bc35c43eebee3796feba4513501cc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 17:55:30 +0100 Subject: [PATCH 157/311] make the trayicon menu property an explicit member of trayicon. --- samples/ControlCatalog/App.xaml | 4 ++-- .../Platform/ITopLevelNativeMenuExporter.cs | 2 +- src/Avalonia.Controls/TrayIcon.cs | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 845413a455..6e57686e00 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -30,7 +30,7 @@ - + @@ -43,7 +43,7 @@ - + diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 5e5f7b18ec..9b779054f3 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -7,7 +7,7 @@ namespace Avalonia.Controls.Platform { public interface INativeMenuExporter { - void SetNativeMenu(NativeMenu menu); + void SetNativeMenu(NativeMenu? menu); } public interface ITopLevelNativeMenuExporter : INativeMenuExporter diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 7b400d3600..5b10fa20ea 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -68,6 +68,12 @@ namespace Avalonia.Controls public static readonly AttachedProperty IconsProperty = AvaloniaProperty.RegisterAttached("Icons"); + /// + /// Defines the property. + /// + public static readonly StyledProperty MenuProperty + = AvaloniaProperty.Register(nameof(Menu)); + /// /// Defines the property. /// @@ -90,6 +96,15 @@ namespace Avalonia.Controls public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); + /// + /// Gets or sets the Menu of the TrayIcon. + /// + public NativeMenu? Menu + { + get => GetValue(MenuProperty); + set => SetValue(MenuProperty, value); + } + /// /// Gets or sets the icon of the TrayIcon. /// @@ -155,6 +170,10 @@ namespace Avalonia.Controls { _impl.SetToolTipText(change.NewValue.GetValueOrDefault()); } + else if (change.Property == MenuProperty) + { + _impl.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + } } /// From 786375aff9016f8c5bb95265a5f2edf4097e33f8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Oct 2021 18:08:48 +0100 Subject: [PATCH 158/311] handle platforms that return null for the trayiconimpl. --- .../Platform/IWindowingPlatform.cs | 4 +- .../Platform/PlatformManager.cs | 5 ++- src/Avalonia.Controls/TrayIcon.cs | 23 ++++++----- .../Remote/PreviewerWindowingPlatform.cs | 2 +- .../Remote/TrayIconStub.cs | 40 ------------------- .../AvaloniaHeadlessPlatform.cs | 5 +-- src/iOS/Avalonia.iOS/Stubs.cs | 2 +- .../WindowingPlatformMock.cs | 2 +- .../MockWindowingPlatform.cs | 2 +- 9 files changed, 24 insertions(+), 61 deletions(-) delete mode 100644 src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 4efa92cc6b..21882b1271 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace Avalonia.Platform { public interface IWindowingPlatform @@ -6,6 +8,6 @@ namespace Avalonia.Platform IWindowImpl CreateEmbeddableWindow(); - ITrayIconImpl CreateTrayIcon(); + ITrayIconImpl? CreateTrayIcon(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index fe83e37909..054f823d6d 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -1,8 +1,9 @@ using System; using System.Reactive.Disposables; -using Avalonia.Media; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Controls.Platform { public static partial class PlatformManager @@ -22,7 +23,7 @@ namespace Avalonia.Controls.Platform { } - public static ITrayIconImpl CreateTrayIcon () + public static ITrayIconImpl? CreateTrayIcon () { var platform = AvaloniaLocator.Current.GetService(); diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 5b10fa20ea..b98e342735 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -16,15 +16,18 @@ namespace Avalonia.Controls public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { - private readonly ITrayIconImpl _impl; + private readonly ITrayIconImpl? _impl; - private TrayIcon(ITrayIconImpl impl) + private TrayIcon(ITrayIconImpl? impl) { - _impl = impl; + if (impl != null) + { + _impl = impl; - _impl.SetIsVisible(IsVisible); + _impl.SetIsVisible(IsVisible); - _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + } } public TrayIcon () : this(PlatformManager.CreateTrayIcon()) @@ -160,25 +163,25 @@ namespace Avalonia.Controls if(change.Property == IconProperty) { - _impl.SetIcon(Icon.PlatformImpl); + _impl?.SetIcon(Icon.PlatformImpl); } else if (change.Property == IsVisibleProperty) { - _impl.SetIsVisible(change.NewValue.GetValueOrDefault()); + _impl?.SetIsVisible(change.NewValue.GetValueOrDefault()); } else if (change.Property == ToolTipTextProperty) { - _impl.SetToolTipText(change.NewValue.GetValueOrDefault()); + _impl?.SetToolTipText(change.NewValue.GetValueOrDefault()); } else if (change.Property == MenuProperty) { - _impl.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + _impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); } } /// /// Disposes the tray icon (removing it from the tray area). /// - public void Dispose() => _impl.Dispose(); + public void Dispose() => _impl?.Dispose(); } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index caca15b3a3..ada63f5326 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -17,7 +17,7 @@ namespace Avalonia.DesignerSupport.Remote private static PreviewerWindowImpl s_lastWindow; public static List PreFlightMessages = new List(); - public ITrayIconImpl CreateTrayIcon() => new TrayIconStub(); + public ITrayIconImpl CreateTrayIcon() => null; public IWindowImpl CreateWindow() => new WindowStub(); diff --git a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs b/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs deleted file mode 100644 index 88ca076f8a..0000000000 --- a/src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Platform; - -namespace Avalonia.DesignerSupport.Remote -{ - class TrayIconStub : ITrayIconImpl - { - public Action Clicked { get; set; } - public Action DoubleClicked { get; set; } - public Action RightClicked { get; set; } - - public INativeMenuExporter MenuExporter => null; - - public Action OnClicked { get; set; } - - public void Dispose() - { - throw new NotImplementedException(); - } - - public void SetIcon(IWindowIconImpl icon) - { - } - - public void SetIsVisible(bool visible) - { - } - - public void SetMenu(NativeMenu menu) - { - throw new NotImplementedException(); - } - - public void SetToolTipText(string text) - { - } - } -} diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index afaec3a8a0..4f0b9e9e8d 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -52,10 +52,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() - { - throw new NotImplementedException(); - } + public ITrayIconImpl CreateTrayIcon() => return null; } internal static void Initialize() diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs index b13dfd39e0..9c46aa78cc 100644 --- a/src/iOS/Avalonia.iOS/Stubs.cs +++ b/src/iOS/Avalonia.iOS/Stubs.cs @@ -22,7 +22,7 @@ namespace Avalonia.iOS public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException(); - public ITrayIconImpl CreateTrayIcon() => throw new NotSupportedException(); + public ITrayIconImpl CreateTrayIcon() => null; } class PlatformIconLoaderStub : IPlatformIconLoader diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index 5c5ec8be90..e8471d41fb 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests public ITrayIconImpl CreateTrayIcon() { - throw new NotImplementedException(); + return null; } public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 4074885505..eb18030ca8 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -128,7 +128,7 @@ namespace Avalonia.UnitTests public ITrayIconImpl CreateTrayIcon() { - throw new NotImplementedException(); + return null; } private static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl From f3436f16c9f4b0d565daaf367b28304fb104207d Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 5 Oct 2021 21:57:38 +0800 Subject: [PATCH 159/311] remove debug code --- src/Avalonia.X11/X11TrayIconImpl.cs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 99a850dabb..c1011271f3 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -15,14 +15,13 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId = 0; private static int GetTID() => s_trayIconInstanceId++; private ObjectPath _dbusmenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; private Connection con; private DbusPixmap _icon; - + private IStatusNotifierWatcher _statusNotifierWatcher; private string _sysTrayServiceName; @@ -30,10 +29,10 @@ namespace Avalonia.X11 private bool _isActive; private bool _isDisposed; private readonly bool _ctorFinished; - + public INativeMenuExporter MenuExporter { get; } public Action OnClicked { get; set; } - + public X11TrayIconImpl() { con = DBusHelper.TryGetConnection(); @@ -42,7 +41,7 @@ namespace Avalonia.X11 CreateTrayIcon(); _ctorFinished = true; } - + public async void CreateTrayIcon() { _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", @@ -111,7 +110,7 @@ namespace Avalonia.X11 _icon = new DbusPixmap(w, h, pixByteArray); _statusNotifierItemDbusObj.SetIcon(_icon); } - + public void SetIsVisible(bool visible) { if (con == null || _isDisposed || !_ctorFinished) return; @@ -134,7 +133,7 @@ namespace Avalonia.X11 _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } } - + /// /// DBus Object used for setting system tray icons. /// @@ -232,12 +231,7 @@ namespace Avalonia.X11 public async Task GetAsync(string prop) { - if (prop.Contains("Menu")) - { - return _backingProperties.Menu; - } - - return default; + return null; } public async Task GetAllAsync() From 7a547025df3fd774d985176968506d06d9b3aaca Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 5 Oct 2021 22:02:17 +0800 Subject: [PATCH 160/311] handle if we're unable to get a dbus connection --- src/Avalonia.X11/X11TrayIconImpl.cs | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index c1011271f3..8f550f4299 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; +using Avalonia.Logging; using Avalonia.Platform; using Tmds.DBus; @@ -19,7 +20,7 @@ namespace Avalonia.X11 private static int GetTID() => s_trayIconInstanceId++; private ObjectPath _dbusmenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private Connection con; + private Connection _con; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; @@ -35,16 +36,26 @@ namespace Avalonia.X11 public X11TrayIconImpl() { - con = DBusHelper.TryGetConnection(); + _con = DBusHelper.TryGetConnection(); + + if (_con is null) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, "Unable to get a dbus connection for system tray icons."); + return; + } + _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con); + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, _con); CreateTrayIcon(); _ctorFinished = true; } public async void CreateTrayIcon() { - _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", + if(_con is null) return; + + _statusNotifierWatcher = _con.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); var pid = Process.GetCurrentProcess().Id; @@ -53,9 +64,9 @@ namespace Avalonia.X11 _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusmenuPath); - await con.RegisterObjectAsync(_statusNotifierItemDbusObj); + await _con.RegisterObjectAsync(_statusNotifierItemDbusObj); - await con.RegisterServiceAsync(_sysTrayServiceName); + await _con.RegisterServiceAsync(_sysTrayServiceName); await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); @@ -72,8 +83,10 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - con.UnregisterObject(_statusNotifierItemDbusObj); - await con.UnregisterServiceAsync(_sysTrayServiceName); + if(_con is null) return; + + _con.UnregisterObject(_statusNotifierItemDbusObj); + await _con.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; } @@ -81,12 +94,12 @@ namespace Avalonia.X11 { _isDisposed = true; DestroyTrayIcon(); - con.Dispose(); + _con?.Dispose(); } public void SetIcon(IWindowIconImpl icon) { - if (con == null || _isDisposed) return; + if (_con is null || _isDisposed) return; if (!(icon is X11IconData x11icon)) return; var w = (int)x11icon.Data[0]; @@ -113,7 +126,7 @@ namespace Avalonia.X11 public void SetIsVisible(bool visible) { - if (con == null || _isDisposed || !_ctorFinished) return; + if (_con is null || _isDisposed || !_ctorFinished) return; if (visible & !_isActive) { @@ -128,7 +141,7 @@ namespace Avalonia.X11 public void SetToolTipText(string text) { - if (con == null || _isDisposed) return; + if (_con is null || _isDisposed) return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } From f97252caa62142ef5393ff918c780352ff7c68c7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 15:19:31 +0100 Subject: [PATCH 161/311] fix warning. --- src/Avalonia.Controls/TrayIcon.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index b98e342735..ad9a668cf2 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls { IconsProperty.Changed.Subscribe(args => { - if (args.Sender is Application application) + if (args.Sender is Application) { if(args.OldValue.Value != null) { @@ -135,7 +135,7 @@ namespace Avalonia.Controls set => SetValue(IsVisibleProperty, value); } - public INativeMenuExporter? NativeMenuExporter => _impl.MenuExporter; + public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter; private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) { From 65beb89ee300c04ef4b1fd7edc5b574ae810d5cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 15:20:17 +0100 Subject: [PATCH 162/311] fix compiler error. --- src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 4f0b9e9e8d..0ca2733cde 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -52,7 +52,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() => return null; + public ITrayIconImpl CreateTrayIcon() => null; } internal static void Initialize() From 6978eababad2fe96427d954e7dd4a869ce404064 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 15:24:26 +0100 Subject: [PATCH 163/311] fix some warnings. --- src/Avalonia.X11/X11TrayIconImpl.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 8f550f4299..032cd07296 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Reactive.Disposables; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; @@ -10,17 +9,17 @@ using Avalonia.Logging; using Avalonia.Platform; using Tmds.DBus; -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId = 0; + private static int s_trayIconInstanceId; private static int GetTID() => s_trayIconInstanceId++; - private ObjectPath _dbusmenuPath; + private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private Connection _con; + private readonly Connection _con; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; @@ -45,8 +44,8 @@ namespace Avalonia.X11 return; } - _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, _con); + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _con); CreateTrayIcon(); _ctorFinished = true; } @@ -62,7 +61,7 @@ namespace Avalonia.X11 var tid = GetTID(); _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusmenuPath); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); await _con.RegisterObjectAsync(_statusNotifierItemDbusObj); From 285e483cf976fbdb15f99dea565e1c74f17e0362 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 5 Oct 2021 22:35:54 +0800 Subject: [PATCH 164/311] remove unnecessary async Tasks and replace them with non-async Task.FromResult --- src/Avalonia.X11/X11TrayIconImpl.cs | 61 ++++++++++++----------------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 032cd07296..20599c4629 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -43,7 +43,7 @@ namespace Avalonia.X11 ?.Log(this, "Unable to get a dbus connection for system tray icons."); return; } - + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _con); CreateTrayIcon(); @@ -52,8 +52,8 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if(_con is null) return; - + if (_con is null) return; + _statusNotifierWatcher = _con.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); @@ -82,7 +82,7 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if(_con is null) return; + if (_con is null) return; _con.UnregisterObject(_statusNotifierItemDbusObj); await _con.UnregisterServiceAsync(_sysTrayServiceName); @@ -179,22 +179,17 @@ namespace Avalonia.X11 InvalidateAll(); } - public async Task ContextMenuAsync(int X, int Y) - { - } + public Task ContextMenuAsync(int X, int Y) => Task.CompletedTask; - public async Task ActivateAsync(int X, int Y) + public Task ActivateAsync(int X, int Y) { ActivationDelegate?.Invoke(); + return Task.CompletedTask; } - public async Task SecondaryActivateAsync(int X, int Y) - { - } + public Task SecondaryActivateAsync(int X, int Y) => Task.CompletedTask; - public async Task ScrollAsync(int Delta, string Orientation) - { - } + public Task ScrollAsync(int Delta, string Orientation) => Task.CompletedTask; public void InvalidateAll() { @@ -205,58 +200,52 @@ namespace Avalonia.X11 OnTooltipChanged?.Invoke(); } - public async Task WatchNewTitleAsync(Action handler, Action onError = null) + public Task WatchNewTitleAsync(Action handler, Action onError = null) { OnTitleChanged += handler; - return Disposable.Create(() => OnTitleChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); } - public async Task WatchNewIconAsync(Action handler, Action onError = null) + public Task WatchNewIconAsync(Action handler, Action onError = null) { OnIconChanged += handler; - return Disposable.Create(() => OnIconChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); } - public async Task WatchNewAttentionIconAsync(Action handler, Action onError = null) + public Task WatchNewAttentionIconAsync(Action handler, Action onError = null) { OnAttentionIconChanged += handler; - return Disposable.Create(() => OnAttentionIconChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); } - public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) + public Task WatchNewOverlayIconAsync(Action handler, Action onError = null) { OnOverlayIconChanged += handler; - return Disposable.Create(() => OnOverlayIconChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); } - public async Task WatchNewToolTipAsync(Action handler, Action onError = null) + public Task WatchNewToolTipAsync(Action handler, Action onError = null) { OnTooltipChanged += handler; - return Disposable.Create(() => OnTooltipChanged -= handler); + return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); } - public async Task WatchNewStatusAsync(Action handler, Action onError = null) + public Task WatchNewStatusAsync(Action handler, Action onError = null) { NewStatusAsync += handler; - return Disposable.Create(() => NewStatusAsync -= handler); + return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); } - public async Task GetAsync(string prop) - { - return null; - } + public Task GetAsync(string prop) => Task.FromResult(new object()); - public async Task GetAllAsync() - { - return _backingProperties; - } + public Task GetAllAsync() => Task.FromResult(_backingProperties); public Task SetAsync(string prop, object val) => Task.CompletedTask; - public async Task WatchPropertiesAsync(Action handler) + public Task WatchPropertiesAsync(Action handler) { OnPropertyChange += handler; - return Disposable.Create(() => OnPropertyChange -= handler); + return Task.FromResult(Disposable.Create(() => OnPropertyChange -= handler)); } public void SetIcon(DbusPixmap dbusPixmap) From b5b614bb60def1a1d85e14556a068700534d310f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 16:26:55 +0100 Subject: [PATCH 165/311] fix warnings. --- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 72a7a6ff35..f8ae128725 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -10,18 +10,18 @@ namespace Avalonia.Win32 { private NativeMenu? _nativeMenu; - public void SetNativeMenu(NativeMenu nativeMenu) + public void SetNativeMenu(NativeMenu? nativeMenu) { _nativeMenu = nativeMenu; } - private IEnumerable? Populate (NativeMenu nativeMenu) + private IEnumerable Populate (NativeMenu nativeMenu) { var items = new List(); foreach (var menuItem in nativeMenu.Items) { - if (menuItem is NativeMenuItemSeparator separator) + if (menuItem is NativeMenuItemSeparator) { items.Add(new MenuItem { Header = "-" }); } From 10a748a235ab22e65729fb8a45242589148a7c73 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Oct 2021 16:38:08 +0100 Subject: [PATCH 166/311] make new win32 types internal. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 7057199c52..8cb7b42833 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -2300,7 +2300,7 @@ namespace Avalonia.Win32.Interop public uint DamageMask; } - public enum NIM : uint + internal enum NIM : uint { ADD = 0x00000000, MODIFY = 0x00000001, @@ -2310,7 +2310,7 @@ namespace Avalonia.Win32.Interop } [Flags] - public enum NIF : uint + internal enum NIF : uint { MESSAGE = 0x00000001, ICON = 0x00000002, @@ -2323,7 +2323,7 @@ namespace Avalonia.Win32.Interop } [Flags] - public enum NIIF : uint + internal enum NIIF : uint { NONE = 0x00000000, INFO = 0x00000001, @@ -2337,7 +2337,7 @@ namespace Avalonia.Win32.Interop } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - public class NOTIFYICONDATA + internal class NOTIFYICONDATA { public int cbSize = Marshal.SizeOf(); public IntPtr hWnd; From 52e188507c7a45d9cf06af180ae86f86397c1c3e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 13:26:03 +0100 Subject: [PATCH 167/311] win32 - fix activate method, now same as wpf. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 3 +++ src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 8cb7b42833..938f4222e0 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1110,6 +1110,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetActiveWindow(IntPtr hWnd); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] public static extern IntPtr SetCapture(IntPtr hWnd); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8fc25f8cfa..8b4703d2ec 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -507,7 +507,7 @@ namespace Avalonia.Win32 public void Activate() { - SetActiveWindow(_hwnd); + SetForegroundWindow(_hwnd); } public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); From d13fc38e6928746d8ac3772f18a9aef9967cafc5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 13:39:07 +0100 Subject: [PATCH 168/311] win32 - actually activate window correctly during show --- src/Windows/Avalonia.Win32/WindowImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8b4703d2ec..d1b2115cf6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1000,6 +1000,7 @@ namespace Avalonia.Win32 if (!Design.IsDesignMode && activate) { SetFocus(_hwnd); + SetForegroundWindow(_hwnd); } } From 261a0f0c2319b9849a3a2f1ae70e4bd6d0b83a79 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 13:39:17 +0100 Subject: [PATCH 169/311] fix tray icon closing. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 25 ++-------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index fce56bcb21..e8fc00fb74 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -181,32 +181,16 @@ namespace Avalonia.Win32 _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); Topmost = true; - Activated += TrayPopupRoot_Activated; Deactivated += TrayPopupRoot_Deactivated; - LostFocus += TrayPopupRoot_LostFocus1; - ShowInTaskbar = false; - } - private void TrayPopupRoot_LostFocus1(object sender, Interactivity.RoutedEventArgs e) - { - Debug.WriteLine("TrayIcon - Lost Focus"); - } - - private void TrayPopupRoot_Activated(object sender, EventArgs e) - { - Debug.WriteLine("TrayIcon - Activated"); + ShowActivated = true; } private void TrayPopupRoot_Deactivated(object sender, EventArgs e) { - Debug.WriteLine("TrayIcon - Deactivated"); - - Dispatcher.UIThread.Post(() => - { - Close(); - }); + Close(); } private void MoveResize(PixelPoint position, Size size, double scaling) @@ -215,11 +199,6 @@ namespace Avalonia.Win32 PlatformImpl.Resize(size, PlatformResizeReason.Layout); } - private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e) - { - Close(); - } - protected override void ArrangeCore(Rect finalRect) { base.ArrangeCore(finalRect); From ffc79482bed8caa68e0c6cfb2af03e615c0a1564 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:25:40 +0800 Subject: [PATCH 170/311] fix review comments on X11TrayIconImpl.cs --- src/Avalonia.X11/X11TrayIconImpl.cs | 69 ++++++++++++++--------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 20599c4629..160537cb26 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,4 +1,6 @@ -using System; +#nullable enable + +using System; using System.Diagnostics; using System.Reactive.Disposables; using System.Runtime.CompilerServices; @@ -15,11 +17,10 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId; - private static int GetTID() => s_trayIconInstanceId++; + private static int trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private readonly Connection _con; + private readonly Connection _connection; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; @@ -35,9 +36,9 @@ namespace Avalonia.X11 public X11TrayIconImpl() { - _con = DBusHelper.TryGetConnection(); + _connection = DBusHelper.TryGetConnection(); - if (_con is null) + if (_connection is null) { Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "Unable to get a dbus connection for system tray icons."); @@ -45,27 +46,27 @@ namespace Avalonia.X11 } _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _con); + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); CreateTrayIcon(); _ctorFinished = true; } public async void CreateTrayIcon() { - if (_con is null) return; + if (_connection is null) return; - _statusNotifierWatcher = _con.CreateProxy("org.kde.StatusNotifierWatcher", + _statusNotifierWatcher = _connection.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); var pid = Process.GetCurrentProcess().Id; - var tid = GetTID(); + var tid = trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - await _con.RegisterObjectAsync(_statusNotifierItemDbusObj); + await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - await _con.RegisterServiceAsync(_sysTrayServiceName); + await _connection.RegisterServiceAsync(_sysTrayServiceName); await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); @@ -82,10 +83,10 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if (_con is null) return; + if (_connection is null) return; - _con.UnregisterObject(_statusNotifierItemDbusObj); - await _con.UnregisterServiceAsync(_sysTrayServiceName); + _connection.UnregisterObject(_statusNotifierItemDbusObj); + await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; } @@ -93,30 +94,28 @@ namespace Avalonia.X11 { _isDisposed = true; DestroyTrayIcon(); - _con?.Dispose(); + _connection?.Dispose(); } public void SetIcon(IWindowIconImpl icon) { - if (_con is null || _isDisposed) return; + if (_connection is null || _isDisposed) return; if (!(icon is X11IconData x11icon)) return; var w = (int)x11icon.Data[0]; var h = (int)x11icon.Data[1]; - var rx = x11icon.Data.AsSpan(2); var pixLength = w * h; - var pixByteArrayCounter = 0; var pixByteArray = new byte[w * h * 4]; for (var i = 0; i < pixLength; i++) { - var u = rx[i].ToUInt32(); - pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF000000) >> 24); - pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF0000) >> 16); - pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF00) >> 8); - pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF); + var rawPixel = x11icon.Data[i+2].ToUInt32(); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); + pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); } _icon = new DbusPixmap(w, h, pixByteArray); @@ -125,7 +124,7 @@ namespace Avalonia.X11 public void SetIsVisible(bool visible) { - if (_con is null || _isDisposed || !_ctorFinished) return; + if (_connection is null || _isDisposed || !_ctorFinished) return; if (visible & !_isActive) { @@ -140,7 +139,7 @@ namespace Avalonia.X11 public void SetToolTipText(string text) { - if (_con is null || _isDisposed) return; + if (_connection is null || _isDisposed) return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } @@ -179,17 +178,17 @@ namespace Avalonia.X11 InvalidateAll(); } - public Task ContextMenuAsync(int X, int Y) => Task.CompletedTask; + public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; - public Task ActivateAsync(int X, int Y) + public Task ActivateAsync(int x, int y) { ActivationDelegate?.Invoke(); return Task.CompletedTask; } - public Task SecondaryActivateAsync(int X, int Y) => Task.CompletedTask; + public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - public Task ScrollAsync(int Delta, string Orientation) => Task.CompletedTask; + public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; public void InvalidateAll() { @@ -276,10 +275,10 @@ namespace Avalonia.X11 [DBusInterface("org.kde.StatusNotifierItem")] internal interface IStatusNotifierItem : IDBusObject { - Task ContextMenuAsync(int X, int Y); - Task ActivateAsync(int X, int Y); - Task SecondaryActivateAsync(int X, int Y); - Task ScrollAsync(int Delta, string Orientation); + Task ContextMenuAsync(int x, int y); + Task ActivateAsync(int x, int y); + Task SecondaryActivateAsync(int x, int y); + Task ScrollAsync(int delta, string orientation); Task WatchNewTitleAsync(Action handler, Action onError = null); Task WatchNewIconAsync(Action handler, Action onError = null); Task WatchNewAttentionIconAsync(Action handler, Action onError = null); @@ -337,7 +336,7 @@ namespace Avalonia.X11 private static readonly DbusPixmap[] s_blank = { - new DbusPixmap(0, 0, new byte[] { }), new DbusPixmap(0, 0, new byte[] { }) + new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) }; public ToolTip(string message) : this("", s_blank, message, "") From 4288565590329400903813f9cfee056cd73e878b Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:51:56 +0800 Subject: [PATCH 171/311] fix nullable warnings --- src/Avalonia.X11/X11TrayIconImpl.cs | 113 +++++++++++----------------- 1 file changed, 45 insertions(+), 68 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 160537cb26..42016ed52a 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -19,20 +19,20 @@ namespace Avalonia.X11 { private static int trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; - private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; - private readonly Connection _connection; + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; + private readonly Connection? _connection; private DbusPixmap _icon; private IStatusNotifierWatcher _statusNotifierWatcher; - private string _sysTrayServiceName; - private string _tooltipText; + private string? _sysTrayServiceName; + private string? _tooltipText; private bool _isActive; private bool _isDisposed; private readonly bool _ctorFinished; - public INativeMenuExporter MenuExporter { get; } - public Action OnClicked { get; set; } + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } public X11TrayIconImpl() { @@ -53,8 +53,8 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if (_connection is null) return; - + if(_connection is null) return; + _statusNotifierWatcher = _connection.CreateProxy("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); @@ -83,8 +83,7 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if (_connection is null) return; - + if(_connection is null) return; _connection.UnregisterObject(_statusNotifierItemDbusObj); await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; @@ -97,9 +96,9 @@ namespace Avalonia.X11 _connection?.Dispose(); } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { - if (_connection is null || _isDisposed) return; + if (_isDisposed) return; if (!(icon is X11IconData x11icon)) return; var w = (int)x11icon.Data[0]; @@ -119,12 +118,12 @@ namespace Avalonia.X11 } _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj.SetIcon(_icon); + _statusNotifierItemDbusObj?.SetIcon(_icon); } public void SetIsVisible(bool visible) { - if (_connection is null || _isDisposed || !_ctorFinished) return; + if (_isDisposed || !_ctorFinished) return; if (visible & !_isActive) { @@ -137,9 +136,9 @@ namespace Avalonia.X11 } } - public void SetToolTipText(string text) + public void SetToolTipText(string? text) { - if (_connection is null || _isDisposed) return; + if (_isDisposed || text is null) return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } @@ -154,15 +153,13 @@ namespace Avalonia.X11 internal class StatusNotifierItemDbusObj : IStatusNotifierItem { private readonly StatusNotifierItemProperties _backingProperties; - private event Action OnPropertyChange; - public event Action OnTitleChanged; - public event Action OnIconChanged; - public event Action OnAttentionIconChanged; - public event Action OnOverlayIconChanged; - public event Action OnTooltipChanged; - public Action SetNativeMenuExporter { get; set; } - public Action NewStatusAsync { get; set; } - public Action ActivationDelegate { get; set; } + public event Action? OnTitleChanged; + public event Action? OnIconChanged; + public event Action? OnAttentionIconChanged; + public event Action? OnOverlayIconChanged; + public event Action? OnTooltipChanged; + public Action? NewStatusAsync { get; set; } + public Action? ActivationDelegate { get; set; } public ObjectPath ObjectPath { get; } public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) @@ -199,37 +196,37 @@ namespace Avalonia.X11 OnTooltipChanged?.Invoke(); } - public Task WatchNewTitleAsync(Action handler, Action onError = null) + public Task WatchNewTitleAsync(Action handler, Action onError) { OnTitleChanged += handler; return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); } - public Task WatchNewIconAsync(Action handler, Action onError = null) + public Task WatchNewIconAsync(Action handler, Action onError) { OnIconChanged += handler; return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); } - public Task WatchNewAttentionIconAsync(Action handler, Action onError = null) + public Task WatchNewAttentionIconAsync(Action handler, Action onError) { OnAttentionIconChanged += handler; return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); } - public Task WatchNewOverlayIconAsync(Action handler, Action onError = null) + public Task WatchNewOverlayIconAsync(Action handler, Action onError) { OnOverlayIconChanged += handler; return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); } - public Task WatchNewToolTipAsync(Action handler, Action onError = null) + public Task WatchNewToolTipAsync(Action handler, Action onError) { OnTooltipChanged += handler; return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); } - public Task WatchNewStatusAsync(Action handler, Action onError = null) + public Task WatchNewStatusAsync(Action handler, Action onError) { NewStatusAsync += handler; return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); @@ -241,11 +238,7 @@ namespace Avalonia.X11 public Task SetAsync(string prop, object val) => Task.CompletedTask; - public Task WatchPropertiesAsync(Action handler) - { - OnPropertyChange += handler; - return Task.FromResult(Disposable.Create(() => OnPropertyChange -= handler)); - } + public Task WatchPropertiesAsync(Action handler) => Task.FromResult(Disposable.Empty); public void SetIcon(DbusPixmap dbusPixmap) { @@ -253,8 +246,10 @@ namespace Avalonia.X11 InvalidateAll(); } - public void SetTitleAndTooltip(string text) + public void SetTitleAndTooltip(string? text) { + if (text is null) return; + _backingProperties.Id = text; _backingProperties.Category = "ApplicationStatus"; _backingProperties.Status = text; @@ -279,12 +274,12 @@ namespace Avalonia.X11 Task ActivateAsync(int x, int y); Task SecondaryActivateAsync(int x, int y); Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError = null); - Task WatchNewIconAsync(Action handler, Action onError = null); - Task WatchNewAttentionIconAsync(Action handler, Action onError = null); - Task WatchNewOverlayIconAsync(Action handler, Action onError = null); - Task WatchNewToolTipAsync(Action handler, Action onError = null); - Task WatchNewStatusAsync(Action handler, Action onError = null); + Task WatchNewTitleAsync(Action handler, Action onError); + Task WatchNewIconAsync(Action handler, Action onError); + Task WatchNewAttentionIconAsync(Action handler, Action onError); + Task WatchNewOverlayIconAsync(Action handler, Action onError); + Task WatchNewToolTipAsync(Action handler, Action onError); + Task WatchNewStatusAsync(Action handler, Action onError); Task GetAsync(string prop); Task GetAllAsync(); Task SetAsync(string prop, object val); @@ -294,36 +289,18 @@ namespace Avalonia.X11 [Dictionary] internal class StatusNotifierItemProperties { - public string Category; - - public string Id; + public string? Category; - public string Title; + public string? Id; - public string Status; + public string? Title; - public int WindowId; - - public string IconThemePath; + public string? Status; public ObjectPath Menu; - - public bool ItemIsMenu; - - public string IconName; - - public DbusPixmap[] IconPixmap; - - public string OverlayIconName; - - public DbusPixmap[] OverlayIconPixmap; - - public string AttentionIconName; - - public DbusPixmap[] AttentionIconPixmap; - - public string AttentionMovieName; - + + public DbusPixmap[]? IconPixmap; + public ToolTip ToolTip; } From 63e616c32ffdd7d5b81642e2f939b5e11b8bf832 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:55:05 +0800 Subject: [PATCH 172/311] zero warnings on X11TrayIconImpl.cs --- src/Avalonia.X11/X11TrayIconImpl.cs | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 42016ed52a..9cba24e9f9 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -17,13 +17,13 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int trayIconInstanceId; + private static int trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; private DbusPixmap _icon; - private IStatusNotifierWatcher _statusNotifierWatcher; + private IStatusNotifierWatcher? _statusNotifierWatcher; private string? _sysTrayServiceName; private string? _tooltipText; @@ -53,10 +53,22 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if(_connection is null) return; - - _statusNotifierWatcher = _connection.CreateProxy("org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); + if (_connection is null) return; + + try + { + _statusNotifierWatcher = _connection.CreateProxy( + "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + } + catch (Exception) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + } + + if (_statusNotifierWatcher is null) return; var pid = Process.GetCurrentProcess().Id; var tid = trayIconInstanceId++; @@ -83,7 +95,7 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if(_connection is null) return; + if (_connection is null) return; _connection.UnregisterObject(_statusNotifierItemDbusObj); await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; @@ -110,7 +122,7 @@ namespace Avalonia.X11 for (var i = 0; i < pixLength; i++) { - var rawPixel = x11icon.Data[i+2].ToUInt32(); + var rawPixel = x11icon.Data[i + 2].ToUInt32(); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); @@ -238,7 +250,8 @@ namespace Avalonia.X11 public Task SetAsync(string prop, object val) => Task.CompletedTask; - public Task WatchPropertiesAsync(Action handler) => Task.FromResult(Disposable.Empty); + public Task WatchPropertiesAsync(Action handler) => + Task.FromResult(Disposable.Empty); public void SetIcon(DbusPixmap dbusPixmap) { @@ -249,7 +262,7 @@ namespace Avalonia.X11 public void SetTitleAndTooltip(string? text) { if (text is null) return; - + _backingProperties.Id = text; _backingProperties.Category = "ApplicationStatus"; _backingProperties.Status = text; @@ -298,9 +311,9 @@ namespace Avalonia.X11 public string? Status; public ObjectPath Menu; - + public DbusPixmap[]? IconPixmap; - + public ToolTip ToolTip; } From 8c4a702a40b579ca0f7b3a4036bf0a5f764b16bb Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:58:20 +0800 Subject: [PATCH 173/311] use less verbose guid generation code --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 932b876088..d5916348be 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -31,7 +31,7 @@ namespace Avalonia.FreeDesktop } public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString().Replace("-", ""); + + Guid.NewGuid().ToString("N"); private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable { From 8183d24200935f7af6c2a7242ee1e229d77bcf14 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 6 Oct 2021 22:12:23 +0800 Subject: [PATCH 174/311] add a comment re: SNIItemProps --- src/Avalonia.X11/X11TrayIconImpl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 9cba24e9f9..fe36e9540e 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -300,6 +300,10 @@ namespace Avalonia.X11 } [Dictionary] + /// This class is used by Tmds.Dbus to ferry properties + /// from the SNI spec. + /// Don't change this to actual C# properties since + /// Tmds.Dbus will get confused. internal class StatusNotifierItemProperties { public string? Category; From daddc71372bc99742e61c292de4c798991bb25ea Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:28:10 +0100 Subject: [PATCH 175/311] remove trayicon events from osx. --- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 4 ++-- native/Avalonia.Native/src/OSX/trayicon.h | 3 +-- native/Avalonia.Native/src/OSX/trayicon.mm | 8 +++----- src/Avalonia.Native/TrayIconImpl.cs | 20 +------------------- src/Avalonia.Native/avn.idl | 9 +-------- 6 files changed, 9 insertions(+), 37 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 5c174eb663..8896fbe88b 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -22,7 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); -extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* events); +extern IAvnTrayIcon* CreateTrayIcon(); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index f179d4f049..eeaaecfdbd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -303,13 +303,13 @@ public: } } - virtual HRESULT CreateTrayIcon (IAvnTrayIconEvents*cb, IAvnTrayIcon** ppv) override + virtual HRESULT CreateTrayIcon (IAvnTrayIcon** ppv) override { START_COM_CALL; @autoreleasepool { - *ppv = ::CreateTrayIcon(cb); + *ppv = ::CreateTrayIcon(); return S_OK; } } diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h index 11ad71756a..f94f9a871b 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.h +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -15,12 +15,11 @@ class AvnTrayIcon : public ComSingleObject { private: NSStatusItem* _native; - ComPtr _events; public: FORWARD_IUNKNOWN() - AvnTrayIcon(IAvnTrayIconEvents* events); + AvnTrayIcon(); ~AvnTrayIcon (); diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm index 79b16f82c6..151990cfb1 100644 --- a/native/Avalonia.Native/src/OSX/trayicon.mm +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -2,18 +2,16 @@ #include "trayicon.h" #include "menu.h" -extern IAvnTrayIcon* CreateTrayIcon(IAvnTrayIconEvents* cb) +extern IAvnTrayIcon* CreateTrayIcon() { @autoreleasepool { - return new AvnTrayIcon(cb); + return new AvnTrayIcon(); } } -AvnTrayIcon::AvnTrayIcon(IAvnTrayIconEvents* events) +AvnTrayIcon::AvnTrayIcon() { - _events = events; - _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; } diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index b8b81214f1..951bbc496e 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -8,31 +8,13 @@ using Avalonia.Platform; namespace Avalonia.Native { - class TrayIconEvents : CallbackBase, IAvnTrayIconEvents - { - private TrayIconImpl _parent; - - public TrayIconEvents (TrayIconImpl parent) - { - _parent = parent; - } - - public void Clicked() - { - } - - public void DoubleClicked() - { - } - } - internal class TrayIconImpl : ITrayIconImpl { private readonly IAvnTrayIcon _native; public TrayIconImpl(IAvaloniaNativeFactory factory) { - _native = factory.CreateTrayIcon(new TrayIconEvents(this)); + _native = factory.CreateTrayIcon(); MenuExporter = new AvaloniaNativeMenuExporter(_native, factory); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index c6fd3850c5..00c54750a4 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -427,7 +427,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); - HRESULT CreateTrayIcon(IAvnTrayIconEvents* cb, IAvnTrayIcon** ppv); + HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -674,13 +674,6 @@ interface IAvnTrayIcon : IUnknown HRESULT SetIsVisible(bool isVisible); } -[uuid(a687a6d9-73aa-4fef-9b4a-61587d7285d3)] -interface IAvnTrayIconEvents : IUnknown -{ - void Clicked (); - void DoubleClicked (); -} - [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] interface IAvnMenu : IUnknown { From ca5d78d507141f5a9b7fdd4c7369ead51dbbb2c0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:32:55 +0100 Subject: [PATCH 176/311] fix warnings and use readonly fields. --- .../AvaloniaNativeMenuExporter.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index dd52bd3544..e89a4bf59e 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -9,15 +9,15 @@ using Avalonia.Threading; namespace Avalonia.Native { - class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter + internal class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { - private IAvaloniaNativeFactory _factory; + private readonly IAvaloniaNativeFactory _factory; private bool _resetQueued = true; - private bool _exported = false; - private IAvnWindow _nativeWindow; + private bool _exported; + private readonly IAvnWindow _nativeWindow; private NativeMenu _menu; private __MicroComIAvnMenuProxy _nativeMenu; - private IAvnTrayIcon _trayIcon; + private readonly IAvnTrayIcon _trayIcon; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -48,7 +48,7 @@ namespace Avalonia.Native public void SetNativeMenu(NativeMenu menu) { - _menu = menu == null ? new NativeMenu() : menu; + _menu = menu ?? new NativeMenu(); DoLayoutReset(true); } @@ -137,7 +137,7 @@ namespace Avalonia.Native var appMenuHolder = menuItem?.Parent; - if (menu.Parent is null) + if (menuItem is null) { menuItem = new NativeMenuItem(); } @@ -155,7 +155,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory); + _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); _nativeMenu.Initialize(this, appMenuHolder, ""); From f0dcaea4046fcb03018d2da2a845e5c994ab2cf1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:38:45 +0100 Subject: [PATCH 177/311] fix warnings and issues. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 36 +++++++++------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index e8fc00fb74..2dbc844ab3 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Styling; -using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -19,20 +16,20 @@ namespace Avalonia.Win32 { public class TrayIconImpl : ITrayIconImpl { - private readonly int _uniqueId = 0; - private static int _nextUniqueId = 0; + private readonly int _uniqueId; + private static int s_nextUniqueId; private bool _iconAdded; private IconImpl? _icon; private string? _tooltipText; private readonly Win32NativeToManagedMenuExporter _exporter; - private static Dictionary s_trayIcons = new Dictionary(); + private static readonly Dictionary s_trayIcons = new Dictionary(); private bool _disposedValue; public TrayIconImpl() { _exporter = new Win32NativeToManagedMenuExporter(); - _uniqueId = ++_nextUniqueId; + _uniqueId = ++s_nextUniqueId; s_trayIcons.Add(_uniqueId, this); } @@ -113,17 +110,12 @@ namespace Avalonia.Win32 case (int)WindowsMessage.WM_RBUTTONUP: OnRightClicked(); break; - - default: - break; } return IntPtr.Zero; } - else - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } + + return DefWindowProc(hWnd, msg, wParam, lParam); } private void OnRightClicked() @@ -150,13 +142,13 @@ namespace Avalonia.Win32 /// /// Custom Win32 window messages for the NotifyIcon /// - enum CustomWindowsMessage : uint + private enum CustomWindowsMessage : uint { WM_TRAYICON = WindowsMessage.WM_APP + 1024, WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 } - class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable + private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable { Type IStyleable.StyleKey => typeof(MenuFlyoutPresenter); @@ -172,9 +164,9 @@ namespace Avalonia.Win32 } } - class TrayPopupRoot : Window + private class TrayPopupRoot : Window { - private ManagedPopupPositioner _positioner; + private readonly ManagedPopupPositioner _positioner; public TrayPopupRoot() { @@ -195,8 +187,8 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { - PlatformImpl.Move(position); - PlatformImpl.Resize(size, PlatformResizeReason.Layout); + PlatformImpl!.Move(position); + PlatformImpl!.Resize(size, PlatformResizeReason.Layout); } protected override void ArrangeCore(Rect finalRect) @@ -217,7 +209,7 @@ namespace Avalonia.Win32 { public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); private readonly MoveResizeDelegate _moveResize; - private Window _hiddenWindow; + private readonly Window _hiddenWindow; public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) { @@ -244,7 +236,7 @@ namespace Avalonia.Win32 _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); } - public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; + public double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; } } From 89f77429097c7c8a01361b34dc6ec56a6c3bc523 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:41:08 +0100 Subject: [PATCH 178/311] more review fixes. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index e89a4bf59e..4431e108ed 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -103,12 +103,9 @@ namespace Avalonia.Native SetMenu(appMenu); } - else + else if (_menu != null) { - if (_menu != null) - { - SetMenu(_trayIcon, _menu); - } + SetMenu(_trayIcon, _menu); } } else From 597239c92a4203bf2e9dd273977645189f10d8eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 15:47:07 +0100 Subject: [PATCH 179/311] more review fixes. --- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 7 +++---- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 10 +++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 2dbc844ab3..23395dd9b5 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -205,13 +205,12 @@ namespace Avalonia.Win32 }); } - class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + private class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup { - public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); - private readonly MoveResizeDelegate _moveResize; + private readonly Action _moveResize; private readonly Window _hiddenWindow; - public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize) + public TrayIconManagedPopupPositionerPopupImplHelper(Action moveResize) { _moveResize = moveResize; _hiddenWindow = new Window(); diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index f8ae128725..7ac6e542ac 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -17,13 +17,11 @@ namespace Avalonia.Win32 private IEnumerable Populate (NativeMenu nativeMenu) { - var items = new List(); - foreach (var menuItem in nativeMenu.Items) { if (menuItem is NativeMenuItemSeparator) { - items.Add(new MenuItem { Header = "-" }); + yield return new MenuItem { Header = "-" }; } else if (menuItem is NativeMenuItem item) { @@ -35,14 +33,12 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (s, e) => bridge.RaiseClicked(); + newItem.Click += (_, __) => bridge.RaiseClicked(); } - items.Add(newItem); + yield return newItem; } } - - return items; } public IEnumerable? GetMenu () From 15829fb1e967fda9a714ea7363cbe744c963f4d4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:10:03 +0100 Subject: [PATCH 180/311] formatting. --- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 7ac6e542ac..f1c5791359 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -15,7 +15,7 @@ namespace Avalonia.Win32 _nativeMenu = nativeMenu; } - private IEnumerable Populate (NativeMenu nativeMenu) + private IEnumerable Populate(NativeMenu nativeMenu) { foreach (var menuItem in nativeMenu.Items) { @@ -41,7 +41,7 @@ namespace Avalonia.Win32 } } - public IEnumerable? GetMenu () + public IEnumerable? GetMenu() { if(_nativeMenu != null) { From 24faaee758480be538568548922e9be6e8e6dc6b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:12:26 +0100 Subject: [PATCH 181/311] fix static variable. --- src/Avalonia.X11/X11TrayIconImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index fe36e9540e..f58e462edf 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int trayIconInstanceId; + private static int s_TrayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; @@ -71,7 +71,7 @@ namespace Avalonia.X11 if (_statusNotifierWatcher is null) return; var pid = Process.GetCurrentProcess().Id; - var tid = trayIconInstanceId++; + var tid = s_TrayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); From ca39b411f54d4e34c8ac27b579994ca5e2e2ad6f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:13:33 +0100 Subject: [PATCH 182/311] use subscribe instead of assign. --- src/Avalonia.X11/X11TrayIconImpl.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index f58e462edf..70b4587b08 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -85,10 +85,7 @@ namespace Avalonia.X11 _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); - _statusNotifierItemDbusObj.ActivationDelegate = () => - { - OnClicked?.Invoke(); - }; + _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; _isActive = true; } From dd3d3944a6554364077193d03c8222f592a78134 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Oct 2021 17:15:11 +0100 Subject: [PATCH 183/311] fix variable name. --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index d5916348be..9e426688d8 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -37,16 +37,17 @@ namespace Avalonia.FreeDesktop { private readonly Connection _dbus; private readonly uint _xid; - private IRegistrar _registar; + private IRegistrar _registrar; private bool _disposed; private uint _revision = 1; private NativeMenu _menu; - private Dictionary _idsToItems = new Dictionary(); - private Dictionary _itemsToIds = new Dictionary(); + private readonly Dictionary _idsToItems = new Dictionary(); + private readonly Dictionary _itemsToIds = new Dictionary(); private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; - private bool AppMenu = true; + private bool _appMenu = true; + public DBusMenuExporterImpl(Connection dbus, IntPtr xid) { _dbus = dbus; @@ -59,7 +60,7 @@ namespace Avalonia.FreeDesktop public DBusMenuExporterImpl(Connection dbus, ObjectPath path) { _dbus = dbus; - AppMenu = false; + _appMenu = false; ObjectPath = path; SetNativeMenu(new NativeMenu()); Init(); @@ -69,14 +70,14 @@ namespace Avalonia.FreeDesktop { try { - if (AppMenu) + if (_appMenu) { await _dbus.RegisterObjectAsync(this); - _registar = DBusHelper.Connection.CreateProxy( + _registrar = DBusHelper.Connection.CreateProxy( "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); if (!_disposed) - await _registar.RegisterWindowAsync(_xid, ObjectPath); + await _registrar.RegisterWindowAsync(_xid, ObjectPath); } else { @@ -102,7 +103,7 @@ namespace Avalonia.FreeDesktop _disposed = true; _dbus.UnregisterObject(this); // Fire and forget - _registar?.UnregisterWindowAsync(_xid); + _registrar?.UnregisterWindowAsync(_xid); } From afd720308727ad08f8f92294e50897d77fb941c5 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:09:52 +0300 Subject: [PATCH 184/311] fix formatting --- .../ViewModels/ApplicationViewModel.cs | 6 ++--- .../Platform/ITrayIconImpl.cs | 3 +-- src/Avalonia.Controls/TrayIcon.cs | 10 ++++---- src/Avalonia.Native/TrayIconImpl.cs | 6 ++--- src/Avalonia.X11/X11TrayIconImpl.cs | 24 ++++++++++++------- .../Win32NativeToManagedMenuExporter.cs | 6 ++--- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs index 6cd44eecaf..7eea7b0657 100644 --- a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs @@ -10,17 +10,17 @@ namespace ControlCatalog.ViewModels { ExitCommand = MiniCommand.Create(() => { - if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) { lifetime.Shutdown(); } }); - + ToggleCommand = MiniCommand.Create(() => { }); } public MiniCommand ExitCommand { get; } - + public MiniCommand ToggleCommand { get; } } } diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs index 12a32ec64b..9768d149f0 100644 --- a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Controls; using Avalonia.Controls.Platform; #nullable enable @@ -21,7 +20,7 @@ namespace Avalonia.Platform /// /// Sets if the tray icon is visible or not. /// - void SetIsVisible (bool visible); + void SetIsVisible(bool visible); /// /// Gets the MenuExporter to allow native menus to be exported to the TrayIcon. diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index ad9a668cf2..6bfddfa877 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -30,22 +30,22 @@ namespace Avalonia.Controls } } - public TrayIcon () : this(PlatformManager.CreateTrayIcon()) + public TrayIcon() : this(PlatformManager.CreateTrayIcon()) { } - static TrayIcon () + static TrayIcon() { IconsProperty.Changed.Subscribe(args => { if (args.Sender is Application) { - if(args.OldValue.Value != null) + if (args.OldValue.Value != null) { RemoveIcons(args.OldValue.Value); } - if(args.NewValue.Value != null) + if (args.NewValue.Value != null) { args.NewValue.Value.CollectionChanged += Icons_CollectionChanged; } @@ -161,7 +161,7 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); - if(change.Property == IconProperty) + if (change.Property == IconProperty) { _impl?.SetIcon(Icon.PlatformImpl); } diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs index 951bbc496e..abcc61d950 100644 --- a/src/Avalonia.Native/TrayIconImpl.cs +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Native internal class TrayIconImpl : ITrayIconImpl { private readonly IAvnTrayIcon _native; - + public TrayIconImpl(IAvaloniaNativeFactory factory) { _native = factory.CreateTrayIcon(); @@ -28,7 +28,7 @@ namespace Avalonia.Native public unsafe void SetIcon(IWindowIconImpl? icon) { - if(icon is null) + if (icon is null) { _native.SetIcon(null, IntPtr.Zero); } @@ -40,7 +40,7 @@ namespace Avalonia.Native var imageData = ms.ToArray(); - fixed(void* ptr = imageData) + fixed (void* ptr = imageData) { _native.SetIcon(ptr, new IntPtr(imageData.Length)); } diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 70b4587b08..3469bd7bcf 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -53,7 +53,8 @@ namespace Avalonia.X11 public async void CreateTrayIcon() { - if (_connection is null) return; + if (_connection is null) + return; try { @@ -68,7 +69,8 @@ namespace Avalonia.X11 "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); } - if (_statusNotifierWatcher is null) return; + if (_statusNotifierWatcher is null) + return; var pid = Process.GetCurrentProcess().Id; var tid = s_TrayIconInstanceId++; @@ -92,7 +94,8 @@ namespace Avalonia.X11 public async void DestroyTrayIcon() { - if (_connection is null) return; + if (_connection is null) + return; _connection.UnregisterObject(_statusNotifierItemDbusObj); await _connection.UnregisterServiceAsync(_sysTrayServiceName); _isActive = false; @@ -107,8 +110,10 @@ namespace Avalonia.X11 public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed) return; - if (!(icon is X11IconData x11icon)) return; + if (_isDisposed) + return; + if (!(icon is X11IconData x11icon)) + return; var w = (int)x11icon.Data[0]; var h = (int)x11icon.Data[1]; @@ -132,7 +137,8 @@ namespace Avalonia.X11 public void SetIsVisible(bool visible) { - if (_isDisposed || !_ctorFinished) return; + if (_isDisposed || !_ctorFinished) + return; if (visible & !_isActive) { @@ -147,7 +153,8 @@ namespace Avalonia.X11 public void SetToolTipText(string? text) { - if (_isDisposed || text is null) return; + if (_isDisposed || text is null) + return; _tooltipText = text; _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); } @@ -258,7 +265,8 @@ namespace Avalonia.X11 public void SetTitleAndTooltip(string? text) { - if (text is null) return; + if (text is null) + return; _backingProperties.Id = text; _backingProperties.Category = "ApplicationStatus"; diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index f1c5791359..fa6f9927b5 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -21,13 +21,13 @@ namespace Avalonia.Win32 { if (menuItem is NativeMenuItemSeparator) { - yield return new MenuItem { Header = "-" }; + yield return new MenuItem { Header = "-" }; } else if (menuItem is NativeMenuItem item) { var newItem = new MenuItem { Header = item.Header, Icon = item.Icon, Command = item.Command, CommandParameter = item.CommandParameter }; - if(item.Menu != null) + if (item.Menu != null) { newItem.Items = Populate(item.Menu); } @@ -43,7 +43,7 @@ namespace Avalonia.Win32 public IEnumerable? GetMenu() { - if(_nativeMenu != null) + if (_nativeMenu != null) { return Populate(_nativeMenu); } From 7b0fbe6d3aef66b117baa1f6628dcf2f23921d83 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:16:19 +0300 Subject: [PATCH 185/311] minor nits --- src/Avalonia.X11/X11TrayIconImpl.cs | 6 +++--- .../Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 3469bd7bcf..ea3653fc83 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_TrayIconInstanceId; + private static int s_trayIconInstanceId; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; @@ -62,7 +62,7 @@ namespace Avalonia.X11 "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); } - catch (Exception) + catch { Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, @@ -73,7 +73,7 @@ namespace Avalonia.X11 return; var pid = Process.GetCurrentProcess().Id; - var tid = s_TrayIconInstanceId++; + var tid = s_trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index fa6f9927b5..69b8e91962 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (_, __) => bridge.RaiseClicked(); + newItem.Click += (_, _) => bridge.RaiseClicked(); } yield return newItem; From 71616ac7f90344fa788271482add7578e10b69f1 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:20:08 +0300 Subject: [PATCH 186/311] nit --- src/Avalonia.Controls/Platform/PlatformManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index 054f823d6d..e39f0b1e99 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Platform { } - public static ITrayIconImpl? CreateTrayIcon () + public static ITrayIconImpl? CreateTrayIcon() { var platform = AvaloniaLocator.Current.GetService(); From 99d983499f5412febf07aafe2bf03872319b412b Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 7 Oct 2021 12:45:28 +0300 Subject: [PATCH 187/311] fix errors related to old sdk --- src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs index 69b8e91962..fa6f9927b5 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -33,7 +33,7 @@ namespace Avalonia.Win32 } else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) { - newItem.Click += (_, _) => bridge.RaiseClicked(); + newItem.Click += (_, __) => bridge.RaiseClicked(); } yield return newItem; From 257edd40ff204499c4971156076f9b40aaf34d49 Mon Sep 17 00:00:00 2001 From: Adir Date: Thu, 7 Oct 2021 14:03:31 +0300 Subject: [PATCH 188/311] Changed CompositionBackdropCornerRadius to be nullable --- src/Windows/Avalonia.Win32/Win32Platform.cs | 5 +++-- .../WinRT/Composition/WinUICompositorConnection.cs | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 9d56306c59..c84ccde653 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -92,9 +92,10 @@ namespace Avalonia /// /// When enabled, create rounded corner blur brushes - /// If set to zero the brushes will be created using default settings (sharp corners) + /// If set to null the brushes will be created using default settings (sharp corners) + /// This can be useful when you need a rounded-corner blurred Windows 10 app, or borderless Windows 11 app /// - public float CompositionBackdropCornerRadius { get; set; } + public float? CompositionBackdropCornerRadius { get; set; } } } diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 21de6f169b..57b0f71306 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -17,7 +17,9 @@ namespace Avalonia.Win32.WinRT.Composition { class WinUICompositorConnection : IRenderTimer { + private readonly float? _backdropCornerRadius; private readonly EglContext _syncContext; + private readonly ICompositionBrush _micaBrush; private ICompositor _compositor; private ICompositor2 _compositor2; private ICompositor5 _compositor5; @@ -27,11 +29,9 @@ namespace Avalonia.Win32.WinRT.Composition private EglPlatformOpenGlInterface _gl; private ICompositorDesktopInterop _compositorDesktopInterop; private ICompositionBrush _blurBrush; - private readonly ICompositionBrush _micaBrush; private object _pumpLock = new object(); - private readonly float _backdropCornerRadius; - public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float backdropCornerRadius) + public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float? backdropCornerRadius) { _gl = gl; _pumpLock = pumpLock; @@ -52,7 +52,7 @@ namespace Avalonia.Win32.WinRT.Composition public EglPlatformOpenGlInterface Egl => _gl; - static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float backdropCornerRadius) + static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float? backdropCornerRadius) { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -135,7 +135,7 @@ namespace Avalonia.Win32.WinRT.Composition } public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle, - float backdropCornerRadius) + float? backdropCornerRadius) { const int majorRequired = 10; const int buildRequired = 17134; @@ -247,10 +247,10 @@ namespace Avalonia.Win32.WinRT.Composition private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals) { - if (_backdropCornerRadius == 0) + if (!_backdropCornerRadius.HasValue) return null; using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry(); - roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius, _backdropCornerRadius)); + roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); using var compositor6 = _compositor.QueryInterface(); using var compositionGeometry = roundedRectangleGeometry From 88d94379fe4384e7d4eff577bbda2dba131a6c44 Mon Sep 17 00:00:00 2001 From: Lighto Date: Thu, 7 Oct 2021 23:24:42 +0300 Subject: [PATCH 189/311] Allow creating custom Datagrid columns (#6689) * Change BindingTarget of DataGridBoundColumn to public, this will allow custom columns * Changed modifier to protected --- src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 90401a00a2..97e247bdc6 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -133,7 +133,7 @@ namespace Avalonia.Controls protected abstract IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem); - internal AvaloniaProperty BindingTarget { get; set; } + protected AvaloniaProperty BindingTarget { get; set; } internal void SetHeaderFromBinding() { From b8059ef35fb0935d20fd8bc3b9c24024b1406e7c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 11 Oct 2021 10:56:48 +0100 Subject: [PATCH 190/311] implement non-client clicks on osx. --- native/Avalonia.Native/src/OSX/window.h | 1 + native/Avalonia.Native/src/OSX/window.mm | 26 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3a54bd4b79..1dc091a48d 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -12,6 +12,7 @@ class WindowBaseImpl; -(AvnPixelSize) getPixelSize; -(AvnPlatformResizeReason) getResizeReason; -(void) setResizeReason:(AvnPlatformResizeReason)reason; ++ (AvnPoint)toAvnPoint:(CGPoint)p; @end @interface AutoFitContentView : NSView diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7a6e7dc72f..d9b42e5ca3 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1541,7 +1541,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return pt; } -- (AvnPoint)toAvnPoint:(CGPoint)p ++ (AvnPoint)toAvnPoint:(CGPoint)p { AvnPoint result; @@ -1598,7 +1598,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; - auto avnPoint = [self toAvnPoint:localPoint]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; AvnVector delta; @@ -1943,7 +1943,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info { auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; - auto avnPoint = [self toAvnPoint:localPoint]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; NSDragOperation nsop = [info draggingSourceOperationMask]; @@ -2376,6 +2376,26 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->PositionChanged(position); } } + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self frame].size.height - pt.Y; + return pt; +} + +- (void)sendEvent:(NSEvent *)event +{ + if(event.type == NSEventTypeLeftMouseDown && _parent != nullptr) + { + auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + } + + [super sendEvent:event]; +} @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup From 00765d53b2eeed4550785b850acf82d71da3479c Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Mon, 11 Oct 2021 20:46:58 +0300 Subject: [PATCH 191/311] Fixed handled event misses in dev tools --- .../Diagnostics/Models/EventChainLink.cs | 2 +- .../Diagnostics/ViewModels/EventTreeNode.cs | 26 +++++++++++++++++++ .../Diagnostics/ViewModels/FiredEvent.cs | 5 ++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs index 4f493bdcc2..d986a11c45 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs @@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.Models } } - public bool Handled { get; } + public bool Handled { get; set; } public RoutingStrategies Route { get; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index 65fd81cc78..a79816390d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels // FIXME: This leaks event handlers. Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true); + Event.RouteFinished.Subscribe(HandleRouteFinished); + _isRegistered = true; } } @@ -92,6 +94,30 @@ namespace Avalonia.Diagnostics.ViewModels else handler(); } + + private void HandleRouteFinished(RoutedEventArgs e) + { + if (!_isRegistered || IsEnabled == false) + return; + if (e.Source is IVisual v && BelongsToDevTool(v)) + return; + + var s = e.Source; + var handled = e.Handled; + var route = e.Route; + + void handler() + { + if (_currentEvent != null && handled) + { + var linkIndex = _currentEvent.EventChain.Count - 1; + var link = _currentEvent.EventChain[linkIndex]; + + link.Handled = true; + _currentEvent.HandledBy = link; + } + } + } private static bool BelongsToDevTool(IVisual v) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs index 32df2f8745..8069300922 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs @@ -63,8 +63,8 @@ namespace Avalonia.Diagnostics.ViewModels { if (EventChain.Count > 0) { - var prevLink = EventChain[EventChain.Count-1]; - + var prevLink = EventChain[EventChain.Count - 1]; + if (prevLink.Route != link.Route) { link.BeginsNewRoute = true; @@ -72,6 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels } EventChain.Add(link); + if (HandledBy == null && link.Handled) HandledBy = link; } From b77978c5586ad40075a17f10c745f7152ff22fe3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 12 Oct 2021 14:45:50 +0200 Subject: [PATCH 192/311] Simulate font weight bold and font style italic when a fallback is used --- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 6 +++++- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 10 +++++++++- src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 2 ++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 178ee8c544..6b560ac739 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -140,7 +140,11 @@ namespace Avalonia.Skia $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); } - return new GlyphTypefaceImpl(skTypeface); + var isFakeBold = (int)typeface.Weight >= 600 && !skTypeface.IsBold; + + var isFakeItalic = typeface.Style == FontStyle.Italic && !skTypeface.IsItalic; + + return new GlyphTypefaceImpl(skTypeface, isFakeBold, isFakeItalic); } } } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index ceccc481f9..9601fece25 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -10,7 +10,7 @@ namespace Avalonia.Skia { private bool _isDisposed; - public GlyphTypefaceImpl(SKTypeface typeface) + public GlyphTypefaceImpl(SKTypeface typeface, bool isFakeBold = false, bool isFakeItalic = false) { Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); @@ -52,6 +52,10 @@ namespace Avalonia.Skia 0; IsFixedPitch = Typeface.IsFixedPitch; + + IsFakeBold = isFakeBold; + + IsFakeItalic = isFakeItalic; } public Face Face { get; } @@ -86,6 +90,10 @@ namespace Avalonia.Skia /// public bool IsFixedPitch { get; } + + public bool IsFakeBold { get; } + + public bool IsFakeItalic { get; } /// public ushort GetGlyph(uint codepoint) diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e2175f1145..4130d38f17 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -217,6 +217,8 @@ namespace Avalonia.Skia s_font.Size = (float)glyphRun.FontRenderingEmSize; s_font.Typeface = typeface; + s_font.Embolden = glyphTypeface.IsFakeBold; + s_font.SkewX = glyphTypeface.IsFakeItalic ? -0.2f : 0; SKTextBlob textBlob; From fc3412383068643e342ea5e4cac4f683e01db68a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 13 Oct 2021 19:24:36 -0400 Subject: [PATCH 193/311] Disable WindowManagerAddShadowHint by default It makes more sense to be disabled by default. --- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index a5cdeefb0e..856bcd1079 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Primitives public class Popup : Control, IVisualTreeHost, IPopupHostProvider { public static readonly StyledProperty WindowManagerAddShadowHintProperty = - AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); /// /// Defines the property. From 74c72f7aee00a6a616dd01b5b719af6b8e8459be Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 14 Oct 2021 18:00:23 +0300 Subject: [PATCH 194/311] [X11] Check for XOpenDisplay error _before_ trying to use display --- src/Avalonia.X11/X11Platform.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 5d80c860a7..d3aeefd088 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -52,11 +52,14 @@ namespace Avalonia.X11 XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); + if (Display == IntPtr.Zero) + throw new Exception("XOpenDisplay failed"); DeferredDisplay = XOpenDisplay(IntPtr.Zero); + if (DeferredDisplay == IntPtr.Zero) + throw new Exception("XOpenDisplay failed"); + OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero); - if (Display == IntPtr.Zero) - throw new Exception("XOpenDisplay failed"); XError.Init(); Info = new X11Info(Display, DeferredDisplay, useXim); From b4738926493db56ab99eb96b361a6b2c55487300 Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 05:29:42 +0200 Subject: [PATCH 195/311] Update DataGridColumn.cs --- .../DataGridColumn.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 4ab2869138..07adac597f 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -653,6 +653,26 @@ namespace Avalonia.Controls return null; } + /// + /// Switches the current state of sort direction + /// + /// Clear the current sort direction instead + public void PerformSort(bool clear) + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(clear ? Input.KeyModifiers.Control : Input.KeyModifiers.None); + } + + /// + /// Changes the sort direction of this column + /// + /// New sort direction + public void PerformSort(ListSortDirection direction) + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction); + } + /// /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value. /// From 68a3f7fa975617fac4134b576b147fc028fede3d Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 05:35:19 +0200 Subject: [PATCH 196/311] Update DataGridColumnHeader.cs --- .../DataGridColumnHeader.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6f957497cb..85fd55800a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -201,21 +201,21 @@ namespace Avalonia.Controls handled = true; } - internal void InvokeProcessSort(KeyModifiers keyModifiers) + internal void InvokeProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null) { Debug.Assert(OwningGrid != null); - if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers))) + if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers, forcedDirection))) { return; } if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) { - Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers)); + Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers, forcedDirection)); } } //TODO GroupSorting - internal void ProcessSort(KeyModifiers keyModifiers) + internal void ProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null) { // if we can sort: // - AllowUserToSortColumns and CanSort are true, and @@ -259,7 +259,14 @@ namespace Avalonia.Controls { if (sort != null) { - newSort = sort.SwitchSortDirection(); + if (forcedDirection == null || sort.Direction != forcedDirection) + { + newSort = sort.SwitchSortDirection(); + } + else + { + newSort = sort; + } // changing direction should not affect sort order, so we replace this column's // sort description instead of just adding it to the end of the collection @@ -276,7 +283,10 @@ namespace Avalonia.Controls } else if (OwningColumn.CustomSortComparer != null) { - newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer); + newSort = forcedDirection != null ? + DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer, forcedDirection.Value) : + DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer); + owningGrid.DataConnection.SortDescriptions.Add(newSort); } @@ -290,6 +300,10 @@ namespace Avalonia.Controls } newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); + if (forcedDirection != null && newSort.Direction != forcedDirection) + { + newSort = newSort.SwitchSortDirection(); + } owningGrid.DataConnection.SortDescriptions.Add(newSort); } From d138924a95527bc55908a3b7374e9ec1517797d9 Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 05:35:55 +0200 Subject: [PATCH 197/311] Update DataGridColumn.cs --- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 07adac597f..7ad06c194d 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -657,10 +657,10 @@ namespace Avalonia.Controls /// Switches the current state of sort direction /// /// Clear the current sort direction instead - public void PerformSort(bool clear) + public void PerformSort(bool? clear) { //InvokeProcessSort is already validating if sorting is possible - _headerCell?.InvokeProcessSort(clear ? Input.KeyModifiers.Control : Input.KeyModifiers.None); + _headerCell?.InvokeProcessSort(clear == true ? Input.KeyModifiers.Control : Input.KeyModifiers.None); } /// From e564a01663c1d538329f2052c1718063d696fc01 Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 06:36:07 +0200 Subject: [PATCH 198/311] Update DataGridColumn.cs --- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 7ad06c194d..f275d7cc94 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -653,21 +653,29 @@ namespace Avalonia.Controls return null; } + /// + /// Clears the current sort direction + /// + public void ClearSort() + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.Control); + } + /// /// Switches the current state of sort direction /// - /// Clear the current sort direction instead - public void PerformSort(bool? clear) + public void Sort() { //InvokeProcessSort is already validating if sorting is possible - _headerCell?.InvokeProcessSort(clear == true ? Input.KeyModifiers.Control : Input.KeyModifiers.None); + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None); } /// /// Changes the sort direction of this column /// /// New sort direction - public void PerformSort(ListSortDirection direction) + public void Sort(ListSortDirection direction) { //InvokeProcessSort is already validating if sorting is possible _headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction); From f6406cecf0d4f39d829f2fb192eff03ade26b50f Mon Sep 17 00:00:00 2001 From: 0x90d <46010672+0x90d@users.noreply.github.com> Date: Fri, 15 Oct 2021 06:47:09 +0200 Subject: [PATCH 199/311] Update DataGridColumn.cs --- src/Avalonia.Controls.DataGrid/DataGridColumn.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index f275d7cc94..5499257171 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -9,6 +9,7 @@ using Avalonia.VisualTree; using Avalonia.Collections; using Avalonia.Utilities; using System; +using System.ComponentModel; using System.Linq; using System.Diagnostics; using Avalonia.Controls.Utils; From 4e65b0296b9113e56d54b63577e6c56ce28860e6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Oct 2021 09:09:24 +0200 Subject: [PATCH 200/311] Added failing tests for #6729. --- .../ItemsRepeaterTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs new file mode 100644 index 0000000000..321676abc0 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs @@ -0,0 +1,24 @@ +using System.Collections.ObjectModel; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ItemsRepeaterTests + { + [Fact] + public void Can_Reassign_Items() + { + var target = new ItemsRepeater(); + target.Items = new ObservableCollection(); + target.Items = new ObservableCollection(); + } + + [Fact] + public void Can_Reassign_Items_To_Null() + { + var target = new ItemsRepeater(); + target.Items = new ObservableCollection(); + target.Items = null; + } + } +} From e2a8b56aad64a5da1962331648c75871eb111b96 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Oct 2021 09:18:12 +0200 Subject: [PATCH 201/311] Unsubscribe from ItemsSourceView before disposing it. --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 01200e87e3..0ff8fcbd28 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -588,14 +588,14 @@ namespace Avalonia.Controls throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); } - ItemsSourceView?.Dispose(); - ItemsSourceView = newValue; - if (oldValue != null) { oldValue.CollectionChanged -= OnItemsSourceViewChanged; } + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + if (newValue != null) { newValue.CollectionChanged += OnItemsSourceViewChanged; From 8322d4f428ee0b682f8663ac6157865b0a51e93e Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 15 Oct 2021 12:13:58 +0300 Subject: [PATCH 202/311] fix --- src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs index ea5dcdeeba..526be6e0f0 100644 --- a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs +++ b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs @@ -7,7 +7,7 @@ Mica } - public interface IBlurHost + internal interface IBlurHost { void SetBlur(BlurEffect enable); } From 2d14a049d7089d2cff8f3b003ca3b5bc6e4446e3 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 15 Oct 2021 13:05:07 +0300 Subject: [PATCH 203/311] add ctor to DrawingImage which accepts Drawing --- src/Avalonia.Visuals/Media/DrawingImage.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs index 56c883014a..6fa8d397a5 100644 --- a/src/Avalonia.Visuals/Media/DrawingImage.cs +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -11,6 +11,14 @@ namespace Avalonia.Media /// public class DrawingImage : AvaloniaObject, IImage, IAffectsRender { + public DrawingImage() + { + } + + public DrawingImage(Drawing drawing) + { + Drawing = drawing; + } /// /// Defines the property. /// From e775ce4b0d9456bbe060e82e5d0a14e5ad484955 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Oct 2021 16:06:33 +0100 Subject: [PATCH 204/311] add missing START_COM_CALL --- 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 d9b42e5ca3..558cd1cc57 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -231,6 +231,8 @@ public: virtual HRESULT GetFrameSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) From e44e0308ff04936249b183db80a87b40295d054c Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 15 Oct 2021 17:12:46 +0200 Subject: [PATCH 205/311] fix: XML Comment --- src/Avalonia.X11/X11TrayIconImpl.cs | 8 ++++---- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index ea3653fc83..371ff75408 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -305,10 +305,10 @@ namespace Avalonia.X11 } [Dictionary] - /// This class is used by Tmds.Dbus to ferry properties - /// from the SNI spec. - /// Don't change this to actual C# properties since - /// Tmds.Dbus will get confused. + // This class is used by Tmds.Dbus to ferry properties + // from the SNI spec. + // Don't change this to actual C# properties since + // Tmds.Dbus will get confused. internal class StatusNotifierItemProperties { public string? Category; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d812f6a059..7026e6d9ce 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -591,7 +591,7 @@ namespace Avalonia.Skia /// Configure paint wrapper for using gradient brush. /// /// Paint wrapper. - /// Target bound rect. + /// Target size. /// Gradient brush. private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) { From a3dc8ca85badb8ea9f38f9a4abe764d11d632ad5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Oct 2021 17:03:57 +0100 Subject: [PATCH 206/311] osx - handle restoring arrow cursor if app set non-standard cursor at edge of client area. --- native/Avalonia.Native/src/OSX/window.mm | 34 +++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 558cd1cc57..bd93de0e78 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -2387,13 +2387,35 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)sendEvent:(NSEvent *)event { - if(event.type == NSEventTypeLeftMouseDown && _parent != nullptr) + if(_parent != nullptr) { - auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; - auto point = [self translateLocalPoint:avnPoint]; - AvnVector delta; - - _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + switch(event.type) + { + case NSEventTypeLeftMouseDown: + { + auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + } + break; + + case NSEventTypeMouseEntered: + { + _parent->UpdateCursor(); + } + break; + + case NSEventTypeMouseExited: + { + [[NSCursor arrowCursor] set]; + } + break; + + default: + break; + } } [super sendEvent:event]; From b6e60b932576ba826feaa52e49b980c6f613265a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Fri, 15 Oct 2021 19:37:22 +0100 Subject: [PATCH 207/311] Changed X11 default cursor to XC_left_ptr. --- src/Avalonia.X11/X11CursorFactory.cs | 4 ++-- src/Avalonia.X11/X11Info.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index d677ababef..9c37de86bf 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -23,7 +23,7 @@ namespace Avalonia.X11 private static readonly Dictionary s_mapping = new Dictionary { - {StandardCursorType.Arrow, CursorFontShape.XC_top_left_arrow}, + {StandardCursorType.Arrow, CursorFontShape.XC_left_ptr}, {StandardCursorType.Cross, CursorFontShape.XC_cross}, {StandardCursorType.Hand, CursorFontShape.XC_hand2}, {StandardCursorType.Help, CursorFontShape.XC_question_arrow}, @@ -67,7 +67,7 @@ namespace Avalonia.X11 { handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] - : _cursors[CursorFontShape.XC_top_left_arrow]; + : _cursors[CursorFontShape.XC_left_ptr]; } return new CursorImpl(handle); } diff --git a/src/Avalonia.X11/X11Info.cs b/src/Avalonia.X11/X11Info.cs index 3bded4cce1..9920907601 100644 --- a/src/Avalonia.X11/X11Info.cs +++ b/src/Avalonia.X11/X11Info.cs @@ -42,7 +42,7 @@ namespace Avalonia.X11 DefaultScreen = XDefaultScreen(display); BlackPixel = XBlackPixel(display, DefaultScreen); RootWindow = XRootWindow(display, DefaultScreen); - DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow); + DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_left_ptr); DefaultRootWindow = XDefaultRootWindow(display); Atoms = new X11Atoms(display); From fd34b52bba48e87c23c869c061964a8595584239 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 00:06:15 +0300 Subject: [PATCH 208/311] LineNode hit test --- .../Rendering/SceneGraph/LineNode.cs | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 54a9ff733d..3d5de96bf6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -82,8 +83,32 @@ namespace Avalonia.Rendering.SceneGraph public override bool HitTest(Point p) { - // TODO: Implement line hit testing. - return false; + var a = P1; + var b = P2; + + //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. + //The distance from a point to a straight line is defined as the + //length of the vector formed by the point and the closest point of the segment + + Vector ap = p - a; + var dot1 = Vector.Dot(b - a, ap); + + if (dot1 < 0) + return ap.Length <= Pen.Thickness/2; + + Vector bp = p - b; + var dot2 = Vector.Dot(a - b, bp); + + if(dot2 < 0) + return bp.Length <= Pen.Thickness/2; + + var bXaX = b.X - a.X; + var bYaY = b.Y - a.Y; + + var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / + (Math.Sqrt(Math.Pow(bXaX, 2) + Math.Pow(bYaY, 2))); + + return Math.Abs(distance) <= Pen.Thickness/2; } } } From ee84eae5cfcebda0f8559e1ed41aacf9cc5c468c Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 00:15:50 +0300 Subject: [PATCH 209/311] Spaces --- .../Rendering/SceneGraph/LineNode.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 3d5de96bf6..5d7df770aa 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -85,30 +85,30 @@ namespace Avalonia.Rendering.SceneGraph { var a = P1; var b = P2; - + //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. //The distance from a point to a straight line is defined as the //length of the vector formed by the point and the closest point of the segment - + Vector ap = p - a; var dot1 = Vector.Dot(b - a, ap); - + if (dot1 < 0) - return ap.Length <= Pen.Thickness/2; + return ap.Length <= Pen.Thickness / 2; Vector bp = p - b; var dot2 = Vector.Dot(a - b, bp); - - if(dot2 < 0) - return bp.Length <= Pen.Thickness/2; + + if (dot2 < 0) + return bp.Length <= Pen.Thickness / 2; var bXaX = b.X - a.X; var bYaY = b.Y - a.Y; - + var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / (Math.Sqrt(Math.Pow(bXaX, 2) + Math.Pow(bYaY, 2))); - - return Math.Abs(distance) <= Pen.Thickness/2; + + return Math.Abs(distance) <= Pen.Thickness / 2; } } } From 22ff7af2ae1156ed32ebc981c232111feea88d5c Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 03:37:13 +0300 Subject: [PATCH 210/311] replace Math.Pow(x, 2) on x*x --- src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 5d7df770aa..19b751f66c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -106,7 +106,7 @@ namespace Avalonia.Rendering.SceneGraph var bYaY = b.Y - a.Y; var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / - (Math.Sqrt(Math.Pow(bXaX, 2) + Math.Pow(bYaY, 2))); + (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); return Math.Abs(distance) <= Pen.Thickness / 2; } From 721e088911c03490be9b54550521cbbe43d268b1 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 03:37:34 +0300 Subject: [PATCH 211/311] LineNode tests --- .../Rendering/SceneGraph/LineNodeTests.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs new file mode 100644 index 0000000000..f065e7def0 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class LineNodeTests + { + [Fact] + public void HitTest_Should_Be_True() + { + var lineNode = new LineNode( + Matrix.Identity, + new Pen(Brushes.Black, 3), + new Point(15, 15), + new Point(150, 150)); + + + List pointsInside = new() + { + new Point(14, 14), + new Point(15, 15), + new Point(32.1, 30), + new Point(30, 32.1), + new Point(150, 150), + new Point(151, 151), + }; + + foreach (var point in pointsInside) + { + Assert.True(lineNode.HitTest(point)); + } + } + + [Fact] + public void HitTest_Should_Be_False() + { + var lineNode = new LineNode( + Matrix.Identity, + new Pen(Brushes.Black, 3), + new Point(15, 15), + new Point(150, 150)); + + + List pointsOutside= new() + { + new Point(13.9, 13.9), + new Point(30, 32.2), + new Point(32.2, 30), + new Point(151.1, 151.1), + new Point(200, 200), + }; + + foreach (var point in pointsOutside) + { + Assert.False(lineNode.HitTest(point)); + } + } + } +} From ce21eee1554d7e31afff4c09e699af05a5fad577 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 03:45:25 +0300 Subject: [PATCH 212/311] replace target-typed object creation --- .../Rendering/SceneGraph/LineNodeTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs index f065e7def0..d4d4a29dfd 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -17,7 +17,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph new Point(150, 150)); - List pointsInside = new() + var pointsInside = new List() { new Point(14, 14), new Point(15, 15), @@ -43,7 +43,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph new Point(150, 150)); - List pointsOutside= new() + var pointsOutside= new List() { new Point(13.9, 13.9), new Point(30, 32.2), From 339ddca0271247c5e4edb46605f81fa5615d0bbd Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 13:04:41 +0300 Subject: [PATCH 213/311] Sharper angle of a LineNode in Tests --- .../Rendering/SceneGraph/LineNodeTests.cs | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs index d4d4a29dfd..2713e4460e 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -13,18 +13,17 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var lineNode = new LineNode( Matrix.Identity, new Pen(Brushes.Black, 3), - new Point(15, 15), - new Point(150, 150)); - - + new Point(15, 10), + new Point(150, 73)); + var pointsInside = new List() { - new Point(14, 14), - new Point(15, 15), - new Point(32.1, 30), - new Point(30, 32.1), - new Point(150, 150), - new Point(151, 151), + new Point(14, 8.9), + new Point(15, 10), + new Point(30, 15.5), + new Point(30, 18.5), + new Point(150, 73), + new Point(151, 71.9), }; foreach (var point in pointsInside) @@ -39,17 +38,17 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var lineNode = new LineNode( Matrix.Identity, new Pen(Brushes.Black, 3), - new Point(15, 15), - new Point(150, 150)); - + new Point(15, 10), + new Point(150, 73)); - var pointsOutside= new List() + var pointsOutside = new List() { - new Point(13.9, 13.9), - new Point(30, 32.2), - new Point(32.2, 30), - new Point(151.1, 151.1), - new Point(200, 200), + new Point(14, 8), + new Point(14, 8.8), + new Point(30, 15.3), + new Point(30, 18.7), + new Point(151, 71.8), + new Point(155, 75), }; foreach (var point in pointsOutside) From 0a0bba8de4489a343c25274155e1d23c05b7cd41 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 13:06:49 +0300 Subject: [PATCH 214/311] Check transform invert and bounds test for LineNode hitTest --- .../Rendering/SceneGraph/LineNode.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 19b751f66c..3dc6d5f50e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -83,6 +83,20 @@ namespace Avalonia.Rendering.SceneGraph public override bool HitTest(Point p) { + if (!Transform.HasInverse) + return false; + + p *= Transform.Invert(); + + var halfThickness = Pen.Thickness / 2; + var minX = Math.Min(P1.X, P2.X) - halfThickness; + var maxX = Math.Max(P1.X, P2.X) + halfThickness; + var minY = Math.Min(P1.Y, P2.Y) - halfThickness; + var maxY = Math.Max(P1.Y, P2.Y) + halfThickness; + + if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) + return false; + var a = P1; var b = P2; @@ -100,7 +114,7 @@ namespace Avalonia.Rendering.SceneGraph var dot2 = Vector.Dot(a - b, bp); if (dot2 < 0) - return bp.Length <= Pen.Thickness / 2; + return bp.Length <= halfThickness; var bXaX = b.X - a.X; var bYaY = b.Y - a.Y; @@ -108,7 +122,7 @@ namespace Avalonia.Rendering.SceneGraph var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); - return Math.Abs(distance) <= Pen.Thickness / 2; + return Math.Abs(distance) <= halfThickness; } } } From 5407e6293f0a2e9f1bbf1859966437fa86c5d1d5 Mon Sep 17 00:00:00 2001 From: GMIKE Date: Sat, 16 Oct 2021 13:07:06 +0300 Subject: [PATCH 215/311] spaces --- .../Rendering/SceneGraph/LineNodeTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs index 2713e4460e..56f2b03932 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -11,11 +11,11 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void HitTest_Should_Be_True() { var lineNode = new LineNode( - Matrix.Identity, + Matrix.Identity, new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); - + var pointsInside = new List() { new Point(14, 8.9), @@ -28,15 +28,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph foreach (var point in pointsInside) { - Assert.True(lineNode.HitTest(point)); + Assert.True(lineNode.HitTest(point)); } } - + [Fact] public void HitTest_Should_Be_False() { var lineNode = new LineNode( - Matrix.Identity, + Matrix.Identity, new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); @@ -53,7 +53,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph foreach (var point in pointsOutside) { - Assert.False(lineNode.HitTest(point)); + Assert.False(lineNode.HitTest(point)); } } } From 1a8ec1ce0ef9c7a9d03f4b84c9511bbcf6261400 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Oct 2021 22:54:09 -0400 Subject: [PATCH 216/311] Fix DataGrid headers horizontal scrolling --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index fea02dabf4..10c7c16488 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2223,6 +2223,7 @@ namespace Avalonia.Controls if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0) { var handled = false; + var ignoreInvalidate = false; var scrollHeight = 0d; // Vertical scroll handling @@ -2252,8 +2253,7 @@ namespace Avalonia.Controls // Horizontal scroll handling if (delta.X != 0) { - var originalHorizontalOffset = HorizontalOffset; - var horizontalOffset = originalHorizontalOffset - delta.X; + var horizontalOffset = HorizontalOffset - delta.X; var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); if (horizontalOffset < 0) @@ -2265,16 +2265,20 @@ namespace Avalonia.Controls horizontalOffset = widthNotVisible; } - if (horizontalOffset != originalHorizontalOffset) + if (UpdateHorizontalOffset(horizontalOffset)) { - HorizontalOffset = horizontalOffset; + // We don't need to invalidate once again after UpdateHorizontalOffset. + ignoreInvalidate = true; handled = true; } } if (handled) { - InvalidateRowsMeasure(invalidateIndividualElements: false); + if (!ignoreInvalidate) + { + InvalidateRowsMeasure(invalidateIndividualElements: false); + } return true; } } @@ -2932,7 +2936,7 @@ namespace Avalonia.Controls return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true); } - internal void UpdateHorizontalOffset(double newValue) + internal bool UpdateHorizontalOffset(double newValue) { if (HorizontalOffset != newValue) { @@ -2940,7 +2944,9 @@ namespace Avalonia.Controls InvalidateColumnHeadersMeasure(); InvalidateRowsMeasure(true); + return true; } + return false; } internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView) From 9f5f0aa84d082add0ebca3babcd2732c1c83112b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Oct 2021 23:35:27 -0400 Subject: [PATCH 217/311] Do not inherit text properties in Popup --- samples/ControlCatalog/Pages/TextBoxPage.xaml | 10 +++++++++- src/Avalonia.Themes.Default/OverlayPopupHost.xaml | 9 ++++++++- src/Avalonia.Themes.Default/PopupRoot.xaml | 9 ++++++++- .../Controls/OverlayPopupHost.xaml | 5 +++++ src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml | 5 +++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index f631c40eb1..233b309caf 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -11,7 +11,15 @@ HorizontalAlignment="Center" Spacing="16"> - + + + + Custom context flyout + + + + + ListBox Hosts a collection of ListBoxItem. + Each 2nd item is highlighted Multiple diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 55645d4dbb..9c86aeb0c8 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,6 +13,7 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -21,7 +22,7 @@ namespace Avalonia.Controls /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener + public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider { /// /// The default value for the property. @@ -506,5 +507,21 @@ namespace Avalonia.Controls return null; } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + if (Presenter is IChildIndexProvider innerProvider) + { + return innerProvider.GetChildIndex(child); + } + + if (child is IControl control) + { + var index = ItemContainerGenerator.IndexFromContainer(control); + return (index, ItemCount); + } + + return (-1, ItemCount); + } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index b7eeb065da..7a3e93ffc2 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -2,8 +2,12 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; + +using Avalonia.Controls.Presenters; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -14,7 +18,7 @@ namespace Avalonia.Controls /// Controls can be added to a by adding them to its /// collection. All children are layed out to fill the panel. /// - public class Panel : Control, IPanel + public class Panel : Control, IPanel, IChildIndexProvider { /// /// Defines the property. @@ -160,5 +164,16 @@ namespace Avalonia.Controls var panel = control?.VisualParent as TPanel; panel?.InvalidateMeasure(); } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + if (child is IControl control) + { + var index = Children.IndexOf(control); + return (index, Children.Count); + } + + return (-1, Children.Count); + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 52f173fc71..cf5fb8ac42 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -5,6 +5,7 @@ using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.LogicalTree; using Avalonia.Styling; namespace Avalonia.Controls.Presenters @@ -12,7 +13,7 @@ namespace Avalonia.Controls.Presenters /// /// Base class for controls that present items inside an . /// - public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl + public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider { /// /// Defines the property. @@ -248,5 +249,27 @@ namespace Avalonia.Controls.Presenters { (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } + + (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + { + int? totalCount = null; + if (Items.TryGetCountFast(out var count)) + { + totalCount = count; + } + + if (child is IControl control) + { + + if (ItemContainerGenerator is { } generator) + { + var index = ItemContainerGenerator.IndexFromContainer(control); + + return (index, totalCount); + } + } + + return (-1, totalCount); + } } } diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9614d079d9..fa5a09e245 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -12,23 +12,36 @@ namespace Avalonia.Controls.Utils return items.IndexOf(item) != -1; } - public static int Count(this IEnumerable items) + public static bool TryGetCountFast(this IEnumerable items, out int count) { if (items != null) { if (items is ICollection collection) { - return collection.Count; + count = collection.Count; + return true; } else if (items is IReadOnlyCollection readOnly) { - return readOnly.Count; - } - else - { - return Enumerable.Count(items.Cast()); + count = readOnly.Count; + return true; } } + + count = 0; + return false; + } + + public static int Count(this IEnumerable items) + { + if (TryGetCountFast(items, out var count)) + { + return count; + } + else if (items != null) + { + return Enumerable.Count(items.Cast()); + } else { return 0; diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs new file mode 100644 index 0000000000..a6b91dea5f --- /dev/null +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -0,0 +1,134 @@ +#nullable enable +using System; +using System.Text; + +using Avalonia.LogicalTree; + +namespace Avalonia.Styling +{ + public interface IChildIndexProvider + { + (int Index, int? TotalCount) GetChildIndex(ILogical child); + } + + public class NthLastChildSelector : NthChildSelector + { + public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) + { + } + } + + public class NthChildSelector : Selector + { + private const string NthChildSelectorName = "nth-child"; + private const string NthLastChildSelectorName = "nth-last-child"; + private readonly Selector? _previous; + private readonly bool _reversed; + + internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed) + { + _previous = previous; + Step = step; + Offset = offset; + _reversed = reversed; + } + + public NthChildSelector(Selector? previous, int step, int offset) + : this(previous, step, offset, false) + { + + } + + public override bool InTemplate => _previous?.InTemplate ?? false; + + public override bool IsCombinator => false; + + public override Type? TargetType => _previous?.TargetType; + + public int Step { get; } + public int Offset { get; } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + var logical = (ILogical)control; + var controlParent = logical.LogicalParent; + + if (controlParent is IChildIndexProvider childIndexProvider) + { + var (index, totalCount) = childIndexProvider.GetChildIndex(logical); + if (index < 0) + { + return SelectorMatch.NeverThisInstance; + } + + if (_reversed) + { + if (totalCount is int totalCountValue) + { + index = totalCountValue - index; + } + else + { + return SelectorMatch.NeverThisInstance; + } + } + else + { + // nth child index is 1-based + index += 1; + } + + + var n = Math.Sign(Step); + + var diff = index - Offset; + var match = diff == 0 || (Math.Sign(diff) == n && diff % Step == 0); + + return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; + } + else + { + return SelectorMatch.NeverThisInstance; + } + + } + + protected override Selector? MovePrevious() => _previous; + + public override string ToString() + { + var expectedCapacity = NthLastChildSelectorName.Length + 8; + var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity); + + stringBuilder.Append(':'); + stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName); + stringBuilder.Append('('); + + var hasStep = false; + if (Step != 0) + { + hasStep = true; + stringBuilder.Append(Step); + stringBuilder.Append('n'); + } + + if (Offset > 0) + { + if (hasStep) + { + stringBuilder.Append('+'); + } + stringBuilder.Append(Offset); + } + else if (Offset < 0) + { + stringBuilder.Append('-'); + stringBuilder.Append(-Offset); + } + + stringBuilder.Append(')'); + + return stringBuilder.ToString(); + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 762ed7b58c..0bccccbd7c 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -123,6 +123,16 @@ namespace Avalonia.Styling return new NotSelector(previous, argument); } + public static Selector NthChild(this Selector previous, int step, int offset) + { + return new NthChildSelector(previous, step, offset); + } + + public static Selector NthLastChild(this Selector previous, int step, int offset) + { + return new NthLastChildSelector(previous, step, offset); + } + /// /// Returns a selector which matches a type. /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index b81d25d613..dfabd66d17 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -97,6 +97,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers case SelectorGrammar.NotSyntax not: result = new XamlIlNotSelector(result, Create(not.Argument, typeResolver)); break; + case SelectorGrammar.NthChildSyntax nth: + result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthChild); + break; + case SelectorGrammar.NthLastChildSyntax nth: + result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthLastChild); + break; case SelectorGrammar.CommaSyntax comma: if (results == null) results = new XamlIlOrSelectorNode(node, selectorType); @@ -273,6 +279,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } + class XamlIlNthChildSelector : XamlIlSelectorNode + { + private readonly int _step; + private readonly int _offset; + private readonly SelectorType _type; + + public enum SelectorType + { + NthChild, + NthLastChild + } + + public XamlIlNthChildSelector(XamlIlSelectorNode previous, int step, int offset, SelectorType type) : base(previous) + { + _step = step; + _offset = offset; + _type = type; + } + + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldc_I4(_step); + codeGen.Ldc_I4(_offset); + EmitCall(context, codeGen, + m => m.Name == _type.ToString() && m.Parameters.Count == 3); + } + } + class XamlIlPropertyEqualsSelector : XamlIlSelectorNode { public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 9d03341f92..56e64329b7 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -160,11 +160,13 @@ namespace Avalonia.Markup.Parsers if (identifier.IsEmpty) { - throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'."); + throw new ExpressionParseException(r.Position, "Expected class name, is, nth-child or nth-last-child selector after ':'."); } const string IsKeyword = "is"; const string NotKeyword = "not"; + const string NthChildKeyword = "nth-child"; + const string NthLastChildKeyword = "nth-last-child"; if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { @@ -181,6 +183,20 @@ namespace Avalonia.Markup.Parsers var syntax = new NotSyntax { Argument = argument }; return (State.Middle, syntax); } + if (identifier.SequenceEqual(NthChildKeyword.AsSpan()) && r.TakeIf('(')) + { + var (step, offset) = ParseNthChildArguments(ref r); + + var syntax = new NthChildSyntax { Step = step, Offset = offset }; + return (State.Middle, syntax); + } + if (identifier.SequenceEqual(NthLastChildKeyword.AsSpan()) && r.TakeIf('(')) + { + var (step, offset) = ParseNthChildArguments(ref r); + + var syntax = new NthLastChildSyntax { Step = step, Offset = offset }; + return (State.Middle, syntax); + } else { return ( @@ -191,7 +207,6 @@ namespace Avalonia.Markup.Parsers }); } } - private static (State, ISyntax?) ParseTraversal(ref CharacterReader r) { r.SkipWhitespace(); @@ -302,6 +317,70 @@ namespace Avalonia.Markup.Parsers return syntax; } + private static (int step, int offset) ParseNthChildArguments(ref CharacterReader r) + { + int step = 0; + int offset = 0; + + if (r.Peek == 'o') + { + var constArg = r.TakeUntil(')').ToString().Trim(); + if (constArg.Equals("odd", StringComparison.Ordinal)) + { + step = 2; + offset = 1; + } + else + { + throw new ExpressionParseException(r.Position, $"Expected nth-child(odd). Actual '{constArg}'."); + } + } + else if (r.Peek == 'e') + { + var constArg = r.TakeUntil(')').ToString().Trim(); + if (constArg.Equals("even", StringComparison.Ordinal)) + { + step = 2; + offset = 0; + } + else + { + throw new ExpressionParseException(r.Position, $"Expected nth-child(even). Actual '{constArg}'."); + } + } + else + { + var stepOrOffsetSpan = r.TakeWhile(c => c != ')' && c != 'n'); + if (!int.TryParse(stepOrOffsetSpan.ToString().Trim(), out var stepOrOffset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected."); + } + + if (r.Peek == ')') + { + step = 0; + offset = stepOrOffset; + } + else + { + step = stepOrOffset; + + r.Skip(1); // skip 'n' + var offsetSpan = r.TakeUntil(')').TrimStart(); + + if (offsetSpan.Length != 0 + && !int.TryParse(offsetSpan.ToString().Trim(), out offset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + } + } + } + + Expect(ref r, ')'); + + return (step, offset); + } + private static void Expect(ref CharacterReader r, char c) { if (r.End) @@ -419,6 +498,28 @@ namespace Avalonia.Markup.Parsers } } + public class NthChildSyntax : ISyntax + { + public int Offset { get; set; } + public int Step { get; set; } + + public override bool Equals(object? obj) + { + return (obj is NthChildSyntax nth) && nth.Offset == Offset && nth.Step == Step; + } + } + + public class NthLastChildSyntax : ISyntax + { + public int Offset { get; set; } + public int Step { get; set; } + + public override bool Equals(object? obj) + { + return (obj is NthLastChildSyntax nth) && nth.Offset == Offset && nth.Step == Step; + } + } + public class CommaSyntax : ISyntax { public override bool Equals(object? obj) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index 92ba744ee1..11fb287d46 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -104,6 +104,12 @@ namespace Avalonia.Markup.Parsers case SelectorGrammar.NotSyntax not: result = result.Not(x => Create(not.Argument)); break; + case SelectorGrammar.NthChildSyntax nth: + result = result.NthChild(nth.Step, nth.Offset); + break; + case SelectorGrammar.NthLastChildSyntax nth: + result = result.NthLastChild(nth.Step, nth.Offset); + break; case SelectorGrammar.CommaSyntax comma: if (results == null) { diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 03f1120796..543d44c492 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -236,6 +236,96 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Fact] + public void OfType_NthChild() + { + var result = SelectorGrammar.Parse("Button:nth-child(2n+1)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = 2, + Offset = 1 + } + }, + result); + } + + [Fact] + public void OfType_NthChild_Without_Offset() + { + var result = SelectorGrammar.Parse("Button:nth-child(2147483647n)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = int.MaxValue, + Offset = 0 + } + }, + result); + } + + [Fact] + public void OfType_NthLastChild() + { + var result = SelectorGrammar.Parse("Button:nth-last-child(2n+1)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthLastChildSyntax() + { + Step = 2, + Offset = 1 + } + }, + result); + } + + [Fact] + public void OfType_NthChild_Odd() + { + var result = SelectorGrammar.Parse("Button:nth-child(odd)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = 2, + Offset = 1 + } + }, + result); + } + + [Fact] + public void OfType_NthChild_Even() + { + var result = SelectorGrammar.Parse("Button:nth-child(even)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NthChildSyntax() + { + Step = 2, + Offset = 0 + } + }, + result); + } + [Fact] public void Is_Descendent_Not_OfType_Class() { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 06b494c3d8..3824b79708 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -267,6 +267,65 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Style_Can_Use_NthChild_Selector() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)b1.Background).Color); + Assert.Null(b2.Background); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + parent.Children.Remove(b1); + parent.Children.Add(b1); + + Assert.Null(b1.Background); + Assert.Equal(Colors.Red, ((ISolidColorBrush)b2.Background).Color); + } + } + [Fact] public void Style_Can_Use_Or_Selector_1() { diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs new file mode 100644 index 0000000000..b72e980821 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -0,0 +1,217 @@ +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_NthChild + { + [Theory] + [InlineData(2, 0, ":nth-child(2n)")] + [InlineData(2, 1, ":nth-child(2n+1)")] + [InlineData(1, 0, ":nth-child(1n)")] + [InlineData(4, -1, ":nth-child(4n-1)")] + [InlineData(0, 1, ":nth-child(1)")] + [InlineData(0, -1, ":nth-child(-1)")] + [InlineData(int.MaxValue, int.MinValue + 1, ":nth-child(2147483647n-2147483647)")] + public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected) + { + var target = default(Selector).NthChild(step, offset); + + Assert.Equal(expected, target.ToString()); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(2, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(2, 1); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(4, -1); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(1, 2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(1, -1); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(0, 2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthChild(0, -2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + { + Border b1, b2; + Button b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new Control[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Button(), + b4 = new Button() + }); + + var previous = default(Selector).OfType(); + var target = previous.NthChild(2, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + { + Border b1; + var contentControl = new ContentControl(); + contentControl.Content = b1 = new Border(); + + var target = default(Selector).NthChild(1, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = new NthChildSelector(default(Selector).OfType(), 1, 0); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : Control + { + } + } +} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs new file mode 100644 index 0000000000..3698e07d3e --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -0,0 +1,217 @@ +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_NthLastChild + { + [Theory] + [InlineData(2, 0, ":nth-last-child(2n)")] + [InlineData(2, 1, ":nth-last-child(2n+1)")] + [InlineData(1, 0, ":nth-last-child(1n)")] + [InlineData(4, -1, ":nth-last-child(4n-1)")] + [InlineData(0, 1, ":nth-last-child(1)")] + [InlineData(0, -1, ":nth-last-child(-1)")] + [InlineData(int.MaxValue, int.MinValue + 1, ":nth-last-child(2147483647n-2147483647)")] + public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected) + { + var target = default(Selector).NthLastChild(step, offset); + + Assert.Equal(expected, target.ToString()); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(2, 0); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(2, 1); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(4, -1); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(1, 2); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(1, -2); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(0, 2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + { + Border b1, b2, b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Border(), + b4 = new Border() + }); + + var target = default(Selector).NthLastChild(0, -2); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + { + Border b1, b2; + Button b3, b4; + var panel = new StackPanel(); + panel.Children.AddRange(new Control[] + { + b1 = new Border(), + b2 = new Border(), + b3 = new Button(), + b4 = new Button() + }); + + var previous = default(Selector).OfType(); + var target = previous.NthLastChild(2, 0); + + Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); + } + + [Fact] + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + { + Border b1; + var contentControl = new ContentControl(); + contentControl.Content = b1 = new Border(); + + var target = default(Selector).NthLastChild(1, 0); + + Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = new NthLastChildSelector(default(Selector).OfType(), 1, 0); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : Control + { + } + } +} From e5ca5c38e8c6cff3f0fd6494578b83a8f4df6d22 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 10 Sep 2021 02:06:02 -0400 Subject: [PATCH 234/311] Update IChildIndexProvider interface with ChildIndexChanged and implement in on items controls --- .../Pages/ItemsRepeaterPage.xaml | 19 ++- src/Avalonia.Controls/ItemsControl.cs | 39 ++++-- src/Avalonia.Controls/Panel.cs | 21 ++-- .../Presenters/ItemsPresenterBase.cs | 43 ++++--- .../Repeater/ItemsRepeater.cs | 29 ++++- .../LogicalTree/ChildIndexChangedEventArgs.cs | 23 ++++ .../LogicalTree/IChildIndexProvider.cs | 14 +++ .../Styling/Activators/NthChildActivator.cs | 56 +++++++++ .../Styling/NthChildSelector.cs | 70 +++++------ .../Styling/NthLastChildSelector.cs | 11 ++ .../Xaml/StyleTests.cs | 119 +++++++++++++++++- .../SelectorTests_NthChild.cs | 85 +++++++------ .../SelectorTests_NthLastChild.cs | 84 +++++++------ .../StyleActivatorExtensions.cs | 7 +- 14 files changed, 456 insertions(+), 164 deletions(-) create mode 100644 src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs create mode 100644 src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs create mode 100644 src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs create mode 100644 src/Avalonia.Styling/Styling/NthLastChildSelector.cs diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 392ccb57c3..93f3c33434 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,17 +1,28 @@ + + + + + - - diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9c86aeb0c8..7b28335a6d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -57,6 +57,7 @@ namespace Avalonia.Controls private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -146,11 +147,30 @@ namespace Avalonia.Controls protected set; } + int? IChildIndexProvider.TotalCount => (Presenter as IChildIndexProvider)?.TotalCount ?? ItemCount; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { + if (Presenter is IChildIndexProvider oldInnerProvider) + { + oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged; + } + Presenter = presenter; ItemContainerGenerator.Clear(); + + if (Presenter is IChildIndexProvider innerProvider) + { + innerProvider.ChildIndexChanged += PresenterChildIndexChanged; + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + } } void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -508,20 +528,15 @@ namespace Avalonia.Controls return null; } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e) { - if (Presenter is IChildIndexProvider innerProvider) - { - return innerProvider.GetChildIndex(child); - } - - if (child is IControl control) - { - var index = ItemContainerGenerator.IndexFromContainer(control); - return (index, ItemCount); - } + _childIndexChanged?.Invoke(this, e); + } - return (-1, ItemCount); + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return Presenter is IChildIndexProvider innerProvider + ? innerProvider.GetChildIndex(child) : -1; } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 7a3e93ffc2..9c93126506 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -34,6 +34,8 @@ namespace Avalonia.Controls AffectsRender(BackgroundProperty); } + private EventHandler _childIndexChanged; + /// /// Initializes a new instance of the class. /// @@ -57,6 +59,14 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + int? IChildIndexProvider.TotalCount => Children.Count; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// /// Renders the visual to a . /// @@ -141,6 +151,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); InvalidateMeasureOnChildrenChanged(); } @@ -165,15 +176,9 @@ namespace Avalonia.Controls panel?.InvalidateMeasure(); } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + int IChildIndexProvider.GetChildIndex(ILogical child) { - if (child is IControl control) - { - var index = Children.IndexOf(control); - return (index, Children.Count); - } - - return (-1, Children.Count); + return child is IControl control ? Children.IndexOf(control) : -1; } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index cf5fb8ac42..d58ef2e510 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Collections.Specialized; +using System.Linq; + using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; @@ -37,6 +39,7 @@ namespace Avalonia.Controls.Presenters private IDisposable _itemsSubscription; private bool _createdPanel; private IItemContainerGenerator _generator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -130,6 +133,14 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : null; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// public override sealed void ApplyTemplate() { @@ -170,9 +181,21 @@ namespace Avalonia.Controls.Presenters result.ItemTemplate = ItemTemplate; } + result.Materialized += ContainerActionHandler; + result.Dematerialized += ContainerActionHandler; + result.Recycled += ContainerActionHandler; + return result; } + private void ContainerActionHandler(object sender, ItemContainerEventArgs e) + { + for (var i = 0; i < e.Containers.Count; i++) + { + _childIndexChanged?.Invoke(sender, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); + } + } + /// protected override Size MeasureOverride(Size availableSize) { @@ -250,26 +273,16 @@ namespace Avalonia.Controls.Presenters (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } - (int Index, int? TotalCount) IChildIndexProvider.GetChildIndex(ILogical child) + int IChildIndexProvider.GetChildIndex(ILogical child) { - int? totalCount = null; - if (Items.TryGetCountFast(out var count)) - { - totalCount = count; - } - - if (child is IControl control) + if (child is IControl control && ItemContainerGenerator is { } generator) { + var index = ItemContainerGenerator.IndexFromContainer(control); - if (ItemContainerGenerator is { } generator) - { - var index = ItemContainerGenerator.IndexFromContainer(control); - - return (index, totalCount); - } + return index; } - return (-1, totalCount); + return -1; } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 0ff8fcbd28..6d89a70670 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -6,10 +6,13 @@ using System; using System.Collections; using System.Collections.Specialized; + using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.LogicalTree; +using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -19,7 +22,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -61,8 +64,9 @@ namespace Avalonia.Controls private readonly ViewportManager _viewportManager; private IEnumerable _items; private VirtualizingLayoutContext _layoutContext; - private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private EventHandler _childIndexChanged; private bool _isLayoutInProgress; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; @@ -163,6 +167,21 @@ namespace Avalonia.Controls } } + int? IChildIndexProvider.TotalCount => ItemsSourceView.Count; + + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is IControl control + ? GetElementIndex(control) + : -1; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// @@ -545,6 +564,8 @@ namespace Avalonia.Controls ElementPrepared(this, _elementPreparedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementClearing(IControl element) @@ -562,6 +583,8 @@ namespace Avalonia.Controls ElementClearing(this, _elementClearingArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) @@ -579,6 +602,8 @@ namespace Avalonia.Controls ElementIndexChanged(this, _elementIndexChangedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs new file mode 100644 index 0000000000..1c90851e13 --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs @@ -0,0 +1,23 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + public class ChildIndexChangedEventArgs : EventArgs + { + public ChildIndexChangedEventArgs() + { + } + + public ChildIndexChangedEventArgs(ILogical child) + { + Child = child; + } + + /// + /// Logical child which index was changed. + /// If null, all children should be reset. + /// + public ILogical? Child { get; } + } +} diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs new file mode 100644 index 0000000000..fdba99baa2 --- /dev/null +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -0,0 +1,14 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + public interface IChildIndexProvider + { + int GetChildIndex(ILogical child); + + int? TotalCount { get; } + + event EventHandler? ChildIndexChanged; + } +} diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs new file mode 100644 index 0000000000..34cca1a396 --- /dev/null +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -0,0 +1,56 @@ +#nullable enable +using System; + +using Avalonia.LogicalTree; + +namespace Avalonia.Styling.Activators +{ + /// + /// An which is active when control's index was changed. + /// + internal sealed class NthChildActivator : StyleActivatorBase + { + private readonly ILogical _control; + private readonly IChildIndexProvider _provider; + private readonly int _step; + private readonly int _offset; + private readonly bool _reversed; + private EventHandler? _childIndexChangedHandler; + + public NthChildActivator( + ILogical control, + IChildIndexProvider provider, + int step, int offset, bool reversed) + { + _control = control; + _provider = provider; + _step = step; + _offset = offset; + _reversed = reversed; + } + + private EventHandler ChildIndexChangedHandler => _childIndexChangedHandler ??= ChildIndexChanged; + + protected override void Initialize() + { + PublishNext(IsMatching()); + _provider.ChildIndexChanged += ChildIndexChangedHandler; + } + + protected override void Deinitialize() + { + _provider.ChildIndexChanged -= ChildIndexChangedHandler; + } + + private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) + { + if (e.Child is null + || e.Child == _control) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + } +} diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index a6b91dea5f..16b97e22f6 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -3,21 +3,10 @@ using System; using System.Text; using Avalonia.LogicalTree; +using Avalonia.Styling.Activators; namespace Avalonia.Styling { - public interface IChildIndexProvider - { - (int Index, int? TotalCount) GetChildIndex(ILogical child); - } - - public class NthLastChildSelector : NthChildSelector - { - public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) - { - } - } - public class NthChildSelector : Selector { private const string NthChildSelectorName = "nth-child"; @@ -55,42 +44,49 @@ namespace Avalonia.Styling if (controlParent is IChildIndexProvider childIndexProvider) { - var (index, totalCount) = childIndexProvider.GetChildIndex(logical); - if (index < 0) - { - return SelectorMatch.NeverThisInstance; - } + return subscribe + ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed)) + : Evaluate(logical, childIndexProvider, Step, Offset, _reversed); + } + else + { + return SelectorMatch.NeverThisInstance; + } + } + + internal static SelectorMatch Evaluate( + ILogical logical, IChildIndexProvider childIndexProvider, + int step, int offset, bool reversed) + { + var index = childIndexProvider.GetChildIndex(logical); + if (index < 0) + { + return SelectorMatch.NeverThisInstance; + } - if (_reversed) + if (reversed) + { + if (childIndexProvider.TotalCount is int totalCountValue) { - if (totalCount is int totalCountValue) - { - index = totalCountValue - index; - } - else - { - return SelectorMatch.NeverThisInstance; - } + index = totalCountValue - index; } else { - // nth child index is 1-based - index += 1; + return SelectorMatch.NeverThisInstance; } - - - var n = Math.Sign(Step); - - var diff = index - Offset; - var match = diff == 0 || (Math.Sign(diff) == n && diff % Step == 0); - - return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } else { - return SelectorMatch.NeverThisInstance; + // nth child index is 1-based + index += 1; } + var n = Math.Sign(step); + + var diff = index - offset; + var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0); + + return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; } protected override Selector? MovePrevious() => _previous; diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs new file mode 100644 index 0000000000..ff7cf0faa1 --- /dev/null +++ b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace Avalonia.Styling +{ + public class NthLastChildSelector : NthChildSelector + { + public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) + { + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 3824b79708..ee633ee66f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,6 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; using System.Xml; using Avalonia.Controls; using Avalonia.Markup.Data; @@ -289,7 +292,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var b1 = window.FindControl("b1"); var b2 = window.FindControl("b2"); - Assert.Equal(Colors.Red, ((ISolidColorBrush)b1.Background).Color); + Assert.Equal(Brushes.Red, b1.Background); Assert.Null(b2.Background); } } @@ -303,7 +306,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - @@ -318,11 +321,119 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var b1 = window.FindControl("b1"); var b2 = window.FindControl("b2"); + Assert.Null(b1.Background); + Assert.Equal(Brushes.Red, b2.Background); + parent.Children.Remove(b1); - parent.Children.Add(b1); Assert.Null(b1.Background); - Assert.Equal(Colors.Red, ((ISolidColorBrush)b2.Background).Color); + Assert.Null(b2.Background); + + parent.Children.Add(b1); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + } + } + + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ListBox() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.VirtualizationMode = ItemVirtualizationMode.Simple; + list.Items = collection; + + window.Show(); + + var items = list.Presenter.Panel.Children.Cast(); + ListBoxItem At(int index) => items.ElementAt(index); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Green, At(1).Background); + Assert.Equal(Brushes.Transparent, At(2).Background); + + collection.Remove(Brushes.Green); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Blue, At(1).Background); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(Brushes.Transparent, At(0).Background); + Assert.Equal(Brushes.Blue, At(1).Background); + Assert.Equal(Brushes.Transparent, At(2).Background); + Assert.Equal(Brushes.Black, At(3).Background); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.Items = collection; + + window.Show(); + + var items = list.Children; + TextBlock At(int index) => (TextBlock)list.GetOrCreateElement(index); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Green, At(1).Foreground); + Assert.Equal(Brushes.Transparent, At(2).Foreground); + + collection.Remove(Brushes.Green); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Blue, At(1).Foreground); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(Brushes.Transparent, At(0).Foreground); + Assert.Equal(Brushes.Blue, At(1).Foreground); + Assert.Equal(Brushes.Transparent, At(2).Foreground); + Assert.Equal(Brushes.Black, At(3).Foreground); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index b72e980821..a70b3c9f29 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,4 +1,7 @@ +using System.Threading.Tasks; + using Avalonia.Controls; + using Xunit; namespace Avalonia.Styling.UnitTests @@ -21,7 +24,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Nth_Child_Match_Control_In_Panel() + public async Task Nth_Child_Match_Control_In_Panel() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -35,14 +38,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(2, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -56,14 +59,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(2, 1); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -77,14 +80,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(4, -1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -98,14 +101,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -119,14 +122,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, -1); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -140,14 +143,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(0, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -161,14 +164,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(0, -2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() { Border b1, b2; Button b3, b4; @@ -184,14 +187,16 @@ namespace Avalonia.Styling.UnitTests var previous = default(Selector).OfType(); var target = previous.NthChild(2, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.Null(target.Match(b3).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Null(target.Match(b4).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); } [Fact] - public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -199,7 +204,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index 3698e07d3e..ed88106295 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + using Avalonia.Controls; using Xunit; @@ -21,7 +23,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Nth_Child_Match_Control_In_Panel() + public async Task Nth_Child_Match_Control_In_Panel() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -35,14 +37,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(2, 0); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -56,14 +58,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(2, 1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -77,14 +79,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(4, -1); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -98,14 +100,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, 2); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -119,14 +121,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, -2); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b4).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.True(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.True(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() + public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -140,14 +142,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(0, 2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.True(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() + public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset() { Border b1, b2, b3, b4; var panel = new StackPanel(); @@ -161,14 +163,14 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(0, -2); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b3).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b4).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.False(await target.Match(b3).Activator!.Take(1)); + Assert.False(await target.Match(b4).Activator!.Take(1)); } [Fact] - public void Nth_Child_Match_Control_In_Panel_With_Previous_Selector() + public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector() { Border b1, b2; Button b3, b4; @@ -184,14 +186,16 @@ namespace Avalonia.Styling.UnitTests var previous = default(Selector).OfType(); var target = previous.NthLastChild(2, 0); - Assert.Equal(SelectorMatchResult.AlwaysThisInstance, target.Match(b1).Result); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b2).Result); + Assert.True(await target.Match(b1).Activator!.Take(1)); + Assert.False(await target.Match(b2).Activator!.Take(1)); + Assert.Null(target.Match(b3).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result); + Assert.Null(target.Match(b4).Activator); Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result); } [Fact] - public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -199,7 +203,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, 0); - Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(b1).Result); + Assert.False(await target.Match(b1).Activator!.Take(1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs b/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs index eb3dabce0b..22f4db79d1 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs @@ -20,13 +20,17 @@ namespace Avalonia.Styling.UnitTests public static IObservable ToObservable(this IStyleActivator activator) { + if (activator == null) + { + throw new ArgumentNullException(nameof(activator)); + } + return new ObservableAdapter(activator); } private class ObservableAdapter : LightweightObservableBase, IStyleActivatorSink { private readonly IStyleActivator _source; - private bool _value; public ObservableAdapter(IStyleActivator source) => _source = source; protected override void Initialize() => _source.Subscribe(this); @@ -34,7 +38,6 @@ namespace Avalonia.Styling.UnitTests void IStyleActivatorSink.OnNext(bool value, int tag) { - _value = value; PublishNext(value); } } From 031e8ac2f0e45c91150f48f1d1d2393c7ae9606c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 16 Oct 2021 22:15:16 -0400 Subject: [PATCH 235/311] Complete --- .../Pages/ItemsRepeaterPage.xaml | 5 ++ samples/ControlCatalog/Pages/ListBoxPage.xaml | 7 +- .../Presenters/ItemsPresenterBase.cs | 7 +- .../LogicalTree/ChildIndexChangedEventArgs.cs | 3 + .../LogicalTree/IChildIndexProvider.cs | 18 +++++ .../Styling/Activators/NthChildActivator.cs | 7 +- .../Styling/NthChildSelector.cs | 12 ++++ .../Styling/NthLastChildSelector.cs | 12 ++++ src/Avalonia.Styling/Styling/Selectors.cs | 6 ++ .../Xaml/StyleTests.cs | 71 +++++++++++++------ .../SelectorTests_NthChild.cs | 4 +- .../SelectorTests_NthLastChild.cs | 4 +- 12 files changed, 125 insertions(+), 31 deletions(-) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 93f3c33434..4d0bd663df 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -12,6 +12,11 @@ + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 897134badb..cb29f54c94 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -3,8 +3,13 @@ x:Class="ControlCatalog.Pages.ListBoxPage"> - + diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index d58ef2e510..b92af1eb9c 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -133,7 +132,7 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; - int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : null; + int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : (int?)null; event EventHandler IChildIndexProvider.ChildIndexChanged { @@ -161,6 +160,8 @@ namespace Avalonia.Controls.Presenters if (Panel != null) { ItemsChanged(e); + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); } } @@ -192,7 +193,7 @@ namespace Avalonia.Controls.Presenters { for (var i = 0; i < e.Containers.Count; i++) { - _childIndexChanged?.Invoke(sender, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); } } diff --git a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs index 1c90851e13..de41f5292c 100644 --- a/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs +++ b/src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs @@ -3,6 +3,9 @@ using System; namespace Avalonia.LogicalTree { + /// + /// Event args for event. + /// public class ChildIndexChangedEventArgs : EventArgs { public ChildIndexChangedEventArgs() diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs index fdba99baa2..53e2199d28 100644 --- a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -3,12 +3,30 @@ using System; namespace Avalonia.LogicalTree { + /// + /// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.) + /// + /// + /// Used by nth-child and nth-last-child selectors. + /// public interface IChildIndexProvider { + /// + /// Gets child's actual index in order of the original source. + /// + /// Logical child. + /// Index or -1 if child was not found. int GetChildIndex(ILogical child); + /// + /// Total children count or null if source is infinite. + /// Some Avalonia features might not work if is null, for instance: nth-last-child selector. + /// int? TotalCount { get; } + /// + /// Notifies subscriber when child's index or total count was changed. + /// event EventHandler? ChildIndexChanged; } } diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs index 34cca1a396..5d23d1ffd1 100644 --- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -44,7 +44,12 @@ namespace Avalonia.Styling.Activators private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) { - if (e.Child is null + // Run matching again if: + // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. + // 2. e.Child is null, when all children indeces were changed. + // 3. Subscribed child index was changed. + if (_reversed + || e.Child is null || e.Child == _control) { PublishNext(IsMatching()); diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index 16b97e22f6..e844fb51f8 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -7,6 +7,12 @@ using Avalonia.Styling.Activators; namespace Avalonia.Styling { + /// + /// The :nth-child() pseudo-class matches elements based on their position in a group of siblings. + /// + /// + /// Element indices are 1-based. + /// public class NthChildSelector : Selector { private const string NthChildSelectorName = "nth-child"; @@ -22,6 +28,12 @@ namespace Avalonia.Styling _reversed = reversed; } + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset. public NthChildSelector(Selector? previous, int step, int offset) : this(previous, step, offset, false) { diff --git a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs index ff7cf0faa1..6f6abbae6a 100644 --- a/src/Avalonia.Styling/Styling/NthLastChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthLastChildSelector.cs @@ -2,8 +2,20 @@ namespace Avalonia.Styling { + /// + /// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end. + /// + /// + /// Element indices are 1-based. + /// public class NthLastChildSelector : NthChildSelector { + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset, counting from the end. public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) { } diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index 0bccccbd7c..64d0a0e96b 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -123,11 +123,17 @@ namespace Avalonia.Styling return new NotSelector(previous, argument); } + /// + /// + /// The selector. public static Selector NthChild(this Selector previous, int step, int offset) { return new NthChildSelector(previous, step, offset); } + /// + /// + /// The selector. public static Selector NthLastChild(this Selector previous, int step, int offset) { return new NthLastChildSelector(previous, step, offset); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index ee633ee66f..28960c8bf6 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -336,6 +337,45 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Style_Can_Use_NthLastChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + + parent.Children.Remove(b1); + + Assert.Null(b1.Background); + Assert.Null(b2.Background); + + parent.Children.Add(b1); + + Assert.Null(b1.Background); + Assert.Equal(Brushes.Red, b2.Background); + } + } + [Fact] public void Style_Can_Use_NthChild_Selector_With_ListBox() @@ -364,25 +404,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.Show(); - var items = list.Presenter.Panel.Children.Cast(); - ListBoxItem At(int index) => items.ElementAt(index); + IEnumerable GetColors() => list.Presenter.Panel.Children.Cast().Select(t => t.Background); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Green, At(1).Background); - Assert.Equal(Brushes.Transparent, At(2).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); collection.Remove(Brushes.Green); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Blue, At(1).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); collection.Add(Brushes.Violet); collection.Add(Brushes.Black); - Assert.Equal(Brushes.Transparent, At(0).Background); - Assert.Equal(Brushes.Blue, At(1).Background); - Assert.Equal(Brushes.Transparent, At(2).Background); - Assert.Equal(Brushes.Black, At(3).Background); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); } } @@ -415,25 +448,19 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml window.Show(); - var items = list.Children; - TextBlock At(int index) => (TextBlock)list.GetOrCreateElement(index); + IEnumerable GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count) + .Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Green, At(1).Foreground); - Assert.Equal(Brushes.Transparent, At(2).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); collection.Remove(Brushes.Green); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Blue, At(1).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); collection.Add(Brushes.Violet); collection.Add(Brushes.Black); - Assert.Equal(Brushes.Transparent, At(0).Foreground); - Assert.Equal(Brushes.Blue, At(1).Foreground); - Assert.Equal(Brushes.Transparent, At(2).Foreground); - Assert.Equal(Brushes.Black, At(3).Foreground); + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index a70b3c9f29..8a8e46fc4b 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -196,7 +196,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -204,7 +204,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthChild(1, 0); - Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index ed88106295..8d9d490724 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -195,7 +195,7 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public async Task Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() + public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent() { Border b1; var contentControl = new ContentControl(); @@ -203,7 +203,7 @@ namespace Avalonia.Styling.UnitTests var target = default(Selector).NthLastChild(1, 0); - Assert.False(await target.Match(b1).Activator!.Take(1)); + Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } [Fact] From f276c4ed8b992017003d94a863128effdd068fe2 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 18:50:02 -0400 Subject: [PATCH 236/311] Changes after review --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- src/Avalonia.Controls/ItemsControl.cs | 15 ++++++++++++--- src/Avalonia.Controls/Panel.cs | 10 ++++++---- .../Presenters/ItemsPresenterBase.cs | 8 +++++--- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 10 ++++++---- .../LogicalTree/IChildIndexProvider.cs | 4 ++-- .../Styling/Activators/NthChildActivator.cs | 9 ++------- src/Avalonia.Styling/Styling/NthChildSelector.cs | 9 ++++++--- .../Xaml/StyleTests.cs | 2 -- .../SelectorTests_NthChild.cs | 2 -- .../SelectorTests_NthLastChild.cs | 1 - 11 files changed, 40 insertions(+), 32 deletions(-) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index cb29f54c94..b36629fb2a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -15,7 +15,7 @@ ListBox Hosts a collection of ListBoxItem. - Each 2nd item is highlighted + Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules. Multiple diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 7b28335a6d..1ff49326b6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls @@ -147,8 +146,6 @@ namespace Avalonia.Controls protected set; } - int? IChildIndexProvider.TotalCount => (Presenter as IChildIndexProvider)?.TotalCount ?? ItemCount; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -538,5 +535,17 @@ namespace Avalonia.Controls return Presenter is IChildIndexProvider innerProvider ? innerProvider.GetChildIndex(child) : -1; } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + if (Presenter is IChildIndexProvider presenter + && presenter.TryGetTotalCount(out count)) + { + return true; + } + + count = ItemCount; + return true; + } } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 9c93126506..b182f9d261 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; - -using Avalonia.Controls.Presenters; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; @@ -59,8 +57,6 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } - int? IChildIndexProvider.TotalCount => Children.Count; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -180,5 +176,11 @@ namespace Avalonia.Controls { return child is IControl control ? Children.IndexOf(control) : -1; } + + public bool TryGetTotalCount(out int count) + { + count = Children.Count; + return true; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b92af1eb9c..aeead7bfd0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Specialized; - using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; @@ -132,8 +131,6 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; - int? IChildIndexProvider.TotalCount => Items.TryGetCountFast(out var count) ? count : (int?)null; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -285,5 +282,10 @@ namespace Avalonia.Controls.Presenters return -1; } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + return Items.TryGetCountFast(out count); + } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 6d89a70670..ecc0fa3a48 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -6,13 +6,11 @@ using System; using System.Collections; using System.Collections.Specialized; - using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; -using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -167,8 +165,6 @@ namespace Avalonia.Controls } } - int? IChildIndexProvider.TotalCount => ItemsSourceView.Count; - event EventHandler IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; @@ -182,6 +178,12 @@ namespace Avalonia.Controls : -1; } + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = ItemsSourceView.Count; + return true; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// diff --git a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs index 53e2199d28..7fcd73273c 100644 --- a/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs @@ -20,9 +20,9 @@ namespace Avalonia.LogicalTree /// /// Total children count or null if source is infinite. - /// Some Avalonia features might not work if is null, for instance: nth-last-child selector. + /// Some Avalonia features might not work if returns false, for instance: nth-last-child selector. /// - int? TotalCount { get; } + bool TryGetTotalCount(out int count); /// /// Notifies subscriber when child's index or total count was changed. diff --git a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs index 5d23d1ffd1..803809a8ce 100644 --- a/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs @@ -1,6 +1,4 @@ #nullable enable -using System; - using Avalonia.LogicalTree; namespace Avalonia.Styling.Activators @@ -15,7 +13,6 @@ namespace Avalonia.Styling.Activators private readonly int _step; private readonly int _offset; private readonly bool _reversed; - private EventHandler? _childIndexChangedHandler; public NthChildActivator( ILogical control, @@ -29,17 +26,15 @@ namespace Avalonia.Styling.Activators _reversed = reversed; } - private EventHandler ChildIndexChangedHandler => _childIndexChangedHandler ??= ChildIndexChanged; - protected override void Initialize() { PublishNext(IsMatching()); - _provider.ChildIndexChanged += ChildIndexChangedHandler; + _provider.ChildIndexChanged += ChildIndexChanged; } protected override void Deinitialize() { - _provider.ChildIndexChanged -= ChildIndexChangedHandler; + _provider.ChildIndexChanged -= ChildIndexChanged; } private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) diff --git a/src/Avalonia.Styling/Styling/NthChildSelector.cs b/src/Avalonia.Styling/Styling/NthChildSelector.cs index e844fb51f8..aff34ea17c 100644 --- a/src/Avalonia.Styling/Styling/NthChildSelector.cs +++ b/src/Avalonia.Styling/Styling/NthChildSelector.cs @@ -1,7 +1,6 @@ #nullable enable using System; using System.Text; - using Avalonia.LogicalTree; using Avalonia.Styling.Activators; @@ -51,7 +50,11 @@ namespace Avalonia.Styling protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) { - var logical = (ILogical)control; + if (!(control is ILogical logical)) + { + return SelectorMatch.NeverThisType; + } + var controlParent = logical.LogicalParent; if (controlParent is IChildIndexProvider childIndexProvider) @@ -78,7 +81,7 @@ namespace Avalonia.Styling if (reversed) { - if (childIndexProvider.TotalCount is int totalCountValue) + if (childIndexProvider.TryGetTotalCount(out var totalCountValue)) { index = totalCountValue - index; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 28960c8bf6..022ff0c3a4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -1,10 +1,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Threading.Tasks; using System.Xml; using Avalonia.Controls; -using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index 8a8e46fc4b..1d101b8ea0 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,7 +1,5 @@ using System.Threading.Tasks; - using Avalonia.Controls; - using Xunit; namespace Avalonia.Styling.UnitTests diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs index 8d9d490724..00a99523c7 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - using Avalonia.Controls; using Xunit; From d64a700b4fd2fae11ee3de45f83e18c4c3984562 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 20:08:12 -0400 Subject: [PATCH 237/311] Imrpove nth-child parsing --- .../Markup/Parsers/SelectorGrammar.cs | 56 +++++++++++++-- .../Parsers/SelectorGrammarTests.cs | 69 +++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index 56e64329b7..953a7e9a15 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -350,12 +350,28 @@ namespace Avalonia.Markup.Parsers } else { - var stepOrOffsetSpan = r.TakeWhile(c => c != ')' && c != 'n'); - if (!int.TryParse(stepOrOffsetSpan.ToString().Trim(), out var stepOrOffset)) + r.SkipWhitespace(); + + var stepOrOffset = 0; + var stepOrOffsetStr = r.TakeWhile(c => char.IsDigit(c) || c == '-' || c == '+').ToString(); + if (stepOrOffsetStr.Length == 0 + || (stepOrOffsetStr.Length == 1 + && stepOrOffsetStr[0] == '+')) + { + stepOrOffset = 1; + } + else if (stepOrOffsetStr.Length == 1 + && stepOrOffsetStr[0] == '-') + { + stepOrOffset = -1; + } + else if (!int.TryParse(stepOrOffsetStr.ToString(), out stepOrOffset)) { throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected."); } + r.SkipWhitespace(); + if (r.Peek == ')') { step = 0; @@ -365,13 +381,41 @@ namespace Avalonia.Markup.Parsers { step = stepOrOffset; + if (r.Peek != 'n') + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step value, \"xn+y\" pattern was expected."); + } + r.Skip(1); // skip 'n' - var offsetSpan = r.TakeUntil(')').TrimStart(); - if (offsetSpan.Length != 0 - && !int.TryParse(offsetSpan.ToString().Trim(), out offset)) + r.SkipWhitespace(); + + if (r.Peek != ')') { - throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + int sign; + var nextChar = r.Take(); + if (nextChar == '+') + { + sign = 1; + } + else if (nextChar == '-') + { + sign = -1; + } + else + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child sign. '+' or '-' was expected."); + } + + r.SkipWhitespace(); + + if (sign != 0 + && !int.TryParse(r.TakeUntil(')').ToString(), out offset)) + { + throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected."); + } + + offset *= sign; } } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 543d44c492..568f6deaf2 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -236,6 +236,75 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Theory] + [InlineData(":nth-child(xn+2)")] + [InlineData(":nth-child(2n+b)")] + [InlineData(":nth-child(2n+)")] + [InlineData(":nth-child(2na)")] + [InlineData(":nth-child(2x+1)")] + public void NthChild_Invalid_Inputs(string input) + { + Assert.Throws(() => SelectorGrammar.Parse(input)); + } + + [Theory] + [InlineData(":nth-child(+1)", 0, 1)] + [InlineData(":nth-child(1)", 0, 1)] + [InlineData(":nth-child(-1)", 0, -1)] + [InlineData(":nth-child(2n+1)", 2, 1)] + [InlineData(":nth-child(n)", 1, 0)] + [InlineData(":nth-child(+n)", 1, 0)] + [InlineData(":nth-child(-n)", -1, 0)] + [InlineData(":nth-child(-2n)", -2, 0)] + [InlineData(":nth-child(n+5)", 1, 5)] + [InlineData(":nth-child(n-5)", 1, -5)] + [InlineData(":nth-child( 2n + 1 )", 2, 1)] + [InlineData(":nth-child( 2n - 1 )", 2, -1)] + public void NthChild_Variations(string input, int step, int offset) + { + var result = SelectorGrammar.Parse(input); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NthChildSyntax() + { + Step = step, + Offset = offset + } + }, + result); + } + + [Theory] + [InlineData(":nth-last-child(+1)", 0, 1)] + [InlineData(":nth-last-child(1)", 0, 1)] + [InlineData(":nth-last-child(-1)", 0, -1)] + [InlineData(":nth-last-child(2n+1)", 2, 1)] + [InlineData(":nth-last-child(n)", 1, 0)] + [InlineData(":nth-last-child(+n)", 1, 0)] + [InlineData(":nth-last-child(-n)", -1, 0)] + [InlineData(":nth-last-child(-2n)", -2, 0)] + [InlineData(":nth-last-child(n+5)", 1, 5)] + [InlineData(":nth-last-child(n-5)", 1, -5)] + [InlineData(":nth-last-child( 2n + 1 )", 2, 1)] + [InlineData(":nth-last-child( 2n - 1 )", 2, -1)] + public void NthLastChild_Variations(string input, int step, int offset) + { + var result = SelectorGrammar.Parse(input); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NthLastChildSyntax() + { + Step = step, + Offset = offset + } + }, + result); + } + [Fact] public void OfType_NthChild() { From bab044980569a7f80ead99792d48da4969a2fd0f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Oct 2021 20:46:24 -0400 Subject: [PATCH 238/311] Added tests from nthmaster.com --- .../SelectorTests_NthChild.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs index 1d101b8ea0..e1507be110 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Xunit; @@ -205,6 +206,76 @@ namespace Avalonia.Styling.UnitTests Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1)); } + + [Theory] // http://nthmaster.com/ + [InlineData(+0, 8, false, false, false, false, false, false, false, true , false, false, false)] + [InlineData(+1, 6, false, false, false, false, false, true , true , true , true , true , true )] + [InlineData(-1, 9, true , true , true , true , true , true , true , true , true , false, false)] + public async Task Nth_Child_Master_Com_Test_Sigle_Selector( + int step, int offset, params bool[] items) + { + var panel = new StackPanel(); + panel.Children.AddRange(items.Select(_ => new Border())); + + var previous = default(Selector).OfType(); + var target = previous.NthChild(step, offset); + + var results = new bool[items.Length]; + for (int index = 0; index < items.Length; index++) + { + var border = panel.Children[index]; + results[index] = await target.Match(border).Activator!.Take(1); + } + + Assert.Equal(items, results); + } + + [Theory] // http://nthmaster.com/ + [InlineData(+1, 4, -1, 8, false, false, false, true , true , true , true , true , false, false, false)] + [InlineData(+3, 1, +2, 0, false, false, false, true , false, false, false, false, false, true , false)] + public async Task Nth_Child_Master_Com_Test_Double_Selector( + int step1, int offset1, int step2, int offset2, params bool[] items) + { + var panel = new StackPanel(); + panel.Children.AddRange(items.Select(_ => new Border())); + + var previous = default(Selector).OfType(); + var middle = previous.NthChild(step1, offset1); + var target = middle.NthChild(step2, offset2); + + var results = new bool[items.Length]; + for (int index = 0; index < items.Length; index++) + { + var border = panel.Children[index]; + results[index] = await target.Match(border).Activator!.Take(1); + } + + Assert.Equal(items, results); + } + + [Theory] // http://nthmaster.com/ + [InlineData(+1, 2, 2, 1, -1, 9, false, false, true , false, true , false, true , false, true , false, false)] + public async Task Nth_Child_Master_Com_Test_Triple_Selector( + int step1, int offset1, int step2, int offset2, int step3, int offset3, params bool[] items) + { + var panel = new StackPanel(); + panel.Children.AddRange(items.Select(_ => new Border())); + + var previous = default(Selector).OfType(); + var middle1 = previous.NthChild(step1, offset1); + var middle2 = middle1.NthChild(step2, offset2); + var target = middle2.NthChild(step3, offset3); + + var results = new bool[items.Length]; + for (int index = 0; index < items.Length; index++) + { + var border = panel.Children[index]; + results[index] = await target.Match(border).Activator!.Take(1); + } + + Assert.Equal(items, results); + } + [Fact] public void Returns_Correct_TargetType() { From 141e749226c95557f4b44347525abbe3fa2db230 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:04:09 +0800 Subject: [PATCH 239/311] Initial Commit for handling DBus SNI Tray Icons gracefully and also making a skeleton class for the future XEmbed Tray Icon impl. --- .../DbusSNITrayIconImpl.cs | 358 +++++++++++++++++ src/Avalonia.X11/X11TrayIconImpl.cs | 379 ++++-------------- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 36 ++ 3 files changed, 462 insertions(+), 311 deletions(-) create mode 100644 src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs create mode 100644 src/Avalonia.X11/XEmbedTrayIconImpl.cs diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs new file mode 100644 index 0000000000..1fb74f132a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -0,0 +1,358 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop; +using Avalonia.Logging; +using Avalonia.Platform; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] + +namespace Avalonia.FreeDesktop +{ + public class DbusSNITrayIconImpl + { + private static int s_trayIconInstanceId; + private readonly ObjectPath _dbusMenuPath; + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; + private readonly Connection? _connection; + private DbusPixmap _icon; + + private IStatusNotifierWatcher? _statusNotifierWatcher; + + private string? _sysTrayServiceName; + private string? _tooltipText; + private bool _isActive; + private bool _isDisposed; + private readonly bool _ctorFinished; + + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } + + public bool IsActive => _isActive; + + public DbusSNITrayIconImpl(Connection connection) + { + _connection = connection; + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); + CreateTrayIcon(); + _ctorFinished = true; + } + + public async void CreateTrayIcon() + { + if (_connection is null) + return; + + try + { + _statusNotifierWatcher = _connection.CreateProxy( + "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + } + catch + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + } + + if (_statusNotifierWatcher is null) + return; + + var pid = Process.GetCurrentProcess().Id; + var tid = s_trayIconInstanceId++; + + _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); + + await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + + await _connection.RegisterServiceAsync(_sysTrayServiceName); + + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + + _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); + _statusNotifierItemDbusObj.SetIcon(_icon); + + _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; + + _isActive = true; + } + + public async void DestroyTrayIcon() + { + if (_connection is null) + return; + _connection.UnregisterObject(_statusNotifierItemDbusObj); + await _connection.UnregisterServiceAsync(_sysTrayServiceName); + _isActive = false; + } + + public void Dispose() + { + _isDisposed = true; + DestroyTrayIcon(); + _connection?.Dispose(); + } + + public void SetIcon(UIntPtr[] x11iconData) + { + if (_isDisposed) + return; + var w = (int)x11iconData[0]; + var h = (int)x11iconData[1]; + + var pixLength = w * h; + var pixByteArrayCounter = 0; + var pixByteArray = new byte[w * h * 4]; + + for (var i = 0; i < pixLength; i++) + { + var rawPixel = x11iconData[i + 2].ToUInt32(); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); + pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); + } + + _icon = new DbusPixmap(w, h, pixByteArray); + _statusNotifierItemDbusObj?.SetIcon(_icon); + } + + public void SetIsVisible(bool visible) + { + if (_isDisposed || !_ctorFinished) + return; + + if (visible & !_isActive) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else if (!visible & _isActive) + { + DestroyTrayIcon(); + } + } + + public void SetToolTipText(string? text) + { + if (_isDisposed || text is null) + return; + _tooltipText = text; + _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); + } + } + + /// + /// DBus Object used for setting system tray icons. + /// + /// + /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html + /// + internal class StatusNotifierItemDbusObj : IStatusNotifierItem + { + private readonly StatusNotifierItemProperties _backingProperties; + public event Action? OnTitleChanged; + public event Action? OnIconChanged; + public event Action? OnAttentionIconChanged; + public event Action? OnOverlayIconChanged; + public event Action? OnTooltipChanged; + public Action? NewStatusAsync { get; set; } + public Action? ActivationDelegate { get; set; } + public ObjectPath ObjectPath { get; } + + public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + { + ObjectPath = new ObjectPath($"/StatusNotifierItem"); + + _backingProperties = new StatusNotifierItemProperties + { + Menu = dbusmenuPath, // Needs a dbus menu somehow + ToolTip = new ToolTip("") + }; + + InvalidateAll(); + } + + public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; + + public Task ActivateAsync(int x, int y) + { + ActivationDelegate?.Invoke(); + return Task.CompletedTask; + } + + public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; + + public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; + + public void InvalidateAll() + { + OnTitleChanged?.Invoke(); + OnIconChanged?.Invoke(); + OnOverlayIconChanged?.Invoke(); + OnAttentionIconChanged?.Invoke(); + OnTooltipChanged?.Invoke(); + } + + public Task WatchNewTitleAsync(Action handler, Action onError) + { + OnTitleChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); + } + + public Task WatchNewIconAsync(Action handler, Action onError) + { + OnIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); + } + + public Task WatchNewAttentionIconAsync(Action handler, Action onError) + { + OnAttentionIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); + } + + public Task WatchNewOverlayIconAsync(Action handler, Action onError) + { + OnOverlayIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); + } + + public Task WatchNewToolTipAsync(Action handler, Action onError) + { + OnTooltipChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); + } + + public Task WatchNewStatusAsync(Action handler, Action onError) + { + NewStatusAsync += handler; + return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); + } + + public Task GetAsync(string prop) => Task.FromResult(new object()); + + public Task GetAllAsync() => Task.FromResult(_backingProperties); + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public Task WatchPropertiesAsync(Action handler) => + Task.FromResult(Disposable.Empty); + + public void SetIcon(DbusPixmap dbusPixmap) + { + _backingProperties.IconPixmap = new[] { dbusPixmap }; + InvalidateAll(); + } + + public void SetTitleAndTooltip(string? text) + { + if (text is null) + return; + + _backingProperties.Id = text; + _backingProperties.Category = "ApplicationStatus"; + _backingProperties.Status = text; + _backingProperties.Title = text; + _backingProperties.ToolTip = new ToolTip(text); + + InvalidateAll(); + } + } + + [DBusInterface("org.kde.StatusNotifierWatcher")] + internal interface IStatusNotifierWatcher : IDBusObject + { + Task RegisterStatusNotifierItemAsync(string Service); + Task RegisterStatusNotifierHostAsync(string Service); + } + + [DBusInterface("org.kde.StatusNotifierItem")] + internal interface IStatusNotifierItem : IDBusObject + { + Task ContextMenuAsync(int x, int y); + Task ActivateAsync(int x, int y); + Task SecondaryActivateAsync(int x, int y); + Task ScrollAsync(int delta, string orientation); + Task WatchNewTitleAsync(Action handler, Action onError); + Task WatchNewIconAsync(Action handler, Action onError); + Task WatchNewAttentionIconAsync(Action handler, Action onError); + Task WatchNewOverlayIconAsync(Action handler, Action onError); + Task WatchNewToolTipAsync(Action handler, Action onError); + Task WatchNewStatusAsync(Action handler, Action onError); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + // This class is used by Tmds.Dbus to ferry properties + // from the SNI spec. + // Don't change this to actual C# properties since + // Tmds.Dbus will get confused. + internal class StatusNotifierItemProperties + { + public string? Category; + + public string? Id; + + public string? Title; + + public string? Status; + + public ObjectPath Menu; + + public DbusPixmap[]? IconPixmap; + + public ToolTip ToolTip; + } + + internal struct ToolTip + { + public readonly string First; + public readonly DbusPixmap[] Second; + public readonly string Third; + public readonly string Fourth; + + private static readonly DbusPixmap[] s_blank = + { + new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) + }; + + public ToolTip(string message) : this("", s_blank, message, "") + { + } + + public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + } + + internal readonly struct DbusPixmap + { + public readonly int Width; + public readonly int Height; + public readonly byte[] Data; + + public DbusPixmap(int width, int height, byte[] data) + { + Width = width; + Height = height; + Data = data; + } + } +} diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 371ff75408..ca8ed8ec35 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,367 +1,124 @@ -#nullable enable - using System; -using System.Diagnostics; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; using Avalonia.Logging; using Avalonia.Platform; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] namespace Avalonia.X11 { internal class X11TrayIconImpl : ITrayIconImpl { - private static int s_trayIconInstanceId; - private readonly ObjectPath _dbusMenuPath; - private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private readonly Connection? _connection; - private DbusPixmap _icon; - - private IStatusNotifierWatcher? _statusNotifierWatcher; - - private string? _sysTrayServiceName; - private string? _tooltipText; - private bool _isActive; - private bool _isDisposed; - private readonly bool _ctorFinished; - - public INativeMenuExporter? MenuExporter { get; } - public Action? OnClicked { get; set; } - public X11TrayIconImpl() { - _connection = DBusHelper.TryGetConnection(); + _xEmbedTrayIcon = new XEmbedTrayIconImpl(); + + var _connection = DBusHelper.TryGetConnection(); if (_connection is null) { Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "Unable to get a dbus connection for system tray icons."); - return; } - _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); - CreateTrayIcon(); - _ctorFinished = true; + _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); } - public async void CreateTrayIcon() - { - if (_connection is null) - return; - - try - { - _statusNotifierWatcher = _connection.CreateProxy( - "org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - } - catch - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) - ?.Log(this, - "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); - } - - if (_statusNotifierWatcher is null) - return; - - var pid = Process.GetCurrentProcess().Id; - var tid = s_trayIconInstanceId++; - - _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - - await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - - await _connection.RegisterServiceAsync(_sysTrayServiceName); - - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - - _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); - _statusNotifierItemDbusObj.SetIcon(_icon); + private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; - } - - public async void DestroyTrayIcon() - { - if (_connection is null) - return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; - } + private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; + private bool _isDisposed; public void Dispose() { + _dbusSniTrayIcon?.Dispose(); + _xEmbedTrayIcon?.Dispose(); _isDisposed = true; - DestroyTrayIcon(); - _connection?.Dispose(); } public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed) - return; - if (!(icon is X11IconData x11icon)) - return; + if (_isDisposed) return; - var w = (int)x11icon.Data[0]; - var h = (int)x11icon.Data[1]; - - var pixLength = w * h; - var pixByteArrayCounter = 0; - var pixByteArray = new byte[w * h * 4]; + if (_dbusSniTrayIcon?.IsActive ?? false) + { + if (!(icon is X11IconData x11icon)) + return; - for (var i = 0; i < pixLength; i++) + _dbusSniTrayIcon.SetIcon(x11icon.Data); + } + else { - var rawPixel = x11icon.Data[i + 2].ToUInt32(); - pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); - pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); - pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); - pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); + _xEmbedTrayIcon.SetIcon(icon); } - - _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj?.SetIcon(_icon); } - public void SetIsVisible(bool visible) + public void SetToolTipText(string? text) { - if (_isDisposed || !_ctorFinished) - return; + if (_isDisposed) return; - if (visible & !_isActive) + if (_dbusSniTrayIcon?.IsActive ?? false) { - DestroyTrayIcon(); - CreateTrayIcon(); + _dbusSniTrayIcon.SetToolTipText(text); } - else if (!visible & _isActive) + else { - DestroyTrayIcon(); + _xEmbedTrayIcon.SetToolTipText(text); } } - public void SetToolTipText(string? text) - { - if (_isDisposed || text is null) - return; - _tooltipText = text; - _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); - } - } - - /// - /// DBus Object used for setting system tray icons. - /// - /// - /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html - /// - internal class StatusNotifierItemDbusObj : IStatusNotifierItem - { - private readonly StatusNotifierItemProperties _backingProperties; - public event Action? OnTitleChanged; - public event Action? OnIconChanged; - public event Action? OnAttentionIconChanged; - public event Action? OnOverlayIconChanged; - public event Action? OnTooltipChanged; - public Action? NewStatusAsync { get; set; } - public Action? ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + public void SetIsVisible(bool visible) { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); + if (_isDisposed) return; - _backingProperties = new StatusNotifierItemProperties + if (_dbusSniTrayIcon?.IsActive ?? false) { - Menu = dbusmenuPath, // Needs a dbus menu somehow - ToolTip = new ToolTip("") - }; - - InvalidateAll(); - } - - public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; - - public Task ActivateAsync(int x, int y) - { - ActivationDelegate?.Invoke(); - return Task.CompletedTask; - } - - public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - - public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; - - public void InvalidateAll() - { - OnTitleChanged?.Invoke(); - OnIconChanged?.Invoke(); - OnOverlayIconChanged?.Invoke(); - OnAttentionIconChanged?.Invoke(); - OnTooltipChanged?.Invoke(); - } - - public Task WatchNewTitleAsync(Action handler, Action onError) - { - OnTitleChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); - } - - public Task WatchNewIconAsync(Action handler, Action onError) - { - OnIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); - } - - public Task WatchNewAttentionIconAsync(Action handler, Action onError) - { - OnAttentionIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); - } - - public Task WatchNewOverlayIconAsync(Action handler, Action onError) - { - OnOverlayIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); - } - - public Task WatchNewToolTipAsync(Action handler, Action onError) - { - OnTooltipChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); - } - - public Task WatchNewStatusAsync(Action handler, Action onError) - { - NewStatusAsync += handler; - return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); - } - - public Task GetAsync(string prop) => Task.FromResult(new object()); - - public Task GetAllAsync() => Task.FromResult(_backingProperties); - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task WatchPropertiesAsync(Action handler) => - Task.FromResult(Disposable.Empty); - - public void SetIcon(DbusPixmap dbusPixmap) - { - _backingProperties.IconPixmap = new[] { dbusPixmap }; - InvalidateAll(); - } - - public void SetTitleAndTooltip(string? text) - { - if (text is null) - return; - - _backingProperties.Id = text; - _backingProperties.Category = "ApplicationStatus"; - _backingProperties.Status = text; - _backingProperties.Title = text; - _backingProperties.ToolTip = new ToolTip(text); - - InvalidateAll(); - } - } - - [DBusInterface("org.kde.StatusNotifierWatcher")] - internal interface IStatusNotifierWatcher : IDBusObject - { - Task RegisterStatusNotifierItemAsync(string Service); - Task RegisterStatusNotifierHostAsync(string Service); - } - - [DBusInterface("org.kde.StatusNotifierItem")] - internal interface IStatusNotifierItem : IDBusObject - { - Task ContextMenuAsync(int x, int y); - Task ActivateAsync(int x, int y); - Task SecondaryActivateAsync(int x, int y); - Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError); - Task WatchNewIconAsync(Action handler, Action onError); - Task WatchNewAttentionIconAsync(Action handler, Action onError); - Task WatchNewOverlayIconAsync(Action handler, Action onError); - Task WatchNewToolTipAsync(Action handler, Action onError); - Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - // This class is used by Tmds.Dbus to ferry properties - // from the SNI spec. - // Don't change this to actual C# properties since - // Tmds.Dbus will get confused. - internal class StatusNotifierItemProperties - { - public string? Category; - - public string? Id; - - public string? Title; - - public string? Status; - - public ObjectPath Menu; - - public DbusPixmap[]? IconPixmap; - - public ToolTip ToolTip; - } - - internal struct ToolTip - { - public readonly string First; - public readonly DbusPixmap[] Second; - public readonly string Third; - public readonly string Fourth; - - private static readonly DbusPixmap[] s_blank = - { - new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) - }; - - public ToolTip(string message) : this("", s_blank, message, "") - { + _dbusSniTrayIcon.SetIsVisible(visible); + } + else + { + _xEmbedTrayIcon.SetIsVisible(visible); + } } - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + public INativeMenuExporter? MenuExporter { - First = first; - Second = second; - Third = third; - Fourth = fourth; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + return _dbusSniTrayIcon.MenuExporter; + } + else + { + return _xEmbedTrayIcon.MenuExporter; + } + } } - } - - internal readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - public DbusPixmap(int width, int height, byte[] data) + public Action? OnClicked { - Width = width; - Height = height; - Data = data; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + return _dbusSniTrayIcon.OnClicked; + } + else + { + return _xEmbedTrayIcon.OnClicked; + } + } + set + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + _dbusSniTrayIcon.OnClicked = value; + } + else + { + _xEmbedTrayIcon.OnClicked = value; + } + } } } } diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs new file mode 100644 index 0000000000..4b5f0d0a57 --- /dev/null +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Logging; +using Avalonia.Platform; + +namespace Avalonia.X11 +{ + internal class XEmbedTrayIconImpl + { + public XEmbedTrayIconImpl() + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); + } + + public void Dispose() + { + } + + public void SetIcon(IWindowIconImpl? icon) + { + } + + public void SetToolTipText(string? text) + { + } + + public void SetIsVisible(bool visible) + { + } + + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } + } +} From c4b0b99027491c78988a81559244eddc99151e4c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 15:05:35 +0800 Subject: [PATCH 240/311] Gracefully handle tray service restarts --- .../DbusSNITrayIconImpl.cs | 129 +++++++++++++----- src/Avalonia.X11/X11TrayIconImpl.cs | 11 +- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 16 ++- 3 files changed, 113 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs index 1fb74f132a..6ca05efe50 100644 --- a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -6,49 +6,55 @@ using System.Reactive.Disposables; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop; using Avalonia.Logging; -using Avalonia.Platform; using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] namespace Avalonia.FreeDesktop { - public class DbusSNITrayIconImpl + public class DbusSNITrayIconImpl { - private static int s_trayIconInstanceId; + private static int s_trayIconInstanceId = 0; private readonly ObjectPath _dbusMenuPath; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; private DbusPixmap _icon; - private IStatusNotifierWatcher? _statusNotifierWatcher; - private string? _sysTrayServiceName; private string? _tooltipText; - private bool _isActive; private bool _isDisposed; - private readonly bool _ctorFinished; + private bool _serviceConnected; + private readonly IDisposable _serviceWatchDisposable; + private bool _isVisible; public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public bool IsActive => _isActive; - - public DbusSNITrayIconImpl(Connection connection) + public bool IsActive => _serviceConnected; + + public DbusSNITrayIconImpl() { - _connection = connection; + _connection = DBusHelper.TryGetConnection(); + + if (_connection is null) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, "Unable to get a dbus connection for system tray icons."); + + return; + } + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); + InitializeSNWService(); CreateTrayIcon(); - _ctorFinished = true; + _serviceWatchDisposable = Watch(); } - public async void CreateTrayIcon() + private void InitializeSNWService() { - if (_connection is null) - return; + if (_connection is null || _isDisposed) return; try { @@ -61,38 +67,83 @@ namespace Avalonia.FreeDesktop Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + + return; } - if (_statusNotifierWatcher is null) + _serviceConnected = true; + } + + + private async Task Watch() => + await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + + + private void OnNameChange(ServiceOwnerChangedEventArgs obj) + { + if (_isDisposed) return; + if (!_serviceConnected & obj.NewOwner != null) + { + _serviceConnected = true; + + if (_isVisible) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else + { + DestroyTrayIcon(); + } + } + else if (_serviceConnected & obj.NewOwner is null) + { + s_trayIconInstanceId = 0; + _serviceConnected = false; + } + } + + public void CreateTrayIcon() + { + if (_connection is null || !_serviceConnected || _isDisposed) + return; + + var pid = Process.GetCurrentProcess().Id; var tid = s_trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - - await _connection.RegisterServiceAsync(_sysTrayServiceName); - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + try + { + _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + _connection.RegisterServiceAsync(_sysTrayServiceName); + _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + } + catch (Exception e) + { + _serviceConnected = false; + } _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; + _isVisible = true; } - public async void DestroyTrayIcon() + public void DestroyTrayIcon() { - if (_connection is null) + if (_connection is null || !_serviceConnected || _isDisposed) return; + _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; + _connection.UnregisterServiceAsync(_sysTrayServiceName); + _isVisible = false; } public void Dispose() @@ -100,12 +151,13 @@ namespace Avalonia.FreeDesktop _isDisposed = true; DestroyTrayIcon(); _connection?.Dispose(); + _serviceWatchDisposable?.Dispose(); } public void SetIcon(UIntPtr[] x11iconData) { if (_isDisposed) - return; + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -128,15 +180,15 @@ namespace Avalonia.FreeDesktop public void SetIsVisible(bool visible) { - if (_isDisposed || !_ctorFinished) + if (_isDisposed) return; - if (visible & !_isActive) + if (visible && !_isVisible) { DestroyTrayIcon(); CreateTrayIcon(); } - else if (!visible & _isActive) + else if (!visible && _isVisible) { DestroyTrayIcon(); } @@ -239,7 +291,20 @@ namespace Avalonia.FreeDesktop return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); } - public Task GetAsync(string prop) => Task.FromResult(new object()); + public async Task GetAsync(string prop) + { + return prop switch + { + nameof(_backingProperties.Category) => _backingProperties.Category, + nameof(_backingProperties.Id) => _backingProperties.Id, + nameof(_backingProperties.Menu) => _backingProperties.Menu, + nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, + nameof(_backingProperties.Status) => _backingProperties.Status, + nameof(_backingProperties.Title) => _backingProperties.Title, + nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, + _ => null + }; + } public Task GetAllAsync() => Task.FromResult(_backingProperties); diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index ca8ed8ec35..9e03dcd604 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -11,16 +11,7 @@ namespace Avalonia.X11 public X11TrayIconImpl() { _xEmbedTrayIcon = new XEmbedTrayIconImpl(); - - var _connection = DBusHelper.TryGetConnection(); - - if (_connection is null) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) - ?.Log(this, "Unable to get a dbus connection for system tray icons."); - } - - _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); + _dbusSniTrayIcon = new DbusSNITrayIconImpl(); } private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index 4b5f0d0a57..c2247565be 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -9,25 +9,39 @@ namespace Avalonia.X11 { public XEmbedTrayIconImpl() { + } + + private bool IsCalled; + + private void NotImplemented() + { + if(IsCalled) return; + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); - } + IsCalled = true; + } + public void Dispose() { + NotImplemented(); } public void SetIcon(IWindowIconImpl? icon) { + NotImplemented(); } public void SetToolTipText(string? text) { + NotImplemented(); } public void SetIsVisible(bool visible) { + NotImplemented(); } public INativeMenuExporter? MenuExporter { get; } From ebd1f5366739447e2d11ff94ecb840d81040c865 Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Fri, 22 Oct 2021 12:42:29 +0200 Subject: [PATCH 241/311] DataGrid minimum distance threshold when dragging headers --- .../DataGridColumnHeader.cs | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 85fd55800a..915b36687c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -35,6 +35,7 @@ namespace Avalonia.Controls private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5; private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; + private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5; private bool _areHandlersSuspended; private static DragMode _dragMode; @@ -448,19 +449,6 @@ namespace Avalonia.Controls OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight); - // if we still haven't done anything about moving the mouse while - // the button is down, we remember that we're dragging, but we don't - // claim to have actually handled the event - if (_dragMode == DragMode.MouseDown) - { - _dragMode = DragMode.Drag; - } - - _lastMousePositionHeaders = mousePositionHeaders; - - if (args.Pointer.Captured != this && _dragMode == DragMode.Drag) - args.Pointer.Capture(this); - SetDragCursor(mousePosition); } @@ -732,15 +720,19 @@ namespace Avalonia.Controls { return; } - + //handle entry into reorder mode - if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) + if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) { - handled = CanReorderColumn(OwningColumn); - - if (handled) + var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders); + if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold) { - OnMouseMove_BeginReorder(mousePosition); + handled = CanReorderColumn(OwningColumn); + + if (handled) + { + OnMouseMove_BeginReorder(mousePosition); + } } } From f3abb8ed64506cc32ece834ee558319d354e27f7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Oct 2021 17:48:12 +0200 Subject: [PATCH 242/311] Display access key in Buttons. Set `RecognizesAccessKey` on button content presenter. --- samples/ControlCatalog/Pages/ButtonPage.xaml | 2 +- src/Avalonia.Themes.Default/Button.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/Button.xaml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index be114bbbc9..b35c112a68 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml index 81d96aaa14..da36abe7ec 100644 --- a/src/Avalonia.Themes.Default/Button.xaml +++ b/src/Avalonia.Themes.Default/Button.xaml @@ -17,6 +17,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" + RecognizesAccessKey="True" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 53d53ef127..533fabfb44 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -34,6 +34,7 @@ Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" + RecognizesAccessKey="True" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> From 90e43897ee1473fa0b20395edc35f4133ae4915e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Oct 2021 17:48:31 +0200 Subject: [PATCH 243/311] Add access keys for menu items. --- samples/ControlCatalog/MainWindow.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index ee42e7a54b..375345f64e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -63,11 +63,11 @@ - - + + - - + + From b663afe06b585bbd776dc14850f984c10d233a52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Oct 2021 17:56:44 +0200 Subject: [PATCH 244/311] Make sure underline is drawn within bounds. --- src/Avalonia.Controls/Primitives/AccessText.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index c42c6f100c..3c82386991 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls.Primitives if (underscore != -1 && ShowAccessKey) { var rect = TextLayout.HitTestTextPosition(underscore); - var offset = new Vector(0, -0.5); + var offset = new Vector(0, -1.5); context.DrawLine( new Pen(Foreground, 1), rect.BottomLeft + offset, From fbfc1e4eb0ba33d32b60ec5879d4d6b5a25f4267 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 22 Oct 2021 16:57:11 +0100 Subject: [PATCH 245/311] restore osx window shadow fix. --- native/Avalonia.Native/src/OSX/window.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index bd93de0e78..26c065fe11 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -52,7 +52,6 @@ public: [Window setBackingType:NSBackingStoreBuffered]; [Window setOpaque:false]; - [Window setContentView: StandardContainer]; } virtual HRESULT ObtainNSWindowHandle(void** ret) override @@ -125,6 +124,8 @@ public: SetPosition(lastPositionSet); UpdateStyle(); + [Window setContentView: StandardContainer]; + [Window setTitle:_lastTitle]; if(ShouldTakeFocusOnShow() && activate) @@ -323,6 +324,7 @@ public: BaseEvents->Resized(AvnSize{x,y}, reason); } + [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; } @finally From f98070fb4dcc281f85fa51a256d9762c772f3388 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Oct 2021 14:37:48 +0200 Subject: [PATCH 246/311] Display access key in checkbox. --- src/Avalonia.Themes.Default/CheckBox.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Themes.Default/CheckBox.xaml b/src/Avalonia.Themes.Default/CheckBox.xaml index 5e10b319a7..75d6f853be 100644 --- a/src/Avalonia.Themes.Default/CheckBox.xaml +++ b/src/Avalonia.Themes.Default/CheckBox.xaml @@ -41,6 +41,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" + RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsVisible="{TemplateBinding Content, Converter={x:Static ObjectConverters.IsNotNull}}" diff --git a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml index 7969cec947..ef28593711 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml @@ -44,6 +44,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" + RecognizesAccessKey="True" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Grid.Column="1" /> From d0037f1df50ddc33dacbe1639f8ab2aa6fa29ed9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Oct 2021 14:38:54 +0200 Subject: [PATCH 247/311] Display access key in ControlCatalog pages. --- .../ControlCatalog/Pages/CheckBoxPage.xaml | 6 +++--- samples/ControlCatalog/Pages/DialogsPage.xaml | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index 1359cfa2ef..769ef26699 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -11,9 +11,9 @@ Spacing="16"> - Unchecked - Checked - Indeterminate + _Unchecked + _Checked + _Indeterminate Disabled Use filters - - - - - - - - - - + + + + + + + + + + From 7f89c2a0dc97cea114b3315db9914a9d5c9115c0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Oct 2021 14:38:57 +0200 Subject: [PATCH 248/311] Make button respond to access key. --- src/Avalonia.Controls/Button.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 614a18c6b5..8b22cdd4ec 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -99,6 +99,7 @@ namespace Avalonia.Controls CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged); + AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler