diff --git a/.ncrunch/NativeEmbedSample.v3.ncrunchproject b/.ncrunch/NativeEmbedSample.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/NativeEmbedSample.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index 13ae06d2e6..4ab647a25e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -201,9 +201,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject @@ -215,8 +215,8 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5 - src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 - src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 5 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5 src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 1cf3bc75b0..6800ff7d68 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -278,6 +278,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT GetWindowState(AvnWindowState*ret) = 0; + virtual HRESULT TakeFocusFromChildren() = 0; }; AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown @@ -493,8 +494,8 @@ AVNCOM(IAvnNativeControlHostTopLevelAttachment, 21) : IUnknown virtual void* GetParentHandle() = 0; virtual HRESULT InitializeWithChildHandle(void* child) = 0; virtual HRESULT AttachTo(IAvnNativeControlHost* host) = 0; - virtual void MoveTo(float x, float y, float width, float height) = 0; - virtual void Hide() = 0; + virtual void ShowInBounds(float x, float y, float width, float height) = 0; + virtual void HideWithSize(float width, float height) = 0; virtual void ReleaseChild() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm index 315ec2f310..5ee2344ac7 100644 --- a/native/Avalonia.Native/src/OSX/controlhost.mm +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -97,7 +97,7 @@ public: return S_OK; }; - virtual void MoveTo(float x, float y, float width, float height) override + virtual void ShowInBounds(float x, float y, float width, float height) override { if(_child == nil) return; @@ -106,7 +106,7 @@ public: IAvnNativeControlHostTopLevelAttachment* slf = this; slf->AddRef(); dispatch_async(dispatch_get_main_queue(), ^{ - slf->MoveTo(x, y, width, height); + slf->ShowInBounds(x, y, width, height); slf->Release(); }); return; @@ -122,9 +122,24 @@ public: [[_holder superview] setNeedsDisplay:true]; } - virtual void Hide() override + virtual void HideWithSize(float width, float height) override { + if(_child == nil) + return; + if(AvnInsidePotentialDeadlock::IsInside()) + { + IAvnNativeControlHostTopLevelAttachment* slf = this; + slf->AddRef(); + dispatch_async(dispatch_get_main_queue(), ^{ + slf->HideWithSize(width, height); + slf->Release(); + }); + return; + } + + NSRect frame = {0, 0, width, height}; [_holder setHidden: true]; + [_child setFrame: frame]; } virtual void ReleaseChild() override diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 86b3584681..7f8a6e1393 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -116,10 +116,15 @@ public: { SetPosition(lastPositionSet); UpdateStyle(); - - [Window makeKeyAndOrderFront:Window]; - [NSApp activateIgnoringOtherApps:YES]; - + if(ShouldTakeFocusOnShow()) + { + [Window makeKeyAndOrderFront:Window]; + [NSApp activateIgnoringOtherApps:YES]; + } + else + { + [Window orderFront: Window]; + } [Window setTitle:_lastTitle]; _shown = true; @@ -128,6 +133,11 @@ public: } } + virtual bool ShouldTakeFocusOnShow() + { + return true; + } + virtual HRESULT Hide () override { @autoreleasepool @@ -774,6 +784,15 @@ private: } } + virtual HRESULT TakeFocusFromChildren () override + { + if(Window == nil) + return S_OK; + if([Window isKeyWindow]) + [Window makeFirstResponder: View]; + return S_OK; + } + void EnterFullScreenMode () { _fullScreenActive = true; @@ -1858,7 +1877,6 @@ private: WindowEvents = events; [Window setLevel:NSPopUpMenuWindowLevel]; } - protected: virtual NSWindowStyleMask GetStyle() override { @@ -1876,6 +1894,11 @@ protected: return S_OK; } } +public: + virtual bool ShouldTakeFocusOnShow() override + { + return false; + } }; extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) diff --git a/nukebuild/Numerge b/nukebuild/Numerge index 4464343aef..aef10ae67d 160000 --- a/nukebuild/Numerge +++ b/nukebuild/Numerge @@ -1 +1 @@ -Subproject commit 4464343aef5c8ab7a42fcb20a483a6058199f8b8 +Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5 diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 2c5a09bee7..cd3ce9adcd 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -41,6 +41,10 @@ true build\ + + true + build\ + diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props index 30bafa37ee..deea3aa391 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.props +++ b/packages/Avalonia/AvaloniaBuildTasks.props @@ -1,3 +1,11 @@ - + + + + + + + + + diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 537495fcad..84a62bb5c0 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -4,6 +4,20 @@ <_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false low + + + + + %(Filename) + Code + + + %(Filename) + Code + + + + + DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)"> + + + + diff --git a/packages/Avalonia/AvaloniaItemSchema.xaml b/packages/Avalonia/AvaloniaItemSchema.xaml new file mode 100644 index 0000000000..a51ea3c0be --- /dev/null +++ b/packages/Avalonia/AvaloniaItemSchema.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/readme.md b/readme.md index a9263d4816..6a04c7e31e 100644 --- a/readme.md +++ b/readme.md @@ -68,7 +68,7 @@ Avalonia is licenced under the [MIT licence](licence.md). ## Contributors -This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. +This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing)]. ### Backers 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/samples/interop/NativeEmbedSample/MainWindow.xaml b/samples/interop/NativeEmbedSample/MainWindow.xaml index dcec9035e0..f2161a1bea 100644 --- a/samples/interop/NativeEmbedSample/MainWindow.xaml +++ b/samples/interop/NativeEmbedSample/MainWindow.xaml @@ -20,7 +20,16 @@ + + + + Text + + + Tooltip + + 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.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index ebafc8b946..56cde9738e 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -14,7 +14,7 @@ namespace Avalonia.Threading private readonly DispatcherPriority _priority; private TimeSpan _interval; - + /// /// Initializes a new instance of the class. /// @@ -154,6 +154,8 @@ namespace Avalonia.Threading TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) { + interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); + var timer = new DispatcherTimer(priority) { Interval = interval }; timer.Tick += (s, e) => @@ -197,7 +199,7 @@ namespace Avalonia.Threading } } - + /// /// Raises the event on the dispatcher thread. diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 406abe6f99..ae2bf99d1e 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -107,7 +107,7 @@ namespace Avalonia.Build.Tasks foreach (var s in sources.ToList()) { - if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml")) + if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml")) { XamlFileInfo info; try @@ -150,7 +150,7 @@ namespace Avalonia.Build.Tasks BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); - foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml"))) + foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml"))) BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); var resources = BuildResourceSources(); diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 3b69109e68..30e8f120d7 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -25,7 +25,8 @@ namespace Avalonia.Build.Tasks public static partial class XamlCompilerTaskExecutor { static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") - || r.Name.ToLowerInvariant().EndsWith(".paml"); + || r.Name.ToLowerInvariant().EndsWith(".paml") + || r.Name.ToLowerInvariant().EndsWith(".axaml"); public class CompileResult { diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 8a21d7aa4b..5ff0fd1feb 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls.Embedding { EnsureInitialized(); ApplyTemplate(); - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index d326ab5734..b037dd9901 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { EnsureInitialized(); ApplyTemplate(); - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); } private void EnsureInitialized() diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 94ef0b2284..20eac11c2c 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -1,7 +1,9 @@ +using System; +using System.Collections.Generic; using Avalonia.Controls.Platform; -using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Threading; +using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -12,14 +14,18 @@ namespace Avalonia.Controls private INativeControlHostControlTopLevelAttachment _attachment; private IPlatformHandle _nativeControlHandle; private bool _queuedForDestruction; + private bool _queuedForMoveResize; + private readonly List _propertyChangedSubscriptions = new List(); + private readonly EventHandler _propertyChangedHandler; static NativeControlHost() { IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); - TransformedBoundsProperty.Changed.AddClassHandler(OnBoundsChanged); } - private static void OnBoundsChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) - => host.UpdateHost(); + public NativeControlHost() + { + _propertyChangedHandler = PropertyChangedHandler; + } private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) => host.UpdateHost(); @@ -27,21 +33,46 @@ namespace Avalonia.Controls protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = e.Root as TopLevel; + var visual = (IVisual)this; + while (visual != _currentRoot) + { + + if (visual is Visual v) + { + v.PropertyChanged += _propertyChangedHandler; + _propertyChangedSubscriptions.Add(v); + } + + visual = visual.GetVisualParent(); + } + UpdateHost(); } + private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.IsEffectiveValueChange && e.Property == BoundsProperty) + EnqueueForMoveResize(); + } + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = null; + if (_propertyChangedSubscriptions != null) + { + foreach (var v in _propertyChangedSubscriptions) + v.PropertyChanged -= _propertyChangedHandler; + _propertyChangedSubscriptions.Clear(); + } UpdateHost(); } - void UpdateHost() + private void UpdateHost() { + _queuedForMoveResize = false; _currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost; var needsAttachment = _currentHost != null; - var needsShow = needsAttachment && IsEffectivelyVisible && TransformedBounds.HasValue; if (needsAttachment) { @@ -93,22 +124,46 @@ namespace Avalonia.Controls } } - if (needsShow) - _attachment?.ShowInBounds(TransformedBounds.Value); - else if (needsAttachment) - _attachment?.Hide(); + if (_attachment?.AttachedTo != _currentHost) + return; + + TryUpdateNativeControlPosition(); + } + + + private Rect? GetAbsoluteBounds() + { + var bounds = Bounds; + var position = this.TranslatePoint(bounds.Position, _currentRoot); + if (position == null) + return null; + return new Rect(position.Value, bounds.Size); + } + + void EnqueueForMoveResize() + { + if(_queuedForMoveResize) + return; + _queuedForMoveResize = true; + Dispatcher.UIThread.Post(UpdateHost, DispatcherPriority.Render); } public bool TryUpdateNativeControlPosition() { - var needsShow = _currentHost != null && IsEffectivelyVisible && TransformedBounds.HasValue; + if (_currentHost == null) + return false; + + var bounds = GetAbsoluteBounds(); + var needsShow = IsEffectivelyVisible && bounds.HasValue; - if(needsShow) - _attachment?.ShowInBounds(TransformedBounds.Value); - return needsShow; + if (needsShow) + _attachment?.ShowInBounds(bounds.Value); + else + _attachment?.HideWithSize(Bounds.Size); + return false; } - void CheckDestruction() + private void CheckDestruction() { _queuedForDestruction = false; if (_currentRoot == null) @@ -117,10 +172,12 @@ namespace Avalonia.Controls protected virtual IPlatformHandle CreateNativeControlCore(IPlatformHandle parent) { + if (_currentHost == null) + throw new InvalidOperationException(); return _currentHost.CreateDefaultChild(parent); } - void DestroyNativeControl() + private void DestroyNativeControl() { if (_nativeControlHandle != null) { diff --git a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs index 7a4568abc6..c6b1d09849 100644 --- a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs +++ b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs @@ -21,8 +21,8 @@ namespace Avalonia.Controls.Platform { INativeControlHostImpl AttachedTo { get; set; } bool IsCompatibleWith(INativeControlHostImpl host); - void Hide(); - void ShowInBounds(TransformedBounds transformedBounds); + void HideWithSize(Size size); + void ShowInBounds(Rect rect); } public interface ITopLevelImplWithNativeControlHost 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/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index b705a518ff..0d22187b34 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -49,8 +49,8 @@ namespace Avalonia.Controls // For non-virtualizing layouts, we do not need to keep // updating viewports and invalidating measure often. So when // a non virtualizing layout is used, we stop doing all that work. - bool _managingViewportDisabled; - private IDisposable _effectiveViewportChangedRevoker; + private bool _managingViewportDisabled; + private bool _effectiveViewportChangedSubscribed; private bool _layoutUpdatedSubscribed; public ViewportManager(ItemsRepeater owner) @@ -228,11 +228,15 @@ namespace Avalonia.Controls _pendingViewportShift = default; _unshiftableShift = default; - _effectiveViewportChangedRevoker?.Dispose(); - - if (!_managingViewportDisabled) + if (_managingViewportDisabled && _effectiveViewportChangedSubscribed) { - _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + _owner.EffectiveViewportChanged -= OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = false; + } + else if (!_managingViewportDisabled && !_effectiveViewportChangedSubscribed) + { + _owner.EffectiveViewportChanged += OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = true; } } @@ -340,6 +344,11 @@ namespace Avalonia.Controls // Note that the element being brought into view could be a descendant. var targetChild = GetImmediateChildOfRepeater((IControl)args.TargetObject); + if (targetChild is null) + { + return; + } + // Make sure that only the target child can be the anchor during the bring into view operation. foreach (var child in _owner.Children) { @@ -373,7 +382,7 @@ namespace Avalonia.Controls if (parent == null) { - throw new InvalidOperationException("OnBringIntoViewRequested called with args.target element not under the ItemsRepeater that recieved the call"); + return null; } return targetChild; @@ -415,15 +424,15 @@ namespace Avalonia.Controls _scroller = null; } - _effectiveViewportChangedRevoker?.Dispose(); - _effectiveViewportChangedRevoker = null; + _owner.EffectiveViewportChanged -= OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = false; _ensuredScroller = false; } - private void OnEffectiveViewportChanged(Rect effectiveViewport) + private void OnEffectiveViewportChanged(object sender, EffectiveViewportChangedEventArgs e) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId); - UpdateViewport(effectiveViewport); + UpdateViewport(e.EffectiveViewport); _pendingViewportShift = default; _unshiftableShift = default; @@ -468,8 +477,8 @@ namespace Avalonia.Controls } else if (!_managingViewportDisabled) { - _effectiveViewportChangedRevoker?.Dispose(); - _effectiveViewportChangedRevoker = SubscribeToEffectiveViewportChanged(_owner); + _owner.EffectiveViewportChanged += OnEffectiveViewportChanged; + _effectiveViewportChangedSubscribed = true; } _ensuredScroller = true; @@ -529,38 +538,6 @@ namespace Avalonia.Controls } } - private IDisposable SubscribeToEffectiveViewportChanged(IControl control) - { - // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - - // we can get this from TransformedBounds, but this property is updated after layout has - // run, which is too late. Instead, for now lets just hook into an internal event on - // ScrollContentPresenter to find out what the offset and viewport will be after arrange - // and use those values. Note that this doesn't handle nested ScrollViewers at all, but - // it's enough to get scrolling to non-uniformly sized items working for now. - // - // UWP uses the EffectiveViewportChanged event (which I think was implemented specially - // for this case): we need to implement that in Avalonia, but the semantics of it aren't - // clear to me. Hopefully the source for this event will be released with WinUI 3. - if (control.VisualParent is ScrollContentPresenter scp) - { - scp.PreArrange += ScrollContentPresenterPreArrange; - return Disposable.Create(() => scp.PreArrange -= ScrollContentPresenterPreArrange); - } - - return Disposable.Empty; - } - - private void ScrollContentPresenterPreArrange(object sender, VectorEventArgs e) - { - var scp = (ScrollContentPresenter)sender; - var effectiveViewport = new Rect((Point)scp.Offset, new Size(e.Vector.X, e.Vector.Y)); - - if (effectiveViewport != _visibleWindow) - { - OnEffectiveViewportChanged(effectiveViewport); - } - } - private class ScrollerInfo { public ScrollerInfo(ScrollViewer scroller) diff --git a/src/Avalonia.Controls/SelectionModel.cs b/src/Avalonia.Controls/SelectionModel.cs index ff1c0260bb..aa6552579f 100644 --- a/src/Avalonia.Controls/SelectionModel.cs +++ b/src/Avalonia.Controls/SelectionModel.cs @@ -189,8 +189,6 @@ namespace Avalonia.Controls } set { - var isSelected = IsSelectedWithPartialAt(value); - if (!IsSelectedAt(value) || SelectedItems.Count > 1) { using var operation = new Operation(this); diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index c5491746a3..f058942116 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -318,7 +318,7 @@ namespace Avalonia.Controls /// /// Creates the layout manager for this . /// - protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(); + protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); /// /// Handles a paint notification from . @@ -340,6 +340,9 @@ namespace Avalonia.Controls _globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved; } + Renderer?.Dispose(); + Renderer = null; + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); @@ -349,8 +352,8 @@ namespace Avalonia.Controls (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; OnClosed(EventArgs.Empty); - Renderer?.Dispose(); - Renderer = null; + + LayoutManager?.Dispose(); } /// 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.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ff7cc41e3b..cedd20ace5 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -519,7 +519,7 @@ namespace Avalonia.Controls } } - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); using (BeginAutoSizing()) { @@ -592,7 +592,7 @@ namespace Avalonia.Controls } } - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); var result = new TaskCompletionSource(); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index afc01db506..eb6e7319f5 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -162,7 +162,7 @@ namespace Avalonia.Controls if (!_hasExecutedInitialLayoutPass) { - LayoutManager.ExecuteInitialLayoutPass(this); + LayoutManager.ExecuteInitialLayoutPass(); _hasExecutedInitialLayoutPass = true; } PlatformImpl?.Show(); 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.Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs new file mode 100644 index 0000000000..1cdc775b13 --- /dev/null +++ b/src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Layout +{ + /// + /// Provides data for the event. + /// + public class EffectiveViewportChangedEventArgs : EventArgs + { + public EffectiveViewportChangedEventArgs(Rect effectiveViewport) + { + EffectiveViewport = effectiveViewport; + } + + /// + /// Gets the representing the effective viewport. + /// + /// + /// The viewport is expressed in coordinates relative to the control that the event is + /// raised on. + /// + public Rect EffectiveViewport { get; } + } +} diff --git a/src/Avalonia.Layout/ILayoutManager.cs b/src/Avalonia.Layout/ILayoutManager.cs index 6e63d3edbb..614670a53b 100644 --- a/src/Avalonia.Layout/ILayoutManager.cs +++ b/src/Avalonia.Layout/ILayoutManager.cs @@ -7,7 +7,7 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - public interface ILayoutManager + public interface ILayoutManager : IDisposable { /// /// Raised when the layout manager completes a layout pass. @@ -35,6 +35,15 @@ namespace Avalonia.Layout /// void ExecuteLayoutPass(); + /// + /// Executes the initial layout pass on a layout root. + /// + /// + /// You should not usually need to call this method explictly, the layout root will call + /// it to carry out the initial layout of the control. + /// + void ExecuteInitialLayoutPass(); + /// /// Executes the initial layout pass on a layout root. /// @@ -43,6 +52,19 @@ namespace Avalonia.Layout /// You should not usually need to call this method explictly, the layout root will call /// it to carry out the initial layout of the control. /// + [Obsolete("Call ExecuteInitialLayoutPass without parameter")] void ExecuteInitialLayoutPass(ILayoutRoot root); + + /// + /// Registers a control as wanting to receive effective viewport notifications. + /// + /// The control. + void RegisterEffectiveViewportListener(ILayoutable control); + + /// + /// Registers a control as no longer wanting to receive effective viewport notifications. + /// + /// The control. + void UnregisterEffectiveViewportListener(ILayoutable control); } } diff --git a/src/Avalonia.Layout/ILayoutable.cs b/src/Avalonia.Layout/ILayoutable.cs index 316a017f1d..54d3ba6a11 100644 --- a/src/Avalonia.Layout/ILayoutable.cs +++ b/src/Avalonia.Layout/ILayoutable.cs @@ -111,5 +111,12 @@ namespace Avalonia.Layout /// /// The child control. void ChildDesiredSizeChanged(ILayoutable control); + + /// + /// Used by the to notify the control that its effective + /// viewport is changed. + /// + /// The viewport information. + void EffectiveViewportChanged(EffectiveViewportChangedEventArgs e); } } diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index aefb319fd0..fc988a8d6c 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -1,7 +1,10 @@ using System; +using System.Buffers; +using System.Collections.Generic; using System.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; +using Avalonia.VisualTree; #nullable enable @@ -10,16 +13,21 @@ namespace Avalonia.Layout /// /// Manages measuring and arranging of controls. /// - public class LayoutManager : ILayoutManager + public class LayoutManager : ILayoutManager, IDisposable { + private const int MaxPasses = 3; + private readonly ILayoutRoot _owner; private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid); private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid); private readonly Action _executeLayoutPass; + private List? _effectiveViewportChangedListeners; + private bool _disposed; private bool _queued; private bool _running; - public LayoutManager() + public LayoutManager(ILayoutRoot owner) { + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); _executeLayoutPass = ExecuteLayoutPass; } @@ -31,6 +39,11 @@ namespace Avalonia.Layout control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -41,6 +54,11 @@ namespace Avalonia.Layout #endif } + if (control.VisualRoot != _owner) + { + throw new ArgumentException("Attempt to call InvalidateMeasure on wrong LayoutManager."); + } + _toMeasure.Enqueue(control); _toArrange.Enqueue(control); QueueLayoutPass(); @@ -52,6 +70,11 @@ namespace Avalonia.Layout control = control ?? throw new ArgumentNullException(nameof(control)); Dispatcher.UIThread.VerifyAccess(); + if (_disposed) + { + return; + } + if (!control.IsAttachedToVisualTree) { #if DEBUG @@ -62,6 +85,11 @@ namespace Avalonia.Layout #endif } + if (control.VisualRoot != _owner) + { + throw new ArgumentException("Attempt to call InvalidateArrange on wrong LayoutManager."); + } + _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -69,14 +97,15 @@ namespace Avalonia.Layout /// public virtual void ExecuteLayoutPass() { - const int MaxPasses = 3; - Dispatcher.UIThread.VerifyAccess(); - if (!_running) + if (_disposed) { - _running = true; + return; + } + if (!_running) + { Stopwatch? stopwatch = null; const LogEventLevel timingLogLevel = LogEventLevel.Information; @@ -99,12 +128,13 @@ namespace Avalonia.Layout try { + _running = true; + for (var pass = 0; pass < MaxPasses; ++pass) { - ExecuteMeasurePass(); - ExecuteArrangePass(); + InnerLayoutPass(); - if (_toMeasure.Count == 0) + if (!RaiseEffectiveViewportChanged()) { break; } @@ -131,13 +161,18 @@ namespace Avalonia.Layout } /// - public virtual void ExecuteInitialLayoutPass(ILayoutRoot root) + public virtual void ExecuteInitialLayoutPass() { + if (_disposed) + { + return; + } + try { _running = true; - Measure(root); - Arrange(root); + Measure(_owner); + Arrange(_owner); } finally { @@ -151,6 +186,60 @@ namespace Avalonia.Layout ExecuteLayoutPass(); } + [Obsolete("Call ExecuteInitialLayoutPass without parameter")] + public void ExecuteInitialLayoutPass(ILayoutRoot root) + { + if (root != _owner) + { + throw new ArgumentException("ExecuteInitialLayoutPass called with incorrect root."); + } + + ExecuteInitialLayoutPass(); + } + + public void Dispose() + { + _disposed = true; + _toMeasure.Dispose(); + _toArrange.Dispose(); + } + + void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control) + { + _effectiveViewportChangedListeners ??= new List(); + _effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener( + control, + CalculateEffectiveViewport(control))); + } + + void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control) + { + if (_effectiveViewportChangedListeners is object) + { + for (var i = _effectiveViewportChangedListeners.Count - 1; i >= 0; --i) + { + if (_effectiveViewportChangedListeners[i].Listener == control) + { + _effectiveViewportChangedListeners.RemoveAt(i); + } + } + } + } + + private void InnerLayoutPass() + { + for (var pass = 0; pass < MaxPasses; ++pass) + { + ExecuteMeasurePass(); + ExecuteArrangePass(); + + if (_toMeasure.Count == 0) + { + break; + } + } + } + private void ExecuteMeasurePass() { while (_toMeasure.Count > 0) @@ -234,5 +323,97 @@ namespace Avalonia.Layout _queued = true; } } + + private bool RaiseEffectiveViewportChanged() + { + var startCount = _toMeasure.Count + _toArrange.Count; + + if (_effectiveViewportChangedListeners is object) + { + var count = _effectiveViewportChangedListeners.Count; + var pool = ArrayPool.Shared; + var listeners = pool.Rent(count); + + _effectiveViewportChangedListeners.CopyTo(listeners); + + try + { + for (var i = 0; i < count; ++i) + { + var l = _effectiveViewportChangedListeners[i]; + + if (!l.Listener.IsAttachedToVisualTree) + { + continue; + } + + var viewport = CalculateEffectiveViewport(l.Listener); + + if (viewport != l.Viewport) + { + l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport)); + _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport); + } + } + } + finally + { + pool.Return(listeners, clearArray: true); + } + } + + return startCount != _toMeasure.Count + _toArrange.Count; + } + + private Rect CalculateEffectiveViewport(IVisual control) + { + var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity); + CalculateEffectiveViewport(control, control, ref viewport); + return viewport; + } + + private void CalculateEffectiveViewport(IVisual target, IVisual control, ref Rect viewport) + { + // Recurse until the top level control. + if (control.VisualParent is object) + { + CalculateEffectiveViewport(target, control.VisualParent, ref viewport); + } + else + { + viewport = new Rect(control.Bounds.Size); + } + + // Apply the control clip bounds if it's not the target control. We don't apply it to + // the target control because it may itself be clipped to bounds and if so the viewport + // we calculate would be of no use. + if (control != target && control.ClipToBounds) + { + viewport = control.Bounds.Intersect(viewport); + } + + // Translate the viewport into this control's coordinate space. + viewport = viewport.Translate(-control.Bounds.Position); + + if (control != target && control.RenderTransform is object) + { + var origin = control.RenderTransformOrigin.ToPixels(control.Bounds.Size); + var offset = Matrix.CreateTranslation(origin); + var renderTransform = (-offset) * control.RenderTransform.Value.Invert() * (offset); + viewport = viewport.TransformToAABB(renderTransform); + } + } + + private readonly struct EffectiveViewportChangedListener + { + public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport) + { + Listener = listener; + Viewport = viewport; + } + + public ILayoutable Listener { get; } + public Rect Viewport { get; } + } } } diff --git a/src/Avalonia.Layout/LayoutQueue.cs b/src/Avalonia.Layout/LayoutQueue.cs index e261a1b48e..1a9eb6b785 100644 --- a/src/Avalonia.Layout/LayoutQueue.cs +++ b/src/Avalonia.Layout/LayoutQueue.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Avalonia.Layout { - internal class LayoutQueue : IReadOnlyCollection + internal class LayoutQueue : IReadOnlyCollection, IDisposable { private struct Info { @@ -84,5 +84,12 @@ namespace Avalonia.Layout _notFinalizedBuffer.Clear(); } + + public void Dispose() + { + _inner.Clear(); + _loopQueueInfo.Clear(); + _notFinalizedBuffer.Clear(); + } } } diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 8d2a825fa0..e62e22f8ec 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -132,6 +132,7 @@ namespace Avalonia.Layout private bool _measuring; private Size? _previousMeasure; private Rect? _previousArrange; + private EventHandler? _effectiveViewportChanged; private EventHandler? _layoutUpdated; /// @@ -152,6 +153,32 @@ namespace Avalonia.Layout VerticalAlignmentProperty); } + /// + /// Occurs when the element's effective viewport changes. + /// + public event EventHandler? EffectiveViewportChanged + { + add + { + if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.RegisterEffectiveViewportListener(this); + } + + _effectiveViewportChanged += value; + } + + remove + { + _effectiveViewportChanged -= value; + + if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r) + { + r.LayoutManager.UnregisterEffectiveViewportListener(this); + } + } + } + /// /// Occurs when a layout pass completes for the control. /// @@ -384,13 +411,6 @@ namespace Avalonia.Layout } } - /// - /// Called by InvalidateMeasure - /// - protected virtual void OnMeasureInvalidated() - { - } - /// /// Invalidates the measurement of the control and queues a new layout pass. /// @@ -436,6 +456,11 @@ namespace Avalonia.Layout } } + void ILayoutable.EffectiveViewportChanged(EffectiveViewportChangedEventArgs e) + { + _effectiveViewportChanged?.Invoke(this, e); + } + /// /// Marks a property as affecting the control's measurement. /// @@ -717,9 +742,17 @@ namespace Avalonia.Layout { base.OnAttachedToVisualTreeCore(e); - if (_layoutUpdated is object && e.Root is ILayoutRoot r) + if (e.Root is ILayoutRoot r) { - r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; + if (_layoutUpdated is object) + { + r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated; + } + + if (_effectiveViewportChanged is object) + { + r.LayoutManager.RegisterEffectiveViewportListener(this); + } } } @@ -727,12 +760,27 @@ namespace Avalonia.Layout { base.OnDetachedFromVisualTreeCore(e); - if (_layoutUpdated is object && e.Root is ILayoutRoot r) + if (e.Root is ILayoutRoot r) { - r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; + if (_layoutUpdated is object) + { + r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated; + } + + if (_effectiveViewportChanged is object) + { + r.LayoutManager.UnregisterEffectiveViewportListener(this); + } } } + /// + /// Called by InvalidateMeasure + /// + protected virtual void OnMeasureInvalidated() + { + } + /// protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) { diff --git a/src/Avalonia.Native/NativeControlHostImpl.cs b/src/Avalonia.Native/NativeControlHostImpl.cs index 0777c6416b..a46528dc48 100644 --- a/src/Avalonia.Native/NativeControlHostImpl.cs +++ b/src/Avalonia.Native/NativeControlHostImpl.cs @@ -114,19 +114,18 @@ namespace Avalonia.Native public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostImpl; - public void Hide() + public void HideWithSize(Size size) { - _native?.Hide(); + _native.HideWithSize(Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height)); } - public void ShowInBounds(TransformedBounds transformedBounds) + public void ShowInBounds(Rect bounds) { if (_attachedTo == null) throw new InvalidOperationException("Native control isn't attached to a toplevel"); - var bounds = transformedBounds.Bounds.TransformToAABB(transformedBounds.Transform); bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), Math.Max(1, bounds.Height)); - _native.MoveTo((float) bounds.X, (float) bounds.Y, (float) bounds.Width, (float) bounds.Height); + _native.ShowInBounds((float) bounds.X, (float) bounds.Y, (float) bounds.Width, (float) bounds.Height); } public void InitWithChild(IPlatformHandle handle) diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index c41be1723b..b0da5fdc43 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -10,6 +10,7 @@ namespace Avalonia.Native private readonly IAvaloniaNativeFactory _factory; private readonly AvaloniaNativePlatformOptions _opts; private readonly GlPlatformFeature _glFeature; + private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, @@ -19,6 +20,7 @@ namespace Avalonia.Native _factory = factory; _opts = opts; _glFeature = glFeature; + _parent = parent; using (var e = new PopupEvents(this)) { var context = _opts.UseGpu ? glFeature?.DeferredContext : null; @@ -58,6 +60,16 @@ namespace Avalonia.Native } } + public override void Show() + { + var parent = _parent; + while (parent is PopupImpl p) + parent = p._parent; + if (parent is WindowImpl w) + w.Native.TakeFocusFromChildren(); + base.Show(); + } + public override IPopupImpl CreatePopup() => new PopupImpl(_factory, _opts, _glFeature, this); public void SetWindowManagerAddShadowHint(bool enabled) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 930b5800ba..9a90f65d1b 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -319,7 +319,7 @@ namespace Avalonia.Native } - public void Show() + public virtual void Show() { _native.Show(); } 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 @@ + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml index 2d5e369573..f20dc2e650 100644 --- a/src/Avalonia.Themes.Fluent/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Fluent/FocusAdorner.xaml @@ -1,10 +1,34 @@ - + + + 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 + +