From ca4dee94fd2ab4884031072da4a92c9f00a5fcd2 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:00:04 -0400 Subject: [PATCH 001/440] Remove timer from undo helper, improve u/r for textbox --- src/Avalonia.Controls/TextBox.cs | 57 ++++++++++++------- src/Avalonia.Controls/Utils/UndoRedoHelper.cs | 11 +--- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1bee15bccd..31ec12c22f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -31,7 +31,7 @@ namespace Avalonia.Controls public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current .GetService()?.Paste.FirstOrDefault(); - + public static readonly StyledProperty AcceptsReturnProperty = AvaloniaProperty.Register(nameof(AcceptsReturn)); @@ -117,7 +117,7 @@ namespace Avalonia.Controls public static readonly StyledProperty RevealPasswordProperty = AvaloniaProperty.Register(nameof(RevealPassword)); - + public static readonly DirectProperty CanCutProperty = AvaloniaProperty.RegisterDirect( nameof(CanCut), @@ -135,7 +135,7 @@ namespace Avalonia.Controls public static readonly StyledProperty IsUndoEnabledProperty = AvaloniaProperty.Register( - nameof(IsUndoEnabled), + nameof(IsUndoEnabled), defaultValue: true); public static readonly DirectProperty UndoLimitProperty = @@ -174,6 +174,10 @@ namespace Avalonia.Controls private string _newLine = Environment.NewLine; private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; + private int _selectedTextChangesMadeSinceLastUndoSnapshot; + private bool _hasDoneSnapshotOnce; + private const int _maxCharsBeforeUndoSnapshot = 7; + static TextBox() { FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); @@ -202,7 +206,8 @@ namespace Avalonia.Controls horizontalScrollBarVisibility, BindingPriority.Style); _undoRedoHelper = new UndoRedoHelper(this); - + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; UpdatePseudoclasses(); } @@ -331,6 +336,7 @@ namespace Avalonia.Controls if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) { _undoRedoHelper.Clear(); + SnapshotUndoRedo(); // so we always have an initial state } } } @@ -341,7 +347,6 @@ namespace Avalonia.Controls get { return GetSelection(); } set { - SnapshotUndoRedo(); if (string.IsNullOrEmpty(value)) { DeleteSelection(); @@ -350,7 +355,6 @@ namespace Avalonia.Controls { HandleTextInput(value); } - SnapshotUndoRedo(); } } @@ -422,7 +426,7 @@ namespace Avalonia.Controls get { return _newLine; } set { SetAndRaise(NewLineProperty, ref _newLine, value); } } - + /// /// Clears the current selection, maintaining the /// @@ -480,11 +484,13 @@ namespace Avalonia.Controls var oldValue = _undoRedoHelper.Limit; _undoRedoHelper.Limit = value; RaisePropertyChanged(UndoLimitProperty, oldValue, value); - } + } // from docs at // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // "Setting UndoLimit clears the undo queue." _undoRedoHelper.Clear(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; } } @@ -515,6 +521,8 @@ namespace Avalonia.Controls // Therefore, if you disable undo and then re-enable it, undo commands still do not work // because the undo stack was emptied when you disabled undo." _undoRedoHelper.Clear(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = false; } } @@ -577,23 +585,25 @@ namespace Avalonia.Controls { return; } - + input = RemoveInvalidCharacters(input); - + if (string.IsNullOrEmpty(input)) { return; } - + _selectedTextChangesMadeSinceLastUndoSnapshot++; + SnapshotUndoRedo(ignoreChangeCount: false); + string text = Text ?? string.Empty; int caretIndex = CaretIndex; int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); - + if (MaxLength > 0 && newLength > MaxLength) { input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); } - + if (!string.IsNullOrEmpty(input)) { DeleteSelection(); @@ -696,6 +706,7 @@ namespace Avalonia.Controls { try { + SnapshotUndoRedo(); _isUndoingRedoing = true; _undoRedoHelper.Undo(); } @@ -830,7 +841,6 @@ namespace Avalonia.Controls CaretIndex -= removedCharacters; ClearSelection(); } - SnapshotUndoRedo(); handled = true; break; @@ -858,7 +868,6 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); } - SnapshotUndoRedo(); handled = true; break; @@ -868,7 +877,6 @@ namespace Avalonia.Controls { SnapshotUndoRedo(); HandleTextInput(NewLine); - SnapshotUndoRedo(); handled = true; } @@ -879,7 +887,6 @@ namespace Avalonia.Controls { SnapshotUndoRedo(); HandleTextInput("\t"); - SnapshotUndoRedo(); handled = true; } else @@ -889,6 +896,10 @@ namespace Avalonia.Controls break; + case Key.Space: + SnapshotUndoRedo(); // always snapshot in between words + break; + default: handled = false; break; @@ -1306,11 +1317,19 @@ namespace Avalonia.Controls } } - private void SnapshotUndoRedo() + private void SnapshotUndoRedo(bool ignoreChangeCount = true) { if (IsUndoEnabled) { - _undoRedoHelper.Snapshot(); + if (ignoreChangeCount || + !_hasDoneSnapshotOnce || + (!ignoreChangeCount && + _selectedTextChangesMadeSinceLastUndoSnapshot >= _maxCharsBeforeUndoSnapshot)) + { + _undoRedoHelper.Snapshot(); + _selectedTextChangesMadeSinceLastUndoSnapshot = 0; + _hasDoneSnapshotOnce = true; + } } } } diff --git a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs index 7374f20a0c..fd1ca54b57 100644 --- a/src/Avalonia.Controls/Utils/UndoRedoHelper.cs +++ b/src/Avalonia.Controls/Utils/UndoRedoHelper.cs @@ -7,7 +7,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Utils { - class UndoRedoHelper : WeakTimer.IWeakTimerSubscriber where TState : struct, IEquatable + class UndoRedoHelper { private readonly IUndoRedoHost _host; @@ -31,7 +31,6 @@ namespace Avalonia.Controls.Utils public UndoRedoHelper(IUndoRedoHost host) { _host = host; - WeakTimer.StartWeakTimer(this, TimeSpan.FromSeconds(1)); } public void Undo() @@ -61,7 +60,7 @@ namespace Avalonia.Controls.Utils if (_states.Last != null) { _states.Last.Value = state; - } + } } public void UpdateLastState() @@ -103,11 +102,5 @@ namespace Avalonia.Controls.Utils _states.Clear(); _currentNode = null; } - - bool WeakTimer.IWeakTimerSubscriber.Tick() - { - Snapshot(); - return true; - } } } From 2c215d1b3917af0c31e55cb22e2ba07db203b535 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:02:29 -0400 Subject: [PATCH 002/440] Make sure to snapshot undo before delete selected --- src/Avalonia.Controls/TextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 31ec12c22f..23c0d70f20 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -349,6 +349,8 @@ namespace Avalonia.Controls { if (string.IsNullOrEmpty(value)) { + _selectedTextChangesMadeSinceLastUndoSnapshot++; + SnapshotUndoRedo(ignoreChangeCount: false); DeleteSelection(); } else From 11138b1fc5937bb268460b36dd259a64c6791caf Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:05:02 -0400 Subject: [PATCH 003/440] Remove extra snapshots after cut, paste --- src/Avalonia.Controls/TextBox.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 23c0d70f20..dad2401921 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -639,7 +639,6 @@ namespace Avalonia.Controls SnapshotUndoRedo(); Copy(); DeleteSelection(); - SnapshotUndoRedo(); } public async void Copy() @@ -659,7 +658,6 @@ namespace Avalonia.Controls SnapshotUndoRedo(); HandleTextInput(text); - SnapshotUndoRedo(); } protected override void OnKeyDown(KeyEventArgs e) From 2fe25f8acf3567ad1869942eb20c6bc068b44387 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:48:12 -0400 Subject: [PATCH 004/440] Fix equality for UndoRedoState In UndoRedoHelper, the .Equals call in Snapshot() did not call Equals(UndoRedoState other). It called the default Equals(object obj) implementation. This could throw of the undo redo snapshotting. Fix that by overriding Equals(object obj). --- src/Avalonia.Controls/TextBox.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index dad2401921..edd268a86a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -157,6 +157,13 @@ namespace Avalonia.Controls } public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (obj.GetType() != GetType()) return false; + return Equals((UndoRedoState)obj); + } } private string _text; From f92804ff05b6361a0e355ad894e59c453927c6c7 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Tue, 8 Jun 2021 18:57:18 -0400 Subject: [PATCH 005/440] Improve UndoRedoState.Equals(object obj) impl --- src/Avalonia.Controls/TextBox.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index edd268a86a..b89bd22038 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -158,12 +158,9 @@ namespace Avalonia.Controls public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (obj.GetType() != GetType()) return false; - return Equals((UndoRedoState)obj); - } + public override bool Equals(object obj) => obj is UndoRedoState other && Equals(other); + + public override int GetHashCode() => Text.GetHashCode(); } private string _text; From b85bba7d2dbb912cfa88cc9b1bd3ace7ce775cb3 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 30 Jun 2021 18:51:18 +0200 Subject: [PATCH 006/440] fixes: Dialog cancel consistency on windows --- src/Windows/Avalonia.Win32/SystemDialogImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index ad81cc1778..f595a58c91 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -19,7 +19,7 @@ namespace Avalonia.Win32 var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { - var result = Array.Empty(); + string[] result = default; Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; @@ -100,7 +100,7 @@ namespace Avalonia.Win32 { return Task.Factory.StartNew(() => { - string result = string.Empty; + string result = default; var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; @@ -164,7 +164,7 @@ namespace Avalonia.Win32 } } } - return ""; + return default; } } } From 05361cdb748a29d67d480246702544165fa1c0bd Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Mon, 5 Jul 2021 15:56:34 +0200 Subject: [PATCH 007/440] Adding option to inspect Popup visual tree --- .../Diagnostics/ViewModels/MainViewModel.cs | 1 - .../Diagnostics/ViewModels/VisualTreeNode.cs | 30 ++++-- .../Diagnostics/Views/MainWindow.xaml.cs | 91 ++++++++++++------- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 3f367165ac..07ae222a9c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel; - using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 48fa636664..b9981babf7 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections; +using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; @@ -24,8 +25,7 @@ namespace Avalonia.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { - var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); + return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); } internal class VisualTreeNodeCollection : TreeNodeCollection @@ -46,10 +46,28 @@ namespace Avalonia.Diagnostics.ViewModels protected override void Initialize(AvaloniaList nodes) { - _subscription = _control.VisualChildren.ForEachItem( - (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), - (i, item) => nodes.RemoveAt(i), - () => nodes.Clear()); + if (_control is Popup p) + { + _subscription = p.GetObservable(Popup.ChildProperty).Subscribe(child => + { + if (child != null) + { + nodes.Add(new VisualTreeNode(child, Owner)); + } + else + { + nodes.Clear(); + } + + }); + } + else + { + _subscription = _control.VisualChildren.ForEachItem( + (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index d1232b749a..9d37b5acdc 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -27,19 +27,19 @@ namespace Avalonia.Diagnostics.Views EventHandler? lh = default; lh = (s, e) => - { - this.Opened -= lh; - if ((DataContext as MainViewModel)?.StartupScreenIndex is int index) - { - var screens = this.Screens; - if (index > -1 && index < screens.ScreenCount) - { - var screen = screens.All[index]; - this.Position = screen.Bounds.TopLeft; - this.WindowState = WindowState.Maximized; - } - } - }; + { + this.Opened -= lh; + if ((DataContext as MainViewModel)?.StartupScreenIndex is { } index) + { + var screens = this.Screens; + if (index > -1 && index < screens.ScreenCount) + { + var screen = screens.All[index]; + this.Position = screen.Bounds.TopLeft; + this.WindowState = WindowState.Maximized; + } + } + }; this.Opened += lh; } @@ -91,6 +91,24 @@ namespace Avalonia.Diagnostics.Views AvaloniaXamlLoader.Load(this); } + private IControl? GetHoveredControl(TopLevel topLevel) + { +#pragma warning disable CS0618 // Type or member is obsolete + var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default; +#pragma warning restore CS0618 // Type or member is obsolete + + return (IControl?)topLevel.GetVisualsAt(point, x => + { + if (x is AdornerLayer || !x.IsVisible) + { + return false; + } + + return !(x is IInputElement ie) || ie.IsHitTestVisible; + }) + .FirstOrDefault(); + } + private void RawKeyDown(RawKeyEventArgs e) { var vm = (MainViewModel?)DataContext; @@ -99,34 +117,39 @@ namespace Avalonia.Diagnostics.Views return; } - const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; - - if (e.Modifiers == modifiers) + switch (e.Modifiers) { -#pragma warning disable CS0618 // Type or member is obsolete - var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; -#pragma warning restore CS0618 // Type or member is obsolete + case RawInputModifiers.Control | RawInputModifiers.Shift: + { + IControl? control = null; - var control = Root.GetVisualsAt(point, x => + foreach (var popup in Root.GetVisualDescendants().OfType()) { - if (x is AdornerLayer || !x.IsVisible) return false; - if (!(x is IInputElement ie)) return true; - return ie.IsHitTestVisible; - }) - .FirstOrDefault(); + if (popup.Host?.HostedVisualTreeRoot is PopupRoot popupRoot) + { + control = GetHoveredControl(popupRoot); + + if (control != null) + { + break; + } + } + } - if (control != null) - { - vm.SelectControl((IControl)control); + control ??= GetHoveredControl(Root); + + if (control != null) + { + vm.SelectControl(control); + } + + break; } - } - else if (e.Modifiers == RawInputModifiers.Alt) - { - if (e.Key == Key.S || e.Key == Key.D) + case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D: { - var enable = e.Key == Key.S; + vm.EnableSnapshotStyles(e.Key == Key.S); - vm.EnableSnapshotStyles(enable); + break; } } } From 31d80a9fb67165417d327e008190bb0344563aae Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 6 Jul 2021 10:30:24 +0200 Subject: [PATCH 008/440] Make tree roots bold --- .../Diagnostics/ViewModels/TreeNode.cs | 25 ++++++------------- .../Diagnostics/Views/TreePageView.xaml | 2 +- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 4cb470eeac..14d704db93 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -4,24 +4,28 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Controls; +using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { internal abstract class TreeNode : ViewModelBase, IDisposable { - private IDisposable? _classesSubscription; + private readonly IDisposable? _classesSubscription; private string _classes; private bool _isExpanded; - public TreeNode(IVisual visual, TreeNode? parent) + protected TreeNode(IVisual visual, TreeNode? parent) { Parent = parent; Type = visual.GetType().Name; Visual = visual; _classes = string.Empty; + FontWeight = Visual is TopLevel or Popup ? FontWeight.Bold : FontWeight.Normal; + if (visual is IControl control) { ElementName = control.Name; @@ -52,6 +56,8 @@ namespace Avalonia.Diagnostics.ViewModels } } + public FontWeight FontWeight { get; } + public abstract TreeNodeCollection Children { get; @@ -95,20 +101,5 @@ namespace Avalonia.Diagnostics.ViewModels _classesSubscription?.Dispose(); Children.Dispose(); } - - private static int IndexOf(IReadOnlyList collection, TreeNode item) - { - var count = collection.Count; - - for (var i = 0; i < count; ++i) - { - if (collection[i] == item) - { - return i; - } - } - - throw new AvaloniaInternalException("TreeNode was not present in parent Children collection."); - } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index a5328716fc..bb661f7f4c 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -11,7 +11,7 @@ - + From cd56e1a9fc6e914571b2b38ab042fcc0275feaeb Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 6 Jul 2021 10:40:07 +0200 Subject: [PATCH 009/440] Remove "or" --- src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 14d704db93..f4c04dbca6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Diagnostics.ViewModels Visual = visual; _classes = string.Empty; - FontWeight = Visual is TopLevel or Popup ? FontWeight.Bold : FontWeight.Normal; + FontWeight = Visual is TopLevel || Visual is Popup ? FontWeight.Bold : FontWeight.Normal; if (visual is IControl control) { From 97c33f432a32b6a5f6f5bc0273e6bbd29089caf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Marki=C4=8D?= Date: Sat, 10 Jul 2021 15:46:08 +0200 Subject: [PATCH 010/440] Applies spell checking on mostly comments, but also non public members and arguments --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- src/Avalonia.Base/AvaloniaProperty.cs | 4 ++-- .../AvaloniaPropertyChangedEventArgs.cs | 4 ++-- .../Collections/Pooled/PooledList.cs | 2 +- .../Data/Core/Plugins/IDataValidationPlugin.cs | 2 +- src/Avalonia.Base/DirectPropertyBase.cs | 6 +++--- src/Avalonia.Base/DirectPropertyMetadata`1.cs | 2 +- .../Collections/DataGridCollectionView.cs | 8 ++++---- src/Avalonia.Controls.DataGrid/DataGrid.cs | 2 +- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 2 +- .../DataGridCellCoordinates.cs | 2 +- .../DataGridClipboard.cs | 2 +- .../DataGridColumn.cs | 2 +- .../DataGridDisplayData.cs | 8 ++++---- src/Avalonia.Controls.DataGrid/DataGridRow.cs | 6 +++--- src/Avalonia.Controls.DataGrid/DataGridRows.cs | 18 +++++++++--------- .../DataGridColumnHeadersPresenter.cs | 2 +- src/Avalonia.Controls/AppBuilderBase.cs | 2 +- .../Converters/MarginMultiplierConverter.cs | 10 +++++----- .../MenuScrollingVisibilityConverter.cs | 4 ++-- .../DateTimePickers/DatePicker.cs | 2 +- .../DateTimePickers/DateTimePickerPanel.cs | 12 ++++++------ src/Avalonia.Controls/DefinitionBase.cs | 8 ++++---- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 2 +- src/Avalonia.Controls/Grid.cs | 18 +++++++++--------- src/Avalonia.Controls/ListBox.cs | 2 +- .../NativeMenuItemSeparator.cs | 2 +- .../Platform/ExtendClientAreaChromeHints.cs | 2 +- .../PopupPositioning/ManagedPopupPositioner.cs | 2 +- src/Avalonia.Controls/Primitives/RangeBase.cs | 2 +- .../Primitives/SelectingItemsControl.cs | 2 +- .../Repeater/IElementFactory.cs | 2 +- .../Repeater/ItemsRepeater.cs | 4 ++-- src/Avalonia.Controls/Repeater/ViewManager.cs | 2 +- .../Repeater/ViewportManager.cs | 2 +- src/Avalonia.Controls/ScrollViewer.cs | 2 +- .../Selection/SelectionModel.cs | 2 +- src/Avalonia.Controls/TickBar.cs | 8 ++++---- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Controls/WindowBase.cs | 2 +- src/Avalonia.Input/Cursor.cs | 2 +- .../ScrollGestureRecognizer.cs | 2 +- src/Avalonia.Input/ICommandSource.cs | 2 +- .../TextInput/ITextInputMethodClient.cs | 4 ++-- src/Avalonia.Interactivity/Interactive.cs | 2 +- src/Avalonia.Layout/ElementManager.cs | 2 +- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 4 ++-- .../AvaloniaNativeMenuExporter.cs | 4 ++-- src/Avalonia.Native/IAvnMenu.cs | 8 ++++---- src/Avalonia.Native/IAvnMenuItem.cs | 6 +++--- .../Unicode/LineBreakEnumerator.cs | 2 +- .../Transformation/InterpolationUtilities.cs | 10 +++++----- 52 files changed, 109 insertions(+), 109 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 6a9cff6b71..ce5b37043f 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -861,7 +861,7 @@ namespace Avalonia } /// - /// Logs a mesage if the notification represents a binding error. + /// Logs a message if the notification represents a binding error. /// /// The property being bound. /// The binding notification. diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 5117fdb170..94aefb8869 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -465,9 +465,9 @@ namespace Avalonia /// Uses the visitor pattern to resolve an untyped property to a typed property. /// /// The type of user data passed. - /// The visitor which will accept the typed property. + /// The visitor which will accept the typed property. /// The user data to pass. - public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + public abstract void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) where TData : struct; /// diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs index c1a2832fde..896d86e29d 100644 --- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs +++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs @@ -58,8 +58,8 @@ namespace Avalonia /// /// This will usually be true, except in /// - /// which recieves notifications for all changes to property values, whether a value with a higher - /// priority is present or not. When this property is false, the change that is being signalled + /// which receives notifications for all changes to property values, whether a value with a higher + /// priority is present or not. When this property is false, the change that is being signaled /// has not resulted in a change to the property value on the object. /// public bool IsEffectiveValueChange { get; private set; } diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index e50e100d32..2cd9758f12 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1271,7 +1271,7 @@ namespace Avalonia.Collections.Pooled /// Reverses the elements in a range of this list. Following a call to this /// method, an element in the range given by index and count /// which was previously located at index i will now be located at - /// index index + (index + count - i - 1). + /// index + (index + count - i - 1). /// public void Reverse(int index, int count) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 324279e9f0..2a580fe75f 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -20,7 +20,7 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the object. /// The property name. - /// The inner property accessor used to aceess the property. + /// The inner property accessor used to access the property. /// /// An interface through which future interactions with the /// property will be made. diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index e6cc1edfdf..a057ad2254 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -13,7 +13,7 @@ namespace Avalonia /// The type of the property's value. /// /// Whereas is typed on the owner type, this base - /// class provides a non-owner-typed interface to a direct poperty. + /// class provides a non-owner-typed interface to a direct property. /// public abstract class DirectPropertyBase : AvaloniaProperty { @@ -123,9 +123,9 @@ namespace Avalonia } /// - public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data) + public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) { - vistor.Visit(this, ref data); + visitor.Visit(this, ref data); } /// diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 205967984d..eabdef05ed 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -38,7 +38,7 @@ namespace Avalonia /// /// Data validation is validation performed at the target of a binding, for example in a /// view model using the INotifyDataErrorInfo interface. Only certain properties on a - /// control (such as a TextBox's Text property) will be interested in recieving data + /// control (such as a TextBox's Text property) will be interested in receiving data /// validation messages so this feature must be explicitly enabled by setting this flag. /// public bool? EnableDataValidation { get; private set; } diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs index 5b1e43b8f4..fe6acdc532 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs @@ -877,7 +877,7 @@ namespace Avalonia.Collections if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) { // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a refernce while paging. + // then create it so that we can use it as a reference while paging. if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count) { PrepareTemporaryGroups(); @@ -889,7 +889,7 @@ namespace Avalonia.Collections else if (IsGrouping) { // if the temporaryGroup was not created yet and is out of sync - // then create it so that we can use it as a refernce while paging. + // then create it so that we can use it as a reference while paging. if (_temporaryGroup.ItemCount != InternalList.Count) { // update the groups that get created for the @@ -1951,7 +1951,7 @@ namespace Avalonia.Collections EnsureCollectionInSync(); VerifyRefreshNotDeferred(); - // for indicies larger than the count + // for indices larger than the count if (index >= Count || index < 0) { throw new ArgumentOutOfRangeException("index"); @@ -3800,7 +3800,7 @@ namespace Avalonia.Collections /// /// /// This method can be called from a constructor - it does not call - /// any virtuals. The 'count' parameter is substitute for the real Count, + /// any virtuals. The 'count' parameter is substitute for the real Count, /// used only when newItem is null. /// In that case, this method sets IsCurrentAfterLast to true if and only /// if newPosition >= count. This distinguishes between a null belonging diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 83f13fe199..21585bd9fc 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5731,7 +5731,7 @@ namespace Avalonia.Controls { if (SelectionMode == DataGridSelectionMode.Single || !ctrl) { - // Unselect the currectly selected rows except the new selected row + // Unselect the currently selected rows except the new selected row action = DataGridSelectionAction.SelectCurrent; } else diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 445dc541a7..0de4612958 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -197,7 +197,7 @@ namespace Avalonia.Controls } // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the - // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column + // right gridline will be collapsed if this cell belongs to the lastVisibleColumn and there is no filler column internal void EnsureGridLine(DataGridColumn lastVisibleColumn) { if (OwningGrid != null && _rightGridLine != null) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs index 2f723154be..26f0b53952 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls return false; } - // There is build warning if this is missiing + // There is build warning if this is missing public override int GetHashCode() { return base.GetHashCode(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs index a4bab8b304..ee69f1c768 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs @@ -189,7 +189,7 @@ namespace Avalonia.Controls } /// - /// DataGrid row item used for proparing the ClipboardRowContent. + /// DataGrid row item used for preparing the ClipboardRowContent. /// public object Item { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 9141fb2463..0849f48686 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -787,7 +787,7 @@ namespace Avalonia.Controls } /// - /// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to + /// If the DataGrid is using layout rounding, the pixel snapping will force all widths to /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal /// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some /// pixel gaps and/or overlaps between columns. diff --git a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs index e659438b43..2b8055dd22 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls set; } - internal void AddRecylableRow(DataGridRow row) + internal void AddRecyclableRow(DataGridRow row) { Debug.Assert(!_recyclableRows.Contains(row)); row.DetachFromDataGrid(true); @@ -120,7 +120,7 @@ namespace Avalonia.Controls { if (row.IsRecyclable) { - AddRecylableRow(row); + AddRecyclableRow(row); } else { @@ -193,7 +193,7 @@ namespace Avalonia.Controls internal void FullyRecycleElements() { - // Fully recycle Recycleable rows and transfer them to Recycled rows + // Fully recycle Recyclable rows and transfer them to Recycled rows while (_recyclableRows.Count > 0) { DataGridRow row = _recyclableRows.Pop(); @@ -202,7 +202,7 @@ namespace Avalonia.Controls Debug.Assert(!_fullyRecycledRows.Contains(row)); _fullyRecycledRows.Push(row); } - // Fully recycle Recycleable GroupHeaders and transfer them to Recycled GroupHeaders + // Fully recycle Recyclable GroupHeaders and transfer them to Recycled GroupHeaders while (_recyclableGroupHeaders.Count > 0) { DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index c3562c53a4..7546970498 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -392,7 +392,7 @@ namespace Avalonia.Controls set; } - // Height that the row will eventually end up at after a possible detalis animation has completed + // Height that the row will eventually end up at after a possible details animation has completed internal double TargetHeight { get @@ -517,7 +517,7 @@ namespace Avalonia.Controls return base.MeasureOverride(availableSize); } - //Allow the DataGrid specific componets to adjust themselves based on new values + //Allow the DataGrid specific components to adjust themselves based on new values if (_headerElement != null) { _headerElement.InvalidateMeasure(); @@ -722,7 +722,7 @@ namespace Avalonia.Controls if (_bottomGridLine != null) { // It looks like setting Visibility sometimes has side effects so make sure the value is actually - // diffferent before setting it + // different before setting it bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All; if (newVisibility != _bottomGridLine.IsVisible) diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index 4bfbd7d818..1d5c899993 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -1193,7 +1193,7 @@ namespace Avalonia.Controls else { groupHeader = element as DataGridRowGroupHeader; - Debug.Assert(groupHeader != null); // Nothig other and Rows and RowGroups now + Debug.Assert(groupHeader != null); // Nothing other and Rows and RowGroups now if (groupHeader != null) { groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1]; @@ -1636,7 +1636,7 @@ namespace Avalonia.Controls if (slot >= DisplayData.FirstScrollingSlot && slot <= DisplayData.LastScrollingSlot) { - // Additional row takes the spot of a displayed row - it is necessarilly displayed + // Additional row takes the spot of a displayed row - it is necessarily displayed return true; } else if (DisplayData.FirstScrollingSlot == -1 && @@ -1825,7 +1825,7 @@ namespace Avalonia.Controls if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) { // We've scrolled off more of the first row than what's possible. This can happen - // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling + // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling // cleanup issue. In this case, simply try to display the next row as the first row instead if (newFirstScrollingSlot < SlotCount - 1) { @@ -2014,7 +2014,7 @@ namespace Avalonia.Controls if (recycleRow) { - DisplayData.AddRecylableRow(dataGridRow); + DisplayData.AddRecyclableRow(dataGridRow); } else { @@ -2265,7 +2265,7 @@ namespace Avalonia.Controls if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1) { // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed - EnsureAnscestorsExpanderButtonChecked(parentGroupInfo); + EnsureAncestorsExpanderButtonChecked(parentGroupInfo); } } } @@ -2407,7 +2407,7 @@ namespace Avalonia.Controls return treeCount; } - private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) + private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) { if (IsSlotVisible(parentGroupInfo.Slot)) { @@ -2789,11 +2789,11 @@ namespace Avalonia.Controls return null; } - internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisibile, bool setCurrent) + internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisible, bool setCurrent) { Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0); - if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisibile, setCurrent); }) || !CommitEdit()) + if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisible, setCurrent); }) || !CommitEdit()) { return; } @@ -2804,7 +2804,7 @@ namespace Avalonia.Controls UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); } - UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisibile, isDisplayed: true); + UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisible, isDisplayed: true); ComputeScrollBarsLayout(); // We need force arrange since our Scrollings Rows could update without automatically triggering layout diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index 1c350a4f14..4eed119240 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -140,7 +140,7 @@ namespace Avalonia.Controls.Primitives if (dataGridColumn.IsFrozen) { columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); - columnHeader.Clip = null; // The layout system could have clipped this becaues it's not aware of our render transform + columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform if (DragColumn == dataGridColumn && DragIndicator != null) { dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset; diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index f616a42cac..d44b2ab0db 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -273,7 +273,7 @@ namespace Avalonia.Controls } /// - /// Sets up the platform-speciic services for the . + /// Sets up the platform-specific services for the . /// private void Setup() { diff --git a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs index 9f3a6da9da..46affcbe33 100644 --- a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs +++ b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs @@ -26,13 +26,13 @@ namespace Avalonia.Controls.Converters Right ? Indent * scalarDepth : 0, Bottom ? Indent * scalarDepth : 0); } - else if (value is Thickness thinknessDepth) + else if (value is Thickness thicknessDepth) { return new Thickness( - Left ? Indent * thinknessDepth.Left : 0, - Top ? Indent * thinknessDepth.Top : 0, - Right ? Indent * thinknessDepth.Right : 0, - Bottom ? Indent * thinknessDepth.Bottom : 0); + Left ? Indent * thicknessDepth.Left : 0, + Top ? Indent * thicknessDepth.Top : 0, + Right ? Indent * thicknessDepth.Right : 0, + Bottom ? Indent * thicknessDepth.Bottom : 0); } return new Thickness(0); diff --git a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs index e6420fe342..65f95808ff 100644 --- a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs +++ b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls.Converters if (parameter == null || values == null || values.Count != 4 || - !(values[0] is ScrollBarVisibility visiblity) || + !(values[0] is ScrollBarVisibility visibility) || !(values[1] is double offset) || !(values[2] is double extent) || !(values[3] is double viewport)) @@ -24,7 +24,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - if (visiblity == ScrollBarVisibility.Auto) + if (visibility == ScrollBarVisibility.Auto) { if (extent == viewport) { diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 43bc7d1df9..5893a02b04 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -71,7 +71,7 @@ namespace Avalonia.Controls x => x.MonthVisible, (x, v) => x.MonthVisible = v); /// - /// Defiens the Property + /// Defines the Property /// public static readonly DirectProperty YearFormatProperty = AvaloniaProperty.RegisterDirect(nameof(YearFormat), diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index a0e8c03195..e16e609a15 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -220,15 +220,15 @@ namespace Avalonia.Controls.Primitives if (dy > 0) // Scroll Down { - int numContsToMove = 0; + int numCountsToMove = 0; for (int i = 0; i < children.Count; i++) { if (children[i].Bounds.Bottom - dy < 0) - numContsToMove++; + numCountsToMove++; else break; } - children.MoveRange(0, numContsToMove, children.Count); + children.MoveRange(0, numCountsToMove, children.Count); var scrollHeight = _extent.Height - Viewport.Height; if (ShouldLoop && value.Y >= scrollHeight - _extentOne) @@ -236,15 +236,15 @@ namespace Avalonia.Controls.Primitives } else if (dy < 0) // Scroll Up { - int numContsToMove = 0; + int numCountsToMove = 0; for (int i = children.Count - 1; i >= 0; i--) { if (children[i].Bounds.Top - dy > Bounds.Height) - numContsToMove++; + numCountsToMove++; else break; } - children.MoveRange(children.Count - numContsToMove, numContsToMove, 0); + children.MoveRange(children.Count - numCountsToMove, numCountsToMove, 0); if (ShouldLoop && value.Y < _extentOne) _offset = new Vector(0, value.Y + (_extentOne * 50)); } diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 3237f6f37b..37b8691ce9 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls if (_sharedState == null) { // start with getting SharedSizeGroup value. - // this property is NOT inhereted which should result in better overall perf. + // this property is NOT inherited which should result in better overall perf. string sharedSizeGroupId = SharedSizeGroup; if (sharedSizeGroupId != null) { @@ -52,7 +52,7 @@ namespace Avalonia.Controls } /// - /// Callback to notify about exitting model tree. + /// Callback to notify about exiting model tree. /// internal void OnExitParentTree() { @@ -458,7 +458,7 @@ namespace Avalonia.Controls private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content" private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's - private double _measureSize; // size, calculated to be the input contstraint size for Child.Measure + private double _measureSize; // size, calculated to be the input constraint size for Child.Measure private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case) @@ -556,7 +556,7 @@ namespace Avalonia.Controls } /// - /// Propogates invalidations for all registered definitions. + /// Propagates invalidations for all registered definitions. /// Resets its own state. /// internal void Invalidate() diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index e4b68c62fd..b9da1138d1 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -485,7 +485,7 @@ namespace Avalonia.Controls.Primitives internal static void SetPresenterClasses(IControl presenter, Classes classes) { - //Remove any classes no longer in use, ignoring pseudoclasses + //Remove any classes no longer in use, ignoring pseudo classes for (int i = presenter.Classes.Count - 1; i >= 0; i--) { if (!classes.Contains(presenter.Classes[i]) && diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index a14df1eb43..4e60b52f83 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -330,7 +330,7 @@ namespace Avalonia.Controls // value of Auto column), "cell 2 1" needs to be calculated first, // as it contributes to the Auto column's calculated value. // At the same time in order to accurately calculate constraint - // height for "cell 2 1", "cell 1 2" needs to be calcualted first, + // height for "cell 2 1", "cell 1 2" needs to be calculated first, // as it contributes to Auto row height, which is used in the // computation of Star row resolved height. // @@ -405,11 +405,11 @@ namespace Avalonia.Controls // // where: // * all [Measure GroupN] - regular children measure process - - // each cell is measured given contraint size as an input + // each cell is measured given constraint size as an input // and each cell's desired size is accumulated on the // corresponding column / row; // * [Measure Group2'] - is when each cell is measured with - // infinit height as a constraint and a cell's desired + // infinite height as a constraint and a cell's desired // height is ignored; // * [Measure Groups''] - is when each cell is measured (second // time during single Grid.MeasureOverride) regularly but its @@ -780,7 +780,7 @@ namespace Avalonia.Controls } /// - /// Initializes DefinitionsU memeber either to user supplied ColumnDefinitions collection + /// Initializes DefinitionsU member either to user supplied ColumnDefinitions collection /// or to a default single element collection. DefinitionsU gets trimmed to size. /// /// @@ -821,7 +821,7 @@ namespace Avalonia.Controls } /// - /// Initializes DefinitionsV memeber either to user supplied RowDefinitions collection + /// Initializes DefinitionsV member either to user supplied RowDefinitions collection /// or to a default single element collection. DefinitionsV gets trimmed to size. /// /// @@ -2132,7 +2132,7 @@ namespace Avalonia.Controls // // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) // each being allocated a large number of pixels (~50 or greater), and - // people don't even notice the kind of 1-pixel anomolies that are + // people don't even notice the kind of 1-pixel anomalies that are // theoretically inevitable, or don't care if they do. At least they shouldn't // care - no one should be using the results WPF's grid layout to make // quantitative decisions; its job is to produce a reasonable display, not @@ -2597,7 +2597,7 @@ namespace Avalonia.Controls if (scale < 0.0) { // if one of the *-weights is Infinity, adjust the weights by mapping - // Infinty to 1.0 and everything else to 0.0: the infinite items share the + // Infinity to 1.0 and everything else to 0.0: the infinite items share the // available space equally, everyone else gets nothing. return (Double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; } @@ -2655,7 +2655,7 @@ namespace Avalonia.Controls private enum Flags { // - // the foolowing flags let grid tracking dirtiness in more granular manner: + // the following flags let grid tracking dirtiness in more granular manner: // * Valid???Structure flags indicate that elements were added or removed. // * Valid???Layout flags indicate that layout time portion of the information // stored on the objects should be updated. @@ -2684,7 +2684,7 @@ namespace Avalonia.Controls /// /// ShowGridLines property. This property is used mostly - /// for simplification of visual debuggig. When it is set + /// for simplification of visual debugging. When it is set /// to true grid lines are drawn to visualize location /// of grid lines. /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f4185650bb..43b4908482 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -89,7 +89,7 @@ namespace Avalonia.Controls /// /// /// Note that the selection mode only applies to selections made via user interaction. - /// Multiple selections can be made programatically regardless of the value of this property. + /// Multiple selections can be made programmatically regardless of the value of this property. /// public new SelectionMode SelectionMode { diff --git a/src/Avalonia.Controls/NativeMenuItemSeparator.cs b/src/Avalonia.Controls/NativeMenuItemSeparator.cs index d3d3721c89..49b36e714d 100644 --- a/src/Avalonia.Controls/NativeMenuItemSeparator.cs +++ b/src/Avalonia.Controls/NativeMenuItemSeparator.cs @@ -3,7 +3,7 @@ namespace Avalonia.Controls { - [Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")] + [Obsolete("This class exists to maintain backwards compatibility with existing code. Use NativeMenuItemSeparator instead")] public class NativeMenuItemSeperator : NativeMenuItemSeparator { } diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs index bb3c0288eb..8513dd1697 100644 --- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -25,7 +25,7 @@ namespace Avalonia.Platform /// /// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used. - /// This is because Windows Chrome can not be shown ontop of user content. + /// This is because Windows Chrome can not be shown on top of user content. /// PreferSystemChrome = 0x02, diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index dd839a0e9b..0f0dd7311d 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// /// An implementation for platforms on which a popup can be - /// aritrarily positioned. + /// arbitrarily positioned. /// public class ManagedPopupPositioner : IPopupPositioner { diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index 0b716ec1ca..acb8e0f006 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -170,7 +170,7 @@ namespace Avalonia.Controls.Primitives } /// - /// Checks if the double value is not inifinity nor NaN. + /// Checks if the double value is not infinity nor NaN. /// /// The value. private static bool ValidateDouble(double value) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 34d3347434..402c34e3e5 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -310,7 +310,7 @@ namespace Avalonia.Controls.Primitives /// /// /// Note that the selection mode only applies to selections made via user interaction. - /// Multiple selections can be made programatically regardless of the value of this property. + /// Multiple selections can be made programmatically regardless of the value of this property. /// protected SelectionMode SelectionMode { diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls/Repeater/IElementFactory.cs index 6a899a6f26..f424ae29b7 100644 --- a/src/Avalonia.Controls/Repeater/IElementFactory.cs +++ b/src/Avalonia.Controls/Repeater/IElementFactory.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls } /// - /// A data template that supports creating and recyling elements for an . + /// A data template that supports creating and recycling elements for an . /// public interface IElementFactory : IDataTemplate { diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index fb2da09e73..01200e87e3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -441,9 +441,9 @@ namespace Avalonia.Controls base.OnPropertyChanged(change); } - internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) + internal IControl GetElementImpl(int index, bool forceCreate, bool suppressAutoRecycle) { - var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); + var element = _viewManager.GetElement(index, forceCreate, suppressAutoRecycle); return element; } diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index cf2066b373..a7b6cf7f18 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -174,7 +174,7 @@ namespace Avalonia.Controls } else { - // We could not find a candiate. + // We could not find a candidate. _lastFocusedElement = null; } } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index da3c2b15e6..1a90da5830 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -186,7 +186,7 @@ namespace Avalonia.Controls _expectedViewportShift.X + _layoutExtent.X - extent.X, _expectedViewportShift.Y + _layoutExtent.Y - extent.Y); - // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. + // We tolerate viewport imprecisions up to 1 pixel to avoid invalidating layout too much. if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})", diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 559edeb204..eee6216587 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -653,7 +653,7 @@ namespace Avalonia.Controls private void CalculatedPropertiesChanged() { // Pass old values of 0 here because we don't have the old values at this point, - // and it shouldn't matter as only the template uses these properies. + // and it shouldn't matter as only the template uses these properties. RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum); RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue); RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize); diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 6ae53a4d59..138a765b43 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -345,7 +345,7 @@ namespace Avalonia.Controls.Selection private protected override void OnSelectionChanged(IReadOnlyList deselectedItems) { // Note: We're *not* putting this in a using scope. A collection update is still in progress - // so the operation won't get commited by normal means: we have to commit it manually. + // so the operation won't get committed by normal means: we have to commit it manually. var update = BatchUpdate(); update.Operation.DeselectedItems = deselectedItems; diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 237bc2ce1d..12ae766052 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -193,7 +193,7 @@ namespace Avalonia.Controls /// /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or - /// top and bottom spacing (for vertical orienation). + /// top and bottom spacing (for vertical orientation). /// The space on both sides of TickBar is half of specified ReservedSpace. /// This property has type of . /// @@ -210,7 +210,7 @@ namespace Avalonia.Controls /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and /// SelectionStart and SelectionEnd are valid. /// - /// The primary ticks (for Mininum and Maximum value) height will be 100% of TickBar's render size (use Width or Height + /// The primary ticks (for Minimum and Maximum value) height will be 100% of TickBar's render size (use Width or Height /// depends on Placement property). /// /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size. @@ -221,7 +221,7 @@ namespace Avalonia.Controls { var size = new Size(Bounds.Width, Bounds.Height); var range = Maximum - Minimum; - var tickLen = 0.0d; // Height for Primary Tick (for Mininum and Maximum value) + var tickLen = 0.0d; // Height for Primary Tick (for Minimum and Maximum value) var tickLen2 = 0.0d; // Height for Secondary Tick var logicalToPhysical = 1.0; var startPoint = new Point(); @@ -285,7 +285,7 @@ namespace Avalonia.Controls tickLen2 = tickLen * 0.75; - // Invert direciton of the ticks + // Invert direction of the ticks if (IsDirectionReversed) { logicalToPhysical *= -1; diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 7028dca769..5b0590c6f0 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls } /// - /// Gets the acheived that the platform was able to provide. + /// Gets the achieved that the platform was able to provide. /// public WindowTransparencyLevel ActualTransparencyLevel { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 2b31cef8bd..72b6e4543b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -264,7 +264,7 @@ namespace Avalonia.Controls } /// - /// Called durung the arrange pass to set the size of the window. + /// Called during the arrange pass to set the size of the window. /// /// The requested size of the window. /// The actual size of the window. diff --git a/src/Avalonia.Input/Cursor.cs b/src/Avalonia.Input/Cursor.cs index 2b99c51472..122838f682 100644 --- a/src/Avalonia.Input/Cursor.cs +++ b/src/Avalonia.Input/Cursor.cs @@ -37,7 +37,7 @@ namespace Avalonia.Input BottomSize = BottomSide // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ - // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image + // We might enable them later, preferably, by loading pixmax directly from theme with fallback image // SizeNorthWestSouthEast, // SizeNorthEastSouthWest, } diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs index 3858cc04f2..84a26a0cc3 100644 --- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -78,7 +78,7 @@ namespace Avalonia.Input.GestureRecognizers // Arbitrary chosen value, probably need to move that to platform settings or something private const double ScrollStartDistance = 30; - // Pixels per second speed that is considered to be the stop of inertiall scroll + // Pixels per second speed that is considered to be the stop of inertial scroll private const double InertialScrollSpeedEnd = 5; public void PointerMoved(PointerEventArgs e) diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs index ba2e8eed4e..eed71759d5 100644 --- a/src/Avalonia.Input/ICommandSource.cs +++ b/src/Avalonia.Input/ICommandSource.cs @@ -22,7 +22,7 @@ namespace Avalonia.Input /// - /// Bor the bheavior CanExecuteChanged + /// Bor the behavior CanExecuteChanged /// /// /// diff --git a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs index d385f5b162..2b5d8958cc 100644 --- a/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Input/TextInput/ITextInputMethodClient.cs @@ -22,11 +22,11 @@ namespace Avalonia.Input.TextInput /// event EventHandler TextViewVisualChanged; /// - /// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position + /// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position /// bool SupportsPreedit { get; } /// - /// Sets the non-commited input string + /// Sets the non-committed input string /// void SetPreeditText(string text); /// diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 4cd810af20..2497150b1a 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -143,7 +143,7 @@ namespace Avalonia.Interactivity /// The routed event. /// An describing the route. /// - /// Usually, calling is sufficent to raise a routed + /// Usually, calling is sufficient to raise a routed /// event, however there are situations in which the construction of the event args is expensive /// and should be avoided if there are no handlers for an event. In these cases you can call /// this method to build the event route and check the diff --git a/src/Avalonia.Layout/ElementManager.cs b/src/Avalonia.Layout/ElementManager.cs index 3f106708e6..c2f829d020 100644 --- a/src/Avalonia.Layout/ElementManager.cs +++ b/src/Avalonia.Layout/ElementManager.cs @@ -40,7 +40,7 @@ namespace Avalonia.Layout { if (IsVirtualizingContext) { - // We proactively clear elements laid out outside of the realizaton + // We proactively clear elements laid out outside of the realization // rect so that they are available for reuse during the current // measure pass. // This is useful during fast panning scenarios in which the realization diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index eace54d2e0..63343fd1a7 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -22,7 +22,7 @@ namespace Avalonia.Layout private int _firstRealizedDataIndexInsideRealizationWindow = -1; private int _lastRealizedDataIndexInsideRealizationWindow = -1; - // If the scroll orientation is the same as the folow orientation + // If the scroll orientation is the same as the follow orientation // we will only have one line since we will never wrap. In that case // we do not want to align the line. We could potentially switch the // meaning of line alignment in this case, but I'll hold off on that @@ -429,7 +429,7 @@ namespace Avalonia.Layout // If we did not reach the top or bottom of the extent, we realized one // extra item before we knew we were outside the realization window. Do not - // account for that element in the indicies inside the realization window. + // account for that element in the indices inside the realization window. if (direction == GenerateDirection.Forward) { int dataCount = _context.ItemCount; diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 2e3408eca5..89efa6af0c 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -138,7 +138,7 @@ namespace Avalonia.Native { _nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory); - _nativeMenu.Initialise(this, appMenuHolder, ""); + _nativeMenu.Initialize(this, appMenuHolder, ""); setMenu = true; } @@ -159,7 +159,7 @@ namespace Avalonia.Native { _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); - _nativeMenu.Initialise(this, menu, ""); + _nativeMenu.Initialize(this, menu, ""); setMenu = true; } diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index f76e9450fc..0e6fdd2df0 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -96,7 +96,7 @@ namespace Avalonia.Native.Interop.Impl _menuItems.Remove(item); RemoveItem(item); - item.Deinitialise(); + item.Deinitialize(); item.Dispose(); } @@ -113,7 +113,7 @@ namespace Avalonia.Native.Interop.Impl { var result = CreateNew(factory, item); - result.Initialise(item); + result.Initialize(item); _menuItemLookup.Add(result.ManagedMenuItem, result); _menuItems.Insert(index, result); @@ -133,7 +133,7 @@ namespace Avalonia.Native.Interop.Impl return nativeItem; } - internal void Initialise(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title) + internal void Initialize(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title) { _exporter = exporter; ManagedMenu = managedMenu; @@ -150,7 +150,7 @@ namespace Avalonia.Native.Interop.Impl foreach (var item in _menuItems) { - item.Deinitialise(); + item.Deinitialize(); item.Dispose(); } } diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index 97838f8dea..ca99cbea4b 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -85,7 +85,7 @@ namespace Avalonia.Native.Interop.Impl SetAction(action, callback); } - internal void Initialise(NativeMenuItemBase nativeMenuItem) + internal void Initialize(NativeMenuItemBase nativeMenuItem) { ManagedMenuItem = nativeMenuItem; @@ -123,7 +123,7 @@ namespace Avalonia.Native.Interop.Impl } } - internal void Deinitialise() + internal void Deinitialize() { if (_subMenu != null) { @@ -150,7 +150,7 @@ namespace Avalonia.Native.Interop.Impl { _subMenu = __MicroComIAvnMenuProxy.Create(factory); - _subMenu.Initialise(exporter, item.Menu, item.Header); + _subMenu.Initialize(exporter, item.Menu, item.Header); SetSubMenu(_subMenu); } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 4d02f94cad..40891a700d 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -284,7 +284,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // - U+0028 (Left Opening Parenthesis) // - U+005B (Opening Square Bracket) // - U+007B (Left Curly Bracket) - // See custom colums|rules in the text pair table. + // See custom columns|rules in the text pair table. // https://www.unicode.org/Public/13.0.0/ucd/auxiliary/LineBreakTest.html _lb30 = _alphaNumericCount > 0 && cls == LineBreakClass.OpenPunctuation diff --git a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs index 1e80eabfc8..742bb9c804 100644 --- a/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs +++ b/src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs @@ -25,14 +25,14 @@ namespace Avalonia.Media.Transformation Matrix.CreateScale(decomposed.Scale); } - public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres) + public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progress) { Matrix.Decomposed result = default; - result.Translate = InterpolateVectors(from.Translate, to.Translate, progres); - result.Scale = InterpolateVectors(from.Scale, to.Scale, progres); - result.Skew = InterpolateVectors(from.Skew, to.Skew, progres); - result.Angle = InterpolateScalars(from.Angle, to.Angle, progres); + result.Translate = InterpolateVectors(from.Translate, to.Translate, progress); + result.Scale = InterpolateVectors(from.Scale, to.Scale, progress); + result.Skew = InterpolateVectors(from.Skew, to.Skew, progress); + result.Angle = InterpolateScalars(from.Angle, to.Angle, progress); return result; } From 641b8887456c2518a52206d4d73a0115a5d52c85 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Mon, 12 Jul 2021 18:59:08 +0300 Subject: [PATCH 011/440] Invalidating measure on TransformedBounds update --- src/Avalonia.Controls/Primitives/AdornerLayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index a397608aba..7c02832af6 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -154,7 +154,7 @@ namespace Avalonia.Controls.Primitives info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => { info.Bounds = x; - InvalidateArrange(); + InvalidateMeasure(); }); } } From f6fce366cdec5da49ba04d1417354903be5ea378 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jul 2021 15:52:17 +0200 Subject: [PATCH 012/440] Added failing tests for TextLayout when used with a transform. --- .../Media/TextFormatting/TextLayoutTests.cs | 97 ++++++++++++++++++ .../TextLayout/TextLayout_Basic.expected.png | Bin 0 -> 2177 bytes .../TextLayout_Rotated.expected.png | Bin 0 -> 2628 bytes .../TextLayout/TextLayout_Basic.expected.png | Bin 0 -> 2013 bytes .../TextLayout_Rotated.expected.png | Bin 0 -> 2468 bytes 5 files changed, 97 insertions(+) create mode 100644 tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png create mode 100644 tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png create mode 100644 tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs new file mode 100644 index 0000000000..c0a27eb9ca --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -0,0 +1,97 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else + +using Avalonia.Direct2D1.RenderTests; + +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class TextLayoutTests : TestBase + { + public TextLayoutTests() + : base(@"Media\TextFormatting\TextLayout") + { + } + + [Fact] + public async Task TextLayout_Basic() + { + var t = new TextLayout( + "Avalonia!", + new Typeface(TestFontFamily), + 24, + Brushes.Black); + + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new DrawnControl(c => + { + var textRect = new Rect(t.Size); + var bounds = new Rect(0, 0, 200, 200); + var rect = bounds.CenterRect(textRect); + c.DrawRectangle(Brushes.Yellow, null, rect); + t.Draw(c, rect.Position); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task TextLayout_Rotated() + { + var t = new TextLayout( + "Avalonia!", + new Typeface(TestFontFamily), + 24, + Brushes.Black); + + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new DrawnControl(c => + { + var textRect = new Rect(t.Size); + var bounds = new Rect(0, 0, 200, 200); + var rect = bounds.CenterRect(textRect); + var rotate = Matrix.CreateTranslation(-100, -100) * + Matrix.CreateRotation(MathUtilities.Deg2Rad(90)) * + Matrix.CreateTranslation(100, 100); + using var transform = c.PushPreTransform(rotate); + c.DrawRectangle(Brushes.Yellow, null, rect); + t.Draw(c, rect.Position); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + private class DrawnControl : Control + { + private readonly Action _render; + public DrawnControl(Action render) => _render = render; + public override void Render(DrawingContext context) => _render(context); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png b/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..215e83106f449cded8d491e55c90a8633372394b GIT binary patch literal 2177 zcmd^>`BxH%7RPZNmrTbp%}FWG>zTQ|F+)quWk3@X#3h&9%IAiu&lPv9oKY*6$%P(j zj+ye*k*QH|OUYw_l8S+ZiOU!v0s`X#q@+IQ%scO&m>=%_o_p>u_kPbk=X1;bU|u>} z###UXK*#$k*#A(+{|F8B!`vN$e{v{F8U9`#0Q~eR{$ZgS@9ygk0KBNu{&G_d0MM}X z2D=C5M2gr>Wd^~gC>t@Is@fOLp83USshFKR@$|aZh?ditcD>sr(aGjv=lOxpzeZ>~ zL4Lj+2)^RuNa`Nq;y1C*+^CO9~v6bdU~%;pDuR)%QPOwUZ^0Flv5ivQ^jxM1#jC6 zYVs}pLfuA^sK5ck<_;I%+}eCgHv{eene&s5oszEUvrH*c(aKq!BI5iR42!1Ab#wvK zqREAQG(5f6ot(XT4eGl8nEt)CsQA7Ma$3}VV_;T1)lh2_1>-La)D|haP5Q?(FT5KF z+&Lb2pqi@ywPgwCffb~EXd;&?^ZGusC3z96NV@#%*#goJM7{Ipz%K(l-R$vvwTpek9CTwE)Y&`65j zC?rE#vFUO15rsGYV@uZipf^a^Enkp5q@nh$i0zG;ySBCccyIC4?1yE!gyws!O>=79 zv96;-PVu^wW(IGKMU}ed+Bu**?9VhMdhC*NpVoGWqtIa8#ww0*oe)!3Rm=q zbL8l|oVud2%ooVQFfTb+SVGP|(%pB1ta-I0=q*dlXpBk9%`gp~$AlD3c4Z;^WoIgX zgz`P<)|?-5A5lt+t;WOM1`T^)(=*Rg%7+?l@cWK+JIbIy@<6p`6R4fh)YofkGx=?} zA{6V8a(qj~b5wAt9+fnlel_wN{)Vjc(u71nL|y}-x3X2b3I+_&K@(F65^ zm9YV*&KBaU(lXE(+_g?9o(~oc*~9Oe4ck~x<*)gP_jlsOHpH|_T5uG>rMV2LqHow^ zTpU5Ell3xW%u#K!}$Yj z9Jyd(lbcL4`#i&G#b~7e9xZ;4rxve?>TKc=euL&8$raAwQN_~hZIV)8x|~|Kpo5aE z;>B}kFK^__KIz75eVC{bl|fDzPI_3y>Fw4FK3feCJA=v>)cGL$h?B??&(;@CSB!_- z9IQJjlpH48hw=KdVoVV`yW21L4cpFklwguroA!IVc~((E!OQ6f|Df=7y^rG=q3tgmvbcZmu7Sk+SujY-+h;L!9vqb z`SQRw44XP{Ah2mn<*RuhA?cofxsFWadwg;97M6bFl#Z~H)#Wc0PRur6k6nYAK;Tb9 zA`=Vx?9I{75;+zfxQN7;&;^15P7N7(&onVTOT6h1ul6==-qD8ZSuRELlbJKkZfi%| zad&E$;`mcAXNdjCrO}@3_3_0K--h&HbU8bfwZ_lbPEf?$-R6d({z)Kg8{!L*8k7KA zW1&y%;dY9_+o376+WBf`;veHl=5@bADb8&K8s)Ko%ivqN-!VEeg>wQyWye#A<}R!y z_Vs7_%fVw_<*9n-=S#)?`!DnKkvrJy!ZIltl+@TI!PIv=T`^uJajJT{u|%h3k19AR zq2X3j7)`XpyhQqK;iwSs6<@Mu>1ErMv490T1!>;WU z@;PAh$C%%CrCi;(ye|h^a4}FCQ*Ye3ai`k}enevH#VpCL;c-qU!$2i$4!ZcwZ z5J>Tu9U2RaZ+||}wZQwC2kQ|qfUjU}Q6N@})))}TokKbzL7u9%{PYfQd(%uYQP&H1jpLFPAG1jwd8WeHC7>glbIUBoqs<0c4!fjIUH2S=PX0ux z9Q$oEo(@x`Z*sw)V$htuSFwA0)J?ayT$xTAOPk{MyzHAh^ldh>E|Bo(GilC}hKOQb zxwriJG)Pn6b{C|nqVfu91yTTmwdCXi|Jk5U{Qb4KDD;!o*sJF+A<@W%SqFA`uRSt9opm-#bEitGO2ZoLlRv9gYWP2v zd2x()4JcEaOgc|@nO=L=LCUh?wlJ!Hgh?IEi?%`SYAwOmf z!N5(NF#M(NYgDu@)J{P_E9aLH2?@&S%hT2{`}x5X-ez^A6~Yd$N68PKG^rHouy@)P z92FP&`qGf9aydIA7n>)zX(qiGcYAz6TAJ8A*`ae7+a1tXBi?Y3f;%nu z25$H?MbG3};nu>j`k1{O_zs45>Ug*7R<*UjR6{!??9HxzOlGH{bvTd!zUHADSE1Vq zqXQm;8Y<~n66Ra23ERwiU~d?v`Dn{6aaXsHw9)vd#WnV)o|0%$(@3_Uve_r()Ggu^ zup4A!y>em&M)=_ymC4bYdEU+SiD;YXPbf@KTP=7qxJ|eJ$_L4Ql@W@Hqr@q}Kk3z$ zoXm7{R}1lE(-#D^;(9d4YEY4gh*%_+6;rkGMJf$V@XbaKG7bj>51=KPdz9ZZLkSE| z16igA!ZsOf?+qNqtUNgLL%|%AtO~TsOkOCvO0+z5j2+NjkZ@S@RqawKU(Fe(d_s0a zztZSV#o#&<(PD4-SY?WTl9d&Tdlc?D8B1HHgqhP!R#&E3{bp-wbCR(+5a^$2ZxvBp zMbK_q7t4`3)~EWv_G#w`(Bp7ni=;SaNF~`kZa1()g-3j~)E|~$w#WaPeQxAgx229 zW~569SfZ@%3*a=jIZsbn)qpiQCxgqr^)2>BO$!&dPUZZuPY%)!nbbaU2)eqdXOYT9 zs;~Zeq>M`B5>(Qp-v!&LJOTd%)3*y(?tdrZ zvXW3zMwb<-^(!ww;+p^$X5{NMER_H@g4!W9z0VWukyr zzlFqbcP|n35`(+(yzSxSVMfc8Ml~aRAy9fmleZ-%`s$Wyj73O?2rk%JO7fMQ-Y4s7 zz%BBYfLBQ z@q)`L%jwZ%T;wtPsg%saaygGXn^B7ZEQeVd!i6$Au^91QH~U@4Y|(5QKw81t)r$!wHjWa4X6Ja^yh^H4M^d*1+b7W)xVk#3b}YrCGN`$0 zhHUs1yo+-q-tO?Hm6SOGB5P*F`%At|?E0b{hnk(}{*W&*;-b*_44`0}1OD@3P>HQ! z(Ecra%5nEV+fM|f@;%wwmIVoFZ_~BY_jdveXpQBZtnvP6I{9EJ$oaU;6A%kPUB)9` z?!)i8+T`tV;$IzWXa}v8ZzGHD8jP`niP9vh#X|sp0DLwmC%QE%knXqt`yxOR=amta z;qgpcK&{r-`p55I(bw|Q=r-GUSDLY3#3hSIuPIhpwwpn)sh5g*isk+X$Rn(IkNnPF z{lmqzizVlr^(l_qK3QAA-cwZpy#lyd*xKQN*EoBh1MY#XI*@j7=)~ng8~xo!2b1fA zRa0Yw3Iiqn`r{@#Z36h{CJ6Sy z)?$9ru}|W*<5L5|E)gJkJTd_rVC>_ou+_%(RgW>xNCptfKx0UMYv)w1!}^l?n->6# zo@5VTk?VmH{huMf0t#>)cO!3VJEU*)4bA<2%SqJM4(3t9?op*IFVl%2XPKwlv{LID z3MDH`ISO9FhN6{Kd~fKZOGY0Px>;$&PiN-&=f?OwCnU<^2(gPKmq5sqwp&}h*9B}( zEm4Px-uc%Ip4OCgTx_+6gFM?~m1YnN;Cfql{w-9GW;oJO(1_9nd||Zd7wvwX^rx%; fk>C}EumXO`ESNaHKF<~S69gUm!wJnoos9h(%bBzn literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..220932ff41084338bb176fec16989ad4a51f1d21 GIT binary patch literal 2013 zcmd^==~t2o6vioQG|8dVvQ?~uS+-cYi%N?|T1h$PRtk-Z85A`oC4#oOlo^ewWR#4y z*rE`UT3JwPW2H!_X^Qe@i=-^BvGY32oHKt#_x$dCK0Wt5=YF{tc88&DtQT2BAP}37 z?LpyYee-Wy{bHU~(d<&Q%s9R!B*MyERI9{X2*k25BxrNQsT;~Z0l_|HN%I&<#fIj1 zmCtXuaQ3L{p5^y)qgR5sTl2jxR_kE4YnohF*%P~5vu^M8!X87o z=Yflkn|jZdnBpd#cHhGa6`FF(v8@>MX~gFa`X0n*&I_=ze;|4{>4>v~&as;D9|%i3 zJA6O@3j2=TReqRMzp%lqBpTQPhR&?*&p zr|Ub3WHQZ>M@y=nTL`Y~>bud?cC!kQ$%M7U7+_CTQ|qN3&;%YYDJJgZrI1xOdZaJ# znh}mDA#CThxcWg_c+>hGB6DiRHMRE5nN|+PMFQKm$!+Wv!V2;|mn=9_G0@zCzyw~WoT#>i`5&^yb zr9@${VGIp*8FvVgNG5-3^xA;t(1(JoEY{SzKIy^R&5KK! zCC){mMv_dn_I|gi9gNX|#-J{SG0}KP(d|gwWib5%`avbi*=DYRHiKr?zWzTjy zst~#dswN&|>mI$gc9$Qkw?RBFBh*Zu>V>!e@6WkYzGM$M9o70^FRFIx^J`IE&Qg$<9OYG9<5OuIzk?C6=zvrrp{O|6 zW|=R|FT+G30h~nLdD@j-%r5+zwAF{5287g6oL;{*hk8BLL*DMp%J#;s_x#X10MxmG zb;7$NO)yx+$?M&pZ+B@%0{dSe(@T{Sntm+7sOIyj&5sNHi!r$=qSBg$3DVGo=6FeX z@^FMc>&YoGN91=sb^JDl1cbrgru}$V5yftTPrQbrQpjWF9IiL%$jzmk?uoSc4F~dL z$-!$`&6#A+`e6M%OqeB{K?rxk=_r>8yxh#^k^%Fs|9chiw?< zCwUZ0)jLlRKyXR(Y~b&WUU(vQKan=P6jeI)g%c!gNye`LpB06RbHwex8hwI%HEdi&y}Jw$%IpkOAsTl@L4r?@F}%H-VVS~ z5!l+s=XWHQvk0AbE|XNoI-N_?E%X>B+dY{|kr$I3I}h4COD*m__*J-QmD%o`pnN`Zgn^OWF-hi($)ebpEA3P1 zbBw^~$mY{F=Qcsk{7+dTnyVE(e3cn*LtH@oecJRhke`zVR3+W`sY9FdPe*lXVSIGbX literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..26fe2b0c91ad39e98f97b40711fcd8cc436186af GIT binary patch literal 2468 zcmcguX;72N7G+|P03*AE0olfgxDZ4UG!P6BG{F&dL_mSCIfxHrQ6LZ!F(9(ZVifZb zB_hEESw*%GHrYl4s8K^u0wS9qVUa-CA@hOO`}KaldR^7sReh_vZ=F8p+?#m9;m9UA zO*ttksZDqs_D^vCdu_rtf@`5mQ8u_iFWcZ9Vc;Oc&Lv1m$-3gP){YVP#s$ax+=g`8 zraX+1$ibL7=^fE(PEUWU#2IqvSrvxijWz|GX1pDsYCuu)79GQ(DS)!)9*vpucQlWPF|!U5@!a|xPlt4SeU^%n;eHB>nRzJ%KTZR#sS`cdd^n8Szc!3n8Vv z<3HZuZP;OMzMZmofpz+6tqyg*!4fC4GkU9JyfUN4wL5?v61Eir=uJ;^o8mtnseFD0 z5D@daJm?BKH}+Ko+UY58mgBB00bP^qTdP@ys0~!8Q_@#qab9NT>%Xw?B4LOUIw61g zt|3Z`ip8ic$xxxQBMfXQBN&bI>}V~m7(Tzn?^kN*gF>SVq_e0H8`5|=KNkob^A1Zu z+;novPYX=B(X6EkL$J#C6N^qS40;-Db!Qvau2h81__0f=n1}pNv^oia6tZ z9p-sCIT1d#+aR)IwR1a(Irj17`0LE+^cX}Hbhx-2h3GOgthqFJtptN3zI*-vKjG@zMBKHz4bIk<>$P^XB z_Cu!+D<>%4D}ZVI8=rpVoV5YF)0>Mqh5d|ElIx zVV9CR6>4UY)fqK-DX`av;?4^1(4H{L%nqLlZgg~ms_pd+gbUj`m|Lc^YQ534zo5RZXqX`~x$7bxdfA0#e}JH*A9 zn)LTpG0<99Zs1H#YVbeYeo+mqqRpIY$s@K_#%|va8GLA~b|ILq-u_Gi>GV3o7y00; z+iqQc4762}It%ZO{f{aUCFD906B}vNAEWpCaw(p0`8`TF=)gdr{M*+uFZI%43c9Ii zh-uOf15lXW*t!(I5@)n-3=K#p?52xa)Gg4XEED2~UyDFgwV$A_+2J#Mz+)XHc!Olp2_7(+^4e)Ih zFo>0cN7*`+I&;F5%u(w2>vS##9`XG}_Jd*I3U zzeMn3q&8OQQ8RSDoO#oF-NHJ=_weGd7O=dqmgSZf(L-^r%c)ab8QrP}nihkcli;tn zjGmwy*JREX*CnB86NmM*^B=bz(;(FAL?Hwz7!-dtd`$T&2K9mGL=vFB_8U5RE-X(7 z3ySQ`gILiF)DY3KBt(sqJZp(Le-6u{WW5*oc+0D}AiMY^Tic0P*VX10^=A3bVG7~9 zf%dxnr#FZqpPooS%YhVt2q%M<0~6zHeB$H!6{(>mLw~{5zUnJAfHnx_aJW+gHN|sZ ztcSYdVxq7wb8NYdu~%BJ+qcI?yvxp

aLFHOmrhtN%Dmnhzd13j*4JmUC-)!KV80;FtzQ_>cLLfha?h``n)`I$Vm%m|(AYTYnxde< zdZ45T(vX8s({--DB4XBjG(^c$q2U1S^UksGYdi5hM!0>MKZ)6J)$|~cdZMGFI?Hj= z(GaJqI+3!}UAA_kY8>_0vAi@dEv;vSy6L1QdH8agtT+UD2VUrLDXYWYxv0v>!RG*+ zBLi}3sy=rmZtlOlQCW3<}P#s`w5fA$hz4DY~=aj4Dx#D{{2TM8Qd7Rdjl(Ew$)Dx Date: Wed, 14 Jul 2021 15:53:07 +0200 Subject: [PATCH 013/440] Use pre-transform when drawing TextLayout. Using a post-transform breaks when there's a transform applied to the drawing context. --- .../Media/TextFormatting/ShapedTextCharacters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index b304b19910..3c17dabbb0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting /// public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPostTransform(Matrix.CreateTranslation(origin))) + using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { if (GlyphRun.GlyphIndices.Length == 0) { From a9affd64bf67d2ff39e3853b7f0adc168be71f3f Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 15:46:47 +0200 Subject: [PATCH 014/440] Add support for Flyouts, ToolTips & ContextMenus --- .../Diagnostics/IPopupHostProvider.cs | 23 +++++ .../Diagnostics/ToolTipDiagnostics.cs | 12 +++ src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 25 ++++- src/Avalonia.Controls/ToolTip.cs | 40 +++++--- .../Diagnostics/ViewModels/TreeNode.cs | 11 ++- .../Diagnostics/ViewModels/VisualTreeNode.cs | 97 ++++++++++++++----- 6 files changed, 163 insertions(+), 45 deletions(-) create mode 100644 src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs create mode 100644 src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs diff --git a/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs new file mode 100644 index 0000000000..11d3b1792a --- /dev/null +++ b/src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Controls.Diagnostics +{ + ///

+ /// Diagnostics interface to retrieve an associated . + /// + public interface IPopupHostProvider + { + /// + /// The popup host. + /// + IPopupHost? PopupHost { get; } + + /// + /// Raised when the popup host changes. + /// + event Action? PopupHostChanged; + } +} diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs new file mode 100644 index 0000000000..58174b1039 --- /dev/null +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace Avalonia.Controls.Diagnostics +{ + /// + /// Helper class to provide some diagnostics insides into . + /// + public static class ToolTipDiagnostics + { + public static AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty; + } +} diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index e4b68c62fd..6b72b8c887 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -1,16 +1,16 @@ using System; using System.ComponentModel; +using Avalonia.Controls.Diagnostics; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; -using Avalonia.Rendering; #nullable enable namespace Avalonia.Controls.Primitives { - public abstract class FlyoutBase : AvaloniaObject + public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider { static FlyoutBase() { @@ -55,6 +55,7 @@ namespace Avalonia.Controls.Primitives private Rect? _enlargedPopupRect; private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; + private Action? _popupHostChangedHandler; protected Popup? Popup { get; private set; } @@ -94,6 +95,14 @@ namespace Avalonia.Controls.Primitives private set => SetAndRaise(TargetProperty, ref _target, value); } + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + public event EventHandler? Closed; public event EventHandler? Closing; public event EventHandler? Opened; @@ -322,9 +331,11 @@ namespace Avalonia.Controls.Primitives private void InitPopup() { - Popup = new Popup(); - Popup.WindowManagerAddShadowHint = false; - Popup.IsLightDismissEnabled = true; + Popup = new Popup + { + WindowManagerAddShadowHint = false, + IsLightDismissEnabled = true + }; Popup.Opened += OnPopupOpened; Popup.Closed += OnPopupClosed; @@ -333,11 +344,15 @@ namespace Avalonia.Controls.Primitives private void OnPopupOpened(object sender, EventArgs e) { IsOpen = true; + + _popupHostChangedHandler?.Invoke(Popup!.Host); } private void OnPopupClosed(object sender, EventArgs e) { HideCore(); + + _popupHostChangedHandler?.Invoke(null); } private void PositionPopup(bool showAtPointer) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index ab507d07a2..ab310d60ef 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,9 +1,8 @@ #nullable enable using System; -using System.Reactive.Linq; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -17,7 +16,7 @@ namespace Avalonia.Controls /// assigning the content that you want displayed. ///
[PseudoClasses(":open")] - public class ToolTip : ContentControl + public class ToolTip : ContentControl, IPopupHostProvider { /// /// Defines the ToolTip.Tip attached property. @@ -61,7 +60,8 @@ namespace Avalonia.Controls internal static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); - private IPopupHost? _popup; + private IPopupHost? _popupHost; + private Action? _popupHostChangedHandler; /// /// Initializes static members of the class. @@ -251,35 +251,45 @@ namespace Avalonia.Controls tooltip.RecalculatePosition(control); } + + IPopupHost? IPopupHostProvider.PopupHost => _popupHost; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } internal void RecalculatePosition(Control control) { - _popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); + _popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); } private void Open(Control control) { Close(); - _popup = OverlayPopupHost.CreatePopupHost(control, null); - _popup.SetChild(this); - ((ISetLogicalParent)_popup).SetParent(control); + _popupHost = OverlayPopupHost.CreatePopupHost(control, null); + _popupHost.SetChild(this); + ((ISetLogicalParent)_popupHost).SetParent(control); - _popup.ConfigurePosition(control, GetPlacement(control), + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); - WindowManagerAddShadowHintChanged(_popup, false); + WindowManagerAddShadowHintChanged(_popupHost, false); - _popup.Show(); + _popupHost.Show(); + _popupHostChangedHandler?.Invoke(_popupHost); } private void Close() { - if (_popup != null) + if (_popupHost != null) { - _popup.SetChild(null); - _popup.Dispose(); - _popup = null; + _popupHost.SetChild(null); + _popupHost.Dispose(); + _popupHost = null; + _popupHostChangedHandler?.Invoke(null); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index f4c04dbca6..9667751e54 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; using Avalonia.Media; @@ -19,12 +20,11 @@ namespace Avalonia.Diagnostics.ViewModels protected TreeNode(IVisual visual, TreeNode? parent) { + _classes = string.Empty; Parent = parent; Type = visual.GetType().Name; Visual = visual; - _classes = string.Empty; - - FontWeight = Visual is TopLevel || Visual is Popup ? FontWeight.Bold : FontWeight.Normal; + FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; if (visual is IControl control) { @@ -56,6 +56,11 @@ namespace Avalonia.Diagnostics.ViewModels } } + private bool IsRoot => Visual is TopLevel || + Visual is Popup || + Visual is ContextMenu || + Visual is IPopupHost; + public FontWeight FontWeight { get; } public abstract TreeNodeCollection Children diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index b9981babf7..6d803a1a7b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,5 +1,10 @@ using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Security.Cryptography.X509Certificates; using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Styling; using Avalonia.VisualTree; @@ -13,7 +18,7 @@ namespace Avalonia.Diagnostics.ViewModels { Children = new VisualTreeNodeCollection(this, visual); - if ((Visual is IStyleable styleable)) + if (Visual is IStyleable styleable) { IsInTemplate = styleable.TemplatedParent != null; } @@ -25,13 +30,15 @@ namespace Avalonia.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { - return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : Array.Empty(); + return control is IVisual visual ? + new[] { new VisualTreeNode(visual, null) } : + Array.Empty(); } internal class VisualTreeNodeCollection : TreeNodeCollection { private readonly IVisual _control; - private IDisposable? _subscription; + private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2); public VisualTreeNodeCollection(TreeNode owner, IVisual control) : base(owner) @@ -41,33 +48,79 @@ namespace Avalonia.Diagnostics.ViewModels public override void Dispose() { - _subscription?.Dispose(); + _subscriptions.Dispose(); } - protected override void Initialize(AvaloniaList nodes) + private static IObservable? GetHostedPopupRootObservable(IVisual visual) { - if (_control is Popup p) + static IObservable GetPopupHostObservable(IPopupHostProvider popupHostProvider) { - _subscription = p.GetObservable(Popup.ChildProperty).Subscribe(child => - { - if (child != null) - { - nodes.Add(new VisualTreeNode(child, Owner)); - } - else - { - nodes.Clear(); - } - - }); + return Observable.FromEvent( + x => popupHostProvider.PopupHostChanged += x, + x => popupHostProvider.PopupHostChanged -= x) + .StartWith(popupHostProvider.PopupHost) + .Select(x => x is IControl c ? c : null); } - else + + return visual switch { - _subscription = _control.VisualChildren.ForEachItem( + Popup p => p.GetObservable(Popup.ChildProperty), + Control c => Observable.CombineLatest( + c.GetObservable(Control.ContextFlyoutProperty), + c.GetObservable(Control.ContextMenuProperty), + c.GetObservable(FlyoutBase.AttachedFlyoutProperty), + c.GetObservable(ToolTipDiagnostics.ToolTipProperty), + (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => + { + if (ContextMenu != null) + { + //Note: ContextMenus are special since all the items are added as visual children. + //So we don't need to go via Popup + return Observable.Return(ContextMenu); + } + + if ((ContextFlyout ?? (IPopupHostProvider?) AttachedFlyout ?? ToolTip) is { } popupHostProvider) + { + return GetPopupHostObservable(popupHostProvider); + } + + return Observable.Return(null); + }) + .Switch(), + _ => null + }; + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscriptions.Clear(); + + if (GetHostedPopupRootObservable(_control) is { } popupRootObservable) + { + VisualTreeNode? childNode = null; + + _subscriptions.Add( + popupRootObservable + .Subscribe(root => + { + if (root != null) + { + childNode = new VisualTreeNode(root, Owner); + + nodes.Add(childNode); + } + else if (childNode != null) + { + nodes.Remove(childNode); + } + })); + } + + _subscriptions.Add( + _control.VisualChildren.ForEachItem( (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), (i, item) => nodes.RemoveAt(i), - () => nodes.Clear()); - } + () => nodes.Clear())); } } } From 4d9131091e04db53b53622238ade11f635a751e1 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 16:19:56 +0200 Subject: [PATCH 015/440] Add custom names to PopupRoot --- src/Avalonia.Controls/Primitives/Popup.cs | 17 ++++- .../Diagnostics/ViewModels/TreeNode.cs | 6 +- .../Diagnostics/ViewModels/VisualTreeNode.cs | 72 ++++++++++++------- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index b445de0472..bdc71f9a62 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives /// /// Displays a popup window. /// - public class Popup : Control, IVisualTreeHost + public class Popup : Control, IVisualTreeHost, IPopupHostProvider { public static readonly StyledProperty WindowManagerAddShadowHintProperty = AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); @@ -133,6 +133,7 @@ namespace Avalonia.Controls.Primitives private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; private IInputElement _overlayInputPassThroughElement; + private Action? _popupHostChangedHandler; /// /// Initializes static members of the class. @@ -348,6 +349,14 @@ namespace Avalonia.Controls.Primitives /// IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot; + IPopupHost? IPopupHostProvider.PopupHost => Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + /// /// Opens the popup. /// @@ -479,6 +488,8 @@ namespace Avalonia.Controls.Primitives } Opened?.Invoke(this, EventArgs.Empty); + + _popupHostChangedHandler?.Invoke(Host); } /// @@ -581,6 +592,8 @@ namespace Avalonia.Controls.Primitives _openState.Dispose(); _openState = null; + _popupHostChangedHandler?.Invoke(null); + using (BeginIgnoringIsOpen()) { IsOpen = false; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 9667751e54..4b957c2382 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Avalonia.Controls; -using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; using Avalonia.Media; @@ -18,11 +16,11 @@ namespace Avalonia.Diagnostics.ViewModels private string _classes; private bool _isExpanded; - protected TreeNode(IVisual visual, TreeNode? parent) + protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null) { _classes = string.Empty; Parent = parent; - Type = visual.GetType().Name; + Type = customName ?? visual.GetType().Name; Visual = visual; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 6d803a1a7b..f03f16d18f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,7 +1,7 @@ using System; +using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Security.Cryptography.X509Certificates; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; @@ -13,18 +13,15 @@ namespace Avalonia.Diagnostics.ViewModels { internal class VisualTreeNode : TreeNode { - public VisualTreeNode(IVisual visual, TreeNode? parent) - : base(visual, parent) + public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null) + : base(visual, parent, customName) { Children = new VisualTreeNodeCollection(this, visual); - if (Visual is IStyleable styleable) - { - IsInTemplate = styleable.TemplatedParent != null; - } + if (Visual is IStyleable styleable) IsInTemplate = styleable.TemplatedParent != null; } - public bool IsInTemplate { get; private set; } + public bool IsInTemplate { get; } public override TreeNodeCollection Children { get; } @@ -51,20 +48,30 @@ namespace Avalonia.Diagnostics.ViewModels _subscriptions.Dispose(); } - private static IObservable? GetHostedPopupRootObservable(IVisual visual) + private static IObservable? GetHostedPopupRootObservable(IVisual visual) { - static IObservable GetPopupHostObservable(IPopupHostProvider popupHostProvider) + static IObservable GetPopupHostObservable( + IPopupHostProvider popupHostProvider, + string? providerName = null) { return Observable.FromEvent( x => popupHostProvider.PopupHostChanged += x, x => popupHostProvider.PopupHostChanged -= x) .StartWith(popupHostProvider.PopupHost) - .Select(x => x is IControl c ? c : null); + .Select(popupHost => + { + if (popupHost is IControl control) + return new PopupRoot( + control, + providerName != null ? $"{providerName} ({control.GetType().Name})" : null); + + return (PopupRoot?)null; + }); } return visual switch { - Popup p => p.GetObservable(Popup.ChildProperty), + Popup p => GetPopupHostObservable(p), Control c => Observable.CombineLatest( c.GetObservable(Control.ContextFlyoutProperty), c.GetObservable(Control.ContextMenuProperty), @@ -73,18 +80,20 @@ namespace Avalonia.Diagnostics.ViewModels (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) => { if (ContextMenu != null) - { //Note: ContextMenus are special since all the items are added as visual children. //So we don't need to go via Popup - return Observable.Return(ContextMenu); - } + return Observable.Return(new PopupRoot(ContextMenu)); - if ((ContextFlyout ?? (IPopupHostProvider?) AttachedFlyout ?? ToolTip) is { } popupHostProvider) - { - return GetPopupHostObservable(popupHostProvider); - } + if (ContextFlyout != null) + return GetPopupHostObservable(ContextFlyout, "ContextFlyout"); + + if (AttachedFlyout != null) + return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout"); + + if (ToolTip != null) + return GetPopupHostObservable(ToolTip, "ToolTip"); - return Observable.Return(null); + return Observable.Return(null); }) .Switch(), _ => null @@ -101,18 +110,21 @@ namespace Avalonia.Diagnostics.ViewModels _subscriptions.Add( popupRootObservable - .Subscribe(root => + .Subscribe(popupRoot => { - if (root != null) + if (popupRoot != null) { - childNode = new VisualTreeNode(root, Owner); + childNode = new VisualTreeNode( + popupRoot.Value.Root, + Owner, + popupRoot.Value.CustomName); nodes.Add(childNode); } else if (childNode != null) { nodes.Remove(childNode); - } + }s })); } @@ -122,6 +134,18 @@ namespace Avalonia.Diagnostics.ViewModels (i, item) => nodes.RemoveAt(i), () => nodes.Clear())); } + + private struct PopupRoot + { + public PopupRoot(IControl root, string? customName = null) + { + Root = root; + CustomName = customName; + } + + public IControl Root { get; } + public string? CustomName { get; } + } } } } From dfc5669507d6910c510a4a7619f377e0b02a5100 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 16:20:22 +0200 Subject: [PATCH 016/440] Typo --- .../Diagnostics/ViewModels/VisualTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index f03f16d18f..5524d06f1b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -124,7 +124,7 @@ namespace Avalonia.Diagnostics.ViewModels else if (childNode != null) { nodes.Remove(childNode); - }s + } })); } From 650ae63bd641868decb2bce426a21aa88ad94496 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 15 Jul 2021 17:46:00 +0200 Subject: [PATCH 017/440] make "ctrl+shift+click" work --- src/Avalonia.Controls/ContextMenu.cs | 17 ++++++- .../Diagnostics/ViewModels/VisualTreeNode.cs | 4 +- .../Diagnostics/Views/MainWindow.xaml.cs | 46 +++++++++++++++---- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index ead5b0d9f3..0c8fc31df1 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; - +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; @@ -21,7 +21,7 @@ namespace Avalonia.Controls /// /// A control context menu. /// - public class ContextMenu : MenuBase, ISetterValue + public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider { /// /// Defines the property. @@ -82,6 +82,7 @@ namespace Avalonia.Controls private Popup? _popup; private List? _attachedControls; private IInputElement? _previousFocus; + private Action? _popupHostChangedHandler; /// /// Initializes a new instance of the class. @@ -304,6 +305,14 @@ namespace Avalonia.Controls } } + IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + protected override IItemContainerGenerator CreateItemContainerGenerator() { return new MenuItemContainerGenerator(this); @@ -364,6 +373,8 @@ namespace Avalonia.Controls { _previousFocus = FocusManager.Instance?.Current; Focus(); + + _popupHostChangedHandler?.Invoke(_popup!.Host); } private void PopupClosing(object sender, CancelEventArgs e) @@ -397,6 +408,8 @@ namespace Avalonia.Controls RoutedEvent = MenuClosedEvent, Source = this, }); + + _popupHostChangedHandler?.Invoke(null); } private void PopupKeyUp(object sender, KeyEventArgs e) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 5524d06f1b..6a430897ba 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Collections; @@ -18,7 +17,8 @@ namespace Avalonia.Diagnostics.ViewModels { Children = new VisualTreeNodeCollection(this, visual); - if (Visual is IStyleable styleable) IsInTemplate = styleable.TemplatedParent != null; + if (Visual is IStyleable styleable) + IsInTemplate = styleable.TemplatedParent != null; } public bool IsInTemplate { get; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 9d37b5acdc..26e197d790 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using Avalonia.Controls; +using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; @@ -109,6 +111,37 @@ namespace Avalonia.Diagnostics.Views .FirstOrDefault(); } + private static IEnumerable GetPopupRoots(IVisual root) + { + foreach (var control in root.GetVisualDescendants().OfType()) + { + if (control is Popup { Host: PopupRoot r0 }) + { + yield return r0; + } + + if (control.GetValue(ContextFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r1 }) + { + yield return r1; + } + + if (control.GetValue(FlyoutBase.AttachedFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r2 }) + { + yield return r2; + } + + if (control.GetValue(ToolTipDiagnostics.ToolTipProperty) is IPopupHostProvider { PopupHost: PopupRoot r3 }) + { + yield return r3; + } + + if (control.GetValue(ContextMenuProperty) is IPopupHostProvider { PopupHost: PopupRoot r4 }) + { + yield return r4; + } + } + } + private void RawKeyDown(RawKeyEventArgs e) { var vm = (MainViewModel?)DataContext; @@ -123,16 +156,13 @@ namespace Avalonia.Diagnostics.Views { IControl? control = null; - foreach (var popup in Root.GetVisualDescendants().OfType()) + foreach (var popupRoot in GetPopupRoots(Root)) { - if (popup.Host?.HostedVisualTreeRoot is PopupRoot popupRoot) - { - control = GetHoveredControl(popupRoot); + control = GetHoveredControl(popupRoot); - if (control != null) - { - break; - } + if (control != null) + { + break; } } From 838626d763a9e446844244636c3cc51d55cbd946 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 16 Jul 2021 10:33:51 +0200 Subject: [PATCH 018/440] remove c#9 features, clean up --- .../Diagnostics/Views/MainWindow.xaml.cs | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 26e197d790..cfb9e2ead9 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -111,35 +111,33 @@ namespace Avalonia.Diagnostics.Views .FirstOrDefault(); } - private static IEnumerable GetPopupRoots(IVisual root) + private static List GetPopupRoots(IVisual root) { - foreach (var control in root.GetVisualDescendants().OfType()) - { - if (control is Popup { Host: PopupRoot r0 }) - { - yield return r0; - } - - if (control.GetValue(ContextFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r1 }) - { - yield return r1; - } + var popupRoots = new List(); - if (control.GetValue(FlyoutBase.AttachedFlyoutProperty) is IPopupHostProvider { PopupHost: PopupRoot r2 }) + void ProcessProperty(IControl control, AvaloniaProperty property) + { + if (control.GetValue(property) is IPopupHostProvider popupProvider + && popupProvider.PopupHost is PopupRoot popupRoot) { - yield return r2; + popupRoots.Add(popupRoot); } + } - if (control.GetValue(ToolTipDiagnostics.ToolTipProperty) is IPopupHostProvider { PopupHost: PopupRoot r3 }) + foreach (var control in root.GetVisualDescendants().OfType()) + { + if (control is Popup p && p.Host is PopupRoot popupRoot) { - yield return r3; + popupRoots.Add(popupRoot); } - if (control.GetValue(ContextMenuProperty) is IPopupHostProvider { PopupHost: PopupRoot r4 }) - { - yield return r4; - } + ProcessProperty(control, ContextFlyoutProperty); + ProcessProperty(control, ContextMenuProperty); + ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty); + ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty); } + + return popupRoots; } private void RawKeyDown(RawKeyEventArgs e) From 9b0287bfd6e397f4150820defa0edf598214167a Mon Sep 17 00:00:00 2001 From: mat1jaczyyy Date: Sat, 17 Jul 2021 04:19:31 +0200 Subject: [PATCH 019/440] Fix existing and add missing osx-specific key shortcuts --- src/Avalonia.Native/AvaloniaNativePlatform.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 82a845ffc1..6ff5294d4a 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -109,10 +109,16 @@ namespace Avalonia.Native .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) - .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta)) + .Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt)) .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)); + var hotkeys = AvaloniaLocator.Current.GetService(); + hotkeys.MoveCursorToTheStartOfLine.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheStartOfLineWithSelection.Add(new KeyGesture(Key.Left, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + hotkeys.MoveCursorToTheEndOfLine.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers)); + hotkeys.MoveCursorToTheEndOfLineWithSelection.Add(new KeyGesture(Key.Right, hotkeys.CommandModifiers | hotkeys.SelectionModifiers)); + if (_options.UseGpu) { try From 2387e0bb5af5fa7448847bf11fd5157a0f111fcd Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Wed, 21 Jul 2021 18:34:14 +0200 Subject: [PATCH 020/440] Add option to freez popups (Alt + Ctrl + F) --- .../Diagnostics/ViewModels/MainViewModel.cs | 7 +++++++ .../Diagnostics/Views/MainWindow.xaml.cs | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 07ae222a9c..d0a4ad38c5 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -21,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeDirtyRects; private bool _showFpsOverlay; + private bool _freezePopups; #nullable disable // Remove "nullable disable" after MemberNotNull will work on our CI. @@ -40,6 +41,12 @@ namespace Avalonia.Diagnostics.ViewModels Console = new ConsoleViewModel(UpdateConsoleContext); } + public bool FreezePopups + { + get => _freezePopups; + set => RaiseAndSetIfChanged(ref _freezePopups, value); + } + public bool ShouldVisualizeMarginPadding { get => _shouldVisualizeMarginPadding; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index cfb9e2ead9..810a630f46 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; @@ -173,6 +174,22 @@ namespace Avalonia.Diagnostics.Views break; } + + case RawInputModifiers.Control | RawInputModifiers.Alt when e.Key == Key.F: + { + vm.FreezePopups = !vm.FreezePopups; + + foreach (var popupRoot in GetPopupRoots(Root)) + { + if (popupRoot.Parent is Popup popup) + { + popup.IsLightDismissEnabled = !vm.FreezePopups; + } + } + + break; + } + case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D: { vm.EnableSnapshotStyles(e.Key == Key.S); From df2079454079e43a59ca3b8032ac5837a8cf8fb6 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Thu, 22 Jul 2021 12:01:20 +0200 Subject: [PATCH 021/440] Set LightDimissEnabled via SetValue() --- .../Diagnostics/Views/MainWindow.xaml.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 810a630f46..1791672940 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,15 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; @@ -18,6 +21,7 @@ namespace Avalonia.Diagnostics.Views internal class MainWindow : Window, IStyleHost { private readonly IDisposable _keySubscription; + private readonly Dictionary _frozenPopupStates; private TopLevel? _root; public MainWindow() @@ -28,6 +32,8 @@ namespace Avalonia.Diagnostics.Views .OfType() .Subscribe(RawKeyDown); + _frozenPopupStates = new Dictionary(); + EventHandler? lh = default; lh = (s, e) => { @@ -183,7 +189,25 @@ namespace Avalonia.Diagnostics.Views { if (popupRoot.Parent is Popup popup) { - popup.IsLightDismissEnabled = !vm.FreezePopups; + if (vm.FreezePopups) + { + var lightDismissEnabledState = popup.SetValue( + Popup.IsLightDismissEnabledProperty, + !vm.FreezePopups, + BindingPriority.Animation); + + if (lightDismissEnabledState != null) + { + _frozenPopupStates[popup] = lightDismissEnabledState; + } + } + else + { + if (_frozenPopupStates.TryGetValue(popup, out var state)) + { + state.Dispose(); + } + } } } From cabe6daa3f6f13d0991655465f6d286029eb1e27 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Fri, 23 Jul 2021 10:57:19 +0200 Subject: [PATCH 022/440] Review changes --- .../Diagnostics/Views/MainWindow.xaml.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 1791672940..a85fe6c0c4 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -86,6 +86,13 @@ namespace Avalonia.Diagnostics.Views base.OnClosed(e); _keySubscription.Dispose(); + foreach (var state in _frozenPopupStates) + { + state.Value.Dispose(); + } + + _frozenPopupStates.Clear(); + if (_root != null) { _root.Closed -= RootClosed; @@ -203,9 +210,11 @@ namespace Avalonia.Diagnostics.Views } else { - if (_frozenPopupStates.TryGetValue(popup, out var state)) + //TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1 + if (_frozenPopupStates.ContainsKey(popup)) { - state.Dispose(); + _frozenPopupStates[popup].Dispose(); + _frozenPopupStates.Remove(popup); } } } From c285a0e6a80faab1388d4dafcb9002a2f164f3ec Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 31 Jul 2021 17:27:22 -0400 Subject: [PATCH 023/440] Fix DataGrid wheel scroll calculation --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 83f13fe199..3887bb3380 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls private const double DATAGRID_minimumColumnHeaderHeight = 4; internal const double DATAGRID_maximumStarColumnWidth = 10000; internal const double DATAGRID_minimumStarColumnWidth = 0.001; - private const double DATAGRID_mouseWheelDelta = 72.0; + private const double DATAGRID_mouseWheelDelta = 50.0; private const double DATAGRID_maxHeadersThickness = 32768; private const double DATAGRID_defaultRowHeight = 22; @@ -2217,20 +2217,23 @@ namespace Avalonia.Controls if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0) { double scrollHeight = 0; - if (e.Delta.Y > 0) + var delta = DATAGRID_mouseWheelDelta * e.Delta.Y; + var deltaAbs = Math.Abs(delta); + + if (delta > 0) { - scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta); + scrollHeight = Math.Max(-_verticalOffset, -deltaAbs); } - else if (e.Delta.Y < 0) + else if (delta < 0) { if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible) { - scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), deltaAbs); } else { double maximum = EdgedRowsHeightCalculated - CellsHeight; - scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta); + scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), deltaAbs); } } if (scrollHeight != 0) From ca2acc951612ad7ba536d2399f60de5a053a4c84 Mon Sep 17 00:00:00 2001 From: mat1jaczyyy Date: Sun, 1 Aug 2021 03:19:23 +0200 Subject: [PATCH 024/440] WindowImpl: Don't change z-order when Position is set --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f20dae3e50..0b7bd13082 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -451,7 +451,7 @@ namespace Avalonia.Win32 value.Y, 0, 0, - SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE); + SetWindowPosFlags.SWP_NOSIZE | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_NOZORDER); } } From 98a436481064952a4176f190820cacadcc47db16 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 1 Aug 2021 01:16:38 -0400 Subject: [PATCH 025/440] Handle horizontal wheel scrolling in DataGrid --- .../ControlCatalog/Pages/DataGridPage.xaml | 2 +- src/Avalonia.Controls.DataGrid/DataGrid.cs | 57 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 820c1324e3..340b3376f5 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -28,7 +28,7 @@ DockPanel.Dock="Top"/> - + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 3887bb3380..63e6950ae8 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -25,6 +25,7 @@ using System.ComponentModel.DataAnnotations; using Avalonia.Controls.Utils; using Avalonia.Layout; using Avalonia.Controls.Metadata; +using Avalonia.Input.GestureRecognizers; namespace Avalonia.Controls { @@ -2214,35 +2215,71 @@ namespace Avalonia.Controls /// PointerWheelEventArgs protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { - if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0) + e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta); + } + + internal bool UpdateScroll(Vector delta) + { + if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0) { - double scrollHeight = 0; - var delta = DATAGRID_mouseWheelDelta * e.Delta.Y; - var deltaAbs = Math.Abs(delta); + var handled = false; + var scrollHeight = 0d; - if (delta > 0) + // Vertical scroll handling + if (delta.Y > 0) { - scrollHeight = Math.Max(-_verticalOffset, -deltaAbs); + scrollHeight = Math.Max(-_verticalOffset, -delta.Y); } - else if (delta < 0) + else if (delta.Y < 0) { if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible) { - scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), deltaAbs); + scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), -delta.Y); } else { double maximum = EdgedRowsHeightCalculated - CellsHeight; - scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), deltaAbs); + scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), -delta.Y); } } + if (scrollHeight != 0) { DisplayData.PendingVerticalScrollHeight = scrollHeight; + handled = true; + } + + // Horizontal scroll handling + if (delta.X != 0) + { + var originalHorizontalOffset = HorizontalOffset; + var horizontalOffset = originalHorizontalOffset - delta.X; + var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); + + if (horizontalOffset < 0) + { + horizontalOffset = 0; + } + if (horizontalOffset > widthNotVisible) + { + horizontalOffset = widthNotVisible; + } + + if (horizontalOffset != originalHorizontalOffset) + { + HorizontalOffset = horizontalOffset; + handled = true; + } + } + + if (handled) + { InvalidateRowsMeasure(invalidateIndividualElements: false); - e.Handled = true; + return true; } } + + return false; } /// From 7e3b6ecff5dc187c0b4a6c535673556ad8d1b168 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 1 Aug 2021 01:16:55 -0400 Subject: [PATCH 026/440] Handle touch scrolling in DataGrid --- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 2 +- .../Primitives/DataGridRowsPresenter.cs | 13 +++++++++++++ src/Avalonia.Controls.DataGrid/Themes/Default.xaml | 6 +++++- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 6 +++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 445dc541a7..47bf7b906c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -173,7 +173,7 @@ namespace Avalonia.Controls } if (OwningRow != null) { - e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index 0d19f4c479..308ebc69d4 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -5,6 +5,9 @@ using System; using System.Diagnostics; + +using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; using Avalonia.Layout; using Avalonia.Media; @@ -16,6 +19,11 @@ namespace Avalonia.Controls.Primitives /// public sealed class DataGridRowsPresenter : Panel { + public DataGridRowsPresenter() + { + AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); + } + internal DataGrid OwningGrid { get; @@ -176,6 +184,11 @@ namespace Avalonia.Controls.Primitives return new Size(totalCellsWidth + headerWidth, totalHeight); } + private void OnScrollGesture(object sender, ScrollGestureEventArgs e) + { + e.Handled = e.Handled || OwningGrid.UpdateScroll(-e.Delta); + } + #if DEBUG internal void PrintChildren() { diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 09d19c8e43..ca0873e183 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -247,7 +247,11 @@ - + + + + + diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index f2dbf42196..8ccf717d94 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -601,7 +601,11 @@ + Grid.ColumnSpan="3"> + + + + Date: Sun, 1 Aug 2021 02:57:57 -0400 Subject: [PATCH 027/440] Handle DataGridCell_PointerPressed if not touch --- src/Avalonia.Controls.DataGrid/DataGridCell.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index 241d543ec3..7dda936317 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -173,7 +173,15 @@ namespace Avalonia.Controls } if (OwningRow != null) { - OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); + + // Do not handle PointerPressed with touch, + // so we can start scroll gesture on the same event. + if (e.Pointer.Type != PointerType.Touch) + { + e.Handled = handled; + } + OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; } } From 64922a34a937757380700199fcb555f8f39f6e90 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 1 Aug 2021 21:45:07 -0400 Subject: [PATCH 028/440] Add CornerRadius to TemplatedControl and use it where possible --- samples/ControlCatalog/SideBar.xaml | 1 - samples/RenderDemo/SideBar.xaml | 1 - .../Flyouts/FlyoutPresenter.cs | 9 ----- .../Primitives/TemplatedControl.cs | 40 ++++++++++++++----- .../AutoCompleteBox.xaml | 1 + src/Avalonia.Themes.Default/Button.xaml | 3 +- .../ButtonSpinner.xaml | 2 + src/Avalonia.Themes.Default/Calendar.xaml | 3 +- .../CalendarDatePicker.xaml | 3 +- src/Avalonia.Themes.Default/CalendarItem.xaml | 5 ++- src/Avalonia.Themes.Default/Carousel.xaml | 3 +- src/Avalonia.Themes.Default/CheckBox.xaml | 1 + src/Avalonia.Themes.Default/ComboBox.xaml | 3 +- src/Avalonia.Themes.Default/ComboBoxItem.xaml | 1 + .../ContentControl.xaml | 3 +- src/Avalonia.Themes.Default/ContextMenu.xaml | 1 + .../DataValidationErrors.xaml | 1 + src/Avalonia.Themes.Default/DatePicker.xaml | 3 ++ src/Avalonia.Themes.Default/Expander.xaml | 5 ++- src/Avalonia.Themes.Default/GridSplitter.xaml | 1 + src/Avalonia.Themes.Default/ItemsControl.xaml | 1 + src/Avalonia.Themes.Default/Label.xaml | 1 + src/Avalonia.Themes.Default/ListBox.xaml | 3 +- src/Avalonia.Themes.Default/ListBoxItem.xaml | 1 + src/Avalonia.Themes.Default/Menu.xaml | 3 +- src/Avalonia.Themes.Default/MenuItem.xaml | 6 ++- .../NotificationCard.xaml | 1 + .../NumericUpDown.xaml | 2 +- src/Avalonia.Themes.Default/ProgressBar.xaml | 2 +- src/Avalonia.Themes.Default/RepeatButton.xaml | 1 + src/Avalonia.Themes.Default/Separator.xaml | 1 + src/Avalonia.Themes.Default/TabControl.xaml | 2 +- src/Avalonia.Themes.Default/TabItem.xaml | 2 +- src/Avalonia.Themes.Default/TabStripItem.xaml | 3 +- src/Avalonia.Themes.Default/TextBox.xaml | 3 +- src/Avalonia.Themes.Default/TimePicker.xaml | 3 ++ src/Avalonia.Themes.Default/ToggleButton.xaml | 3 +- src/Avalonia.Themes.Default/ToolTip.xaml | 3 +- src/Avalonia.Themes.Default/TreeView.xaml | 3 +- src/Avalonia.Themes.Default/TreeViewItem.xaml | 1 + src/Avalonia.Themes.Default/UserControl.xaml | 1 + .../Controls/AutoCompleteBox.xaml | 4 +- .../Controls/Button.xaml | 6 +-- .../Controls/ButtonSpinner.xaml | 3 +- .../Controls/Calendar.xaml | 10 ++++- .../Controls/CalendarDatePicker.xaml | 2 + .../Controls/CalendarItem.xaml | 8 +++- .../Controls/Carousel.xaml | 3 +- .../Controls/CheckBox.xaml | 13 ++---- .../Controls/ComboBox.xaml | 13 ++---- .../Controls/ComboBoxItem.xaml | 1 + .../Controls/ContentControl.xaml | 3 +- .../Controls/ContextMenu.xaml | 3 +- .../Controls/DataValidationErrors.xaml | 2 + .../Controls/DatePicker.xaml | 16 ++++++-- .../Controls/Expander.xaml | 29 +++++++------- .../Controls/GridSplitter.xaml | 1 + .../Controls/ItemsControl.xaml | 1 + .../Controls/Label.xaml | 3 +- .../Controls/ListBox.xaml | 8 ++-- .../Controls/ListBoxItem.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 1 + .../Controls/MenuItem.xaml | 6 ++- .../Controls/NotificationCard.xaml | 6 ++- .../Controls/NumericUpDown.xaml | 2 + .../Controls/ProgressBar.xaml | 3 +- .../Controls/RadioButton.xaml | 11 ++--- .../Controls/RepeatButton.xaml | 2 + .../Controls/Separator.xaml | 1 + .../Controls/Slider.xaml | 4 +- .../Controls/TabControl.xaml | 4 +- .../Controls/TabItem.xaml | 1 + .../Controls/TabStrip.xaml | 14 +++++-- .../Controls/TabStripItem.xaml | 1 + .../Controls/TextBox.xaml | 6 +-- .../Controls/TimePicker.xaml | 9 +++-- .../Controls/ToggleButton.xaml | 2 + .../Controls/ToolTip.xaml | 3 +- .../Controls/TreeView.xaml | 3 +- .../Controls/TreeViewItem.xaml | 1 + .../Controls/UserControl.xaml | 1 + 81 files changed, 223 insertions(+), 124 deletions(-) diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 7c911e91e9..2b5215a3fe 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -16,7 +16,6 @@ diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index fd23067f61..b82a7b0514 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -7,7 +7,6 @@ diff --git a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs index 10f97794d7..0f257224dd 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs @@ -6,15 +6,6 @@ namespace Avalonia.Controls { public class FlyoutPresenter : ContentControl { - public static readonly StyledProperty CornerRadiusProperty = - Border.CornerRadiusProperty.AddOwner(); - - public CornerRadius CornerRadius - { - get => GetValue(CornerRadiusProperty); - set => SetValue(CornerRadiusProperty, value); - } - protected override void OnKeyDown(KeyEventArgs e) { if (e.Key == Key.Escape) diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 9c73ff2411..59975b072d 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -5,7 +5,8 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Styling; -using Avalonia.VisualTree; + +#nullable enable namespace Avalonia.Controls.Primitives { @@ -17,13 +18,13 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty BackgroundProperty = + public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); /// /// Defines the property. /// - public static readonly StyledProperty BorderBrushProperty = + public static readonly StyledProperty BorderBrushProperty = Border.BorderBrushProperty.AddOwner(); /// @@ -32,6 +33,12 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty BorderThicknessProperty = Border.BorderThicknessProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty CornerRadiusProperty = + Border.CornerRadiusProperty.AddOwner(); + /// /// Defines the property. /// @@ -59,7 +66,7 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty ForegroundProperty = + public static readonly StyledProperty ForegroundProperty = TextBlock.ForegroundProperty.AddOwner(); /// @@ -71,8 +78,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty TemplateProperty = - AvaloniaProperty.Register(nameof(Template)); + public static readonly StyledProperty TemplateProperty = + AvaloniaProperty.Register(nameof(Template)); /// /// Defines the IsTemplateFocusTarget attached property. @@ -88,7 +95,7 @@ namespace Avalonia.Controls.Primitives "TemplateApplied", RoutingStrategies.Direct); - private IControlTemplate _appliedTemplate; + private IControlTemplate? _appliedTemplate; /// /// Initializes static members of the class. @@ -111,7 +118,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the brush used to draw the control's background. /// - public IBrush Background + public IBrush? Background { get { return GetValue(BackgroundProperty); } set { SetValue(BackgroundProperty, value); } @@ -120,7 +127,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the brush used to draw the control's border. /// - public IBrush BorderBrush + public IBrush? BorderBrush { get { return GetValue(BorderBrushProperty); } set { SetValue(BorderBrushProperty, value); } @@ -135,6 +142,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(BorderThicknessProperty, value); } } + /// + /// Gets or sets the radius of the border rounded corners. + /// + public CornerRadius CornerRadius + { + get { return GetValue(CornerRadiusProperty); } + set { SetValue(CornerRadiusProperty, value); } + } + /// /// Gets or sets the font family used to draw the control's text. /// @@ -174,7 +190,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the brush used to draw the control's text and other foreground elements. /// - public IBrush Foreground + public IBrush? Foreground { get { return GetValue(ForegroundProperty); } set { SetValue(ForegroundProperty, value); } @@ -192,7 +208,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the template that defines the control's appearance. /// - public IControlTemplate Template + public IControlTemplate? Template { get { return GetValue(TemplateProperty); } set { SetValue(TemplateProperty, value); } @@ -265,7 +281,9 @@ namespace Avalonia.Controls.Primitives var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e); +#pragma warning disable CS0618 // Type or member is obsolete OnTemplateApplied(e); +#pragma warning restore CS0618 // Type or member is obsolete RaiseEvent(e); } diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 66d0f17ede..fe4cd48e72 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -11,6 +11,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Padding="{TemplateBinding Padding}" Watermark="{TemplateBinding Watermark}" DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" /> diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml index 698ddec2a8..81d96aaa14 100644 --- a/src/Avalonia.Themes.Default/Button.xaml +++ b/src/Avalonia.Themes.Default/Button.xaml @@ -13,6 +13,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" @@ -31,4 +32,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ButtonSpinner.xaml b/src/Avalonia.Themes.Default/ButtonSpinner.xaml index 89fbb9d64d..ce2b85d2b5 100644 --- a/src/Avalonia.Themes.Default/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Default/ButtonSpinner.xaml @@ -47,6 +47,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"> @@ -73,6 +74,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}"> diff --git a/src/Avalonia.Themes.Default/Calendar.xaml b/src/Avalonia.Themes.Default/Calendar.xaml index 6bbee4ef17..4b67aa232b 100644 --- a/src/Avalonia.Themes.Default/Calendar.xaml +++ b/src/Avalonia.Themes.Default/Calendar.xaml @@ -22,10 +22,11 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" HeaderBackground="{TemplateBinding HeaderBackground}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml index aab7d06c46..57b77f70ea 100644 --- a/src/Avalonia.Themes.Default/CalendarDatePicker.xaml +++ b/src/Avalonia.Themes.Default/CalendarDatePicker.xaml @@ -88,7 +88,8 @@ \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index 9b84253c8a..0df4866184 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -9,6 +9,7 @@ diff --git a/src/Avalonia.Themes.Default/DatePicker.xaml b/src/Avalonia.Themes.Default/DatePicker.xaml index da878c88e2..c6c117138d 100644 --- a/src/Avalonia.Themes.Default/DatePicker.xaml +++ b/src/Avalonia.Themes.Default/DatePicker.xaml @@ -134,6 +134,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" IsEnabled="{TemplateBinding IsEnabled}" MinWidth="{DynamicResource DatePickerThemeMinWidth}" MaxWidth="{DynamicResource DatePickerThemeMaxWidth}" @@ -148,6 +149,7 @@ BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" @@ -242,6 +244,7 @@ diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Expander.xaml index 08d8b4c995..5e0958c54c 100644 --- a/src/Avalonia.Themes.Default/Expander.xaml +++ b/src/Avalonia.Themes.Default/Expander.xaml @@ -10,7 +10,10 @@ \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 4bfae4c223..18bf79ce6c 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -14,7 +14,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> @@ -96,7 +97,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Default/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml index 025e822404..6740be69bb 100644 --- a/src/Avalonia.Themes.Default/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml @@ -9,6 +9,7 @@ - + diff --git a/src/Avalonia.Themes.Default/RepeatButton.xaml b/src/Avalonia.Themes.Default/RepeatButton.xaml index 702e4e6ebd..a9a03c8ed5 100644 --- a/src/Avalonia.Themes.Default/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/RepeatButton.xaml @@ -20,6 +20,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" diff --git a/src/Avalonia.Themes.Default/Separator.xaml b/src/Avalonia.Themes.Default/Separator.xaml index cf0db16ee6..6a318d2e85 100644 --- a/src/Avalonia.Themes.Default/Separator.xaml +++ b/src/Avalonia.Themes.Default/Separator.xaml @@ -6,6 +6,7 @@ diff --git a/src/Avalonia.Themes.Default/TabControl.xaml b/src/Avalonia.Themes.Default/TabControl.xaml index ed2e67df28..afb5010baa 100644 --- a/src/Avalonia.Themes.Default/TabControl.xaml +++ b/src/Avalonia.Themes.Default/TabControl.xaml @@ -3,9 +3,9 @@ diff --git a/src/Avalonia.Themes.Default/TabItem.xaml b/src/Avalonia.Themes.Default/TabItem.xaml index 6e344ce58e..c7748299a0 100644 --- a/src/Avalonia.Themes.Default/TabItem.xaml +++ b/src/Avalonia.Themes.Default/TabItem.xaml @@ -12,11 +12,11 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" - Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/TabStripItem.xaml b/src/Avalonia.Themes.Default/TabStripItem.xaml index 28c4c68a3d..61eecc0395 100644 --- a/src/Avalonia.Themes.Default/TabStripItem.xaml +++ b/src/Avalonia.Themes.Default/TabStripItem.xaml @@ -9,6 +9,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" @@ -20,4 +21,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 12df4b6213..9471beaaeb 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -30,7 +30,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Default/TimePicker.xaml b/src/Avalonia.Themes.Default/TimePicker.xaml index c76f900cfe..a58fd62a99 100644 --- a/src/Avalonia.Themes.Default/TimePicker.xaml +++ b/src/Avalonia.Themes.Default/TimePicker.xaml @@ -58,6 +58,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" IsEnabled="{TemplateBinding IsEnabled}" MinWidth="{DynamicResource TimePickerThemeMinWidth}" MaxWidth="{DynamicResource TimePickerThemeMaxWidth}" @@ -71,6 +72,7 @@ BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" @@ -178,6 +180,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Padding="{DynamicResource DateTimeFlyoutBorderPadding}" MaxHeight="398"> diff --git a/src/Avalonia.Themes.Default/ToggleButton.xaml b/src/Avalonia.Themes.Default/ToggleButton.xaml index 9e05c38eef..ffebd4f63d 100644 --- a/src/Avalonia.Themes.Default/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/ToggleButton.xaml @@ -13,6 +13,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" @@ -35,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ToolTip.xaml b/src/Avalonia.Themes.Default/ToolTip.xaml index 1fc0202dd3..35c1dceb8d 100644 --- a/src/Avalonia.Themes.Default/ToolTip.xaml +++ b/src/Avalonia.Themes.Default/ToolTip.xaml @@ -9,9 +9,10 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TreeView.xaml b/src/Avalonia.Themes.Default/TreeView.xaml index 026bed5899..990d5d0823 100644 --- a/src/Avalonia.Themes.Default/TreeView.xaml +++ b/src/Avalonia.Themes.Default/TreeView.xaml @@ -8,7 +8,8 @@ + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> - + + @@ -36,6 +37,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" FontWeight="{TemplateBinding FontWeight}" diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 597f5d00ec..53d53ef127 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -16,6 +16,7 @@ + @@ -29,6 +30,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" @@ -93,8 +95,4 @@ - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml index 12b4845522..d228c37912 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ButtonSpinner.xaml @@ -58,6 +58,7 @@ + @@ -69,7 +70,7 @@ - + + + + + + - - - - - - - - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml index 5110d70a80..df800b4a06 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ContextMenu.xaml @@ -39,6 +39,7 @@ + @@ -55,7 +56,7 @@ MaxWidth="{TemplateBinding MaxWidth}" MinHeight="{TemplateBinding MinHeight}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" - CornerRadius="{DynamicResource OverlayCornerRadius}"> + CornerRadius="{TemplateBinding CornerRadius}"> @@ -88,6 +89,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" /> diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 032fdd9ae4..3e4471cada 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -8,6 +8,12 @@ + + + + + + 0,0,0,4 40 @@ -50,6 +56,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml index 3f70939953..d5d44e1270 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml @@ -1,23 +1,23 @@ - - + + Expanded content - + Expanded content - + Expanded content - + Expanded content @@ -51,6 +51,7 @@ + @@ -140,31 +141,31 @@ - - - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index 1c52c6272c..3320fc9a41 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -37,6 +37,7 @@ + @@ -59,6 +60,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" IsEnabled="{TemplateBinding IsEnabled}" MinWidth="{DynamicResource TimePickerThemeMinWidth}" MaxWidth="{DynamicResource TimePickerThemeMaxWidth}" @@ -72,11 +74,11 @@ BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="Stretch" - VerticalContentAlignment="Stretch" - CornerRadius="{DynamicResource ControlCornerRadius}" /> + VerticalContentAlignment="Stretch" /> @@ -176,13 +178,14 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index dd8e51e4e5..b1d07059b8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -18,6 +18,7 @@ + @@ -29,6 +30,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" Padding="{TemplateBinding Padding}" diff --git a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml index f7a1ebbc6b..debdfb2772 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToolTip.xaml @@ -49,6 +49,7 @@ + @@ -61,7 +62,7 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" Padding="{TemplateBinding Padding}" - CornerRadius="{DynamicResource OverlayCornerRadius}"> + CornerRadius="{TemplateBinding CornerRadius}"> + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> Date: Mon, 2 Aug 2021 10:38:40 +0300 Subject: [PATCH 029/440] add comments --- .../AvaloniaNativePlatformExtensions.cs | 35 ++++++++++++++- src/Avalonia.X11/X11Platform.cs | 43 ++++++++++++++++++- src/Windows/Avalonia.Win32/Win32Platform.cs | 35 ++++++++++++++- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 76cb7a8057..51d3556dfd 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -26,19 +26,52 @@ namespace Avalonia } } + /// + /// OSX backend options. + /// public class AvaloniaNativePlatformOptions { + /// + /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// + /// + /// Avalonia has two different renderers - Immediate and Deferred. + /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. + /// public bool UseDeferredRendering { get; set; } = true; + + /// + /// Determines whether to use GPU for rendering in your project. The default value is true. + /// public bool UseGpu { get; set; } = true; + + /// + /// Embeds popups to the window when set to true. The default value is false. + /// public bool OverlayPopups { get; set; } + + /// + /// This property should be used in case you want to build Avalonia OSX native part by yourself + /// and make your Avalonia app run with it. The default value is null. + /// public string AvaloniaNativeLibraryPath { get; set; } } // ReSharper disable once InconsistentNaming + /// + /// OSX front-end options. + /// public class MacOSPlatformOptions { + /// + /// Determines whether to show your application in the dock when it runs. The default value is true. + /// public bool ShowInDock { get; set; } = true; - + + /// + /// By default, Avalonia adds items like Quit, Hide to the OSX Application Menu. + /// You can prevent Avalonia from adding those items to the OSX Application Menu with this property. The default value is false. + /// public bool DisableDefaultApplicationMenuItems { get; set; } } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a57bdbdf87..0eb716badd 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -162,14 +162,48 @@ namespace Avalonia.X11 namespace Avalonia { - + /// + /// Platform-specific options which apply to Linux. + /// public class X11PlatformOptions { + /// + /// Enables native Linux EGL when set to true. The default value is false. + /// public bool UseEGL { get; set; } + + /// + /// Determines whether to use GPU for rendering in your project. The default value is true. + /// public bool UseGpu { get; set; } = true; + + /// + /// Embeds popups to the window when set to true. The default value is false. + /// public bool OverlayPopups { get; set; } + + /// + /// Enables global KDE menu. The default value is false. + /// public bool UseDBusMenu { get; set; } + + /// + /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// + /// + /// Avalonia has two different renderers - Immediate and Deferred. + /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. + /// public bool UseDeferredRendering { get; set; } = true; + + /// + /// Determines whether to use IME. + /// IME would be enabled by default if user input languages contain one of the following languages: Mandarin Chinese, Japanese, Vietnamese, Korean. + /// + /// + /// Input method editor is a component that enables users to generate characters not natively available + /// on their input devices by using sequences of characters or mouse operations that are natively available on their input devices. + /// public bool? EnableIme { get; set; } public IList GlProfiles { get; set; } = new List @@ -190,6 +224,13 @@ namespace Avalonia "llvmpipe" }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; + + /// + /// Enables multitouch support. The default value is false. + /// + /// + /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. + /// public bool? EnableMultiTouch { get; set; } } public static class AvaloniaX11PlatformExtensions diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 10a6db0b57..cf46db69eb 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -35,15 +35,46 @@ namespace Avalonia } } + /// + /// Platform-specific options which apply to Windows. + /// public class Win32PlatformOptions { + /// + /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// + /// + /// Avalonia has two different renderers - Immediate and Deferred. + /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. + /// public bool UseDeferredRendering { get; set; } = true; - + + /// + /// Enables ANGLE for Windows. For every Windows which is above Win 7, the default is true,otherwise-false. + /// + /// + /// GPU would not be used for rendering if you would set that to false. + /// public bool? AllowEglInitialization { get; set; } - + + /// + /// Enables multitouch support. The default value is false. + /// + /// + /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. + /// public bool? EnableMultitouch { get; set; } + + /// + /// Embeds popups to the window when set to true. The default value is false. + /// public bool OverlayPopups { get; set; } + + /// + /// Avalonia would try to use native Widows OpenGL when set to true. The default value is false. + /// public bool UseWgl { get; set; } + public IList WglProfiles { get; set; } = new List { new GlVersion(GlProfileType.OpenGL, 4, 0), From 01b87381b7d56414e9e718132a0733aedf94fc62 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 3 Aug 2021 12:22:37 +0300 Subject: [PATCH 030/440] enable multitouch --- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a57bdbdf87..4561993279 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -190,7 +190,7 @@ namespace Avalonia "llvmpipe" }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; - public bool? EnableMultiTouch { get; set; } + public bool? EnableMultiTouch { get; set; } = true; } public static class AvaloniaX11PlatformExtensions { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 10a6db0b57..8cfb910e4b 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -40,8 +40,8 @@ namespace Avalonia public bool UseDeferredRendering { get; set; } = true; public bool? AllowEglInitialization { get; set; } - - public bool? EnableMultitouch { get; set; } + + public bool? EnableMultitouch { get; set; } = true; public bool OverlayPopups { get; set; } public bool UseWgl { get; set; } public IList WglProfiles { get; set; } = new List From c2c97901118dee637bd84925af7119ed77f430ea Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 4 Aug 2021 12:21:56 +0300 Subject: [PATCH 031/440] update --- src/Avalonia.X11/X11Platform.cs | 2 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index f4a1079455..df1c542159 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -226,7 +226,7 @@ namespace Avalonia public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; /// - /// Enables multitouch support. The default value is false. + /// Enables multitouch support. The default value is true. /// /// /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 86f348bd01..0240655761 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -58,7 +58,7 @@ namespace Avalonia public bool? AllowEglInitialization { get; set; } /// - /// Enables multitouch support. The default value is false. + /// Enables multitouch support. The default value is true. /// /// /// Multitouch allows a surface (a touchpad or touchscreen) to recognize the presence of more than one point of contact with the surface at the same time. From f283921e281077c74fd6c3b135e624b00b848293 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 4 Aug 2021 12:50:15 +0300 Subject: [PATCH 032/440] fix --- src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs | 4 ++-- src/Avalonia.X11/X11Platform.cs | 6 +++--- src/Windows/Avalonia.Win32/Win32Platform.cs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs index 51d3556dfd..eef765e7ec 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs @@ -32,10 +32,10 @@ namespace Avalonia public class AvaloniaNativePlatformOptions { /// - /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. /// /// - /// Avalonia has two different renderers - Immediate and Deferred. + /// Avalonia has two rendering modes: Immediate and Deferred rendering. /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index df1c542159..1f9c91aa2b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -188,17 +188,17 @@ namespace Avalonia public bool UseDBusMenu { get; set; } /// - /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. /// /// - /// Avalonia has two different renderers - Immediate and Deferred. + /// Avalonia has two rendering modes: Immediate and Deferred rendering. /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; /// /// Determines whether to use IME. - /// IME would be enabled by default if user input languages contain one of the following languages: Mandarin Chinese, Japanese, Vietnamese, Korean. + /// IME would be enabled by default if the current user input language is one of the following: Mandarin, Japanese, Vietnamese or Korean. /// /// /// Input method editor is a component that enables users to generate characters not natively available diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 0240655761..a881c45cd0 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -41,19 +41,19 @@ namespace Avalonia public class Win32PlatformOptions { /// - /// Deferred renderer would be used on Windows when set to true. Immediate renderer would be used when set to false. The default value is true. + /// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true. /// /// - /// Avalonia has two different renderers - Immediate and Deferred. + /// Avalonia has two rendering modes: Immediate and Deferred rendering. /// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements. /// public bool UseDeferredRendering { get; set; } = true; /// - /// Enables ANGLE for Windows. For every Windows which is above Win 7, the default is true,otherwise-false. + /// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false. /// /// - /// GPU would not be used for rendering if you would set that to false. + /// GPU rendering will not be enabled if this is set to false. /// public bool? AllowEglInitialization { get; set; } From 037047299791cee0338a17f241614300bdc96e85 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 4 Aug 2021 13:13:36 +0300 Subject: [PATCH 033/440] update --- src/Avalonia.X11/X11Platform.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 1f9c91aa2b..3a919c8814 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -183,7 +183,8 @@ namespace Avalonia public bool OverlayPopups { get; set; } /// - /// Enables global KDE menu. The default value is false. + /// Enables global menu support on Linux desktop environments where it's supported (e. g. XFCE and MATE with plugin, KDE, etc). + /// The default value is false. /// public bool UseDBusMenu { get; set; } From dbb8427e3a13580ac2cc53558e40d2d50ec21396 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Wed, 4 Aug 2021 15:01:14 +0200 Subject: [PATCH 034/440] Make event actually keydown --- src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index a85fe6c0c4..ea06c33e4d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reactive.Subjects; using Avalonia.Controls; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Primitives; @@ -12,7 +10,6 @@ using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Markup.Xaml; -using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; @@ -30,6 +27,7 @@ namespace Avalonia.Diagnostics.Views _keySubscription = InputManager.Instance.Process .OfType() + .Where(x => x.Type == RawKeyEventType.KeyDown) .Subscribe(RawKeyDown); _frozenPopupStates = new Dictionary(); From 5fb8126d2dfbb55ed62772a718f1610276794961 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Wed, 4 Aug 2021 15:41:31 +0200 Subject: [PATCH 035/440] Add label for frozen popups to status bar --- .../Diagnostics/Views/MainView.xaml | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml index 8c4db33f91..6f2ac96a66 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -5,14 +5,14 @@ - + + IsEnabled="False" /> @@ -21,58 +21,68 @@ + IsEnabled="False" /> + IsEnabled="False" /> - + + IsEnabled="False" /> - + - - - + + + + Content="{Binding Content}" /> - + IsVisible="False" /> + + IsVisible="{Binding IsVisible}" /> + - - Hold Ctrl+Shift over a control to inspect. - - Focused: - - - Pointer Over: - - + + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + + + + From 7e6d59d6702e1c679c5cd77193bda8f8f3bd3812 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 5 Aug 2021 11:04:52 +0200 Subject: [PATCH 036/440] Respect CancellationToken in RunLoop. --- .../Platform/InternalPlatformThreadingInterface.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index cb1291410a..a5495fdfc9 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -21,10 +21,12 @@ namespace Avalonia.Controls.Platform public void RunLoop(CancellationToken cancellationToken) { - while (true) + var handles = new[] { _signaled, cancellationToken.WaitHandle }; + + while (!cancellationToken.IsCancellationRequested) { Signaled?.Invoke(null); - _signaled.WaitOne(); + WaitHandle.WaitAny(handles); } } From dc404b545e534ea904911ca9647f312f77e7ed6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 6 Aug 2021 08:49:05 +0200 Subject: [PATCH 037/440] Fix skia OpacityMask push and pop methods --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2352b8b076..ea4c35d6e7 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -534,7 +534,11 @@ namespace Avalonia.Skia public void PushOpacityMask(IBrush mask, Rect bounds) { // TODO: This should be disposed - var paint = new SKPaint(); + var paint = new SKPaint() + { + IsAntialias = true, + Style = SKPaintStyle.StrokeAndFill + }; Canvas.SaveLayer(paint); _maskStack.Push(CreatePaint(paint, mask, bounds, true)); @@ -543,7 +547,14 @@ namespace Avalonia.Skia /// public void PopOpacityMask() { - using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) + using (var paint = new SKPaint + { + IsAntialias = true, + Style = SKPaintStyle.StrokeAndFill, + BlendMode = SKBlendMode.DstIn, + Color = new SKColor(0, 0, 0, 255), + ColorFilter = SKColorFilter.CreateLumaColor() + }) { Canvas.SaveLayer(paint); using (var paintWrapper = _maskStack.Pop()) From 39e7d362b2fffc02c88c306301951b844d3849d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 6 Aug 2021 09:07:18 +0200 Subject: [PATCH 038/440] Enable antialiasing for PushGeometryClip --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2352b8b076..1f186396ab 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -506,7 +506,7 @@ namespace Avalonia.Skia public void PushGeometryClip(IGeometryImpl clip) { Canvas.Save(); - Canvas.ClipPath(((GeometryImpl)clip).EffectivePath); + Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); } /// diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 9336c9a7bb..09e5b7c71a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -541,7 +541,8 @@ namespace Avalonia.Direct2D1.Media ContentBounds = PrimitiveExtensions.RectangleInfinite, MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = 1, - GeometricMask = ((GeometryImpl)clip).Geometry + GeometricMask = ((GeometryImpl)clip).Geometry, + MaskAntialiasMode = AntialiasMode.PerPrimitive }; var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext); _deviceContext.PushLayer(ref parameters, layer); From 5b9f06865e212be05e8ec5c2d1657029fe0f074a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Fri, 6 Aug 2021 20:10:03 +0200 Subject: [PATCH 039/440] Update expected test files --- .../CustomRender/GeometryClip.expected.png | Bin 2258 -> 4082 bytes .../Geometry_Clip_Clips_Path.expected.png | Bin 728 -> 660 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/TestFiles/Skia/Controls/CustomRender/GeometryClip.expected.png b/tests/TestFiles/Skia/Controls/CustomRender/GeometryClip.expected.png index 535d411cc7a02d6f26a14b9ef1392a9a7cc928d6..e2604d28362214beb03612498936b8c90a420418 100644 GIT binary patch literal 4082 zcmYk92UJtdv%u+pdIt$cI!Z60H)#<9Qlx{40-+-%fFfYPpr6!$bV8MmbO_xjCQqRo$tK&=A3(X@7)9^_EYXPYTNoH;X_ z$Hj`@7PFag!N8LYtZc@qo8Mi=!rd)83#xrqg{Af^&Z21jxBcR_it$p{tt@j$SzS|* zt*))cg!o!J7mGGITCO*5qSH~z)Tdpo+=f96U`@smljDj>ARwOxe===}RJI<0*L!iO zH5LZ7%`@QDr$R)>5!c48%}Pn3wUF{b6jdcx-ssmW1k1G7)Q!7_*ftc>z4{x z1Jn~~BPM-uD{d{k2URq`jB0~os|EjH#z5pw*Rff)*zR*5>a;A%biAfn zefYDyQHh(_MN{_&%2!9yM^s}6TbIrB)-_z)E-9lNn8Jw;3qGc^zPQX8TS@3>>4NKB zLh~|uyc~tJ!%2b;FoC~6d6OIMpYo3)i8+hhvw{7UL%&ge<)0S@8_Wb`+n^?9>mT74 zInKFsraNk@O;}Z-x1EG11fY07q>@C&m9Y+N**atLI_7LoYEpd<# zO9tk)lLw9^v-eGgU1}smeep;Tw*h}QRK9@!v{OXZNsHzTZA5>E5uj~Q4U0D$@JCEF z91Y27y)RDzHjY&Y`~<5h2F$~z5L>!76EM)^SStht^gPlYhQhpYJbNEryzK*3*z~Z@ z*WoX}|JV(Y%l^r)1@LZW*_&l=+vju$mfMArM5my&zET|+HST1ZxgmBrB1?4@*|ltu zt0_EY9V09Ve4Qvy_r*!W1XMMa~m@ zq^<{PG)$O|7geP2ZJS8T2Z?9aGWaDE=*nV(yLx?(KdHs7hztM5zH0A8g`5bb?1=(W zlF&uZd_zAi?-q+k(=`{UbzneSk%C&mmMXJjE*H_4R_gdB5;GBG@6_pOzyi@)?1-5nC99_PSSlU&r#Bd~E64UYen*!Of`Ri;|xb8fdSA zF#-534HZE5nEh+0%(aoe3L(~=t~w!2%x!M>2p2Yg+Ed!cl0f)9(Y~{1IONswY@V_l zs+eXIc`m8cI}om;42=kon}jr>bfF3XaaXn?xGk&m7#_kE=6TrTGYoQ1@|8CG zisb^YoG|Wd1=y&I1?NhvJzgM&11+Tq!Vy+RI?jkU8%E$WRSfB~B6iqLY=UP^(o+)1 z=`;@*u^HJ{j@jl}qhAE{#*%N(LVk9JNhhR%tlRgo)Q0m27ij_%_8I|Mi|lbKTsr&m zNU!H4#F#@jtkeFUf79K~-tS0#rbw8j3};r?j+ff`U^%I*ffn0$_k-;VL%5r6WO%&v zy~SxeJ@}uP#I)9hfM(DVoa8Q4?pK-wbKpvtNx6L$3NT>MO8)b97h3DpX|a+r<0Cr# zWxLq}$nM`DR=)tl0sNbCC5z&8Fo%!1;`0MqniDCxn<@`Nx&oT2F=YsdU+bqNwGQ>t zKkh*GRQ2IQ{xJz5>72BWUq!6aee{5@^i`G6D}KSVSrFy?FxEX1a|tO^Ouu4-ofeF> zxNb~40IhZP^JFsAX%XjSF`S(|P4?>YYf@D*TS>vs^xPQ;WZ0Atdb*NV$j=$iUpd`U zibCSOEl1(X_5{+FKr@qW4et>#A!GirK~{6_5_#o54IMl3Pn7i{@WBf zhiaMp^zZLF6l7{&Txbq%VttD3v(^)<;$}~k+hi7*q@@LyF_BFMW1Z$u+!H_}L6PE+ zVE;-Mh~?c#A0si}eAql=oV%P6O;5E8jJ0r;y;3NQ4iN^uYDs`Bn7euywL9pE)wuJg zw2Tu~Tr_VkjVQ!458i@`9G5HX6mNEPrKREB6jTpbF1D~b9Jq_Z%h3}{f>MnEQBp-YBxZ}NDS8gQ52maME#D~HN}^cPrRt8M8(ICBts{CGOoE(hMeI%vbX)@^0job|sB#|XMuup1`kuTc_D zA{2D(g{0{>-^;BaLdf;a*h9B3pe)8}4MALkAAibb3d>_t9~gHPUm07WULsQc#x73>i8l9bO=O?fegh4? zW_#irUj^E7>htJ|?Tw#sNMSw72)en8&AZTNmS_0*YVqyxY4o{X(gNdTF*@Qtk&(=l zHMH>^g)RFa2ggN!M<#kb+AlI5bc-7BW|sJ(A3#@0rP@pX)GsIsSYM*{;Aq=?N?dqa zUT4A;|Mt}Tj^o!b1ITSl=R3DbiHf={gK#c(Y9i&2=8I9Q*}6YDe+h&!j`BdgJ(rs_ z)ooRckd754c6W*&#(%nZIVC_Ok^KYZy*Nt}aJ7`>J^UId0 zu3#xBg1ad;yCcoG zGyRjgAMRR^WVUM5V0UAhB=@8{?wYYkcb}$}3J+{J1lhkl8*mGx@J}7t#$}@AUr^j> zcf3(ul&gCn>)#1qv%bIHfD{d0L5PG>Mqe%m>kuOAofXWUT^W%55)W_h)BJe%dN7ye zgJB*;t3~Sqi6Ce#m$|#_QO2Hv?%ukG3qEfNxF&p&nS_o*sv}*`O*8?Yy_gl3y{bs6 z*tV{5>jQw{j#`?&hn=3LuS%vTb8Fi?A(!8(pViw3c6CmF7 zC6AgY{6oe6Vf$bDoUZ?B3wg{%67_5q(RsNAo0(bTUxqmF3xLm_t4ryKC_`I^TbJEMp<>&u=SE$?*acIJ9hv!IRiFN!Is|wi%jML4SnsDKoLd7~% z$7itcFlJ07;H$Y@PuXQdLMC@iVp#gN(@3lKmH7?R@O5Lo+-T&oW{C|lv^QgVw%8bb<(U9U->GsJ0|zw(XUYTAdRJ?P-n_)R&e$r~SV?kfLF|4-V^1kXiA WkKxzPlE_Isg~=@oaJ_-cv;P8IdBoQM literal 2258 zcmb_eYfuwc6wX6LQ$VW*7r{ zEgK_<3bJe%P)Y$&*&SYA>H9^}*|5uv0KgJ&mr8Imw3Y7U8XGWWBA zI0x`PJbC_#R1(Rh30`FFx?fh4NW&5qgwGLfT=n#72gal zto+<0Vcrm%|9-dPripA`)T-EFzXkd8%` z{Brn)qpl!s>DG!xMZTv%6{_6Jnyjg_GJ&jmZTxMXvXBcxJ(5-=7DcxB0N0+A=0M|7 z%LqWKHDYcZw+2C47Iv?@V|rR9$Xo6->q6GuJFx)e9hSSkDD9OSa+P{^+(E8^{yvWL zShK+QqzR=Dc-_PfLr6>FLmBwq3lgZ>Tbn~YlkW{eo!)m7;I9G^ErC{!OK(zohhF(m z=I-9mYux8|v5UpZIlul${bX0|UdoZ>o-=5BBpy1E(>iaJHtTK_ zRN;1WSCFx$vpAq7)kE&(m_Fm9(l$rEA%#`e0YO8ydxLtP`GL}Mi?sms=WbBCw1e%^ z`^*OBxyLgB(6FFX7wa=)f+uqRDjSlT8y$P`*vj`JpY{eSl`lW7Yuy z1df>!hIoc%0AVwBs1=vZR!12BxfekROwjotK$?nX+QYn81OePQhFGS_*pGsN=5XC? zjY0s>GnPtqBM@MbinctP$wj?nG(mB#c-u6@mxMe${=W0P>o!Uj{rTsjsfb?bPk$cx z&b}#Tv_)uQP~$8)FRWK$RTL;w_7qeO-1`IG#9RXhrX0GFq>pwqH(7= zh5CsdNi+^Or%XWOBY3kPv=N=R5Y)4M}ydxpS|5&PKNYl(}~b9%sdj(o5l-@$r@}A6!3$S#~?KAV|pO^ zO>qpY8TUf;v)J^d@wyzOtNp>&!p~MPgO9>&BH|bsO*j>~+aHfYf-_y@g7hoQQb#x7 zThRfNAU$4evW5f6Y-enbJ65QUM`}WfJ2f&eP-SZkdfh)l8abd!5`pf}-PN+UDd2hu zHvF6F#6$h%1G2%v>X0)+k81(XGlB!2{RLP=Bz2nYy#2xN!=00K)%L_t(|obB4XP3=GsfZ^Xc-O#{k z;sQqksXZog2`ruzd16>X|PFsNu_jeK9v^wP0Q1~3S1^FjPyRhTAa{4i+%jjbpG0WCzL3{f z0dz9sE>B)m#8(nUg=K#CWu8J zAG}nsg~`nSr&X|lJkX-=sGc=i^!-e-M2o(gSypJ#_cF}_E&5L8X`@Bo$0bd)=)1V3 zg%*7e*K{<{qVM3|RcH0pHZAnByRCr$Pm(fw=Knz5k!G=NzOD2#Rp%4-w7-_c+X4Pvv;~Ci= z&8vG~jMb=1S9Lci<0yZ+tGpGaupf4HKf4?MG+OG!-ho=>*v7+ zwk}rBhYf96ti263xK*+8cG&P1#kyr+0h6Dfx3D7WESBM+gsuERW} zZkT0D=zV39>55sngq~L>dG45nx9WLilIPFc+b~`(cJVEGU72Lry4-beNLnM*6Q&3Z^Kv_I0U}b6QMcwXaK+oKy2t zmdACew|~@=<91!@J@VvuU6+c;^TU>_A>!JAEm&GawF6tSq=;z?wrD94(H?Bs5@LLt zu!XmZ(e1*P-Xg}f4O=`{jBFpae2y3w16Dw)7!?OrLW&p@3s%HhF(MwUj5Xr(n6N_r z6Q9O~m9iv0iw!I0nfRo`%3 Date: Sat, 7 Aug 2021 13:12:09 +0200 Subject: [PATCH 040/440] Allow for controling delay of scrollbar hide/show. --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index d264ed76cc..d5d6af8bfa 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -57,6 +57,18 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty AllowAutoHideProperty = AvaloniaProperty.Register(nameof(AllowAutoHide), true); + /// + /// Defines the property. + /// + public static readonly StyledProperty HideDelayProperty = + AvaloniaProperty.Register(nameof(HideDelay), TimeSpan.FromSeconds(2)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowDelayProperty = + AvaloniaProperty.Register(nameof(ShowDelay), TimeSpan.FromSeconds(0.5)); + private Button _lineUpButton; private Button _lineDownButton; private Button _pageUpButton; @@ -126,6 +138,24 @@ namespace Avalonia.Controls.Primitives get => GetValue(AllowAutoHideProperty); set => SetValue(AllowAutoHideProperty, value); } + + /// + /// Gets a value that determines how long will be the hide delay after user stops interacting with the scrollbar. + /// + public TimeSpan HideDelay + { + get => GetValue(HideDelayProperty); + set => SetValue(HideDelayProperty, value); + } + + /// + /// Gets a value that determines how long will be the show delay when user starts interacting with the scrollbar. + /// + public TimeSpan ShowDelay + { + get => GetValue(ShowDelayProperty); + set => SetValue(ShowDelayProperty, value); + } public event EventHandler Scroll; @@ -296,12 +326,12 @@ namespace Avalonia.Controls.Primitives private void CollapseAfterDelay() { - InvokeAfterDelay(Collapse, TimeSpan.FromSeconds(2)); + InvokeAfterDelay(Collapse, HideDelay); } private void ExpandAfterDelay() { - InvokeAfterDelay(Expand, TimeSpan.FromMilliseconds(400)); + InvokeAfterDelay(Expand, ShowDelay); } private void Collapse() From da06a15d16d9fb0fbe12e970aceebedd0a7611ad Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sun, 8 Aug 2021 02:15:21 +0300 Subject: [PATCH 041/440] [OverlayPopupHost] remove render white rect override --- src/Avalonia.Controls/Primitives/OverlayPopupHost.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index 762d8d37a6..403902f676 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -140,10 +140,5 @@ namespace Avalonia.Controls.Primitives return new OverlayPopupHost(overlayLayer); } - - public override void Render(DrawingContext context) - { - context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size)); - } } } From 397267d1b5b39a11c700003039f3da67025e177f Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 9 Aug 2021 23:46:20 +0300 Subject: [PATCH 042/440] [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 043/440] 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 ae222e25e6b27b0035421d2583c2b37a4036fff1 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 10 Aug 2021 11:30:19 +0200 Subject: [PATCH 044/440] Mark IVisualTreeHost [Obsolete] --- src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs index b63c49bd5a..01212b238d 100644 --- a/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs +++ b/src/Avalonia.Visuals/VisualTree/IVisualTreeHost.cs @@ -1,8 +1,11 @@ +using System; + namespace Avalonia.VisualTree { /// /// Interface for controls that host their own separate visual tree, such as popups. /// + [Obsolete] public interface IVisualTreeHost { /// From d666a4263c580b21c7221a972ee34aab65c86c6a Mon Sep 17 00:00:00 2001 From: "Luis v.d.Eltz" Date: Tue, 10 Aug 2021 11:30:42 +0200 Subject: [PATCH 045/440] Update comment Co-authored-by: Steven Kirk --- src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs index 58174b1039..4dfe5eb174 100644 --- a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -3,7 +3,7 @@ namespace Avalonia.Controls.Diagnostics { /// - /// Helper class to provide some diagnostics insides into . + /// Helper class to provide diagnostics information for . /// public static class ToolTipDiagnostics { From 08849539a72ccb7163bee028fd70fea20aa13b67 Mon Sep 17 00:00:00 2001 From: "Luis v.d.Eltz" Date: Tue, 10 Aug 2021 11:31:39 +0200 Subject: [PATCH 046/440] Update comment Co-authored-by: Steven Kirk --- src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs index 4dfe5eb174..4acf2a217f 100644 --- a/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs +++ b/src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs @@ -7,6 +7,9 @@ namespace Avalonia.Controls.Diagnostics /// public static class ToolTipDiagnostics { + /// + /// Provides access to the internal for use in DevTools. + /// public static AvaloniaProperty ToolTipProperty = ToolTip.ToolTipProperty; } } From 71ac5e3db8990c3b5a10bdf809dad8d68ceca4e9 Mon Sep 17 00:00:00 2001 From: Luis von der Eltz Date: Tue, 10 Aug 2021 11:41:05 +0200 Subject: [PATCH 047/440] Only IPopupHost should be root visual --- src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 4b957c2382..94707ac189 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -54,8 +54,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - private bool IsRoot => Visual is TopLevel || - Visual is Popup || + private bool IsRoot => Visual is TopLevel || Visual is ContextMenu || Visual is IPopupHost; From 346015d804e777a1ea2f32f2252841a9135845ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Aug 2021 21:03:52 +0200 Subject: [PATCH 048/440] Call WindowStateChanged when setting WindowState via code. Fixes #6399 --- native/Avalonia.Native/src/OSX/window.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 9c6a0e6187..0a78558d27 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1204,6 +1204,7 @@ private: } _actualWindowState = _lastWindowState; + WindowEvents->WindowStateChanged(_actualWindowState); } From 24c8af1b626dbc2376cd67006f847b8464ef40b4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Aug 2021 21:04:37 +0200 Subject: [PATCH 049/440] Manually restore parent windows before showing child. --- native/Avalonia.Native/src/OSX/window.mm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 0a78558d27..14fe60ab0b 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -713,6 +713,12 @@ private: if(cparent == nullptr) return E_INVALIDARG; + // If one tries to show a child window with a minimized parent window, then the parent window will be + // restored but MacOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive + // state. Detect this and explicitly restore the parent window ourselves to avoid this situation. + if (cparent->WindowState() == Minimized) + cparent->SetWindowState(Normal); + [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; UpdateStyle(); From 964121312c29c43ef743f7a2d0076ec268696838 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Sat, 14 Aug 2021 13:53:49 +0300 Subject: [PATCH 050/440] fix --- build/MicroCom.targets | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/MicroCom.targets b/build/MicroCom.targets index b48e377fd4..49d2cdce72 100644 --- a/build/MicroCom.targets +++ b/build/MicroCom.targets @@ -15,7 +15,8 @@ Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs" Outputs="%(AvnComIdl.OutputFile)"> - + From 29f4806cfdeb1e59c36686b3f9d8264c009cfd19 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Mon, 16 Aug 2021 09:01:07 +0300 Subject: [PATCH 051/440] [OSX] [Native] fix tab shortcuts in NativeMenu --- native/Avalonia.Native/src/OSX/KeyTransform.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 6b7d95b619..4817ad0ccf 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -222,6 +222,7 @@ std::map s_QwertyKeyMap = { 45, "n" }, { 46, "m" }, { 47, "." }, + { 48, "\t" }, { 49, " " }, { 50, "`" }, { 51, "" }, From 08dea6498ede1422991635994409ff6eb9df81bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 16 Aug 2021 19:46:08 +0200 Subject: [PATCH 052/440] Check for TextBox.Text null in AutoCompleteBox --- src/Avalonia.Controls/AutoCompleteBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c656ba6f6c..5a6e78f441 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -2005,7 +2005,7 @@ namespace Avalonia.Controls // The TextBox.TextChanged event was not firing immediately and // was causing an immediate update, even with wrapping. If there is // a selection currently, no update should happen. - if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length) + if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != (TextBox.Text?.Length ?? 0)) { return; } @@ -2303,7 +2303,7 @@ namespace Avalonia.Controls { if (IsTextCompletionEnabled && TextBox != null && userInitiated) { - int currentLength = TextBox.Text.Length; + int currentLength = TextBox.Text?.Length ?? 0; int selectionStart = TextBoxSelectionStart; if (selectionStart == text.Length && selectionStart > _textSelectionStart) { From 834f3b01b8601790ed1e4b6bbe8b35fca47c81bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 16 Aug 2021 20:25:46 +0200 Subject: [PATCH 053/440] Add Not value converter to BoolConverters --- src/Avalonia.Base/Data/Converters/BoolConverters.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs index 9329cdd6af..3985c5e32f 100644 --- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs +++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs @@ -18,5 +18,11 @@ namespace Avalonia.Data.Converters /// public static readonly IMultiValueConverter Or = new FuncMultiValueConverter(x => x.Any(y => y)); + + /// + /// A value converter that returns true when input is false and false when input is true. + /// + public static readonly IValueConverter Not = + new FuncValueConverter(x => !x); } } From 7aa6152b340d3c65ffbdcde5761f9fc56459f2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 23 Aug 2021 16:53:07 +0200 Subject: [PATCH 054/440] Revert "Fix skia OpacityMask push and pop methods" This reverts commit dc404b545e534ea904911ca9647f312f77e7ed6d. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index c9a5544672..1f186396ab 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -534,11 +534,7 @@ namespace Avalonia.Skia public void PushOpacityMask(IBrush mask, Rect bounds) { // TODO: This should be disposed - var paint = new SKPaint() - { - IsAntialias = true, - Style = SKPaintStyle.StrokeAndFill - }; + var paint = new SKPaint(); Canvas.SaveLayer(paint); _maskStack.Push(CreatePaint(paint, mask, bounds, true)); @@ -547,14 +543,7 @@ namespace Avalonia.Skia /// public void PopOpacityMask() { - using (var paint = new SKPaint - { - IsAntialias = true, - Style = SKPaintStyle.StrokeAndFill, - BlendMode = SKBlendMode.DstIn, - Color = new SKColor(0, 0, 0, 255), - ColorFilter = SKColorFilter.CreateLumaColor() - }) + using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { Canvas.SaveLayer(paint); using (var paintWrapper = _maskStack.Pop()) From 51862d1b4b6cfc346c63eafd39369ca85259d6dc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Aug 2021 17:15:41 +0200 Subject: [PATCH 055/440] Don't display warning when TryCreateAndRegister succeeds. --- .../WinRT/Composition/WinUICompositorConnection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 1c3c959acf..bc8f5a606c 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -144,6 +144,7 @@ namespace Avalonia.Win32.WinRT.Composition try { TryCreateAndRegisterCore(angle); + return; } catch (Exception e) { From 9718a77fa95d86f3b6224f50234a2f17a4150313 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Aug 2021 23:42:13 +0200 Subject: [PATCH 056/440] Skip TextLayout render tests on OSX. They were failing due to slightly different text rendering on that platform. --- .../Media/TextFormatting/TextLayoutTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index c0a27eb9ca..b1a2d00b00 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; +using System.Runtime.InteropServices; #if AVALONIA_SKIA namespace Avalonia.Skia.RenderTests @@ -30,6 +31,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task TextLayout_Basic() { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + var t = new TextLayout( "Avalonia!", new Typeface(TestFontFamily), @@ -58,6 +63,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task TextLayout_Rotated() { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + var t = new TextLayout( "Avalonia!", new Typeface(TestFontFamily), From 0f83ccb4b026d3fec9ee4864c9874aca971feff3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 23 Aug 2021 18:30:18 -0400 Subject: [PATCH 057/440] ContentPresenter should create child without content, if template was set (#6226) * Fix #6224 * Fix data templates Match * Do not use preview features * Do not create Child if Content is null and DataTemplate was set * Update src/Avalonia.Base/Utilities/TypeUtilities.cs * Update src/Avalonia.Controls/Presenters/ContentPresenter.cs * Update src/Avalonia.Controls/Presenters/ContentPresenter.cs --- .../ControlCatalog/Pages/ComboBoxPage.xaml | 32 ++++++- src/Avalonia.Base/Utilities/TypeUtilities.cs | 34 +++++--- .../Presenters/ContentPresenter.cs | 6 +- .../Templates/FuncDataTemplate`1.cs | 6 +- .../Templates/DataTemplate.cs | 1 - .../ContentPresenterTests_Standalone.cs | 86 +++++++++++++++++++ 6 files changed, 147 insertions(+), 18 deletions(-) diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 025b85492c..d440b7cce3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -1,12 +1,20 @@ + xmlns:sys="using:System" + xmlns:col="using:System.Collections"> ComboBox A drop-down list. - + + + + Inline Items Inline Item 2 @@ -14,6 +22,24 @@ Inline Item 4 + + + + + Hello + World + + + + + + + + + + + + @@ -46,7 +72,7 @@ - + diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 9f2308a062..179ded3549 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -93,13 +93,25 @@ namespace Avalonia.Utilities return !type.IsValueType || IsNullableType(type); } + /// + /// 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. + /// + /// The type to cast to + /// The value to check if cast possible + /// True if the cast is possible, otherwise false. + public static bool CanCast(object value) + { + return value is T || (value is null && AcceptsNull(typeof(T))); + } + /// /// Try to convert a value to a type by any means possible. /// - /// The type to cast to. - /// The value to cast. + /// The type to convert to. + /// The value to convert. /// The culture to use. - /// If successful, contains the cast value. + /// If successful, contains the convert value. /// True if the cast was successful, otherwise false. public static bool TryConvert(Type to, object value, CultureInfo culture, out object result) { @@ -216,10 +228,10 @@ namespace Avalonia.Utilities /// Try to convert a value to a type using the implicit conversions allowed by the C# /// language. /// - /// The type to cast to. - /// The value to cast. - /// If successful, contains the cast value. - /// True if the cast was successful, otherwise false. + /// The type to convert to. + /// The value to convert. + /// If successful, contains the converted value. + /// True if the convert was successful, otherwise false. public static bool TryConvertImplicit(Type to, object value, out object result) { if (value == null) @@ -278,8 +290,8 @@ namespace Avalonia.Utilities /// Convert a value to a type by any means possible, returning the default for that type /// if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to.. /// The culture to use. /// A value of . public static object ConvertOrDefault(object value, Type type, CultureInfo culture) @@ -291,8 +303,8 @@ namespace Avalonia.Utilities /// Convert a value to a type using the implicit conversions allowed by the C# language or /// return the default for the type if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to. /// A value of . public static object ConvertImplicitOrDefault(object value, Type type) { diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 472727823a..1a46d84558 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -327,7 +327,11 @@ namespace Avalonia.Controls.Presenters var oldChild = Child; var newChild = content as IControl; - if (content != null && newChild == null) + // We want to allow creating Child from the Template, if Content is null. + // But it's important to not use DataTemplates, otherwise we will break content presenters in many places, + // otherwise it will blow up every ContentPresenter without Content set. + if (newChild == null + && (content != null || ContentTemplate != null)) { var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? ( diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 4a6a1c6cfb..8e7b290247 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -1,5 +1,7 @@ using System; +using Avalonia.Utilities; + namespace Avalonia.Controls.Templates { /// @@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates /// /// Whether the control can be recycled. public FuncDataTemplate(Func build, bool supportsRecycling = false) - : base(typeof(T), CastBuild(build), supportsRecycling) + : base(o => TypeUtilities.CanCast(o), CastBuild(build), supportsRecycling) { } @@ -63,7 +65,7 @@ namespace Avalonia.Controls.Templates /// The weakly typed function. private static Func CastMatch(Func f) { - return o => (o is T) && f((T)o); + return o => TypeUtilities.CanCast(o) && f((T)o); } /// diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 650534b347..b7db1a3fbb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -9,7 +9,6 @@ namespace Avalonia.Markup.Xaml.Templates { public Type DataType { get; set; } - //we need content to be object otherwise portable.xaml is crashing [Content] [TemplateContent] public object Content { get; set; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index c7aa583b6f..6b744ed79c 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -264,5 +264,91 @@ namespace Avalonia.Controls.UnitTests.Presenters // InheritanceParent is exposed via StylingParent. Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); } + + [Fact] + public void Should_Create_Child_Even_With_Null_Content_When_ContentTemplate_Is_Set() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + var textBlock = Assert.IsType(target.Child); + Assert.Equal("Hello World", textBlock.Text); + } + + [Fact] + public void Should_Not_Create_Child_Even_With_Null_Content_And_DataTemplates_InsteadOf_ContentTemplate() + { + var target = new ContentPresenter + { + DataTemplates = + { + new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }) + }, + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Not_Create_Child_When_Content_And_Template_Are_Null() + { + var target = new ContentPresenter + { + ContentTemplate = null, + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Not_Create_When_Child_Content_Is_Null_But_Expected_ValueType_With_FuncDataTemplate() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Create_Child_When_Content_Is_Null_And_Expected_NullableValueType_With_FuncDataTemplate() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + Assert.NotNull(target.Child); + } } } From 5df9e5760c8eb76dd6f3b49db77635b4eb3d4619 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 Aug 2021 14:04:40 +0200 Subject: [PATCH 058/440] Add #nullable and XML doc comments to system dialogs. --- src/Avalonia.Controls/SystemDialog.cs | 99 ++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index e74b950f23..d81926ecc7 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -4,30 +4,65 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; +#nullable enable + namespace Avalonia.Controls { + /// + /// Base class for system file dialogs. + /// public abstract class FileDialog : FileSystemDialog { + /// + /// Gets or sets a collection of filters which determine the types of files displayed in an + /// or an . + /// public List Filters { get; set; } = new List(); - public string InitialFileName { get; set; } + + /// + /// Gets or sets initial file name that is displayed when the dialog is opened. + /// + public string? InitialFileName { get; set; } } + /// + /// Base class for system file and directory dialogs. + /// public abstract class FileSystemDialog : SystemDialog { [Obsolete("Use Directory")] - public string InitialDirectory + public string? InitialDirectory { get => Directory; set => Directory = value; } - public string Directory { get; set; } + + /// + /// Gets or sets the initial directory that will be displayed when the file system dialog + /// is opened. + /// + public string? Directory { get; set; } } + /// + /// Represents a system dialog that prompts the user to select a location for saving a file. + /// public class SaveFileDialog : FileDialog { - public string DefaultExtension { get; set; } + /// + /// Gets or sets the default extension to be used to save the file (including the period "."). + /// + public string? DefaultExtension { get; set; } - public async Task ShowAsync(Window parent) + /// + /// Shows the save file dialog. + /// + /// The parent window. + /// + /// A task that on completion contains the full path of the save location, or null if the + /// dialog was canceled. + /// + public async Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -37,11 +72,25 @@ namespace Avalonia.Controls } } + /// + /// Represents a system dialog that allows the user to select one or more files to open. + /// public class OpenFileDialog : FileDialog { + /// + /// Gets or sets a value indicating whether the user can select multiple files. + /// public bool AllowMultiple { get; set; } - public Task ShowAsync(Window parent) + /// + /// Shows the open file dialog. + /// + /// The parent window. + /// + /// A task that on completion returns an array containing the full path to the selected + /// files, or null if the dialog was canceled. + /// + public Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -49,15 +98,27 @@ namespace Avalonia.Controls } } + /// + /// Represents a system dialog that allows the user to select a directory. + /// public class OpenFolderDialog : FileSystemDialog { [Obsolete("Use Directory")] - public string DefaultDirectory + public string? DefaultDirectory { get => Directory; set => Directory = value; } - public Task ShowAsync(Window parent) + + /// + /// Shows the open folder dialog. + /// + /// The parent window. + /// + /// A task that on completion returns the full path of the selected directory, or null if the + /// dialog was canceled. + /// + public Task ShowAsync(Window parent) { if(parent == null) throw new ArgumentNullException(nameof(parent)); @@ -65,14 +126,32 @@ namespace Avalonia.Controls } } + + /// + /// Base class for system dialogs. + /// public abstract class SystemDialog { - public string Title { get; set; } + /// + /// Gets or sets the dialog title. + /// + public string? Title { get; set; } } + /// + /// Represents a filter in an or an . + /// public class FileDialogFilter { - public string Name { get; set; } + /// + /// Gets or sets the name of the filter, e.g. ("Text files (.txt)"). + /// + public string? Name { get; set; } + + /// + /// Gets or sets a list of file extensions matched by the filter (e.g. "txt" or "*" for all + /// files). + /// public List Extensions { get; set; } = new List(); } } From 1a95905efe9a3c00f712c0e68c9fefaceb23103e Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Wed, 25 Aug 2021 03:37:06 +0300 Subject: [PATCH 059/440] [Menu] [Interaction] Allow end user to change menu show delay globally (#6392) Co-authored-by: Max Katz --- 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 dcc033f8197dd857ab8b02faef5ce81f3873fb37 Mon Sep 17 00:00:00 2001 From: kaminova <45092470+kaminova@users.noreply.github.com> Date: Wed, 25 Aug 2021 04:13:54 +0200 Subject: [PATCH 060/440] Close popup if combobox is not visible (#6404) * Close popup if combobox is not visible * fix unsupported feature Co-authored-by: kaminova Co-authored-by: Max Katz --- src/Avalonia.Controls/ComboBox.cs | 30 ++++++++++---- .../Mixins/DisposableMixin.cs | 38 +++++++++++++++++ src/Avalonia.Controls/Primitives/Popup.cs | 41 ++++++++----------- 3 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 src/Avalonia.Controls/Mixins/DisposableMixin.cs diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 57c07916db..274696d501 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using System.Reactive.Disposables; using Avalonia.Controls.Generators; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; @@ -80,7 +82,7 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; - private IDisposable _subscriptionsOnOpen; + private readonly CompositeDisposable _subscriptionsOnOpen = new CompositeDisposable(); /// /// Initializes static members of the class. @@ -291,6 +293,7 @@ namespace Avalonia.Controls _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; + _popup.Closed += PopupClosed; } internal void ItemFocused(ComboBoxItem dropDownItem) @@ -303,8 +306,7 @@ namespace Avalonia.Controls private void PopupClosed(object sender, EventArgs e) { - _subscriptionsOnOpen?.Dispose(); - _subscriptionsOnOpen = null; + _subscriptionsOnOpen.Clear(); if (CanFocus(this)) { @@ -316,20 +318,34 @@ namespace Avalonia.Controls { TryFocusSelectedItem(); - _subscriptionsOnOpen?.Dispose(); - _subscriptionsOnOpen = null; + _subscriptionsOnOpen.Clear(); var toplevel = this.GetVisualRoot() as TopLevel; if (toplevel != null) { - _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => + toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) => { //eat wheel scroll event outside dropdown popup while it's open if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) { ev.Handled = true; } - }, Interactivity.RoutingStrategies.Tunnel); + }, Interactivity.RoutingStrategies.Tunnel).DisposeWith(_subscriptionsOnOpen); + } + + this.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); + + foreach (var parent in this.GetVisualAncestors().OfType()) + { + parent.GetObservable(IsVisibleProperty).Subscribe(IsVisibleChanged).DisposeWith(_subscriptionsOnOpen); + } + } + + private void IsVisibleChanged(bool isVisible) + { + if (!isVisible && IsDropDownOpen) + { + IsDropDownOpen = false; } } diff --git a/src/Avalonia.Controls/Mixins/DisposableMixin.cs b/src/Avalonia.Controls/Mixins/DisposableMixin.cs new file mode 100644 index 0000000000..9b30b4ba4c --- /dev/null +++ b/src/Avalonia.Controls/Mixins/DisposableMixin.cs @@ -0,0 +1,38 @@ +using System; +using System.Reactive.Disposables; + +namespace Avalonia.Controls.Mixins +{ + /// + /// Extension methods associated with the IDisposable interface. + /// + public static class DisposableMixin + { + /// + /// Ensures the provided disposable is disposed with the specified . + /// + /// + /// The type of the disposable. + /// + /// + /// The disposable we are going to want to be disposed by the CompositeDisposable. + /// + /// + /// The to which will be added. + /// + /// + /// The disposable. + /// + public static T DisposeWith(this T item, CompositeDisposable compositeDisposable) + where T : IDisposable + { + if (compositeDisposable is null) + { + throw new ArgumentNullException(nameof(compositeDisposable)); + } + + compositeDisposable.Add(item); + return item; + } + } +} diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d5fb69a672..e804c4b4a9 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; @@ -393,18 +394,8 @@ namespace Avalonia.Controls.Primitives var handlerCleanup = new CompositeDisposable(5); - void DeferCleanup(IDisposable? disposable) - { - if (disposable is null) - { - return; - } - - handlerCleanup.Add(disposable); - } - - DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, - HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); + popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, + HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup); popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); @@ -418,19 +409,19 @@ namespace Avalonia.Controls.Primitives PlacementConstraintAdjustment, PlacementRect); - DeferCleanup(SubscribeToEventHandler>(popupHost, RootTemplateApplied, + SubscribeToEventHandler>(popupHost, RootTemplateApplied, (x, handler) => x.TemplateApplied += handler, - (x, handler) => x.TemplateApplied -= handler)); + (x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup); if (topLevel is Window window) { - DeferCleanup(SubscribeToEventHandler(window, WindowDeactivated, + SubscribeToEventHandler(window, WindowDeactivated, (x, handler) => x.Deactivated += handler, - (x, handler) => x.Deactivated -= handler)); + (x, handler) => x.Deactivated -= handler).DisposeWith(handlerCleanup); - DeferCleanup(SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, + SubscribeToEventHandler(window.PlatformImpl, WindowLostFocus, (x, handler) => x.LostFocus += handler, - (x, handler) => x.LostFocus -= handler)); + (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); } else { @@ -438,13 +429,13 @@ namespace Avalonia.Controls.Primitives if (parentPopupRoot?.Parent is Popup popup) { - DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, + SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, - (x, handler) => x.Closed -= handler)); + (x, handler) => x.Closed -= handler).DisposeWith(handlerCleanup); } } - DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); + InputManager.Instance?.Process.Subscribe(ListenForNonClientClick).DisposeWith(handlerCleanup); var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state => { @@ -466,17 +457,17 @@ namespace Avalonia.Controls.Primitives dismissLayer.IsVisible = true; dismissLayer.InputPassThroughElement = _overlayInputPassThroughElement; - DeferCleanup(Disposable.Create(() => + Disposable.Create(() => { dismissLayer.IsVisible = false; dismissLayer.InputPassThroughElement = null; - })); + }).DisposeWith(handlerCleanup); - DeferCleanup(SubscribeToEventHandler>( + SubscribeToEventHandler>( dismissLayer, PointerPressedDismissOverlay, (x, handler) => x.PointerPressed += handler, - (x, handler) => x.PointerPressed -= handler)); + (x, handler) => x.PointerPressed -= handler).DisposeWith(handlerCleanup); } } From cd80536d58834f52e6b2746463dbb2b9c0cc4759 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 Aug 2021 18:21:34 +0200 Subject: [PATCH 061/440] Added failing test for #6439. --- .../KeyboardNavigationTests_Tab.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index edcbf75a1d..9a117bb71d 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -1225,5 +1225,32 @@ namespace Avalonia.Input.UnitTests "Button2", "Button3", "Button5", "Button1", "Button6", "Button4" }, result); } + + [Fact] + public void Cannot_Focus_Child_Of_Disabled_Control() + { + Button start; + Button expected; + + var top = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + (start = new Button { Name = "Button1" }), + new Border + { + IsEnabled = false, + Child = new Button { Name = "Button2" }, + }, + (expected = new Button { Name = "Button3" }), + } + }; + + var current = (IInputElement)start; + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Same(expected, result); + } } } 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 062/440] 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 341e435321df26178dcaddfea768eaaa65a8b4d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 Aug 2021 19:10:18 +0200 Subject: [PATCH 063/440] Check enabled state as well as visibility. WPF and Avalonia's `IsEnabled` properties are slightly different. In WPF if reflects both the enabled state of the actual control and the effectively enabled state which comes from ancestor controls. In Avalonia that effectively enabled state is exposed on a different property: `IsEffectivelyEnabled`. When I ported the tab navigation code from WPF, I didn't take that into account. WPF's visibility property however doesn't reflect the state of a control's owners and so tab navigation for invisible controls works correctly. Take advantage of this fact by changing any checks for `IsVisible` to also check `IsEnabled`. --- src/Avalonia.Input/Navigation/TabNavigation.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index ed7df67bf2..c8290cb3b7 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -234,7 +234,7 @@ namespace Avalonia.Input.Navigation // Return the first visible element. var uiElement = e as InputElement; - if (uiElement is null || uiElement.IsVisible) + if (uiElement is null || IsVisibleAndEnabled(uiElement)) { if (e is IVisual elementAsVisual) { @@ -245,7 +245,7 @@ namespace Avalonia.Input.Navigation { if (children[i] is InputElement ie) { - if (ie.IsVisible) + if (IsVisibleAndEnabled(ie)) return ie; else { @@ -270,7 +270,7 @@ namespace Avalonia.Input.Navigation // Return the last visible element. var uiElement = e as InputElement; - if (uiElement == null || uiElement.IsVisible) + if (uiElement == null || IsVisibleAndEnabled(uiElement)) { var elementAsVisual = e as IVisual; @@ -283,7 +283,7 @@ namespace Avalonia.Input.Navigation { if (children[i] is InputElement ie) { - if (ie.IsVisible) + if (IsVisibleAndEnabled(ie)) return ie; else { @@ -600,7 +600,7 @@ namespace Avalonia.Input.Navigation var vchild = children[i]; if (vchild == elementAsVisual) break; - if (vchild.IsVisible == true && vchild is IInputElement ie) + if (vchild is IInputElement ie && IsVisibleAndEnabled(ie)) prev = ie; } return prev; @@ -668,5 +668,6 @@ namespace Avalonia.Input.Navigation } private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e); + private static bool IsVisibleAndEnabled(IInputElement e) => e.IsVisible && e.IsEnabled; } } From 8dbf3bf86e2ecdb3432a18ad02ab4c45189f37d2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 26 Aug 2021 17:57:35 +0200 Subject: [PATCH 064/440] fixes(Win32.Interop): field 'Direct2DImageSurface._oldDpi' is never assigned to, and will always have its default value --- src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs index 5b04c5d7ff..bc0a399d7f 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/Direct2DImageSurface.cs @@ -150,6 +150,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_image == null || _oldDpi.X != dpi.X || _oldDpi.Y != dpi.Y) { _image = new D3DImage(dpi.X, dpi.Y); + _oldDpi = dpi; } _impl.ImageSource = _image; From 3867a68ade09cd12910afc5256e7b73c05703571 Mon Sep 17 00:00:00 2001 From: Tako <53405089+Takoooooo@users.noreply.github.com> Date: Thu, 26 Aug 2021 22:07:58 +0300 Subject: [PATCH 065/440] Fix EntryPointNotFound on Windows 8 due to missing version check (#6471) * Prevent calls to unavailable entrypoint 'GetDpiForMonitor' on Win8 (#5357) * Introduce Windows8 platform constant * wip Co-authored-by: Tim Schneeberger Co-authored-by: Tim Schneeberger Co-authored-by: Dan Walmsley --- src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj | 1 + src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs | 3 ++- src/Windows/Avalonia.Win32/FramebufferManager.cs | 2 +- src/Windows/Avalonia.Win32/PlatformConstants.cs | 7 ++++++- src/Windows/Avalonia.Win32/Win32GlManager.cs | 4 +--- src/Windows/Avalonia.Win32/Win32Platform.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 4 ++-- 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index cda95d2ebb..8533e1e176 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs index 589f85f208..0597e88ed6 100644 --- a/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/HwndRenderTarget.cs @@ -1,4 +1,5 @@ using Avalonia.Platform; +using Avalonia.Win32; using Avalonia.Win32.Interop; using SharpDX; using SharpDX.DXGI; @@ -21,7 +22,7 @@ namespace Avalonia.Direct2D1 protected override Size2F GetWindowDpi() { - if (UnmanagedMethods.ShCoreAvailable) + if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) { uint dpix, dpiy; diff --git a/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index 699dc7c25d..0240ac9701 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -87,7 +87,7 @@ namespace Avalonia.Win32 private Vector GetCurrentDpi() { - if (UnmanagedMethods.ShCoreAvailable) + if (UnmanagedMethods.ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) { var monitor = UnmanagedMethods.MonitorFromWindow(_hwnd, UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST); diff --git a/src/Windows/Avalonia.Win32/PlatformConstants.cs b/src/Windows/Avalonia.Win32/PlatformConstants.cs index af1eca29be..9dd4780637 100644 --- a/src/Windows/Avalonia.Win32/PlatformConstants.cs +++ b/src/Windows/Avalonia.Win32/PlatformConstants.cs @@ -1,8 +1,13 @@ +using System; + namespace Avalonia.Win32 { - static class PlatformConstants + public static class PlatformConstants { public const string WindowHandleType = "HWND"; public const string CursorHandleType = "HCURSOR"; + + public static readonly Version Windows8 = new Version(6, 2); + public static readonly Version Windows7 = new Version(6, 1); } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index e159ba3b78..e70ea52106 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,4 +1,3 @@ -using System; using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; @@ -9,7 +8,6 @@ namespace Avalonia.Win32 { static class Win32GlManager { - private static readonly Version Windows7 = new Version(6, 1); public static void Initialize() { @@ -22,7 +20,7 @@ namespace Avalonia.Win32 return wgl; } - if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > Windows7) + if (opts?.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index a881c45cd0..c011a458c3 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -94,7 +94,7 @@ namespace Avalonia namespace Avalonia.Win32 { - class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl + public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IPlatformLifetimeEventsImpl { private static readonly Win32Platform s_instance = new Win32Platform(); private static Thread _uiThread; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0b7bd13082..3b7d3efa2f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -764,8 +764,8 @@ namespace Avalonia.Win32 RegisterTouchWindow(_hwnd, 0); } - if (ShCoreAvailable) - { + if (ShCoreAvailable && Win32Platform.WindowsVersion > PlatformConstants.Windows8) + { var monitor = MonitorFromWindow( _hwnd, MONITOR.MONITOR_DEFAULTTONEAREST); From 158ebe7710f9cdc8ec69a113200a6b8a66566695 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 26 Aug 2021 22:52:49 +0200 Subject: [PATCH 066/440] 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 067/440] 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 068/440] 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 8b6f264379637b3b2c0a770327171765e880edc5 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 15:50:55 +0200 Subject: [PATCH 069/440] ref(test): removed warning CS0105 The using directive for 'System' appeared previously in this namespace --- tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 2a3db9992c..b0eb694944 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -10,7 +10,6 @@ using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; -using System; using Avalonia.Input.Raw; using Factory = System.Func, Avalonia.Controls.Window, Avalonia.AvaloniaObject>; From 7d8e6ea75f908f334f4413af18780f7946ac1a9f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 15:54:33 +0200 Subject: [PATCH 070/440] ref(tests): warning CS0067 The event 'KeyboardDeviceTests.DelegateCommand.CanExecuteChanged' is never used --- tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index 7730cee78c..c354dbe72e 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -2,7 +2,6 @@ using System.Windows.Input; using Avalonia.Controls; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.UnitTests; using Moq; using Xunit; @@ -126,7 +125,7 @@ namespace Avalonia.Input.UnitTests { private readonly Action _action; public DelegateCommand(Action action) => _action = action; - public event EventHandler CanExecuteChanged; + public event EventHandler CanExecuteChanged { add { } remove { } } public bool CanExecute(object parameter) => true; public void Execute(object parameter) => _action(); } From df5250e155410e7cbb238fdc75a9f7ee71816aaf Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 16:16:06 +0200 Subject: [PATCH 071/440] ref(tests): warning CS0067 The event 'TrackingResourceProvider.OwnerChanged' is never used --- .../MarkupExtensions/DynamicResourceExtensionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index e7f0230254..592dbfc0d1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -923,7 +923,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public bool HasResources => true; public List RequestedResources { get; } = new List(); - public event EventHandler OwnerChanged; + public event EventHandler OwnerChanged { add { } remove { } } public void AddOwner(IResourceHost owner) => Owner = owner; public void RemoveOwner(IResourceHost owner) => Owner = null; From 7ee5446ee4838b06d483e1cfe47e69812219029a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 27 Aug 2021 18:12:06 +0200 Subject: [PATCH 072/440] fixes(DataGrid): Warning CS0649 Field 'DataGridPathGroupDescription._valueConverter' is never assigned to, and will always have its default value null --- .../Collections/DataGridGroupDescription.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs index 9d8ebbfac1..587dd228a3 100644 --- a/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs +++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs @@ -83,8 +83,9 @@ namespace Avalonia.Collections if (key == null) key = item; - if (_valueConverter != null) - key = _valueConverter.Convert(key, typeof(object), level, culture); + var valueConverter = ValueConverter; + if (valueConverter != null) + key = valueConverter.Convert(key, typeof(object), level, culture); return key; } @@ -99,6 +100,8 @@ namespace Avalonia.Collections } public override string PropertyName => _propertyPath; + public IValueConverter ValueConverter { get => _valueConverter; set => _valueConverter = value; } + private Type GetPropertyType(object o) { return o.GetType().GetNestedPropertyType(_propertyPath); From 9682f014b2e71ded52b82d0e602a56506147b883 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Sat, 28 Aug 2021 15:41:52 +0300 Subject: [PATCH 073/440] [OSX] fix middle button #5784 --- native/Avalonia.Native/src/OSX/window.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 14fe60ab0b..e15f4cc311 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1672,6 +1672,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = true; [self mouseEvent:event withType:MiddleButtonDown]; @@ -1704,6 +1705,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { switch(event.buttonNumber) { + case 2: case 3: _isMiddlePressed = false; [self mouseEvent:event withType:MiddleButtonUp]; From 56709c9982d78063984c4865278a7afd913e571b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 Aug 2021 14:59:29 -0400 Subject: [PATCH 074/440] Add text box clipboard events --- src/Avalonia.Controls/TextBox.cs | 47 +++++++++++++++---- .../TextBoxClipboardEventArgs.cs | 18 +++++++ 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 src/Avalonia.Controls/TextBoxClipboardEventArgs.cs diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 0eade8d6df..657d8fae8f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -500,6 +500,10 @@ namespace Avalonia.Controls } } + public event EventHandler CopyingToClipboard; + public event EventHandler CuttingToClipboard; + public event EventHandler PastingFromClipboard; + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { _presenter = e.NameScope.Get("PART_TextPresenter"); @@ -638,27 +642,54 @@ namespace Avalonia.Controls public async void Cut() { var text = GetSelection(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } - SnapshotUndoRedo(); - Copy(); - DeleteSelection(); + var eventArgs = new TextBoxClipboardEventArgs(); + CuttingToClipboard?.Invoke(this, eventArgs); + if (!eventArgs.Handled) + { + SnapshotUndoRedo(); + await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) + .SetTextAsync(text); + DeleteSelection(); + } } public async void Copy() { var text = GetSelection(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } - await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) - .SetTextAsync(text); + var eventArgs = new TextBoxClipboardEventArgs(); + CopyingToClipboard?.Invoke(this, eventArgs); + if (!eventArgs.Handled) + { + await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) + .SetTextAsync(text); + } } public async void Paste() { + var eventArgs = new TextBoxClipboardEventArgs(); + PastingFromClipboard?.Invoke(this, eventArgs); + if (eventArgs.Handled) + { + return; + } + var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync(); - if (text is null) return; + if (string.IsNullOrEmpty(text)) + { + return; + } SnapshotUndoRedo(); HandleTextInput(text); diff --git a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs new file mode 100644 index 0000000000..0fe4cadf39 --- /dev/null +++ b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Provides event data for the , and events. + /// + /// + /// If you perform any action in the handler for a clipboard event, set the Handled property to true; otherwise, the default action is performed. + /// + public class TextBoxClipboardEventArgs : EventArgs + { + /// + /// Gets or sets a value that marks the event as handled. A true value for Handled prevents most handlers along the event from handling the same event again. + /// + public bool Handled { get; set; } + } +} From 825ddc9ccaa2e3d7e002564f96f22c3b1d23a2d8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 Aug 2021 18:29:56 -0400 Subject: [PATCH 075/440] Use routed events --- src/Avalonia.Controls/TextBox.cs | 44 +++++++++++++++---- .../TextBoxClipboardEventArgs.cs | 18 -------- 2 files changed, 35 insertions(+), 27 deletions(-) delete mode 100644 src/Avalonia.Controls/TextBoxClipboardEventArgs.cs diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 657d8fae8f..9eae928eeb 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -145,6 +145,18 @@ namespace Avalonia.Controls (o, v) => o.UndoLimit = v, unsetValue: -1); + public static readonly RoutedEvent CopyingToClipboardEvent = + RoutedEvent.Register( + "CopyingToClipboard", RoutingStrategies.Bubble); + + public static readonly RoutedEvent CuttingToClipboardEvent = + RoutedEvent.Register( + "CuttingToClipboard", RoutingStrategies.Bubble); + + public static readonly RoutedEvent PastingFromClipboardEvent = + RoutedEvent.Register( + "PastingFromClipboard", RoutingStrategies.Bubble); + readonly struct UndoRedoState : IEquatable { public string Text { get; } @@ -500,9 +512,23 @@ namespace Avalonia.Controls } } - public event EventHandler CopyingToClipboard; - public event EventHandler CuttingToClipboard; - public event EventHandler PastingFromClipboard; + public event EventHandler CopyingToClipboard + { + add => AddHandler(CopyingToClipboardEvent, value); + remove => RemoveHandler(CopyingToClipboardEvent, value); + } + + public event EventHandler CuttingToClipboard + { + add => AddHandler(CuttingToClipboardEvent, value); + remove => RemoveHandler(CuttingToClipboardEvent, value); + } + + public event EventHandler PastingFromClipboard + { + add => AddHandler(PastingFromClipboardEvent, value); + remove => RemoveHandler(PastingFromClipboardEvent, value); + } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -647,8 +673,8 @@ namespace Avalonia.Controls return; } - var eventArgs = new TextBoxClipboardEventArgs(); - CuttingToClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent); + RaiseEvent(eventArgs); if (!eventArgs.Handled) { SnapshotUndoRedo(); @@ -666,8 +692,8 @@ namespace Avalonia.Controls return; } - var eventArgs = new TextBoxClipboardEventArgs(); - CopyingToClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent); + RaiseEvent(eventArgs); if (!eventArgs.Handled) { await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))) @@ -677,8 +703,8 @@ namespace Avalonia.Controls public async void Paste() { - var eventArgs = new TextBoxClipboardEventArgs(); - PastingFromClipboard?.Invoke(this, eventArgs); + var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent); + RaiseEvent(eventArgs); if (eventArgs.Handled) { return; diff --git a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs b/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs deleted file mode 100644 index 0fe4cadf39..0000000000 --- a/src/Avalonia.Controls/TextBoxClipboardEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - /// - /// Provides event data for the , and events. - /// - /// - /// If you perform any action in the handler for a clipboard event, set the Handled property to true; otherwise, the default action is performed. - /// - public class TextBoxClipboardEventArgs : EventArgs - { - /// - /// Gets or sets a value that marks the event as handled. A true value for Handled prevents most handlers along the event from handling the same event again. - /// - public bool Handled { get; set; } - } -} From 10e3fc78283e3c6beff5ce7d066e8b703eeed8ef Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Aug 2021 10:39:17 +0200 Subject: [PATCH 076/440] fixes(DataGrid): Warning CS0414 The field 'CellEditBinding.SubjectWrapper._settingSourceValue' is assigned but its value is never used --- .../Utils/CellEditBinding.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs index 6ac77fbb99..1d1a595ccf 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs @@ -1,10 +1,8 @@ using Avalonia.Data; using Avalonia.Reactive; using System; -using System.ComponentModel.DataAnnotations; using System.Collections.Generic; using System.Reactive.Subjects; -using System.Text; namespace Avalonia.Controls.Utils { @@ -67,11 +65,14 @@ namespace Avalonia.Controls.Utils private void SetSourceValue(object value) { - _settingSourceValue = true; + if (!_settingSourceValue) + { + _settingSourceValue = true; - _sourceSubject.OnNext(value); + _sourceSubject.OnNext(value); - _settingSourceValue = false; + _settingSourceValue = false; + } } private void SetControlValue(object value) { @@ -157,4 +158,4 @@ namespace Avalonia.Controls.Utils } } } -} \ No newline at end of file +} From c8b97e358e5aac8c8a320ef45d64fe76f4a118af Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Aug 2021 17:39:02 +0200 Subject: [PATCH 077/440] fixes(Doc): fixes AvaloniaList XML Comments --- src/Avalonia.Base/Collections/AvaloniaList.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 2c7f34c5be..2f1cb2888e 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -280,8 +280,8 @@ namespace Avalonia.Collections /// /// Gets a range of items from the collection. /// - /// The first index to remove. - /// The number of items to remove. + /// The zero-based index at which the range starts. + /// The number of elements in the range. public IEnumerable GetRange(int index, int count) { return _inner.GetRange(index, count); @@ -455,7 +455,7 @@ namespace Avalonia.Collections } /// - /// Ensures that the capacity of the list is at least . + /// Ensures that the capacity of the list is at least . /// /// The capacity. public void EnsureCapacity(int capacity) From 7a027792cbfc4426bea431ea4b24268ef68c68a3 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Mon, 30 Aug 2021 18:39:21 +0300 Subject: [PATCH 078/440] 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 079/440] 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 080/440] 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 081/440] 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 082/440] [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 083/440] 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 084/440] 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 085/440] 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 086/440] 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 087/440] 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 088/440] fixes(DataGrid): Issue #6508 does not re-enter the edit mode after pressing ESC --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index ab1aff9220..cb1ce0c731 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -4489,8 +4489,19 @@ namespace Avalonia.Controls element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext); if (element != null) { - // Subscribe to the new element's events - element.Initialized += EditingElement_Initialized; + + if (element.IsInitialized) + { + Threading.Dispatcher.UIThread.Post(() => + { + PreparingCellForEditPrivate(element as Control); + }); + } + else + { + // Subscribe to the new element's events + element.Initialized += EditingElement_Initialized; + } } } else From 3aa38398ff28012d7d296966a6df7d86e42f1424 Mon Sep 17 00:00:00 2001 From: Sergey Mikolaytis Date: Tue, 31 Aug 2021 23:47:25 +0300 Subject: [PATCH 089/440] Fix alt down shortcuts and allow alt down handling for end users (#6491) * [Menu] [Interaction] Allow end user to change menu show delay globally * Fix all alt down handle = true by AccessKeyHandler Co-authored-by: Max Katz --- src/Avalonia.Input/AccessKeyHandler.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 5c4af68d79..5082265ea6 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/src/Avalonia.Input/AccessKeyHandler.cs @@ -157,10 +157,9 @@ namespace Avalonia.Input _restoreFocusElement?.Focus(); _restoreFocusElement = null; + + e.Handled = true; } - - // We always handle the Alt key. - e.Handled = true; } else if (_altIsDown) { From 063e5e7be3b94d4a3f929acbd2ac552534877e41 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 1 Sep 2021 11:42:16 +0300 Subject: [PATCH 090/440] 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 091/440] 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 092/440] 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 093/440] 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 094/440] 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 095/440] 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 096/440] 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 097/440] 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 098/440] 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 099/440] 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 100/440] 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 101/440] 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 102/440] 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 103/440] 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 104/440] 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 105/440] 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 106/440] 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 107/440] 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 108/440] 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 109/440] 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 110/440] 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 111/440] 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 112/440] 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 113/440] 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 114/440] 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 115/440] 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 120/440] 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 121/440] 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 122/440] 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 123/440] 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 124/440] 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 125/440] 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 126/440] 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 127/440] 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 128/440] 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 129/440] 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 130/440] 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 131/440] 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 132/440] 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 133/440] 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 134/440] 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 135/440] 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 136/440] 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 137/440] 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 138/440] 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 139/440] 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 140/440] 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 141/440] 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 142/440] 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 227/440] 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 228/440] 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 229/440] 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 230/440] 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 231/440] 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 232/440] 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 233/440] 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 234/440] 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 235/440] 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 236/440] 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 237/440] 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 238/440] 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 239/440] 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 240/440] 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 241/440] 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 242/440] 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 243/440] 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 244/440] 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 245/440] 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 246/440] 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 247/440] 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 248/440] 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 249/440] 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 250/440] 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 251/440] 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 252/440] 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 253/440] 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 254/440] 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 255/440] 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 256/440] 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 257/440] 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 258/440] 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 259/440] 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 260/440] 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 261/440] 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 262/440] 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 263/440] 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 264/440] 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 265/440] [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 266/440] 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 267/440] 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 268/440] 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 269/440] 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 270/440] 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 271/440] 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 272/440] 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 273/440] 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 274/440] 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 275/440] 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 276/440] 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 277/440] 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 278/440] 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 279/440] 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 280/440] 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 281/440] 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 282/440] 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 283/440] 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 284/440] 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 285/440] 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 286/440] 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 287/440] 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 288/440] 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 305/440] 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 306/440] 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 307/440] 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 308/440] 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 309/440] 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 310/440] 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 311/440] 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 312/440] 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 313/440] 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 314/440] 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 315/440] 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 316/440] 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 317/440] 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 318/440] 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 319/440] 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