diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index 13bae59805..2ec0b48c76 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -1,29 +1,19 @@ - + ProgressBar A progress bar control - - - + + + - - + - - + - - + + diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index d915887e4c..b52829a60f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -140,6 +140,7 @@ namespace Avalonia.Collections } } + [Obsolete("Causes memory leaks. Use DynamicData or similar instead.")] public static IAvaloniaReadOnlyList CreateDerivedList( this IAvaloniaReadOnlyList collection, Func select) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index c7356f2b4d..e904957429 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,8 +1,7 @@ - using System; using Avalonia.Controls.Primitives; -using Avalonia.Data; using Avalonia.Layout; +using Avalonia.Media; namespace Avalonia.Controls { @@ -11,6 +10,92 @@ namespace Avalonia.Controls /// public class ProgressBar : RangeBase { + public class ProgressBarTemplateProperties : AvaloniaObject + { + private double _container2Width; + private double _containerWidth; + private double _containerAnimationStartPosition; + private double _containerAnimationEndPosition; + private double _container2AnimationStartPosition; + private double _container2AnimationEndPosition; + + public static readonly DirectProperty ContainerAnimationStartPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(ContainerAnimationStartPosition), + p => p.ContainerAnimationStartPosition, + (p, o) => p.ContainerAnimationStartPosition = o, 0d); + + public static readonly DirectProperty ContainerAnimationEndPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(ContainerAnimationEndPosition), + p => p.ContainerAnimationEndPosition, + (p, o) => p.ContainerAnimationEndPosition = o, 0d); + + public static readonly DirectProperty Container2AnimationStartPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(Container2AnimationStartPosition), + p => p.Container2AnimationStartPosition, + (p, o) => p.Container2AnimationStartPosition = o, 0d); + + public static readonly DirectProperty Container2AnimationEndPositionProperty = + AvaloniaProperty.RegisterDirect( + nameof(Container2AnimationEndPosition), + p => p.Container2AnimationEndPosition, + (p, o) => p.Container2AnimationEndPosition = o); + + public static readonly DirectProperty Container2WidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(Container2Width), + p => p.Container2Width, + (p, o) => p.Container2Width = o); + + public static readonly DirectProperty ContainerWidthProperty = + AvaloniaProperty.RegisterDirect( + nameof(ContainerWidth), + p => p.ContainerWidth, + (p, o) => p.ContainerWidth = o); + + public double ContainerAnimationStartPosition + { + get => _containerAnimationStartPosition; + set => SetAndRaise(ContainerAnimationStartPositionProperty, ref _containerAnimationStartPosition, value); + } + + public double ContainerAnimationEndPosition + { + get => _containerAnimationEndPosition; + set => SetAndRaise(ContainerAnimationEndPositionProperty, ref _containerAnimationEndPosition, value); + } + + public double Container2AnimationStartPosition + { + get => _container2AnimationStartPosition; + set => SetAndRaise(Container2AnimationStartPositionProperty, ref _container2AnimationStartPosition, value); + } + + public double Container2Width + { + get => _container2Width; + set => SetAndRaise(Container2WidthProperty, ref _container2Width, value); + } + + public double ContainerWidth + { + get => _containerWidth; + set => SetAndRaise(ContainerWidthProperty, ref _containerWidth, value); + } + + public double Container2AnimationEndPosition + { + get => _container2AnimationEndPosition; + set => SetAndRaise(Container2AnimationEndPositionProperty, ref _container2AnimationEndPosition, value); + } + } + + private double _indeterminateStartingOffset; + private double _indeterminateEndingOffset; + private Border _indicator; + public static readonly StyledProperty IsIndeterminateProperty = AvaloniaProperty.Register(nameof(IsIndeterminate)); @@ -20,19 +105,33 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); - private static readonly DirectProperty IndeterminateStartingOffsetProperty = + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public static readonly DirectProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateStartingOffset), p => p.IndeterminateStartingOffset, (p, o) => p.IndeterminateStartingOffset = o); - private static readonly DirectProperty IndeterminateEndingOffsetProperty = + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public static readonly DirectProperty IndeterminateEndingOffsetProperty = AvaloniaProperty.RegisterDirect( nameof(IndeterminateEndingOffset), p => p.IndeterminateEndingOffset, (p, o) => p.IndeterminateEndingOffset = o); - private Border _indicator; + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public double IndeterminateStartingOffset + { + get => _indeterminateStartingOffset; + set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); + } + + [Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")] + public double IndeterminateEndingOffset + { + get => _indeterminateEndingOffset; + set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); + } static ProgressBar() { @@ -45,6 +144,8 @@ namespace Avalonia.Controls UpdatePseudoClasses(IsIndeterminate, Orientation); } + public ProgressBarTemplateProperties TemplateProperties { get; } = new ProgressBarTemplateProperties(); + public bool IsIndeterminate { get => GetValue(IsIndeterminateProperty); @@ -62,19 +163,6 @@ namespace Avalonia.Controls get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } - private double _indeterminateStartingOffset; - private double IndeterminateStartingOffset - { - get => _indeterminateStartingOffset; - set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value); - } - - private double _indeterminateEndingOffset; - private double IndeterminateEndingOffset - { - get => _indeterminateEndingOffset; - set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value); - } /// protected override Size ArrangeOverride(Size finalSize) @@ -111,21 +199,33 @@ namespace Avalonia.Controls { if (IsIndeterminate) { - if (Orientation == Orientation.Horizontal) - { - var width = bounds.Width / 5.0; - IndeterminateStartingOffset = -width; - _indicator.Width = width; - IndeterminateEndingOffset = bounds.Width; + // Pulled from ModernWPF. - } - else - { - var height = bounds.Height / 5.0; - IndeterminateStartingOffset = -bounds.Height; - _indicator.Height = height; - IndeterminateEndingOffset = height; - } + var dim = Orientation == Orientation.Horizontal ? bounds.Width : bounds.Height; + var barIndicatorWidth = dim * 0.4; // Indicator width at 40% of ProgressBar + var barIndicatorWidth2 = dim * 0.6; // Indicator width at 60% of ProgressBar + + TemplateProperties.ContainerWidth = barIndicatorWidth; + TemplateProperties.Container2Width = barIndicatorWidth2; + + TemplateProperties.ContainerAnimationStartPosition = barIndicatorWidth * -1.8; // Position at -180% + TemplateProperties.ContainerAnimationEndPosition = barIndicatorWidth * 3.0; // Position at 300% + + TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150% + TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166% + + // Remove these properties when we switch to fluent as default and removed the old one. + IndeterminateStartingOffset = -(dim / 5d); + IndeterminateEndingOffset = dim; + + var padding = Padding; + var rectangle = new RectangleGeometry( + new Rect( + padding.Left, + padding.Top, + bounds.Width - (padding.Right + padding.Left), + bounds.Height - (padding.Bottom + padding.Top) + )); } else { diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9b6444fc66..9614d079d9 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; namespace Avalonia.Controls.Utils @@ -15,12 +16,14 @@ namespace Avalonia.Controls.Utils { if (items != null) { - var collection = items as ICollection; - - if (collection != null) + if (items is ICollection collection) { return collection.Count; } + else if (items is IReadOnlyCollection readOnly) + { + return readOnly.Count; + } else { return Enumerable.Count(items.Cast()); diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index b3a8f4745e..4899be2955 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -45,7 +45,15 @@ namespace Avalonia.Diagnostics window.Closed += DevToolsClosed; s_open.Add(root, window); - window.Show(); + + if (root is Window inspectedWindow) + { + window.Show(inspectedWindow); + } + else + { + window.Show(); + } } return Disposable.Create(() => window?.Close()); diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index a7c2997346..38788ef8ee 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; @@ -9,7 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels public LogicalTreeNode(ILogical logical, TreeNode parent) : base((Control)logical, parent) { - Children = logical.LogicalChildren.CreateDerivedList(x => new LogicalTreeNode(x, this)); + Children = new LogicalTreeNodeCollection(this, logical); } public static LogicalTreeNode[] Create(object control) @@ -17,5 +18,31 @@ namespace Avalonia.Diagnostics.ViewModels var logical = control as ILogical; return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; } + + internal class LogicalTreeNodeCollection : TreeNodeCollection + { + private readonly ILogical _control; + private IDisposable _subscription; + + public LogicalTreeNodeCollection(TreeNode owner, ILogical control) + : base(owner) + { + _control = control; + } + + public override void Dispose() + { + base.Dispose(); + _subscription?.Dispose(); + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscription = _control.LogicalChildren.ForEachItem( + (i, item) => nodes.Insert(i, new LogicalTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index 1d19e1a346..acc3ef16c2 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls; using Avalonia.Diagnostics.Models; using Avalonia.Input; @@ -12,6 +13,7 @@ namespace Avalonia.Diagnostics.ViewModels private readonly TreePageViewModel _logicalTree; private readonly TreePageViewModel _visualTree; private readonly EventsPageViewModel _events; + private readonly IDisposable _pointerOverSubscription; private ViewModelBase _content; private int _selectedTab; private string _focusedControl; @@ -25,16 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels _events = new EventsPageViewModel(root); UpdateFocusedControl(); - KeyboardDevice.Instance.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) - { - UpdateFocusedControl(); - } - }; - + KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged; SelectedTab = 0; - root.GetObservable(TopLevel.PointerOverElementProperty) + _pointerOverSubscription = root.GetObservable(TopLevel.PointerOverElementProperty) .Subscribe(x => PointerOverElement = x?.GetType().Name); Console = new ConsoleViewModel(UpdateConsoleContext); } @@ -129,6 +124,8 @@ namespace Avalonia.Diagnostics.ViewModels public void Dispose() { + KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged; + _pointerOverSubscription.Dispose(); _logicalTree.Dispose(); _visualTree.Dispose(); } @@ -137,5 +134,13 @@ namespace Avalonia.Diagnostics.ViewModels { FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; } + + private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) + { + UpdateFocusedControl(); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index aa27538abc..cb5f5b1fda 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -3,15 +3,15 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; -using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreeNode : ViewModelBase + internal class TreeNode : ViewModelBase, IDisposable { + private IDisposable _classesSubscription; private string _classes; private bool _isExpanded; @@ -33,7 +33,7 @@ namespace Avalonia.Diagnostics.ViewModels x => control.Classes.CollectionChanged -= x) .TakeUntil(removed); - classesChanged.Select(_ => Unit.Default) + _classesSubscription = classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { @@ -49,7 +49,7 @@ namespace Avalonia.Diagnostics.ViewModels } } - public IAvaloniaReadOnlyList Children + public TreeNodeCollection Children { get; protected set; @@ -104,6 +104,12 @@ namespace Avalonia.Diagnostics.ViewModels } } + public void Dispose() + { + _classesSubscription.Dispose(); + Children.Dispose(); + } + private static int IndexOf(IReadOnlyList collection, TreeNode item) { var count = collection.Count; diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs new file mode 100644 index 0000000000..8b4f03bd23 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Avalonia.Collections; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable + { + private AvaloniaList _inner; + + public TreeNodeCollection(TreeNode owner) => Owner = owner; + + public TreeNode this[int index] + { + get + { + EnsureInitialized(); + return _inner[index]; + } + } + + public int Count + { + get + { + EnsureInitialized(); + return _inner.Count; + } + } + + protected TreeNode Owner { get; } + + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => _inner.CollectionChanged += value; + remove => _inner.CollectionChanged -= value; + } + + public event PropertyChangedEventHandler PropertyChanged + { + add => _inner.PropertyChanged += value; + remove => _inner.PropertyChanged -= value; + } + + public virtual void Dispose() + { + if (_inner is object) + { + foreach (var node in _inner) + { + node.Dispose(); + } + } + } + + public IEnumerator GetEnumerator() + { + EnsureInitialized(); + return _inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + protected abstract void Initialize(AvaloniaList nodes); + + private void EnsureInitialized() + { + if (_inner is null) + { + _inner = new AvaloniaList(); + Initialize(_inner); + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index 38ac88a83c..ec48cff399 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -62,7 +62,15 @@ namespace Avalonia.Diagnostics.ViewModels } } - public void Dispose() => _details?.Dispose(); + public void Dispose() + { + foreach (var node in Nodes) + { + node.Dispose(); + } + + _details?.Dispose(); + } public TreeNode FindNode(IControl control) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 5383cb2b68..bc40edf477 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; using Avalonia.Styling; using Avalonia.VisualTree; @@ -9,16 +10,7 @@ namespace Avalonia.Diagnostics.ViewModels public VisualTreeNode(IVisual visual, TreeNode parent) : base(visual, parent) { - var host = visual as IVisualTreeHost; - - if (host?.Root == null) - { - Children = visual.VisualChildren.CreateDerivedList(x => new VisualTreeNode(x, this)); - } - else - { - Children = new AvaloniaList(new[] { new VisualTreeNode(host.Root, this) }); - } + Children = new VisualTreeNodeCollection(this, visual); if ((Visual is IStyleable styleable)) { @@ -33,5 +25,30 @@ namespace Avalonia.Diagnostics.ViewModels var visual = control as IVisual; return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; } + + internal class VisualTreeNodeCollection : TreeNodeCollection + { + private readonly IVisual _control; + private IDisposable _subscription; + + public VisualTreeNodeCollection(TreeNode owner, IVisual control) + : base(owner) + { + _control = control; + } + + public override void Dispose() + { + _subscription?.Dispose(); + } + + protected override void Initialize(AvaloniaList nodes) + { + _subscription = _control.VisualChildren.ForEachItem( + (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)), + (i, item) => nodes.RemoveAt(i), + () => nodes.Clear()); + } + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs index 3abdb5034a..10861538ae 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -14,8 +14,8 @@ namespace Avalonia.Diagnostics.Views { internal class MainWindow : Window, IStyleHost { + private readonly IDisposable _keySubscription; private TopLevel _root; - private IDisposable _keySubscription; public MainWindow() { @@ -33,8 +33,22 @@ namespace Avalonia.Diagnostics.Views { if (_root != value) { + if (_root != null) + { + _root.Closed -= RootClosed; + } + _root = value; - DataContext = new MainViewModel(value); + + if (_root != null) + { + _root.Closed += RootClosed; + DataContext = new MainViewModel(value); + } + else + { + DataContext = null; + } } } } @@ -45,6 +59,8 @@ namespace Avalonia.Diagnostics.Views { base.OnClosed(e); _keySubscription.Dispose(); + _root.Closed -= RootClosed; + _root = null; ((MainViewModel)DataContext)?.Dispose(); } @@ -70,5 +86,7 @@ namespace Avalonia.Diagnostics.Views } } } + + private void RootClosed(object sender, EventArgs e) => Close(); } } diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index d3c2f0c784..43a0752bc8 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -1,4 +1,12 @@ + + + + + + + + + + + 0 + 2 + 1 + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/ProgressBar.xaml index d3c2f0c784..ac5dd7c0ed 100644 --- a/src/Avalonia.Themes.Fluent/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/ProgressBar.xaml @@ -1,81 +1,176 @@ - + + + + + + + + + - + + + + + + + + - + + + + diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml index cf6f32f9bc..47ad494bbf 100644 --- a/src/Avalonia.Themes.Fluent/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml @@ -1,11 +1,14 @@ - + + @@ -19,7 +22,7 @@ @@ -34,6 +37,10 @@ + + 320 + +