diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 88c4d36282..e636461ad9 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index dd241409c7..fb945a105e 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1209,6 +1209,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _queuedDisplayFromThread; NSTrackingArea* _area; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed, _isMouseOver; + AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; bool _lastKeyHandled; AvnPixelSize _lastPixelSize; @@ -1251,6 +1252,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _lastPixelSize.Height = 100; _lastPixelSize.Width = 100; [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; + + _modifierState = AvnInputModifiersNone; return self; } @@ -1594,6 +1597,63 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return result; } +- (void)flagsChanged:(NSEvent *)event +{ + auto newModifierState = [self getModifiers:[event modifierFlags]]; + + bool isAltCurrentlyPressed = (_modifierState & Alt) == Alt; + bool isControlCurrentlyPressed = (_modifierState & Control) == Control; + bool isShiftCurrentlyPressed = (_modifierState & Shift) == Shift; + bool isCommandCurrentlyPressed = (_modifierState & Windows) == Windows; + + bool isAltPressed = (newModifierState & Alt) == Alt; + bool isControlPressed = (newModifierState & Control) == Control; + bool isShiftPressed = (newModifierState & Shift) == Shift; + bool isCommandPressed = (newModifierState & Windows) == Windows; + + + if (isAltPressed && !isAltCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isAltCurrentlyPressed && !isAltPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isControlPressed && !isControlCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if (isControlCurrentlyPressed && !isControlPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if (isShiftPressed && !isShiftCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isShiftCurrentlyPressed && !isShiftPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + if(isCommandPressed && !isCommandCurrentlyPressed) + { + [self keyboardEvent:event withType:KeyDown]; + } + else if(isCommandCurrentlyPressed && ! isCommandPressed) + { + [self keyboardEvent:event withType:KeyUp]; + } + + _modifierState = newModifierState; + + [[self inputContext] handleEvent:event]; + [super flagsChanged:event]; +} + - (void)keyDown:(NSEvent *)event { [self keyboardEvent:event withType:KeyDown]; diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index 2dbc095156..db1e16166a 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -9,6 +9,10 @@ ControlCatalog.iOS Resources ControlCatalogiOS + true + NSUrlSessionHandler + PackageReference + automatic true @@ -19,8 +23,8 @@ prompt 4 false - i386 - SdkOnly + x86_64 + None True 9.1 False @@ -43,7 +47,7 @@ prompt 4 None - i386 + x86_64 false @@ -173,8 +177,10 @@ {d0a739b9-3c68-4ba6-a328-41606954b6bd} ControlCatalog + + diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml index 7e629db2da..6902b27715 100644 --- a/samples/ControlCatalog/Pages/SplitViewPage.xaml +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml @@ -58,12 +58,18 @@ + - + + + + + + @@ -76,7 +82,7 @@ - + diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 7ce2624d02..2556bd4c4c 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -436,6 +436,29 @@ namespace Avalonia.Controls.Selection } } + private protected override bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) + { + if (!base.IsValidCollectionChange(e)) + { + return false; + } + + if (ItemsView is object && e.Action == NotifyCollectionChangedAction.Add) + { + if (e.NewStartingIndex <= _selectedIndex) + { + return _selectedIndex + e.NewItems.Count < ItemsView.Count; + } + + if (e.NewStartingIndex <= _anchorIndex) + { + return _anchorIndex + e.NewItems.Count < ItemsView.Count; + } + } + + return true; + } + protected override void OnSourceCollectionChangeFinished() { if (_operation is object) diff --git a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs index ff3b8f43a8..230575074a 100644 --- a/src/Avalonia.Controls/Selection/SelectionNodeBase.cs +++ b/src/Avalonia.Controls/Selection/SelectionNodeBase.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Controls.Utils; #nullable enable @@ -234,6 +235,11 @@ namespace Avalonia.Controls.Selection var shiftIndex = -1; List? removed = null; + if (!IsValidCollectionChange(e)) + { + return; + } + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -276,6 +282,34 @@ namespace Avalonia.Controls.Selection } } + private protected virtual bool IsValidCollectionChange(NotifyCollectionChangedEventArgs e) + { + // If the selection is modified in a CollectionChanged handler before the selection + // model's CollectionChanged handler has had chance to run then we can end up with + // a selected index that refers to the *new* state of the Source intermixed with + // indexes that reference an old state of the source. + // + // There's not much we can do in this situation, so detect whether shifting the + // current selected indexes would result in an invalid index in the source, and if + // so bail. + // + // See unit test Handles_Selection_Made_In_CollectionChanged for more details. + if (ItemsView is object && + RangesEnabled && + Ranges.Count > 0 && + e.Action == NotifyCollectionChangedAction.Add) + { + var lastIndex = Ranges.Last().End; + + if (e.NewStartingIndex <= lastIndex) + { + return lastIndex + e.NewItems.Count < ItemsView.Count; + } + } + + return true; + } + private protected struct CollectionChangeState { public int ShiftIndex; diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index fc3ff51f24..b71858f796 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls private bool _isPaneOpen; private Panel _pane; - private CompositeDisposable _pointerDisposables; + private IDisposable _pointerDisposable; public SplitView() { @@ -320,37 +320,14 @@ namespace Avalonia.Controls var topLevel = this.VisualRoot; if (topLevel is Window window) { - //Logic adapted from Popup - //Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane - IDisposable subscribeToEventHandler(T target, TEventHandler handler, - Action subscribe, Action unsubscribe) - { - subscribe(target, handler); - return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler)); - } - - _pointerDisposables = new CompositeDisposable( - window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel), - InputManager.Instance?.Process.Subscribe(OnNonClientClick), - subscribeToEventHandler(window, Window_Deactivated, - (x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler), - subscribeToEventHandler(window.PlatformImpl, OnWindowLostFocus, - (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler)); + _pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); } } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - _pointerDisposables?.Dispose(); - } - - private void OnWindowLostFocus() - { - if (IsPaneOpen && ShouldClosePane()) - { - IsPaneOpen = false; - } + _pointerDisposable?.Dispose(); } private void PointerPressedOutside(object sender, PointerPressedEventArgs e) @@ -371,7 +348,12 @@ namespace Avalonia.Controls var src = e.Source as IVisual; while (src != null) { - if (src == _pane) + // Make assumption that if Popup is in visual tree, + // owning control is within pane + // This works because if pane is triggered to close + // when clicked anywhere else in Window, the pane + // would close before the popup is opened + if (src == _pane || src is PopupRoot) { closePane = false; break; @@ -385,31 +367,7 @@ namespace Avalonia.Controls e.Handled = true; } } - - private void OnNonClientClick(RawInputEventArgs obj) - { - if (!IsPaneOpen) - { - return; - } - - var mouse = obj as RawPointerEventArgs; - if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) - - { - if (ShouldClosePane()) - IsPaneOpen = false; - } - } - - private void Window_Deactivated(object sender, EventArgs e) - { - if (IsPaneOpen && ShouldClosePane()) - { - IsPaneOpen = false; - } - } - + private bool ShouldClosePane() { return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs index 915ba9eeee..d4b988acd4 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -12,12 +12,13 @@ namespace Avalonia.Diagnostics.ViewModels private readonly IVisual _control; private readonly IDictionary> _propertyIndex; private AvaloniaPropertyViewModel _selectedProperty; - private string _propertyFilter; - public ControlDetailsViewModel(IVisual control, string propertyFilter) + public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control) { _control = control; + TreePage = treePage; + var properties = GetAvaloniaProperties(control) .Concat(GetClrProperties(control)) .OrderBy(x => x, PropertyComparer.Instance) @@ -25,7 +26,6 @@ namespace Avalonia.Diagnostics.ViewModels .ToList(); _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); - _propertyFilter = propertyFilter; var view = new DataGridCollectionView(properties); view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); @@ -43,19 +43,9 @@ namespace Avalonia.Diagnostics.ViewModels } } - public DataGridCollectionView PropertiesView { get; } + public TreePageViewModel TreePage { get; } - public string PropertyFilter - { - get => _propertyFilter; - set - { - if (RaiseAndSetIfChanged(ref _propertyFilter, value)) - { - PropertiesView.Refresh(); - } - } - } + public DataGridCollectionView PropertiesView { get; } public AvaloniaPropertyViewModel SelectedProperty { @@ -137,9 +127,14 @@ namespace Avalonia.Diagnostics.ViewModels private bool FilterProperty(object arg) { - if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property) + if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property) { - return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1; + if (TreePage.UseRegexFilter) + { + return TreePage.FilterRegex?.IsMatch(property.Name) ?? true; + } + + return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1; } return true; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index d02c8994eb..bd65a3b06b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -1,15 +1,20 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text.RegularExpressions; using Avalonia.Controls; -using Avalonia.Controls.Selection; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ViewModelBase, IDisposable + internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo { + private readonly Dictionary _errors = new Dictionary(); private TreeNode _selectedNode; private ControlDetailsViewModel _details; - private string _propertyFilter; + private string _propertyFilter = string.Empty; + private bool _useRegexFilter; public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes) { @@ -26,15 +31,10 @@ namespace Avalonia.Diagnostics.ViewModels get => _selectedNode; private set { - if (Details != null) - { - _propertyFilter = Details.PropertyFilter; - } - if (RaiseAndSetIfChanged(ref _selectedNode, value)) { Details = value != null ? - new ControlDetailsViewModel(value.Visual, _propertyFilter) : + new ControlDetailsViewModel(this, value.Visual) : null; } } @@ -54,6 +54,63 @@ namespace Avalonia.Diagnostics.ViewModels } } + public Regex FilterRegex { get; set; } + + private void UpdateFilterRegex() + { + void ClearError() + { + if (_errors.Remove(nameof(PropertyFilter))) + { + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter))); + } + } + + if (UseRegexFilter) + { + try + { + FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled); + ClearError(); + } + catch (Exception exception) + { + _errors[nameof(PropertyFilter)] = exception.Message; + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter))); + } + } + else + { + ClearError(); + } + } + + public string PropertyFilter + { + get => _propertyFilter; + set + { + if (RaiseAndSetIfChanged(ref _propertyFilter, value)) + { + UpdateFilterRegex(); + Details.PropertiesView.Refresh(); + } + } + } + + public bool UseRegexFilter + { + get => _useRegexFilter; + set + { + if (RaiseAndSetIfChanged(ref _useRegexFilter, value)) + { + UpdateFilterRegex(); + Details.PropertiesView.Refresh(); + } + } + } + public void Dispose() { foreach (var node in Nodes) @@ -130,5 +187,17 @@ namespace Avalonia.Diagnostics.ViewModels return null; } + + public IEnumerable GetErrors(string propertyName) + { + if (_errors.TryGetValue(propertyName, out var error)) + { + yield return error; + } + } + + public bool HasErrors => _errors.Count > 0; + + public event EventHandler ErrorsChanged; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index 29ce26fcdf..b2d3a8bddb 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -2,24 +2,31 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"> - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index a90faf1448..081db5d26a 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -35,7 +35,6 @@ namespace Avalonia.Skia IGlPlatformSurfaceRenderingSession glSession) { GrContext = grContext; - GrContext.PurgeResources(); _backendRenderTarget = backendRenderTarget; _surface = surface; _glSession = glSession; @@ -46,7 +45,6 @@ namespace Avalonia.Skia _surface.Dispose(); _backendRenderTarget.Dispose(); GrContext.Flush(); - GrContext.PurgeResources(); _glSession.Dispose(); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 33744949c3..00d148093a 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1335,6 +1335,47 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(model.SingleSelect); } + [Fact] + public void Does_The_Best_It_Can_With_AutoSelecting_ViewModel() + { + // Tests the following scenario: + // + // - Items changes from empty to having 1 item + // - ViewModel auto-selects item 0 in CollectionChanged + // - SelectionModel receives CollectionChanged + // - And so adjusts the selected item from 0 to 1, which is past the end of the items. + // + // There's not much we can do about this situation because the order in which + // CollectionChanged handlers are called can't be known (the problem also exists with + // WPF). The best we can do is not select an invalid index. + var vm = new SelectionViewModel(); + + vm.Items.CollectionChanged += (s, e) => + { + if (vm.SelectedIndex == -1 && vm.Items.Count > 0) + { + vm.SelectedIndex = 0; + } + }; + + var target = new ListBox + { + [!ListBox.ItemsProperty] = new Binding("Items"), + [!ListBox.SelectedIndexProperty] = new Binding("SelectedIndex"), + DataContext = vm, + }; + + Prepare(target); + + vm.Items.Add("foo"); + vm.Items.Add("bar"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { 0 }, target.Selection.SelectedIndexes); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot @@ -1397,6 +1438,28 @@ namespace Avalonia.Controls.UnitTests.Primitives public int SelectedIndex { get; set; } } + private class SelectionViewModel : NotifyingBase + { + private int _selectedIndex = -1; + + public SelectionViewModel() + { + Items = new ObservableCollection(); + } + + public int SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + RaisePropertyChanged(); + } + } + + public ObservableCollection Items { get; } + } + private class RootWithItems : TestRoot { public List Items { get; set; } = new List() { "a", "b", "c", "d", "e" }; diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs index 3640faf7cb..3eddd35465 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Multiple.cs @@ -1230,6 +1230,38 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, resetRaised); Assert.Equal(1, selectedIndexRaised); } + + [Fact] + public void Handles_Selection_Made_In_CollectionChanged() + { + // Tests the following scenario: + // + // - Items changes from empty to having 2 items + // - ViewModel auto-selects range 0..1 in CollectionChanged + // - SelectionModel receives CollectionChanged + // - And so adjusts the selected item from 0..1 to 2..4, which is past the end of + // the items. + // + // There's not much we can do about this situation because the order in which + // CollectionChanged handlers are called can't be known (the problem also exists with + // WPF). The best we can do is not select an invalid index. + var target = CreateTarget(createData: false); + var data = new AvaloniaList(); + + data.CollectionChanged += (s, e) => + { + target.SelectRange(0, 1); + }; + + target.Source = data; + data.AddRange(new[] { "foo", "bar" }); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { 0, 1 }, target.SelectedIndexes); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo", "bar" }, target.SelectedItems); + Assert.Equal(0, target.AnchorIndex); + } } public class BatchUpdate diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 345518e729..1b37730797 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -1052,6 +1052,37 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(1, resetRaised); Assert.Equal(1, selectedIndexRaised); } + + [Fact] + public void Handles_Selection_Made_In_CollectionChanged() + { + // Tests the following scenario: + // + // - Items changes from empty to having 1 item + // - ViewModel auto-selects item 0 in CollectionChanged + // - SelectionModel receives CollectionChanged + // - And so adjusts the selected item from 0 to 1, which is past the end of the items. + // + // There's not much we can do about this situation because the order in which + // CollectionChanged handlers are called can't be known (the problem also exists with + // WPF). The best we can do is not select an invalid index. + var target = CreateTarget(createData: false); + var data = new AvaloniaList(); + + data.CollectionChanged += (s, e) => + { + target.Select(0); + }; + + target.Source = data; + data.Add("foo"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { 0 }, target.SelectedIndexes); + Assert.Equal("foo", target.SelectedItem); + Assert.Equal(new[] { "foo" }, target.SelectedItems); + Assert.Equal(0, target.AnchorIndex); + } } public class BatchUpdate