diff --git a/Avalonia.sln b/Avalonia.sln index ac678ba9ba..568a16ce0e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -197,7 +197,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1842,6 +1846,54 @@ Global {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.Build.0 = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.ActiveCfg = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props index 8a5c18e1ae..67947296b3 100644 --- a/build/AndroidWorkarounds.props +++ b/build/AndroidWorkarounds.props @@ -5,4 +5,12 @@ + + + + + + + false + diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d989e643b8..3923bdeeda 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -13,6 +13,7 @@ + diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 6a40f7187d..7919c3ac5a 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 09d2612ac3..5aef0b5520 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -8,6 +8,9 @@ using Avalonia.Controls; using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; +using Avalonia.Dialogs; +using System.Collections.Generic; +using System.Threading.Tasks; namespace ControlCatalog.NetCore { @@ -51,21 +54,22 @@ namespace ControlCatalog.NetCore else return builder.StartWithClassicDesktopLifetime(args); } - + /// /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() - .With(new X11PlatformOptions {EnableMultiTouch = true}) + .With(new X11PlatformOptions { EnableMultiTouch = true }) .With(new Win32PlatformOptions { EnableMultitouch = true, AllowEglInitialization = true }) .UseSkia() - .UseReactiveUI(); + .UseReactiveUI() + .UseManagedSystemDialogs(); static void SilenceConsole() { @@ -74,7 +78,8 @@ namespace ControlCatalog.NetCore Console.CursorVisible = false; while (true) Console.ReadKey(true); - }) {IsBackground = true}.Start(); + }) + { IsBackground = true }.Start(); } } } diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 91d9f034a5..95c65ed92f 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -6,6 +6,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Threading; using ControlCatalog.ViewModels; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace ControlCatalog diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 4d4a561b08..3c8d4ca7e6 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -55,15 +55,15 @@ namespace Avalonia.Collections /// public class AvaloniaList : IAvaloniaList, IList, INotifyCollectionChangedDebug { - private List _inner; + private readonly List _inner; private NotifyCollectionChangedEventHandler _collectionChanged; /// /// Initializes a new instance of the class. /// public AvaloniaList() - : this(Enumerable.Empty()) { + _inner = new List(); } /// @@ -89,8 +89,8 @@ namespace Avalonia.Collections /// public event NotifyCollectionChangedEventHandler CollectionChanged { - add { _collectionChanged += value; } - remove { _collectionChanged -= value; } + add => _collectionChanged += value; + remove => _collectionChanged -= value; } /// @@ -150,7 +150,7 @@ namespace Avalonia.Collections T old = _inner[index]; - if (!object.Equals(old, value)) + if (!EqualityComparer.Default.Equals(old, value)) { _inner[index] = value; @@ -187,45 +187,38 @@ namespace Avalonia.Collections Validate?.Invoke(item); int index = _inner.Count; _inner.Add(item); - NotifyAdd(new[] { item }, index); + NotifyAdd(item, index); } /// /// Adds multiple items to the collection. /// /// The items. - public virtual void AddRange(IEnumerable items) - { - Contract.Requires(items != null); - - var list = (items as IList) ?? items.ToList(); - - if (list.Count > 0) - { - if (Validate != null) - { - foreach (var item in list) - { - Validate((T)item); - } - } - - int index = _inner.Count; - _inner.AddRange(items); - NotifyAdd(list, index); - } - } + public virtual void AddRange(IEnumerable items) => InsertRange(_inner.Count, items); /// /// Removes all items from the collection. /// public virtual void Clear() { - if (this.Count > 0) + if (Count > 0) { - var old = _inner; - _inner = new List(); - NotifyReset(old); + if (_collectionChanged != null) + { + var e = ResetBehavior == ResetBehavior.Reset ? + EventArgsCache.ResetCollectionChanged : + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, _inner.ToList(), 0); + + _inner.Clear(); + + _collectionChanged(this, e); + } + else + { + _inner.Clear(); + } + + NotifyCountChanged(); } } @@ -253,9 +246,20 @@ namespace Avalonia.Collections /// Returns an enumerator that enumerates the items in the collection. /// /// An . - public IEnumerator GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() { - return _inner.GetEnumerator(); + return new Enumerator(_inner); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(_inner); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_inner); } /// @@ -289,7 +293,7 @@ namespace Avalonia.Collections { Validate?.Invoke(item); _inner.Insert(index, item); - NotifyAdd(new[] { item }, index); + NotifyAdd(item, index); } /// @@ -301,20 +305,83 @@ namespace Avalonia.Collections { Contract.Requires(items != null); - var list = (items as IList) ?? items.ToList(); + bool willRaiseCollectionChanged = _collectionChanged != null; + bool hasValidation = Validate != null; - if (list.Count > 0) + if (items is IList list) { - if (Validate != null) + if (list.Count > 0) { - foreach (var item in list) + if (list is ICollection collection) { - Validate((T)item); + if (hasValidation) + { + foreach (T item in collection) + { + Validate(item); + } + } + + _inner.InsertRange(index, collection); + NotifyAdd(list, index); + } + else + { + using (IEnumerator en = items.GetEnumerator()) + { + int insertIndex = index; + + while (en.MoveNext()) + { + T item = en.Current; + + if (hasValidation) + { + Validate(item); + } + + _inner.Insert(insertIndex++, item); + } + } + + NotifyAdd(list, index); } } + } + else + { + using (IEnumerator en = items.GetEnumerator()) + { + if (en.MoveNext()) + { + // Avoid allocating list for collection notification if there is no event subscriptions. + List notificationItems = willRaiseCollectionChanged ? + new List() : + null; + + int insertIndex = index; + + do + { + T item = en.Current; + + if (hasValidation) + { + Validate(item); + } - _inner.InsertRange(index, items); - NotifyAdd((items as IList) ?? items.ToList(), index); + _inner.Insert(insertIndex++, item); + + if (willRaiseCollectionChanged) + { + notificationItems.Add(item); + } + + } while (en.MoveNext()); + + NotifyAdd(notificationItems, index); + } + } } } @@ -382,7 +449,7 @@ namespace Avalonia.Collections if (index != -1) { _inner.RemoveAt(index); - NotifyRemove(new[] { item }, index); + NotifyRemove(item , index); return true; } @@ -412,7 +479,7 @@ namespace Avalonia.Collections { T item = _inner[index]; _inner.RemoveAt(index); - NotifyRemove(new[] { item }, index); + NotifyRemove(item , index); } /// @@ -480,12 +547,6 @@ namespace Avalonia.Collections _inner.CopyTo((T[])array, index); } - /// - IEnumerator IEnumerable.GetEnumerator() - { - return _inner.GetEnumerator(); - } - /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); @@ -505,13 +566,29 @@ namespace Avalonia.Collections NotifyCountChanged(); } + /// + /// Raises the event with a add action. + /// + /// The item that was added. + /// The starting index. + private void NotifyAdd(T item, int index) + { + if (_collectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new[] { item }, index); + _collectionChanged(this, e); + } + + NotifyCountChanged(); + } + /// /// Raises the event when the property /// changes. /// private void NotifyCountChanged() { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, EventArgsCache.CountPropertyChanged); } /// @@ -531,23 +608,57 @@ namespace Avalonia.Collections } /// - /// Raises the event with a reset action. + /// Raises the event with a remove action. /// - /// The items that were removed. - private void NotifyReset(IList t) + /// The item that was removed. + /// The starting index. + private void NotifyRemove(T item, int index) { if (_collectionChanged != null) { - NotifyCollectionChangedEventArgs e; - - e = ResetBehavior == ResetBehavior.Reset ? - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) : - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, t, 0); - + var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { item }, index); _collectionChanged(this, e); } NotifyCountChanged(); } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator + { + private List.Enumerator _innerEnumerator; + + public Enumerator(List inner) + { + _innerEnumerator = inner.GetEnumerator(); + } + + public bool MoveNext() + { + return _innerEnumerator.MoveNext(); + } + + void IEnumerator.Reset() + { + ((IEnumerator)_innerEnumerator).Reset(); + } + + public T Current => _innerEnumerator.Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _innerEnumerator.Dispose(); + } + } + } + + internal static class EventArgsCache + { + internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs(nameof(AvaloniaList.Count)); + internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); } } diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 28c0dce518..0a33eeb2c1 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Data.Core { try { - if (Target.IsAlive && Target.Target is IAvaloniaObject obj) + if (Target.TryGetTarget(out object target) && target is IAvaloniaObject obj) { obj.SetValue(_property, value, priority); return true; @@ -37,9 +37,9 @@ namespace Avalonia.Data.Core } } - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - if (reference.Target is IAvaloniaObject obj) + if (reference.TryGetTarget(out object target) && target is IAvaloniaObject obj) { _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 8a2dd46b86..ce40b3e517 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -8,27 +8,27 @@ namespace Avalonia.Data.Core public abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); - protected static readonly WeakReference UnsetReference = - new WeakReference(AvaloniaProperty.UnsetValue); + protected static readonly WeakReference UnsetReference = + new WeakReference(AvaloniaProperty.UnsetValue); - private WeakReference _target = UnsetReference; + private WeakReference _target = UnsetReference; private Action _subscriber; private bool _listening; - protected WeakReference LastValue { get; private set; } + protected WeakReference LastValue { get; private set; } public abstract string Description { get; } public ExpressionNode Next { get; set; } - public WeakReference Target + public WeakReference Target { get { return _target; } set { Contract.Requires(value != null); - var oldTarget = _target?.Target; - var newTarget = value.Target; + _target.TryGetTarget(out var oldTarget); + value.TryGetTarget(out object newTarget); if (!ReferenceEquals(oldTarget, newTarget)) { @@ -72,9 +72,11 @@ namespace Avalonia.Data.Core _subscriber = null; } - protected virtual void StartListeningCore(WeakReference reference) + protected virtual void StartListeningCore(WeakReference reference) { - ValueChanged(reference.Target); + reference.TryGetTarget(out object target); + + ValueChanged(target); } protected virtual void StopListeningCore() @@ -96,7 +98,7 @@ namespace Avalonia.Data.Core if (notification == null) { - LastValue = new WeakReference(value); + LastValue = new WeakReference(value); if (Next != null) { @@ -109,7 +111,7 @@ namespace Avalonia.Data.Core } else { - LastValue = new WeakReference(notification.Value); + LastValue = new WeakReference(notification.Value); if (Next != null) { @@ -125,7 +127,7 @@ namespace Avalonia.Data.Core private void StartListening() { - var target = _target.Target; + _target.TryGetTarget(out object target); if (target == null) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 65f26df011..7060fd3451 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -78,7 +78,7 @@ namespace Avalonia.Data.Core _node = node; Description = description; - _root = new WeakReference(root); + _root = new WeakReference(root); } /// @@ -120,7 +120,7 @@ namespace Avalonia.Data.Core Contract.Requires(update != null); Description = description; _node = node; - _node.Target = new WeakReference(rootGetter()); + _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } @@ -285,13 +285,13 @@ namespace Avalonia.Data.Core if (_root is IObservable observable) { _rootSubscription = observable.Subscribe( - x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), x => PublishCompleted(), () => PublishCompleted()); } else { - _node.Target = (WeakReference)_root; + _node.Target = (WeakReference)_root; } } diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs index 4206a99e3d..a3852cc371 100644 --- a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -36,7 +36,9 @@ namespace Avalonia.Data.Core { try { - _setDelegate.DynamicInvoke(Target.Target, value); + Target.TryGetTarget(out object target); + + _setDelegate.DynamicInvoke(target, value); return true; } catch (Exception) @@ -64,6 +66,11 @@ namespace Avalonia.Data.Core return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName; } - protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + protected override int? TryGetFirstArgumentAsInt() + { + Target.TryGetTarget(out object target); + + return _firstArgumentDelegate.DynamicInvoke(target) as int?; + } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 5e09bbcc2f..47d5147ac2 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -13,9 +13,10 @@ namespace Avalonia.Data.Core { private IDisposable _subscription; - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - var target = reference.Target; + reference.TryGetTarget(out object target); + var incc = target as INotifyCollectionChanged; var inpc = target as INotifyPropertyChanged; var inputs = new List>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 8d2ed905ee..ab4a109cc2 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -31,12 +31,12 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start(WeakReference reference, string propertyName) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - var instance = reference.Target; + reference.TryGetTarget(out object instance); var o = (AvaloniaObject)instance; var p = LookupProperty(o, propertyName); diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index 33c40abea8..f5b545d2ff 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -15,9 +15,11 @@ namespace Avalonia.Data.Core.Plugins public class DataAnnotationsValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference, string memberName) + public bool Match(WeakReference reference, string memberName) { - return reference.Target? + reference.TryGetTarget(out object target); + + return target? .GetType() .GetRuntimeProperty(memberName)? .GetCustomAttributes() @@ -25,25 +27,22 @@ namespace Avalonia.Data.Core.Plugins } /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) { return new Accessor(reference, name, inner); } - private class Accessor : DataValidationBase + private sealed class Accessor : DataValidationBase { - private ValidationContext _context; + private readonly ValidationContext _context; - public Accessor(WeakReference reference, string name, IPropertyAccessor inner) + public Accessor(WeakReference reference, string name, IPropertyAccessor inner) : base(inner) { - _context = new ValidationContext(reference.Target); - _context.MemberName = name; - } + reference.TryGetTarget(out object target); - public override bool SetValue(object value, BindingPriority priority) - { - return base.SetValue(value, priority); + _context = new ValidationContext(target); + _context.MemberName = name; } protected override void InnerValueChanged(object value) diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index eabfa31d4b..f305912fe1 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -12,17 +12,17 @@ namespace Avalonia.Data.Core.Plugins public class ExceptionValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference, string memberName) => true; + public bool Match(WeakReference reference, string memberName) => true; /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) { return new Validator(reference, name, inner); } - private class Validator : DataValidationBase + private sealed class Validator : DataValidationBase { - public Validator(WeakReference reference, string name, IPropertyAccessor inner) + public Validator(WeakReference reference, string name, IPropertyAccessor inner) : base(inner) { } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 2c3a9a53b4..5b1af22f14 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -16,7 +16,7 @@ namespace Avalonia.Data.Core.Plugins /// A weak reference to the object. /// The name of the member to validate. /// True if the plugin can handle the object; otherwise false. - bool Match(WeakReference reference, string memberName); + bool Match(WeakReference reference, string memberName); /// /// Starts monitoring the data validation state of a property on an object. @@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - IPropertyAccessor Start( - WeakReference reference, + IPropertyAccessor Start(WeakReference reference, string propertyName, IPropertyAccessor inner); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs index 539f518083..a0021fa4d4 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs @@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - IPropertyAccessor Start( - WeakReference reference, + IPropertyAccessor Start(WeakReference reference, string propertyName); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs index b80d9d75c8..3df578d25b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs @@ -15,7 +15,7 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - bool Match(WeakReference reference); + bool Match(WeakReference reference); /// /// Starts producing output based on the specified value. @@ -24,6 +24,6 @@ namespace Avalonia.Data.Core.Plugins /// /// An observable that produces the output for the value. /// - IObservable Start(WeakReference reference); + IObservable Start(WeakReference reference); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 7abbcab245..b353d03bcd 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -15,20 +15,25 @@ namespace Avalonia.Data.Core.Plugins public class IndeiValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo; + public bool Match(WeakReference reference, string memberName) + { + reference.TryGetTarget(out object target); + + return target is INotifyDataErrorInfo; + } /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor) { return new Validator(reference, name, accessor); } private class Validator : DataValidationBase, IWeakSubscriber { - WeakReference _reference; - string _name; + private readonly WeakReference _reference; + private readonly string _name; - public Validator(WeakReference reference, string name, IPropertyAccessor inner) + public Validator(WeakReference reference, string name, IPropertyAccessor inner) : base(inner) { _reference = reference; @@ -45,7 +50,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - var target = _reference.Target as INotifyDataErrorInfo; + var target = GetReferenceTarget() as INotifyDataErrorInfo; if (target != null) { @@ -60,7 +65,7 @@ namespace Avalonia.Data.Core.Plugins protected override void UnsubscribeCore() { - var target = _reference.Target as INotifyDataErrorInfo; + var target = GetReferenceTarget() as INotifyDataErrorInfo; if (target != null) { @@ -80,7 +85,7 @@ namespace Avalonia.Data.Core.Plugins private BindingNotification CreateBindingNotification(object value) { - var target = (INotifyDataErrorInfo)_reference.Target; + var target = (INotifyDataErrorInfo)GetReferenceTarget(); if (target != null) { @@ -101,6 +106,13 @@ namespace Avalonia.Data.Core.Plugins return new BindingNotification(value); } + private object GetReferenceTarget() + { + _reference.TryGetTarget(out object target); + + return target; + } + private Exception GenerateException(IList errors) { if (errors.Count == 1) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 4047489ccc..4716b45340 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -28,12 +28,12 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start(WeakReference reference, string propertyName) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - var instance = reference.Target; + reference.TryGetTarget(out object instance); var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName); if (p != null) @@ -50,11 +50,11 @@ namespace Avalonia.Data.Core.Plugins private class Accessor : PropertyAccessorBase, IWeakSubscriber { - private readonly WeakReference _reference; + private readonly WeakReference _reference; private readonly PropertyInfo _property; private bool _eventRaised; - public Accessor(WeakReference reference, PropertyInfo property) + public Accessor(WeakReference reference, PropertyInfo property) { Contract.Requires(reference != null); Contract.Requires(property != null); @@ -69,7 +69,7 @@ namespace Avalonia.Data.Core.Plugins { get { - var o = _reference.Target; + var o = GetReferenceTarget(); return (o != null) ? _property.GetValue(o) : null; } } @@ -79,7 +79,7 @@ namespace Avalonia.Data.Core.Plugins if (_property.CanWrite) { _eventRaised = false; - _property.SetValue(_reference.Target, value); + _property.SetValue(GetReferenceTarget(), value); if (!_eventRaised) { @@ -109,7 +109,7 @@ namespace Avalonia.Data.Core.Plugins protected override void UnsubscribeCore() { - var inpc = _reference.Target as INotifyPropertyChanged; + var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) { @@ -120,6 +120,13 @@ namespace Avalonia.Data.Core.Plugins } } + private object GetReferenceTarget() + { + _reference.TryGetTarget(out object target); + + return target; + } + private void SendCurrentValue() { try @@ -132,7 +139,7 @@ namespace Avalonia.Data.Core.Plugins private void SubscribeToChanges() { - var inpc = _reference.Target as INotifyPropertyChanged; + var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index e48c671a13..c19ee8dba7 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -9,12 +9,12 @@ namespace Avalonia.Data.Core.Plugins public bool Match(object obj, string methodName) => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName); - public IPropertyAccessor Start(WeakReference reference, string methodName) + public IPropertyAccessor Start(WeakReference reference, string methodName) { Contract.Requires(reference != null); Contract.Requires(methodName != null); - var instance = reference.Target; + reference.TryGetTarget(out object instance); var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName); if (method != null) @@ -35,9 +35,9 @@ namespace Avalonia.Data.Core.Plugins } } - private class Accessor : PropertyAccessorBase + private sealed class Accessor : PropertyAccessorBase { - public Accessor(WeakReference reference, MethodInfo method) + public Accessor(WeakReference reference, MethodInfo method) { Contract.Requires(reference != null); Contract.Requires(method != null); @@ -61,8 +61,17 @@ namespace Avalonia.Data.Core.Plugins var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters); } - - Value = method.IsStatic ? method.CreateDelegate(PropertyType) : method.CreateDelegate(PropertyType, reference.Target); + + if (method.IsStatic) + { + Value = method.CreateDelegate(PropertyType); + } + else + { + reference.TryGetTarget(out object target); + + Value = method.CreateDelegate(PropertyType, target); + } } public override Type PropertyType { get; } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index c41097c274..ef5ce05821 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -20,9 +20,11 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) + public virtual bool Match(WeakReference reference) { - return reference.Target.GetType().GetInterfaces().Any(x => + reference.TryGetTarget(out object target); + + return target != null && target.GetType().GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)); } @@ -34,9 +36,9 @@ namespace Avalonia.Data.Core.Plugins /// /// An observable that produces the output for the value. /// - public virtual IObservable Start(WeakReference reference) + public virtual IObservable Start(WeakReference reference) { - var target = reference.Target; + reference.TryGetTarget(out object target); // If the observable returns a reference type then we can cast it. if (target is IObservable result) @@ -46,7 +48,7 @@ namespace Avalonia.Data.Core.Plugins // If the observable returns a value type then we need to call Observable.Select on it. // First get the type of T in `IObservable`. - var sourceType = reference.Target.GetType().GetInterfaces().First(x => + var sourceType = target.GetType().GetInterfaces().First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0]; diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 16862f576d..a3d2714747 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -19,7 +19,12 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) => reference.Target is Task; + public virtual bool Match(WeakReference reference) + { + reference.TryGetTarget(out object target); + + return target is Task; + } /// /// Starts producing output based on the specified value. @@ -28,11 +33,11 @@ namespace Avalonia.Data.Core.Plugins /// /// An observable that produces the output for the value. /// - public virtual IObservable Start(WeakReference reference) + public virtual IObservable Start(WeakReference reference) { - var task = reference.Target as Task; + reference.TryGetTarget(out object target); - if (task != null) + if (target is Task task) { var resultProperty = task.GetType().GetRuntimeProperty("Result"); diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index df8f46a7d7..70f53b8b88 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -37,9 +37,11 @@ namespace Avalonia.Data.Core return false; } - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); + reference.TryGetTarget(out object target); + + var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 7c839acb78..eb98b9e8d6 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -19,11 +19,25 @@ namespace Avalonia.Data.Core { return false; } + + if (LastValue == null) + { + return false; + } + + bool isLastValueAlive = LastValue.TryGetTarget(out object lastValue); + + if (!isLastValueAlive) + { + return false; + } + if (PropertyType.IsValueType) { - return LastValue?.Target != null && LastValue.Target.Equals(value); + return lastValue.Equals(value); } - return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value); + + return ReferenceEquals(lastValue, value); } protected abstract bool SetTargetValueCore(object value, BindingPriority priority); diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 6fc178e7f8..183e0662aa 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -12,7 +12,7 @@ namespace Avalonia.Data.Core public override string Description => "^"; - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { foreach (var plugin in ExpressionObserver.StreamHandlers) { diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 59ff35be76..307ddd284c 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.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.Collections.Generic; using System.Reflection; using System.Linq; using Avalonia.Controls.ApplicationLifetimes; @@ -59,6 +60,8 @@ namespace Avalonia.Controls public Action AfterSetupCallback { get; private set; } = builder => { }; + public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; + protected AppBuilderBase(IRuntimePlatform platform, Action platformServices) { RuntimePlatform = platform; @@ -97,6 +100,13 @@ namespace Avalonia.Controls AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); return Self; } + + + public TAppBuilder AfterPlatformServicesSetup(Action callback) + { + AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); + return Self; + } /// /// Starts the application with an instance of . @@ -274,6 +284,7 @@ namespace Avalonia.Controls RuntimePlatformServicesInitializer(); WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); + AfterPlatformServicesSetupCallback(Self); Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); diff --git a/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs b/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..1420fce3c2 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs @@ -0,0 +1,23 @@ +// 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.ObjectModel; +using System.Threading.Tasks; +using Avalonia.Platform; + +namespace Avalonia.Controls.Platform +{ + /// + /// Defines a platform-specific mount volumes info provider implementation. + /// + public interface IMountedVolumeInfoProvider + { + /// + /// Listens to any changes in volume mounts and + /// forwards updates to the referenced + /// . + /// + IDisposable Listen(ObservableCollection mountedDrives); + } +} diff --git a/src/Avalonia.Controls/Platform/MountedDriveInfo.cs b/src/Avalonia.Controls/Platform/MountedDriveInfo.cs new file mode 100644 index 0000000000..b534d11d40 --- /dev/null +++ b/src/Avalonia.Controls/Platform/MountedDriveInfo.cs @@ -0,0 +1,24 @@ +// 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.Platform +{ + /// + /// Describes a Drive's properties. + /// + public class MountedVolumeInfo : IEquatable + { + public string VolumeLabel { get; set; } + public string VolumePath { get; set; } + public ulong VolumeSizeBytes { get; set; } + + public bool Equals(MountedVolumeInfo other) + { + return this.VolumeSizeBytes.Equals(other.VolumeSizeBytes) && + this.VolumePath.Equals(other.VolumePath) && + (this.VolumeLabel ?? string.Empty).Equals(other.VolumeLabel ?? string.Empty); + } + } +} diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index f321625bcc..6ccaa3c742 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -14,7 +14,13 @@ namespace Avalonia.Controls public abstract class FileSystemDialog : SystemDialog { - public string InitialDirectory { get; set; } + [Obsolete("Use Directory")] + public string InitialDirectory + { + get => Directory; + set => Directory = value; + } + public string Directory { get; set; } } public class SaveFileDialog : FileDialog @@ -45,8 +51,12 @@ namespace Avalonia.Controls public class OpenFolderDialog : FileSystemDialog { - public string DefaultDirectory { get; set; } - + [Obsolete("Use Directory")] + public string DefaultDirectory + { + get => Directory; + set => Directory = value; + } public Task ShowAsync(Window parent) { if(parent == null) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj new file mode 100644 index 0000000000..3bfb254601 --- /dev/null +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -0,0 +1,18 @@ + + + netstandard2.0 + + + + + Designer + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/ByteSizeHelper.cs b/src/Avalonia.Dialogs/ByteSizeHelper.cs new file mode 100644 index 0000000000..d849e33399 --- /dev/null +++ b/src/Avalonia.Dialogs/ByteSizeHelper.cs @@ -0,0 +1,40 @@ +using System; + +namespace Avalonia.Dialogs +{ + internal static class ByteSizeHelper + { + private const string formatTemplate = "{0}{1:0.#} {2}"; + + private static readonly string[] Prefixes = + { + "B", + "KB", + "MB", + "GB", + "TB", + "PB", + "EB", + "ZB", + "YB" + }; + + public static string ToString(ulong bytes) + { + if (bytes == 0) + { + return string.Format(formatTemplate, null, 0, Prefixes[0]); + } + + var absSize = Math.Abs((double)bytes); + var fpPower = Math.Log(absSize, 1000); + var intPower = (int)fpPower; + var iUnit = intPower >= Prefixes.Length + ? Prefixes.Length - 1 + : intPower; + var normSize = absSize / Math.Pow(1000, iUnit); + + return string.Format(formatTemplate,bytes < 0 ? "-" : null, normSize, Prefixes[iUnit]); + } + } +} diff --git a/src/Avalonia.Dialogs/ChildFitter.cs b/src/Avalonia.Dialogs/ChildFitter.cs new file mode 100644 index 0000000000..744d455d9d --- /dev/null +++ b/src/Avalonia.Dialogs/ChildFitter.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; + +namespace Avalonia.Dialogs +{ + internal class ChildFitter : Decorator + { + protected override Size MeasureOverride(Size availableSize) + { + return new Size(0, 0); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Child.Measure(finalSize); + base.ArrangeOverride(finalSize); + return finalSize; + } + } +} diff --git a/src/Avalonia.Dialogs/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/FileSizeStringConverter.cs new file mode 100644 index 0000000000..5b41b9da35 --- /dev/null +++ b/src/Avalonia.Dialogs/FileSizeStringConverter.cs @@ -0,0 +1,26 @@ +using Avalonia.Data.Converters; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Avalonia.Dialogs +{ + internal class FileSizeStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is long size && size > 0) + { + return ByteSizeHelper.ToString((ulong)size); + } + + return ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Dialogs/InternalViewModelBase.cs b/src/Avalonia.Dialogs/InternalViewModelBase.cs new file mode 100644 index 0000000000..520bc15bfe --- /dev/null +++ b/src/Avalonia.Dialogs/InternalViewModelBase.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Avalonia.Dialogs +{ + internal class InternalViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + return false; + } + + [NotifyPropertyChangedInvocator] + protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml new file mode 100644 index 0000000000..af0c91e7bd --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Show hidden files + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs new file mode 100644 index 0000000000..b967b40c0d --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooser : UserControl + { + private Control _quickLinksRoot; + private ListBox _filesView; + + public ManagedFileChooser() + { + AvaloniaXamlLoader.Load(this); + AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + _quickLinksRoot = this.FindControl("QuickLinks"); + _filesView = this.FindControl("Files"); + } + + ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel; + + private void OnPointerPressed(object sender, PointerPressedEventArgs e) + { + var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel; + + if (model == null) + { + return; + } + + var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); + if (e.ClickCount == 2 || isQuickLink) + { + if (model.ItemType == ManagedFileChooserItemType.File) + { + Model?.SelectSingleFile(model); + } + else + { + Model?.Navigate(model.Path); + } + + e.Handled = true; + } + } + + protected override async void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + + var model = (DataContext as ManagedFileChooserViewModel); + + if (model == null) + { + return; + } + + var preselected = model.SelectedItems.FirstOrDefault(); + + if (preselected == null) + { + return; + } + + //Let everything to settle down and scroll to selected item + await Task.Delay(100); + + if (preselected != model.SelectedItems.FirstOrDefault()) + { + return; + } + + // Workaround for ListBox bug, scroll to the previous file + var indexOfPreselected = model.Items.IndexOf(preselected); + + if (indexOfPreselected > 1) + { + _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]); + } + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs new file mode 100644 index 0000000000..a0cb664b40 --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserFilterViewModel : InternalViewModelBase + { + private readonly string[] _extensions; + public string Name { get; } + + public ManagedFileChooserFilterViewModel(FileDialogFilter filter) + { + Name = filter.Name; + + if (filter.Extensions.Contains("*")) + { + return; + } + + _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray(); + } + + public ManagedFileChooserFilterViewModel() + { + Name = "All files"; + } + + public bool Match(string filename) + { + if (_extensions == null) + { + return true; + } + + foreach (var ext in _extensions) + { + if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override string ToString() => Name; + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs b/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs new file mode 100644 index 0000000000..835e64a59a --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Dialogs +{ + public enum ManagedFileChooserItemType + { + File, + Folder, + Volume + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs new file mode 100644 index 0000000000..2801930028 --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs @@ -0,0 +1,77 @@ +using System; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserItemViewModel : InternalViewModelBase + { + private string _displayName; + private string _path; + private DateTime _modified; + private string _type; + private long _size; + private ManagedFileChooserItemType _itemType; + + public string DisplayName + { + get => _displayName; + set => this.RaiseAndSetIfChanged(ref _displayName, value); + } + + public string Path + { + get => _path; + set => this.RaiseAndSetIfChanged(ref _path, value); + } + + public DateTime Modified + { + get => _modified; + set => this.RaiseAndSetIfChanged(ref _modified, value); + } + + public string Type + { + get => _type; + set => this.RaiseAndSetIfChanged(ref _type, value); + } + + public long Size + { + get => _size; + set => this.RaiseAndSetIfChanged(ref _size, value); + } + + public ManagedFileChooserItemType ItemType + { + get => _itemType; + set => this.RaiseAndSetIfChanged(ref _itemType, value); + } + + public string IconKey + { + get + { + switch (ItemType) + { + case ManagedFileChooserItemType.Folder: + return "Icon_Folder"; + case ManagedFileChooserItemType.Volume: + return "Icon_Volume"; + default: + return "Icon_File"; + } + } + } + + public ManagedFileChooserItemViewModel() + { + } + + public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item) + { + ItemType = item.ItemType; + Path = item.Path; + DisplayName = item.DisplayName; + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs b/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs new file mode 100644 index 0000000000..8dac14bf8b --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserNavigationItem + { + public string DisplayName { get; set; } + public string Path { get; set; } + public ManagedFileChooserItemType ItemType { get; set; } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs new file mode 100644 index 0000000000..0dc024c4dd --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform; +using Avalonia.Threading; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserSources + { + public Func GetUserDirectories { get; set; } + = DefaultGetUserDirectories; + + public Func GetFileSystemRoots { get; set; } + = DefaultGetFileSystemRoots; + + public Func GetAllItemsDelegate { get; set; } + = DefaultGetAllItems; + + public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); + public static readonly ObservableCollection MountedVolumes = new ObservableCollection(); + + public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources) + { + return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).ToArray(); + } + + private static Environment.SpecialFolder[] s_folders = new[] + { + Environment.SpecialFolder.Desktop, + Environment.SpecialFolder.UserProfile, + Environment.SpecialFolder.MyDocuments, + Environment.SpecialFolder.MyMusic, + Environment.SpecialFolder.MyPictures, + Environment.SpecialFolder.MyVideos + }; + + public static ManagedFileChooserNavigationItem[] DefaultGetUserDirectories() + { + return s_folders.Select(Environment.GetFolderPath).Distinct() + .Where(d => !string.IsNullOrWhiteSpace(d)) + .Where(Directory.Exists) + .Select(d => new ManagedFileChooserNavigationItem + { + ItemType = ManagedFileChooserItemType.Folder, + Path = d, + DisplayName = Path.GetFileName(d) + }).ToArray(); + } + + public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() + { + return MountedVolumes + .Select(x => + { + var displayName = x.VolumeLabel; + + if (displayName == null & x.VolumeSizeBytes > 0) + { + displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes)} Volume"; + }; + + try + { + Directory.GetFiles(x.VolumePath); + } + catch (UnauthorizedAccessException _) + { + return null; + } + + return new ManagedFileChooserNavigationItem + { + ItemType = ManagedFileChooserItemType.Volume, + DisplayName = displayName, + Path = x.VolumePath + }; + }) + .Where(x => x != null) + .ToArray(); + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs new file mode 100644 index 0000000000..03e05e7a75 --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Threading; + +namespace Avalonia.Dialogs +{ + internal class ManagedFileChooserViewModel : InternalViewModelBase + { + public event Action CancelRequested; + public event Action CompleteRequested; + + public AvaloniaList QuickLinks { get; } = + new AvaloniaList(); + + public AvaloniaList Items { get; } = + new AvaloniaList(); + + public AvaloniaList Filters { get; } = + new AvaloniaList(); + + public AvaloniaList SelectedItems { get; } = + new AvaloniaList(); + + string _location; + string _fileName; + private bool _showHiddenFiles; + private ManagedFileChooserFilterViewModel _selectedFilter; + private bool _selectingDirectory; + private bool _savingFile; + private bool _scheduledSelectionValidation; + private bool _alreadyCancelled = false; + private string _defaultExtension; + private CompositeDisposable _disposables; + + public string Location + { + get => _location; + private set => this.RaiseAndSetIfChanged(ref _location, value); + } + + public string FileName + { + get => _fileName; + private set => this.RaiseAndSetIfChanged(ref _fileName, value); + } + + public bool SelectingFolder => _selectingDirectory; + + public bool ShowFilters { get; } + public SelectionMode SelectionMode { get; } + public string Title { get; } + + public int QuickLinksSelectedIndex + { + get + { + for (var index = 0; index < QuickLinks.Count; index++) + { + var i = QuickLinks[index]; + + if (i.Path == Location) + { + return index; + } + } + + return -1; + } + set => this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + public ManagedFileChooserFilterViewModel SelectedFilter + { + get => _selectedFilter; + set + { + this.RaiseAndSetIfChanged(ref _selectedFilter, value); + Refresh(); + } + } + + public bool ShowHiddenFiles + { + get => _showHiddenFiles; + set + { + this.RaiseAndSetIfChanged(ref _showHiddenFiles, value); + Refresh(); + } + } + + private void RefreshQuickLinks(ManagedFileChooserSources quickSources) + { + QuickLinks.Clear(); + QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); + } + + public ManagedFileChooserViewModel(FileSystemDialog dialog) + { + _disposables = new CompositeDisposable(); + + var quickSources = AvaloniaLocator.Current + .GetService() + ?? new ManagedFileChooserSources(); + + var sub1 = AvaloniaLocator.Current + .GetService() + .Listen(ManagedFileChooserSources.MountedVolumes); + + var sub2 = Observable.FromEventPattern(ManagedFileChooserSources.MountedVolumes, + nameof(ManagedFileChooserSources.MountedVolumes.CollectionChanged)) + .ObserveOn(AvaloniaScheduler.Instance) + .Subscribe(x => RefreshQuickLinks(quickSources)); + + _disposables.Add(sub1); + _disposables.Add(sub2); + + CompleteRequested += delegate { _disposables?.Dispose(); }; + CancelRequested += delegate { _disposables?.Dispose(); }; + + RefreshQuickLinks(quickSources); + + Title = dialog.Title ?? ( + dialog is OpenFileDialog ? "Open file" + : dialog is SaveFileDialog ? "Save file" + : dialog is OpenFolderDialog ? "Select directory" + : throw new ArgumentException(nameof(dialog))); + + var directory = dialog.InitialDirectory; + + if (directory == null || !Directory.Exists(directory)) + { + directory = Directory.GetCurrentDirectory(); + } + + if (dialog is FileDialog fd) + { + if (fd.Filters?.Count > 0) + { + Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f))); + _selectedFilter = Filters[0]; + ShowFilters = true; + } + + if (dialog is OpenFileDialog ofd) + { + if (ofd.AllowMultiple) + { + SelectionMode = SelectionMode.Multiple; + } + } + } + + _selectingDirectory = dialog is OpenFolderDialog; + + if (dialog is SaveFileDialog sfd) + { + _savingFile = true; + _defaultExtension = sfd.DefaultExtension; + FileName = sfd.InitialFileName; + } + + Navigate(directory, (dialog as FileDialog)?.InitialFileName); + SelectedItems.CollectionChanged += OnSelectionChangedAsync; + } + + public void EnterPressed() + { + if (Directory.Exists(Location)) + { + Navigate(Location); + } + else if (File.Exists(Location)) + { + CompleteRequested?.Invoke(new[] { Location }); + } + } + + private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) + { + if (_scheduledSelectionValidation) + { + return; + } + + _scheduledSelectionValidation = true; + await Dispatcher.UIThread.InvokeAsync(() => + { + try + { + if (_selectingDirectory) + { + SelectedItems.Clear(); + } + else + { + var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder).ToList(); + foreach (var item in invalidItems) + { + SelectedItems.Remove(item); + } + + if (!_selectingDirectory) + { + FileName = SelectedItems.FirstOrDefault()?.DisplayName; + } + } + } + finally + { + _scheduledSelectionValidation = false; + } + }); + } + + void NavigateRoot(string initialSelectionName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName); + } + else + { + Navigate("/", initialSelectionName); + } + } + + public void Refresh() => Navigate(Location); + + public void Navigate(string path, string initialSelectionName = null) + { + if (!Directory.Exists(path)) + { + NavigateRoot(initialSelectionName); + } + else + { + Location = path; + Items.Clear(); + SelectedItems.Clear(); + + try + { + var infos = new DirectoryInfo(path).EnumerateFileSystemInfos(); + + if (!ShowHiddenFiles) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + infos = infos.Where(i => (i.Attributes & (FileAttributes.Hidden | FileAttributes.System)) == 0); + } + else + { + infos = infos.Where(i => !i.Name.StartsWith(".")); + } + } + + if (SelectedFilter != null) + { + infos = infos.Where(i => i is DirectoryInfo || SelectedFilter.Match(i.Name)); + } + + Items.AddRange(infos.Where(x => + { + if (_selectingDirectory) + { + if (!(x is DirectoryInfo)) + { + return false; + } + } + + return true; + }) + .Where(x => x.Exists) + .Select(info => new ManagedFileChooserItemViewModel + { + DisplayName = info.Name, + Path = info.FullName, + Type = info is FileInfo ? info.Extension : "File Folder", + ItemType = info is FileInfo ? ManagedFileChooserItemType.File + : ManagedFileChooserItemType.Folder, + Size = info is FileInfo f ? f.Length : 0, + Modified = info.LastWriteTime + }) + .OrderByDescending(x => x.ItemType == ManagedFileChooserItemType.Folder) + .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase)); + + if (initialSelectionName != null) + { + var sel = Items.FirstOrDefault(i => i.ItemType == ManagedFileChooserItemType.File && i.DisplayName == initialSelectionName); + + if (sel != null) + { + SelectedItems.Add(sel); + } + } + + this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + catch (System.UnauthorizedAccessException) + { + } + } + } + + public void GoUp() + { + var parent = Path.GetDirectoryName(Location); + + if (string.IsNullOrWhiteSpace(parent)) + { + return; + } + + Navigate(parent); + } + + public void Cancel() + { + if (!_alreadyCancelled) + { + // INFO: Don't misplace this check or it might cause + // StackOverflowException because of recursive + // event invokes. + _alreadyCancelled = true; + CancelRequested?.Invoke(); + } + } + + public void Ok() + { + if (_selectingDirectory) + { + CompleteRequested?.Invoke(new[] { Location }); + } + else if (_savingFile) + { + if (!string.IsNullOrWhiteSpace(FileName)) + { + if (!Path.HasExtension(FileName) && !string.IsNullOrWhiteSpace(_defaultExtension)) + { + FileName = Path.ChangeExtension(FileName, _defaultExtension); + } + + CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) }); + } + } + else + { + CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); + } + } + + public void SelectSingleFile(ManagedFileChooserItemViewModel item) + { + CompleteRequested?.Invoke(new[] { item.Path }); + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs new file mode 100644 index 0000000000..771d2b1b5e --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Dialogs; +using Avalonia.Platform; + +namespace Avalonia.Dialogs +{ + public static class ManagedFileDialogExtensions + { + class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() + { + async Task Show(SystemDialog d, IWindowImpl parent) + { + var model = new ManagedFileChooserViewModel((FileSystemDialog)d); + + var dialog = new T + { + Content = new ManagedFileChooser(), + DataContext = model + }; + + dialog.Closed += delegate { model.Cancel(); }; + + string[] result = null; + + model.CompleteRequested += items => + { + result = items; + dialog.Close(); + }; + + model.CancelRequested += dialog.Close; + + await dialog.ShowDialog(parent); + return result; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return await Show(dialog, parent); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return (await Show(dialog, parent))?.FirstOrDefault(); + } + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); + return builder; + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() where TWindow : Window, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); + return builder; + } + } +} diff --git a/src/Avalonia.Dialogs/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs new file mode 100644 index 0000000000..9d8b6cb1c7 --- /dev/null +++ b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Avalonia.Dialogs +{ + internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter + { + public object Convert(object key, Type targetType, object parameter, CultureInfo culture) + { + TryGetResource((string)key, out var value); + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj new file mode 100644 index 0000000000..d7e1d8cdb3 --- /dev/null +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs new file mode 100644 index 0000000000..8081528e55 --- /dev/null +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.FreeDesktop +{ + internal class LinuxMountedVolumeInfoListener : IDisposable + { + private const string DevByLabelDir = "/dev/disk/by-label/"; + private const string ProcPartitionsDir = "/proc/partitions"; + private const string ProcMountsDir = "/proc/mounts"; + private CompositeDisposable _disposables; + private ObservableCollection _targetObs; + private bool _beenDisposed = false; + + public LinuxMountedVolumeInfoListener(ref ObservableCollection target) + { + _disposables = new CompositeDisposable(); + this._targetObs = target; + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x))); + + private void Poll(long _) + { + var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) + .Skip(1) + .Where(p => !string.IsNullOrEmpty(p)) + .Select(p => Regex.Replace(p, @"\s{2,}", " ").Trim().Split(' ')) + .Select(p => (p[2].Trim(), p[3].Trim())) + .Select(p => (Convert.ToUInt64(p.Item1) * 1024, "/dev/" + p.Item2)); + + var fProcMounts = File.ReadAllLines(ProcMountsDir) + .Select(x => x.Split(' ')) + .Select(x => (x[0], x[1])); + + var labelDirEnum = Directory.Exists(DevByLabelDir) ? + new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); + + var labelDevPathPairs = labelDirEnum + .Select(x => (GetSymlinkTarget(x.FullName), x.Name)); + + var q1 = from mount in fProcMounts + join device in fProcPartitions on mount.Item1 equals device.Item2 + join label in labelDevPathPairs on device.Item2 equals label.Item1 into labelMatches + from x in labelMatches.DefaultIfEmpty() + select new MountedVolumeInfo() + { + VolumePath = mount.Item2, + VolumeSizeBytes = device.Item1, + VolumeLabel = x.Name + }; + + var mountVolInfos = q1.ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + _disposables.Dispose(); + _targetObs.Clear(); + } + + _beenDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..d68c02bfd6 --- /dev/null +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.ObjectModel; + +using Avalonia.Controls.Platform; + +namespace Avalonia.FreeDesktop +{ + public class LinuxMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new LinuxMountedVolumeInfoListener(ref mountedDrives); + } + } +} diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs new file mode 100644 index 0000000000..d9b6dce082 --- /dev/null +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.FreeDesktop +{ + internal static class NativeMethods + { + [DllImport("libc", SetLastError = true)] + private static extern long readlink([MarshalAs(UnmanagedType.LPArray)] byte[] filename, + [MarshalAs(UnmanagedType.LPArray)] byte[] buffer, + long len); + + public static string ReadLink(string path) + { + var symlink = Encoding.UTF8.GetBytes(path); + var result = new byte[4095]; + readlink(symlink, result, result.Length); + var rawstr = Encoding.UTF8.GetString(result); + return rawstr.Substring(0, rawstr.IndexOf('\0')); + } + } +} diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 02dda45e99..bb8c8b8c40 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -31,7 +31,7 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - private static WeakReference s_lastPress; + private static WeakReference s_lastPress; static Gestures() { @@ -47,11 +47,11 @@ namespace Avalonia.Input if (e.ClickCount <= 1) { - s_lastPress = new WeakReference(e.Source); + s_lastPress = new WeakReference(e.Source); } - else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source) + else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton != MouseButton.Right) { - if (e.MouseButton != MouseButton.Right) + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); } @@ -65,10 +65,10 @@ namespace Avalonia.Input { var e = (PointerReleasedEventArgs)ev; - if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; - ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(et)); + e.Source.RaiseEvent(new RoutedEventArgs(et)); } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index edde2176bd..0da97b915c 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -84,8 +84,8 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) - .Bind() - .ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)); + .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) + .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..eea695d77e --- /dev/null +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls.Platform; + +namespace Avalonia.Native +{ + internal class WindowsMountedVolumeInfoListener : IDisposable + { + private readonly CompositeDisposable _disposables; + private readonly ObservableCollection _targetObs; + private bool _beenDisposed = false; + private ObservableCollection mountedDrives; + + public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + { + this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private void Poll(long _) + { + var mountVolInfos = Directory.GetDirectories("/Volumes") + .Select(p => new MountedVolumeInfo() + { + VolumeLabel = Path.GetFileName(p), + VolumePath = p, + VolumeSizeBytes = 0 + }) + .ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + + } + _beenDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + } + + public class MacOSMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new WindowsMountedVolumeInfoListener(mountedDrives); + } + } +} diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 59afc877de..c160fd7726 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index c68cb04733..87a4174c06 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -52,16 +52,6 @@ namespace Avalonia.X11 {X11Key.Delete, Key.Delete}, {X11Key.KP_Delete, Key.Delete}, {X11Key.Help, Key.Help}, - {X11Key.XK_0, Key.D0}, - {X11Key.XK_1, Key.D1}, - {X11Key.XK_2, Key.D2}, - {X11Key.XK_3, Key.D3}, - {X11Key.XK_4, Key.D4}, - {X11Key.XK_5, Key.D5}, - {X11Key.XK_6, Key.D6}, - {X11Key.XK_7, Key.D7}, - {X11Key.XK_8, Key.D8}, - {X11Key.XK_9, Key.D9}, {X11Key.A, Key.A}, {X11Key.B, Key.B}, {X11Key.C, Key.C}, @@ -114,8 +104,8 @@ namespace Avalonia.X11 {X11Key.x, Key.X}, {X11Key.y, Key.Y}, {X11Key.z, Key.Z}, - //{ X11Key.?, Key.LWin } - //{ X11Key.?, Key.RWin } + {X11Key.Meta_L, Key.LWin }, + {X11Key.Meta_R, Key.RWin }, {X11Key.Menu, Key.Apps}, //{ X11Key.?, Key.Sleep } {X11Key.KP_0, Key.NumPad0}, @@ -185,20 +175,51 @@ namespace Avalonia.X11 //{ X11Key.?, Key.SelectMedia } //{ X11Key.?, Key.LaunchApplication1 } //{ X11Key.?, Key.LaunchApplication2 } - {X11Key.semicolon, Key.OemSemicolon}, + {X11Key.minus, Key.OemMinus}, + {X11Key.underscore, Key.OemMinus}, {X11Key.plus, Key.OemPlus}, {X11Key.equal, Key.OemPlus}, + {X11Key.bracketleft, Key.OemOpenBrackets}, + {X11Key.braceleft, Key.OemOpenBrackets}, + {X11Key.bracketright, Key.OemCloseBrackets}, + {X11Key.braceright, Key.OemCloseBrackets}, + {X11Key.backslash, Key.OemPipe}, + {X11Key.bar, Key.OemPipe}, + {X11Key.semicolon, Key.OemSemicolon}, + {X11Key.colon, Key.OemSemicolon}, + {X11Key.apostrophe, Key.OemQuotes}, + {X11Key.quotedbl, Key.OemQuotes}, {X11Key.comma, Key.OemComma}, - {X11Key.minus, Key.OemMinus}, + {X11Key.less, Key.OemComma}, {X11Key.period, Key.OemPeriod}, + {X11Key.greater, Key.OemPeriod}, {X11Key.slash, Key.Oem2}, + {X11Key.question, Key.Oem2}, {X11Key.grave, Key.OemTilde}, + {X11Key.asciitilde, Key.OemTilde}, + {X11Key.XK_1, Key.D1}, + {X11Key.exclam, Key.D1}, + {X11Key.XK_2, Key.D2}, + {X11Key.at, Key.D2}, + {X11Key.XK_3, Key.D3}, + {X11Key.numbersign, Key.D3}, + {X11Key.XK_4, Key.D4}, + {X11Key.dollar, Key.D4}, + {X11Key.XK_5, Key.D5}, + {X11Key.percent, Key.D5}, + {X11Key.XK_6, Key.D6}, + {X11Key.asciicircum, Key.D6}, + {X11Key.XK_7, Key.D7}, + {X11Key.ampersand, Key.D7}, + {X11Key.XK_8, Key.D8}, + {X11Key.asterisk, Key.D8}, + {X11Key.XK_9, Key.D9}, + {X11Key.parenleft, Key.D9}, + {X11Key.XK_0, Key.D0}, + {X11Key.parenright, Key.D0}, + //{ X11Key.?, Key.AbntC1 } //{ X11Key.?, Key.AbntC2 } - {X11Key.bracketleft, Key.OemOpenBrackets}, - {X11Key.backslash, Key.OemPipe}, - {X11Key.bracketright, Key.OemCloseBrackets}, - {X11Key.apostrophe, Key.OemQuotes}, //{ X11Key.?, Key.Oem8 } //{ X11Key.?, Key.Oem102 } //{ X11Key.?, Key.ImeProcessed } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e88a7d8db2..1d2290236c 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; @@ -12,6 +13,7 @@ using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; + namespace Avalonia.X11 { class AvaloniaX11Platform : IWindowingPlatform @@ -48,7 +50,8 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) - .Bind().ToConstant(new GtkSystemDialog()); + .Bind().ToConstant(new GtkSystemDialog()) + .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index bbacef43dd..548f0161d6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -164,6 +164,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public bool IsStatic => true; public string Name { get; protected set; } public IXamlIlType DeclaringType { get; } + public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) + => throw new System.NotSupportedException(); public bool Equals(IXamlIlMethod other) => diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index c2ec091f79..c7155c5f6c 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit c2ec091f79fb4e1eea629bc823c9c24da7050022 +Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 981e93c534..7eec80fc00 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Markup.Parsers.Nodes public override string Description => $"#{_name}"; - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { if (_nameScope.TryGetTarget(out var scope)) _subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 221df44327..321a85c1d7 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -31,9 +31,9 @@ namespace Avalonia.Markup.Parsers.Nodes } } - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - if (reference.Target is ILogical logical) + if (reference.TryGetTarget(out object target) && target is ILogical logical) { _subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged); } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index ea847bde11..a11879238b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -26,9 +26,11 @@ namespace Avalonia.Markup.Parsers.Nodes protected override bool SetTargetValueCore(object value, BindingPriority priority) { - var typeInfo = Target.Target.GetType().GetTypeInfo(); - var list = Target.Target as IList; - var dictionary = Target.Target as IDictionary; + Target.TryGetTarget(out object target); + + var typeInfo = target.GetType().GetTypeInfo(); + var list = target as IList; + var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); var indexerParameters = indexerProperty?.GetIndexParameters(); @@ -53,7 +55,7 @@ namespace Avalonia.Markup.Parsers.Nodes // Try special cases where we can validate indices if (typeInfo.IsArray) { - return SetValueInArray((Array)Target.Target, intArgs, value); + return SetValueInArray((Array)target, intArgs, value); } else if (Arguments.Count == 1) { @@ -83,14 +85,14 @@ namespace Avalonia.Markup.Parsers.Nodes else { // Fallback to unchecked access - indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + indexerProperty.SetValue(target, value, convertedObjectArray); return true; } } else { // Fallback to unchecked access - indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + indexerProperty.SetValue(target, value, convertedObjectArray); return true; } } @@ -98,7 +100,7 @@ namespace Avalonia.Markup.Parsers.Nodes // multidimensional indexer, which doesn't take the same number of arguments else if (typeInfo.IsArray) { - SetValueInArray((Array)Target.Target, value); + SetValueInArray((Array)target, value); return true; } return false; @@ -126,7 +128,15 @@ namespace Avalonia.Markup.Parsers.Nodes public IList Arguments { get; } - public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + public override Type PropertyType + { + get + { + Target.TryGetTarget(out object target); + + return GetIndexer(target.GetType().GetTypeInfo())?.PropertyType; + } + } protected override object GetValue(object target) { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index bc40ec2ff7..ac3fa021f1 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -90,7 +90,9 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToSingleton() - .Bind().ToConstant(s_instance); + .Bind().ToConstant(s_instance) + .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()); + if (options.AllowEglInitialization) Win32GlManager.Initialize(); diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs new file mode 100644 index 0000000000..102e027584 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls.Platform; + +namespace Avalonia.Win32 +{ + internal class WindowsMountedVolumeInfoListener : IDisposable + { + private readonly CompositeDisposable _disposables; + private readonly ObservableCollection _targetObs = new ObservableCollection(); + private bool _beenDisposed = false; + private ObservableCollection mountedDrives; + + public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + { + this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private void Poll(long _) + { + var allDrives = DriveInfo.GetDrives(); + + var mountVolInfos = allDrives + .Select(p => new MountedVolumeInfo() + { + VolumeLabel = p.VolumeLabel, + VolumePath = p.RootDirectory.FullName, + VolumeSizeBytes = (ulong)p.TotalSize + }) + .ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + + } + _beenDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..e1b5f5a3a0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; + +namespace Avalonia.Win32 +{ + public class WindowsMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new WindowsMountedVolumeInfoListener(mountedDrives); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 8a38a00493..5c01e6a588 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -148,6 +148,23 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + [Fact] + public void AddRange_Items_Should_Raise_Correct_CollectionChanged() + { + var target = new AvaloniaList(); + + var eventItems = new List(); + + target.CollectionChanged += (sender, args) => + { + eventItems.AddRange(args.NewItems.Cast()); + }; + + target.AddRange(Enumerable.Range(0,10).Select(i => new object())); + + Assert.Equal(eventItems, target); + } + [Fact] public void Replacing_Item_Should_Raise_CollectionChanged() { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index b56afa33a4..a2ef8eedad 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -574,7 +574,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var source = new Class1 { Foo = "foo" }; var target = new PropertyAccessorNode("Foo", false); Assert.NotNull(target); - target.Target = new WeakReference(source); + target.Target = new WeakReference(source); target.Subscribe(_ => { }); target.Unsubscribe(); target.Unsubscribe(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs index 378c225e23..435ead0b80 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10))); + Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10))); } [Fact] @@ -29,7 +29,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber))); + Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber))); } [Fact] @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated))); + Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated))); } [Fact] @@ -47,8 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new DataAnnotationsValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor); var result = new List(); var errmsg = new RangeAttribute(5, 10).FormatErrorMessage(nameof(Data.Between5And10)); @@ -79,8 +79,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new DataAnnotationsValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs index 2a307f9a61..6bd5fe5093 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs @@ -19,8 +19,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new ExceptionValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index 2423900c7a..26b64182e1 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -18,8 +18,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new IndeiValidationPlugin(); var data = new Data { Maximum = 5 }; - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); @@ -53,8 +53,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new IndeiValidationPlugin(); var data = new Data { Maximum = 5 }; - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); validator.Subscribe(_ => { }); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index d19accb0ad..0ba06980af 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -60,6 +60,80 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", source.Foo); } + [Fact] + public void TwoWay_Binding_Should_Be_Set_Up_GC_Collect() + { + var source = new WeakRefSource { Foo = null }; + var target = new TestControl { DataContext = source }; + + var binding = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + }; + + target.Bind(TestControl.ValueProperty, binding); + + var ref1 = AssignValue(target, "ref1"); + + Assert.Equal(ref1.Target, source.Foo); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + var ref2 = AssignValue(target, "ref2"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + target.Value = null; + + Assert.Null(source.Foo); + } + + private class DummyObject : ICloneable + { + private readonly string _val; + + public DummyObject(string val) + { + _val = val; + } + + public object Clone() + { + return new DummyObject(_val); + } + + protected bool Equals(DummyObject other) + { + return string.Equals(_val, other._val); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DummyObject) obj); + } + + public override int GetHashCode() + { + return (_val != null ? _val.GetHashCode() : 0); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private WeakReference AssignValue(TestControl source, string val) + { + var obj = new DummyObject(val); + + source.Value = obj; + + return new WeakReference(obj); + } + [Fact] public void OneTime_Binding_Should_Be_Set_Up() { @@ -568,12 +642,70 @@ namespace Avalonia.Markup.UnitTests.Data } } + public class WeakRefSource : INotifyPropertyChanged + { + private WeakReference _foo; + + public object Foo + { + get + { + if (_foo == null) + { + return null; + } + + if (_foo.TryGetTarget(out object target)) + { + if (target is ICloneable cloneable) + { + return cloneable.Clone(); + } + + return target; + } + + return null; + } + set + { + _foo = new WeakReference(value); + + RaisePropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged([CallerMemberName] string prop = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); + } + } + private class OldDataContextViewModel { public int Foo { get; set; } = 1; public int Bar { get; set; } = 2; } + private class TestControl : Control + { + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(Value), + o => o.Value, + (o, v) => o.Value = v); + + private object _value; + + public object Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + } + private class OldDataContextTest : Control { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 7281542bc1..b1abc9ea54 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -309,8 +309,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] - public void Binding_To_TextBlock_Text_With_StringConverter_Works() + [Theory, + InlineData(@"Hello \{0\}"), + InlineData(@"'Hello {0}'"), + InlineData(@"Hello {0}")] + + public void Binding_To_TextBlock_Text_With_StringConverter_Works(string fmt) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -318,8 +322,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - -"; + +"; var loader = new AvaloniaXamlLoader(); var window = (Window)loader.Load(xaml); var textBlock = window.FindControl("textBlock"); @@ -331,8 +335,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact(Skip="Issue #2592")] - public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works() + [Theory, + InlineData("{}{0} {1}!"), + InlineData(@"\{0\} \{1\}!")] + public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works(string fmt) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -342,7 +348,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> - +