From 24f208bab1fce4a7ed9403703a0b7f9ff07d2c2b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 25 Mar 2019 17:45:42 +0100 Subject: [PATCH 01/33] !R Rework deferred setter for better performance. --- src/Avalonia.Base/AvaloniaObject.cs | 91 +++++++++-- .../Utilities/AvaloniaPropertyCollection.cs | 143 ++++++++++++++++ .../Utilities/DeferredSetterOptimized.cs | 84 ++++++++++ src/Avalonia.Base/ValueStore.cs | 153 ++++-------------- .../Base/DirectPropertyBenchmark.cs | 82 ++++++++++ tests/Avalonia.Benchmarks/Base/Properties.cs | 3 +- 6 files changed, 420 insertions(+), 136 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs create mode 100644 src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs create mode 100644 tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7601b64ce9..22aeccc3fb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -561,6 +561,7 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); + return SetAndRaise( property, ref field, @@ -568,19 +569,89 @@ namespace Avalonia => SetAndRaiseCore(property, ref backing, val, notifyWrapper), value); } + + /// + /// Default setter handler that will set backing field and raise notification. + /// + private sealed class DefaultSetterHandler : DeferredSetterOptimized.ISetterHandler + { + public static readonly DefaultSetterHandler Instance = new DefaultSetterHandler(); + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + var old = backing; + backing = value; + + source.RaisePropertyChanged(property, old, value); + + return true; + } + } /// - /// Default assignment logic for SetAndRaise. + /// Setter handler that will run custom user callback. /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + private sealed class CallbackSetterHandler : DeferredSetterOptimized.ISetterHandler + { + private readonly SetAndRaiseCallback _callback; + + public CallbackSetterHandler(SetAndRaiseCallback callback) + { + _callback = callback; + } + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + _callback(value, ref backing, notification => notification()); + + return true; + } + } + + protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, DefaultSetterHandler.Instance, ref field, value); + } + + protected bool SetAndRaiseOptimized( + AvaloniaProperty property, + ref T field, + SetAndRaiseCallback setterCallback, + T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, new CallbackSetterHandler(setterCallback) , ref field, value); + } + + /// + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) { var old = field; field = value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs new file mode 100644 index 0000000000..5b1492a7a8 --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + internal sealed class AvaloniaPropertyCollection + { + private Entry[] _entries; + + public AvaloniaPropertyCollection() + { + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) + return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) + return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) + return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) + return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) + return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) + return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) + return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) + return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) + return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) + return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) + return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } + + public bool TryGetValue(AvaloniaProperty property, out TValue value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = default; + return false; + } + + value = _entries[index].Value; + return true; + } + + public void AddValueInternal(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValueInternal(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public Dictionary ToDictionary() + { + var dict = new Dictionary(_entries.Length - 1); + + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } + + private struct Entry + { + internal int PropertyId; + internal TValue Value; + } + } +} diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs new file mode 100644 index 0000000000..e3bed90498 --- /dev/null +++ b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs @@ -0,0 +1,84 @@ +using System; + +namespace Avalonia.Utilities +{ + /// + /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// Used to fix #855. + /// + /// The type of value with which to track the delayed assignment. + internal sealed class DeferredSetterOptimized + { + private bool _isNotifying; + private readonly SingleOrQueue _pendingValues; + + public DeferredSetterOptimized() + { + _pendingValues = new SingleOrQueue(); + } + + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ISetterHandler handler, + ref TSetRecord backing, + TSetRecord value) + { + if (!_isNotifying) + { + bool updated; + + using (new NotifyDisposable(this)) + { + updated = handler.Update(source, property, ref backing, value); + } + + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } + + return updated; + } + + _pendingValues.Enqueue(value); + + return false; + } + + /// + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. + /// + private readonly struct NotifyDisposable : IDisposable + { + private readonly DeferredSetterOptimized _setter; + + internal NotifyDisposable(DeferredSetterOptimized setter) + { + _setter = setter; + _setter._isNotifying = true; + } + + public void Dispose() + { + _setter._isNotifying = false; + } + } + + public interface ISetterHandler + { + bool Update( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value); + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 24f85ea6b1..b2c4c68f9c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,21 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private struct Entry - { - internal int PropertyId; - internal object Value; - } - + private readonly AvaloniaPropertyCollection _propertyValues; + private readonly AvaloniaPropertyCollection _deferredSetters; private readonly AvaloniaObject _owner; - private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; - - // The last item in the list is always int.MaxValue - _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; + _propertyValues = new AvaloniaPropertyCollection(); + _deferredSetters = new AvaloniaPropertyCollection(); } public IDisposable AddBinding( @@ -31,7 +25,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -39,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -55,7 +49,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -63,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValueInternal(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -71,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } } @@ -84,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValueInternal(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } } @@ -110,14 +104,9 @@ namespace Avalonia public IDictionary GetSetValues() { - var dict = new Dictionary(_entries.Length - 1); - for (int i = 0; i < _entries.Length - 1; ++i) - { - dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); - } - - return dict; + return _propertyValues.ToDictionary(); } + public void LogError(AvaloniaProperty property, Exception e) { _owner.LogBindingError(property, e); @@ -127,7 +116,7 @@ namespace Avalonia { var result = AvaloniaProperty.UnsetValue; - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -137,12 +126,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -152,7 +141,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -189,113 +178,29 @@ namespace Avalonia return value; } - private DeferredSetter _deferredSetter; - - public DeferredSetter Setter - { - get - { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); - } - } - - private bool TryGetValue(AvaloniaProperty property, out object value) + public DeferredSetterOptimized GetDeferredSetter(AvaloniaProperty property) { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) + if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - value = null; - return false; + return (DeferredSetterOptimized)deferredSetter; } - value = _entries[index].Value; - return true; - } - - private void AddValueInternal(AvaloniaProperty property, object value) - { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) - { - if (_entries[i].PropertyId > property.Id) - { - if (i > 0) - { - Array.Copy(_entries, 0, entries, 0, i); - } + var newDeferredSetter = new DeferredSetterOptimized(); - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } + _deferredSetters.AddValueInternal(property, newDeferredSetter); - _entries = entries; + return newDeferredSetter; } - private void SetValueInternal(AvaloniaProperty property, object value) - { - _entries[TryFindEntry(property.Id).Item1].Value = value; - } + private DeferredSetter _deferredSetter; - private (int, bool) TryFindEntry(int propertyId) + public DeferredSetter Setter { - if (_entries.Length <= 12) - { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - } - else + get { - int low = 0; - int high = _entries.Length; - int id; - - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } - - do - { - id = _entries[low].PropertyId; - - if (id == propertyId) - return (low, true); - - if (id > propertyId) - break; - - ++low; - } - while (low < high); + return _deferredSetter ?? + (_deferredSetter = new DeferredSetter()); } - - return (0, false); } } } diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs new file mode 100644 index 0000000000..4f36b54414 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -0,0 +1,82 @@ +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class DirectPropertyBenchmark + { + [Benchmark(Baseline = true)] + public void SetAndRaiseOriginal() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValue += 1; + } + } + + [Benchmark] + public void SetAndRaiseOptimized() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueOptimized += 1; + } + } + + [Benchmark] + public void SetAndRaiseSimple() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueSimple += 1; + } + } + + class DirectClass : AvaloniaObject + { + private int _intValue; + + public static readonly DirectProperty IntValueProperty = + AvaloniaProperty.RegisterDirect(nameof(IntValue), + o => o.IntValue, + (o, v) => o.IntValue = v); + + public int IntValue + { + get => _intValue; + set => SetAndRaise(IntValueProperty, ref _intValue, value); + } + + public int IntValueOptimized + { + get => _intValue; + set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value); + } + + public int IntValueSimple + { + get => _intValue; + set + { + VerifyAccess(); + + if (_intValue == value) + { + return; + } + + var old = _intValue; + _intValue = value; + + RaisePropertyChanged(IntValueProperty, old, _intValue); + } + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs index 0a020961d5..45fc68ac96 100644 --- a/tests/Avalonia.Benchmarks/Base/Properties.cs +++ b/tests/Avalonia.Benchmarks/Base/Properties.cs @@ -1,5 +1,4 @@ -using System; -using System.Reactive.Subjects; +using System.Reactive.Subjects; using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Base From 67bd1b8d86744ac65f4427b0b85239856c6b71ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 21:28:56 +0200 Subject: [PATCH 02/33] Added failing tests for #1701. --- .../AvaloniaObjectTests_DataValidation.cs | 78 +++++++++++++++---- .../TextBoxTests.cs | 23 ++++++ 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index b12b2e3c31..428f878945 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests @@ -34,10 +35,10 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7)); Assert.Equal( new[] @@ -73,7 +74,7 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var target = new Class1 { - [!Class1.ValidatedDirectProperty] = source.ToBinding(), + [!Class1.ValidatedDirectIntProperty] = source.ToBinding(), }; source.OnNext(new BindingNotification(6)); @@ -92,6 +93,30 @@ namespace Avalonia.Base.UnitTests target.Notifications.AsEnumerable()); } + [Fact] + public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null() + { + var source = new ViewModel + { + StringValue = "foo", + }; + + var target = new Class1 + { + [!Class1.ValidatedDirectStringProperty] = new Binding + { + Path = nameof(ViewModel.StringValue), + Source = source, + }, + }; + + Assert.Equal("foo", target.ValidatedDirectString); + + source.StringValue = null; + + Assert.Null(target.ValidatedDirectString); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty NonValidatedProperty = @@ -104,15 +129,23 @@ namespace Avalonia.Base.UnitTests o => o.NonValidatedDirect, (o, v) => o.NonValidatedDirect = v); - public static readonly DirectProperty ValidatedDirectProperty = + public static readonly DirectProperty ValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( - nameof(ValidatedDirect), - o => o.ValidatedDirect, - (o, v) => o.ValidatedDirect = v, + nameof(ValidatedDirectInt), + o => o.ValidatedDirectInt, + (o, v) => o.ValidatedDirectInt = v, + enableDataValidation: true); + + public static readonly DirectProperty ValidatedDirectStringProperty = + AvaloniaProperty.RegisterDirect( + nameof(ValidatedDirectString), + o => o.ValidatedDirectString, + (o, v) => o.ValidatedDirectString = v, enableDataValidation: true); private int _nonValidatedDirect; - private int _direct; + private int _directInt; + private string _directString; public int NonValidated { @@ -122,14 +155,20 @@ namespace Avalonia.Base.UnitTests public int NonValidatedDirect { - get { return _direct; } + get { return _directInt; } set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); } } - public int ValidatedDirect + public int ValidatedDirectInt + { + get { return _directInt; } + set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); } + } + + public string ValidatedDirectString { - get { return _direct; } - set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); } + get { return _directString; } + set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } public IList Notifications { get; } = new List(); @@ -139,5 +178,16 @@ namespace Avalonia.Base.UnitTests Notifications.Add(notification); } } + + public class ViewModel : NotifyingBase + { + private string _stringValue; + + public string StringValue + { + get { return _stringValue; } + set { _stringValue = value; RaisePropertyChanged(); } + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 932aada64e..cef5fe61fc 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -444,6 +444,22 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_Bound_Text_To_Null_Works() + { + using (UnitTestApplication.Start(Services)) + { + var source = new Class1 { Bar = "bar" }; + var target = new TextBox { DataContext = source }; + + target.Bind(TextBox.TextProperty, new Binding("Bar")); + + Assert.Equal("bar", target.Text); + source.Bar = null; + Assert.Null(target.Text); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), @@ -492,12 +508,19 @@ namespace Avalonia.Controls.UnitTests 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(); } + } } } } From ecabeba4d15540168507822472f917c58bd768ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 25 Jun 2019 22:49:02 +0200 Subject: [PATCH 03/33] Fix conversion of null in DefaultValueConverter. `null` is a valid value for a non-value type so don't convert it to `{unset}`. Fixes #1701. --- src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 990a4b04f2..0ffd6a9539 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters { if (value == null) { - return AvaloniaProperty.UnsetValue; + return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null; } if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) From 11a3f0c0934204410f7e1eb2848a8cf627161564 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jun 2019 01:07:37 +0200 Subject: [PATCH 04/33] Added failing test for #2565. Along with a passing test I wrote looking for the solution, and some documentation about SelectionMode. --- src/Avalonia.Controls/ListBox.cs | 8 +++- .../Primitives/SelectingItemsControl.cs | 4 ++ .../SelectingItemsControlTests_Multiple.cs | 44 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3150b6be91..f26cd47bcb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -68,7 +68,13 @@ namespace Avalonia.Controls /// public new IList SelectedItems => base.SelectedItems; - /// + /// + /// Gets or sets the selection mode. + /// + /// + /// 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. + /// public new SelectionMode SelectionMode { get { return base.SelectionMode; } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 91a9fa7e40..6f7bf4eb3f 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -222,6 +222,10 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the selection mode. /// + /// + /// 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. + /// protected SelectionMode SelectionMode { get { return GetValue(SelectionModeProperty); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index a33d97779e..a44e23096f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Assigning_SelectedItems_Should_Set_SelectedIndex() + public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex() { var target = new TestSelector { @@ -62,9 +62,51 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); target.SelectedItems = new AvaloniaList("bar"); Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { 1 }, SelectedContainers(target)); + } + + [Fact] + public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex() + { + // Note that we don't need SelectionMode = Multiple here. Multiple selections can always + // be made in code. + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } + + [Fact] + public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedItems_Is_Set() + { + // Issue #2565. + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + target.Presenter.ApplyTemplate(); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); } [Fact] From fc9a8db010acd04949de0814461aed10a636ff27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jun 2019 01:08:29 +0200 Subject: [PATCH 05/33] Correctly select materialized containers. And add another test. --- .../Primitives/SelectingItemsControl.cs | 34 +++++++++++++------ .../SelectingItemsControlTests_Multiple.cs | 31 +++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 6f7bf4eb3f..4f01f5467b 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -342,24 +342,36 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); - var selectedIndex = SelectedIndex; - var selectedContainer = e.Containers - .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true); + var resetSelectedItems = false; - if (selectedContainer != null) + foreach (var container in e.Containers) { - SelectedIndex = selectedContainer.Index; - } - else if (selectedIndex >= e.StartingIndex && - selectedIndex < e.StartingIndex + e.Containers.Count) - { - var container = e.Containers[selectedIndex - e.StartingIndex]; + if ((container.ContainerControl as ISelectable)?.IsSelected == true) + { + if (SelectedIndex == -1) + { + SelectedIndex = container.Index; + } + else + { + if (_selection.Add(container.Index)) + { + resetSelectedItems = true; + } + } - if (container.ContainerControl != null) + MarkContainerSelected(container.ContainerControl, true); + } + else if (_selection.Contains(container.Index)) { MarkContainerSelected(container.ContainerControl, true); } } + + if (resetSelectedItems) + { + ResetSelectedItems(); + } } /// diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index a44e23096f..2faced358b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1068,6 +1068,31 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedItems.Count); } + [Fact] + public void Adding_Selected_ItemContainers_Should_Update_Selection() + { + var items = new AvaloniaList(new[] + { + new ItemContainer(), + new ItemContainer(), + }); + + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + items.Add(new ItemContainer { IsSelected = true }); + items.Add(new ItemContainer { IsSelected = true }); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(items[2], target.SelectedItem); + Assert.Equal(new[] { items[2], items[3] }, target.SelectedItems); + } + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children @@ -1120,5 +1145,11 @@ namespace Avalonia.Controls.UnitTests.Primitives public List Items { get; } public List SelectedItems { get; } } + + private class ItemContainer : Control, ISelectable + { + public string Value { get; set; } + public bool IsSelected { get; set; } + } } } From 2fa8247f014af936577b1dbaf6372d281fbb168c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jun 2019 10:55:27 +0200 Subject: [PATCH 06/33] Removed MemberSelector. --- .../Pages/AutoCompleteBoxPage.xaml | 4 - samples/ControlCatalog/SideBar.xaml | 3 +- samples/RenderDemo/SideBar.xaml | 5 +- src/Avalonia.Controls/AutoCompleteBox.cs | 26 --- src/Avalonia.Controls/ComboBox.cs | 3 +- .../Generators/IItemContainerGenerator.cs | 12 +- .../Generators/ItemContainerGenerator.cs | 17 +- .../Generators/ItemContainerGenerator`1.cs | 14 +- .../Generators/TreeItemContainerGenerator.cs | 5 +- src/Avalonia.Controls/ItemsControl.cs | 15 -- .../Presenters/CarouselPresenter.cs | 2 +- .../Presenters/ItemContainerSync.cs | 2 +- .../Presenters/ItemVirtualizerNone.cs | 2 +- .../Presenters/ItemVirtualizerSimple.cs | 9 +- .../Presenters/ItemsPresenterBase.cs | 15 -- src/Avalonia.Controls/Primitives/TabStrip.cs | 9 - .../Templates/FuncMemberSelector.cs | 35 ---- .../Templates/IMemberSelector.cs | 18 -- .../AutoCompleteBox.xaml | 1 - src/Avalonia.Themes.Default/Carousel.xaml | 3 +- src/Avalonia.Themes.Default/ComboBox.xaml | 1 - .../DataValidationErrors.xaml | 2 +- src/Avalonia.Themes.Default/ItemsControl.xaml | 5 +- src/Avalonia.Themes.Default/ListBox.xaml | 3 +- src/Avalonia.Themes.Default/MenuItem.xaml | 6 +- src/Avalonia.Themes.Default/TabControl.xaml | 3 +- src/Avalonia.Themes.Default/TabStrip.xaml | 3 +- src/Avalonia.Themes.Default/TreeView.xaml | 3 +- src/Avalonia.Themes.Default/TreeViewItem.xaml | 3 +- .../Avalonia.Markup.Xaml.csproj | 2 - .../Converters/MemberSelectorTypeConverter.cs | 24 --- .../Templates/MemberSelector.cs | 48 ------ .../AvaloniaXamlIlLanguage.cs | 2 - .../Generators/ItemContainerGeneratorTests.cs | 6 +- .../ItemContainerGeneratorTypedTests.cs | 2 +- .../ItemsControlTests.cs | 22 --- .../Presenters/ItemsPresenterTests.cs | 40 ----- .../Primitives/TabStripTests.cs | 55 ------ .../Avalonia.LeakTests/MemberSelectorTests.cs | 55 ------ .../Templates/MemberSelectorTests.cs | 159 ------------------ .../AutoDataTemplateBindingHookTest.cs | 3 +- 41 files changed, 36 insertions(+), 611 deletions(-) delete mode 100644 src/Avalonia.Controls/Templates/FuncMemberSelector.cs delete mode 100644 src/Avalonia.Controls/Templates/IMemberSelector.cs delete mode 100644 src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs delete mode 100644 src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs delete mode 100644 tests/Avalonia.LeakTests/MemberSelectorTests.cs delete mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 0ca3567970..f90a0c4658 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -37,10 +37,6 @@ - - + ItemTemplate="{TemplateBinding ItemTemplate}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> - \ No newline at end of file + diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index b87e10d284..787abc1891 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -345,7 +345,6 @@ namespace Avalonia.Controls /// private IDisposable _collectionChangeSubscription; - private IMemberSelector _valueMemberSelector; private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; @@ -541,12 +540,6 @@ namespace Avalonia.Controls o => o.Items, (o, v) => o.Items = v); - public static readonly DirectProperty ValueMemberSelectorProperty = - AvaloniaProperty.RegisterDirect( - nameof(ValueMemberSelector), - o => o.ValueMemberSelector, - (o, v) => o.ValueMemberSelector = v); - public static readonly DirectProperty>>> AsyncPopulatorProperty = AvaloniaProperty.RegisterDirect>>>( nameof(AsyncPopulator), @@ -958,20 +951,6 @@ namespace Avalonia.Controls } } - /// - /// Gets or sets the MemberSelector that is used to get values for - /// display in the text portion of the - /// control. - /// - /// The MemberSelector that is used to get values for display in - /// the text portion of the - /// control. - public IMemberSelector ValueMemberSelector - { - get { return _valueMemberSelector; } - set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); } - } - /// /// Gets or sets the selected item in the drop-down. /// @@ -1841,11 +1820,6 @@ namespace Avalonia.Controls return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty; } - if (_valueMemberSelector != null) - { - value = _valueMemberSelector.Select(value); - } - return value == null ? String.Empty : value.ToString(); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 5d427df5a6..f32b8fabc6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -333,8 +333,7 @@ namespace Avalonia.Controls } else { - var selector = MemberSelector; - SelectionBoxItem = selector != null ? selector.Select(item) : item; + SelectionBoxItem = item; } } diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 653a4f5dcb..2d6757219d 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators /// The index of the item of data in the control's items. /// /// The item. - /// An optional member selector. /// The created controls. - ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector); + ItemContainerInfo Materialize(int index, object item); /// /// Removes a set of created containers. @@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators /// The removed containers. IEnumerable RemoveRange(int startingIndex, int count); - bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector); + bool TryRecycle(int oldIndex, int newIndex, object item); /// /// Clears all created containers and returns the removed controls. diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index f1a1f94a01..4fd6f4135c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators public virtual Type ContainerType => null; /// - public ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector) + public ItemContainerInfo Materialize(int index, object item) { - var i = selector != null ? selector.Select(item) : item; - var container = new ItemContainerInfo(CreateContainer(i), item, index); + var container = new ItemContainerInfo(CreateContainer(item), item, index); _containers.Add(container.Index, container); Materialized?.Invoke(this, new ItemContainerEventArgs(container)); @@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators } /// - public virtual bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) - { - return false; - } + public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false; /// public virtual IEnumerable Clear() diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 320d6c8faf..d1d1c2b172 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators } /// - public override bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) + public override bool TryRecycle(int oldIndex, int newIndex, object item) { var container = ContainerFromIndex(oldIndex); @@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators throw new IndexOutOfRangeException("Could not recycle container: not materialized."); } - var i = selector != null ? selector.Select(item) : item; - - container.SetValue(ContentProperty, i); + container.SetValue(ContentProperty, item); if (!(item is IControl)) { - container.DataContext = i; + container.DataContext = item; } - var info = MoveContainer(oldIndex, newIndex, i); + var info = MoveContainer(oldIndex, newIndex, item); RaiseRecycled(new ItemContainerEventArgs(info)); return true; diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 304c86dbf7..fce7579ec0 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -118,10 +118,7 @@ namespace Avalonia.Controls.Generators return base.RemoveRange(startingIndex, count); } - public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) - { - return false; - } + public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate primary) { diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a292ff7d0a..902e55bde6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -54,12 +54,6 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - AvaloniaProperty.Register(nameof(MemberSelector)); - private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -144,15 +138,6 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the items presenter control. /// diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index a3123cf8c6..dedab3e43e 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters if (container == null && IsVirtualized) { var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); + var materialized = ItemContainerGenerator.Materialize(index, item); Panel.Children.Add(materialized.ContainerControl); container = materialized.ContainerControl; } diff --git a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs index 035d404dec..6e72908e6b 100644 --- a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs +++ b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs @@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 413855bcc6..56f64779f6 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, Owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d11ce9a7ea..b8b8094582 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree) { - var memberSelector = Owner.MemberSelector; var index = NextIndex; var step = 1; @@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters } } - var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); + var materialized = generator.Materialize(index, Items.ElementAt(index)); if (step == 1) { @@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; var containers = generator.Containers.ToList(); var itemIndex = FirstIndex; @@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters if (!object.Equals(container.Item, item)) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + if (!generator.TryRecycle(itemIndex, itemIndex, item)) { throw new NotImplementedException(); } @@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; //validate delta it should never overflow last index or generate index < 0 delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count); @@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters var item = Items.ElementAt(newItemIndex); - if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item)) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b4b792139d..ea56a0c6fc 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty ItemTemplateProperty = ItemsControl.ItemTemplateProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - ItemsControl.MemberSelectorProperty.AddOwner(); - private IEnumerable _items; private IDisposable _itemsSubscription; private bool _createdPanel; @@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the panel used to display the items. /// diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 0e15ae4d7b..a61757e628 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -12,11 +12,8 @@ namespace Avalonia.Controls.Primitives private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); - private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); - static TabStrip() { - MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); @@ -51,11 +48,5 @@ namespace Avalonia.Controls.Primitives e.Handled = UpdateSelectionFromEventSource(e.Source); } } - - private static object SelectHeader(object o) - { - var headered = o as IHeadered; - return (headered != null) ? (headered.Header ?? string.Empty) : o; - } } } diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs deleted file mode 100644 index 5ab186261e..0000000000 --- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object using a . - /// - public class FuncMemberSelector : IMemberSelector - { - private readonly Func _selector; - - /// - /// Initializes a new instance of the - /// class. - /// - /// The selector. - public FuncMemberSelector(Func selector) - { - this._selector = selector; - } - - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - public object Select(object o) - { - return (o is TObject) ? _selector((TObject)o) : default(TMember); - } - } -} diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs deleted file mode 100644 index e1ec42a849..0000000000 --- a/src/Avalonia.Controls/Templates/IMemberSelector.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object. - /// - public interface IMemberSelector - { - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - object Select(object o); - } -} diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 11d8a344d9..788b60892b 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -27,7 +27,6 @@ Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding ValueMemberSelector}" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" /> diff --git a/src/Avalonia.Themes.Default/Carousel.xaml b/src/Avalonia.Themes.Default/Carousel.xaml index efe12c4333..955a49a974 100644 --- a/src/Avalonia.Themes.Default/Carousel.xaml +++ b/src/Avalonia.Themes.Default/Carousel.xaml @@ -8,10 +8,9 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" Margin="{TemplateBinding Padding}" - MemberSelector="{TemplateBinding MemberSelector}" SelectedIndex="{TemplateBinding SelectedIndex}" PageTransition="{TemplateBinding PageTransition}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index ca6c2e372e..6227962a48 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -45,7 +45,6 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}" VirtualizationMode="{TemplateBinding VirtualizationMode}" /> diff --git a/src/Avalonia.Themes.Default/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/DataValidationErrors.xaml index 0c40a7eb25..f4145a51f5 100644 --- a/src/Avalonia.Themes.Default/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Default/DataValidationErrors.xaml @@ -29,7 +29,7 @@ - + diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml index 7b6671b42c..f3def542fc 100644 --- a/src/Avalonia.Themes.Default/ItemsControl.xaml +++ b/src/Avalonia.Themes.Default/ItemsControl.xaml @@ -4,8 +4,7 @@ + ItemTemplate="{TemplateBinding ItemTemplate}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index 57b0c541b8..59c596bcaa 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -18,10 +18,9 @@ ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" Margin="{TemplateBinding Padding}" - MemberSelector="{TemplateBinding MemberSelector}" VirtualizationMode="{TemplateBinding VirtualizationMode}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index be86e8b14c..a794d15577 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -56,8 +56,7 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" - Margin="2" - MemberSelector="{TemplateBinding MemberSelector}"/> + Margin="2"/> + Margin="2"/> + ItemTemplate="{TemplateBinding ItemTemplate}"> @@ -18,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TreeView.xaml b/src/Avalonia.Themes.Default/TreeView.xaml index 4e38c6db3a..6ed2fd17b8 100644 --- a/src/Avalonia.Themes.Default/TreeView.xaml +++ b/src/Avalonia.Themes.Default/TreeView.xaml @@ -15,8 +15,7 @@ + Margin="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index b5e0e7a005..5dd082cf7a 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -32,8 +32,7 @@ + ItemsPanel="{TemplateBinding ItemsPanel}"/> diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 6f3dabd568..06c5375520 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -12,7 +12,6 @@ - @@ -33,7 +32,6 @@ - diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs deleted file mode 100644 index 8dc052fe63..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Globalization; -using Avalonia.Markup.Xaml.Templates; - -namespace Avalonia.Markup.Xaml.Converters -{ - using System.ComponentModel; - - public class MemberSelectorTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return MemberSelector.Parse((string)value); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs deleted file mode 100644 index fa91ab60ff..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Markup.Parsers; -using System; -using System.Reactive.Linq; - -namespace Avalonia.Markup.Xaml.Templates -{ - public class MemberSelector : IMemberSelector - { - private string _memberName; - - public string MemberName - { - get { return _memberName; } - set - { - if (_memberName != value) - { - _memberName = value; - } - } - } - - public static MemberSelector Parse(string s) - { - return new MemberSelector { MemberName = s }; - } - - public object Select(object o) - { - if (string.IsNullOrEmpty(MemberName)) - { - return o; - } - - var expression = ExpressionObserverBuilder.Build(o, MemberName); - object result = AvaloniaProperty.UnsetValue; - - expression.Subscribe(x => result = x); - return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result; - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index c25e1186d0..830e354274 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -81,8 +81,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); - Add("Avalonia.Controls.Templates.IMemberSelector", - "Avalonia.Markup.Xaml.Converters.MemberSelectorTypeConverter"); Add("Avalonia.Controls.WindowIcon","Avalonia.Markup.Xaml.Converters.IconTypeConverter"); Add("System.Globalization.CultureInfo", "System.ComponentModel.CultureInfoConverter"); Add("System.Uri", "Avalonia.Markup.Xaml.Converters.AvaloniaUriTypeConverter"); diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index 9b4be59647..70410dff0d 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -118,7 +118,7 @@ namespace Avalonia.Controls.UnitTests.Generators { var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var container = (ContentPresenter)target.Materialize(0, "foo", null).ContainerControl; + var container = (ContentPresenter)target.Materialize(0, "foo").ContainerControl; Assert.Equal("foo", container.Content); @@ -135,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Generators { var owner = new Decorator(); var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); - var container = (ListBoxItem)target.Materialize(0, "foo", null).ContainerControl; + var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl; Assert.Equal("foo", container.Content); @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Generators foreach (var item in items) { - var container = generator.Materialize(index++, item, null); + var container = generator.Materialize(index++, item); result.Add(container); } diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs index f63c0efbf9..05954cbcd2 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.UnitTests.Generators foreach (var item in items) { - var container = generator.Materialize(index++, item, null); + var container = generator.Materialize(index++, item); result.Add(container); } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3cf886ade4..ca945c66bf 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -430,27 +430,6 @@ namespace Avalonia.Controls.UnitTests dataContexts); } - [Fact] - public void MemberSelector_Should_Select_Member() - { - var target = new ItemsControl - { - Template = GetTemplate(), - Items = new[] { new Item("Foo"), new Item("Bar") }, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var text = target.Presenter.Panel.Children - .Cast() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, text); - } - [Fact] public void Control_Item_Should_Not_Be_NameScope() { @@ -586,7 +565,6 @@ namespace Avalonia.Controls.UnitTests Child = new ItemsPresenter { Name = "PART_ItemsPresenter", - MemberSelector = parent.MemberSelector, [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], } }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 3d13e4c32f..0c9d36ba3a 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -310,46 +310,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(target.Panel, child); } - [Fact] - public void MemberSelector_Should_Select_Member() - { - var target = new ItemsPresenter - { - Items = new[] { new Item("Foo"), new Item("Bar") }, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - - var text = target.Panel.Children - .Cast() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, text); - } - - [Fact] - public void MemberSelector_Should_Set_DataContext() - { - var items = new[] { new Item("Foo"), new Item("Bar") }; - var target = new ItemsPresenter - { - Items = items, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - - var dataContexts = target.Panel.Children - .Cast() - .Do(x => x.UpdateChild()) - .Select(x => x.DataContext) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, dataContexts); - } - private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs index 55b3d6f756..622169f8f6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs @@ -14,60 +14,6 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class TabStripTests { - [Fact] - public void Header_Of_IHeadered_Items_Should_Be_Used() - { - var items = new[] - { -#pragma warning disable CS0252 // Possible unintended reference comparison; left hand side needs cast - Mock.Of(x => x.Header == "foo"), - Mock.Of(x => x.Header == "bar"), -#pragma warning restore CS0252 // Possible unintended reference comparison; left hand side needs cast - }; - - var target = new TabStrip - { - Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var result = target.GetLogicalChildren() - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "bar" }, result); - } - - [Fact] - public void Data_Of_Non_IHeadered_Items_Should_Be_Used() - { - var items = new[] - { - "foo", - "bar" - }; - - var target = new TabStrip - { - Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var result = target.GetLogicalChildren() - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "bar" }, result); - } - [Fact] public void First_Tab_Should_Be_Selected_By_Default() { @@ -165,7 +111,6 @@ namespace Avalonia.Controls.UnitTests.Primitives { Name = "itemsPresenter", [!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty], - [!ItemsPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], }; } } diff --git a/tests/Avalonia.LeakTests/MemberSelectorTests.cs b/tests/Avalonia.LeakTests/MemberSelectorTests.cs deleted file mode 100644 index ffee18ae0a..0000000000 --- a/tests/Avalonia.LeakTests/MemberSelectorTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Avalonia.Markup.Xaml.Templates; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using JetBrains.dotMemoryUnit; -using Xunit; -using Xunit.Abstractions; - -namespace Avalonia.LeakTests -{ - [DotMemoryUnit(FailIfRunWithoutSupport = false)] - public class MemberSelectorTests - { - public MemberSelectorTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] - public void Should_Not_Hold_Reference_To_Object() - { - WeakReference dataRef = null; - - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - Action run = () => - { - var data = new Item() - { - Child = new Item() { StringValue = "Value1" } - }; - - Assert.Same("Value1", selector.Select(data)); - - dataRef = new WeakReference(data); - }; - - run(); - - GC.Collect(); - - Assert.False(dataRef.IsAlive); - } - - private class Item - { - public Item Child { get; set; } - public int IntValue { get; set; } - - public string StringValue { get; set; } - } - } -} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs deleted file mode 100644 index aa1e56f2a5..0000000000 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Markup.Xaml.Templates; -using System; -using Xunit; - -namespace Avalonia.Markup.Xaml.UnitTests.Templates -{ - public class MemberSelectorTests - { - [Fact] - public void Should_Select_Child_Property_Value() - { - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - var data = new Item() - { - Child = new Item() { StringValue = "Value1" } - }; - - Assert.Same("Value1", selector.Select(data)); - } - - [Fact] - public void Should_Select_Child_Property_Value_In_Multiple_Items() - { - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - var data = new Item[] - { - new Item() { Child = new Item() { StringValue = "Value1" } }, - new Item() { Child = new Item() { StringValue = "Value2" } }, - new Item() { Child = new Item() { StringValue = "Value3" } } - }; - - Assert.Same("Value1", selector.Select(data[0])); - Assert.Same("Value2", selector.Select(data[1])); - Assert.Same("Value3", selector.Select(data[2])); - } - - [Fact] - public void Should_Select_MoreComplex_Property_Value() - { - var selector = new MemberSelector() { MemberName = "Child.Child.Child.StringValue" }; - - var data = new Item() - { - Child = new Item() - { - Child = new Item() - { - Child = new Item() { StringValue = "Value1" } - } - } - }; - - Assert.Same("Value1", selector.Select(data)); - } - - [Fact] - public void Should_Select_Null_Value_On_Null_Object() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - Assert.Null(selector.Select(null)); - } - - [Fact] - public void Should_Select_Null_Value_On_Wrong_MemberName() - { - var selector = new MemberSelector() { MemberName = "WrongProperty" }; - - var data = new Item() { StringValue = "Value1" }; - - Assert.Null(selector.Select(data)); - } - - [Fact] - public void Should_Select_Simple_Property_Value() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item() { StringValue = "Value1" }; - - Assert.Same("Value1", selector.Select(data)); - } - - [Fact] - public void Should_Select_Simple_Property_Value_In_Multiple_Items() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item[] - { - new Item() { StringValue = "Value1" }, - new Item() { StringValue = "Value2" }, - new Item() { StringValue = "Value3" } - }; - - Assert.Same("Value1", selector.Select(data[0])); - Assert.Same("Value2", selector.Select(data[1])); - Assert.Same("Value3", selector.Select(data[2])); - } - - [Fact] - public void Should_Select_Target_On_Empty_MemberName() - { - var selector = new MemberSelector(); - - var data = new Item() { StringValue = "Value1" }; - - Assert.Same(data, selector.Select(data)); - } - - [Fact] - public void Should_Support_Change_Of_MemberName() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item() - { - StringValue = "Value1", - IntValue = 1 - }; - - Assert.Same("Value1", selector.Select(data)); - - selector.MemberName = "IntValue"; - - Assert.Equal(1, selector.Select(data)); - } - - [Fact] - public void Should_Support_Change_Of_Target_Value() - { - var selector = new MemberSelector() { MemberName = "StringValue" }; - - var data = new Item() - { - StringValue = "Value1" - }; - - Assert.Same("Value1", selector.Select(data)); - - data.StringValue = "Value2"; - - Assert.Same("Value2", selector.Select(data)); - } - - private class Item - { - public Item Child { get; set; } - public int IntValue { get; set; } - - public string StringValue { get; set; } - } - } -} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 667462eb91..b8fd7d166d 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -106,11 +106,10 @@ namespace Avalonia.ReactiveUI.UnitTests Child = new ItemsPresenter { Name = "PART_ItemsPresenter", - MemberSelector = parent.MemberSelector, [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], } }; }); } } -} \ No newline at end of file +} From 139475de1c26323c62ddef0f2031c4a7094a7c95 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 27 Jun 2019 11:23:11 +0200 Subject: [PATCH 07/33] Added TabStripPage to ControlCatalog. To make sure that everything still works with TabStrip. --- samples/ControlCatalog/MainView.xaml | 1 + .../ControlCatalog/Pages/TabStripPage.xaml | 33 ++++++++++++++ .../ControlCatalog/Pages/TabStripPage.xaml.cs | 45 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 samples/ControlCatalog/Pages/TabStripPage.xaml create mode 100644 samples/ControlCatalog/Pages/TabStripPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 1cddb9d295..8699508320 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -36,6 +36,7 @@ + diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml b/samples/ControlCatalog/Pages/TabStripPage.xaml new file mode 100644 index 0000000000..a824336f75 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml @@ -0,0 +1,33 @@ + + + TabStrip + A control which displays a selectable strip of tabs + + + + Defined in XAML + + Item 1 + Item 2 + Disabled + + + + + Dynamically generated + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs new file mode 100644 index 0000000000..f0630cf534 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs @@ -0,0 +1,45 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public class TabStripPage : UserControl + { + public TabStripPage() + { + InitializeComponent(); + + DataContext = new[] + { + new TabStripItemViewModel + { + Header = "Item 1", + }, + new TabStripItemViewModel + { + Header = "Item 2", + }, + new TabStripItemViewModel + { + Header = "Disabled", + IsEnabled = false, + }, + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private class TabStripItemViewModel + { + public string Header { get; set; } + public bool IsEnabled { get; set; } = true; + } + } +} From 4e4c1918ab6dc7cf9b4979dd5b4dde0fc8afd364 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 1 Jul 2019 22:00:19 +0200 Subject: [PATCH 08/33] Added failing test for #2660. And a passing test too. Also added some useful methods to `TreeView` to help with this. --- src/Avalonia.Controls/TreeView.cs | 44 +++++++++++- .../TreeViewTests.cs | 67 ++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 888f4a2013..e186f08561 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -164,6 +164,48 @@ namespace Avalonia.Controls } } + /// + /// Expands the specified all descendent s. + /// + /// The item to expand. + public void ExpandSubTree(TreeViewItem item) + { + item.IsExpanded = true; + + var panel = item.Presenter.Panel; + + if (panel != null) + { + foreach (var child in panel.Children) + { + if (child is TreeViewItem treeViewItem) + { + ExpandSubTree(treeViewItem); + } + } + } + } + + /// + /// Selects all items in the . + /// + /// + /// Note that this method only selects nodes currently visible due to their parent nodes + /// being expanded: it does not expand nodes. + /// + public void SelectAll() + { + SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + } + + /// + /// Deselects all items in the . + /// + public void UnselectAll() + { + SelectedItems.Clear(); + } + /// /// Subscribes to the CollectionChanged event, if any. /// @@ -409,7 +451,7 @@ namespace Avalonia.Controls if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll)) { - SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items); + SelectAll(); e.Handled = true; } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b66d6ed11c..8defb353b3 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -11,6 +11,7 @@ using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.UnitTests; using Xunit; @@ -740,6 +741,71 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Right_Click_On_SelectedItem_Should_Not_Clear_Existing_Selection() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + target.SelectAll(); + + AssertChildrenSelected(target, tree[0]); + Assert.Equal(5, target.SelectedItems.Count); + + _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Right); + + Assert.Equal(5, target.SelectedItems.Count); + } + + [Fact] + public void Right_Click_On_UnselectedItem_Should_Clear_Existing_Selection() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + + var rootNode = tree[0]; + var to = rootNode.Children[0]; + var then = rootNode.Children[1]; + + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + var thenContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(then); + + ClickContainer(fromContainer, InputModifiers.None); + ClickContainer(toContainer, InputModifiers.Shift); + + Assert.Equal(2, target.SelectedItems.Count); + + _mouse.Click(thenContainer, MouseButton.Right); + + Assert.Equal(1, target.SelectedItems.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); @@ -874,7 +940,6 @@ namespace Avalonia.Controls.UnitTests } } - private class Node : NotifyingBase { private IAvaloniaList _children; From 67126261813c0ec28ca013287392ffb46e6f166b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 1 Jul 2019 22:25:51 +0200 Subject: [PATCH 09/33] Simplify setting TreeView.SelectedItem. Calling the overload of `SetAndRaise` which takes a callback should not be necessary because that was only needed in `SelectingItemsControl` due to the interaction between `SelectedIndex` and `SelectedItem`. --- src/Avalonia.Controls/TreeView.cs | 39 +++++++++++-------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index e186f08561..4404e46ad9 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -105,32 +105,21 @@ namespace Avalonia.Controls get => _selectedItem; set { - SetAndRaise(SelectedItemProperty, ref _selectedItem, - (object val, ref object backing, Action notifyWrapper) => - { - var old = backing; - backing = val; - - notifyWrapper(() => - RaisePropertyChanged( - SelectedItemProperty, - old, - val)); + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); - if (val != null) - { - if (SelectedItems.Count != 1 || SelectedItems[0] != val) - { - _syncingSelectedItems = true; - SelectSingleItem(val); - _syncingSelectedItems = false; - } - } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - }, value); + if (value != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != value) + { + _syncingSelectedItems = true; + SelectSingleItem(value); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) + { + SelectedItems.Clear(); + } } } From f8741ead2c072c87a489ed0697a2dbfbe595c6b4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 08:40:36 +0200 Subject: [PATCH 10/33] Added failing SelectingItemsControl tests. --- .../SelectingItemsControlTests_Multiple.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index a33d97779e..d66acdf9ce 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1026,6 +1026,46 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedItems.Count); } + [Fact] + public void Shift_Right_Click_Should_Not_Select_Mutiple() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } + + [Fact] + public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + { + var target = new ListBox + { + Template = Template(), + Items = new[] { "Foo", "Bar", "Baz" }, + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + SelectionMode = SelectionMode.Multiple, + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + _helper.Click((Interactive)target.Presenter.Panel.Children[0]); + _helper.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Right, modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } + private IEnumerable SelectedContainers(SelectingItemsControl target) { return target.Presenter.Panel.Children From 07a625d48942b0343c2d98e4de1394f459e156a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 08:55:17 +0200 Subject: [PATCH 11/33] Added more failing TreeViewTests. --- .../TreeViewTests.cs | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 8defb353b3..bb8466ea3e 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -749,7 +749,6 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTreeViewTemplate(), Items = tree, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -777,7 +776,6 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTreeViewTemplate(), Items = tree, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -806,6 +804,66 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, target.SelectedItems.Count); } + [Fact] + public void Shift_Right_Click_Should_Not_Select_Mutiple() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Shift); + + Assert.Equal(1, target.SelectedItems.Count); + } + + [Fact] + public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + SelectionMode = SelectionMode.Multiple, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + target.ExpandSubTree((TreeViewItem)target.Presenter.Panel.Children[0]); + + var rootNode = tree[0]; + var from = rootNode.Children[0]; + var to = rootNode.Children[1]; + var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from); + var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to); + + _mouse.Click(fromContainer); + _mouse.Click(toContainer, MouseButton.Right, modifiers: InputModifiers.Control); + + Assert.Equal(1, target.SelectedItems.Count); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); From f634e9af81bec1669a9526286965549d34b868c3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 09:09:39 +0200 Subject: [PATCH 12/33] Fix TreeView right-click selection. Fixes #2660. --- src/Avalonia.Controls/TreeView.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 4404e46ad9..4514109e12 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -510,7 +510,8 @@ namespace Avalonia.Controls e.Source, true, (e.InputModifiers & InputModifiers.Shift) != 0, - (e.InputModifiers & InputModifiers.Control) != 0); + (e.InputModifiers & InputModifiers.Control) != 0, + e.MouseButton == MouseButton.Right); } } @@ -521,11 +522,13 @@ namespace Avalonia.Controls /// Whether the item should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. protected void UpdateSelectionFromContainer( IControl container, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var item = ItemContainerGenerator.Index.ItemFromContainer(container); @@ -546,7 +549,14 @@ namespace Avalonia.Controls var multi = (mode & SelectionMode.Multiple) != 0; var range = multi && selectedContainer != null && rangeModifier; - if (!toggle && !range) + if (rightButton) + { + if (!SelectedItems.Contains(item)) + { + SelectSingleItem(item); + } + } + else if (!toggle && !range) { SelectSingleItem(item); } @@ -715,6 +725,7 @@ namespace Avalonia.Controls /// Whether the container should be selected or unselected. /// Whether the range modifier is enabled (i.e. shift key). /// Whether the toggle modifier is enabled (i.e. ctrl key). + /// Whether the event is a right-click. /// /// True if the event originated from a container that belongs to the control; otherwise /// false. @@ -723,13 +734,14 @@ namespace Avalonia.Controls IInteractive eventSource, bool select = true, bool rangeModifier = false, - bool toggleModifier = false) + bool toggleModifier = false, + bool rightButton = false) { var container = GetContainerFromEventSource(eventSource); if (container != null) { - UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier); + UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton); return true; } From afa38852cf4dbb45ce9428dc2e84b4a0e4310ca7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 2 Jul 2019 11:41:42 +0200 Subject: [PATCH 13/33] Fix SelectingItemsControl multiple selection. - Allow `SelectAll` regardless of `SelectionMode`: `SelectionMode` should only apply to user-interaction - Don't select multiple on shift/ctrl-right click --- .../Primitives/SelectingItemsControl.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 91a9fa7e40..2c0f67fada 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -469,11 +469,6 @@ namespace Avalonia.Controls.Primitives /// protected void SelectAll() { - if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0) - { - throw new NotSupportedException("Multiple selection is not enabled on this control."); - } - UpdateSelectedItems(() => { _selection.Clear(); @@ -523,7 +518,14 @@ namespace Avalonia.Controls.Primitives var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0); var range = multi && rangeModifier; - if (range) + if (rightButton) + { + if (!_selection.Contains(index)) + { + UpdateSelectedItem(index); + } + } + else if (range) { UpdateSelectedItems(() => { @@ -582,7 +584,7 @@ namespace Avalonia.Controls.Primitives } else { - UpdateSelectedItem(index, !(rightButton && _selection.Contains(index))); + UpdateSelectedItem(index); } if (Presenter?.Panel != null) From b0cce6aaca7cd576e741594a7b5f5ad8629244f8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 16 Jul 2019 16:13:41 +0300 Subject: [PATCH 14/33] Removed outdated link to appveyor --- readme.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/readme.md b/readme.md index 7a03fda384..ee44a0cc3f 100644 --- a/readme.md +++ b/readme.md @@ -32,9 +32,6 @@ Install-Package Avalonia.Desktop ## Bleeding Edge Builds -Try out the latest build of Avalonia available for download here: -https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts - or use nightly build feeds as described here: https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed From bd790e075672222ff4733a7c0d836fce012b2a64 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 16 Jul 2019 20:49:56 +0300 Subject: [PATCH 15/33] Update VS image --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3f4fbb0d50..7e3532ee23 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,7 +102,7 @@ jobs: - job: Windows pool: - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' steps: - task: CmdLine@2 displayName: 'Install Nuke' From 9118872498fa92a829fd7ca043a5916136e800e0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 16 Jul 2019 21:53:18 +0300 Subject: [PATCH 16/33] Android -> 8.0 --- samples/ControlCatalog.Android/ControlCatalog.Android.csproj | 2 +- src/Android/Avalonia.Android/Avalonia.Android.csproj | 2 +- .../Avalonia.AndroidTestApplication.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index d8f0f39977..bc76a39f08 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 9c3d4fb3a1..0089ea3b8d 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid44 + monoandroid80 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index 9c72321472..1b2b205d45 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml From 3113776f28b89352ed1703e4df766b413e450311 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 16 Jul 2019 22:21:09 +0300 Subject: [PATCH 17/33] Java SDK workaround for azure pipelines --- nukebuild/Build.cs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index a99ac4d026..dd2f27116d 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -89,6 +89,29 @@ partial class Build : NukeBuild } + IReadOnlyCollection MsBuildCommon( + string projectFile, + Configure configurator = null) + { + return MSBuild(projectFile, c => + { + // This is required for VS2019 image on Azure Pipelines + if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) + { + var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); + if (javaSdk != null) + c = c.AddProperty("JavaSdkDirectory", javaSdk); + } + + c = c.AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", "true") + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal); + c = configurator?.Invoke(c) ?? c; + return c; + }); + } Target Clean => _ => _.Executes(() => { DeleteDirectories(Parameters.BuildDirs); @@ -105,13 +128,8 @@ partial class Build : NukeBuild .Executes(() => { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c + MsBuildCommon(Parameters.MSBuildSolution, c => c .SetArgumentConfigurator(a => a.Add("/r")) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) .AddTargets("Build") ); @@ -237,12 +255,7 @@ partial class Build : NukeBuild { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) + MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else DotNetPack(Parameters.MSBuildSolution, c => From e4ee2c3a6a3facad9dcf4829ca04276a409471a1 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 17 Jul 2019 18:28:05 +0200 Subject: [PATCH 18/33] Clear _inner collection in RenderLayers.Clear --- src/Avalonia.Visuals/Rendering/RenderLayers.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index 0ff7862ab6..e82934fbad 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -8,8 +8,8 @@ namespace Avalonia.Rendering { public class RenderLayers : IEnumerable { - private List _inner = new List(); - private Dictionary _index = new Dictionary(); + private readonly List _inner = new List(); + private readonly Dictionary _index = new Dictionary(); public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -56,6 +56,7 @@ namespace Avalonia.Rendering } _index.Clear(); + _inner.Clear(); } public bool TryGetValue(IVisual layerRoot, out RenderLayer value) From 3c787b3d4ddfa583af0d91b76054374e7a576157 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 17 Jul 2019 18:55:58 +0200 Subject: [PATCH 19/33] Fix events for ContextMenu and Popup. --- src/Avalonia.Controls/ContextMenu.cs | 38 ++++++++++--- src/Avalonia.Controls/Menu.cs | 46 ++++++++-------- src/Avalonia.Controls/MenuBase.cs | 4 +- src/Avalonia.Controls/Primitives/Popup.cs | 35 ++++++++++-- .../ContextMenuTests.cs | 54 +++++++++++++++++++ .../Primitives/PopupTests.cs | 48 +++++++++++++++++ 6 files changed, 192 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92293a32d6..d39cab0284 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -90,9 +91,14 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (IsOpen) + { + return; + } + if (_popup == null) { - _popup = new Popup() + _popup = new Popup { PlacementMode = PlacementMode.Pointer, PlacementTarget = control, @@ -107,7 +113,14 @@ namespace Avalonia.Controls ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// @@ -115,13 +128,15 @@ namespace Avalonia.Controls /// public override void Close() { + if (!IsOpen) + { + return; + } + if (_popup != null && _popup.IsVisible) { _popup.IsOpen = false; } - - SelectedIndex = -1; - IsOpen = false; } protected override IItemContainerGenerator CreateItemContainerGenerator() @@ -129,6 +144,18 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + private void CloseCore() + { + SelectedIndex = -1; + IsOpen = false; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } + private void PopupOpened(object sender, EventArgs e) { Focus(); @@ -145,8 +172,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu.IsOpen = false; - contextMenu.SelectedIndex = -1; + contextMenu.CloseCore(); } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..b60a97e1c8 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -40,37 +40,41 @@ namespace Avalonia.Controls /// public override void Close() { - if (IsOpen) + if (!IsOpen) { - foreach (var i in ((IMenu)this).SubItems) - { - i.Close(); - } - - IsOpen = false; - SelectedIndex = -1; + return; + } - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuClosedEvent, - Source = this, - }); + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); } + + IsOpen = false; + SelectedIndex = -1; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); } /// public override void Open() { - if (!IsOpen) + if (IsOpen) { - IsOpen = true; - - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuOpenedEvent, - Source = this, - }); + return; } + + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index d6eb40360b..aeee685980 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -31,13 +31,13 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); private bool _isOpen; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index e02d46c1df..42c1a98349 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Diagnostics; using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; @@ -270,9 +271,10 @@ namespace Avalonia.Controls.Primitives _popupRoot.SnapInsideScreenEdges(); } - _ignoreIsOpenChanged = true; - IsOpen = true; - _ignoreIsOpenChanged = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = true; + } Opened?.Invoke(this, EventArgs.Empty); } @@ -305,7 +307,11 @@ namespace Avalonia.Controls.Primitives _popupRoot.Hide(); } - IsOpen = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + Closed?.Invoke(this, EventArgs.Empty); } @@ -467,5 +473,26 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private IgnoreIsOpenScope BeginIgnoringIsOpen() + { + return new IgnoreIsOpenScope(this); + } + + private readonly struct IgnoreIsOpenScope : IDisposable + { + private readonly Popup _owner; + + public IgnoreIsOpenScope(Popup owner) + { + _owner = owner; + _owner._ignoreIsOpenChanged = true; + } + + public void Dispose() + { + _owner._ignoreIsOpenChanged = false; + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 6482fcb4da..58d205deaa 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -16,6 +16,60 @@ namespace Avalonia.Controls.UnitTests private Mock popupImpl; private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] + public void Opening_Raises_Single_Opened_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + int openedCount = 0; + + sut.MenuOpened += (sender, args) => + { + openedCount++; + }; + + sut.Open(null); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Closing_Raises_Single_Closed_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + sut.Open(null); + + int closedCount = 0; + + sut.MenuClosed += (sender, args) => + { + closedCount++; + }; + + sut.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void Clicking_On_Control_Toggles_ContextMenu() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index e266150901..2e22725125 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Moq; using Avalonia.Controls.Presenters; @@ -185,6 +186,53 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Popup_Open_Should_Raise_Single_Opened_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + + int openedCount = 0; + + target.Opened += (sender, args) => + { + openedCount++; + }; + + target.Open(); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Popup_Close_Should_Raise_Single_Closed_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + target.Open(); + + int closedCount = 0; + + target.Closed += (sender, args) => + { + closedCount++; + }; + + target.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void PopupRoot_Should_Have_Template_Applied() { From dfe853e24ae677698b22737717880ba557928bae Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Wed, 17 Jul 2019 19:01:30 +0200 Subject: [PATCH 20/33] Cleanup usings. --- src/Avalonia.Controls/ContextMenu.cs | 1 - src/Avalonia.Controls/MenuBase.cs | 1 - src/Avalonia.Controls/Primitives/Popup.cs | 2 -- 3 files changed, 4 deletions(-) diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index d39cab0284..58b4324a3e 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index aeee685980..8eed58bb4d 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -7,7 +7,6 @@ using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 42c1a98349..058658357f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,12 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Diagnostics; using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; From d721373836361016d45ac21ec3fd9d33e854c5d1 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 18 Jul 2019 11:50:57 +0200 Subject: [PATCH 21/33] Improve DevTools UX and code. --- src/Avalonia.Diagnostics/DevTools.xaml | 37 ++++++----- src/Avalonia.Diagnostics/DevTools.xaml.cs | 8 ++- src/Avalonia.Diagnostics/ViewLocator.cs | 2 +- .../ViewModels/DevToolsViewModel.cs | 65 +++++++------------ .../ViewModels/EventsViewModel.cs | 13 ++-- .../ViewModels/IDevToolViewModel.cs | 16 +++++ .../ViewModels/TreePageViewModel.cs | 7 +- .../Views/ControlDetailsView.cs | 16 ++--- ...enions.cs => PropertyChangedExtensions.cs} | 5 +- .../Views/TreePage.xaml.cs | 3 + 10 files changed, 88 insertions(+), 84 deletions(-) create mode 100644 src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs rename src/Avalonia.Diagnostics/Views/{PropertyChangedExtenions.cs => PropertyChangedExtensions.cs} (85%) diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index 0f55d42e33..a538516c1a 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -1,23 +1,24 @@ - - - - - - + - - - - Hold Ctrl+Shift over a control to inspect. - - Focused: - - - Pointer Over: - - - + + + + + + + + + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index e0bacf326b..ccb6151ada 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -1,10 +1,13 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Diagnostics.ViewModels; using Avalonia.Input; using Avalonia.Input.Raw; @@ -82,7 +85,8 @@ namespace Avalonia.Diagnostics DataTemplates = { new ViewLocator(), - } + }, + Title = "Avalonia DevTools" }; devToolsWindow.Closed += devTools.DevToolsClosed; diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index b107338aec..cda511909a 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics return data is TViewModel; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index c6d3f02e8b..bc80ab0550 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using Avalonia.Controls; using Avalonia.Input; @@ -10,21 +12,23 @@ namespace Avalonia.Diagnostics.ViewModels { internal class DevToolsViewModel : ViewModelBase { - private ViewModelBase _content; - private int _selectedTab; - private TreePageViewModel _logicalTree; - private TreePageViewModel _visualTree; - private EventsViewModel _eventsView; + private IDevToolViewModel _selectedTool; private string _focusedControl; private string _pointerOverElement; public DevToolsViewModel(IControl root) { - _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); - _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); - _eventsView = new EventsViewModel(root); + Tools = new ObservableCollection + { + new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"), + new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"), + new EventsViewModel(root) + }; + + SelectedTool = Tools.First(); UpdateFocusedControl(); + KeyboardDevice.Instance.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) @@ -33,58 +37,33 @@ namespace Avalonia.Diagnostics.ViewModels } }; - SelectedTab = 0; root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); } - public ViewModelBase Content + public IDevToolViewModel SelectedTool { - get { return _content; } - private set { RaiseAndSetIfChanged(ref _content, value); } + get => _selectedTool; + set => RaiseAndSetIfChanged(ref _selectedTool, value); } - public int SelectedTab - { - get { return _selectedTab; } - set - { - _selectedTab = value; - - switch (value) - { - case 0: - Content = _logicalTree; - break; - case 1: - Content = _visualTree; - break; - case 2: - Content = _eventsView; - break; - } - - RaisePropertyChanged(); - } - } + public ObservableCollection Tools { get; } public string FocusedControl { - get { return _focusedControl; } - private set { RaiseAndSetIfChanged(ref _focusedControl, value); } + get => _focusedControl; + private set => RaiseAndSetIfChanged(ref _focusedControl, value); } public string PointerOverElement { - get { return _pointerOverElement; } - private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } + get => _pointerOverElement; + private set => RaiseAndSetIfChanged(ref _pointerOverElement, value); } public void SelectControl(IControl control) { - var tree = Content as TreePageViewModel; - - if (tree != null) + if (SelectedTool is TreePageViewModel tree) { tree.SelectControl(control); } diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs index a23677afc8..1c868148ce 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs @@ -5,8 +5,6 @@ using System; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; -using System.Windows.Input; - using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Interactivity; @@ -14,21 +12,24 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { - internal class EventsViewModel : ViewModelBase + internal class EventsViewModel : ViewModelBase, IDevToolViewModel { private readonly IControl _root; private FiredEvent _selectedEvent; public EventsViewModel(IControl root) { - this._root = root; - this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered() + _root = root; + + Nodes = RoutedEventRegistry.Instance.GetAllRegistered() .GroupBy(e => e.OwnerType) .OrderBy(e => e.Key.Name) .Select(g => new EventOwnerTreeNode(g.Key, g, this)) .ToArray(); } + public string Name => "Events"; + public EventTreeNodeBase[] Nodes { get; } public ObservableCollection RecordedEvents { get; } = new ObservableCollection(); @@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (bool)value ? Brushes.LightGreen : Brushes.Transparent; + return (bool)value ? Brushes.Green : Brushes.Transparent; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs new file mode 100644 index 0000000000..0434230a63 --- /dev/null +++ b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs @@ -0,0 +1,16 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Diagnostics.ViewModels +{ + /// + /// View model interface for tool showing up in DevTools + /// + public interface IDevToolViewModel + { + /// + /// Name of a tool. + /// + string Name { get; } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs index dba44c5d0c..6b294c98bd 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs @@ -6,16 +6,19 @@ using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ViewModelBase + internal class TreePageViewModel : ViewModelBase, IDevToolViewModel { private TreeNode _selected; private ControlDetailsViewModel _details; - public TreePageViewModel(TreeNode[] nodes) + public TreePageViewModel(TreeNode[] nodes, string name) { Nodes = nodes; + Name = name; } + public string Name { get; } + public TreeNode[] Nodes { get; protected set; } public TreeNode SelectedNode diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs index 381b2e04b4..868bc774bb 100644 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs @@ -7,7 +7,6 @@ using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Diagnostics.ViewModels; using Avalonia.Media; -using Avalonia.Styling; namespace Avalonia.Diagnostics.Views { @@ -42,16 +41,6 @@ namespace Avalonia.Diagnostics.Views { Content = _grid = new SimpleGrid { - Styles = - { - new Style(x => x.Is()) - { - Setters = new[] - { - new Setter(MarginProperty, new Thickness(2)), - } - }, - }, [GridRepeater.TemplateProperty] = pt, } }; @@ -61,8 +50,11 @@ namespace Avalonia.Diagnostics.Views { var property = (PropertyDetails)i; + var margin = new Thickness(2); + yield return new TextBlock { + Margin = margin, Text = property.Name, TextWrapping = TextWrapping.NoWrap, [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding(), @@ -70,6 +62,7 @@ namespace Avalonia.Diagnostics.Views yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) .Select(v => v?.ToString()) @@ -78,6 +71,7 @@ namespace Avalonia.Diagnostics.Views yield return new TextBlock { + Margin = margin, TextWrapping = TextWrapping.NoWrap, [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding(), }; diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs similarity index 85% rename from src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs rename to src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs index 2d833763a6..0bd08929ad 100644 --- a/src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs +++ b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.ComponentModel; using System.Reactive.Linq; using System.Reflection; diff --git a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs index d445f1cd70..88cbb03c34 100644 --- a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs +++ b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Controls; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; From 0b88151b658cc67cb8a8dd70ffff60e2a55964a2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 18 Jul 2019 15:16:29 +0200 Subject: [PATCH 22/33] Fix tests failing after merge. --- .../Primitives/SelectingItemsControlTests_Multiple.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 972b933bb9..88a4820286 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1100,7 +1100,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -1120,7 +1120,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; From cae46af2870b9e5fd54c9fe94f1bcb11acc81db4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 19 Jul 2019 00:21:03 +0200 Subject: [PATCH 23/33] Fixed typos. --- .../Primitives/SelectingItemsControlTests_Multiple.cs | 4 ++-- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 88a4820286..4bcfeb6d03 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -1094,7 +1094,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Shift_Right_Click_Should_Not_Select_Mutiple() + public void Shift_Right_Click_Should_Not_Select_Multiple() { var target = new ListBox { @@ -1114,7 +1114,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + public void Ctrl_Right_Click_Should_Not_Select_Multiple() { var target = new ListBox { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 919b600a1c..5646e86f7a 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -784,7 +784,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Shift_Right_Click_Should_Not_Select_Mutiple() + public void Shift_Right_Click_Should_Not_Select_Multiple() { var tree = CreateTestTreeData(); var target = new TreeView @@ -814,7 +814,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Ctrl_Right_Click_Should_Not_Select_Mutiple() + public void Ctrl_Right_Click_Should_Not_Select_Multiple() { var tree = CreateTestTreeData(); var target = new TreeView From bc1ddc31ab3e9ad21440f91bca870ced98794285 Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Fri, 19 Jul 2019 17:44:00 +0200 Subject: [PATCH 24/33] remove duplicate setter for "HeadersVisibility". --- src/Avalonia.Controls.DataGrid/Themes/Default.xaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 2889a5c77c..eaa267ba66 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -195,7 +195,6 @@ - @@ -230,4 +229,4 @@ - \ No newline at end of file + From 812c5f30c90d09473f40375ebe59e42641843608 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 21 Jul 2019 17:23:11 +0200 Subject: [PATCH 25/33] Remove SetAndRaise callback handling. Move event raising to setter. --- src/Avalonia.Base/AvaloniaObject.cs | 82 +++---------------- .../Utilities/AvaloniaPropertyCollection.cs | 4 +- .../Utilities/DeferredSetterOptimized.cs | 27 +++--- src/Avalonia.Base/ValueStore.cs | 14 ++-- .../Avalonia.Benchmarks.csproj | 2 +- .../Base/DirectPropertyBenchmark.cs | 2 +- 6 files changed, 36 insertions(+), 95 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 22aeccc3fb..99e27e7daf 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -466,7 +466,7 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected void RaisePropertyChanged( + protected internal void RaisePropertyChanged( AvaloniaProperty property, object oldValue, object newValue, @@ -569,44 +569,6 @@ namespace Avalonia => SetAndRaiseCore(property, ref backing, val, notifyWrapper), value); } - - /// - /// Default setter handler that will set backing field and raise notification. - /// - private sealed class DefaultSetterHandler : DeferredSetterOptimized.ISetterHandler - { - public static readonly DefaultSetterHandler Instance = new DefaultSetterHandler(); - - public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) - { - var old = backing; - backing = value; - - source.RaisePropertyChanged(property, old, value); - - return true; - } - } - - /// - /// Setter handler that will run custom user callback. - /// - private sealed class CallbackSetterHandler : DeferredSetterOptimized.ISetterHandler - { - private readonly SetAndRaiseCallback _callback; - - public CallbackSetterHandler(SetAndRaiseCallback callback) - { - _callback = callback; - } - - public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) - { - _callback(value, ref backing, notification => notification()); - - return true; - } - } protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) { @@ -619,39 +581,21 @@ namespace Avalonia DeferredSetterOptimized setter = Values.GetDeferredSetter(property); - return setter.SetAndNotify(this, property, DefaultSetterHandler.Instance, ref field, value); - } - - protected bool SetAndRaiseOptimized( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - VerifyAccess(); - - if (EqualityComparer.Default.Equals(field, value)) - { - return false; - } - - DeferredSetterOptimized setter = Values.GetDeferredSetter(property); - - return setter.SetAndNotify(this, property, new CallbackSetterHandler(setterCallback) , ref field, value); + return setter.SetAndNotify(this, property, ref field, value); } /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) { var old = field; field = value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs index 5b1492a7a8..9b64342814 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs @@ -95,7 +95,7 @@ namespace Avalonia.Utilities return true; } - public void AddValueInternal(AvaloniaProperty property, TValue value) + public void AddValue(AvaloniaProperty property, TValue value) { Entry[] entries = new Entry[_entries.Length + 1]; @@ -117,7 +117,7 @@ namespace Avalonia.Utilities _entries = entries; } - public void SetValueInternal(AvaloniaProperty property, TValue value) + public void SetValue(AvaloniaProperty property, TValue value) { _entries[TryFindEntry(property.Id).Item1].Value = value; } diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs index e3bed90498..4858372828 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs @@ -17,20 +17,26 @@ namespace Avalonia.Utilities _pendingValues = new SingleOrQueue(); } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; + + backing = value; + + source.RaisePropertyChanged(property, old, value); + } + public bool SetAndNotify( AvaloniaObject source, AvaloniaProperty property, - ISetterHandler handler, ref TSetRecord backing, TSetRecord value) { if (!_isNotifying) { - bool updated; - using (new NotifyDisposable(this)) { - updated = handler.Update(source, property, ref backing, value); + SetAndRaisePropertyChanged(source, property, ref backing, value); } if (!_pendingValues.Empty) @@ -39,12 +45,12 @@ namespace Avalonia.Utilities { while (!_pendingValues.Empty) { - updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue()); + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); } } } - return updated; + return true; } _pendingValues.Enqueue(value); @@ -71,14 +77,5 @@ namespace Avalonia.Utilities _setter._isNotifying = false; } } - - public interface ISetterHandler - { - bool Update( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value); - } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index b2c4c68f9c..b7f5c26801 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -33,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - _propertyValues.AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -57,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValue(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -65,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } } @@ -78,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValue(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - _propertyValues.AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } } @@ -187,7 +187,7 @@ namespace Avalonia var newDeferredSetter = new DeferredSetterOptimized(); - _deferredSetters.AddValueInternal(property, newDeferredSetter); + _deferredSetters.AddValue(property, newDeferredSetter); return newDeferredSetter; } diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 6550a23b7b..f503bf66a7 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs index 4f36b54414..ecf8a1fb2b 100644 --- a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -79,4 +79,4 @@ namespace Avalonia.Benchmarks.Base } } } -} \ No newline at end of file +} From 48be9dc2600acf8a3269e6f93206b6b7d4f275cd Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 21 Jul 2019 22:37:19 +0200 Subject: [PATCH 26/33] Remove old DeferredSetter implementation. Cleanup code, add license info. --- src/Avalonia.Base/AvaloniaObject.cs | 73 +------ ...ction.cs => AvaloniaPropertyValueStore.cs} | 13 +- src/Avalonia.Base/Utilities/DeferredSetter.cs | 180 +++++------------- .../Utilities/DeferredSetterOptimized.cs | 81 -------- src/Avalonia.Base/ValueStore.cs | 14 +- .../Base/DirectPropertyBenchmark.cs | 17 -- 6 files changed, 66 insertions(+), 312 deletions(-) rename src/Avalonia.Base/Utilities/{AvaloniaPropertyCollection.cs => AvaloniaPropertyValueStore.cs} (91%) delete mode 100644 src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 99e27e7daf..85dccd53cb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -508,45 +508,6 @@ namespace Avalonia } } - /// - /// A callback type for encapsulating complex logic for setting direct properties. - /// - /// The type of the property. - /// The value to which to set the property. - /// The backing field for the property. - /// A wrapper for the property-changed notification. - protected delegate void SetAndRaiseCallback(T value, ref T field, Action notifyWrapper); - - /// - /// Sets the backing field for a direct avalonia property, raising the - /// event if the value has changed. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// A callback called to actually set the value to the backing field. - /// The value. - /// - /// True if the value changed, otherwise false. - /// - protected bool SetAndRaise( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - Contract.Requires(setterCallback != null); - return Values.Setter.SetAndNotify( - property, - ref field, - (object update, ref T backing, Action notify) => - { - setterCallback((T)update, ref backing, notify); - return true; - }, - value); - } - /// /// Sets the backing field for a direct avalonia property, raising the /// event if the value has changed. @@ -562,48 +523,16 @@ namespace Avalonia { VerifyAccess(); - return SetAndRaise( - property, - ref field, - (T val, ref T backing, Action notifyWrapper) - => SetAndRaiseCore(property, ref backing, val, notifyWrapper), - value); - } - - protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) - { - VerifyAccess(); - if (EqualityComparer.Default.Equals(field, value)) { return false; } - DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + DeferredSetter setter = Values.GetDeferredSetter(property); return setter.SetAndNotify(this, property, ref field, value); } - /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) - { - var old = field; - field = value; - - notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue)); - return true; - } - /// /// Tries to cast a value to a type, taking into account that the value may be a /// . diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs similarity index 91% rename from src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs rename to src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index 9b64342814..ac128d83de 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -1,13 +1,20 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; namespace Avalonia.Utilities { - internal sealed class AvaloniaPropertyCollection + /// + /// Stores values with as key. + /// + /// Stored value type. + internal sealed class AvaloniaPropertyValueStore { private Entry[] _entries; - public AvaloniaPropertyCollection() + public AvaloniaPropertyValueStore() { // The last item in the list is always int.MaxValue _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 1b1324b1c5..54458d6e6a 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,5 +1,7 @@ -using System; -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; namespace Avalonia.Utilities { @@ -8,161 +10,75 @@ namespace Avalonia.Utilities /// Used to fix #855. /// /// The type of value with which to track the delayed assignment. - class DeferredSetter + internal sealed class DeferredSetter { - private struct NotifyDisposable : IDisposable + private readonly SingleOrQueue _pendingValues; + private bool _isNotifying; + + public DeferredSetter() { - private readonly SettingStatus status; + _pendingValues = new SingleOrQueue(); + } - internal NotifyDisposable(SettingStatus status) - { - this.status = status; - status.Notifying = true; - } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; - public void Dispose() - { - status.Notifying = false; - } + backing = value; + + source.RaisePropertyChanged(property, old, value); } - /// - /// Information on current setting/notification status of a property. - /// - private class SettingStatus + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value) { - public bool Notifying { get; set; } - - private SingleOrQueue pendingValues; - - public SingleOrQueue PendingValues + if (!_isNotifying) { - get + using (new NotifyDisposable(this)) { - return pendingValues ?? (pendingValues = new SingleOrQueue()); + SetAndRaisePropertyChanged(source, property, ref backing, value); } - } - } - private Dictionary _setRecords; - private Dictionary SetRecords - => _setRecords ?? (_setRecords = new Dictionary()); + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } - private SettingStatus GetOrCreateStatus(AvaloniaProperty property) - { - if (!SetRecords.TryGetValue(property, out var status)) - { - status = new SettingStatus(); - SetRecords.Add(property, status); + return true; } - return status; - } - - /// - /// Mark the property as currently notifying. - /// - /// The property to mark as notifying. - /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(AvaloniaProperty property) - { - Contract.Requires(!IsNotifying(property)); - - SettingStatus status = GetOrCreateStatus(property); - - return new NotifyDisposable(status); - } - - /// - /// Check if the property is currently notifying listeners. - /// - /// The property. - /// If the property is currently notifying listeners. - private bool IsNotifying(AvaloniaProperty property) - => SetRecords.TryGetValue(property, out var value) && value.Notifying; - - /// - /// Add a pending assignment for the property. - /// - /// The property. - /// The value to assign. - private void AddPendingSet(AvaloniaProperty property, TSetRecord value) - { - Contract.Requires(IsNotifying(property)); - - GetOrCreateStatus(property).PendingValues.Enqueue(value); - } + _pendingValues.Enqueue(value); - /// - /// Checks if there are any pending assignments for the property. - /// - /// The property to check. - /// If the property has any pending assignments. - private bool HasPendingSet(AvaloniaProperty property) - { - return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; + return false; } /// - /// Gets the first pending assignment for the property. + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. /// - /// The property to check. - /// The first pending assignment for the property. - private TSetRecord GetFirstPendingSet(AvaloniaProperty property) + private readonly struct NotifyDisposable : IDisposable { - return GetOrCreateStatus(property).PendingValues.Dequeue(); - } - - public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback); + private readonly DeferredSetter _setter; - /// - /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824 - /// - /// The property to set. - /// The backing field for the property - /// - /// A callback that actually sets the property. - /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification. - /// - /// The value to try to set. - public bool SetAndNotify( - AvaloniaProperty property, - ref TValue backing, - SetterDelegate setterCallback, - TSetRecord value) - { - Contract.Requires(setterCallback != null); - if (!IsNotifying(property)) + internal NotifyDisposable(DeferredSetter setter) { - bool updated = false; - if (!object.Equals(value, backing)) - { - updated = setterCallback(value, ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - while (HasPendingSet(property)) - { - updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - - return updated; + _setter = setter; + _setter._isNotifying = true; } - else if(!object.Equals(value, backing)) + + public void Dispose() { - AddPendingSet(property, value); + _setter._isNotifying = false; } - return false; } } } diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs deleted file mode 100644 index 4858372828..0000000000 --- a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; - -namespace Avalonia.Utilities -{ - /// - /// A utility class to enable deferring assignment until after property-changed notifications are sent. - /// Used to fix #855. - /// - /// The type of value with which to track the delayed assignment. - internal sealed class DeferredSetterOptimized - { - private bool _isNotifying; - private readonly SingleOrQueue _pendingValues; - - public DeferredSetterOptimized() - { - _pendingValues = new SingleOrQueue(); - } - - private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) - { - var old = backing; - - backing = value; - - source.RaisePropertyChanged(property, old, value); - } - - public bool SetAndNotify( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value) - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - SetAndRaisePropertyChanged(source, property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - /// - /// Disposable that marks the property as currently notifying. - /// When disposed, marks the property as done notifying. - /// - private readonly struct NotifyDisposable : IDisposable - { - private readonly DeferredSetterOptimized _setter; - - internal NotifyDisposable(DeferredSetterOptimized setter) - { - _setter = setter; - _setter._isNotifying = true; - } - - public void Dispose() - { - _setter._isNotifying = false; - } - } - } -} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index b7f5c26801..8dfabc71a2 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,15 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private readonly AvaloniaPropertyCollection _propertyValues; - private readonly AvaloniaPropertyCollection _deferredSetters; + private readonly AvaloniaPropertyValueStore _propertyValues; + private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; public ValueStore(AvaloniaObject owner) { _owner = owner; - _propertyValues = new AvaloniaPropertyCollection(); - _deferredSetters = new AvaloniaPropertyCollection(); + _propertyValues = new AvaloniaPropertyValueStore(); + _deferredSetters = new AvaloniaPropertyValueStore(); } public IDisposable AddBinding( @@ -178,14 +178,14 @@ namespace Avalonia return value; } - public DeferredSetterOptimized GetDeferredSetter(AvaloniaProperty property) + public DeferredSetter GetDeferredSetter(AvaloniaProperty property) { if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - return (DeferredSetterOptimized)deferredSetter; + return (DeferredSetter)deferredSetter; } - var newDeferredSetter = new DeferredSetterOptimized(); + var newDeferredSetter = new DeferredSetter(); _deferredSetters.AddValue(property, newDeferredSetter); diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs index ecf8a1fb2b..02f9397e2c 100644 --- a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -16,17 +16,6 @@ namespace Avalonia.Benchmarks.Base } } - [Benchmark] - public void SetAndRaiseOptimized() - { - var obj = new DirectClass(); - - for (var i = 0; i < 100; ++i) - { - obj.IntValueOptimized += 1; - } - } - [Benchmark] public void SetAndRaiseSimple() { @@ -53,12 +42,6 @@ namespace Avalonia.Benchmarks.Base set => SetAndRaise(IntValueProperty, ref _intValue, value); } - public int IntValueOptimized - { - get => _intValue; - set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value); - } - public int IntValueSimple { get => _intValue; From ad994a685cd6a8778bfebc6bcc77fce2fbd9a704 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 22 Jul 2019 00:19:45 +0200 Subject: [PATCH 27/33] Fix PriorityValue usage of DeferredSetter. Implement slower path in DeferredSetter that deals with callbacks. --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- src/Avalonia.Base/IPriorityValueOwner.cs | 9 ++++- src/Avalonia.Base/PriorityValue.cs | 34 +++++++++-------- src/Avalonia.Base/Utilities/DeferredSetter.cs | 38 +++++++++++++++++++ src/Avalonia.Base/ValueStore.cs | 15 ++++---- .../PriorityValueTests.cs | 2 +- 6 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 85dccd53cb..a3d5803ab0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -528,7 +528,7 @@ namespace Avalonia return false; } - DeferredSetter setter = Values.GetDeferredSetter(property); + DeferredSetter setter = Values.GetDirectDeferredSetter(property); return setter.SetAndNotify(this, property, ref field, value); } diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 540b1bf19b..1d6e5e59ad 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -29,6 +29,13 @@ namespace Avalonia /// The notification. void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); + /// + /// Returns deferred setter for given non-direct property. + /// + /// Property. + /// Deferred setter for given property. + DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property); + /// /// Logs a binding error. /// @@ -40,7 +47,5 @@ namespace Avalonia /// Ensures that the current thread is the UI thread. /// void VerifyAccess(); - - DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 89a893577f..4996420fe7 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -30,7 +30,9 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; + private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback; private (object value, int priority) _value; + private DeferredSetter _setter; /// /// Initializes a new instance of the class. @@ -50,6 +52,7 @@ namespace Avalonia _valueType = valueType; _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; + _setAndNotifyCallback = SetAndNotify; } /// @@ -242,22 +245,22 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - Owner.Setter.SetAndNotify(Property, - ref _value, - UpdateCore, - (value, priority)); - } + var newValue = (value, priority); + + if (newValue == _value) + { + return; + } - private bool UpdateCore( - object update, - ref (object value, int priority) backing, - Action notify) - => UpdateCore(((object, int))update, ref backing, notify); + if (_setter == null) + { + _setter = Owner.GetNonDirectDeferredSetter(Property); + } + + _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); + } - private bool UpdateCore( - (object value, int priority) update, - ref (object value, int priority) backing, - Action notify) + private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) { var val = update.value; var notification = val as BindingNotification; @@ -286,7 +289,7 @@ namespace Avalonia if (notification == null || notification.HasValue) { - notify(() => Owner?.Changed(Property, ValuePriority, old, Value)); + Owner?.Changed(Property, ValuePriority, old, Value); } if (notification != null) @@ -305,7 +308,6 @@ namespace Avalonia val, val?.GetType()); } - return true; } } } diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 54458d6e6a..fd7a66fb52 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -5,6 +5,15 @@ using System; namespace Avalonia.Utilities { + /// + /// Callback invoked when deferred setter wants to set a value. + /// + /// Value type. + /// Property being set. + /// Backing field reference. + /// New value. + internal delegate void SetAndNotifyCallback(AvaloniaProperty property, ref TValue backing, TValue value); + /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. /// Used to fix #855. @@ -61,6 +70,35 @@ namespace Avalonia.Utilities return false; } + public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + where TValue : TSetRecord + { + if (!_isNotifying) + { + using (new NotifyDisposable(this)) + { + setAndNotifyCallback(property, ref backing, value); + } + + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + } + } + } + + return true; + } + + _pendingValues.Enqueue(value); + + return false; + } + /// /// Disposable that marks the property as currently notifying. /// When disposed, marks the property as done notifying. diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 8dfabc71a2..1bdbd4ca7c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -178,7 +178,7 @@ namespace Avalonia return value; } - public DeferredSetter GetDeferredSetter(AvaloniaProperty property) + private DeferredSetter GetDeferredSetter(AvaloniaProperty property) { if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { @@ -192,15 +192,14 @@ namespace Avalonia return newDeferredSetter; } - private DeferredSetter _deferredSetter; + public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) + { + return GetDeferredSetter(property); + } - public DeferredSetter Setter + public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) { - get - { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); - } + return GetDeferredSetter(property); } } } diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 2f1b7862a7..63e1790cce 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests private static Mock GetMockOwner() { var owner = new Mock(); - owner.SetupGet(o => o.Setter).Returns(new DeferredSetter()); + owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny())).Returns(new DeferredSetter()); return owner; } } From 0680f2012833833fe728be003f3f3e0d37819621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 21 Jul 2019 23:57:50 +0100 Subject: [PATCH 28/33] XML comment fixes. --- src/Avalonia.Controls/LayoutTransformControl.cs | 2 +- src/Avalonia.Styling/Controls/NameScopeLocator.cs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 07372eb714..1430c39c76 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -45,7 +45,7 @@ namespace Avalonia.Controls } /// - /// Utilize the for layout transforms. + /// Utilize the for layout transforms. /// public bool UseRenderTransform { diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs index 719cf9344b..354ed33657 100644 --- a/src/Avalonia.Styling/Controls/NameScopeLocator.cs +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -15,10 +15,8 @@ namespace Avalonia.Controls /// /// Tracks a named control relative to another control. /// - /// - /// The control relative from which the other control should be found. - /// - /// The name of the control to find. + /// The scope relative from which the object should be resolved. + /// The name of the object to find. public static IObservable Track(INameScope scope, string name) { return new NeverEndingSynchronousCompletionAsyncResultObservable(scope.FindAsync(name)); From a48bc262af183e58a82cad1116a101a0b4e4e183 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 23 Jul 2019 00:01:02 +0300 Subject: [PATCH 29/33] Support touch input for fbdev via libinput --- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 3 +- src/Avalonia.Input/TouchDevice.cs | 8 ++ .../Avalonia.LinuxFramebuffer/EvDevDevice.cs | 88 ------------ .../FramebufferToplevelImpl.cs | 14 +- .../Input/IInputBackend.cs | 12 ++ .../Input/IScreenInfoProvider.cs | 7 + .../Input/LibInput/LibInputBackend.cs | 125 ++++++++++++++++++ .../LibInput/LibInputNativeUnsafeMethods.cs | 122 +++++++++++++++++ .../LinuxFramebufferPlatform.cs | 3 +- src/Linux/Avalonia.LinuxFramebuffer/Mice.cs | 117 ---------------- .../NativeUnsafeMethods.cs | 2 +- 11 files changed, 288 insertions(+), 213 deletions(-) delete mode 100644 src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs delete mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Mice.cs diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index b728844e97..6fac90f255 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -19,7 +19,8 @@ namespace Avalonia.Input.Raw NonClientLeftButtonDown, TouchBegin, TouchUpdate, - TouchEnd + TouchEnd, + TouchCancel } /// diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 7f473bb320..c85f98b04a 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -61,6 +61,12 @@ namespace Avalonia.Input pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); } } + if (args.Type == RawPointerEventType.TouchCancel) + { + _pointers.Remove(args.TouchPointId); + using (pointer) + pointer.Capture(null); + } if (args.Type == RawPointerEventType.TouchUpdate) { @@ -68,6 +74,8 @@ namespace Avalonia.Input target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers)); } + + } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs deleted file mode 100644 index f28dca81b8..0000000000 --- a/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; - -namespace Avalonia.LinuxFramebuffer -{ - unsafe class EvDevDevice - { - private static readonly Lazy> AllMouseDevices = new Lazy>(() - => OpenMouseDevices()); - - private static List OpenMouseDevices() - { - var rv = new List(); - foreach (var dev in Directory.GetFiles("/dev/input", "event*").Select(Open)) - { - if (!dev.IsMouse) - NativeUnsafeMethods.close(dev.Fd); - else - rv.Add(dev); - } - return rv; - } - - public static IReadOnlyList MouseDevices => AllMouseDevices.Value; - - - public int Fd { get; } - private IntPtr _dev; - public string Name { get; } - public List EventTypes { get; private set; } = new List(); - public input_absinfo? AbsX { get; } - public input_absinfo? AbsY { get; } - - public EvDevDevice(int fd, IntPtr dev) - { - Fd = fd; - _dev = dev; - Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev)); - foreach (EvType type in Enum.GetValues(typeof(EvType))) - { - if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0) - EventTypes.Add(type); - } - var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X); - if (ptr != null) - AbsX = *ptr; - ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y); - if (ptr != null) - AbsY = *ptr; - } - - public input_event? NextEvent() - { - input_event ev; - if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0) - return ev; - return null; - } - - public bool IsMouse => EventTypes.Contains(EvType.EV_REL); - - public static EvDevDevice Open(string device) - { - var fd = NativeUnsafeMethods.open(device, 2048, 0); - if (fd <= 0) - throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}"); - IntPtr dev; - var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev); - if (rc < 0) - { - NativeUnsafeMethods.close(fd); - throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}"); - } - return new EvDevDevice(fd, dev); - } - - - } - - public class EvDevAxisInfo - { - public int Minimum { get; set; } - public int Maximum { get; set; } - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 78369a3648..9b9bd6c7b8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -2,25 +2,26 @@ using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.LinuxFramebuffer.Input; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : IEmbeddableWindowImpl + class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider { private readonly LinuxFramebuffer _fb; + private readonly IInputBackend _inputBackend; private bool _renderQueued; public IInputRoot InputRoot { get; private set; } - public FramebufferToplevelImpl(LinuxFramebuffer fb) + public FramebufferToplevelImpl(LinuxFramebuffer fb, IInputBackend inputBackend) { _fb = fb; + _inputBackend = inputBackend; Invalidate(default(Rect)); - var mice = new Mice(this, ClientSize.Width, ClientSize.Height); - mice.Start(); - mice.Event += e => Input?.Invoke(e); + _inputBackend.Initialize(this, e => Input?.Invoke(e)); } public IRenderer CreateRenderer(IRenderRoot root) @@ -49,6 +50,7 @@ namespace Avalonia.LinuxFramebuffer public void SetInputRoot(IInputRoot inputRoot) { InputRoot = inputRoot; + _inputBackend.SetInputRoot(inputRoot); } public Point PointToClient(PixelPoint p) => p.ToPoint(1); @@ -73,5 +75,7 @@ namespace Avalonia.LinuxFramebuffer add {} remove {} } + + public Size ScaledSize => _fb.PixelSize / Scaling; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs new file mode 100644 index 0000000000..84a903bb9d --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs @@ -0,0 +1,12 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.LinuxFramebuffer.Input +{ + public interface IInputBackend + { + void Initialize(IScreenInfoProvider info, Action onInput); + void SetInputRoot(IInputRoot root); + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs new file mode 100644 index 0000000000..cb0e51862a --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs @@ -0,0 +1,7 @@ +namespace Avalonia.LinuxFramebuffer.Input +{ + public interface IScreenInfoProvider + { + Size ScaledSize { get; } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs new file mode 100644 index 0000000000..ca31db6100 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Threading; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Threading; +using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; +namespace Avalonia.LinuxFramebuffer.Input.LibInput +{ + public class LibInputBackend : IInputBackend + { + private IScreenInfoProvider _screen; + private IInputRoot _inputRoot; + private readonly Queue _inputThreadActions = new Queue(); + private TouchDevice _touch = new TouchDevice(); + + private readonly Queue _inputQueue = new Queue(); + private Action _onInput; + private Dictionary _pointers = new Dictionary(); + + public LibInputBackend() + { + var ctx = libinput_path_create_context(); + + new Thread(()=>InputThread(ctx)).Start(); + } + + + + private unsafe void InputThread(IntPtr ctx) + { + var fd = libinput_get_fd(ctx); + + var timeval = stackalloc IntPtr[2]; + + + foreach (var f in Directory.GetFiles("/dev/input", "event*")) + libinput_path_add_device(ctx, f); + while (true) + { + IntPtr ev; + while ((ev = libinput_get_event(ctx)) != IntPtr.Zero) + { + + var type = libinput_event_get_type(ev); + if (type >= LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN && + type <= LibInputEventType.LIBINPUT_EVENT_TOUCH_CANCEL) + HandleTouch(ev, type); + + libinput_event_destroy(ev); + } + libinput_dispatch(ctx); + } + } + + private void ScheduleInput(RawInputEventArgs ev) + { + _inputQueue.Enqueue(ev); + if (_inputQueue.Count == 1) + { + Dispatcher.UIThread.Post(() => + { + while (_inputQueue.Count > 0) + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + var dequeuedEvent = _inputQueue.Dequeue(); + _onInput?.Invoke(dequeuedEvent); + } + }, DispatcherPriority.Input); + } + } + + private void HandleTouch(IntPtr ev, LibInputEventType type) + { + var tev = libinput_event_get_touch_event(ev); + if(tev == IntPtr.Zero) + return; + if (type < LibInputEventType.LIBINPUT_EVENT_TOUCH_FRAME) + { + var info = _screen.ScaledSize; + var slot = libinput_event_touch_get_slot(tev); + Point pt; + + if (type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN + || type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION) + { + var x = libinput_event_touch_get_x_transformed(tev, (int)info.Width); + var y = libinput_event_touch_get_y_transformed(tev, (int)info.Height); + pt = new Point(x, y); + _pointers[slot] = pt; + } + else + { + _pointers.TryGetValue(slot, out pt); + _pointers.Remove(slot); + } + + var ts = libinput_event_touch_get_time_usec(tev) / 1000; + if (_inputRoot == null) + return; + ScheduleInput(new RawTouchEventArgs(_touch, ts, + _inputRoot, + type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN ? RawPointerEventType.TouchBegin + : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_UP ? RawPointerEventType.TouchEnd + : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION ? RawPointerEventType.TouchUpdate + : RawPointerEventType.TouchCancel, + pt, InputModifiers.None, slot)); + } + } + + + public void Initialize(IScreenInfoProvider screen, Action onInput) + { + _screen = screen; + _onInput = onInput; + } + + public void SetInputRoot(IInputRoot root) + { + _inputRoot = root; + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs new file mode 100644 index 0000000000..6c82f53071 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs @@ -0,0 +1,122 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.LinuxFramebuffer.Input.LibInput +{ + unsafe class LibInputNativeUnsafeMethods + { + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + delegate int OpenRestrictedCallbackDelegate(IntPtr path, int flags, IntPtr userData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + delegate void CloseRestrictedCallbackDelegate(int fd, IntPtr userData); + + static int OpenRestricted(IntPtr path, int flags, IntPtr userData) + { + var fd = NativeUnsafeMethods.open(Marshal.PtrToStringAnsi(path), flags, 0); + if (fd == -1) + return -Marshal.GetLastWin32Error(); + + return fd; + } + + static void CloseRestricted(int fd, IntPtr userData) + { + NativeUnsafeMethods.close(fd); + } + + private static readonly IntPtr* s_Interface; + + static LibInputNativeUnsafeMethods() + { + s_Interface = (IntPtr*)Marshal.AllocHGlobal(IntPtr.Size * 2); + + IntPtr Convert(TDelegate del) + { + GCHandle.Alloc(del); + return Marshal.GetFunctionPointerForDelegate(del); + } + + s_Interface[0] = Convert(OpenRestricted); + s_Interface[1] = Convert(CloseRestricted); + } + + private const string LibInput = "libinput.so.10"; + + [DllImport(LibInput)] + public extern static IntPtr libinput_path_create_context(IntPtr* iface, IntPtr userData); + + public static IntPtr libinput_path_create_context() => + libinput_path_create_context(s_Interface, IntPtr.Zero); + + [DllImport(LibInput)] + public extern static IntPtr libinput_path_add_device(IntPtr ctx, [MarshalAs(UnmanagedType.LPStr)] string path); + + [DllImport(LibInput)] + public extern static IntPtr libinput_path_remove_device(IntPtr device); + + [DllImport(LibInput)] + public extern static int libinput_get_fd(IntPtr ctx); + + [DllImport(LibInput)] + public extern static void libinput_dispatch(IntPtr ctx); + + [DllImport(LibInput)] + public extern static IntPtr libinput_get_event(IntPtr ctx); + + [DllImport(LibInput)] + public extern static LibInputEventType libinput_event_get_type(IntPtr ev); + + public enum LibInputEventType + { + LIBINPUT_EVENT_NONE = 0, + LIBINPUT_EVENT_DEVICE_ADDED, + LIBINPUT_EVENT_DEVICE_REMOVED, + LIBINPUT_EVENT_KEYBOARD_KEY = 300, + LIBINPUT_EVENT_POINTER_MOTION = 400, + LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE, + LIBINPUT_EVENT_POINTER_BUTTON, + LIBINPUT_EVENT_POINTER_AXIS, + LIBINPUT_EVENT_TOUCH_DOWN = 500, + LIBINPUT_EVENT_TOUCH_UP, + LIBINPUT_EVENT_TOUCH_MOTION, + LIBINPUT_EVENT_TOUCH_CANCEL, + LIBINPUT_EVENT_TOUCH_FRAME, + LIBINPUT_EVENT_TABLET_TOOL_AXIS = 600, + LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY, + LIBINPUT_EVENT_TABLET_TOOL_TIP, + LIBINPUT_EVENT_TABLET_TOOL_BUTTON, + LIBINPUT_EVENT_TABLET_PAD_BUTTON = 700, + LIBINPUT_EVENT_TABLET_PAD_RING, + LIBINPUT_EVENT_TABLET_PAD_STRIP, + LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN = 800, + LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE, + LIBINPUT_EVENT_GESTURE_SWIPE_END, + LIBINPUT_EVENT_GESTURE_PINCH_BEGIN, + LIBINPUT_EVENT_GESTURE_PINCH_UPDATE, + LIBINPUT_EVENT_GESTURE_PINCH_END, + LIBINPUT_EVENT_SWITCH_TOGGLE = 900, + } + + + [DllImport(LibInput)] + public extern static void libinput_event_destroy(IntPtr ev); + + [DllImport(LibInput)] + public extern static IntPtr libinput_event_get_touch_event(IntPtr ev); + + [DllImport(LibInput)] + public extern static int libinput_event_touch_get_slot(IntPtr ev); + + [DllImport(LibInput)] + public extern static ulong libinput_event_touch_get_time_usec(IntPtr ev); + + [DllImport(LibInput)] + public extern static double libinput_event_touch_get_x_transformed(IntPtr ev, int width); + + [DllImport(LibInput)] + public extern static double libinput_event_touch_get_y_transformed(IntPtr ev, int height); + + + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 396942c8dd..1eea1c07f7 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.LinuxFramebuffer; +using Avalonia.LinuxFramebuffer.Input.LibInput; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -69,7 +70,7 @@ namespace Avalonia.LinuxFramebuffer if (_topLevel == null) { - var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb)); + var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend())); tl.Prepare(); _topLevel = tl; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs deleted file mode 100644 index 2b82b4f4aa..0000000000 --- a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Platform; - -namespace Avalonia.LinuxFramebuffer -{ - unsafe class Mice - { - private readonly FramebufferToplevelImpl _topLevel; - private readonly double _width; - private readonly double _height; - private double _x; - private double _y; - - public event Action Event; - - public Mice(FramebufferToplevelImpl topLevel, double width, double height) - { - _topLevel = topLevel; - _width = width; - _height = height; - } - - public void Start() => ThreadPool.UnsafeQueueUserWorkItem(_ => Worker(), null); - - private void Worker() - { - - var mouseDevices = EvDevDevice.MouseDevices.Where(d => d.IsMouse).ToList(); - if (mouseDevices.Count == 0) - return; - var are = new AutoResetEvent(false); - while (true) - { - try - { - var rfds = new fd_set {count = mouseDevices.Count}; - for (int c = 0; c < mouseDevices.Count; c++) - rfds.fds[c] = mouseDevices[c].Fd; - IntPtr* timeval = stackalloc IntPtr[2]; - timeval[0] = new IntPtr(0); - timeval[1] = new IntPtr(100); - are.WaitOne(30); - foreach (var dev in mouseDevices) - { - while(true) - { - var ev = dev.NextEvent(); - if (!ev.HasValue) - break; - - LinuxFramebufferPlatform.Threading.Send(() => ProcessEvent(dev, ev.Value)); - } - } - } - catch (Exception e) - { - Console.Error.WriteLine(e.ToString()); - } - } - } - - static double TranslateAxis(input_absinfo axis, int value, double max) - { - return (value - axis.minimum) / (double) (axis.maximum - axis.minimum) * max; - } - - private void ProcessEvent(EvDevDevice device, input_event ev) - { - if (ev.type == (short)EvType.EV_REL) - { - if (ev.code == (short) AxisEventCode.REL_X) - _x = Math.Min(_width, Math.Max(0, _x + ev.value)); - else if (ev.code == (short) AxisEventCode.REL_Y) - _y = Math.Min(_height, Math.Max(0, _y + ev.value)); - else - return; - Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, - LinuxFramebufferPlatform.Timestamp, - _topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y), - InputModifiers.None)); - } - if (ev.type ==(int) EvType.EV_ABS) - { - if (ev.code == (short) AbsAxis.ABS_X && device.AbsX.HasValue) - _x = TranslateAxis(device.AbsX.Value, ev.value, _width); - else if (ev.code == (short) AbsAxis.ABS_Y && device.AbsY.HasValue) - _y = TranslateAxis(device.AbsY.Value, ev.value, _height); - else - return; - Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, - LinuxFramebufferPlatform.Timestamp, - _topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y), - InputModifiers.None)); - } - if (ev.type == (short) EvType.EV_KEY) - { - RawPointerEventType? type = null; - if (ev.code == (ushort) EvKey.BTN_LEFT) - type = ev.value == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp; - if (ev.code == (ushort)EvKey.BTN_RIGHT) - type = ev.value == 1 ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp; - if (ev.code == (ushort) EvKey.BTN_MIDDLE) - type = ev.value == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp; - if (!type.HasValue) - return; - - Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, - LinuxFramebufferPlatform.Timestamp, - _topLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers))); - } - } - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs index 5427af7d44..8fbd78588f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs @@ -188,7 +188,7 @@ namespace Avalonia.LinuxFramebuffer unsafe struct fd_set { public int count; - public fixed int fds [256]; + public fixed byte fds [256]; } enum AxisEventCode From e9baedcbfc6443ef5150a657af6b8de2c89ee573 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 23 Jul 2019 00:55:29 +0300 Subject: [PATCH 30/33] Fixed platform threading for fbdev --- .../InternalPlatformThreadingInterface.cs | 85 +++++++------------ .../Remote/PreviewerWindowingPlatform.cs | 2 +- .../FramebufferToplevelImpl.cs | 12 +-- .../Input/LibInput/LibInputBackend.cs | 33 ++++--- .../LinuxFramebufferPlatform.cs | 14 +-- .../NativeUnsafeMethods.cs | 11 +++ 6 files changed, 75 insertions(+), 82 deletions(-) diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index bb357453ff..cb1291410a 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -9,94 +9,69 @@ using Avalonia.Threading; namespace Avalonia.Controls.Platform { - public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer + public class InternalPlatformThreadingInterface : IPlatformThreadingInterface { public InternalPlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer( - DispatcherPriority.Render, - new TimeSpan(0, 0, 0, 0, 66), - () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount))); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); - private readonly AutoResetEvent _queued = new AutoResetEvent(false); - private readonly Queue _actions = new Queue(); public void RunLoop(CancellationToken cancellationToken) { - var handles = new[] {_signaled, _queued}; while (true) { - if (0 == WaitHandle.WaitAny(handles)) - Signaled?.Invoke(null); - else - { - while (true) - { - Action item; - lock (_actions) - if (_actions.Count == 0) - break; - else - item = _actions.Dequeue(); - item(); - } - } + Signaled?.Invoke(null); + _signaled.WaitOne(); } } - public void Send(Action cb) - { - lock (_actions) - { - _actions.Enqueue(cb); - _queued.Set(); - } - } - class WatTimer : IDisposable + class TimerImpl : IDisposable { - private readonly IDisposable _timer; + private readonly DispatcherPriority _priority; + private readonly TimeSpan _interval; + private readonly Action _tick; + private Timer _timer; private GCHandle _handle; - public WatTimer(IDisposable timer) + public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick) { - _timer = timer; + _priority = priority; + _interval = interval; + _tick = tick; + _timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1)); _handle = GCHandle.Alloc(_timer); } + private void OnTimer(object state) + { + if (_timer == null) + return; + Dispatcher.UIThread.Post(() => + { + + if (_timer == null) + return; + _tick(); + _timer?.Change(_interval, TimeSpan.FromMilliseconds(-1)); + }); + } + + public void Dispose() { _handle.Free(); _timer.Dispose(); + _timer = null; } } public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - return new WatTimer(new System.Threading.Timer(delegate - { - var tcs = new TaskCompletionSource(); - Send(() => - { - try - { - tick(); - } - finally - { - tcs.SetResult(0); - } - }); - - - tcs.Task.Wait(); - }, null, TimeSpan.Zero, interval)); - - + return new TimerImpl(priority, interval, tick); } public void Signal(DispatcherPriority prio) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 3b6d071583..a7a94130ea 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(instance) .Bind().ToConstant(threading) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(threading) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 9b9bd6c7b8..3376fa8a32 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -26,7 +26,7 @@ namespace Avalonia.LinuxFramebuffer public IRenderer CreateRenderer(IRenderRoot root) { - return new ImmediateRenderer(root); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); } public void Dispose() @@ -37,14 +37,6 @@ namespace Avalonia.LinuxFramebuffer public void Invalidate(Rect rect) { - if(_renderQueued) - return; - _renderQueued = true; - Dispatcher.UIThread.Post(() => - { - Paint?.Invoke(new Rect(default(Point), ClientSize)); - _renderQueued = false; - }); } public void SetInputRoot(IInputRoot inputRoot) @@ -62,7 +54,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => _fb.PixelSize; - public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice; + public IMouseDevice MouseDevice => new MouseDevice(); public double Scaling => 1; public IEnumerable Surfaces => new object[] {_fb}; public Action Input { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index ca31db6100..0589e30edb 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -40,7 +40,9 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput libinput_path_add_device(ctx, f); while (true) { + IntPtr ev; + libinput_dispatch(ctx); while ((ev = libinput_get_event(ctx)) != IntPtr.Zero) { @@ -50,25 +52,36 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput HandleTouch(ev, type); libinput_event_destroy(ev); + libinput_dispatch(ctx); } - libinput_dispatch(ctx); + + pollfd pfd = new pollfd {fd = fd, events = 1}; + NativeUnsafeMethods.poll(&pfd, new IntPtr(1), 10); } } private void ScheduleInput(RawInputEventArgs ev) { - _inputQueue.Enqueue(ev); - if (_inputQueue.Count == 1) + lock (_inputQueue) { - Dispatcher.UIThread.Post(() => + _inputQueue.Enqueue(ev); + if (_inputQueue.Count == 1) { - while (_inputQueue.Count > 0) + Dispatcher.UIThread.Post(() => { - Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var dequeuedEvent = _inputQueue.Dequeue(); - _onInput?.Invoke(dequeuedEvent); - } - }, DispatcherPriority.Input); + while (true) + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + RawInputEventArgs dequeuedEvent = null; + lock(_inputQueue) + if (_inputQueue.Count != 0) + dequeuedEvent = _inputQueue.Dequeue(); + if (dequeuedEvent == null) + return; + _onInput?.Invoke(dequeuedEvent); + } + }, DispatcherPriority.Input); + } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 1eea1c07f7..85e3f97b74 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -18,8 +18,6 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferPlatform { LinuxFramebuffer _fb; - public static KeyboardDevice KeyboardDevice = new KeyboardDevice(); - public static MouseDevice MouseDevice = new MouseDevice(); private static readonly Stopwatch St = Stopwatch.StartNew(); internal static uint Timestamp => (uint)St.ElapsedTicks; public static InternalPlatformThreadingInterface Threading; @@ -33,13 +31,15 @@ namespace Avalonia.LinuxFramebuffer { Threading = new InternalPlatformThreadingInterface(); AvaloniaLocator.CurrentMutable + .Bind().ToConstant(Threading) + .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() - .Bind().ToConstant(KeyboardDevice) + .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() - .Bind().ToConstant(Threading) .Bind().ToConstant(new RenderLoop()) - .Bind().ToSingleton() - .Bind().ToConstant(Threading); + .Bind().ToSingleton(); + } internal static LinuxFramebufferLifetime Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() @@ -73,7 +73,9 @@ namespace Avalonia.LinuxFramebuffer var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend())); tl.Prepare(); _topLevel = tl; + _topLevel.Renderer.Start(); } + _topLevel.Content = value; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs index 8fbd78588f..18db176bcd 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs @@ -33,6 +33,10 @@ namespace Avalonia.LinuxFramebuffer [DllImport("libc", EntryPoint = "select", SetLastError = true)] public static extern int select(int nfds, void* rfds, void* wfds, void* exfds, IntPtr* timevals); + + [DllImport("libc", EntryPoint = "poll", SetLastError = true)] + public static extern int poll(pollfd* fds, IntPtr nfds, int timeout); + [DllImport("libevdev.so.2", EntryPoint = "libevdev_new_from_fd", SetLastError = true)] public static extern int libevdev_new_from_fd(int fd, out IntPtr dev); @@ -48,6 +52,13 @@ namespace Avalonia.LinuxFramebuffer public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code); } + [StructLayout(LayoutKind.Sequential)] + struct pollfd { + public int fd; /* file descriptor */ + public short events; /* requested events */ + public short revents; /* returned events */ + }; + enum FbIoCtl : uint { FBIOGET_VSCREENINFO = 0x4600, From 6f74b568b23ad604977ccb2c0b662a6123779c90 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Jul 2019 01:01:22 -0800 Subject: [PATCH 31/33] Implemented libdrm backend with OpenGL acceleration support --- samples/ControlCatalog.NetCore/Program.cs | 21 +- src/Avalonia.OpenGL/EglContext.cs | 8 +- src/Avalonia.OpenGL/EglDisplay.cs | 94 +++--- src/Avalonia.OpenGL/EglInterface.cs | 5 + src/Avalonia.OpenGL/GlInterface.cs | 2 +- .../FramebufferToplevelImpl.cs | 18 +- .../LinuxFramebufferPlatform.cs | 29 +- .../Avalonia.LinuxFramebuffer/Output/Drm.cs | 292 ++++++++++++++++++ .../Output/DrmBindings.cs | 158 ++++++++++ .../Output/DrmOutput.cs | 250 +++++++++++++++ .../FbdevOutput.cs} | 11 +- .../Output/IOutputBackend.cs | 7 + 12 files changed, 828 insertions(+), 67 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs rename src/Linux/Avalonia.LinuxFramebuffer/{LinuxFramebuffer.cs => Output/FbdevOutput.cs} (92%) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 40321496c0..de9ca02ed1 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; @@ -29,8 +30,13 @@ namespace ControlCatalog.NetCore var builder = BuildAvaloniaApp(); if (args.Contains("--fbdev")) { - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - return builder.StartLinuxFramebuffer(args); + SilenceConsole(); + return builder.StartLinuxFbDev(args); + } + else if (args.Contains("--drm")) + { + SilenceConsole(); + return builder.StartLinuxDrm(args); } else return builder.StartWithClassicDesktopLifetime(args); @@ -51,11 +57,14 @@ namespace ControlCatalog.NetCore .UseSkia() .UseReactiveUI(); - static void ConsoleSilencer() + static void SilenceConsole() { - Console.CursorVisible = false; - while (true) - Console.ReadKey(true); + new Thread(() => + { + Console.CursorVisible = false; + while (true) + Console.ReadKey(true); + }) {IsBackground = true}.Start(); } } } diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/EglContext.cs index 17caf84179..a39000f198 100644 --- a/src/Avalonia.OpenGL/EglContext.cs +++ b/src/Avalonia.OpenGL/EglContext.cs @@ -10,7 +10,7 @@ namespace Avalonia.OpenGL private readonly EglInterface _egl; private readonly object _lock = new object(); - public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, IntPtr offscreenSurface) + public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface) { _disp = display; _egl = egl; @@ -19,7 +19,7 @@ namespace Avalonia.OpenGL } public IntPtr Context { get; } - public IntPtr OffscreenSurface { get; } + public EglSurface OffscreenSurface { get; } public IGlDisplay Display => _disp; public IDisposable Lock() @@ -36,8 +36,8 @@ namespace Avalonia.OpenGL public void MakeCurrent(EglSurface surface) { - var surf = surface?.DangerousGetHandle() ?? OffscreenSurface; - if (!_egl.MakeCurrent(_disp.Handle, surf, surf, Context)) + var surf = surface ?? OffscreenSurface; + if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index b2b5a1a646..66418c0e15 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -12,49 +12,62 @@ namespace Avalonia.OpenGL private readonly IntPtr _display; private readonly IntPtr _config; private readonly int[] _contextAttributes; + private readonly int _surfaceType; public IntPtr Handle => _display; private AngleOptions.PlatformApi? _angleApi; - public EglDisplay(EglInterface egl) + + public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null) + { + + } + public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) { - _egl = egl; + _egl = egl; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (platformType == -1 && platformDisplay == IntPtr.Zero) { - if (_egl.GetPlatformDisplayEXT == null) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX9}; - - foreach (var platformApi in allowedApis) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] - { - EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE - }); - if (_display != IntPtr.Zero) + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) { - _angleApi = platformApi; - break; + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + + _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] {EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE}); + if (_display != IntPtr.Zero) + { + _angleApi = platformApi; + break; + } } + + if (_display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); } if (_display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); + _display = _egl.GetDisplay(IntPtr.Zero); + } + else + { + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); + _display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); } - if (_display == IntPtr.Zero) - _display = _egl.GetDisplay(IntPtr.Zero); - if (_display == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglGetDisplay", _egl); @@ -85,16 +98,14 @@ namespace Avalonia.OpenGL { if (!_egl.BindApi(cfg.Api)) continue; - + foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT}) foreach(var stencilSize in new[]{8, 1, 0}) foreach (var depthSize in new []{8, 1, 0}) { var attribs = new[] { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - + EGL_SURFACE_TYPE, surfaceType, EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, @@ -108,6 +119,7 @@ namespace Avalonia.OpenGL if (numConfigs == 0) continue; _contextAttributes = cfg.Attributes; + _surfaceType = surfaceType; Type = cfg.Type; } } @@ -126,8 +138,10 @@ namespace Avalonia.OpenGL public GlDisplayType Type { get; } public GlInterface GlInterface { get; } public EglInterface EglInterface => _egl; - public IGlContext CreateContext(IGlContext share) + public EglContext CreateContext(IGlContext share) { + if((_surfaceType|EGL_PBUFFER_BIT) == 0) + throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); var shareCtx = (EglContext)share; var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) @@ -140,7 +154,17 @@ namespace Avalonia.OpenGL }); if (surf == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, ctx, surf); + var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf)); + rv.MakeCurrent(null); + return rv; + } + + public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) + { + var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); + if (ctx == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateContext", _egl); + var rv = new EglContext(this, _egl, ctx, offscreenSurface); rv.MakeCurrent(null); return rv; } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 0a99778ddf..47088972a4 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -10,6 +10,11 @@ namespace Avalonia.OpenGL public EglInterface() : base(Load()) { + } + + public EglInterface(Func getProcAddress) : base(getProcAddress) + { + } public EglInterface(string library) : base(Load(library)) diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index f556949cfa..30f7d67152 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -39,7 +39,7 @@ namespace Avalonia.OpenGL [GlEntryPoint("glClearStencil")] public GlClearStencil ClearStencil { get; } - public delegate void GlClearColor(int r, int g, int b, int a); + public delegate void GlClearColor(float r, float g, float b, float a); [GlEntryPoint("glClearColor")] public GlClearColor ClearColor { get; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 3376fa8a32..5e2ba51caf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.LinuxFramebuffer.Input; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -11,14 +12,14 @@ namespace Avalonia.LinuxFramebuffer { class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider { - private readonly LinuxFramebuffer _fb; + private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; private bool _renderQueued; public IInputRoot InputRoot { get; private set; } - public FramebufferToplevelImpl(LinuxFramebuffer fb, IInputBackend inputBackend) + public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { - _fb = fb; + _outputBackend = outputBackend; _inputBackend = inputBackend; Invalidate(default(Rect)); _inputBackend.Initialize(this, e => Input?.Invoke(e)); @@ -26,7 +27,10 @@ namespace Avalonia.LinuxFramebuffer public IRenderer CreateRenderer(IRenderRoot root) { - return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) + { + + }; } public void Dispose() @@ -53,10 +57,10 @@ namespace Avalonia.LinuxFramebuffer { } - public Size ClientSize => _fb.PixelSize; + public Size ClientSize => ScaledSize; public IMouseDevice MouseDevice => new MouseDevice(); public double Scaling => 1; - public IEnumerable Surfaces => new object[] {_fb}; + public IEnumerable Surfaces => new object[] {_outputBackend}; public Action Input { get; set; } public Action Paint { get; set; } public Action Resized { get; set; } @@ -68,6 +72,6 @@ namespace Avalonia.LinuxFramebuffer remove {} } - public Size ScaledSize => _fb.PixelSize / Scaling; + public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 85e3f97b74..2cc1f65202 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -9,6 +9,8 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.LinuxFramebuffer; using Avalonia.LinuxFramebuffer.Input.LibInput; +using Avalonia.LinuxFramebuffer.Output; +using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -17,19 +19,21 @@ namespace Avalonia.LinuxFramebuffer { class LinuxFramebufferPlatform { - LinuxFramebuffer _fb; + IOutputBackend _fb; private static readonly Stopwatch St = Stopwatch.StartNew(); internal static uint Timestamp => (uint)St.ElapsedTicks; public static InternalPlatformThreadingInterface Threading; - LinuxFramebufferPlatform(string fbdev = null) + LinuxFramebufferPlatform(IOutputBackend backend) { - _fb = new LinuxFramebuffer(fbdev); + _fb = backend; } void Initialize() { Threading = new InternalPlatformThreadingInterface(); + if (_fb is IWindowingPlatformGlFeature glFeature) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(glFeature); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) @@ -42,9 +46,10 @@ namespace Avalonia.LinuxFramebuffer } - internal static LinuxFramebufferLifetime Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() + + internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend) where T : AppBuilderBase, new() { - var platform = new LinuxFramebufferPlatform(fbdev); + var platform = new LinuxFramebufferPlatform(outputBackend); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); return new LinuxFramebufferLifetime(platform._fb); } @@ -52,12 +57,12 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime { - private readonly LinuxFramebuffer _fb; + private readonly IOutputBackend _fb; private TopLevel _topLevel; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public CancellationToken Token => _cts.Token; - public LinuxFramebufferLifetime(LinuxFramebuffer fb) + public LinuxFramebufferLifetime(IOutputBackend fb) { _fb = fb; } @@ -102,10 +107,16 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFramebuffer(this T builder, string[] args, string fbdev = null) + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev)); + + public static int StartLinuxDrm(this T builder, string[] args, string card = null) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card)); + + public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) where T : AppBuilderBase, new() { - var lifetime = LinuxFramebufferPlatform.Initialize(builder, fbdev); + var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); builder.Instance.ApplicationLifetime = lifetime; builder.SetupWithoutStarting(); lifetime.Start(args); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs new file mode 100644 index 0000000000..e266c5ee54 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs @@ -0,0 +1,292 @@ +using System; +using System.Runtime.InteropServices; +// ReSharper disable FieldCanBeMadeReadOnly.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable FieldCanBeMadeReadOnly.Local + +namespace Avalonia.LinuxFramebuffer.Output +{ + public enum DrmModeConnection + { + DRM_MODE_CONNECTED = 1, + DRM_MODE_DISCONNECTED = 2, + DRM_MODE_UNKNOWNCONNECTION = 3 + } + + public enum DrmModeSubPixel{ + DRM_MODE_SUBPIXEL_UNKNOWN = 1, + DRM_MODE_SUBPIXEL_HORIZONTAL_RGB = 2, + DRM_MODE_SUBPIXEL_HORIZONTAL_BGR = 3, + DRM_MODE_SUBPIXEL_VERTICAL_RGB = 4, + DRM_MODE_SUBPIXEL_VERTICAL_BGR = 5, + DRM_MODE_SUBPIXEL_NONE = 6 + } + + static unsafe class LibDrm + { + private const string libdrm = "libdrm.so.2"; + private const string libgbm = "libgbm.so.1"; + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventVBlankHandlerDelegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventPageFlipHandlerDelegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate IntPtr DrmEventPageFlipHandler2Delegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + uint crtc_id, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventSequenceHandlerDelegate(int fd, + ulong sequence, + ulong ns, + ulong user_data); + + [StructLayout(LayoutKind.Sequential)] + public struct DrmEventContext + { + public int version; //4 + public IntPtr vblank_handler; + public IntPtr page_flip_handler; + public IntPtr page_flip_handler2; + public IntPtr sequence_handler; + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeRes { + + public int count_fbs; + public uint *fbs; + + public int count_crtcs; + public uint *crtcs; + + public int count_connectors; + public uint *connectors; + + public int count_encoders; + public uint *encoders; + + uint min_width, max_width; + uint min_height, max_height; + } + + [Flags] + public enum DrmModeType + { + DRM_MODE_TYPE_BUILTIN = (1 << 0), + DRM_MODE_TYPE_CLOCK_C = ((1 << 1) | DRM_MODE_TYPE_BUILTIN), + DRM_MODE_TYPE_CRTC_C = ((1 << 2) | DRM_MODE_TYPE_BUILTIN), + DRM_MODE_TYPE_PREFERRED = (1 << 3), + DRM_MODE_TYPE_DEFAULT = (1 << 4), + DRM_MODE_TYPE_USERDEF = (1 << 5), + DRM_MODE_TYPE_DRIVER = (1 << 6) + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeModeInfo + { + public uint clock; + public ushort hdisplay, hsync_start, hsync_end, htotal, hskew; + public ushort vdisplay, vsync_start, vsync_end, vtotal, vscan; + + public uint vrefresh; + + public uint flags; + public DrmModeType type; + public fixed byte name[32]; + public PixelSize Resolution => new PixelSize(hdisplay, vdisplay); + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeConnector { + public uint connector_id; + public uint encoder_id; /**< Encoder currently connected to */ + public uint connector_type; + public uint connector_type_id; + public DrmModeConnection connection; + public uint mmWidth, mmHeight; /**< HxW in millimeters */ + public DrmModeSubPixel subpixel; + + public int count_modes; + public drmModeModeInfo* modes; + + public int count_props; + public uint *props; /**< List of property ids */ + public ulong *prop_values; /**< List of property values */ + + public int count_encoders; + public uint *encoders; /**< List of encoder ids */ + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeEncoder { + public uint encoder_id; + public uint encoder_type; + public uint crtc_id; + public uint possible_crtcs; + public uint possible_clones; + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeCrtc { + public uint crtc_id; + public uint buffer_id; /**< FB id to connect to 0 = disconnect */ + + public uint x, y; /**< Position on the framebuffer */ + public uint width, height; + public int mode_valid; + public drmModeModeInfo mode; + + public int gamma_size; /**< Number of gamma stops */ + + } + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeRes* drmModeGetResources(int fd); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeResources(drmModeRes* res); + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeConnector* drmModeGetConnector(int fd, uint connector); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeConnector(drmModeConnector* res); + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeEncoder* drmModeGetEncoder(int fd, uint id); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeEncoder(drmModeEncoder* enc); + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeCrtc* drmModeGetCrtc(int fd, uint id); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeCrtc(drmModeCrtc* enc); + + [DllImport(libdrm, SetLastError = true)] + public static extern int drmModeAddFB(int fd, uint width, uint height, byte depth, + byte bpp, uint pitch, uint bo_handle, + out uint buf_id); + [DllImport(libdrm, SetLastError = true)] + public static extern int drmModeSetCrtc(int fd, uint crtcId, uint bufferId, + uint x, uint y, uint *connectors, int count, + drmModeModeInfo* mode); + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeRmFB(int fd, int id); + + [Flags] + public enum DrmModePageFlip + { + Event = 1, + Async = 2, + Absolute = 4, + Relative = 8, + } + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModePageFlip(int fd, uint crtc_id, uint fb_id, + DrmModePageFlip flags, void *user_data); + + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmHandleEvent(int fd, DrmEventContext* context); + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_create_device(int fd); + + + [Flags] + public enum GbmBoFlags { + /** + * Buffer is going to be presented to the screen using an API such as KMS + */ + GBM_BO_USE_SCANOUT = (1 << 0), + /** + * Buffer is going to be used as cursor + */ + GBM_BO_USE_CURSOR = (1 << 1), + /** + * Deprecated + */ + GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR, + /** + * Buffer is to be used for rendering - for example it is going to be used + * as the storage for a color buffer + */ + GBM_BO_USE_RENDERING = (1 << 2), + /** + * Buffer can be used for gbm_bo_write. This is guaranteed to work + * with GBM_BO_USE_CURSOR, but may not work for other combinations. + */ + GBM_BO_USE_WRITE = (1 << 3), + /** + * Buffer is linear, i.e. not tiled. + */ + GBM_BO_USE_LINEAR = (1 << 4), + }; + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_surface_create(IntPtr device, int width, int height, uint format, GbmBoFlags flags); + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_surface_lock_front_buffer(IntPtr surface); + [DllImport(libgbm, SetLastError = true)] + public static extern int gbm_surface_release_buffer(IntPtr surface, IntPtr bo); + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_bo_get_user_data(IntPtr surface); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void GbmBoUserDataDestroyCallbackDelegate(IntPtr bo, IntPtr data); + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_bo_set_user_data(IntPtr bo, IntPtr userData, + GbmBoUserDataDestroyCallbackDelegate onFree); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_width(IntPtr bo); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_height(IntPtr bo); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_stride(IntPtr bo); + + + [StructLayout(LayoutKind.Explicit)] + public struct GbmBoHandle + { + [FieldOffset(0)] + public void *ptr; + [FieldOffset(0)] + public int s32; + [FieldOffset(0)] + public uint u32; + [FieldOffset(0)] + public long s64; + [FieldOffset(0)] + public ulong u64; + } + + [DllImport(libgbm, SetLastError = true)] + public static extern ulong gbm_bo_get_handle(IntPtr bo); + + public static class GbmColorFormats + { + public static uint FourCC(char a, char b, char c, char d) => + (uint)a | ((uint)b) << 8 | ((uint)c) << 16 | ((uint)d) << 24; + + public static uint GBM_FORMAT_XRGB8888 { get; } = FourCC('X', 'R', '2', '4'); + } + } + +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs new file mode 100644 index 0000000000..b5ebc4bcb7 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; +using static Avalonia.LinuxFramebuffer.Output.LibDrm; + +namespace Avalonia.LinuxFramebuffer.Output +{ + public unsafe class DrmConnector + { + private static string[] KnownConnectorTypes = + { + "None", "VGA", "DVI-I", "DVI-D", "DVI-A", "Composite", "S-Video", "LVDS", "Component", "DIN", + "DisplayPort", "HDMI-A", "HDMI-B", "TV", "eDP", "Virtual", "DSI" + }; + + public DrmModeConnection Connection { get; } + public uint Id { get; } + public string Name { get; } + public Size SizeMm { get; } + public DrmModeSubPixel SubPixel { get; } + internal uint EncoderId { get; } + internal List EncoderIds { get; } = new List(); + public List Modes { get; } = new List(); + internal DrmConnector(drmModeConnector* conn) + { + Connection = conn->connection; + Id = conn->connector_id; + SizeMm = new Size(conn->mmWidth, conn->mmHeight); + SubPixel = conn->subpixel; + for (var c = 0; c < conn->count_encoders;c++) + EncoderIds.Add(conn->encoders[c]); + EncoderId = conn->encoder_id; + for(var c=0; ccount_modes; c++) + Modes.Add(new DrmModeInfo(ref conn->modes[c])); + + if (conn->connector_type > KnownConnectorTypes.Length - 1) + Name = $"Unknown({conn->connector_type})-{conn->connector_type_id}"; + else + Name = KnownConnectorTypes[conn->connector_type] + "-" + conn->connector_type_id; + } + } + + public unsafe class DrmModeInfo + { + internal drmModeModeInfo Mode; + + internal DrmModeInfo(ref drmModeModeInfo info) + { + Mode = info; + fixed (void* pName = info.name) + Name = Marshal.PtrToStringAnsi(new IntPtr(pName)); + } + + public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay); + public bool IsPreferred => Mode.type.HasFlag(DrmModeType.DRM_MODE_TYPE_PREFERRED); + + public string Name { get; } + } + + unsafe class DrmEncoder + { + public drmModeEncoder Encoder { get; } + public List PossibleCrtcs { get; } = new List(); + + public DrmEncoder(drmModeEncoder encoder, drmModeCrtc[] crtcs) + { + Encoder = encoder; + for (var c = 0; c < crtcs.Length; c++) + { + var bit = 1 << c; + if ((encoder.possible_crtcs & bit) != 0) + PossibleCrtcs.Add(crtcs[c]); + } + } + } + + + + public unsafe class DrmResources + { + public List Connectors { get; }= new List(); + internal Dictionary Encoders { get; } = new Dictionary(); + public DrmResources(int fd) + { + var res = drmModeGetResources(fd); + if (res == null) + throw new Win32Exception("drmModeGetResources failed"); + + var crtcs = new drmModeCrtc[res->count_crtcs]; + for (var c = 0; c < res->count_crtcs; c++) + { + var crtc = drmModeGetCrtc(fd, res->crtcs[c]); + crtcs[c] = *crtc; + drmModeFreeCrtc(crtc); + } + + for (var c = 0; c < res->count_encoders; c++) + { + var enc = drmModeGetEncoder(fd, res->encoders[c]); + Encoders[res->encoders[c]] = new DrmEncoder(*enc, crtcs); + drmModeFreeEncoder(enc); + } + + for (var c = 0; c < res->count_connectors; c++) + { + var conn = drmModeGetConnector(fd, res->connectors[c]); + Connectors.Add(new DrmConnector(conn)); + drmModeFreeConnector(conn); + } + + + } + + public void Dump() + { + void Print(int off, string s) + { + for (var c = 0; c < off; c++) + Console.Write(" "); + Console.WriteLine(s); + } + Print(0, "Connectors"); + foreach (var conn in Connectors) + { + Print(1, $"{conn.Name}:"); + Print(2, $"Id: {conn.Id}"); + Print(2, $"Size: {conn.SizeMm} mm"); + Print(2, $"Encoder id: {conn.EncoderId}"); + Print(2, "Modes"); + foreach (var m in conn.Modes) + Print(3, $"{m.Name} {(m.IsPreferred ? "PREFERRED" : "")}"); + + + } + } + } + + public unsafe class DrmCard : IDisposable + { + public int Fd { get; private set; } + public DrmCard(string path = null) + { + path = path ?? "/dev/dri/card0"; + Fd = open(path, 2, 0); + if (Fd == -1) + throw new Win32Exception("Couldn't open " + path); + } + + public DrmResources GetResources() => new DrmResources(Fd); + public void Dispose() + { + close(Fd); + Fd = -1; + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs new file mode 100644 index 0000000000..6a76977352 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.Platform.Interop; +using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; +using static Avalonia.LinuxFramebuffer.Output.LibDrm; +namespace Avalonia.LinuxFramebuffer.Output +{ + public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature + { + private DrmCard _card; + private readonly EglGlPlatformSurface _eglPlatformSurface; + public PixelSize PixelSize => _mode.Resolution; + + public DrmOutput(string path = null) + { + var card = new DrmCard(path); + + var resources = card.GetResources(); + + + var connector = + resources.Connectors.FirstOrDefault(x => x.Connection == DrmModeConnection.DRM_MODE_CONNECTED); + if(connector == null) + throw new InvalidOperationException("Unable to find connected DRM connector"); + + var mode = connector.Modes.OrderByDescending(x => x.IsPreferred) + .ThenByDescending(x => x.Resolution.Width * x.Resolution.Height) + //.OrderByDescending(x => x.Resolution.Width * x.Resolution.Height) + .FirstOrDefault(); + if(mode == null) + throw new InvalidOperationException("Unable to find a usable DRM mode"); + Init(card, resources, connector, mode); + } + + public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo) + { + Init(card, resources, connector, modeInfo); + } + + [DllImport("libEGL.so.1")] + static extern IntPtr eglGetProcAddress(Utf8Buffer proc); + + private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate; + private drmModeModeInfo _mode; + private EglDisplay _eglDisplay; + private EglSurface _eglSurface; + private EglContext _immediateContext; + private EglContext _deferredContext; + private IntPtr _currentBo; + private IntPtr _gbmTargetSurface; + private uint _crtcId; + + void FbDestroyCallback(IntPtr bo, IntPtr userData) + { + drmModeRmFB(_card.Fd, userData.ToInt32()); + } + + uint GetFbIdForBo(IntPtr bo) + { + if (bo == IntPtr.Zero) + throw new ArgumentException("bo is 0"); + var data = gbm_bo_get_user_data(bo); + if (data != IntPtr.Zero) + return (uint)data.ToInt32(); + + var w = gbm_bo_get_width(bo); + var h = gbm_bo_get_height(bo); + var stride = gbm_bo_get_stride(bo); + var handle = gbm_bo_get_handle(bo); + + var ret = drmModeAddFB(_card.Fd, w, h, 24, 32, stride, (uint)handle, out var fbHandle); + if (ret != 0) + throw new Win32Exception(ret, "drmModeAddFb failed"); + + gbm_bo_set_user_data(bo, new IntPtr((int)fbHandle), FbDestroyDelegate); + + + return fbHandle; + } + + + void Init(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo) + { + FbDestroyDelegate = FbDestroyCallback; + _card = card; + uint GetCrtc() + { + if (resources.Encoders.TryGetValue(connector.EncoderId, out var encoder)) + { + // Not sure why that should work + return encoder.Encoder.crtc_id; + } + else + { + foreach (var encId in connector.EncoderIds) + { + if (resources.Encoders.TryGetValue(encId, out encoder) + && encoder.PossibleCrtcs.Count>0) + return encoder.PossibleCrtcs.First().crtc_id; + } + + throw new InvalidOperationException("Unable to find CRTC matching the desired mode"); + } + } + + _crtcId = GetCrtc(); + var device = gbm_create_device(card.Fd); + _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height, + GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING); + if(_gbmTargetSurface == null) + throw new InvalidOperationException("Unable to create GBM surface"); + + + + _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null); + _eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface); + + + EglContext CreateContext(EglContext share) + { + var offSurf = gbm_surface_create(device, 1, 1, GbmColorFormats.GBM_FORMAT_XRGB8888, + GbmBoFlags.GBM_BO_USE_RENDERING); + if (offSurf == null) + throw new InvalidOperationException("Unable to create 1x1 sized GBM surface"); + return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf)); + } + + _immediateContext = CreateContext(null); + _deferredContext = CreateContext(_immediateContext); + + _immediateContext.MakeCurrent(_eglSurface); + _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); + _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + _eglSurface.SwapBuffers(); + var bo = gbm_surface_lock_front_buffer(_gbmTargetSurface); + var fbId = GetFbIdForBo(bo); + var connectorId = connector.Id; + var mode = modeInfo.Mode; + + + var res = drmModeSetCrtc(_card.Fd, _crtcId, fbId, 0, 0, &connectorId, 1, &mode); + if (res != 0) + throw new Win32Exception(res, "drmModeSetCrtc failed"); + + _mode = mode; + _currentBo = bo; + + // Go trough two cycles of buffer swapping (there are render artifacts otherwise) + for(var c=0;c<2;c++) + using (CreateGlRenderTarget().BeginDraw()) + { + _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); + _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + } + } + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + { + return new RenderTarget(this); + } + + class RenderTarget : IGlPlatformSurfaceRenderTarget + { + private readonly DrmOutput _parent; + + public RenderTarget(DrmOutput parent) + { + _parent = parent; + } + public void Dispose() + { + // We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim + } + + class RenderSession : IGlPlatformSurfaceRenderingSession + { + private readonly DrmOutput _parent; + + public RenderSession(DrmOutput parent) + { + _parent = parent; + } + + public void Dispose() + { + _parent._eglDisplay.GlInterface.Flush(); + _parent._eglSurface.SwapBuffers(); + + var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface); + if (nextBo == IntPtr.Zero) + { + // Not sure what else can be done + Console.WriteLine("gbm_surface_lock_front_buffer failed"); + } + else + { + + var fb = _parent.GetFbIdForBo(nextBo); + bool waitingForFlip = true; + + drmModePageFlip(_parent._card.Fd, _parent._crtcId, fb, DrmModePageFlip.Event, null); + + DrmEventPageFlipHandlerDelegate flipCb = + (int fd, uint sequence, uint tv_sec, uint tv_usec, void* user_data) => + { + waitingForFlip = false; + }; + var cbHandle = GCHandle.Alloc(flipCb); + var ctx = new DrmEventContext + { + version = 4, page_flip_handler = Marshal.GetFunctionPointerForDelegate(flipCb) + }; + while (waitingForFlip) + { + var pfd = new pollfd {events = 1, fd = _parent._card.Fd}; + poll(&pfd, new IntPtr(1), -1); + drmHandleEvent(_parent._card.Fd, &ctx); + } + + cbHandle.Free(); + gbm_surface_release_buffer(_parent._gbmTargetSurface, _parent._currentBo); + _parent._currentBo = nextBo; + } + _parent._eglDisplay.ClearContext(); + } + + + public IGlDisplay Display => _parent._eglDisplay; + + public PixelSize Size => _parent._mode.Resolution; + + public double Scaling => 1; + } + + public IGlPlatformSurfaceRenderingSession BeginDraw() + { + _parent._deferredContext.MakeCurrent(_parent._eglSurface); + return new RenderSession(_parent); + } + } + + IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext; + } + + +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs similarity index 92% rename from src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs rename to src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 1e25bd4a8a..3021c29015 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -2,11 +2,12 @@ using System.Runtime.InteropServices; using System.Text; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer { - public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable + public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend { private readonly Vector _dpi; private int _fd; @@ -15,7 +16,7 @@ namespace Avalonia.LinuxFramebuffer private IntPtr _mappedLength; private IntPtr _mappedAddress; - public LinuxFramebuffer(string fileName = null, Vector? dpi = null) + public FbdevOutput(string fileName = null, Vector? dpi = null) { _dpi = dpi ?? new Vector(96, 96); fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; @@ -85,14 +86,14 @@ namespace Avalonia.LinuxFramebuffer public string Id { get; private set; } - public Size PixelSize + public PixelSize PixelSize { get { fb_var_screeninfo nfo; if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, &nfo)) throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); - return new Size(nfo.xres, nfo.yres); + return new PixelSize((int)nfo.xres, (int)nfo.yres); } } @@ -123,7 +124,7 @@ namespace Avalonia.LinuxFramebuffer GC.SuppressFinalize(this); } - ~LinuxFramebuffer() + ~FbdevOutput() { ReleaseUnmanagedResources(); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs new file mode 100644 index 0000000000..01690f07ac --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -0,0 +1,7 @@ +namespace Avalonia.LinuxFramebuffer.Output +{ + public interface IOutputBackend + { + PixelSize PixelSize { get; } + } +} From 567defb825167d29e2bcd5cd0d9be9a3b4ab6a3c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Jul 2019 01:39:57 -0800 Subject: [PATCH 32/33] Basic support for absolute pointing libinput devices --- .../Input/LibInput/LibInputBackend.cs | 45 +++++++++++++++++++ .../LibInput/LibInputNativeUnsafeMethods.cs | 21 ++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 0589e30edb..723028c666 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -15,6 +15,8 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput private IInputRoot _inputRoot; private readonly Queue _inputThreadActions = new Queue(); private TouchDevice _touch = new TouchDevice(); + private MouseDevice _mouse = new MouseDevice(); + private Point _mousePosition; private readonly Queue _inputQueue = new Queue(); private Action _onInput; @@ -50,6 +52,10 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput if (type >= LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN && type <= LibInputEventType.LIBINPUT_EVENT_TOUCH_CANCEL) HandleTouch(ev, type); + + if (type >= LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION + && type <= LibInputEventType.LIBINPUT_EVENT_POINTER_AXIS) + HandlePointer(ev, type); libinput_event_destroy(ev); libinput_dispatch(ctx); @@ -123,6 +129,45 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput } } + private void HandlePointer(IntPtr ev, LibInputEventType type) + { + //TODO: support input modifiers + var pev = libinput_event_get_pointer_event(ev); + var info = _screen.ScaledSize; + var ts = libinput_event_pointer_get_time_usec(pev) / 1000; + if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE) + { + _mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width), + libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height)); + ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition, + InputModifiers.None)); + } + else if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON) + { + var button = (EvKey)libinput_event_pointer_get_button(pev); + var buttonState = libinput_event_pointer_get_button_state(pev); + + + var evnt = button == EvKey.BTN_LEFT ? + (buttonState == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) : + button == EvKey.BTN_MIDDLE ? + (buttonState == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) : + button == EvKey.BTN_RIGHT ? + (buttonState == 1 ? + RawPointerEventType.RightButtonDown : + RawPointerEventType.RightButtonUp) : + (RawPointerEventType)(-1); + if (evnt == (RawPointerEventType)(-1)) + return; + + + ScheduleInput( + new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, InputModifiers.None)); + } + + } + + public void Initialize(IScreenInfoProvider screen, Action onInput) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs index 6c82f53071..0492090461 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs @@ -116,7 +116,24 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput [DllImport(LibInput)] public extern static double libinput_event_touch_get_y_transformed(IntPtr ev, int height); - - + + [DllImport(LibInput)] + public extern static IntPtr libinput_event_get_pointer_event(IntPtr ev); + + + [DllImport(LibInput)] + public extern static ulong libinput_event_pointer_get_time_usec(IntPtr ev); + + [DllImport(LibInput)] + public extern static double libinput_event_pointer_get_absolute_x_transformed(IntPtr ev, int width); + + [DllImport(LibInput)] + public extern static double libinput_event_pointer_get_absolute_y_transformed(IntPtr ev, int height); + + [DllImport(LibInput)] + public extern static int libinput_event_pointer_get_button(IntPtr ev); + + [DllImport(LibInput)] + public extern static int libinput_event_pointer_get_button_state(IntPtr ev); } } From 53d060d707f3e66d881ea9fa34fc4082e235a0f8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Jul 2019 13:06:33 +0300 Subject: [PATCH 33/33] Bump skia linux binaries version --- build/SkiaSharp.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index cf8e0fd13a..c03ad0fefd 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - +