From 1eee17345bf2958f0179794a4b21fb9d15469196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Thu, 6 Jul 2017 17:45:21 +0100 Subject: [PATCH 01/21] Implemented Window.WindowStartupLocation and WindowBase.Owner. --- src/Avalonia.Controls/Window.cs | 28 +++++++ src/Avalonia.Controls/WindowBase.cs | 15 ++++ .../WindowStartupLocation.cs | 23 ++++++ src/Avalonia.DotNetCoreRuntime/AppBuilder.cs | 3 + .../WindowTests.cs | 77 ++++++++++++++++++- 5 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Controls/WindowStartupLocation.cs diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3802f2b6ea..3535510ce3 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -183,6 +183,15 @@ namespace Avalonia.Controls set { SetValue(IconProperty, value); } } + /// + /// Gets or sets the startup location of the window. + /// + public WindowStartupLocation WindowStartupLocation + { + get; + set; + } + /// Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; @@ -246,6 +255,7 @@ namespace Avalonia.Controls s_windows.Add(this); EnsureInitialized(); + SetWindowStartupLocation(); IsVisible = true; LayoutManager.Instance.ExecuteInitialLayoutPass(this); @@ -285,6 +295,7 @@ namespace Avalonia.Controls s_windows.Add(this); EnsureInitialized(); + SetWindowStartupLocation(); IsVisible = true; LayoutManager.Instance.ExecuteInitialLayoutPass(this); @@ -321,6 +332,23 @@ namespace Avalonia.Controls } } + void SetWindowStartupLocation() + { + if (WindowStartupLocation == WindowStartupLocation.CenterScreen) + { + var positionAsSize = PlatformImpl.MaxClientSize / 2 - ClientSize / 2; + Position = new Point(positionAsSize.Width, positionAsSize.Height); + } + else if (WindowStartupLocation == WindowStartupLocation.CenterOwner) + { + if (Owner != null) + { + var positionAsSize = Owner.ClientSize / 2 - ClientSize / 2; + Position = Owner.Position + new Point(positionAsSize.Width, positionAsSize.Height); + } + } + } + /// void INameScope.Register(string name, object element) { diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index fbdf64b14a..dd1e8fbef1 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -29,6 +29,12 @@ namespace Avalonia.Controls public static readonly DirectProperty IsActiveProperty = AvaloniaProperty.RegisterDirect(nameof(IsActive), o => o.IsActive); + /// + /// Defines the property. + /// + public static readonly StyledProperty OwnerProperty = + AvaloniaProperty.Register(nameof(Owner)); + private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; @@ -100,6 +106,15 @@ namespace Avalonia.Controls private set; } + /// + /// Gets or sets the owner of the window. + /// + public WindowBase Owner + { + get { return GetValue(OwnerProperty); } + set { SetValue(OwnerProperty, value); } + } + /// /// Activates the window. /// diff --git a/src/Avalonia.Controls/WindowStartupLocation.cs b/src/Avalonia.Controls/WindowStartupLocation.cs new file mode 100644 index 0000000000..1818636076 --- /dev/null +++ b/src/Avalonia.Controls/WindowStartupLocation.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls +{ + /// + /// Determines the startup location of the window. + /// + public enum WindowStartupLocation + { + /// + /// The startup location is defined by the Position property. + /// + Manual, + + /// + /// The startup location is the center of the screen. + /// + CenterScreen, + + /// + /// The startup location is the center of the owner window. If the owner window is not specified, the startup location will be . + /// + CenterOwner + } +} diff --git a/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs b/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs index 2b9b3083b1..bf8d7a20fd 100644 --- a/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs +++ b/src/Avalonia.DotNetCoreRuntime/AppBuilder.cs @@ -10,6 +10,9 @@ using Avalonia.Shared.PlatformSupport; namespace Avalonia { + /// + /// Initializes platform-specific services for an . + /// public sealed class AppBuilder : AppBuilderBase { /// diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e0dd908bbb..83f86ce5a0 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void IsVisible_Should_Be_False_Atfer_Hide() + public void IsVisible_Should_Be_False_After_Hide() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -82,7 +82,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void IsVisible_Should_Be_False_Atfer_Close() + public void IsVisible_Should_Be_False_After_Close() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -96,7 +96,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close() + public void IsVisible_Should_Be_False_After_Impl_Signals_Close() { var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); @@ -191,5 +191,76 @@ namespace Avalonia.Controls.UnitTests // AvaloniaLocator scopes. ((IList)Window.OpenWindows).Clear(); } + + [Fact] + public void Window_Should_Be_Centered_When_Window_Startup_Location_Is_Center_Screen() + { + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Position); + windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); + windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); + windowImpl.Setup(x => x.Scaling).Returns(1); + + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + window.WindowStartupLocation = WindowStartupLocation.CenterScreen; + window.Position = new Point(60, 40); + + window.Show(); + + var expectedPosition = new Point( + window.PlatformImpl.MaxClientSize.Width / 2 - window.ClientSize.Width / 2, + window.PlatformImpl.MaxClientSize.Height / 2 - window.ClientSize.Height / 2); + + Assert.Equal(window.Position, expectedPosition); + } + } + + [Fact] + public void Window_Should_Be_Centered_Relative_To_Owner_When_Window_Startup_Location_Is_Center_Owner() + { + var parentWindowImpl = new Mock(); + parentWindowImpl.SetupProperty(x => x.Position); + parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); + parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); + parentWindowImpl.Setup(x => x.Scaling).Returns(1); + + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Position); + windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); + windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); + windowImpl.Setup(x => x.Scaling).Returns(1); + + var parentWindowServices = TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform(() => parentWindowImpl.Object)); + + var windowServices = TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); + + using (UnitTestApplication.Start(parentWindowServices)) + { + var parentWindow = new Window(); + parentWindow.Position = new Point(60, 40); + + parentWindow.Show(); + + using (UnitTestApplication.Start(windowServices)) + { + var window = new Window(); + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + window.Position = new Point(60, 40); + window.Owner = parentWindow; + + window.Show(); + + var expectedPosition = new Point( + parentWindow.Position.X + parentWindow.ClientSize.Width / 2 - window.ClientSize.Width / 2, + parentWindow.Position.Y + parentWindow.ClientSize.Height / 2 - window.ClientSize.Height / 2); + + Assert.Equal(window.Position, expectedPosition); + } + } + } } } From 87e8cb892384127b7be314a7da8678bc1deefe44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 8 Jul 2017 18:14:06 +0100 Subject: [PATCH 02/21] Fixed PossibleNullReferenceException. --- src/Avalonia.Controls/Window.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 3535510ce3..7f0cab3237 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -336,8 +336,11 @@ namespace Avalonia.Controls { if (WindowStartupLocation == WindowStartupLocation.CenterScreen) { - var positionAsSize = PlatformImpl.MaxClientSize / 2 - ClientSize / 2; - Position = new Point(positionAsSize.Width, positionAsSize.Height); + if (PlatformImpl != null) + { + var positionAsSize = PlatformImpl.MaxClientSize / 2 - ClientSize / 2; + Position = new Point(positionAsSize.Width, positionAsSize.Height); + } } else if (WindowStartupLocation == WindowStartupLocation.CenterOwner) { From e54d0c75d1c64f26be1aa6e67fede778729e0040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 9 Jul 2017 14:03:22 +0100 Subject: [PATCH 03/21] Property changes. --- src/Avalonia.Controls/Window.cs | 13 +++++++++++-- src/Avalonia.Controls/WindowBase.cs | 9 +++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 7f0cab3237..f60768980d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -78,9 +78,16 @@ namespace Avalonia.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + /// + /// Defines the proeprty. + /// + public static readonly DirectProperty WindowStartupLocationProperty = + AvaloniaProperty.RegisterDirect(nameof(WindowStartupLocation), o => o.WindowStartupLocation); + private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; private readonly Size _maxPlatformClientSize; + private WindowStartupLocation _windowStartupLoction; /// /// Initializes static members of the class. @@ -188,8 +195,8 @@ namespace Avalonia.Controls /// public WindowStartupLocation WindowStartupLocation { - get; - set; + get { return _windowStartupLoction; } + set { SetAndRaise(WindowStartupLocationProperty, ref _windowStartupLoction, value); } } /// @@ -336,6 +343,8 @@ namespace Avalonia.Controls { if (WindowStartupLocation == WindowStartupLocation.CenterScreen) { + // This should be using a Screen API, but we don't have one yet and + // PlatformImpl.MaxClientSize is the best we have. if (PlatformImpl != null) { var positionAsSize = PlatformImpl.MaxClientSize / 2 - ClientSize / 2; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index dd1e8fbef1..dd1e0ae842 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -32,12 +32,13 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty OwnerProperty = - AvaloniaProperty.Register(nameof(Owner)); + public static readonly DirectProperty OwnerProperty = + AvaloniaProperty.RegisterDirect(nameof(Owner), o => o.Owner); private bool _hasExecutedInitialLayoutPass; private bool _isActive; private bool _ignoreVisibilityChange; + private WindowBase _owner; static WindowBase() { @@ -111,8 +112,8 @@ namespace Avalonia.Controls /// public WindowBase Owner { - get { return GetValue(OwnerProperty); } - set { SetValue(OwnerProperty, value); } + get { return _owner; } + set { SetAndRaise(OwnerProperty, ref _owner, value); } } /// From efb9fd4c5732db991a6e70e7c410226bea68202c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 6 Aug 2017 18:02:16 +0100 Subject: [PATCH 04/21] Added missing setters. --- src/Avalonia.Controls/Window.cs | 5 ++++- src/Avalonia.Controls/WindowBase.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f60768980d..a94609122a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -82,7 +82,10 @@ namespace Avalonia.Controls /// Defines the proeprty. /// public static readonly DirectProperty WindowStartupLocationProperty = - AvaloniaProperty.RegisterDirect(nameof(WindowStartupLocation), o => o.WindowStartupLocation); + AvaloniaProperty.RegisterDirect( + nameof(WindowStartupLocation), + o => o.WindowStartupLocation, + (o, v) => o.WindowStartupLocation = v); private readonly NameScope _nameScope = new NameScope(); private object _dialogResult; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index dd1e0ae842..73d6d8c7cc 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -33,7 +33,10 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty OwnerProperty = - AvaloniaProperty.RegisterDirect(nameof(Owner), o => o.Owner); + AvaloniaProperty.RegisterDirect( + nameof(Owner), + o => o.Owner, + (o, v) => o.Owner = v); private bool _hasExecutedInitialLayoutPass; private bool _isActive; From 2cc4b41acc7521f72fe552cfbf079c85f06199dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Tue, 3 Oct 2017 22:12:11 +0100 Subject: [PATCH 05/21] Use Screen API. --- src/Avalonia.Controls/Screens.cs | 2 +- src/Avalonia.Controls/Window.cs | 11 ++++------- src/Windows/Avalonia.Win32/ScreenImpl.cs | 4 ++-- .../Avalonia.Controls.UnitTests/WindowTests.cs | 18 ++++++++++++------ 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs index b8ddce3aea..2bfddc048b 100644 --- a/src/Avalonia.Controls/Screens.cs +++ b/src/Avalonia.Controls/Screens.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls return currMaxScreen; } - public Screen SceenFromPoint(Point point) + public Screen ScreenFromPoint(Point point) { return All.FirstOrDefault(x=>x.Bounds.Contains(point)); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 4cbd73841b..7890a7dd5a 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -376,13 +376,10 @@ namespace Avalonia.Controls { if (WindowStartupLocation == WindowStartupLocation.CenterScreen) { - // This should be using a Screen API, but we don't have one yet and - // PlatformImpl.MaxClientSize is the best we have. - if (PlatformImpl != null) - { - var positionAsSize = PlatformImpl.MaxClientSize / 2 - ClientSize / 2; - Position = new Point(positionAsSize.Width, positionAsSize.Height); - } + var screen = Screens.ScreenFromPoint(Bounds.Position); + + if (screen != null) + Position = screen.WorkingArea.CenterRect(new Rect(ClientSize)).Position; } else if (WindowStartupLocation == WindowStartupLocation.CenterOwner) { diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index 4f4331e461..113b2811dc 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -41,8 +41,8 @@ namespace Avalonia.Win32 Rect avaloniaBounds = new Rect(bounds.left, bounds.top, bounds.right - bounds.left, bounds.bottom - bounds.top); Rect avaloniaWorkArea = - new Rect(workingArea.left, workingArea.top, workingArea.right - bounds.left, - workingArea.bottom - bounds.top); + new Rect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, + workingArea.bottom - workingArea.top); screens[index] = new WinScreen(avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 4de1cfe356..6168421919 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -239,32 +239,38 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Window_Should_Be_Centered_When_Window_Startup_Location_Is_Center_Screen() + public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen() { + var screen1 = new Mock(new Rect(new Size(1920, 1080)), new Rect(new Size(1920, 1040)), true); + var screen2 = new Mock(new Rect(new Size(1366, 768)), new Rect(new Size(1366, 728)), false); + + var screens = new Mock(); + screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object }); + var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Position); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); - windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.Screen).Returns(screens.Object); using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var window = new Window(); + var window = new Window(windowImpl.Object); window.WindowStartupLocation = WindowStartupLocation.CenterScreen; window.Position = new Point(60, 40); window.Show(); var expectedPosition = new Point( - window.PlatformImpl.MaxClientSize.Width / 2 - window.ClientSize.Width / 2, - window.PlatformImpl.MaxClientSize.Height / 2 - window.ClientSize.Height / 2); + screen1.Object.WorkingArea.Size.Width / 2 - window.ClientSize.Width / 2, + screen1.Object.WorkingArea.Size.Height / 2 - window.ClientSize.Height / 2); Assert.Equal(window.Position, expectedPosition); } } [Fact] - public void Window_Should_Be_Centered_Relative_To_Owner_When_Window_Startup_Location_Is_Center_Owner() + public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() { var parentWindowImpl = new Mock(); parentWindowImpl.SetupProperty(x => x.Position); From a912f03167aec9b04f6197b684efc7c3ee5ceb65 Mon Sep 17 00:00:00 2001 From: lindexi Date: Wed, 27 Dec 2017 16:32:02 +0800 Subject: [PATCH 06/21] add UserControl comment --- src/Avalonia.Controls/UserControl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs index 7c9e591e31..e063a65e09 100644 --- a/src/Avalonia.Controls/UserControl.cs +++ b/src/Avalonia.Controls/UserControl.cs @@ -6,6 +6,9 @@ using Avalonia.Styling; namespace Avalonia.Controls { + /// + /// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic. + /// public class UserControl : ContentControl, IStyleable, INameScope { private readonly NameScope _nameScope = new NameScope(); @@ -24,6 +27,7 @@ namespace Avalonia.Controls remove { _nameScope.Unregistered -= value; } } + /// Type IStyleable.StyleKey => typeof(ContentControl); /// From 78fa1dc17a424da1a5cb5a43432130a92bda6627 Mon Sep 17 00:00:00 2001 From: lindexi Date: Wed, 27 Dec 2017 16:59:02 +0800 Subject: [PATCH 07/21] add LayoutTransformControl comment --- src/Avalonia.Controls/LayoutTransformControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index ed7a50d9b0..87e3853643 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -15,6 +15,9 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + /// + /// Control that implements support for transformations as if applied by LayoutTransform. + /// public class LayoutTransformControl : ContentControl { public static readonly AvaloniaProperty LayoutTransformProperty = @@ -26,6 +29,9 @@ namespace Avalonia.Controls .AddClassHandler(x => x.OnLayoutTransformChanged); } + /// + /// Gets or sets a graphics transformation that should apply to this element when layout is performed. + /// public Transform LayoutTransform { get { return GetValue(LayoutTransformProperty); } From f94381acac60e9c00695175f623fcbab51096b23 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 28 Feb 2018 15:41:13 -0500 Subject: [PATCH 08/21] Adds Methods Weakly Subscribe to CollectionChange --- .../Collections/AvaloniaListExtensions.cs | 26 +++- .../NotifyCollectionChangedExtensions.cs | 127 ++++++++++++++++++ 2 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index 54cd132b95..b27b06a277 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -34,14 +34,18 @@ namespace Avalonia.Collections /// /// An action called when the collection is reset. /// + /// + /// Indicates if a weak subscription should be used to track changes to the collection. + /// /// A disposable used to terminate the subscription. public static IDisposable ForEachItem( this IAvaloniaReadOnlyList collection, Action added, Action removed, - Action reset) + Action reset, + bool weakSubscription = false) { - return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset); + return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset, weakSubscription); } /// @@ -63,12 +67,16 @@ namespace Avalonia.Collections /// An action called when the collection is reset. This will be followed by calls to /// for each item present in the collection after the reset. /// + /// + /// Indicates if a weak subscription should be used to track changes to the collection. + /// /// A disposable used to terminate the subscription. public static IDisposable ForEachItem( this IAvaloniaReadOnlyList collection, Action added, Action removed, - Action reset) + Action reset, + bool weakSubscription = false) { void Add(int index, IList items) { @@ -118,9 +126,17 @@ namespace Avalonia.Collections }; Add(0, (IList)collection); - collection.CollectionChanged += handler; - return Disposable.Create(() => collection.CollectionChanged -= handler); + if (weakSubscription) + { + return collection.WeakSubscribe(handler); + } + else + { + collection.CollectionChanged += handler; + + return Disposable.Create(() => collection.CollectionChanged -= handler); + } } public static IAvaloniaReadOnlyList CreateDerivedList( diff --git a/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs new file mode 100644 index 0000000000..d295cb91ce --- /dev/null +++ b/src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs @@ -0,0 +1,127 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Utilities; + +namespace Avalonia.Collections +{ + public static class NotifyCollectionChangedExtensions + { + /// + /// Gets a weak observable for the CollectionChanged event. + /// + /// The collection. + /// An observable. + public static IObservable GetWeakCollectionChangedObservable( + this INotifyCollectionChanged collection) + { + Contract.Requires(collection != null); + + return new WeakCollectionChangedObservable(new WeakReference(collection)); + } + + /// + /// Subcribes to the CollectionChanged event using a weak subscription. + /// + /// The collection. + /// + /// An action called when the collection event is raised. + /// + /// A disposable used to terminate the subscription. + public static IDisposable WeakSubscribe( + this INotifyCollectionChanged collection, + NotifyCollectionChangedEventHandler handler) + { + Contract.Requires(collection != null); + Contract.Requires(handler != null); + + return + collection.GetWeakCollectionChangedObservable() + .Subscribe(e => handler.Invoke(collection, e)); + } + + /// + /// Subcribes to the CollectionChanged event using a weak subscription. + /// + /// The collection. + /// + /// An action called when the collection event is raised. + /// + /// A disposable used to terminate the subscription. + public static IDisposable WeakSubscribe( + this INotifyCollectionChanged collection, + Action handler) + { + Contract.Requires(collection != null); + Contract.Requires(handler != null); + + return + collection.GetWeakCollectionChangedObservable() + .Subscribe(handler); + } + + private class WeakCollectionChangedObservable : ObservableBase, + IWeakSubscriber + { + private WeakReference _sourceReference; + private readonly Subject _changed = new Subject(); + + private int _count; + + public WeakCollectionChangedObservable(WeakReference source) + { + _sourceReference = source; + } + + public void OnEvent(object sender, NotifyCollectionChangedEventArgs e) + { + _changed.OnNext(e); + } + + protected override IDisposable SubscribeCore(IObserver observer) + { + if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) + { + if (_count++ == 0) + { + WeakSubscriptionManager.Subscribe( + instance, + nameof(instance.CollectionChanged), + this); + } + + return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed) + .Subscribe(observer); + } + else + { + _changed.OnCompleted(); + observer.OnCompleted(); + return Disposable.Empty; + } + } + + private void DecrementCount() + { + if (--_count == 0) + { + if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance)) + { + WeakSubscriptionManager.Unsubscribe( + instance, + nameof(instance.CollectionChanged), + this); + } + } + } + } + } +} From 6103bab8606a04588d4ff232250076e9a4f344d4 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 28 Feb 2018 15:41:56 -0500 Subject: [PATCH 09/21] Altered ItemsControl to use Weak CollectionChange Subscription --- src/Avalonia.Controls/ItemsControl.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 4366de1cd6..6a26e29187 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -1,6 +1,7 @@ // 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; using System.Collections.Generic; using System.Collections.Specialized; @@ -54,6 +55,7 @@ namespace Avalonia.Controls private IEnumerable _items = new AvaloniaList(); private IItemContainerGenerator _itemContainerGenerator; + private IDisposable _itemsCollectionChangedSubscription; /// /// Initializes static members of the class. @@ -326,12 +328,8 @@ namespace Avalonia.Controls /// The event args. protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e) { - var incc = e.OldValue as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged -= ItemsCollectionChanged; - } + _itemsCollectionChangedSubscription?.Dispose(); + _itemsCollectionChangedSubscription = null; var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; @@ -428,7 +426,7 @@ namespace Avalonia.Controls if (incc != null) { - incc.CollectionChanged += ItemsCollectionChanged; + _itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } } From 5fecddef513878c8e335a3e61317b456ef076d36 Mon Sep 17 00:00:00 2001 From: Vinicius de Melo Rocha Date: Thu, 1 Mar 2018 08:18:16 -0300 Subject: [PATCH 10/21] Update README.md with new contributing link --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 5a5d40c984..eeee39dabe 100644 --- a/readme.md +++ b/readme.md @@ -51,7 +51,7 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con ### Contributors -This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. +This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)]. From 27ab312be5b7e76efbba0d5245f29d4ee77bb5d7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 1 Mar 2018 19:59:15 +0300 Subject: [PATCH 11/21] Fixed nuget deps --- build/System.Drawing.Common.props | 5 +++++ packages.cake | 4 ++-- src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj | 3 --- src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 6 ++---- 4 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 build/System.Drawing.Common.props diff --git a/build/System.Drawing.Common.props b/build/System.Drawing.Common.props new file mode 100644 index 0000000000..a568152bbd --- /dev/null +++ b/build/System.Drawing.Common.props @@ -0,0 +1,5 @@ + + + + + diff --git a/packages.cake b/packages.cake index 134340f459..17411aef4c 100644 --- a/packages.cake +++ b/packages.cake @@ -370,10 +370,10 @@ public class Packages new NuGetPackSettings() { Id = "Avalonia.Win32", - Dependencies = new [] + Dependencies = new DependencyBuilder(this) { new NuSpecDependency() { Id = "Avalonia", Version = parameters.Version } - }, + }.Deps(new string[]{null}, "System.Drawing.Common"), Files = new [] { new NuSpecContent { Source = "Avalonia.Win32/bin/" + parameters.DirSuffix + "/netstandard2.0/Avalonia.Win32.dll", Target = "lib/netstandard2.0" } diff --git a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj index 3a279c05fb..5f6be91571 100644 --- a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj +++ b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj @@ -8,9 +8,6 @@ - - - diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index e055f3c20f..5f26e4ad3e 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -13,7 +13,5 @@ - - - - \ No newline at end of file + + From 87d7f65eb8bcf4b631170b349a660df77a4ae26d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 4 Mar 2018 21:34:19 +0100 Subject: [PATCH 12/21] Added failing test for #1420. --- .../Rendering/SceneGraph/IVisualNode.cs | 5 +++ .../Rendering/SceneGraph/VisualNode.cs | 4 +- .../Rendering/SceneGraph/SceneBuilderTests.cs | 37 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 1668f592ec..75ef49f8e7 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -69,6 +69,11 @@ namespace Avalonia.Rendering.SceneGraph /// IReadOnlyList> DrawOperations { get; } + /// + /// Gets the opacity of the scene graph node. + /// + double Opacity { get; } + /// /// Sets up the drawing context for rendering the node's geometry. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 3ee689b6d2..306036ca2c 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -67,9 +67,7 @@ namespace Avalonia.Rendering.SceneGraph /// public bool HasAncestorGeometryClip { get; } - /// - /// Gets or sets the opacity of the scene graph node. - /// + /// public double Opacity { get { return _opacity; } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index dda1d73649..2ada7bdbba 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -656,6 +656,43 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph } } + [Fact] + public void Setting_Opacity_Should_Add_Descendent_Bounds_To_DirtyRects() + { + using (TestApplication()) + { + Decorator decorator; + Border border; + var tree = new TestRoot + { + Child = decorator = new Decorator + { + Child = border = new Border + { + Background = Brushes.Red, + Width = 100, + Height = 100, + } + } + }; + + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + + var scene = new Scene(tree); + var sceneBuilder = new SceneBuilder(); + sceneBuilder.UpdateAll(scene); + + decorator.Opacity = 0.5; + scene = scene.CloneScene(); + sceneBuilder.Update(scene, decorator); + + Assert.NotEmpty(scene.Layers.Single().Dirty); + var dirty = scene.Layers.Single().Dirty.Single(); + Assert.Equal(new Rect(0, 0, 100, 100), dirty); + } + } + [Fact] public void Should_Set_GeometryClip() { From 7af3eb90586ebf1604226a9b384d0c4a2213cf91 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 4 Mar 2018 21:35:33 +0100 Subject: [PATCH 13/21] Force recurse when opacity changed. Fixes #1420. --- src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index b219a74119..6005ee8b8f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -170,8 +170,9 @@ namespace Avalonia.Rendering.SceneGraph var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip); forceRecurse = forceRecurse || - node.Transform != contextImpl.Transform || - node.ClipBounds != clipBounds; + node.ClipBounds != clipBounds || + node.Opacity != opacity || + node.Transform != contextImpl.Transform; node.Transform = contextImpl.Transform; node.ClipBounds = clipBounds; From b31f7385dc79debc8fe5d6111d18ea016cd83d5f Mon Sep 17 00:00:00 2001 From: ahopper Date: Mon, 5 Mar 2018 07:55:16 +0000 Subject: [PATCH 14/21] Allow progressbar width and height less than default --- src/Avalonia.Controls/ProgressBar.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 8cf6b149cb..19744d3038 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -90,16 +90,16 @@ namespace Avalonia.Controls { if (orientation == Orientation.Horizontal) { - MinHeight = 14; - MinWidth = 200; + MinHeight = Math.Min(14, double.IsNaN(Height) ? double.MaxValue : Height); + MinWidth = Math.Min(200, double.IsNaN(Width) ? double.MaxValue : Width); _indicator.HorizontalAlignment = HorizontalAlignment.Left; _indicator.VerticalAlignment = VerticalAlignment.Stretch; } else { - MinHeight = 200; - MinWidth = 14; + MinHeight = Math.Min(200, double.IsNaN(Height) ? double.MaxValue : Height); + MinWidth = Math.Min(14, double.IsNaN(Width) ? double.MaxValue : Width); _indicator.HorizontalAlignment = HorizontalAlignment.Stretch; _indicator.VerticalAlignment = VerticalAlignment.Bottom; From 82acff0d57087b1fed5c8c0cc71a9b6a0ea5a983 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 5 Mar 2018 11:37:09 +0300 Subject: [PATCH 15/21] [GTK3] Unregister tick callback and properly close the window. Fixes #1424 --- src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 10 +++++++++- src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 22 ++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 3e945798d4..2451d26126 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -219,7 +219,10 @@ namespace Avalonia.Gtk3.Interop public delegate void gtk_widget_queue_draw_area(GtkWidget widget, int x, int y, int width, int height); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] - public delegate void gtk_widget_add_tick_callback(GtkWidget widget, TickCallback callback, IntPtr userData, IntPtr destroy); + public delegate uint gtk_widget_add_tick_callback(GtkWidget widget, TickCallback callback, IntPtr userData, IntPtr destroy); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate uint gtk_widget_remove_tick_callback(GtkWidget widget, uint id); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate GtkImContext gtk_im_multicontext_new(); @@ -256,6 +259,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate void gtk_window_unmaximize(GtkWindow window); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_close(GtkWindow window); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate void gtk_window_set_geometry_hints(GtkWindow window, IntPtr geometry_widget, ref GdkGeometry geometry, GdkWindowHints geom_mask); @@ -434,6 +440,7 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_window_invalidate_rect GdkWindowInvalidateRect; public static D.gtk_widget_queue_draw_area GtkWidgetQueueDrawArea; public static D.gtk_widget_add_tick_callback GtkWidgetAddTickCallback; + public static D.gtk_widget_remove_tick_callback GtkWidgetRemoveTickCallback; public static D.gtk_widget_activate GtkWidgetActivate; public static D.gtk_clipboard_get_for_display GtkClipboardGetForDisplay; public static D.gtk_clipboard_request_text GtkClipboardRequestText; @@ -456,6 +463,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_window_deiconify GtkWindowDeiconify; public static D.gtk_window_maximize GtkWindowMaximize; public static D.gtk_window_unmaximize GtkWindowUnmaximize; + public static D.gtk_window_close GtkWindowClose; public static D.gdk_window_begin_move_drag GdkWindowBeginMoveDrag; public static D.gdk_window_begin_resize_drag GdkWindowBeginResizeDrag; public static D.gdk_event_request_motions GdkEventRequestMotions; diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index c41a136bce..5aac4466b2 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -33,11 +33,11 @@ namespace Avalonia.Gtk3 private readonly AutoResetEvent _canSetNextOperation = new AutoResetEvent(true); internal IntPtr? GdkWindowHandle; private bool _overrideRedirect; + private uint? _tickCallback; public WindowBaseImpl(GtkWindow gtkWidget) { GtkWidget = gtkWidget; - Disposables.Add(gtkWidget); _framebuffer = new FramebufferManager(this); _imContext = Native.GtkImMulticontextNew(); Disposables.Add(_imContext); @@ -62,7 +62,7 @@ namespace Avalonia.Gtk3 { Native.GtkWidgetSetDoubleBuffered(gtkWidget, false); _gcHandle = GCHandle.Alloc(this); - Native.GtkWidgetAddTickCallback(GtkWidget, PinnedStaticCallback, GCHandle.ToIntPtr(_gcHandle), IntPtr.Zero); + _tickCallback = Native.GtkWidgetAddTickCallback(GtkWidget, PinnedStaticCallback, GCHandle.ToIntPtr(_gcHandle), IntPtr.Zero); } } @@ -103,7 +103,7 @@ namespace Avalonia.Gtk3 private bool OnDestroy(IntPtr gtkwidget, IntPtr userdata) { - Dispose(); + DoDispose(true); return false; } @@ -297,14 +297,28 @@ namespace Avalonia.Gtk3 } - public void Dispose() + public void Dispose() => DoDispose(false); + + void DoDispose(bool fromDestroy) { + if (_tickCallback.HasValue) + { + if (!GtkWidget.IsClosed) + Native.GtkWidgetRemoveTickCallback(GtkWidget, _tickCallback.Value); + _tickCallback = null; + } + //We are calling it here, since signal handler will be detached if (!GtkWidget.IsClosed) Closed?.Invoke(); foreach(var d in Disposables.AsEnumerable().Reverse()) d.Dispose(); Disposables.Clear(); + + if (!fromDestroy && !GtkWidget.IsClosed) + Native.GtkWindowClose(GtkWidget); + GtkWidget.Dispose(); + if (_gcHandle.IsAllocated) { _gcHandle.Free(); From bc39fbe778ed544cc9ed84d45fa5b5883359d899 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 5 Mar 2018 11:40:06 +0300 Subject: [PATCH 16/21] Mirror G_IS_OBJECT check from g_object_unref to make sure that it's not caused by our Dispose --- src/Gtk/Avalonia.Gtk3/Interop/GObject.cs | 6 ++++++ src/Gtk/Avalonia.Gtk3/Interop/Native.cs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs index 8d14515d28..24bcfd71e9 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -21,7 +22,12 @@ namespace Avalonia.Gtk3.Interop protected override bool ReleaseHandle() { if (handle != IntPtr.Zero) + { + Debug.Assert(Native.GTypeCheckInstanceIsFundamentallyA(handle, new IntPtr(Native.G_TYPE_OBJECT)), + "Handle is not a GObject"); Native.GObjectUnref(handle); + } + handle = IntPtr.Zero; return true; } diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index 2451d26126..d4618e7bc1 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -347,6 +347,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)] public delegate ulong g_free(IntPtr data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] + public delegate bool g_type_check_instance_is_fundamentally_a(IntPtr instance, IntPtr type); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)] public unsafe delegate void g_slist_free(GSList* data); @@ -433,6 +436,7 @@ namespace Avalonia.Gtk3.Interop public static D.g_timeout_add GTimeoutAdd; public static D.g_timeout_add_full GTimeoutAddFull; public static D.g_free GFree; + public static D.g_type_check_instance_is_fundamentally_a GTypeCheckInstanceIsFundamentallyA; public static D.g_slist_free GSlistFree; public static D.g_memory_input_stream_new_from_data GMemoryInputStreamNewFromData; public static D.gtk_widget_set_double_buffered GtkWidgetSetDoubleBuffered; @@ -498,6 +502,7 @@ namespace Avalonia.Gtk3.Interop public static D.cairo_set_font_size CairoSetFontSize; public static D.cairo_move_to CairoMoveTo; public static D.cairo_destroy CairoDestroy; + public const int G_TYPE_OBJECT = 80; } public enum GtkWindowType From 53397a2aab9fd98a0fac967e7540ea0e95601798 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Mar 2018 09:54:25 +0100 Subject: [PATCH 17/21] Added failing test for #1423. --- .../WindowTests.cs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 6168421919..a85c4df8af 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -231,11 +232,23 @@ namespace Avalonia.Controls.UnitTests } } - private void ClearOpenWindows() + [Fact] + public async Task ShowDialog_With_ValueType_Returns_Default_When_Closed() { - // HACK: We really need a decent way to have "statics" that can be scoped to - // AvaloniaLocator scopes. - ((IList)Window.OpenWindows).Clear(); + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windowImpl = new Mock(); + windowImpl.SetupProperty(x => x.Closed); + windowImpl.Setup(x => x.Scaling).Returns(1); + + var target = new Window(windowImpl.Object); + var task = target.ShowDialog(); + + windowImpl.Object.Closed(); + + var result = await task; + Assert.False(result); + } } [Fact] @@ -321,5 +334,12 @@ namespace Avalonia.Controls.UnitTests x.Scaling == 1 && x.CreateRenderer(It.IsAny()) == renderer.Object); } + + private void ClearOpenWindows() + { + // HACK: We really need a decent way to have "statics" that can be scoped to + // AvaloniaLocator scopes. + ((IList)Window.OpenWindows).Clear(); + } } } From 0b828e1810f5f3d959149005ca41906ef43c89fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Mar 2018 09:56:43 +0100 Subject: [PATCH 18/21] Don't try to cast null to value type. The return type for `ShowDialog` can be a value type. If so, use `default(T)` for the return value when the dialog is closed using the OS close button instead of trying to cast `null` to `T`. Fixes #1423 --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 141a9ac377..9db7db365d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls modal?.Dispose(); SetIsEnabled(affectedWindows, true); activated?.Activate(); - result.SetResult((TResult)_dialogResult); + result.SetResult((TResult)(_dialogResult ?? default(TResult))); }); return result.Task; From 77b74444bac5564371094e353d5856342e0ba237 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 5 Mar 2018 12:51:07 +0300 Subject: [PATCH 19/21] Improved unmanaged memory allocation diagnostics. ref #1411 --- src/Gtk/Avalonia.Gtk3/FramebufferManager.cs | 5 +-- .../Avalonia.MonoMac/EmulatedFramebuffer.cs | 10 +++--- .../StandardRuntimePlatform.cs | 31 ++++++++++++------- .../Avalonia.Win32/WindowFramebuffer.cs | 22 ++++--------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs index a673047e8c..82dbf53579 100644 --- a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs +++ b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs @@ -26,8 +26,9 @@ namespace Avalonia.Gtk3 { // This method may be called from non-UI thread, don't touch anything that calls back to GTK/GDK var s = _window.ClientSize; - var width = (int) s.Width; - var height = (int) s.Height; + var width = Math.Max(1, (int) s.Width); + var height = Math.Max(1, (int) s.Height); + if (!Dispatcher.UIThread.CheckAccess() && Gtk3Platform.DisplayClassName.ToLower().Contains("x11")) { diff --git a/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs b/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs index 935ab53432..6cd2b16afa 100644 --- a/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs +++ b/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs @@ -12,6 +12,7 @@ namespace Avalonia.MonoMac private readonly TopLevelImpl.TopLevelView _view; private readonly CGSize _logicalSize; private readonly bool _isDeferred; + private readonly IUnmanagedBlob _blob; [DllImport("libc")] static extern void memset(IntPtr p, int c, IntPtr size); @@ -29,13 +30,13 @@ namespace Avalonia.MonoMac Dpi = new Vector(96 * pixelSize.Width / _logicalSize.Width, 96 * pixelSize.Height / _logicalSize.Height); Format = PixelFormat.Rgba8888; var size = Height * RowBytes; - Address = Marshal.AllocHGlobal(size); + _blob = AvaloniaLocator.Current.GetService().AllocBlob(size); memset(Address, 0, new IntPtr(size)); } public void Dispose() { - if (Address == IntPtr.Zero) + if (_blob.IsDisposed) return; var nfo = (int) CGBitmapFlags.ByteOrder32Big | (int) CGImageAlphaInfo.PremultipliedLast; CGImage image = null; @@ -71,14 +72,13 @@ namespace Avalonia.MonoMac else _view.SetBackBufferImage(new SavedImage(image, _logicalSize)); } - Marshal.FreeHGlobal(Address); - Address = IntPtr.Zero; + _blob.Dispose(); } } - public IntPtr Address { get; private set; } + public IntPtr Address => _blob.Address; public int Width { get; } public int Height { get; } public int RowBytes { get; } diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index b777736f06..d352a588e0 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -28,11 +28,13 @@ namespace Avalonia.Shared.PlatformSupport class UnmanagedBlob : IUnmanagedBlob { private readonly StandardRuntimePlatform _plat; + private IntPtr _address; #if DEBUG private static readonly List Backtraces = new List(); private static Thread GCThread; private readonly string _backtrace; - + private readonly object _lock = new object(); + private static readonly object _btlock = new object(); class GCThreadDetector { @@ -55,28 +57,35 @@ namespace Avalonia.Shared.PlatformSupport public UnmanagedBlob(StandardRuntimePlatform plat, int size) { + if (size <= 0) + throw new ArgumentException("Positive number required", nameof(size)); _plat = plat; - Address = plat.Alloc(size); + _address = plat.Alloc(size); GC.AddMemoryPressure(size); Size = size; #if DEBUG _backtrace = Environment.StackTrace; - Backtraces.Add(_backtrace); + lock (_btlock) + Backtraces.Add(_backtrace); #endif } void DoDispose() { - if (!IsDisposed) + lock (_lock) { + if (!IsDisposed) + { #if DEBUG - Backtraces.Remove(_backtrace); + lock (_btlock) + Backtraces.Remove(_backtrace); #endif - _plat.Free(Address, Size); - GC.RemoveMemoryPressure(Size); - IsDisposed = true; - Address = IntPtr.Zero; - Size = 0; + _plat.Free(_address, Size); + GC.RemoveMemoryPressure(Size); + IsDisposed = true; + _address = IntPtr.Zero; + Size = 0; + } } } @@ -102,7 +111,7 @@ namespace Avalonia.Shared.PlatformSupport DoDispose(); } - public IntPtr Address { get; private set; } + public IntPtr Address => IsDisposed ? throw new ObjectDisposedException("UnmanagedBlob") : _address; public int Size { get; private set; } public bool IsDisposed { get; private set; } } diff --git a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs index df238c919e..83ab288c54 100644 --- a/src/Windows/Avalonia.Win32/WindowFramebuffer.cs +++ b/src/Windows/Avalonia.Win32/WindowFramebuffer.cs @@ -10,7 +10,7 @@ namespace Avalonia.Win32 public class WindowFramebuffer : ILockedFramebuffer { private readonly IntPtr _handle; - private IntPtr _pBitmap; + private IUnmanagedBlob _bitmapBlob; private UnmanagedMethods.BITMAPINFOHEADER _bmpInfo; public WindowFramebuffer(IntPtr handle, int width, int height) @@ -27,7 +27,7 @@ namespace Avalonia.Win32 _bmpInfo.Init(); _bmpInfo.biWidth = width; _bmpInfo.biHeight = -height; - _pBitmap = Marshal.AllocHGlobal(width * height * 4); + _bitmapBlob = AvaloniaLocator.Current.GetService().AllocBlob(width * height * 4); } ~WindowFramebuffer() @@ -35,7 +35,7 @@ namespace Avalonia.Win32 Deallocate(); } - public IntPtr Address => _pBitmap; + public IntPtr Address => _bitmapBlob.Address; public int RowBytes => Width * 4; public PixelFormat Format => PixelFormat.Bgra8888; @@ -70,21 +70,18 @@ namespace Avalonia.Win32 public void DrawToDevice(IntPtr hDC, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1, int height = -1) { - if(_pBitmap == IntPtr.Zero) - throw new ObjectDisposedException("Framebuffer"); if (width == -1) width = Width; if (height == -1) height = Height; UnmanagedMethods.SetDIBitsToDevice(hDC, destX, destY, (uint) width, (uint) height, srcX, srcY, - 0, (uint)Height, _pBitmap, ref _bmpInfo, 0); + 0, (uint)Height, _bitmapBlob.Address, ref _bmpInfo, 0); } public bool DrawToWindow(IntPtr hWnd, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1, int height = -1) { - - if (_pBitmap == IntPtr.Zero) + if (_bitmapBlob.IsDisposed) throw new ObjectDisposedException("Framebuffer"); if (hWnd == IntPtr.Zero) return false; @@ -102,13 +99,6 @@ namespace Avalonia.Win32 DrawToWindow(_handle); } - public void Deallocate() - { - if (_pBitmap != IntPtr.Zero) - { - Marshal.FreeHGlobal(_pBitmap); - _pBitmap = IntPtr.Zero; - } - } + public void Deallocate() => _bitmapBlob.Dispose(); } } \ No newline at end of file From 1784b5178124cee838de330c9cbbfd9875861629 Mon Sep 17 00:00:00 2001 From: ahopper Date: Mon, 5 Mar 2018 10:55:06 +0000 Subject: [PATCH 20/21] ProgressBar MinWidth & MinHeight set in template --- src/Avalonia.Controls/ProgressBar.cs | 26 +-------- src/Avalonia.Themes.Default/ProgressBar.xaml | 58 +++++++++++++------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 19744d3038..34954dd0d5 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -26,12 +26,13 @@ namespace Avalonia.Controls static ProgressBar() { + PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); + PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); IsIndeterminateProperty.Changed.AddClassHandler( (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); - OrientationProperty.Changed.AddClassHandler( - (p, e) => { if (p._indicator != null) p.UpdateOrientation((Orientation)e.NewValue); }); } public bool IsIndeterminate @@ -59,7 +60,6 @@ namespace Avalonia.Controls _indicator = e.NameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); - UpdateOrientation(Orientation); UpdateIsIndeterminate(IsIndeterminate); } @@ -86,26 +86,6 @@ namespace Avalonia.Controls } } - private void UpdateOrientation(Orientation orientation) - { - if (orientation == Orientation.Horizontal) - { - MinHeight = Math.Min(14, double.IsNaN(Height) ? double.MaxValue : Height); - MinWidth = Math.Min(200, double.IsNaN(Width) ? double.MaxValue : Width); - - _indicator.HorizontalAlignment = HorizontalAlignment.Left; - _indicator.VerticalAlignment = VerticalAlignment.Stretch; - } - else - { - MinHeight = Math.Min(200, double.IsNaN(Height) ? double.MaxValue : Height); - MinWidth = Math.Min(14, double.IsNaN(Width) ? double.MaxValue : Width); - - _indicator.HorizontalAlignment = HorizontalAlignment.Stretch; - _indicator.VerticalAlignment = VerticalAlignment.Bottom; - } - } - private void UpdateIsIndeterminate(bool isIndeterminate) { if (isIndeterminate) diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index 82f385e16b..c9c898562c 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -1,20 +1,38 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file From 8f92a1a8183955e27332fc98eaa8bfebf5f38a13 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 5 Mar 2018 15:25:48 +0300 Subject: [PATCH 21/21] Conditional compilation --- src/Shared/PlatformSupport/StandardRuntimePlatform.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index d352a588e0..16977d5f97 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -29,11 +29,11 @@ namespace Avalonia.Shared.PlatformSupport { private readonly StandardRuntimePlatform _plat; private IntPtr _address; + private readonly object _lock = new object(); #if DEBUG private static readonly List Backtraces = new List(); private static Thread GCThread; private readonly string _backtrace; - private readonly object _lock = new object(); private static readonly object _btlock = new object(); class GCThreadDetector @@ -164,4 +164,4 @@ namespace Avalonia.Shared.PlatformSupport void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr); #endif } -} \ No newline at end of file +}