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 001/211] 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 002/211] 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 003/211] 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 004/211] 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 898e7e4747f0d0b56092c5e7687415753fae8b54 Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Tue, 22 Aug 2017 09:55:24 +0300 Subject: [PATCH 005/211] Using a tokenizer instead of string split --- .../Avalonia.Controls.csproj | 1 + src/Avalonia.Controls/GridLength.cs | 9 +- src/Avalonia.Visuals/Matrix.cs | 24 +- src/Avalonia.Visuals/Point.cs | 16 +- src/Avalonia.Visuals/Rect.cs | 20 +- src/Avalonia.Visuals/RelativePoint.cs | 26 +-- src/Avalonia.Visuals/RelativeRect.cs | 55 ++--- src/Avalonia.Visuals/Size.cs | 15 +- src/Avalonia.Visuals/Thickness.cs | 35 ++- .../Utilities/StringTokenizer.cs | 205 ++++++++++++++++++ .../RelativeRectTests.cs | 8 + 11 files changed, 304 insertions(+), 110 deletions(-) create mode 100644 src/Avalonia.Visuals/Utilities/StringTokenizer.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 037546b186..0b1e778cf0 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -26,6 +26,7 @@ true + Properties\SharedAssemblyInfo.cs diff --git a/src/Avalonia.Controls/GridLength.cs b/src/Avalonia.Controls/GridLength.cs index c711553e05..b17dc584bd 100644 --- a/src/Avalonia.Controls/GridLength.cs +++ b/src/Avalonia.Controls/GridLength.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 Avalonia.Utilities; using System; using System.Collections.Generic; using System.Globalization; @@ -210,7 +211,13 @@ namespace Avalonia.Controls /// The . public static IEnumerable ParseLengths(string s, CultureInfo culture) { - return s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(x => Parse(x, culture)); + using (var tokenizer = new StringTokenizer(s, culture)) + { + while (tokenizer.NextString(out var item)) + { + yield return Parse(item, culture); + } + } } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Matrix.cs b/src/Avalonia.Visuals/Matrix.cs index 10549b967d..6c7d104680 100644 --- a/src/Avalonia.Visuals/Matrix.cs +++ b/src/Avalonia.Visuals/Matrix.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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -305,23 +306,16 @@ namespace Avalonia /// The . public static Matrix Parse(string s, CultureInfo culture) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToArray(); - - if (parts.Length == 6) + using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Matrix")) { return new Matrix( - double.Parse(parts[0], culture), - double.Parse(parts[1], culture), - double.Parse(parts[2], culture), - double.Parse(parts[3], culture), - double.Parse(parts[4], culture), - double.Parse(parts[5], culture)); - } - else - { - throw new FormatException("Invalid Matrix."); + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired() + ); } } } diff --git a/src/Avalonia.Visuals/Point.cs b/src/Avalonia.Visuals/Point.cs index 5fbd082967..49d2a401bf 100644 --- a/src/Avalonia.Visuals/Point.cs +++ b/src/Avalonia.Visuals/Point.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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -173,17 +174,12 @@ namespace Avalonia /// The . public static Point Parse(string s, CultureInfo culture) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 2) - { - return new Point(double.Parse(parts[0], culture), double.Parse(parts[1], culture)); - } - else + using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Point")) { - throw new FormatException("Invalid Point."); + return new Point( + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired() + ); } } diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index d562429fc7..11f3db31da 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -490,21 +491,14 @@ namespace Avalonia /// The parsed . public static Rect Parse(string s, CultureInfo culture) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 4) + using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Rect")) { return new Rect( - double.Parse(parts[0], culture), - double.Parse(parts[1], culture), - double.Parse(parts[2], culture), - double.Parse(parts[3], culture)); - } - else - { - throw new FormatException("Invalid Rect."); + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired() + ); } } } diff --git a/src/Avalonia.Visuals/RelativePoint.cs b/src/Avalonia.Visuals/RelativePoint.cs index cc34feb5f3..625ee61439 100644 --- a/src/Avalonia.Visuals/RelativePoint.cs +++ b/src/Avalonia.Visuals/RelativePoint.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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -157,37 +158,32 @@ namespace Avalonia /// The parsed . public static RelativePoint Parse(string s, CultureInfo culture) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 2) + using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativePoint")) { + var x = tokenizer.NextStringRequired(); + var y = tokenizer.NextStringRequired(); + var unit = RelativeUnit.Absolute; var scale = 1.0; - if (parts[0].EndsWith("%")) + if (x.EndsWith("%")) { - if (!parts[1].EndsWith("%")) + if (!y.EndsWith("%")) { throw new FormatException("If one coordinate is relative, both must be."); } - parts[0] = parts[0].TrimEnd('%'); - parts[1] = parts[1].TrimEnd('%'); + x = x.TrimEnd('%'); + y = y.TrimEnd('%'); unit = RelativeUnit.Relative; scale = 0.01; } return new RelativePoint( - double.Parse(parts[0], culture) * scale, - double.Parse(parts[1], culture) * scale, + double.Parse(x, culture) * scale, + double.Parse(y, culture) * scale, unit); } - else - { - throw new FormatException("Invalid Point."); - } } } } diff --git a/src/Avalonia.Visuals/RelativeRect.cs b/src/Avalonia.Visuals/RelativeRect.cs index a11f080e94..05e344f42b 100644 --- a/src/Avalonia.Visuals/RelativeRect.cs +++ b/src/Avalonia.Visuals/RelativeRect.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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -12,6 +13,8 @@ namespace Avalonia /// public struct RelativeRect : IEquatable { + private static readonly char[] PercentChar = { '%' }; + /// /// A rectangle that represents 100% of an area. /// @@ -159,7 +162,7 @@ namespace Avalonia Rect.Width * size.Width, Rect.Height * size.Height); } - + /// /// Parses a string. /// @@ -168,43 +171,43 @@ namespace Avalonia /// The parsed . public static RelativeRect Parse(string s, CultureInfo culture) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 4) + using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid RelativeRect")) { + var x = tokenizer.NextStringRequired(); + var y = tokenizer.NextStringRequired(); + var width = tokenizer.NextStringRequired(); + var height = tokenizer.NextStringRequired(); + var unit = RelativeUnit.Absolute; var scale = 1.0; - if (parts[0].EndsWith("%")) + var xRelative = x.EndsWith("%", StringComparison.Ordinal); + var yRelative = y.EndsWith("%", StringComparison.Ordinal); + var widthRelative = width.EndsWith("%", StringComparison.Ordinal); + var heightRelative = height.EndsWith("%", StringComparison.Ordinal); + + if (xRelative && yRelative && widthRelative && heightRelative) { - if (!parts[1].EndsWith("%") - || !parts[2].EndsWith("%") - || !parts[3].EndsWith("%")) - { - throw new FormatException("If one coordinate is relative, all other must be too."); - } - - parts[0] = parts[0].TrimEnd('%'); - parts[1] = parts[1].TrimEnd('%'); - parts[2] = parts[2].TrimEnd('%'); - parts[3] = parts[3].TrimEnd('%'); + x = x.TrimEnd(PercentChar); + y = y.TrimEnd(PercentChar); + width = width.TrimEnd(PercentChar); + height = height.TrimEnd(PercentChar); + unit = RelativeUnit.Relative; scale = 0.01; } + else if (xRelative || yRelative || widthRelative || heightRelative) + { + throw new FormatException("If one coordinate is relative, all must be."); + } return new RelativeRect( - double.Parse(parts[0], culture) * scale, - double.Parse(parts[1], culture) * scale, - double.Parse(parts[2], culture) * scale, - double.Parse(parts[3], culture) * scale, + double.Parse(x, culture) * scale, + double.Parse(y, culture) * scale, + double.Parse(width, culture) * scale, + double.Parse(height, culture) * scale, unit); } - else - { - throw new FormatException("Invalid RelativeRect."); - } } } } diff --git a/src/Avalonia.Visuals/Size.cs b/src/Avalonia.Visuals/Size.cs index 6ad87c6120..ff734146c2 100644 --- a/src/Avalonia.Visuals/Size.cs +++ b/src/Avalonia.Visuals/Size.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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -153,17 +154,11 @@ namespace Avalonia /// The . public static Size Parse(string s, CultureInfo culture) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - if (parts.Count == 2) - { - return new Size(double.Parse(parts[0], culture), double.Parse(parts[1], culture)); - } - else + using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Size")) { - throw new FormatException("Invalid Size."); + return new Size( + tokenizer.NextDoubleRequired(), + tokenizer.NextDoubleRequired()); } } diff --git a/src/Avalonia.Visuals/Thickness.cs b/src/Avalonia.Visuals/Thickness.cs index dc9be7341d..2a1cf6ac57 100644 --- a/src/Avalonia.Visuals/Thickness.cs +++ b/src/Avalonia.Visuals/Thickness.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 Avalonia.Utilities; using System; using System.Globalization; using System.Linq; @@ -163,28 +164,22 @@ namespace Avalonia /// The . public static Thickness Parse(string s, CultureInfo culture) { - var parts = s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim()) - .ToList(); - - switch (parts.Count) + using (var tokenizer = new StringTokenizer(s, culture, exceptionMessage: "Invalid Thickness")) { - case 1: - var uniform = double.Parse(parts[0], culture); - return new Thickness(uniform); - case 2: - var horizontal = double.Parse(parts[0], culture); - var vertical = double.Parse(parts[1], culture); - return new Thickness(horizontal, vertical); - case 4: - var left = double.Parse(parts[0], culture); - var top = double.Parse(parts[1], culture); - var right = double.Parse(parts[2], culture); - var bottom = double.Parse(parts[3], culture); - return new Thickness(left, top, right, bottom); + var a = tokenizer.NextDoubleRequired(); + + if (tokenizer.NextDouble(out var b)) + { + if (tokenizer.NextDouble(out var c)) + { + return new Thickness(a, b, c, tokenizer.NextDoubleRequired()); + } + + return new Thickness(a, b); + } + + return new Thickness(a); } - - throw new FormatException("Invalid Thickness."); } /// diff --git a/src/Avalonia.Visuals/Utilities/StringTokenizer.cs b/src/Avalonia.Visuals/Utilities/StringTokenizer.cs new file mode 100644 index 0000000000..2f378f44df --- /dev/null +++ b/src/Avalonia.Visuals/Utilities/StringTokenizer.cs @@ -0,0 +1,205 @@ +using System; +using System.Globalization; +using static System.Char; + +namespace Avalonia.Utilities +{ + internal struct StringTokenizer : IDisposable + { + private const char DefaultSeparatorChar = ','; + + private readonly string _s; + private readonly int _length; + private readonly char _separator; + private readonly string _exceptionMessage; + private readonly IFormatProvider _formatProvider; + private int _index; + private int _tokenIndex; + private int _tokenLength; + + public StringTokenizer(string s, IFormatProvider formatProvider, string exceptionMessage = null) + : this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage) + { + _formatProvider = formatProvider; + } + + public StringTokenizer(string s, char separator = DefaultSeparatorChar, string exceptionMessage = null) + { + _s = s ?? throw new ArgumentNullException(nameof(s)); + _length = s?.Length ?? 0; + _separator = separator; + _exceptionMessage = exceptionMessage; + _formatProvider = CultureInfo.InvariantCulture; + _index = 0; + _tokenIndex = -1; + _tokenLength = 0; + + while (_index < _length && IsWhiteSpace(_s, _index)) + { + _index++; + } + } + + public string CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength); + + public void Dispose() + { + if (_index != _length) + { + throw GetFormatException(); + } + } + + public bool NextInt32(out Int32 result, char? separator = null) + { + var success = NextString(out var stringResult, separator); + result = success ? int.Parse(stringResult, _formatProvider) : 0; + return success; + } + + public int NextInt32Required(char? separator = null) + { + if (!NextInt32(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + public bool NextDouble(out double result, char? separator = null) + { + var success = NextString(out var stringResult, separator); + result = success ? double.Parse(stringResult, _formatProvider) : 0; + return success; + } + + public double NextDoubleRequired(char? separator = null) + { + if (!NextDouble(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + public bool NextString(out string result, char? separator = null) + { + var success = NextToken(separator ?? _separator); + result = CurrentToken; + return success; + } + + public string NextStringRequired(char? separator = null) + { + if (!NextString(out var result, separator)) + { + throw GetFormatException(); + } + + return result; + } + + private bool NextToken(char separator) + { + _tokenIndex = -1; + + if (_index >= _length) + { + return false; + } + + var c = _s[_index]; + + var index = _index; + var length = 0; + + while (_index < _length) + { + c = _s[_index]; + + if (IsWhiteSpace(c) || c == separator) + { + break; + } + + _index++; + length++; + } + + SkipToNextToken(separator); + + _tokenIndex = index; + _tokenLength = length; + + if (_tokenLength < 1) + { + throw GetFormatException(); + } + + return true; + } + + private void SkipToNextToken(char separator) + { + if (_index < _length) + { + var c = _s[_index]; + + if (c != separator && !IsWhiteSpace(c)) + { + throw GetFormatException(); + } + + var length = 0; + + while (_index < _length) + { + c = _s[_index]; + + if (c == separator) + { + length++; + _index++; + + if (length > 1) + { + throw GetFormatException(); + } + } + else + { + if (!IsWhiteSpace(c)) + { + break; + } + + _index++; + } + } + + if (length > 0 && _index >= _length) + { + throw GetFormatException(); + } + } + } + + private FormatException GetFormatException() => + _exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException(); + + private static char GetSeparatorFromFormatProvider(IFormatProvider provider) + { + var c = DefaultSeparatorChar; + + var formatInfo = NumberFormatInfo.GetInstance(provider); + if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0]) + { + c = ';'; + } + + return c; + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs index 8ba4f3b739..9f25dcd413 100644 --- a/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/RelativeRectTests.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.Globalization; using Xunit; @@ -25,5 +26,12 @@ namespace Avalonia.Visuals.UnitTests Assert.Equal(new RelativeRect(0.1, 0.2, 0.4, 0.7, RelativeUnit.Relative), result, Compare); } + + [Fact] + public void Parse_Should_Throw_Mixed_Values() + { + Assert.Throws(() => + RelativeRect.Parse("10%, 20%, 40, 70%", CultureInfo.InvariantCulture)); + } } } From cbdc1b16a59311d7ebf57e3578e62d9c0888b3e7 Mon Sep 17 00:00:00 2001 From: Eli Arbel Date: Tue, 22 Aug 2017 11:28:48 +0300 Subject: [PATCH 006/211] Improve known color/brush parsing --- src/Avalonia.Visuals/Media/Brush.cs | 23 +- src/Avalonia.Visuals/Media/Brushes.cs | 434 ++++++------------ src/Avalonia.Visuals/Media/Color.cs | 27 +- src/Avalonia.Visuals/Media/Colors.cs | 284 ++++++------ src/Avalonia.Visuals/Media/KnownColors.cs | 224 +++++++++ .../Media/BrushTests.cs | 18 +- 6 files changed, 544 insertions(+), 466 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/KnownColors.cs diff --git a/src/Avalonia.Visuals/Media/Brush.cs b/src/Avalonia.Visuals/Media/Brush.cs index 40ac24b605..abceb39961 100644 --- a/src/Avalonia.Visuals/Media/Brush.cs +++ b/src/Avalonia.Visuals/Media/Brush.cs @@ -34,26 +34,21 @@ namespace Avalonia.Media /// The . public static IBrush Parse(string s) { + if (s == null) throw new ArgumentNullException(nameof(s)); + if (s.Length == 0) throw new FormatException(); + if (s[0] == '#') { return new SolidColorBrush(Color.Parse(s)); } - else - { - var upper = s.ToUpperInvariant(); - var member = typeof(Brushes).GetTypeInfo().DeclaredProperties - .FirstOrDefault(x => x.Name.ToUpperInvariant() == upper); - if (member != null) - { - var brush = (ISolidColorBrush)member.GetValue(null); - return new SolidColorBrush(brush.Color, brush.Opacity); - } - else - { - throw new FormatException($"Invalid brush string: '{s}'."); - } + var brush = KnownColors.GetKnownBrush(s); + if (brush != null) + { + return brush; } + + throw new FormatException($"Invalid brush string: '{s}'."); } } } diff --git a/src/Avalonia.Visuals/Media/Brushes.cs b/src/Avalonia.Visuals/Media/Brushes.cs index 4c89c97b49..83ff043397 100644 --- a/src/Avalonia.Visuals/Media/Brushes.cs +++ b/src/Avalonia.Visuals/Media/Brushes.cs @@ -1,8 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Media.Immutable; - namespace Avalonia.Media { /// @@ -10,857 +8,709 @@ namespace Avalonia.Media /// public static class Brushes { - /// - /// Initializes static members of the class. - /// - static Brushes() - { - AliceBlue = new ImmutableSolidColorBrush(Colors.AliceBlue); - AntiqueWhite = new ImmutableSolidColorBrush(Colors.AntiqueWhite); - Aqua = new ImmutableSolidColorBrush(Colors.Aqua); - Aquamarine = new ImmutableSolidColorBrush(Colors.Aquamarine); - Azure = new ImmutableSolidColorBrush(Colors.Azure); - Beige = new ImmutableSolidColorBrush(Colors.Beige); - Bisque = new ImmutableSolidColorBrush(Colors.Bisque); - Black = new ImmutableSolidColorBrush(Colors.Black); - BlanchedAlmond = new ImmutableSolidColorBrush(Colors.BlanchedAlmond); - Blue = new ImmutableSolidColorBrush(Colors.Blue); - BlueViolet = new ImmutableSolidColorBrush(Colors.BlueViolet); - Brown = new ImmutableSolidColorBrush(Colors.Brown); - BurlyWood = new ImmutableSolidColorBrush(Colors.BurlyWood); - CadetBlue = new ImmutableSolidColorBrush(Colors.CadetBlue); - Chartreuse = new ImmutableSolidColorBrush(Colors.Chartreuse); - Chocolate = new ImmutableSolidColorBrush(Colors.Chocolate); - Coral = new ImmutableSolidColorBrush(Colors.Coral); - CornflowerBlue = new ImmutableSolidColorBrush(Colors.CornflowerBlue); - Cornsilk = new ImmutableSolidColorBrush(Colors.Cornsilk); - Crimson = new ImmutableSolidColorBrush(Colors.Crimson); - Cyan = new ImmutableSolidColorBrush(Colors.Cyan); - DarkBlue = new ImmutableSolidColorBrush(Colors.DarkBlue); - DarkCyan = new ImmutableSolidColorBrush(Colors.DarkCyan); - DarkGoldenrod = new ImmutableSolidColorBrush(Colors.DarkGoldenrod); - DarkGray = new ImmutableSolidColorBrush(Colors.DarkGray); - DarkGreen = new ImmutableSolidColorBrush(Colors.DarkGreen); - DarkKhaki = new ImmutableSolidColorBrush(Colors.DarkKhaki); - DarkMagenta = new ImmutableSolidColorBrush(Colors.DarkMagenta); - DarkOliveGreen = new ImmutableSolidColorBrush(Colors.DarkOliveGreen); - DarkOrange = new ImmutableSolidColorBrush(Colors.DarkOrange); - DarkOrchid = new ImmutableSolidColorBrush(Colors.DarkOrchid); - DarkRed = new ImmutableSolidColorBrush(Colors.DarkRed); - DarkSalmon = new ImmutableSolidColorBrush(Colors.DarkSalmon); - DarkSeaGreen = new ImmutableSolidColorBrush(Colors.DarkSeaGreen); - DarkSlateBlue = new ImmutableSolidColorBrush(Colors.DarkSlateBlue); - DarkSlateGray = new ImmutableSolidColorBrush(Colors.DarkSlateGray); - DarkTurquoise = new ImmutableSolidColorBrush(Colors.DarkTurquoise); - DarkViolet = new ImmutableSolidColorBrush(Colors.DarkViolet); - DeepPink = new ImmutableSolidColorBrush(Colors.DeepPink); - DeepSkyBlue = new ImmutableSolidColorBrush(Colors.DeepSkyBlue); - DimGray = new ImmutableSolidColorBrush(Colors.DimGray); - DodgerBlue = new ImmutableSolidColorBrush(Colors.DodgerBlue); - Firebrick = new ImmutableSolidColorBrush(Colors.Firebrick); - FloralWhite = new ImmutableSolidColorBrush(Colors.FloralWhite); - ForestGreen = new ImmutableSolidColorBrush(Colors.ForestGreen); - Fuchsia = new ImmutableSolidColorBrush(Colors.Fuchsia); - Gainsboro = new ImmutableSolidColorBrush(Colors.Gainsboro); - GhostWhite = new ImmutableSolidColorBrush(Colors.GhostWhite); - Gold = new ImmutableSolidColorBrush(Colors.Gold); - Goldenrod = new ImmutableSolidColorBrush(Colors.Goldenrod); - Gray = new ImmutableSolidColorBrush(Colors.Gray); - Green = new ImmutableSolidColorBrush(Colors.Green); - GreenYellow = new ImmutableSolidColorBrush(Colors.GreenYellow); - Honeydew = new ImmutableSolidColorBrush(Colors.Honeydew); - HotPink = new ImmutableSolidColorBrush(Colors.HotPink); - IndianRed = new ImmutableSolidColorBrush(Colors.IndianRed); - Indigo = new ImmutableSolidColorBrush(Colors.Indigo); - Ivory = new ImmutableSolidColorBrush(Colors.Ivory); - Khaki = new ImmutableSolidColorBrush(Colors.Khaki); - Lavender = new ImmutableSolidColorBrush(Colors.Lavender); - LavenderBlush = new ImmutableSolidColorBrush(Colors.LavenderBlush); - LawnGreen = new ImmutableSolidColorBrush(Colors.LawnGreen); - LemonChiffon = new ImmutableSolidColorBrush(Colors.LemonChiffon); - LightBlue = new ImmutableSolidColorBrush(Colors.LightBlue); - LightCoral = new ImmutableSolidColorBrush(Colors.LightCoral); - LightCyan = new ImmutableSolidColorBrush(Colors.LightCyan); - LightGoldenrodYellow = new ImmutableSolidColorBrush(Colors.LightGoldenrodYellow); - LightGray = new ImmutableSolidColorBrush(Colors.LightGray); - LightGreen = new ImmutableSolidColorBrush(Colors.LightGreen); - LightPink = new ImmutableSolidColorBrush(Colors.LightPink); - LightSalmon = new ImmutableSolidColorBrush(Colors.LightSalmon); - LightSeaGreen = new ImmutableSolidColorBrush(Colors.LightSeaGreen); - LightSkyBlue = new ImmutableSolidColorBrush(Colors.LightSkyBlue); - LightSlateGray = new ImmutableSolidColorBrush(Colors.LightSlateGray); - LightSteelBlue = new ImmutableSolidColorBrush(Colors.LightSteelBlue); - LightYellow = new ImmutableSolidColorBrush(Colors.LightYellow); - Lime = new ImmutableSolidColorBrush(Colors.Lime); - LimeGreen = new ImmutableSolidColorBrush(Colors.LimeGreen); - Linen = new ImmutableSolidColorBrush(Colors.Linen); - Magenta = new ImmutableSolidColorBrush(Colors.Magenta); - Maroon = new ImmutableSolidColorBrush(Colors.Maroon); - MediumAquamarine = new ImmutableSolidColorBrush(Colors.MediumAquamarine); - MediumBlue = new ImmutableSolidColorBrush(Colors.MediumBlue); - MediumOrchid = new ImmutableSolidColorBrush(Colors.MediumOrchid); - MediumPurple = new ImmutableSolidColorBrush(Colors.MediumPurple); - MediumSeaGreen = new ImmutableSolidColorBrush(Colors.MediumSeaGreen); - MediumSlateBlue = new ImmutableSolidColorBrush(Colors.MediumSlateBlue); - MediumSpringGreen = new ImmutableSolidColorBrush(Colors.MediumSpringGreen); - MediumTurquoise = new ImmutableSolidColorBrush(Colors.MediumTurquoise); - MediumVioletRed = new ImmutableSolidColorBrush(Colors.MediumVioletRed); - MidnightBlue = new ImmutableSolidColorBrush(Colors.MidnightBlue); - MintCream = new ImmutableSolidColorBrush(Colors.MintCream); - MistyRose = new ImmutableSolidColorBrush(Colors.MistyRose); - Moccasin = new ImmutableSolidColorBrush(Colors.Moccasin); - NavajoWhite = new ImmutableSolidColorBrush(Colors.NavajoWhite); - Navy = new ImmutableSolidColorBrush(Colors.Navy); - OldLace = new ImmutableSolidColorBrush(Colors.OldLace); - Olive = new ImmutableSolidColorBrush(Colors.Olive); - OliveDrab = new ImmutableSolidColorBrush(Colors.OliveDrab); - Orange = new ImmutableSolidColorBrush(Colors.Orange); - OrangeRed = new ImmutableSolidColorBrush(Colors.OrangeRed); - Orchid = new ImmutableSolidColorBrush(Colors.Orchid); - PaleGoldenrod = new ImmutableSolidColorBrush(Colors.PaleGoldenrod); - PaleGreen = new ImmutableSolidColorBrush(Colors.PaleGreen); - PaleTurquoise = new ImmutableSolidColorBrush(Colors.PaleTurquoise); - PaleVioletRed = new ImmutableSolidColorBrush(Colors.PaleVioletRed); - PapayaWhip = new ImmutableSolidColorBrush(Colors.PapayaWhip); - PeachPuff = new ImmutableSolidColorBrush(Colors.PeachPuff); - Peru = new ImmutableSolidColorBrush(Colors.Peru); - Pink = new ImmutableSolidColorBrush(Colors.Pink); - Plum = new ImmutableSolidColorBrush(Colors.Plum); - PowderBlue = new ImmutableSolidColorBrush(Colors.PowderBlue); - Purple = new ImmutableSolidColorBrush(Colors.Purple); - Red = new ImmutableSolidColorBrush(Colors.Red); - RosyBrown = new ImmutableSolidColorBrush(Colors.RosyBrown); - RoyalBlue = new ImmutableSolidColorBrush(Colors.RoyalBlue); - SaddleBrown = new ImmutableSolidColorBrush(Colors.SaddleBrown); - Salmon = new ImmutableSolidColorBrush(Colors.Salmon); - SandyBrown = new ImmutableSolidColorBrush(Colors.SandyBrown); - SeaGreen = new ImmutableSolidColorBrush(Colors.SeaGreen); - SeaShell = new ImmutableSolidColorBrush(Colors.SeaShell); - Sienna = new ImmutableSolidColorBrush(Colors.Sienna); - Silver = new ImmutableSolidColorBrush(Colors.Silver); - SkyBlue = new ImmutableSolidColorBrush(Colors.SkyBlue); - SlateBlue = new ImmutableSolidColorBrush(Colors.SlateBlue); - SlateGray = new ImmutableSolidColorBrush(Colors.SlateGray); - Snow = new ImmutableSolidColorBrush(Colors.Snow); - SpringGreen = new ImmutableSolidColorBrush(Colors.SpringGreen); - SteelBlue = new ImmutableSolidColorBrush(Colors.SteelBlue); - Tan = new ImmutableSolidColorBrush(Colors.Tan); - Teal = new ImmutableSolidColorBrush(Colors.Teal); - Thistle = new ImmutableSolidColorBrush(Colors.Thistle); - Tomato = new ImmutableSolidColorBrush(Colors.Tomato); - Transparent = new ImmutableSolidColorBrush(Colors.Transparent); - Turquoise = new ImmutableSolidColorBrush(Colors.Turquoise); - Violet = new ImmutableSolidColorBrush(Colors.Violet); - Wheat = new ImmutableSolidColorBrush(Colors.Wheat); - White = new ImmutableSolidColorBrush(Colors.White); - WhiteSmoke = new ImmutableSolidColorBrush(Colors.WhiteSmoke); - Yellow = new ImmutableSolidColorBrush(Colors.Yellow); - YellowGreen = new ImmutableSolidColorBrush(Colors.YellowGreen); - } - /// /// Gets an colored brush. /// - public static ISolidColorBrush AliceBlue { get; private set; } + public static ISolidColorBrush AliceBlue => KnownColor.AliceBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush AntiqueWhite { get; private set; } + public static ISolidColorBrush AntiqueWhite => KnownColor.AntiqueWhite.ToBrush(); /// - /// Gets an colored brush. + /// Gets an colored brush. /// - public static ISolidColorBrush Aqua { get; private set; } + public static ISolidColorBrush Aqua => KnownColor.Aqua.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Aquamarine { get; private set; } + public static ISolidColorBrush Aquamarine => KnownColor.Aquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Azure { get; private set; } + public static ISolidColorBrush Azure => KnownColor.Azure.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Beige { get; private set; } + public static ISolidColorBrush Beige => KnownColor.Beige.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Bisque { get; private set; } + public static ISolidColorBrush Bisque => KnownColor.Bisque.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Black { get; private set; } + public static ISolidColorBrush Black => KnownColor.Black.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlanchedAlmond { get; private set; } + public static ISolidColorBrush BlanchedAlmond => KnownColor.BlanchedAlmond.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Blue { get; private set; } + public static ISolidColorBrush Blue => KnownColor.Blue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BlueViolet { get; private set; } + public static ISolidColorBrush BlueViolet => KnownColor.BlueViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Brown { get; private set; } + public static ISolidColorBrush Brown => KnownColor.Brown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush BurlyWood { get; private set; } + public static ISolidColorBrush BurlyWood => KnownColor.BurlyWood.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CadetBlue { get; private set; } + public static ISolidColorBrush CadetBlue => KnownColor.CadetBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chartreuse { get; private set; } + public static ISolidColorBrush Chartreuse => KnownColor.Chartreuse.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Chocolate { get; private set; } + public static ISolidColorBrush Chocolate => KnownColor.Chocolate.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Coral { get; private set; } + public static ISolidColorBrush Coral => KnownColor.Coral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush CornflowerBlue { get; private set; } + public static ISolidColorBrush CornflowerBlue => KnownColor.CornflowerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cornsilk { get; private set; } + public static ISolidColorBrush Cornsilk => KnownColor.Cornsilk.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Crimson { get; private set; } + public static ISolidColorBrush Crimson => KnownColor.Crimson.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Cyan { get; private set; } + public static ISolidColorBrush Cyan => KnownColor.Cyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkBlue { get; private set; } + public static ISolidColorBrush DarkBlue => KnownColor.DarkBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkCyan { get; private set; } + public static ISolidColorBrush DarkCyan => KnownColor.DarkCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGoldenrod { get; private set; } + public static ISolidColorBrush DarkGoldenrod => KnownColor.DarkGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGray { get; private set; } + public static ISolidColorBrush DarkGray => KnownColor.DarkGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkGreen { get; private set; } + public static ISolidColorBrush DarkGreen => KnownColor.DarkGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkKhaki { get; private set; } + public static ISolidColorBrush DarkKhaki => KnownColor.DarkKhaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkMagenta { get; private set; } + public static ISolidColorBrush DarkMagenta => KnownColor.DarkMagenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOliveGreen { get; private set; } + public static ISolidColorBrush DarkOliveGreen => KnownColor.DarkOliveGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrange { get; private set; } + public static ISolidColorBrush DarkOrange => KnownColor.DarkOrange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkOrchid { get; private set; } + public static ISolidColorBrush DarkOrchid => KnownColor.DarkOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkRed { get; private set; } + public static ISolidColorBrush DarkRed => KnownColor.DarkRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSalmon { get; private set; } + public static ISolidColorBrush DarkSalmon => KnownColor.DarkSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSeaGreen { get; private set; } + public static ISolidColorBrush DarkSeaGreen => KnownColor.DarkSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateBlue { get; private set; } + public static ISolidColorBrush DarkSlateBlue => KnownColor.DarkSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkSlateGray { get; private set; } + public static ISolidColorBrush DarkSlateGray => KnownColor.DarkSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkTurquoise { get; private set; } + public static ISolidColorBrush DarkTurquoise => KnownColor.DarkTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DarkViolet { get; private set; } + public static ISolidColorBrush DarkViolet => KnownColor.DarkViolet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepPink { get; private set; } + public static ISolidColorBrush DeepPink => KnownColor.DeepPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DeepSkyBlue { get; private set; } + public static ISolidColorBrush DeepSkyBlue => KnownColor.DeepSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DimGray { get; private set; } + public static ISolidColorBrush DimGray => KnownColor.DimGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush DodgerBlue { get; private set; } + public static ISolidColorBrush DodgerBlue => KnownColor.DodgerBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Firebrick { get; private set; } + public static ISolidColorBrush Firebrick => KnownColor.Firebrick.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush FloralWhite { get; private set; } + public static ISolidColorBrush FloralWhite => KnownColor.FloralWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush ForestGreen { get; private set; } + public static ISolidColorBrush ForestGreen => KnownColor.ForestGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Fuchsia { get; private set; } + public static ISolidColorBrush Fuchsia => KnownColor.Fuchsia.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gainsboro { get; private set; } + public static ISolidColorBrush Gainsboro => KnownColor.Gainsboro.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GhostWhite { get; private set; } + public static ISolidColorBrush GhostWhite => KnownColor.GhostWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gold { get; private set; } + public static ISolidColorBrush Gold => KnownColor.Gold.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Goldenrod { get; private set; } + public static ISolidColorBrush Goldenrod => KnownColor.Goldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Gray { get; private set; } + public static ISolidColorBrush Gray => KnownColor.Gray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Green { get; private set; } + public static ISolidColorBrush Green => KnownColor.Green.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush GreenYellow { get; private set; } + public static ISolidColorBrush GreenYellow => KnownColor.GreenYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Honeydew { get; private set; } + public static ISolidColorBrush Honeydew => KnownColor.Honeydew.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush HotPink { get; private set; } + public static ISolidColorBrush HotPink => KnownColor.HotPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush IndianRed { get; private set; } + public static ISolidColorBrush IndianRed => KnownColor.IndianRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Indigo { get; private set; } + public static ISolidColorBrush Indigo => KnownColor.Indigo.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Ivory { get; private set; } + public static ISolidColorBrush Ivory => KnownColor.Ivory.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Khaki { get; private set; } + public static ISolidColorBrush Khaki => KnownColor.Khaki.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lavender { get; private set; } + public static ISolidColorBrush Lavender => KnownColor.Lavender.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LavenderBlush { get; private set; } + public static ISolidColorBrush LavenderBlush => KnownColor.LavenderBlush.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LawnGreen { get; private set; } + public static ISolidColorBrush LawnGreen => KnownColor.LawnGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LemonChiffon { get; private set; } + public static ISolidColorBrush LemonChiffon => KnownColor.LemonChiffon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightBlue { get; private set; } + public static ISolidColorBrush LightBlue => KnownColor.LightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCoral { get; private set; } + public static ISolidColorBrush LightCoral => KnownColor.LightCoral.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightCyan { get; private set; } + public static ISolidColorBrush LightCyan => KnownColor.LightCyan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGoldenrodYellow { get; private set; } + public static ISolidColorBrush LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGray { get; private set; } + public static ISolidColorBrush LightGray => KnownColor.LightGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightGreen { get; private set; } + public static ISolidColorBrush LightGreen => KnownColor.LightGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightPink { get; private set; } + public static ISolidColorBrush LightPink => KnownColor.LightPink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSalmon { get; private set; } + public static ISolidColorBrush LightSalmon => KnownColor.LightSalmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSeaGreen { get; private set; } + public static ISolidColorBrush LightSeaGreen => KnownColor.LightSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSkyBlue { get; private set; } + public static ISolidColorBrush LightSkyBlue => KnownColor.LightSkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSlateGray { get; private set; } + public static ISolidColorBrush LightSlateGray => KnownColor.LightSlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightSteelBlue { get; private set; } + public static ISolidColorBrush LightSteelBlue => KnownColor.LightSteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LightYellow { get; private set; } + public static ISolidColorBrush LightYellow => KnownColor.LightYellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Lime { get; private set; } + public static ISolidColorBrush Lime => KnownColor.Lime.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush LimeGreen { get; private set; } + public static ISolidColorBrush LimeGreen => KnownColor.LimeGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Linen { get; private set; } + public static ISolidColorBrush Linen => KnownColor.Linen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Magenta { get; private set; } + public static ISolidColorBrush Magenta => KnownColor.Magenta.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Maroon { get; private set; } + public static ISolidColorBrush Maroon => KnownColor.Maroon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumAquamarine { get; private set; } + public static ISolidColorBrush MediumAquamarine => KnownColor.MediumAquamarine.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumBlue { get; private set; } + public static ISolidColorBrush MediumBlue => KnownColor.MediumBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumOrchid { get; private set; } + public static ISolidColorBrush MediumOrchid => KnownColor.MediumOrchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumPurple { get; private set; } + public static ISolidColorBrush MediumPurple => KnownColor.MediumPurple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSeaGreen { get; private set; } + public static ISolidColorBrush MediumSeaGreen => KnownColor.MediumSeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSlateBlue { get; private set; } + public static ISolidColorBrush MediumSlateBlue => KnownColor.MediumSlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumSpringGreen { get; private set; } + public static ISolidColorBrush MediumSpringGreen => KnownColor.MediumSpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumTurquoise { get; private set; } + public static ISolidColorBrush MediumTurquoise => KnownColor.MediumTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MediumVioletRed { get; private set; } + public static ISolidColorBrush MediumVioletRed => KnownColor.MediumVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MidnightBlue { get; private set; } + public static ISolidColorBrush MidnightBlue => KnownColor.MidnightBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MintCream { get; private set; } + public static ISolidColorBrush MintCream => KnownColor.MintCream.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush MistyRose { get; private set; } + public static ISolidColorBrush MistyRose => KnownColor.MistyRose.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Moccasin { get; private set; } + public static ISolidColorBrush Moccasin => KnownColor.Moccasin.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush NavajoWhite { get; private set; } + public static ISolidColorBrush NavajoWhite => KnownColor.NavajoWhite.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Navy { get; private set; } + public static ISolidColorBrush Navy => KnownColor.Navy.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OldLace { get; private set; } + public static ISolidColorBrush OldLace => KnownColor.OldLace.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Olive { get; private set; } + public static ISolidColorBrush Olive => KnownColor.Olive.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OliveDrab { get; private set; } + public static ISolidColorBrush OliveDrab => KnownColor.OliveDrab.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orange { get; private set; } + public static ISolidColorBrush Orange => KnownColor.Orange.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush OrangeRed { get; private set; } + public static ISolidColorBrush OrangeRed => KnownColor.OrangeRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Orchid { get; private set; } + public static ISolidColorBrush Orchid => KnownColor.Orchid.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGoldenrod { get; private set; } + public static ISolidColorBrush PaleGoldenrod => KnownColor.PaleGoldenrod.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleGreen { get; private set; } + public static ISolidColorBrush PaleGreen => KnownColor.PaleGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleTurquoise { get; private set; } + public static ISolidColorBrush PaleTurquoise => KnownColor.PaleTurquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PaleVioletRed { get; private set; } + public static ISolidColorBrush PaleVioletRed => KnownColor.PaleVioletRed.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PapayaWhip { get; private set; } + public static ISolidColorBrush PapayaWhip => KnownColor.PapayaWhip.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PeachPuff { get; private set; } + public static ISolidColorBrush PeachPuff => KnownColor.PeachPuff.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Peru { get; private set; } + public static ISolidColorBrush Peru => KnownColor.Peru.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Pink { get; private set; } + public static ISolidColorBrush Pink => KnownColor.Pink.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Plum { get; private set; } + public static ISolidColorBrush Plum => KnownColor.Plum.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush PowderBlue { get; private set; } + public static ISolidColorBrush PowderBlue => KnownColor.PowderBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Purple { get; private set; } + public static ISolidColorBrush Purple => KnownColor.Purple.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Red { get; private set; } + public static ISolidColorBrush Red => KnownColor.Red.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RosyBrown { get; private set; } + public static ISolidColorBrush RosyBrown => KnownColor.RosyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush RoyalBlue { get; private set; } + public static ISolidColorBrush RoyalBlue => KnownColor.RoyalBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SaddleBrown { get; private set; } + public static ISolidColorBrush SaddleBrown => KnownColor.SaddleBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Salmon { get; private set; } + public static ISolidColorBrush Salmon => KnownColor.Salmon.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SandyBrown { get; private set; } + public static ISolidColorBrush SandyBrown => KnownColor.SandyBrown.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaGreen { get; private set; } + public static ISolidColorBrush SeaGreen => KnownColor.SeaGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SeaShell { get; private set; } + public static ISolidColorBrush SeaShell => KnownColor.SeaShell.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Sienna { get; private set; } + public static ISolidColorBrush Sienna => KnownColor.Sienna.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Silver { get; private set; } + public static ISolidColorBrush Silver => KnownColor.Silver.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SkyBlue { get; private set; } + public static ISolidColorBrush SkyBlue => KnownColor.SkyBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateBlue { get; private set; } + public static ISolidColorBrush SlateBlue => KnownColor.SlateBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SlateGray { get; private set; } + public static ISolidColorBrush SlateGray => KnownColor.SlateGray.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Snow { get; private set; } + public static ISolidColorBrush Snow => KnownColor.Snow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SpringGreen { get; private set; } + public static ISolidColorBrush SpringGreen => KnownColor.SpringGreen.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush SteelBlue { get; private set; } + public static ISolidColorBrush SteelBlue => KnownColor.SteelBlue.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tan { get; private set; } + public static ISolidColorBrush Tan => KnownColor.Tan.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Teal { get; private set; } + public static ISolidColorBrush Teal => KnownColor.Teal.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Thistle { get; private set; } + public static ISolidColorBrush Thistle => KnownColor.Thistle.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Tomato { get; private set; } + public static ISolidColorBrush Tomato => KnownColor.Tomato.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Transparent { get; private set; } + public static ISolidColorBrush Transparent => KnownColor.Transparent.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Turquoise { get; private set; } + public static ISolidColorBrush Turquoise => KnownColor.Turquoise.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Violet { get; private set; } + public static ISolidColorBrush Violet => KnownColor.Violet.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Wheat { get; private set; } + public static ISolidColorBrush Wheat => KnownColor.Wheat.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush White { get; private set; } + public static ISolidColorBrush White => KnownColor.White.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush WhiteSmoke { get; private set; } + public static ISolidColorBrush WhiteSmoke => KnownColor.WhiteSmoke.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush Yellow { get; private set; } + public static ISolidColorBrush Yellow => KnownColor.Yellow.ToBrush(); /// /// Gets an colored brush. /// - public static ISolidColorBrush YellowGreen { get; private set; } + public static ISolidColorBrush YellowGreen => KnownColor.YellowGreen.ToBrush(); } } diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index cbf5a86fd6..82cc19347a 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -88,6 +88,9 @@ namespace Avalonia.Media /// The . public static Color Parse(string s) { + if (s == null) throw new ArgumentNullException(nameof(s)); + if (s.Length == 0) throw new FormatException(); + if (s[0] == '#') { var or = 0u; @@ -103,21 +106,15 @@ namespace Avalonia.Media return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or); } - else - { - var upper = s.ToUpperInvariant(); - var member = typeof(Colors).GetTypeInfo().DeclaredProperties - .FirstOrDefault(x => x.Name.ToUpperInvariant() == upper); - if (member != null) - { - return (Color)member.GetValue(null); - } - else - { - throw new FormatException($"Invalid color string: '{s}'."); - } + var knownColor = KnownColors.GetKnownColor(s); + + if (knownColor != KnownColor.None) + { + return knownColor.ToColor(); } + + throw new FormatException($"Invalid color string: '{s}'."); } /// @@ -128,8 +125,8 @@ namespace Avalonia.Media /// public override string ToString() { - uint rgb = ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B; - return $"#{rgb:x8}"; + uint rgb = ToUint32(); + return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb:x8}"; } /// diff --git a/src/Avalonia.Visuals/Media/Colors.cs b/src/Avalonia.Visuals/Media/Colors.cs index 7296d7dd39..1067cf66aa 100644 --- a/src/Avalonia.Visuals/Media/Colors.cs +++ b/src/Avalonia.Visuals/Media/Colors.cs @@ -1,6 +1,8 @@ // 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.Linq; + namespace Avalonia.Media { /// @@ -11,706 +13,706 @@ namespace Avalonia.Media /// /// Gets a color with an ARGB value of #fff0f8ff. /// - public static Color AliceBlue => Color.FromUInt32(0xfff0f8ff); + public static Color AliceBlue => KnownColor.AliceBlue.ToColor(); /// /// Gets a color with an ARGB value of #fffaebd7. /// - public static Color AntiqueWhite => Color.FromUInt32(0xfffaebd7); + public static Color AntiqueWhite => KnownColor.AntiqueWhite.ToColor(); /// /// Gets a color with an ARGB value of #ff00ffff. /// - public static Color Aqua => Color.FromUInt32(0xff00ffff); + public static Color Aqua => KnownColor.Aqua.ToColor(); /// /// Gets a color with an ARGB value of #ff7fffd4. /// - public static Color Aquamarine => Color.FromUInt32(0xff7fffd4); + public static Color Aquamarine => KnownColor.Aquamarine.ToColor(); /// /// Gets a color with an ARGB value of #fff0ffff. /// - public static Color Azure => Color.FromUInt32(0xfff0ffff); + public static Color Azure => KnownColor.Azure.ToColor(); /// /// Gets a color with an ARGB value of #fff5f5dc. /// - public static Color Beige => Color.FromUInt32(0xfff5f5dc); + public static Color Beige => KnownColor.Beige.ToColor(); /// /// Gets a color with an ARGB value of #ffffe4c4. /// - public static Color Bisque => Color.FromUInt32(0xffffe4c4); + public static Color Bisque => KnownColor.Bisque.ToColor(); /// /// Gets a color with an ARGB value of #ff000000. /// - public static Color Black => Color.FromUInt32(0xff000000); + public static Color Black => KnownColor.Black.ToColor(); /// /// Gets a color with an ARGB value of #ffffebcd. /// - public static Color BlanchedAlmond => Color.FromUInt32(0xffffebcd); + public static Color BlanchedAlmond => KnownColor.BlanchedAlmond.ToColor(); /// /// Gets a color with an ARGB value of #ff0000ff. /// - public static Color Blue => Color.FromUInt32(0xff0000ff); + public static Color Blue => KnownColor.Blue.ToColor(); /// /// Gets a color with an ARGB value of #ff8a2be2. /// - public static Color BlueViolet => Color.FromUInt32(0xff8a2be2); + public static Color BlueViolet => KnownColor.BlueViolet.ToColor(); /// /// Gets a color with an ARGB value of #ffa52a2a. /// - public static Color Brown => Color.FromUInt32(0xffa52a2a); + public static Color Brown => KnownColor.Brown.ToColor(); /// /// Gets a color with an ARGB value of #ffdeb887. /// - public static Color BurlyWood => Color.FromUInt32(0xffdeb887); + public static Color BurlyWood => KnownColor.BurlyWood.ToColor(); /// /// Gets a color with an ARGB value of #ff5f9ea0. /// - public static Color CadetBlue => Color.FromUInt32(0xff5f9ea0); + public static Color CadetBlue => KnownColor.CadetBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff7fff00. /// - public static Color Chartreuse => Color.FromUInt32(0xff7fff00); + public static Color Chartreuse => KnownColor.Chartreuse.ToColor(); /// /// Gets a color with an ARGB value of #ffd2691e. /// - public static Color Chocolate => Color.FromUInt32(0xffd2691e); + public static Color Chocolate => KnownColor.Chocolate.ToColor(); /// /// Gets a color with an ARGB value of #ffff7f50. /// - public static Color Coral => Color.FromUInt32(0xffff7f50); + public static Color Coral => KnownColor.Coral.ToColor(); /// /// Gets a color with an ARGB value of #ff6495ed. /// - public static Color CornflowerBlue => Color.FromUInt32(0xff6495ed); + public static Color CornflowerBlue => KnownColor.CornflowerBlue.ToColor(); /// /// Gets a color with an ARGB value of #fffff8dc. /// - public static Color Cornsilk => Color.FromUInt32(0xfffff8dc); + public static Color Cornsilk => KnownColor.Cornsilk.ToColor(); /// /// Gets a color with an ARGB value of #ffdc143c. /// - public static Color Crimson => Color.FromUInt32(0xffdc143c); + public static Color Crimson => KnownColor.Crimson.ToColor(); /// /// Gets a color with an ARGB value of #ff00ffff. /// - public static Color Cyan => Color.FromUInt32(0xff00ffff); + public static Color Cyan => KnownColor.Cyan.ToColor(); /// /// Gets a color with an ARGB value of #ff00008b. /// - public static Color DarkBlue => Color.FromUInt32(0xff00008b); + public static Color DarkBlue => KnownColor.DarkBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff008b8b. /// - public static Color DarkCyan => Color.FromUInt32(0xff008b8b); + public static Color DarkCyan => KnownColor.DarkCyan.ToColor(); /// /// Gets a color with an ARGB value of #ffb8860b. /// - public static Color DarkGoldenrod => Color.FromUInt32(0xffb8860b); + public static Color DarkGoldenrod => KnownColor.DarkGoldenrod.ToColor(); /// /// Gets a color with an ARGB value of #ffa9a9a9. /// - public static Color DarkGray => Color.FromUInt32(0xffa9a9a9); + public static Color DarkGray => KnownColor.DarkGray.ToColor(); /// /// Gets a color with an ARGB value of #ff006400. /// - public static Color DarkGreen => Color.FromUInt32(0xff006400); + public static Color DarkGreen => KnownColor.DarkGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffbdb76b. /// - public static Color DarkKhaki => Color.FromUInt32(0xffbdb76b); + public static Color DarkKhaki => KnownColor.DarkKhaki.ToColor(); /// /// Gets a color with an ARGB value of #ff8b008b. /// - public static Color DarkMagenta => Color.FromUInt32(0xff8b008b); + public static Color DarkMagenta => KnownColor.DarkMagenta.ToColor(); /// /// Gets a color with an ARGB value of #ff556b2f. /// - public static Color DarkOliveGreen => Color.FromUInt32(0xff556b2f); + public static Color DarkOliveGreen => KnownColor.DarkOliveGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffff8c00. /// - public static Color DarkOrange => Color.FromUInt32(0xffff8c00); + public static Color DarkOrange => KnownColor.DarkOrange.ToColor(); /// /// Gets a color with an ARGB value of #ff9932cc. /// - public static Color DarkOrchid => Color.FromUInt32(0xff9932cc); + public static Color DarkOrchid => KnownColor.DarkOrchid.ToColor(); /// /// Gets a color with an ARGB value of #ff8b0000. /// - public static Color DarkRed => Color.FromUInt32(0xff8b0000); + public static Color DarkRed => KnownColor.DarkRed.ToColor(); /// /// Gets a color with an ARGB value of #ffe9967a. /// - public static Color DarkSalmon => Color.FromUInt32(0xffe9967a); + public static Color DarkSalmon => KnownColor.DarkSalmon.ToColor(); /// /// Gets a color with an ARGB value of #ff8fbc8f. /// - public static Color DarkSeaGreen => Color.FromUInt32(0xff8fbc8f); + public static Color DarkSeaGreen => KnownColor.DarkSeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff483d8b. /// - public static Color DarkSlateBlue => Color.FromUInt32(0xff483d8b); + public static Color DarkSlateBlue => KnownColor.DarkSlateBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff2f4f4f. /// - public static Color DarkSlateGray => Color.FromUInt32(0xff2f4f4f); + public static Color DarkSlateGray => KnownColor.DarkSlateGray.ToColor(); /// /// Gets a color with an ARGB value of #ff00ced1. /// - public static Color DarkTurquoise => Color.FromUInt32(0xff00ced1); + public static Color DarkTurquoise => KnownColor.DarkTurquoise.ToColor(); /// /// Gets a color with an ARGB value of #ff9400d3. /// - public static Color DarkViolet => Color.FromUInt32(0xff9400d3); + public static Color DarkViolet => KnownColor.DarkViolet.ToColor(); /// /// Gets a color with an ARGB value of #ffff1493. /// - public static Color DeepPink => Color.FromUInt32(0xffff1493); + public static Color DeepPink => KnownColor.DeepPink.ToColor(); /// /// Gets a color with an ARGB value of #ff00bfff. /// - public static Color DeepSkyBlue => Color.FromUInt32(0xff00bfff); + public static Color DeepSkyBlue => KnownColor.DeepSkyBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff696969. /// - public static Color DimGray => Color.FromUInt32(0xff696969); + public static Color DimGray => KnownColor.DimGray.ToColor(); /// /// Gets a color with an ARGB value of #ff1e90ff. /// - public static Color DodgerBlue => Color.FromUInt32(0xff1e90ff); + public static Color DodgerBlue => KnownColor.DodgerBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffb22222. /// - public static Color Firebrick => Color.FromUInt32(0xffb22222); + public static Color Firebrick => KnownColor.Firebrick.ToColor(); /// /// Gets a color with an ARGB value of #fffffaf0. /// - public static Color FloralWhite => Color.FromUInt32(0xfffffaf0); + public static Color FloralWhite => KnownColor.FloralWhite.ToColor(); /// /// Gets a color with an ARGB value of #ff228b22. /// - public static Color ForestGreen => Color.FromUInt32(0xff228b22); + public static Color ForestGreen => KnownColor.ForestGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffff00ff. /// - public static Color Fuchsia => Color.FromUInt32(0xffff00ff); + public static Color Fuchsia => KnownColor.Fuchsia.ToColor(); /// /// Gets a color with an ARGB value of #ffdcdcdc. /// - public static Color Gainsboro => Color.FromUInt32(0xffdcdcdc); + public static Color Gainsboro => KnownColor.Gainsboro.ToColor(); /// /// Gets a color with an ARGB value of #fff8f8ff. /// - public static Color GhostWhite => Color.FromUInt32(0xfff8f8ff); + public static Color GhostWhite => KnownColor.GhostWhite.ToColor(); /// /// Gets a color with an ARGB value of #ffffd700. /// - public static Color Gold => Color.FromUInt32(0xffffd700); + public static Color Gold => KnownColor.Gold.ToColor(); /// /// Gets a color with an ARGB value of #ffdaa520. /// - public static Color Goldenrod => Color.FromUInt32(0xffdaa520); + public static Color Goldenrod => KnownColor.Goldenrod.ToColor(); /// /// Gets a color with an ARGB value of #ff808080. /// - public static Color Gray => Color.FromUInt32(0xff808080); + public static Color Gray => KnownColor.Gray.ToColor(); /// /// Gets a color with an ARGB value of #ff008000. /// - public static Color Green => Color.FromUInt32(0xff008000); + public static Color Green => KnownColor.Green.ToColor(); /// /// Gets a color with an ARGB value of #ffadff2f. /// - public static Color GreenYellow => Color.FromUInt32(0xffadff2f); + public static Color GreenYellow => KnownColor.GreenYellow.ToColor(); /// /// Gets a color with an ARGB value of #fff0fff0. /// - public static Color Honeydew => Color.FromUInt32(0xfff0fff0); + public static Color Honeydew => KnownColor.Honeydew.ToColor(); /// /// Gets a color with an ARGB value of #ffff69b4. /// - public static Color HotPink => Color.FromUInt32(0xffff69b4); + public static Color HotPink => KnownColor.HotPink.ToColor(); /// /// Gets a color with an ARGB value of #ffcd5c5c. /// - public static Color IndianRed => Color.FromUInt32(0xffcd5c5c); + public static Color IndianRed => KnownColor.IndianRed.ToColor(); /// /// Gets a color with an ARGB value of #ff4b0082. /// - public static Color Indigo => Color.FromUInt32(0xff4b0082); + public static Color Indigo => KnownColor.Indigo.ToColor(); /// /// Gets a color with an ARGB value of #fffffff0. /// - public static Color Ivory => Color.FromUInt32(0xfffffff0); + public static Color Ivory => KnownColor.Ivory.ToColor(); /// /// Gets a color with an ARGB value of #fff0e68c. /// - public static Color Khaki => Color.FromUInt32(0xfff0e68c); + public static Color Khaki => KnownColor.Khaki.ToColor(); /// /// Gets a color with an ARGB value of #ffe6e6fa. /// - public static Color Lavender => Color.FromUInt32(0xffe6e6fa); + public static Color Lavender => KnownColor.Lavender.ToColor(); /// /// Gets a color with an ARGB value of #fffff0f5. /// - public static Color LavenderBlush => Color.FromUInt32(0xfffff0f5); + public static Color LavenderBlush => KnownColor.LavenderBlush.ToColor(); /// /// Gets a color with an ARGB value of #ff7cfc00. /// - public static Color LawnGreen => Color.FromUInt32(0xff7cfc00); + public static Color LawnGreen => KnownColor.LawnGreen.ToColor(); /// /// Gets a color with an ARGB value of #fffffacd. /// - public static Color LemonChiffon => Color.FromUInt32(0xfffffacd); + public static Color LemonChiffon => KnownColor.LemonChiffon.ToColor(); /// /// Gets a color with an ARGB value of #ffadd8e6. /// - public static Color LightBlue => Color.FromUInt32(0xffadd8e6); + public static Color LightBlue => KnownColor.LightBlue.ToColor(); /// /// Gets a color with an ARGB value of #fff08080. /// - public static Color LightCoral => Color.FromUInt32(0xfff08080); + public static Color LightCoral => KnownColor.LightCoral.ToColor(); /// /// Gets a color with an ARGB value of #ffe0ffff. /// - public static Color LightCyan => Color.FromUInt32(0xffe0ffff); + public static Color LightCyan => KnownColor.LightCyan.ToColor(); /// /// Gets a color with an ARGB value of #fffafad2. /// - public static Color LightGoldenrodYellow => Color.FromUInt32(0xfffafad2); + public static Color LightGoldenrodYellow => KnownColor.LightGoldenrodYellow.ToColor(); /// /// Gets a color with an ARGB value of #ffd3d3d3. /// - public static Color LightGray => Color.FromUInt32(0xffd3d3d3); + public static Color LightGray => KnownColor.LightGray.ToColor(); /// /// Gets a color with an ARGB value of #ff90ee90. /// - public static Color LightGreen => Color.FromUInt32(0xff90ee90); + public static Color LightGreen => KnownColor.LightGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffffb6c1. /// - public static Color LightPink => Color.FromUInt32(0xffffb6c1); + public static Color LightPink => KnownColor.LightPink.ToColor(); /// /// Gets a color with an ARGB value of #ffffa07a. /// - public static Color LightSalmon => Color.FromUInt32(0xffffa07a); + public static Color LightSalmon => KnownColor.LightSalmon.ToColor(); /// /// Gets a color with an ARGB value of #ff20b2aa. /// - public static Color LightSeaGreen => Color.FromUInt32(0xff20b2aa); + public static Color LightSeaGreen => KnownColor.LightSeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff87cefa. /// - public static Color LightSkyBlue => Color.FromUInt32(0xff87cefa); + public static Color LightSkyBlue => KnownColor.LightSkyBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff778899. /// - public static Color LightSlateGray => Color.FromUInt32(0xff778899); + public static Color LightSlateGray => KnownColor.LightSlateGray.ToColor(); /// /// Gets a color with an ARGB value of #ffb0c4de. /// - public static Color LightSteelBlue => Color.FromUInt32(0xffb0c4de); + public static Color LightSteelBlue => KnownColor.LightSteelBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffffffe0. /// - public static Color LightYellow => Color.FromUInt32(0xffffffe0); + public static Color LightYellow => KnownColor.LightYellow.ToColor(); /// /// Gets a color with an ARGB value of #ff00ff00. /// - public static Color Lime => Color.FromUInt32(0xff00ff00); + public static Color Lime => KnownColor.Lime.ToColor(); /// /// Gets a color with an ARGB value of #ff32cd32. /// - public static Color LimeGreen => Color.FromUInt32(0xff32cd32); + public static Color LimeGreen => KnownColor.LimeGreen.ToColor(); /// /// Gets a color with an ARGB value of #fffaf0e6. /// - public static Color Linen => Color.FromUInt32(0xfffaf0e6); + public static Color Linen => KnownColor.Linen.ToColor(); /// /// Gets a color with an ARGB value of #ffff00ff. /// - public static Color Magenta => Color.FromUInt32(0xffff00ff); + public static Color Magenta => KnownColor.Magenta.ToColor(); /// /// Gets a color with an ARGB value of #ff800000. /// - public static Color Maroon => Color.FromUInt32(0xff800000); + public static Color Maroon => KnownColor.Maroon.ToColor(); /// /// Gets a color with an ARGB value of #ff66cdaa. /// - public static Color MediumAquamarine => Color.FromUInt32(0xff66cdaa); + public static Color MediumAquamarine => KnownColor.MediumAquamarine.ToColor(); /// /// Gets a color with an ARGB value of #ff0000cd. /// - public static Color MediumBlue => Color.FromUInt32(0xff0000cd); + public static Color MediumBlue => KnownColor.MediumBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffba55d3. /// - public static Color MediumOrchid => Color.FromUInt32(0xffba55d3); + public static Color MediumOrchid => KnownColor.MediumOrchid.ToColor(); /// /// Gets a color with an ARGB value of #ff9370db. /// - public static Color MediumPurple => Color.FromUInt32(0xff9370db); + public static Color MediumPurple => KnownColor.MediumPurple.ToColor(); /// /// Gets a color with an ARGB value of #ff3cb371. /// - public static Color MediumSeaGreen => Color.FromUInt32(0xff3cb371); + public static Color MediumSeaGreen => KnownColor.MediumSeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff7b68ee. /// - public static Color MediumSlateBlue => Color.FromUInt32(0xff7b68ee); + public static Color MediumSlateBlue => KnownColor.MediumSlateBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff00fa9a. /// - public static Color MediumSpringGreen => Color.FromUInt32(0xff00fa9a); + public static Color MediumSpringGreen => KnownColor.MediumSpringGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff48d1cc. /// - public static Color MediumTurquoise => Color.FromUInt32(0xff48d1cc); + public static Color MediumTurquoise => KnownColor.MediumTurquoise.ToColor(); /// /// Gets a color with an ARGB value of #ffc71585. /// - public static Color MediumVioletRed => Color.FromUInt32(0xffc71585); + public static Color MediumVioletRed => KnownColor.MediumVioletRed.ToColor(); /// /// Gets a color with an ARGB value of #ff191970. /// - public static Color MidnightBlue => Color.FromUInt32(0xff191970); + public static Color MidnightBlue => KnownColor.MidnightBlue.ToColor(); /// /// Gets a color with an ARGB value of #fff5fffa. /// - public static Color MintCream => Color.FromUInt32(0xfff5fffa); + public static Color MintCream => KnownColor.MintCream.ToColor(); /// /// Gets a color with an ARGB value of #ffffe4e1. /// - public static Color MistyRose => Color.FromUInt32(0xffffe4e1); + public static Color MistyRose => KnownColor.MistyRose.ToColor(); /// /// Gets a color with an ARGB value of #ffffe4b5. /// - public static Color Moccasin => Color.FromUInt32(0xffffe4b5); + public static Color Moccasin => KnownColor.Moccasin.ToColor(); /// /// Gets a color with an ARGB value of #ffffdead. /// - public static Color NavajoWhite => Color.FromUInt32(0xffffdead); + public static Color NavajoWhite => KnownColor.NavajoWhite.ToColor(); /// /// Gets a color with an ARGB value of #ff000080. /// - public static Color Navy => Color.FromUInt32(0xff000080); + public static Color Navy => KnownColor.Navy.ToColor(); /// /// Gets a color with an ARGB value of #fffdf5e6. /// - public static Color OldLace => Color.FromUInt32(0xfffdf5e6); + public static Color OldLace => KnownColor.OldLace.ToColor(); /// /// Gets a color with an ARGB value of #ff808000. /// - public static Color Olive => Color.FromUInt32(0xff808000); + public static Color Olive => KnownColor.Olive.ToColor(); /// /// Gets a color with an ARGB value of #ff6b8e23. /// - public static Color OliveDrab => Color.FromUInt32(0xff6b8e23); + public static Color OliveDrab => KnownColor.OliveDrab.ToColor(); /// /// Gets a color with an ARGB value of #ffffa500. /// - public static Color Orange => Color.FromUInt32(0xffffa500); + public static Color Orange => KnownColor.Orange.ToColor(); /// /// Gets a color with an ARGB value of #ffff4500. /// - public static Color OrangeRed => Color.FromUInt32(0xffff4500); + public static Color OrangeRed => KnownColor.OrangeRed.ToColor(); /// /// Gets a color with an ARGB value of #ffda70d6. /// - public static Color Orchid => Color.FromUInt32(0xffda70d6); + public static Color Orchid => KnownColor.Orchid.ToColor(); /// /// Gets a color with an ARGB value of #ffeee8aa. /// - public static Color PaleGoldenrod => Color.FromUInt32(0xffeee8aa); + public static Color PaleGoldenrod => KnownColor.PaleGoldenrod.ToColor(); /// /// Gets a color with an ARGB value of #ff98fb98. /// - public static Color PaleGreen => Color.FromUInt32(0xff98fb98); + public static Color PaleGreen => KnownColor.PaleGreen.ToColor(); /// /// Gets a color with an ARGB value of #ffafeeee. /// - public static Color PaleTurquoise => Color.FromUInt32(0xffafeeee); + public static Color PaleTurquoise => KnownColor.PaleTurquoise.ToColor(); /// /// Gets a color with an ARGB value of #ffdb7093. /// - public static Color PaleVioletRed => Color.FromUInt32(0xffdb7093); + public static Color PaleVioletRed => KnownColor.PaleVioletRed.ToColor(); /// /// Gets a color with an ARGB value of #ffffefd5. /// - public static Color PapayaWhip => Color.FromUInt32(0xffffefd5); + public static Color PapayaWhip => KnownColor.PapayaWhip.ToColor(); /// /// Gets a color with an ARGB value of #ffffdab9. /// - public static Color PeachPuff => Color.FromUInt32(0xffffdab9); + public static Color PeachPuff => KnownColor.PeachPuff.ToColor(); /// /// Gets a color with an ARGB value of #ffcd853f. /// - public static Color Peru => Color.FromUInt32(0xffcd853f); + public static Color Peru => KnownColor.Peru.ToColor(); /// /// Gets a color with an ARGB value of #ffffc0cb. /// - public static Color Pink => Color.FromUInt32(0xffffc0cb); + public static Color Pink => KnownColor.Pink.ToColor(); /// /// Gets a color with an ARGB value of #ffdda0dd. /// - public static Color Plum => Color.FromUInt32(0xffdda0dd); + public static Color Plum => KnownColor.Plum.ToColor(); /// /// Gets a color with an ARGB value of #ffb0e0e6. /// - public static Color PowderBlue => Color.FromUInt32(0xffb0e0e6); + public static Color PowderBlue => KnownColor.PowderBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff800080. /// - public static Color Purple => Color.FromUInt32(0xff800080); + public static Color Purple => KnownColor.Purple.ToColor(); /// /// Gets a color with an ARGB value of #ffff0000. /// - public static Color Red => Color.FromUInt32(0xffff0000); + public static Color Red => KnownColor.Red.ToColor(); /// /// Gets a color with an ARGB value of #ffbc8f8f. /// - public static Color RosyBrown => Color.FromUInt32(0xffbc8f8f); + public static Color RosyBrown => KnownColor.RosyBrown.ToColor(); /// /// Gets a color with an ARGB value of #ff4169e1. /// - public static Color RoyalBlue => Color.FromUInt32(0xff4169e1); + public static Color RoyalBlue => KnownColor.RoyalBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff8b4513. /// - public static Color SaddleBrown => Color.FromUInt32(0xff8b4513); + public static Color SaddleBrown => KnownColor.SaddleBrown.ToColor(); /// /// Gets a color with an ARGB value of #fffa8072. /// - public static Color Salmon => Color.FromUInt32(0xfffa8072); + public static Color Salmon => KnownColor.Salmon.ToColor(); /// /// Gets a color with an ARGB value of #fff4a460. /// - public static Color SandyBrown => Color.FromUInt32(0xfff4a460); + public static Color SandyBrown => KnownColor.SandyBrown.ToColor(); /// /// Gets a color with an ARGB value of #ff2e8b57. /// - public static Color SeaGreen => Color.FromUInt32(0xff2e8b57); + public static Color SeaGreen => KnownColor.SeaGreen.ToColor(); /// /// Gets a color with an ARGB value of #fffff5ee. /// - public static Color SeaShell => Color.FromUInt32(0xfffff5ee); + public static Color SeaShell => KnownColor.SeaShell.ToColor(); /// /// Gets a color with an ARGB value of #ffa0522d. /// - public static Color Sienna => Color.FromUInt32(0xffa0522d); + public static Color Sienna => KnownColor.Sienna.ToColor(); /// /// Gets a color with an ARGB value of #ffc0c0c0. /// - public static Color Silver => Color.FromUInt32(0xffc0c0c0); + public static Color Silver => KnownColor.Silver.ToColor(); /// /// Gets a color with an ARGB value of #ff87ceeb. /// - public static Color SkyBlue => Color.FromUInt32(0xff87ceeb); + public static Color SkyBlue => KnownColor.SkyBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff6a5acd. /// - public static Color SlateBlue => Color.FromUInt32(0xff6a5acd); + public static Color SlateBlue => KnownColor.SlateBlue.ToColor(); /// /// Gets a color with an ARGB value of #ff708090. /// - public static Color SlateGray => Color.FromUInt32(0xff708090); + public static Color SlateGray => KnownColor.SlateGray.ToColor(); /// /// Gets a color with an ARGB value of #fffffafa. /// - public static Color Snow => Color.FromUInt32(0xfffffafa); + public static Color Snow => KnownColor.Snow.ToColor(); /// /// Gets a color with an ARGB value of #ff00ff7f. /// - public static Color SpringGreen => Color.FromUInt32(0xff00ff7f); + public static Color SpringGreen => KnownColor.SpringGreen.ToColor(); /// /// Gets a color with an ARGB value of #ff4682b4. /// - public static Color SteelBlue => Color.FromUInt32(0xff4682b4); + public static Color SteelBlue => KnownColor.SteelBlue.ToColor(); /// /// Gets a color with an ARGB value of #ffd2b48c. /// - public static Color Tan => Color.FromUInt32(0xffd2b48c); + public static Color Tan => KnownColor.Tan.ToColor(); /// /// Gets a color with an ARGB value of #ff008080. /// - public static Color Teal => Color.FromUInt32(0xff008080); + public static Color Teal => KnownColor.Teal.ToColor(); /// /// Gets a color with an ARGB value of #ffd8bfd8. /// - public static Color Thistle => Color.FromUInt32(0xffd8bfd8); + public static Color Thistle => KnownColor.Thistle.ToColor(); /// /// Gets a color with an ARGB value of #ffff6347. /// - public static Color Tomato => Color.FromUInt32(0xffff6347); + public static Color Tomato => KnownColor.Tomato.ToColor(); /// /// Gets a color with an ARGB value of #00ffffff. /// - public static Color Transparent => Color.FromUInt32(0x00ffffff); + public static Color Transparent => KnownColor.Transparent.ToColor(); /// /// Gets a color with an ARGB value of #ff40e0d0. /// - public static Color Turquoise => Color.FromUInt32(0xff40e0d0); + public static Color Turquoise => KnownColor.Turquoise.ToColor(); /// /// Gets a color with an ARGB value of #ffee82ee. /// - public static Color Violet => Color.FromUInt32(0xffee82ee); + public static Color Violet => KnownColor.Violet.ToColor(); /// /// Gets a color with an ARGB value of #fff5deb3. /// - public static Color Wheat => Color.FromUInt32(0xfff5deb3); + public static Color Wheat => KnownColor.Wheat.ToColor(); /// /// Gets a color with an ARGB value of #ffffffff. /// - public static Color White => Color.FromUInt32(0xffffffff); + public static Color White => KnownColor.White.ToColor(); /// /// Gets a color with an ARGB value of #fff5f5f5. /// - public static Color WhiteSmoke => Color.FromUInt32(0xfff5f5f5); + public static Color WhiteSmoke => KnownColor.WhiteSmoke.ToColor(); /// /// Gets a color with an ARGB value of #ffffff00. /// - public static Color Yellow => Color.FromUInt32(0xffffff00); + public static Color Yellow => KnownColor.Yellow.ToColor(); /// /// Gets a color with an ARGB value of #ff9acd32. /// - public static Color YellowGreen => Color.FromUInt32(0xff9acd32); + public static Color YellowGreen => KnownColor.YellowGreen.ToColor(); } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Media/KnownColors.cs b/src/Avalonia.Visuals/Media/KnownColors.cs new file mode 100644 index 0000000000..0887d2c913 --- /dev/null +++ b/src/Avalonia.Visuals/Media/KnownColors.cs @@ -0,0 +1,224 @@ +using System; +using System.Reflection; +using System.Collections.Generic; + +namespace Avalonia.Media +{ + internal static class KnownColors + { + private static readonly IReadOnlyDictionary _knownColorNames; + private static readonly IReadOnlyDictionary _knownColors; + private static readonly Dictionary _knownBrushes; + + static KnownColors() + { + var knownColorNames = new Dictionary(StringComparer.OrdinalIgnoreCase); + var knownColors = new Dictionary(); + + foreach (var field in typeof(KnownColor).GetRuntimeFields()) + { + if (field.FieldType != typeof(KnownColor)) continue; + var knownColor = (KnownColor)field.GetValue(null); + if (knownColor == KnownColor.None) continue; + + knownColorNames.Add(field.Name, knownColor); + + // some known colors have the same value, so use the first + if (!knownColors.ContainsKey((uint)knownColor)) + { + knownColors.Add((uint)knownColor, field.Name); + } + } + + _knownColorNames = knownColorNames; + _knownColors = knownColors; + _knownBrushes = new Dictionary(); + } + + public static ISolidColorBrush GetKnownBrush(string s) + { + var color = GetKnownColor(s); + return color != KnownColor.None ? color.ToBrush() : null; + } + + public static KnownColor GetKnownColor(string s) + { + if (_knownColorNames.TryGetValue(s, out var color)) + { + return color; + } + + return KnownColor.None; + } + + public static string GetKnownColorName(uint rgb) + { + return _knownColors.TryGetValue(rgb, out var name) ? name : null; + } + + public static Color ToColor(this KnownColor color) + { + return Color.FromUInt32((uint)color); + } + + public static ISolidColorBrush ToBrush(this KnownColor color) + { + lock (_knownBrushes) + { + if (!_knownBrushes.TryGetValue(color, out var brush)) + { + brush = new Immutable.ImmutableSolidColorBrush(color.ToColor()); + _knownBrushes.Add(color, brush); + } + + return brush; + } + } + } + + internal enum KnownColor : uint + { + None, + AliceBlue = 0xfff0f8ff, + AntiqueWhite = 0xfffaebd7, + Aqua = 0xff00ffff, + Aquamarine = 0xff7fffd4, + Azure = 0xfff0ffff, + Beige = 0xfff5f5dc, + Bisque = 0xffffe4c4, + Black = 0xff000000, + BlanchedAlmond = 0xffffebcd, + Blue = 0xff0000ff, + BlueViolet = 0xff8a2be2, + Brown = 0xffa52a2a, + BurlyWood = 0xffdeb887, + CadetBlue = 0xff5f9ea0, + Chartreuse = 0xff7fff00, + Chocolate = 0xffd2691e, + Coral = 0xffff7f50, + CornflowerBlue = 0xff6495ed, + Cornsilk = 0xfffff8dc, + Crimson = 0xffdc143c, + Cyan = 0xff00ffff, + DarkBlue = 0xff00008b, + DarkCyan = 0xff008b8b, + DarkGoldenrod = 0xffb8860b, + DarkGray = 0xffa9a9a9, + DarkGreen = 0xff006400, + DarkKhaki = 0xffbdb76b, + DarkMagenta = 0xff8b008b, + DarkOliveGreen = 0xff556b2f, + DarkOrange = 0xffff8c00, + DarkOrchid = 0xff9932cc, + DarkRed = 0xff8b0000, + DarkSalmon = 0xffe9967a, + DarkSeaGreen = 0xff8fbc8f, + DarkSlateBlue = 0xff483d8b, + DarkSlateGray = 0xff2f4f4f, + DarkTurquoise = 0xff00ced1, + DarkViolet = 0xff9400d3, + DeepPink = 0xffff1493, + DeepSkyBlue = 0xff00bfff, + DimGray = 0xff696969, + DodgerBlue = 0xff1e90ff, + Firebrick = 0xffb22222, + FloralWhite = 0xfffffaf0, + ForestGreen = 0xff228b22, + Fuchsia = 0xffff00ff, + Gainsboro = 0xffdcdcdc, + GhostWhite = 0xfff8f8ff, + Gold = 0xffffd700, + Goldenrod = 0xffdaa520, + Gray = 0xff808080, + Green = 0xff008000, + GreenYellow = 0xffadff2f, + Honeydew = 0xfff0fff0, + HotPink = 0xffff69b4, + IndianRed = 0xffcd5c5c, + Indigo = 0xff4b0082, + Ivory = 0xfffffff0, + Khaki = 0xfff0e68c, + Lavender = 0xffe6e6fa, + LavenderBlush = 0xfffff0f5, + LawnGreen = 0xff7cfc00, + LemonChiffon = 0xfffffacd, + LightBlue = 0xffadd8e6, + LightCoral = 0xfff08080, + LightCyan = 0xffe0ffff, + LightGoldenrodYellow = 0xfffafad2, + LightGreen = 0xff90ee90, + LightGray = 0xffd3d3d3, + LightPink = 0xffffb6c1, + LightSalmon = 0xffffa07a, + LightSeaGreen = 0xff20b2aa, + LightSkyBlue = 0xff87cefa, + LightSlateGray = 0xff778899, + LightSteelBlue = 0xffb0c4de, + LightYellow = 0xffffffe0, + Lime = 0xff00ff00, + LimeGreen = 0xff32cd32, + Linen = 0xfffaf0e6, + Magenta = 0xffff00ff, + Maroon = 0xff800000, + MediumAquamarine = 0xff66cdaa, + MediumBlue = 0xff0000cd, + MediumOrchid = 0xffba55d3, + MediumPurple = 0xff9370db, + MediumSeaGreen = 0xff3cb371, + MediumSlateBlue = 0xff7b68ee, + MediumSpringGreen = 0xff00fa9a, + MediumTurquoise = 0xff48d1cc, + MediumVioletRed = 0xffc71585, + MidnightBlue = 0xff191970, + MintCream = 0xfff5fffa, + MistyRose = 0xffffe4e1, + Moccasin = 0xffffe4b5, + NavajoWhite = 0xffffdead, + Navy = 0xff000080, + OldLace = 0xfffdf5e6, + Olive = 0xff808000, + OliveDrab = 0xff6b8e23, + Orange = 0xffffa500, + OrangeRed = 0xffff4500, + Orchid = 0xffda70d6, + PaleGoldenrod = 0xffeee8aa, + PaleGreen = 0xff98fb98, + PaleTurquoise = 0xffafeeee, + PaleVioletRed = 0xffdb7093, + PapayaWhip = 0xffffefd5, + PeachPuff = 0xffffdab9, + Peru = 0xffcd853f, + Pink = 0xffffc0cb, + Plum = 0xffdda0dd, + PowderBlue = 0xffb0e0e6, + Purple = 0xff800080, + Red = 0xffff0000, + RosyBrown = 0xffbc8f8f, + RoyalBlue = 0xff4169e1, + SaddleBrown = 0xff8b4513, + Salmon = 0xfffa8072, + SandyBrown = 0xfff4a460, + SeaGreen = 0xff2e8b57, + SeaShell = 0xfffff5ee, + Sienna = 0xffa0522d, + Silver = 0xffc0c0c0, + SkyBlue = 0xff87ceeb, + SlateBlue = 0xff6a5acd, + SlateGray = 0xff708090, + Snow = 0xfffffafa, + SpringGreen = 0xff00ff7f, + SteelBlue = 0xff4682b4, + Tan = 0xffd2b48c, + Teal = 0xff008080, + Thistle = 0xffd8bfd8, + Tomato = 0xffff6347, + Transparent = 0x00ffffff, + Turquoise = 0xff40e0d0, + Violet = 0xffee82ee, + Wheat = 0xfff5deb3, + White = 0xffffffff, + WhiteSmoke = 0xfff5f5f5, + Yellow = 0xffffff00, + YellowGreen = 0xff9acd32 + } +} \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs index ae88a94073..a6015c52e5 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/BrushTests.cs @@ -12,7 +12,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_RGB_Hash_Brush() { - var result = (SolidColorBrush)Brush.Parse("#ff8844"); + var result = (ISolidColorBrush)Brush.Parse("#ff8844"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x88, result.Color.G); @@ -23,7 +23,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_ARGB_Hash_Brush() { - var result = (SolidColorBrush)Brush.Parse("#40ff8844"); + var result = (ISolidColorBrush)Brush.Parse("#40ff8844"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x88, result.Color.G); @@ -34,7 +34,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_Named_Brush_Lowercase() { - var result = (SolidColorBrush)Brush.Parse("red"); + var result = (ISolidColorBrush)Brush.Parse("red"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x00, result.Color.G); @@ -45,7 +45,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Parse_Parses_Named_Brush_Uppercase() { - var result = (SolidColorBrush)Brush.Parse("RED"); + var result = (ISolidColorBrush)Brush.Parse("RED"); Assert.Equal(0xff, result.Color.R); Assert.Equal(0x00, result.Color.G); @@ -53,6 +53,16 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Equal(0xff, result.Color.A); } + [Fact] + public void Parse_ToString_Named_Brush_Roundtrip() + { + const string expectedName = "Red"; + var brush = (ISolidColorBrush)Brush.Parse(expectedName); + var name = brush.ToString(); + + Assert.Equal(expectedName, name); + } + [Fact] public void Parse_Hex_Value_Doesnt_Accept_Too_Few_Chars() { 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 007/211] 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 e55ec59ec31ca1015cebe9c40f747f7327c96251 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 27 Jan 2018 17:18:39 +0100 Subject: [PATCH 008/211] WIP --- Avalonia.sln | 49 -------- .../Avalonia.DotNetCoreRuntime.csproj | 2 +- .../Avalonia.Win32.Interop.csproj | 4 + .../WinForms}/WinFormsAvaloniaControlHost.cs | 1 - .../Avalonia.Win32/Avalonia.Win32.csproj | 106 +++--------------- .../Embedding/EmbeddedWindowImpl.cs | 50 ++++----- .../Avalonia.Win32/Properties/AssemblyInfo.cs | 2 - src/Windows/Avalonia.Win32/Win32Platform.cs | 4 - src/Windows/Avalonia.Win32/WindowImpl.cs | 13 +-- 9 files changed, 47 insertions(+), 184 deletions(-) rename src/Windows/{Avalonia.Win32/Embedding => Avalonia.Win32.Interop/WinForms}/WinFormsAvaloniaControlHost.cs (99%) diff --git a/Avalonia.sln b/Avalonia.sln index 7cf2cf3b8a..6215ff61a1 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -126,10 +126,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RenderTest", "samples\Rende EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.Win32.Shared", "src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.shproj", "{9DEFC6B7-845B-4D8F-AFC0-D32BF0032B8C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.NetStandard", "src\Windows\Avalonia.Win32.NetStandard\Avalonia.Win32.NetStandard.csproj", "{40759A76-D0F2-464E-8000-6FF0F5C4BD7C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DotNetCoreRuntime", "src\Avalonia.DotNetCoreRuntime\Avalonia.DotNetCoreRuntime.csproj", "{7863EA94-F0FB-4386-BF8C-E5BFA761560A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia", "src\Skia\Avalonia.Skia\Avalonia.Skia.csproj", "{7D2D3083-71DD-4CC9-8907-39A0D86FB322}" @@ -196,14 +192,11 @@ 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 = 4 - src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{40759a76-d0f2-464e-8000-6ff0f5c4bd7c}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{4a1abb09-9047-4bd5-a4ad-a055e52c5ee0}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{7863ea94-f0fb-4386-bf8c-e5bfa761560a}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 4 - src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 - src\Windows\Avalonia.Win32\Avalonia.Win32.Shared.projitems*{9defc6b7-845b-4d8f-afc0-d32bf0032b8c}*SharedItemsImports = 13 tests\Avalonia.RenderTests\Avalonia.RenderTests.projitems*{dabfd304-d6a4-4752-8123-c2ccf7ac7831}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 EndGlobalSection @@ -1994,46 +1987,6 @@ Global {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.ActiveCfg = Release|Any CPU {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Build.0 = Release|Any CPU {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Deploy.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|NetCoreOnly.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|NetCoreOnly.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Ad-Hoc|x86.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|Any CPU.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhone.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|NetCoreOnly.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|NetCoreOnly.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|x86.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.AppStore|x86.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhone.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|NetCoreOnly.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|NetCoreOnly.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|x86.ActiveCfg = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Debug|x86.Build.0 = Debug|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|Any CPU.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhone.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhone.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|NetCoreOnly.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|NetCoreOnly.Build.0 = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|x86.ActiveCfg = Release|Any CPU - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C}.Release|x86.Build.0 = Release|Any CPU {7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {7863EA94-F0FB-4386-BF8C-E5BFA761560A}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -2623,8 +2576,6 @@ Global {C7A69145-60B6-4882-97D6-A3921DD43978} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {F1FDC5B0-4654-416F-AE69-E3E9BBD87801} = {9B9E3891-2366-4253-A952-D08BCEB71098} {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {9DEFC6B7-845B-4D8F-AFC0-D32BF0032B8C} = {B39A8919-9F95-48FE-AD7B-76E08B509888} - {40759A76-D0F2-464E-8000-6FF0F5C4BD7C} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {BB1F7BB5-6AD4-4776-94D9-C09D0A972658} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index 8630e7b228..a7586ac7ac 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj index 368e5986b2..7b480ef328 100644 --- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj +++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj @@ -34,6 +34,7 @@ + @@ -48,6 +49,9 @@ UnmanagedMethods.cs + + Component + diff --git a/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs similarity index 99% rename from src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs rename to src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index a484d6c0d2..fe626f4d38 100644 --- a/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs +++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using System.Windows.Forms; -using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Input; using Avalonia.VisualTree; diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 92d973238e..eee6156274 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -1,104 +1,24 @@ - - - + - Debug - AnyCPU - {811A76CF-1CF6-440F-963B-BBE31BD72A82} - Library - Properties - Avalonia.Win32 - Avalonia.Win32 - v4.6.1 - 512 - - + netstandard2.0 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\Avalonia.Win32.xml + true - CS1591 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\Avalonia.Win32.xml - true - CS1591 - true - - - - - - - - - - - - + + + + + + + + - - - Component - - - + - - {D211E587-D8BC-45B9-95A4-F297C8FA5200} - Avalonia.Animation - - - {B09B78D8-9B26-48B0-9149-D64A2F120F3F} - Avalonia.Base - - - {D2221C82-4A25-4583-9B43-D791E3F6820C} - Avalonia.Controls - - - {7062AE20-5DCC-4442-9645-8195BDECE63E} - Avalonia.Diagnostics - - - {62024B2D-53EB-4638-B26B-85EEAA54866E} - Avalonia.Input - - - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} - Avalonia.Interactivity - - - {42472427-4774-4C81-8AFF-9F27B8E31721} - Avalonia.Layout - - - {EB582467-6ABB-43A1-B052-E981BA910E3A} - Avalonia.Visuals - - - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} - Avalonia.Styling - + - - - \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs index d27c28cb67..5e91ede4a5 100644 --- a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs @@ -9,34 +9,34 @@ namespace Avalonia.Win32 { class EmbeddedWindowImpl : WindowImpl, IEmbeddableWindowImpl { - private static readonly System.Windows.Forms.UserControl WinFormsControl = new System.Windows.Forms.UserControl(); + //private static readonly System.Windows.Forms.UserControl WinFormsControl = new System.Windows.Forms.UserControl(); - public static IntPtr DefaultParentWindow = WinFormsControl.Handle; + //public static IntPtr DefaultParentWindow = WinFormsControl.Handle; - protected override IntPtr CreateWindowOverride(ushort atom) - { - var hWnd = UnmanagedMethods.CreateWindowEx( - 0, - atom, - null, - (int)UnmanagedMethods.WindowStyles.WS_CHILD, - 0, - 0, - 640, - 480, - DefaultParentWindow, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - return hWnd; - } + //protected override IntPtr CreateWindowOverride(ushort atom) + //{ + // var hWnd = UnmanagedMethods.CreateWindowEx( + // 0, + // atom, + // null, + // (int)UnmanagedMethods.WindowStyles.WS_CHILD, + // 0, + // 0, + // 640, + // 480, + // DefaultParentWindow, + // IntPtr.Zero, + // IntPtr.Zero, + // IntPtr.Zero); + // return hWnd; + //} - protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - if(msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) - LostFocus?.Invoke(); - return base.WndProc(hWnd, msg, wParam, lParam); - } + //protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + //{ + // if(msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) + // LostFocus?.Invoke(); + // return base.WndProc(hWnd, msg, wParam, lParam); + //} public event Action LostFocus; } diff --git a/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs b/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs index 5b4d2cef23..38ec891f76 100644 --- a/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs +++ b/src/Windows/Avalonia.Win32/Properties/AssemblyInfo.cs @@ -3,7 +3,5 @@ using Avalonia.Platform; using Avalonia.Win32; -using System.Reflection; -[assembly: AssemblyTitle("Avalonia.Win32")] [assembly: ExportWindowingSubsystem(OperatingSystemType.WinNT, 1, "Win32", typeof(Win32Platform), nameof(Win32Platform.Initialize))] diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index e265749249..a490e59cb7 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -196,13 +196,9 @@ namespace Avalonia.Win32 public IEmbeddableWindowImpl CreateEmbeddableWindow() { -#if NETSTANDARD - throw new NotSupportedException(); -#else var embedded = new EmbeddedWindowImpl(); embedded.Show(); return embedded; -#endif } public IPopupImpl CreatePopup() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 348468e0e7..d9b5e25c63 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -1,29 +1,24 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Input; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Reactive.Disposables; using System.Runtime.InteropServices; using Avalonia.Controls; -using System.Reactive.Disposables; +using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.Rendering; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; -using Avalonia.Rendering; -using Avalonia.Threading; -#if NETSTANDARD -using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception; -#endif namespace Avalonia.Win32 { - class WindowImpl : IWindowImpl + public class WindowImpl : IWindowImpl { private static readonly List s_instances = new List(); From 728c19eb8639e7d5cd438344d9d48e730d3971a4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jan 2018 17:17:29 +0100 Subject: [PATCH 009/211] Removed unused files. --- .../Avalonia.Win32.Shared.projitems | 34 ---- src/Windows/Avalonia.Win32/Settings.StyleCop | 184 ------------------ 2 files changed, 218 deletions(-) delete mode 100644 src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems delete mode 100644 src/Windows/Avalonia.Win32/Settings.StyleCop diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems b/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems deleted file mode 100644 index 6f13747cb6..0000000000 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems +++ /dev/null @@ -1,34 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 9defc6b7-845b-4d8f-afc0-d32bf0032b8c - - - Avalonia.Win32.Shared - - - - - - - - - - - - - - - - - - - - - - Properties\SharedAssemblyInfo.cs - - - \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Settings.StyleCop b/src/Windows/Avalonia.Win32/Settings.StyleCop deleted file mode 100644 index 5bd6cda777..0000000000 --- a/src/Windows/Avalonia.Win32/Settings.StyleCop +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - False - - - - - - - \ No newline at end of file From 0e126d2b0638014646dc11590ec27f8dcf31e12b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jan 2018 19:09:27 +0100 Subject: [PATCH 010/211] Reimplement EmbeddedWindowImpl. Without System.Windows.Forms. --- .../Embedding/EmbeddedWindowImpl.cs | 104 +++++++++++++----- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs index 5e91ede4a5..3871936605 100644 --- a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Windows.Forms; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -9,35 +12,80 @@ namespace Avalonia.Win32 { class EmbeddedWindowImpl : WindowImpl, IEmbeddableWindowImpl { - //private static readonly System.Windows.Forms.UserControl WinFormsControl = new System.Windows.Forms.UserControl(); - - //public static IntPtr DefaultParentWindow = WinFormsControl.Handle; - - //protected override IntPtr CreateWindowOverride(ushort atom) - //{ - // var hWnd = UnmanagedMethods.CreateWindowEx( - // 0, - // atom, - // null, - // (int)UnmanagedMethods.WindowStyles.WS_CHILD, - // 0, - // 0, - // 640, - // 480, - // DefaultParentWindow, - // IntPtr.Zero, - // IntPtr.Zero, - // IntPtr.Zero); - // return hWnd; - //} - - //protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - //{ - // if(msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) - // LostFocus?.Invoke(); - // return base.WndProc(hWnd, msg, wParam, lParam); - //} + private static IntPtr DefaultParentWindow = CreateParentWindow(); + private static UnmanagedMethods.WndProc _wndProcDelegate; + + protected override IntPtr CreateWindowOverride(ushort atom) + { + var hWnd = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_CHILD, + 0, + 0, + 640, + 480, + DefaultParentWindow, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + return hWnd; + } + + protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) + LostFocus?.Invoke(); + return base.WndProc(hWnd, msg, wParam, lParam); + } public event Action LostFocus; + + private static IntPtr CreateParentWindow() + { + _wndProcDelegate = new UnmanagedMethods.WndProc(ParentWndProc); + + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = _wndProcDelegate, + lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), + }; + + var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + + if (atom == 0) + { + throw new Win32Exception(); + } + + var hwnd = UnmanagedMethods.CreateWindowEx( + 0, + atom, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + + if (hwnd == IntPtr.Zero) + { + throw new Win32Exception(); + } + + return hwnd; + } + + private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } } } From 79f35161be956bdd7baa896d55effd8de05a268d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jan 2018 19:13:33 +0100 Subject: [PATCH 011/211] Use correct CharSet for WNDCLASSEX And prepend `Avalonia-` to window class. Looking in Spy++ I noticed that our window class name was garbage because the `CharSet` on `WNDCLASSEX` was wrong. Fix that and include "Avalonia" in our window classes. --- src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs | 1 - src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs index 3871936605..cb9c753f4d 100644 --- a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs @@ -4,7 +4,6 @@ using System; using System.ComponentModel; using System.Runtime.InteropServices; -using System.Windows.Forms; using Avalonia.Platform; using Avalonia.Win32.Interop; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index a0518cf92e..f13dd3272c 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1098,7 +1098,7 @@ namespace Avalonia.Win32.Interop } } - [StructLayout(LayoutKind.Sequential)] + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct WNDCLASSEX { public int cbSize; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index d9b5e25c63..3ba926b42a 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -641,7 +641,7 @@ namespace Avalonia.Win32 // Ensure that the delegate doesn't get garbage collected by storing it as a field. _wndProcDelegate = new UnmanagedMethods.WndProc(WndProc); - _className = Guid.NewGuid().ToString(); + _className = "Avalonia-" + Guid.NewGuid(); UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX { From a6cfbdac9338e55a33ea24ab9f7ff72eae077dac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 29 Jan 2018 20:45:49 +0100 Subject: [PATCH 012/211] Remove Avalonia.Win32.NetStandard dir. --- .../Avalonia.Win32.NetStandard.csproj | 49 ----- .../ComStreamWrapper.cs | 197 ------------------ .../Avalonia.Win32.NetStandard/Gdip.cs | 128 ------------ .../Avalonia.Win32.NetStandard/IconImpl.cs | 45 ---- .../NativeWin32Platform.cs | 30 --- .../Win32Exception.cs | 12 -- 6 files changed, 461 deletions(-) delete mode 100644 src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj delete mode 100644 src/Windows/Avalonia.Win32.NetStandard/ComStreamWrapper.cs delete mode 100644 src/Windows/Avalonia.Win32.NetStandard/Gdip.cs delete mode 100644 src/Windows/Avalonia.Win32.NetStandard/IconImpl.cs delete mode 100644 src/Windows/Avalonia.Win32.NetStandard/NativeWin32Platform.cs delete mode 100644 src/Windows/Avalonia.Win32.NetStandard/Win32Exception.cs diff --git a/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj b/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj deleted file mode 100644 index bae634b030..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/Avalonia.Win32.NetStandard.csproj +++ /dev/null @@ -1,49 +0,0 @@ - - - netstandard2.0 - False - false - Avalonia.Win32 - Avalonia.Win32 - - - true - full - false - bin\Debug\ - TRACE;DEBUG;NETSTANDARD - prompt - 4 - true - - - pdbonly - true - bin\Release\ - TRACE;NETSTANDARD - prompt - 4 - true - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32.NetStandard/ComStreamWrapper.cs b/src/Windows/Avalonia.Win32.NetStandard/ComStreamWrapper.cs deleted file mode 100644 index 083021f58d..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/ComStreamWrapper.cs +++ /dev/null @@ -1,197 +0,0 @@ -// -// System.Drawing.ComIStreamWrapper.cs -// -// Author: -// Kornél Pál -// -// Copyright (C) 2005-2008 Kornél Pál -// - -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; - -namespace Avalonia.Win32 -{ - // Stream to IStream wrapper for COM interop - internal sealed class ComIStreamWrapper : IStream - { - private const int STG_E_INVALIDFUNCTION = unchecked((int)0x80030001); - - private readonly Stream baseStream; - private long position = -1; - - internal ComIStreamWrapper(Stream stream) - { - baseStream = stream; - } - - private void SetSizeToPosition() - { - if (position != -1) - { - if (position > baseStream.Length) - baseStream.SetLength(position); - baseStream.Position = position; - position = -1; - } - } - - public void Read(byte[] pv, int cb, IntPtr pcbRead) - { - int read = 0; - - if (cb != 0) - { - SetSizeToPosition(); - read = baseStream.Read(pv, 0, cb); - } - - if (pcbRead != IntPtr.Zero) - Marshal.WriteInt32(pcbRead, read); - } - - public void Write(byte[] pv, int cb, IntPtr pcbWritten) - { - if (cb != 0) - { - SetSizeToPosition(); - baseStream.Write(pv, 0, cb); - } - - if (pcbWritten != IntPtr.Zero) - Marshal.WriteInt32(pcbWritten, cb); - } - - public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition) - { - long length = baseStream.Length; - long newPosition; - - switch ((SeekOrigin)dwOrigin) - { - case SeekOrigin.Begin: - newPosition = dlibMove; - break; - case SeekOrigin.Current: - if (position == -1) - newPosition = baseStream.Position + dlibMove; - else - newPosition = position + dlibMove; - break; - case SeekOrigin.End: - newPosition = length + dlibMove; - break; - default: - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - if (newPosition > length) - position = newPosition; - else - { - baseStream.Position = newPosition; - position = -1; - } - - if (plibNewPosition != IntPtr.Zero) - Marshal.WriteInt64(plibNewPosition, newPosition); - } - - public void SetSize(long libNewSize) - { - baseStream.SetLength(libNewSize); - } - - public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) - { - byte[] buffer; - long written = 0; - int read; - int count; - - if (cb != 0) - { - if (cb < 4096) - count = (int)cb; - else - count = 4096; - buffer = new byte[count]; - SetSizeToPosition(); - while (true) - { - if ((read = baseStream.Read(buffer, 0, count)) == 0) - break; - pstm.Write(buffer, read, IntPtr.Zero); - written += read; - if (written >= cb) - break; - if (cb - written < 4096) - count = (int)(cb - written); - } - } - - if (pcbRead != IntPtr.Zero) - Marshal.WriteInt64(pcbRead, written); - if (pcbWritten != IntPtr.Zero) - Marshal.WriteInt64(pcbWritten, written); - } - - public void Commit(int grfCommitFlags) - { - baseStream.Flush(); - SetSizeToPosition(); - } - - public void Revert() - { - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - public void LockRegion(long libOffset, long cb, int dwLockType) - { - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - public void UnlockRegion(long libOffset, long cb, int dwLockType) - { - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - - public void Stat(out STATSTG pstatstg, int grfStatFlag) - { - pstatstg = new STATSTG(); - pstatstg.cbSize = baseStream.Length; - } - - public void Clone(out IStream ppstm) - { - ppstm = null; - throw new COMException(null, STG_E_INVALIDFUNCTION); - } - } -} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32.NetStandard/Gdip.cs b/src/Windows/Avalonia.Win32.NetStandard/Gdip.cs deleted file mode 100644 index b3d1c28689..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/Gdip.cs +++ /dev/null @@ -1,128 +0,0 @@ -// -// Code copy-pasted from from Mono / System.Drawing.*.cs -// Original license below: -// -// Authors: -// Alexandre Pigolkine (pigolkine@gmx.de) -// Jordi Mas (jordi@ximian.com) -// Sanjay Gupta (gsanjay@novell.com) -// Ravindra (rkumar@novell.com) -// Peter Dennis Bartok (pbartok@novell.com) -// Sebastien Pouliot -// -// -// Copyright (C) 2004, 2007 Novell, Inc (http://www.novell.com) -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Win32 -{ - static class Gdip - { - public enum Status - { - Ok = 0, - GenericError = 1, - InvalidParameter = 2, - OutOfMemory = 3, - ObjectBusy = 4, - InsufficientBuffer = 5, - NotImplemented = 6, - Win32Error = 7, - WrongState = 8, - Aborted = 9, - FileNotFound = 10, - ValueOverflow = 11, - AccessDenied = 12, - UnknownImageFormat = 13, - FontFamilyNotFound = 14, - FontStyleNotFound = 15, - NotTrueTypeFont = 16, - UnsupportedGdiplusVersion = 17, - GdiplusNotInitialized = 18, - PropertyNotFound = 19, - PropertyNotSupported = 20, - ProfileNotFound = 21 - } - - [StructLayout(LayoutKind.Sequential)] - internal struct GdiplusStartupInput - { - // internalted to silent compiler - internal uint GdiplusVersion; - internal IntPtr DebugEventCallback; - internal int SuppressBackgroundThread; - internal int SuppressExternalCodecs; - - internal static GdiplusStartupInput MakeGdiplusStartupInput() - { - GdiplusStartupInput result = new GdiplusStartupInput(); - result.GdiplusVersion = 1; - result.DebugEventCallback = IntPtr.Zero; - result.SuppressBackgroundThread = 0; - result.SuppressExternalCodecs = 0; - return result; - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct GdiplusStartupOutput - { - internal IntPtr NotificationHook; - internal IntPtr NotificationUnhook; - - internal static GdiplusStartupOutput MakeGdiplusStartupOutput() - { - GdiplusStartupOutput result = new GdiplusStartupOutput(); - result.NotificationHook = result.NotificationUnhook = IntPtr.Zero; - return result; - } - } - - - [DllImport("gdiplus.dll")] - public static extern Status GdiplusStartup(ref ulong token, ref GdiplusStartupInput input, ref GdiplusStartupOutput output); - - [DllImport("gdiplus.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] - public static extern Status GdipLoadImageFromStream([MarshalAs(UnmanagedType.Interface, MarshalTypeRef = typeof(IStream))] IStream stream, out IntPtr image); - [DllImport("gdiplus.dll")] - public static extern Status GdipCreateHICONFromBitmap(IntPtr bmp, out IntPtr HandleIcon); - - [DllImport("gdiplus.dll")] - internal static extern Status GdipDisposeImage(IntPtr image); - - static Gdip() - { - ulong token = 0; - var input = GdiplusStartupInput.MakeGdiplusStartupInput(); - var output = GdiplusStartupOutput.MakeGdiplusStartupOutput(); - GdiplusStartup(ref token, ref input, ref output); - } - } -} diff --git a/src/Windows/Avalonia.Win32.NetStandard/IconImpl.cs b/src/Windows/Avalonia.Win32.NetStandard/IconImpl.cs deleted file mode 100644 index 49d039e655..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/IconImpl.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; - -namespace Avalonia.Win32 -{ - public class IconImpl : IWindowIconImpl - { - private readonly MemoryStream _ms; - - public IconImpl(Stream data) - { - _ms = new MemoryStream(); - data.CopyTo(_ms); - _ms.Seek(0, SeekOrigin.Begin); - IntPtr bitmap; - var status = Gdip.GdipLoadImageFromStream(new ComIStreamWrapper(_ms), out bitmap); - if (status != Gdip.Status.Ok) - throw new Exception("Unable to load icon, gdip status: " + (int) status); - IntPtr icon; - status = Gdip.GdipCreateHICONFromBitmap(bitmap, out icon); - if (status != Gdip.Status.Ok) - throw new Exception("Unable to create HICON, gdip status: " + (int)status); - Gdip.GdipDisposeImage(bitmap); - HIcon = icon; - } - - public IntPtr HIcon { get;} - public void Save(Stream outputStream) - { - lock (_ms) - { - _ms.Seek(0, SeekOrigin.Begin); - _ms.CopyTo(outputStream); - } - } - - } -} diff --git a/src/Windows/Avalonia.Win32.NetStandard/NativeWin32Platform.cs b/src/Windows/Avalonia.Win32.NetStandard/NativeWin32Platform.cs deleted file mode 100644 index 2695a5b8b6..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/NativeWin32Platform.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; - -namespace Avalonia.Win32 -{ - partial class Win32Platform - { - //TODO: An actual implementation - public IWindowIconImpl LoadIcon(string fileName) - { - //No file IO for netstandard, still waiting for proper net core tooling - throw new NotSupportedException(); - } - - public IWindowIconImpl LoadIcon(Stream stream) => new IconImpl(stream); - - public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) - { - var ms = new MemoryStream(); - bitmap.Save(ms); - ms.Seek(0, SeekOrigin.Begin); - return new IconImpl(ms); - } - } -} diff --git a/src/Windows/Avalonia.Win32.NetStandard/Win32Exception.cs b/src/Windows/Avalonia.Win32.NetStandard/Win32Exception.cs deleted file mode 100644 index 45926a881d..0000000000 --- a/src/Windows/Avalonia.Win32.NetStandard/Win32Exception.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Avalonia.Win32.NetStandard -{ - class AvaloniaWin32Exception : Exception - { - } -} From f50223cb0dcf988601c60fb0a9088c761b35f7bf Mon Sep 17 00:00:00 2001 From: sdoroff Date: Mon, 12 Feb 2018 18:10:00 -0500 Subject: [PATCH 013/211] Fixed a bug in tab navigation TabNavigation.GetFirstInNextContainer would only check one sibling and would move up to the parent if the first sibling was not focuable --- src/Avalonia.Input/Navigation/TabNavigation.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 6e077e887f..a9d5b83073 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -221,17 +221,16 @@ namespace Avalonia.Input.Navigation return parent; } - var siblings = parent.GetVisualChildren() + var allSiblings = parent.GetVisualChildren() .OfType() .Where(FocusExtensions.CanFocusDescendants); - var sibling = direction == NavigationDirection.Next ? - siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : - siblings.TakeWhile(x => x != container).LastOrDefault(); + var siblings = direction == NavigationDirection.Next ? + allSiblings.SkipWhile(x => x != container).Skip(1) : + allSiblings.TakeWhile(x => x != container).Reverse(); - if (sibling != null) + foreach (var sibling in siblings) { var customNext = GetCustomNext(sibling, direction); - if (customNext.handled) { return customNext.next; @@ -239,13 +238,17 @@ namespace Avalonia.Input.Navigation if (sibling.CanFocus()) { - next = sibling; + return sibling; } else { next = direction == NavigationDirection.Next ? GetFocusableDescendants(sibling, direction).FirstOrDefault() : GetFocusableDescendants(sibling, direction).LastOrDefault(); + if(next != null) + { + return next; + } } } From 7969749cd1ca55d3e9e81c685c9e16408657e61b Mon Sep 17 00:00:00 2001 From: sdoroff Date: Mon, 12 Feb 2018 20:06:55 -0500 Subject: [PATCH 014/211] Added a unit test --- .../KeyboardNavigationTests_Tab.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index ad70dcd470..60d31145cf 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -80,6 +80,43 @@ namespace Avalonia.Input.UnitTests Assert.Equal(next, result); } + [Fact] + public void Next_Skips_Unfocasable_Siblings() + { + Button current; + Button next; + + var top = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + new Button { Name = "Button1" }, + new Button { Name = "Button2" }, + new StackPanel + { + Children = + { + (current = new Button { Name = "Button3" }), + } + }, + new TextBlock { Name = "TextBlock" }, + (next = new Button { Name = "Button4" }), + } + }, + new Button { Name = "Button5" }, + new Button { Name = "Button6" }, + } + }; + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Equal(next, result); + } + [Fact] public void Next_Continue_Doesnt_Enter_Panel_With_TabNavigation_None() { From 6e774349f8164e9dc0b514fa10fdb8a4dc8d16b0 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 31 Jan 2018 20:20:56 -0500 Subject: [PATCH 015/211] Added a DatePicker control ported from Silverlight --- samples/ControlCatalog/ControlCatalog.csproj | 6 + samples/ControlCatalog/MainView.xaml | 3 +- .../ControlCatalog/Pages/DatePickerPage.xaml | 46 + .../Pages/DatePickerPage.xaml.cs | 36 + src/Avalonia.Controls/Calendar/DatePicker.cs | 1188 +++++++++++++++++ src/Avalonia.Themes.Default/DatePicker.xaml | 126 ++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + 7 files changed, 1405 insertions(+), 1 deletion(-) create mode 100644 samples/ControlCatalog/Pages/DatePickerPage.xaml create mode 100644 samples/ControlCatalog/Pages/DatePickerPage.xaml.cs create mode 100644 src/Avalonia.Controls/Calendar/DatePicker.cs create mode 100644 src/Avalonia.Themes.Default/DatePicker.xaml diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 0b4463ddb7..37f9da0c43 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -60,6 +60,9 @@ Designer + + Designer + Designer @@ -128,6 +131,9 @@ DropDownPage.xaml + + DatePickerPage.xaml + ExpanderPage.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 311a61a6dc..060369e404 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -7,11 +7,12 @@ - + + diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/DatePickerPage.xaml new file mode 100644 index 0000000000..92cfa7e178 --- /dev/null +++ b/samples/ControlCatalog/Pages/DatePickerPage.xaml @@ -0,0 +1,46 @@ + + + DatePicker + A control for selecting dates with a calendar drop-down + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs new file mode 100644 index 0000000000..ef01887c9e --- /dev/null +++ b/samples/ControlCatalog/Pages/DatePickerPage.xaml.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System; + +namespace ControlCatalog.Pages +{ + public class DatePickerPage : UserControl + { + public DatePickerPage() + { + InitializeComponent(); + + var dp1 = this.FindControl("DatePicker1"); + var dp2 = this.FindControl("DatePicker2"); + var dp3 = this.FindControl("DatePicker3"); + var dp4 = this.FindControl("DatePicker4"); + var dp5 = this.FindControl("DatePicker5"); + + dp1.SelectedDate = DateTime.Today; + dp2.SelectedDate = DateTime.Today.AddDays(10); + dp3.SelectedDate = DateTime.Today.AddDays(20); + dp5.SelectedDate = DateTime.Today; + + dp4.TemplateApplied += (s, e) => + { + dp4.BlackoutDates.AddDatesInPast(); + }; + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs new file mode 100644 index 0000000000..418ef50b2c --- /dev/null +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -0,0 +1,1188 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; + +namespace Avalonia.Controls +{ + /// + /// Provides data for the + /// + /// event. + /// + public class DatePickerDateValidationErrorEventArgs : EventArgs + { + private bool _throwException; + + /// + /// Initializes a new instance of the + /// + /// class. + /// + /// + /// The initial exception from the + /// + /// event. + /// + /// + /// The text that caused the + /// + /// event. + /// + public DatePickerDateValidationErrorEventArgs(Exception exception, string text) + { + this.Text = text; + this.Exception = exception; + } + + /// + /// Gets the initial exception associated with the + /// + /// event. + /// + /// + /// The exception associated with the validation failure. + /// + public Exception Exception { get; private set; } + + /// + /// Gets the text that caused the + /// + /// event. + /// + /// + /// The text that caused the validation failure. + /// + public string Text { get; private set; } + + /// + /// Gets or sets a value indicating whether + /// + /// should be thrown. + /// + /// + /// True if the exception should be thrown; otherwise, false. + /// + /// + /// If set to true and + /// + /// is null. + /// + public bool ThrowException + { + get { return this._throwException; } + set + { + if (value && this.Exception == null) + { + throw new ArgumentException("Cannot Throw Null Exception"); + } + this._throwException = value; + } + } + } + + /// + /// Specifies date formats for a + /// . + /// + public enum DatePickerFormat + { + /// + /// Specifies that the date should be displayed using unabbreviated days + /// of the week and month names. + /// + Long = 0, + + /// + /// Specifies that the date should be displayed using abbreviated days + /// of the week and month names. + /// + Short = 1, + + /// + /// Specifies that the date should be displayed using a custom format string. + /// + Custom = 2 + } + + public class DatePicker : TemplatedControl + { + private const string ElementTextBox = "PART_TextBox"; + private const string ElementButton = "PART_Button"; + private const string ElementPopup = "PART_Popup"; + private const string ElementCalendar = "PART_Calendar"; + + private Calendar _calendar; + private string _defaultText; + private Button _dropDownButton; + //private Canvas _outsideCanvas; + //private Canvas _outsidePopupCanvas; + private Popup _popUp; + private TextBox _textBox; + private IDisposable _textBoxTextChangedSubscription; + private IDisposable _buttonPointerPressedSubscription; + + private DateTime? _onOpenSelectedDate; + private bool _settingSelectedDate; + + private DateTime _displayDate; + private DateTime? _displayDateStart; + private DateTime? _displayDateEnd; + private bool _isDropDownOpen; + private DateTime? _selectedDate; + private string _text; + private bool _suspendTextChangeHandler = false; + private bool _isPopupClosing = false; + private bool _ignoreButtonClick = false; + + /// + /// Gets a collection of dates that are marked as not selectable. + /// + /// + /// A collection of dates that cannot be selected. The default value is + /// an empty collection. + /// + public CalendarBlackoutDatesCollection BlackoutDates { get; private set; } + + public static readonly DirectProperty DisplayDateProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDate), + o => o.DisplayDate, + (o, v) => o.DisplayDate = v); + public static readonly DirectProperty DisplayDateStartProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDateStart), + o => o.DisplayDateStart, + (o, v) => o.DisplayDateStart = v); + public static readonly DirectProperty DisplayDateEndProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDateEnd), + o => o.DisplayDateEnd, + (o, v) => o.DisplayDateEnd = v); + public static readonly StyledProperty FirstDayOfWeekProperty = + AvaloniaProperty.Register(nameof(FirstDayOfWeek)); + + public static readonly DirectProperty IsDropDownOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsDropDownOpen), + o => o.IsDropDownOpen, + (o, v) => o.IsDropDownOpen = v); + + public static readonly StyledProperty IsTodayHighlightedProperty = + AvaloniaProperty.Register(nameof(IsTodayHighlighted)); + public static readonly DirectProperty SelectedDateProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedDate), + o => o.SelectedDate, + (o, v) => o.SelectedDate = v); + + public static readonly StyledProperty SelectedDateFormatProperty = + AvaloniaProperty.Register( + nameof(SelectedDateFormat), + defaultValue: DatePickerFormat.Short, + validate: ValidateSelectedDateFormat); + + public static readonly StyledProperty CustomDateFormatStringProperty = + AvaloniaProperty.Register( + nameof(CustomDateFormatString), + defaultValue: "d", + validate: ValidateDateFormatString); + + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( + nameof(Text), + o => o.Text, + (o, v) => o.Text = v); + public static readonly StyledProperty WatermarkProperty = + TextBox.WatermarkProperty.AddOwner(); + public static readonly StyledProperty UseFloatingWatermarkProperty = + TextBox.UseFloatingWatermarkProperty.AddOwner(); + + + /// + /// Gets or sets the date to display. + /// + /// + /// The date to display. The default + /// . + /// + /// + /// The specified date is not in the range defined by + /// + /// and + /// . + /// + public DateTime DisplayDate + { + get { return _displayDate; } + set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); } + } + + /// + /// Gets or sets the first date to be displayed. + /// + /// The first date to display. + public DateTime? DisplayDateStart + { + get { return _displayDateStart; } + set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); } + } + + /// + /// Gets or sets the last date to be displayed. + /// + /// The last date to display. + public DateTime? DisplayDateEnd + { + get { return _displayDateEnd; } + set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); } + } + + /// + /// Gets or sets the day that is considered the beginning of the week. + /// + /// + /// A representing the beginning of + /// the week. The default is . + /// + public DayOfWeek FirstDayOfWeek + { + get { return GetValue(FirstDayOfWeekProperty); } + set { SetValue(FirstDayOfWeekProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the drop-down + /// is open or closed. + /// + /// + /// True if the is + /// open; otherwise, false. The default is false. + /// + public bool IsDropDownOpen + { + get { return _isDropDownOpen; } + set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } + } + + /// + /// Gets or sets a value indicating whether the current date will be + /// highlighted. + /// + /// + /// True if the current date is highlighted; otherwise, false. The + /// default is true. + /// + public bool IsTodayHighlighted + { + get { return GetValue(IsTodayHighlightedProperty); } + set { SetValue(IsTodayHighlightedProperty, value); } + } + + /// + /// Gets or sets the currently selected date. + /// + /// + /// The date currently selected. The default is null. + /// + /// + /// The specified date is not in the range defined by + /// + /// and + /// , + /// or the specified date is in the + /// + /// collection. + /// + public DateTime? SelectedDate + { + get { return _selectedDate; } + set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); } + } + + /// + /// Gets or sets the format that is used to display the selected date. + /// + /// + /// The format that is used to display the selected date. The default is + /// . + /// + /// + /// An specified format is not valid. + /// + public DatePickerFormat SelectedDateFormat + { + get { return GetValue(SelectedDateFormatProperty); } + set { SetValue(SelectedDateFormatProperty, value); } + } + + public string CustomDateFormatString + { + get { return GetValue(CustomDateFormatStringProperty); } + set { SetValue(CustomDateFormatStringProperty, value); } + } + + /// + /// Gets or sets the text that is displayed by the + /// . + /// + /// + /// The text displayed by the + /// . + /// + /// + /// The text entered cannot be parsed to a valid date, and the exception + /// is not suppressed. + /// + /// + /// The text entered parses to a date that is not selectable. + /// + public string Text + { + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } + } + + public string Watermark + { + get { return GetValue(WatermarkProperty); } + set { SetValue(WatermarkProperty, value); } + } + public bool UseFloatingWatermark + { + get { return GetValue(UseFloatingWatermarkProperty); } + set { SetValue(UseFloatingWatermarkProperty, value); } + } + + /// + /// Occurs when the drop-down + /// is closed. + /// + public event EventHandler CalendarClosed; + + /// + /// Occurs when the drop-down + /// is opened. + /// + public event EventHandler CalendarOpened; + + /// + /// Occurs when + /// is assigned a value that cannot be interpreted as a date. + /// + public event EventHandler DateValidationError; + + /// + /// Occurs when the + /// + /// property is changed. + /// + public event EventHandler SelectedDateChanged; + + static DatePicker() + { + FocusableProperty.OverrideDefaultValue(true); + + DisplayDateProperty.Changed.AddClassHandler(x => x.OnDisplayDateChanged); + DisplayDateStartProperty.Changed.AddClassHandler(x => x.OnDisplayDateStartChanged); + DisplayDateEndProperty.Changed.AddClassHandler(x => x.OnDisplayDateEndChanged); + IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged); + SelectedDateProperty.Changed.AddClassHandler(x => x.OnSelectedDateChanged); + SelectedDateFormatProperty.Changed.AddClassHandler(x => x.OnSelectedDateFormatChanged); + CustomDateFormatStringProperty.Changed.AddClassHandler(x => x.OnCustomDateFormatStringChanged); + TextProperty.Changed.AddClassHandler(x => x.OnTextChanged); + } + /// + /// Initializes a new instance of the + /// class. + /// + public DatePicker() + { + FirstDayOfWeek = DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek; + _defaultText = string.Empty; + DisplayDate = DateTime.Today; + } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + if (_calendar != null) + { + _calendar.DayButtonMouseUp -= Calendar_DayButtonMouseUp; + _calendar.DisplayDateChanged -= Calendar_DisplayDateChanged; + _calendar.SelectedDatesChanged -= Calendar_SelectedDatesChanged; + _calendar.PointerPressed -= Calendar_PointerPressed; + _calendar.KeyDown -= Calendar_KeyDown; + } + _calendar = e.NameScope.Find(ElementCalendar); + if (_calendar != null) + { + _calendar.SelectionMode = CalendarSelectionMode.SingleDate; + _calendar.SelectedDate = SelectedDate; + SetCalendarDisplayDate(DisplayDate); + SetCalendarDisplayDateStart(DisplayDateStart); + SetCalendarDisplayDateEnd(DisplayDateEnd); + + _calendar.DayButtonMouseUp += Calendar_DayButtonMouseUp; + _calendar.DisplayDateChanged += Calendar_DisplayDateChanged; + _calendar.SelectedDatesChanged += Calendar_SelectedDatesChanged; + _calendar.PointerPressed += Calendar_PointerPressed; + _calendar.KeyDown += Calendar_KeyDown; + //_calendar.SizeChanged += new SizeChangedEventHandler(Calendar_SizeChanged); + //_calendar.IsTabStop = true; + + var currentBlackoutDays = BlackoutDates; + BlackoutDates = _calendar.BlackoutDates; + if(currentBlackoutDays != null) + { + foreach (var range in currentBlackoutDays) + { + BlackoutDates.Add(range); + } + } + } + + if (_popUp != null) + { + _popUp.Child = null; + _popUp.Closed -= PopUp_Closed; + } + _popUp = e.NameScope.Find(ElementPopup); + if(_popUp != null) + { + _popUp.Closed += PopUp_Closed; + if (IsDropDownOpen) + { + OpenDropDown(); + } + } + + if(_dropDownButton != null) + { + _dropDownButton.Click -= DropDownButton_Click; + _buttonPointerPressedSubscription?.Dispose(); + } + _dropDownButton = e.NameScope.Find class EmulatedFramebuffer : ILockedFramebuffer { + private nfloat _viewWidth; + private nfloat _viewHeight; public EmulatedFramebuffer(UIView view) { var factor = (int) UIScreen.MainScreen.Scale; var frame = view.Frame; + _viewWidth = frame.Width; + _viewHeight = frame.Height; Width = (int) frame.Width * factor; Height = (int) frame.Height * factor; RowBytes = Width * 4; @@ -41,9 +45,9 @@ namespace Avalonia.iOS using (var context = UIGraphics.GetCurrentContext()) { // flip the image for CGContext.DrawImage - context.TranslateCTM(0, Height); + context.TranslateCTM(0, _viewHeight); context.ScaleCTM(1, -1); - context.DrawImage(new CGRect(0, 0, Width, Height), image); + context.DrawImage(new CGRect(0, 0, _viewWidth, _viewHeight), image); } Marshal.FreeHGlobal(Address); Address = IntPtr.Zero; @@ -57,3 +61,4 @@ namespace Avalonia.iOS public PixelFormat Format { get; } } } + From e5a3370acd47f4570d2ba0a2b472de1aa923938f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 27 Feb 2018 00:01:50 +0300 Subject: [PATCH 033/211] Fixes #1405 --- src/OSX/Avalonia.MonoMac/ClipboardImpl.cs | 31 +++++++++++++++++++++ src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 src/OSX/Avalonia.MonoMac/ClipboardImpl.cs diff --git a/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs b/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs new file mode 100644 index 0000000000..f7b98c0c1f --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/ClipboardImpl.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input.Platform; +using MonoMac.AppKit; + +namespace Avalonia.MonoMac +{ + class ClipboardImpl : IClipboard + { + public Task GetTextAsync() + { + return Task.FromResult(NSPasteboard.GeneralPasteboard.GetStringForType(NSPasteboard.NSStringType)); + } + + public Task SetTextAsync(string text) + { + NSPasteboard.GeneralPasteboard.ClearContents(); + if (text != null) + NSPasteboard.GeneralPasteboard.SetStringForType(text, NSPasteboard.NSStringType); + return Task.CompletedTask; + } + + public async Task ClearAsync() + { + NSPasteboard.GeneralPasteboard.ClearContents(); + } + } +} + diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs index a6b1f1d5b4..5907459459 100644 --- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs +++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs @@ -3,6 +3,7 @@ using System.Threading; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Rendering; using MonoMac.AppKit; @@ -32,6 +33,7 @@ namespace Avalonia.MonoMac .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(s_renderLoop) .Bind().ToConstant(PlatformThreadingInterface.Instance); } From 20e7db8587460f20b3096db165e6555ccb91b58d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 27 Feb 2018 00:08:23 +0300 Subject: [PATCH 034/211] Bump version so nightly builds will be treated as newer ones --- parameters.cake | 2 +- src/Shared/SharedAssemblyInfo.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/parameters.cake b/parameters.cake index c727b3107f..e224cce151 100644 --- a/parameters.cake +++ b/parameters.cake @@ -97,7 +97,7 @@ public class Parameters else { // Use AssemblyVersion with Build as version - Version += "-build" + context.EnvironmentVariable("APPVEYOR_BUILD_NUMBER") + "-alpha"; + Version += "-build" + context.EnvironmentVariable("APPVEYOR_BUILD_NUMBER") + "-beta"; } } diff --git a/src/Shared/SharedAssemblyInfo.cs b/src/Shared/SharedAssemblyInfo.cs index 98047b4cc8..44ebef4cd2 100644 --- a/src/Shared/SharedAssemblyInfo.cs +++ b/src/Shared/SharedAssemblyInfo.cs @@ -14,6 +14,6 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTrademark("")] [assembly: NeutralResourcesLanguage("en")] -[assembly: AssemblyVersion("0.6.0")] -[assembly: AssemblyFileVersion("0.6.0")] -[assembly: AssemblyInformationalVersion("0.6.0")] +[assembly: AssemblyVersion("0.6.2")] +[assembly: AssemblyFileVersion("0.6.2")] +[assembly: AssemblyInformationalVersion("0.6.2")] From 6e1c7e37a84270fed27ebf86c2ba38620969175e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 27 Feb 2018 19:59:40 +0300 Subject: [PATCH 035/211] Fixed Styles preview substitute --- src/Avalonia.DesignerSupport/DesignWindowLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 9ad2a216b4..5235beee0f 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport } }; } - if (loaded is Application) + else if (loaded is Application) control = new TextBlock {Text = "Application can't be previewed in design view"}; else control = (Control) loaded; @@ -73,4 +73,4 @@ namespace Avalonia.DesignerSupport return window; } } -} \ No newline at end of file +} From f94381acac60e9c00695175f623fcbab51096b23 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 28 Feb 2018 15:41:13 -0500 Subject: [PATCH 036/211] 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 037/211] 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 038/211] 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 f136eeccb115891b50676360b2c16850facf8d90 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Mar 2018 23:15:00 +0100 Subject: [PATCH 039/211] Added failing test for #1408. --- .../Presenters/ScrollContentPresenterTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index 3c8a692bfb..4501315c94 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -223,6 +223,26 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(100, child.Bounds.Width); } + [Fact] + public void Extent_Should_Include_Content_Margin() + { + var target = new ScrollContentPresenter + { + Content = new Border + { + Width = 100, + Height = 100, + Margin = new Thickness(5), + } + }; + + target.UpdateChild(); + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 50, 50)); + + Assert.Equal(new Size(110, 110), target.Extent); + } + [Fact] public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False() { From 970363571b5080f3f78e576a8dcac104acebe7d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Mar 2018 23:15:23 +0100 Subject: [PATCH 040/211] Include margin in Extent size. Fixes #1408. --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index a68979cfa1..8d0c6f16cb 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls.Presenters CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height); ArrangeOverrideImpl(size, -Offset); Viewport = finalSize; - Extent = Child.Bounds.Size; + Extent = Child.Bounds.Size.Inflate(Child.Margin); return finalSize; } From 7ba9d33b0e687e91960780fca45190747b7935ac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Mar 2018 23:16:11 +0100 Subject: [PATCH 041/211] Added extra test for Bounds/Margin. And removed empty test. --- .../Presenters/ContentPresenterTests_Layout.cs | 5 ----- tests/Avalonia.Layout.UnitTests/ArrangeTests.cs | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs index 450b85696e..2ab02a0418 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs @@ -185,10 +185,5 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds); } - - [Fact] - public void Padding_Is_Applied_To_TopLeft_Aligned_Content() - { - } } } \ No newline at end of file diff --git a/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs b/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs index df4bb17522..7562b7eff3 100644 --- a/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs +++ b/tests/Avalonia.Layout.UnitTests/ArrangeTests.cs @@ -8,6 +8,22 @@ namespace Avalonia.Layout.UnitTests { public class ArrangeTests { + [Fact] + public void Bounds_Should_Not_Include_Margin() + { + var target = new Decorator + { + Width = 100, + Height = 100, + Margin = new Thickness(5), + }; + + Assert.False(target.IsMeasureValid); + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + Assert.Equal(new Rect(5, 5, 100, 100), target.Bounds); + } + [Fact] public void Margin_Should_Be_Subtracted_From_Arrange_FinalSize() { From 27ab312be5b7e76efbba0d5245f29d4ee77bb5d7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 1 Mar 2018 19:59:15 +0300 Subject: [PATCH 042/211] 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 edf6b12c4d7c9ba6844201f6c29f83b9ac9d4b1d Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Sat, 3 Mar 2018 11:28:21 +0100 Subject: [PATCH 043/211] first bits of drag+drop support for osx --- src/Avalonia.Controls/DragDrop/DataFormats.cs | 15 ++++ .../DragDrop/DragOperation.cs | 13 ++++ src/Avalonia.Controls/DragDrop/IDragData.cs | 15 ++++ .../DragDrop/IDragDispatcher.cs | 12 +++ .../Avalonia.MonoMac/Avalonia.MonoMac.csproj | 4 +- src/OSX/Avalonia.MonoMac/DraggingInfo.cs | 63 +++++++++++++++ src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 76 +++++++++++++++++++ 7 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls/DragDrop/DataFormats.cs create mode 100644 src/Avalonia.Controls/DragDrop/DragOperation.cs create mode 100644 src/Avalonia.Controls/DragDrop/IDragData.cs create mode 100644 src/Avalonia.Controls/DragDrop/IDragDispatcher.cs create mode 100644 src/OSX/Avalonia.MonoMac/DraggingInfo.cs diff --git a/src/Avalonia.Controls/DragDrop/DataFormats.cs b/src/Avalonia.Controls/DragDrop/DataFormats.cs new file mode 100644 index 0000000000..f46651ed3b --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/DataFormats.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls.DragDrop +{ + public static class DataFormats + { + /// + /// Dataformat for plaintext + /// + public static string Text = nameof(Text); + + /// + /// Dataformat for one or more filenames + /// + public static string FileNames = nameof(FileNames); + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/DragOperation.cs b/src/Avalonia.Controls/DragDrop/DragOperation.cs new file mode 100644 index 0000000000..ffc6f666f7 --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/DragOperation.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Controls.DragDrop +{ + [Flags] + public enum DragOperation + { + None = 0, + Copy = 1, + Move = 2, + Link = 4, + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/IDragData.cs b/src/Avalonia.Controls/DragDrop/IDragData.cs new file mode 100644 index 0000000000..b6dc53d32d --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/IDragData.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Avalonia.Controls.DragDrop +{ + public interface IDragData + { + IEnumerable GetDataFormats(); + + bool Contains(string dataFormat); + + string GetText(); + + IEnumerable GetFileNames(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs new file mode 100644 index 0000000000..5c39635dd0 --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs @@ -0,0 +1,12 @@ +using Avalonia.Input; + +namespace Avalonia.Controls.DragDrop +{ + public interface IDragDispatcher + { + DragOperation DragEnter(IInputRoot inputRoot, Point point, IDragData data, DragOperation operation); + DragOperation DragOver(IInputRoot inputRoot, Point point, IDragData data, DragOperation operation); + void DragLeave(IInputRoot inputRoot); + DragOperation Drop(IInputRoot inputRoot, Point point, IDragData data, DragOperation operation); + } +} \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj index 5f6be91571..c31c131ea9 100644 --- a/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj +++ b/src/OSX/Avalonia.MonoMac/Avalonia.MonoMac.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 True @@ -19,4 +19,4 @@ - + \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs new file mode 100644 index 0000000000..3abcb8c68c --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Avalonia.Controls.DragDrop; +using MonoMac.AppKit; + +namespace Avalonia.MonoMac +{ + class DraggingInfo : IDragData + { + private readonly NSDraggingInfo _info; + + public DraggingInfo(NSDraggingInfo info) + { + _info = info; + } + + + internal static NSDragOperation ConvertDragOperation(DragOperation d) + { + NSDragOperation result = NSDragOperation.None; + if (d.HasFlag(DragOperation.Copy)) + result |= NSDragOperation.Copy; + if (d.HasFlag(DragOperation.Link)) + result |= NSDragOperation.Link; + if (d.HasFlag(DragOperation.Move)) + result |= NSDragOperation.Move; + return result; + } + + internal static DragOperation ConvertDragOperation(NSDragOperation d) + { + DragOperation result = DragOperation.None; + if (d.HasFlag(NSDragOperation.Copy)) + result |= DragOperation.Copy; + if (d.HasFlag(NSDragOperation.Link)) + result |= DragOperation.Link; + if (d.HasFlag(NSDragOperation.Move)) + result |= DragOperation.Move; + return result; + } + + public Point Location => new Point(_info.DraggingLocation.X, _info.DraggingLocation.Y); + + public IEnumerable GetDataFormats() + { + yield break; + } + + public string GetText() + { + return null; + } + + public IEnumerable GetFileNames() + { + yield break; + } + + public bool Contains(string dataFormat) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 667ee12fa0..91e822f802 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Avalonia.Controls.DragDrop; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; @@ -36,6 +37,7 @@ namespace Avalonia.MonoMac bool _isLeftPressed, _isRightPressed, _isMiddlePressed; private readonly IMouseDevice _mouse; private readonly IKeyboardDevice _keyboard; + private readonly IDragDispatcher _dragDispatcher; private NSTrackingArea _area; private NSCursor _cursor; private bool _nonUiRedrawQueued; @@ -52,6 +54,11 @@ namespace Avalonia.MonoMac _tl = tl; _mouse = AvaloniaLocator.Current.GetService(); _keyboard = AvaloniaLocator.Current.GetService(); + _dragDispatcher = AvaloniaLocator.Current.GetService(); + + RegisterForDraggedTypes(new string[] { + "public.data" // register for any kind of data. + }); } protected override void Dispose(bool disposing) @@ -144,6 +151,75 @@ namespace Avalonia.MonoMac UpdateCursor(); } + public override NSDragOperation DraggingEntered(NSDraggingInfo sender) + { + IInputRoot root = _tl?.InputRoot; + if (root == null || _dragDispatcher == null) + return NSDragOperation.None; + + var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); + DraggingInfo info = new DraggingInfo(sender); + var pt = TranslateLocalPoint(info.Location); + + dragOp = _dragDispatcher.DragEnter(root, pt, info, dragOp); + + return DraggingInfo.ConvertDragOperation(dragOp); + } + + public override NSDragOperation DraggingUpdated(NSDraggingInfo sender) + { + IInputRoot root = _tl?.InputRoot; + if (root == null || _dragDispatcher == null) + return NSDragOperation.None; + + var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); + DraggingInfo info = new DraggingInfo(sender); + var pt = TranslateLocalPoint(info.Location); + + dragOp = _dragDispatcher.DragOver(root, pt, info, dragOp); + + return DraggingInfo.ConvertDragOperation(dragOp); + } + + public override void DraggingExited(NSDraggingInfo sender) + { + IInputRoot root = _tl?.InputRoot; + if (root == null || _dragDispatcher == null) + return; + _dragDispatcher.DragLeave(root); + } + + public override bool PrepareForDragOperation(NSDraggingInfo sender) + { + IInputRoot root = _tl?.InputRoot; + if (root == null || _dragDispatcher == null) + return false; + + var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); + DraggingInfo info = new DraggingInfo(sender); + var pt = TranslateLocalPoint(info.Location); + + dragOp = _dragDispatcher.DragOver(root, pt, info, dragOp); + + return DraggingInfo.ConvertDragOperation(dragOp) != DragOperation.None; + } + + public override bool PerformDragOperation(NSDraggingInfo sender) + { + IInputRoot root = _tl?.InputRoot; + if (root == null || _dragDispatcher == null) + return false; + + var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); + DraggingInfo info = new DraggingInfo(sender); + var pt = TranslateLocalPoint(info.Location); + + dragOp = _dragDispatcher.Drop(root, pt, info, dragOp); + + return DraggingInfo.ConvertDragOperation(dragOp) != DragOperation.None; + } + + public override void SetFrameSize(CGSize newSize) { lock (SyncRoot) From 9ae9f6d2218de3bbde789fb044f22b773b31177d Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Sat, 3 Mar 2018 12:40:06 +0100 Subject: [PATCH 044/211] raise routed events on drag and drop --- src/Avalonia.Controls/Application.cs | 4 +- .../DragDrop/DefaultDragDispatcher.cs | 90 +++++++++++++++++++ src/Avalonia.Controls/DragDrop/DragDrop.cs | 24 +++++ .../DragDrop/DragEventArgs.cs | 18 ++++ .../DragDrop/IDragDispatcher.cs | 11 ++- .../Properties/AssemblyInfo.cs | 1 + src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 4 +- 7 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs create mode 100644 src/Avalonia.Controls/DragDrop/DragDrop.cs create mode 100644 src/Avalonia.Controls/DragDrop/DragEventArgs.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 06c1a8b4cc..63b17530ff 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -12,6 +12,7 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; using System.Reactive.Concurrency; +using Avalonia.Controls.DragDrop; namespace Avalonia { @@ -234,7 +235,8 @@ namespace Avalonia .Bind().ToConstant(_styler) .Bind().ToSingleton() .Bind().ToConstant(this) - .Bind().ToConstant(AvaloniaScheduler.Instance); + .Bind().ToConstant(AvaloniaScheduler.Instance) + .Bind().ToConstant(DefaultDragDispatcher.Instance); } } } diff --git a/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs new file mode 100644 index 0000000000..2ce5a32dc5 --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs @@ -0,0 +1,90 @@ +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; +using System.Linq; + +namespace Avalonia.Controls.DragDrop +{ + class DefaultDragDispatcher : IDragDispatcher + { + public static readonly DefaultDragDispatcher Instance = new DefaultDragDispatcher(); + + private Interactive _lastTarget = null; + + private DefaultDragDispatcher() + { + } + + private Interactive GetTarget(IInputElement root, Point local) + { + var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); + if (target != null && DragDrop.GetAcceptDrag(target)) + return target; + return null; + } + + private DragOperation RaiseDragEvent(Interactive target, RoutedEvent routedEvent, DragOperation operation, IDragData data) + { + if (target == null) + return DragOperation.None; + var args = new DragEventArgs(routedEvent, data) + { + RoutedEvent = routedEvent, + DragOperation = operation + }; + target.RaiseEvent(args); + return args.DragOperation; + } + + public DragOperation DragEnter(IInputElement inputRoot, Point point, IDragData data, DragOperation operation) + { + _lastTarget = GetTarget(inputRoot, point); + return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, operation, data); + } + + public DragOperation DragOver(IInputElement inputRoot, Point point, IDragData data, DragOperation operation) + { + var target = GetTarget(inputRoot, point); + + if (target == _lastTarget) + return RaiseDragEvent(target, DragDrop.DragOverEvent, operation, data); + + try + { + if (_lastTarget != null) + _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); + return RaiseDragEvent(target, DragDrop.DragEnterEvent, operation, data); + } + finally + { + _lastTarget = target; + } + } + + public void DragLeave(IInputElement inputRoot) + { + if (_lastTarget == null) + return; + try + { + _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); + } + finally + { + _lastTarget = null; + } + } + + public DragOperation Drop(IInputElement inputRoot, Point point, IDragData data, DragOperation operation) + { + try + { + return RaiseDragEvent(_lastTarget, DragDrop.DropEvent, operation, data); + } + finally + { + _lastTarget = null; + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/DragDrop.cs b/src/Avalonia.Controls/DragDrop/DragDrop.cs new file mode 100644 index 0000000000..3645409a5a --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/DragDrop.cs @@ -0,0 +1,24 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls.DragDrop +{ + public sealed class DragDrop : AvaloniaObject + { + public static RoutedEvent DragEnterEvent = RoutedEvent.Register("DragEnter", RoutingStrategies.Bubble, typeof(DragDrop)); + public static RoutedEvent DragLeaveEvent = RoutedEvent.Register("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop)); + public static RoutedEvent DragOverEvent = RoutedEvent.Register("DragOver", RoutingStrategies.Bubble, typeof(DragDrop)); + public static RoutedEvent DropEvent = RoutedEvent.Register("Drop", RoutingStrategies.Bubble, typeof(DragDrop)); + + public static AvaloniaProperty AcceptDragProperty = AvaloniaProperty.RegisterAttached("AcceptDrag", typeof(DragDrop), inherits: true); + + public static bool GetAcceptDrag(Interactive interactive) + { + return interactive.GetValue(AcceptDragProperty); + } + + public static void SetAcceptDrag(Interactive interactive, bool value) + { + interactive.SetValue(AcceptDragProperty, value); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/DragEventArgs.cs b/src/Avalonia.Controls/DragDrop/DragEventArgs.cs new file mode 100644 index 0000000000..e8bd2528c7 --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/DragEventArgs.cs @@ -0,0 +1,18 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls.DragDrop +{ + public class DragEventArgs : RoutedEventArgs + { + public DragOperation DragOperation { get; set; } + + public IDragData Data { get; private set; } + + public DragEventArgs(RoutedEvent routedEvent, IDragData data) + : base(routedEvent) + { + this.Data = data; + } + + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs index 5c39635dd0..05a211ff76 100644 --- a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs +++ b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs @@ -2,11 +2,14 @@ namespace Avalonia.Controls.DragDrop { + /// + /// Dispatches Drag+Drop events to the correct visual targets, based on the input root and the drag location. + /// public interface IDragDispatcher { - DragOperation DragEnter(IInputRoot inputRoot, Point point, IDragData data, DragOperation operation); - DragOperation DragOver(IInputRoot inputRoot, Point point, IDragData data, DragOperation operation); - void DragLeave(IInputRoot inputRoot); - DragOperation Drop(IInputRoot inputRoot, Point point, IDragData data, DragOperation operation); + DragOperation DragEnter(IInputElement inputRoot, Point point, IDragData data, DragOperation operation); + DragOperation DragOver(IInputElement inputRoot, Point point, IDragData data, DragOperation operation); + void DragLeave(IInputElement inputRoot); + DragOperation Drop(IInputElement inputRoot, Point point, IDragData data, DragOperation operation); } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index ae8c88f7e8..b0877e0fb7 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -11,6 +11,7 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.DragDrop")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 91e822f802..711f6d2787 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -201,7 +201,7 @@ namespace Avalonia.MonoMac dragOp = _dragDispatcher.DragOver(root, pt, info, dragOp); - return DraggingInfo.ConvertDragOperation(dragOp) != DragOperation.None; + return dragOp != DragOperation.None; } public override bool PerformDragOperation(NSDraggingInfo sender) @@ -216,7 +216,7 @@ namespace Avalonia.MonoMac dragOp = _dragDispatcher.Drop(root, pt, info, dragOp); - return DraggingInfo.ConvertDragOperation(dragOp) != DragOperation.None; + return dragOp != DragOperation.None; } From a6e8dc0ffc79d00cb635109d6c3d70b6d105fa49 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Mar 2018 13:48:24 +0100 Subject: [PATCH 045/211] implemented IDragData for OSX --- src/OSX/Avalonia.MonoMac/DraggingInfo.cs | 30 +++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs index 3abcb8c68c..8164470548 100644 --- a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs +++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs @@ -1,11 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Avalonia.Controls.DragDrop; using MonoMac.AppKit; +using MonoMac.Foundation; namespace Avalonia.MonoMac { class DraggingInfo : IDragData - { + { private readonly NSDraggingInfo _info; public DraggingInfo(NSDraggingInfo info) @@ -42,22 +45,37 @@ namespace Avalonia.MonoMac public IEnumerable GetDataFormats() { - yield break; + return _info.DraggingPasteboard.Types.Select(NSTypeToWellknownType); + } + + private string NSTypeToWellknownType(string type) + { + if (type == NSPasteboard.NSStringType) + return DataFormats.Text; + if (type == NSPasteboard.NSFilenamesType) + return DataFormats.FileNames; + return type; } public string GetText() { - return null; + return _info.DraggingPasteboard.GetStringForType(NSPasteboard.NSStringType); } public IEnumerable GetFileNames() { - yield break; + using(var fileNames = (NSArray)_info.DraggingPasteboard.GetPropertyListForType(NSPasteboard.NSFilenamesType)) + { + if (fileNames != null) + return NSArray.StringArrayFromHandle(fileNames.Handle); + } + + return Enumerable.Empty(); } public bool Contains(string dataFormat) { - return false; + return GetDataFormats().Any(f => f == dataFormat); } } } \ No newline at end of file From 1647d95aa6a192dc6978fe264d0b6422a83ba632 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Mar 2018 16:17:45 +0100 Subject: [PATCH 046/211] Initial Drag+Drop support for windows --- .../Avalonia.Win32/ClipboardFormats.cs | 80 ++++++++++ .../Interop/UnmanagedMethods.cs | 71 ++++++++- src/Windows/Avalonia.Win32/OleContext.cs | 47 ++++++ src/Windows/Avalonia.Win32/OleDataObject.cs | 141 ++++++++++++++++++ src/Windows/Avalonia.Win32/OleDropTarget.cs | 131 ++++++++++++++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 10 ++ 6 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 src/Windows/Avalonia.Win32/ClipboardFormats.cs create mode 100644 src/Windows/Avalonia.Win32/OleContext.cs create mode 100644 src/Windows/Avalonia.Win32/OleDataObject.cs create mode 100644 src/Windows/Avalonia.Win32/OleDropTarget.cs diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs new file mode 100644 index 0000000000..254facf81a --- /dev/null +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using Avalonia.Controls.DragDrop; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + static class ClipboardFormats + { + class ClipboardFormat + { + public short Format { get; private set; } + public string Name { get; private set; } + + public ClipboardFormat(string name, short format) + { + Format = format; + Name = name; + } + } + + private static readonly List FormatList = new List() + { + new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT), + new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP), + }; + + + private static string QueryFormatName(short format) + { + int len = UnmanagedMethods.GetClipboardFormatName(format, null, 0); + if (len > 0) + { + StringBuilder sb = new StringBuilder(len); + if (UnmanagedMethods.GetClipboardFormatName(format, sb, len) <= len) + return sb.ToString(); + } + return null; + } + + public static string GetFormat(short format) + { + lock (FormatList) + { + var pd = FormatList.FirstOrDefault(f => f.Format == format); + if (pd == null) + { + string name = QueryFormatName(format); + if (string.IsNullOrEmpty(name)) + name = string.Format("Unknown_Format_{0}", format); + pd = new ClipboardFormat(name, format); + FormatList.Add(pd); + } + return pd.Name; + } + } + + public static short GetFormat(string format) + { + lock (FormatList) + { + var pd = FormatList.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Name, format)); + if (pd == null) + { + int id = UnmanagedMethods.RegisterClipboardFormat(format); + if (id == 0) + throw new Win32Exception(); + pd = new ClipboardFormat(format, (short)id); + FormatList.Add(pd); + } + return pd.Format; + } + } + + + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index f13dd3272c..dada2eb7e6 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; using System.Text; // ReSharper disable InconsistentNaming @@ -951,6 +952,28 @@ namespace Avalonia.Win32.Interop [DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)] public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target); + + [DllImport("ole32.dll", EntryPoint = "OleInitialize")] + public static extern HRESULT OleInitialize(IntPtr val); + + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + internal static extern void ReleaseStgMedium(ref STGMEDIUM medium); + + [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)] + public static extern int GetClipboardFormatName(int format, StringBuilder lpString, int cchMax); + + [DllImport("user32.dll", BestFitMapping = false, CharSet = CharSet.Auto, SetLastError = true)] + public static extern int RegisterClipboardFormat(string format); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true, SetLastError = true)] + public static extern IntPtr GlobalSize(IntPtr hGlobal); + + [DllImport("shell32.dll", BestFitMapping = false, CharSet = CharSet.Auto)] + public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch); + + public enum MONITOR { MONITOR_DEFAULTTONULL = 0x00000000, @@ -993,7 +1016,8 @@ namespace Avalonia.Win32.Interop public enum ClipboardFormat { CF_TEXT = 1, - CF_UNICODETEXT = 13 + CF_UNICODETEXT = 13, + CF_HDROP = 15 } public struct MSG @@ -1300,4 +1324,49 @@ namespace Avalonia.Win32.Interop uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder); } + + [Flags] + internal enum DropEffect : int + { + None = 0, + Copy = 1, + Move = 2, + Link = 4, + Scroll = -2147483648, + } + + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("0000010E-0000-0000-C000-000000000046")] + [ComImport] + internal interface IOleDataObject + { + void GetData([In] ref FORMATETC format, out STGMEDIUM medium); + void GetDataHere([In] ref FORMATETC format, ref STGMEDIUM medium); + [PreserveSig] + int QueryGetData([In] ref FORMATETC format); + [PreserveSig] + int GetCanonicalFormatEtc([In] ref FORMATETC formatIn, out FORMATETC formatOut); + void SetData([In] ref FORMATETC formatIn, [In] ref STGMEDIUM medium, [MarshalAs(UnmanagedType.Bool)] bool release); + IEnumFORMATETC EnumFormatEtc(DATADIR direction); + [PreserveSig] + int DAdvise([In] ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection); + void DUnadvise(int connection); + [PreserveSig] + int EnumDAdvise(out IEnumSTATDATA enumAdvise); + } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("00000122-0000-0000-C000-000000000046")] + internal interface IDropTarget + { + [PreserveSig] + UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + [PreserveSig] + UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + [PreserveSig] + UnmanagedMethods.HRESULT DragLeave(); + [PreserveSig] + UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + } } diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs new file mode 100644 index 0000000000..0c597e3d17 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.Win32.Interop; + +namespace Zippr.UIServices.Avalonia.Windows +{ + class OleContext + { + private static OleContext fCurrent; + + internal static OleContext Current + { + get + { + if (!IsValidOleThread()) + return null; + + if (fCurrent == null) + fCurrent = new OleContext(); + return fCurrent; + } + } + + + private OleContext() + { + if (UnmanagedMethods.OleInitialize(IntPtr.Zero) != UnmanagedMethods.HRESULT.S_OK) + throw new SystemException("Failed to initialize OLE"); + } + + private static bool IsValidOleThread() + { + return Dispatcher.UIThread.CheckAccess() && + Thread.CurrentThread.GetApartmentState() == ApartmentState.STA; + } + + internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target) + { + if (hwnd?.HandleDescriptor != "HWND" || target == null) + return false; + + return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK; + } + } +} diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs new file mode 100644 index 0000000000..6c86cd03e0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; +using Avalonia.Controls.DragDrop; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class OleDataObject : IDragData + { + private IOleDataObject _wrapped; + + public OleDataObject(IOleDataObject wrapped) + { + _wrapped = wrapped; + } + + public bool Contains(string dataFormat) + { + return GetDataFormatsCore().Any(df => StringComparer.OrdinalIgnoreCase.Equals(df, dataFormat)); + } + + public IEnumerable GetDataFormats() + { + return GetDataFormatsCore().Distinct(); + } + + public string GetText() + { + return GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT) as string; + } + + public IEnumerable GetFileNames() + { + return GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT) as IEnumerable; + } + + private object GetDataFromOleHGLOBAL(string format, DVASPECT aspect) + { + FORMATETC formatEtc = new FORMATETC(); + formatEtc.cfFormat = ClipboardFormats.GetFormat(format); + formatEtc.dwAspect = aspect; + formatEtc.lindex = -1; + formatEtc.tymed = TYMED.TYMED_HGLOBAL; + if (_wrapped.QueryGetData(ref formatEtc) == 0) + { + _wrapped.GetData(ref formatEtc, out STGMEDIUM medium); + try + { + if (medium.unionmember != IntPtr.Zero && medium.tymed == TYMED.TYMED_HGLOBAL) + { + if (format == DataFormats.Text) + return ReadStringFromHGlobal(medium.unionmember); + if (format == DataFormats.FileNames) + return ReadFileNamesFromHGlobal(medium.unionmember); + return ReadBytesFromHGlobal(medium.unionmember); + } + } + finally + { + UnmanagedMethods.ReleaseStgMedium(ref medium); + } + } + return null; + } + + private static IEnumerable ReadFileNamesFromHGlobal(IntPtr hGlobal) + { + List files = new List(); + int fileCount = UnmanagedMethods.DragQueryFile(hGlobal, -1, null, 0); + if (fileCount > 0) + { + for (int i = 0; i < fileCount; i++) + { + int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0); + StringBuilder sb = new StringBuilder(pathLen+1); + + if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen) + { + files.Add(sb.ToString()); + } + } + } + return files; + } + + private static string ReadStringFromHGlobal(IntPtr hGlobal) + { + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + return Marshal.PtrToStringAuto(ptr); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + private static byte[] ReadBytesFromHGlobal(IntPtr hGlobal) + { + IntPtr source = UnmanagedMethods.GlobalLock(hGlobal); + try + { + int size = (int)UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + byte[] data = new byte[size]; + Marshal.Copy(source, data, 0, size); + return data; + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + private IEnumerable GetDataFormatsCore() + { + var enumFormat = _wrapped.EnumFormatEtc(DATADIR.DATADIR_GET); + if (enumFormat != null) + { + enumFormat.Reset(); + FORMATETC[] formats = new FORMATETC[1]; + int[] fetched = { 1 }; + while (fetched[0] > 0) + { + fetched[0] = 0; + if (enumFormat.Next(1, formats, fetched) == 0 && fetched[0] > 0) + { + if (formats[0].ptd != IntPtr.Zero) + Marshal.FreeCoTaskMem(formats[0].ptd); + + yield return ClipboardFormats.GetFormat(formats[0].cfFormat); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs new file mode 100644 index 0000000000..b0f592403b --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -0,0 +1,131 @@ +using System; +using System.Runtime.InteropServices.ComTypes; +using Avalonia.Controls; +using Avalonia.Controls.DragDrop; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.VisualTree; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class OleDropTarget : IDropTarget + { + private readonly IDragDispatcher _dragDispatcher; + private readonly IInputElement _target; + + private IDragData _currentDrag = null; + + public OleDropTarget(IInputElement target) + { + _dragDispatcher = AvaloniaLocator.Current.GetService(); + _target = target; + } + + static DropEffect ConvertDropEffect(DragOperation operation) + { + DropEffect result = DropEffect.None; + if (operation.HasFlag(DragOperation.Copy)) + result |= DropEffect.Copy; + if (operation.HasFlag(DragOperation.Move)) + result |= DropEffect.Move; + if (operation.HasFlag(DragOperation.Link)) + result |= DropEffect.Link; + return result; + } + + static DragOperation ConvertDropEffect(DropEffect effect) + { + DragOperation result = DragOperation.None; + if (effect.HasFlag(DropEffect.Copy)) + result |= DragOperation.Copy; + if (effect.HasFlag(DropEffect.Move)) + result |= DragOperation.Move; + if (effect.HasFlag(DropEffect.Link)) + result |= DragOperation.Link; + return result; + } + + UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) + { + if (_dragDispatcher == null) + { + pdwEffect = DropEffect.None; + return UnmanagedMethods.HRESULT.S_OK; + } + + _currentDrag = new OleDataObject(pDataObj); + var dragLocation = GetDragLocation(pt); + + var operation = ConvertDropEffect(pdwEffect); + operation = _dragDispatcher.DragEnter(_target, dragLocation, _currentDrag, operation); + pdwEffect = ConvertDropEffect(operation); + + return UnmanagedMethods.HRESULT.S_OK; + } + + UnmanagedMethods.HRESULT IDropTarget.DragOver(int grfKeyState, long pt, ref DropEffect pdwEffect) + { + if (_dragDispatcher == null) + { + pdwEffect = DropEffect.None; + return UnmanagedMethods.HRESULT.S_OK; + } + + var dragLocation = GetDragLocation(pt); + + var operation = ConvertDropEffect(pdwEffect); + operation = _dragDispatcher.DragOver(_target, dragLocation, _currentDrag, operation); + pdwEffect = ConvertDropEffect(operation); + + return UnmanagedMethods.HRESULT.S_OK; + } + + UnmanagedMethods.HRESULT IDropTarget.DragLeave() + { + try + { + _dragDispatcher?.DragLeave(_target); + return UnmanagedMethods.HRESULT.S_OK; + } + finally + { + _currentDrag = null; + } + } + + UnmanagedMethods.HRESULT IDropTarget.Drop(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) + { + try + { + if (_dragDispatcher == null) + { + pdwEffect = DropEffect.None; + return UnmanagedMethods.HRESULT.S_OK; + } + + _currentDrag= new OleDataObject(pDataObj); + var dragLocation = GetDragLocation(pt); + + var operation = ConvertDropEffect(pdwEffect); + operation = _dragDispatcher.Drop(_target, dragLocation, _currentDrag, operation); + pdwEffect = ConvertDropEffect(operation); + + return UnmanagedMethods.HRESULT.S_OK; + } + finally + { + _currentDrag = null; + } + } + + private Point GetDragLocation(long dragPoint) + { + int x = (int)dragPoint; + int y = (int)(dragPoint >> 32); + + Point screenPt = new Point(x, y); + return _target.PointToClient(screenPt); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 3ba926b42a..2fca0baac7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -14,6 +14,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; +using Zippr.UIServices.Avalonia.Windows; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 @@ -34,6 +35,7 @@ namespace Avalonia.Win32 private double _scaling = 1; private WindowState _showWindowState; private FramebufferManager _framebuffer; + private OleDropTarget _dropTarget; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; #endif @@ -308,6 +310,7 @@ namespace Avalonia.Win32 public void SetInputRoot(IInputRoot inputRoot) { _owner = inputRoot; + CreateDropTarget(); } public void SetTitle(string title) @@ -689,6 +692,13 @@ namespace Avalonia.Win32 } } + private void CreateDropTarget() + { + OleDropTarget odt = new OleDropTarget(_owner); + if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false) + _dropTarget = odt; + } + private Point DipFromLParam(IntPtr lParam) { return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; From 50212da915cd32db81211a29840150d8eed6d1b2 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Mar 2018 16:20:12 +0100 Subject: [PATCH 047/211] DragDrop is now a static class --- src/Avalonia.Controls/DragDrop/DragDrop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DragDrop/DragDrop.cs b/src/Avalonia.Controls/DragDrop/DragDrop.cs index 3645409a5a..317a903d3c 100644 --- a/src/Avalonia.Controls/DragDrop/DragDrop.cs +++ b/src/Avalonia.Controls/DragDrop/DragDrop.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls.DragDrop { - public sealed class DragDrop : AvaloniaObject + public static class DragDrop { public static RoutedEvent DragEnterEvent = RoutedEvent.Register("DragEnter", RoutingStrategies.Bubble, typeof(DragDrop)); public static RoutedEvent DragLeaveEvent = RoutedEvent.Register("DragLeave", RoutingStrategies.Bubble, typeof(DragDrop)); From 4c362818779eec17a1c016faad5c22056055d160 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Mar 2018 16:29:41 +0100 Subject: [PATCH 048/211] renamed DragOperation to DragDropEffects --- .../DragDrop/DefaultDragDispatcher.cs | 22 +++++++++---------- .../{DragOperation.cs => DragDropEffects.cs} | 2 +- .../DragDrop/DragEventArgs.cs | 2 +- .../DragDrop/IDragDispatcher.cs | 6 ++--- src/OSX/Avalonia.MonoMac/DraggingInfo.cs | 18 +++++++-------- src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 4 ++-- src/Windows/Avalonia.Win32/OleDropTarget.cs | 18 +++++++-------- 7 files changed, 36 insertions(+), 36 deletions(-) rename src/Avalonia.Controls/DragDrop/{DragOperation.cs => DragDropEffects.cs} (82%) diff --git a/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs index 2ce5a32dc5..68fa6cad75 100644 --- a/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs +++ b/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs @@ -23,37 +23,37 @@ namespace Avalonia.Controls.DragDrop return null; } - private DragOperation RaiseDragEvent(Interactive target, RoutedEvent routedEvent, DragOperation operation, IDragData data) + private DragDropEffects RaiseDragEvent(Interactive target, RoutedEvent routedEvent, DragDropEffects operation, IDragData data) { if (target == null) - return DragOperation.None; + return DragDropEffects.None; var args = new DragEventArgs(routedEvent, data) { RoutedEvent = routedEvent, - DragOperation = operation + DragEffects = operation }; target.RaiseEvent(args); - return args.DragOperation; + return args.DragEffects; } - public DragOperation DragEnter(IInputElement inputRoot, Point point, IDragData data, DragOperation operation) + public DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects) { _lastTarget = GetTarget(inputRoot, point); - return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, operation, data); + return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, effects, data); } - public DragOperation DragOver(IInputElement inputRoot, Point point, IDragData data, DragOperation operation) + public DragDropEffects DragOver(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects) { var target = GetTarget(inputRoot, point); if (target == _lastTarget) - return RaiseDragEvent(target, DragDrop.DragOverEvent, operation, data); + return RaiseDragEvent(target, DragDrop.DragOverEvent, effects, data); try { if (_lastTarget != null) _lastTarget.RaiseEvent(new RoutedEventArgs(DragDrop.DragLeaveEvent)); - return RaiseDragEvent(target, DragDrop.DragEnterEvent, operation, data); + return RaiseDragEvent(target, DragDrop.DragEnterEvent, effects, data); } finally { @@ -75,11 +75,11 @@ namespace Avalonia.Controls.DragDrop } } - public DragOperation Drop(IInputElement inputRoot, Point point, IDragData data, DragOperation operation) + public DragDropEffects Drop(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects) { try { - return RaiseDragEvent(_lastTarget, DragDrop.DropEvent, operation, data); + return RaiseDragEvent(_lastTarget, DragDrop.DropEvent, effects, data); } finally { diff --git a/src/Avalonia.Controls/DragDrop/DragOperation.cs b/src/Avalonia.Controls/DragDrop/DragDropEffects.cs similarity index 82% rename from src/Avalonia.Controls/DragDrop/DragOperation.cs rename to src/Avalonia.Controls/DragDrop/DragDropEffects.cs index ffc6f666f7..cec3dab89d 100644 --- a/src/Avalonia.Controls/DragDrop/DragOperation.cs +++ b/src/Avalonia.Controls/DragDrop/DragDropEffects.cs @@ -3,7 +3,7 @@ namespace Avalonia.Controls.DragDrop { [Flags] - public enum DragOperation + public enum DragDropEffects { None = 0, Copy = 1, diff --git a/src/Avalonia.Controls/DragDrop/DragEventArgs.cs b/src/Avalonia.Controls/DragDrop/DragEventArgs.cs index e8bd2528c7..2b6658d395 100644 --- a/src/Avalonia.Controls/DragDrop/DragEventArgs.cs +++ b/src/Avalonia.Controls/DragDrop/DragEventArgs.cs @@ -4,7 +4,7 @@ namespace Avalonia.Controls.DragDrop { public class DragEventArgs : RoutedEventArgs { - public DragOperation DragOperation { get; set; } + public DragDropEffects DragEffects { get; set; } public IDragData Data { get; private set; } diff --git a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs index 05a211ff76..84c023ddbc 100644 --- a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs +++ b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs @@ -7,9 +7,9 @@ namespace Avalonia.Controls.DragDrop /// public interface IDragDispatcher { - DragOperation DragEnter(IInputElement inputRoot, Point point, IDragData data, DragOperation operation); - DragOperation DragOver(IInputElement inputRoot, Point point, IDragData data, DragOperation operation); + DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects); + DragDropEffects DragOver(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects); void DragLeave(IInputElement inputRoot); - DragOperation Drop(IInputElement inputRoot, Point point, IDragData data, DragOperation operation); + DragDropEffects Drop(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects); } } \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs index 8164470548..dccc39e861 100644 --- a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs +++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs @@ -17,27 +17,27 @@ namespace Avalonia.MonoMac } - internal static NSDragOperation ConvertDragOperation(DragOperation d) + internal static NSDragOperation ConvertDragOperation(DragDropEffects d) { NSDragOperation result = NSDragOperation.None; - if (d.HasFlag(DragOperation.Copy)) + if (d.HasFlag(DragDropEffects.Copy)) result |= NSDragOperation.Copy; - if (d.HasFlag(DragOperation.Link)) + if (d.HasFlag(DragDropEffects.Link)) result |= NSDragOperation.Link; - if (d.HasFlag(DragOperation.Move)) + if (d.HasFlag(DragDropEffects.Move)) result |= NSDragOperation.Move; return result; } - internal static DragOperation ConvertDragOperation(NSDragOperation d) + internal static DragDropEffects ConvertDragOperation(NSDragOperation d) { - DragOperation result = DragOperation.None; + DragDropEffects result = DragDropEffects.None; if (d.HasFlag(NSDragOperation.Copy)) - result |= DragOperation.Copy; + result |= DragDropEffects.Copy; if (d.HasFlag(NSDragOperation.Link)) - result |= DragOperation.Link; + result |= DragDropEffects.Link; if (d.HasFlag(NSDragOperation.Move)) - result |= DragOperation.Move; + result |= DragDropEffects.Move; return result; } diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 711f6d2787..4398b6bddb 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -201,7 +201,7 @@ namespace Avalonia.MonoMac dragOp = _dragDispatcher.DragOver(root, pt, info, dragOp); - return dragOp != DragOperation.None; + return dragOp != DragDropEffects.None; } public override bool PerformDragOperation(NSDraggingInfo sender) @@ -216,7 +216,7 @@ namespace Avalonia.MonoMac dragOp = _dragDispatcher.Drop(root, pt, info, dragOp); - return dragOp != DragOperation.None; + return dragOp != DragDropEffects.None; } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index b0f592403b..e3725d3bbf 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -22,27 +22,27 @@ namespace Avalonia.Win32 _target = target; } - static DropEffect ConvertDropEffect(DragOperation operation) + static DropEffect ConvertDropEffect(DragDropEffects operation) { DropEffect result = DropEffect.None; - if (operation.HasFlag(DragOperation.Copy)) + if (operation.HasFlag(DragDropEffects.Copy)) result |= DropEffect.Copy; - if (operation.HasFlag(DragOperation.Move)) + if (operation.HasFlag(DragDropEffects.Move)) result |= DropEffect.Move; - if (operation.HasFlag(DragOperation.Link)) + if (operation.HasFlag(DragDropEffects.Link)) result |= DropEffect.Link; return result; } - static DragOperation ConvertDropEffect(DropEffect effect) + static DragDropEffects ConvertDropEffect(DropEffect effect) { - DragOperation result = DragOperation.None; + DragDropEffects result = DragDropEffects.None; if (effect.HasFlag(DropEffect.Copy)) - result |= DragOperation.Copy; + result |= DragDropEffects.Copy; if (effect.HasFlag(DropEffect.Move)) - result |= DragOperation.Move; + result |= DragDropEffects.Move; if (effect.HasFlag(DropEffect.Link)) - result |= DragOperation.Link; + result |= DragDropEffects.Link; return result; } From 83b18944d3ac81ed089902fea6dd7105e3664c55 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Mar 2018 16:57:25 +0100 Subject: [PATCH 049/211] corrected namespace. (copy+paste error) --- src/Windows/Avalonia.Win32/OleContext.cs | 2 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/OleContext.cs b/src/Windows/Avalonia.Win32/OleContext.cs index 0c597e3d17..085c0f8ea9 100644 --- a/src/Windows/Avalonia.Win32/OleContext.cs +++ b/src/Windows/Avalonia.Win32/OleContext.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; using Avalonia.Threading; using Avalonia.Win32.Interop; -namespace Zippr.UIServices.Avalonia.Windows +namespace Avalonia.Win32 { class OleContext { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 2fca0baac7..ed224a6525 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -14,7 +14,6 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; -using Zippr.UIServices.Avalonia.Windows; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 From 0adc6e62de8a95129e4cc73b9772db838ec5b74e Mon Sep 17 00:00:00 2001 From: boombuler Date: Sat, 3 Mar 2018 17:36:11 +0100 Subject: [PATCH 050/211] renamed IDragData to IDataObject --- .../DragDrop/DefaultDragDispatcher.cs | 8 +++---- .../DragDrop/DragEventArgs.cs | 4 ++-- .../DragDrop/{IDragData.cs => IDataObject.cs} | 2 +- .../DragDrop/IDragDispatcher.cs | 6 ++--- src/OSX/Avalonia.MonoMac/DraggingInfo.cs | 2 +- .../Interop/UnmanagedMethods.cs | 24 +++---------------- src/Windows/Avalonia.Win32/OleDataObject.cs | 7 +++--- src/Windows/Avalonia.Win32/OleDropTarget.cs | 11 ++++----- 8 files changed, 22 insertions(+), 42 deletions(-) rename src/Avalonia.Controls/DragDrop/{IDragData.cs => IDataObject.cs} (88%) diff --git a/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs index 68fa6cad75..2db227248b 100644 --- a/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs +++ b/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.DragDrop return null; } - private DragDropEffects RaiseDragEvent(Interactive target, RoutedEvent routedEvent, DragDropEffects operation, IDragData data) + private DragDropEffects RaiseDragEvent(Interactive target, RoutedEvent routedEvent, DragDropEffects operation, IDataObject data) { if (target == null) return DragDropEffects.None; @@ -36,13 +36,13 @@ namespace Avalonia.Controls.DragDrop return args.DragEffects; } - public DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects) + public DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) { _lastTarget = GetTarget(inputRoot, point); return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, effects, data); } - public DragDropEffects DragOver(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects) + public DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) { var target = GetTarget(inputRoot, point); @@ -75,7 +75,7 @@ namespace Avalonia.Controls.DragDrop } } - public DragDropEffects Drop(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects) + public DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) { try { diff --git a/src/Avalonia.Controls/DragDrop/DragEventArgs.cs b/src/Avalonia.Controls/DragDrop/DragEventArgs.cs index 2b6658d395..f31bfd4820 100644 --- a/src/Avalonia.Controls/DragDrop/DragEventArgs.cs +++ b/src/Avalonia.Controls/DragDrop/DragEventArgs.cs @@ -6,9 +6,9 @@ namespace Avalonia.Controls.DragDrop { public DragDropEffects DragEffects { get; set; } - public IDragData Data { get; private set; } + public IDataObject Data { get; private set; } - public DragEventArgs(RoutedEvent routedEvent, IDragData data) + public DragEventArgs(RoutedEvent routedEvent, IDataObject data) : base(routedEvent) { this.Data = data; diff --git a/src/Avalonia.Controls/DragDrop/IDragData.cs b/src/Avalonia.Controls/DragDrop/IDataObject.cs similarity index 88% rename from src/Avalonia.Controls/DragDrop/IDragData.cs rename to src/Avalonia.Controls/DragDrop/IDataObject.cs index b6dc53d32d..5ffecc13ed 100644 --- a/src/Avalonia.Controls/DragDrop/IDragData.cs +++ b/src/Avalonia.Controls/DragDrop/IDataObject.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls.DragDrop { - public interface IDragData + public interface IDataObject { IEnumerable GetDataFormats(); diff --git a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs index 84c023ddbc..538ffa171b 100644 --- a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs +++ b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs @@ -7,9 +7,9 @@ namespace Avalonia.Controls.DragDrop /// public interface IDragDispatcher { - DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects); - DragDropEffects DragOver(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects); + DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects); + DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects); void DragLeave(IInputElement inputRoot); - DragDropEffects Drop(IInputElement inputRoot, Point point, IDragData data, DragDropEffects effects); + DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects); } } \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs index dccc39e861..ef4d9d99f6 100644 --- a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs +++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs @@ -7,7 +7,7 @@ using MonoMac.Foundation; namespace Avalonia.MonoMac { - class DraggingInfo : IDragData + class DraggingInfo : IDataObject { private readonly NSDraggingInfo _info; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index dada2eb7e6..f43cdb98cc 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1335,25 +1335,7 @@ namespace Avalonia.Win32.Interop Scroll = -2147483648, } - [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - [Guid("0000010E-0000-0000-C000-000000000046")] - [ComImport] - internal interface IOleDataObject - { - void GetData([In] ref FORMATETC format, out STGMEDIUM medium); - void GetDataHere([In] ref FORMATETC format, ref STGMEDIUM medium); - [PreserveSig] - int QueryGetData([In] ref FORMATETC format); - [PreserveSig] - int GetCanonicalFormatEtc([In] ref FORMATETC formatIn, out FORMATETC formatOut); - void SetData([In] ref FORMATETC formatIn, [In] ref STGMEDIUM medium, [MarshalAs(UnmanagedType.Bool)] bool release); - IEnumFORMATETC EnumFormatEtc(DATADIR direction); - [PreserveSig] - int DAdvise([In] ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection); - void DUnadvise(int connection); - [PreserveSig] - int EnumDAdvise(out IEnumSTATDATA enumAdvise); - } + [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] @@ -1361,12 +1343,12 @@ namespace Avalonia.Win32.Interop internal interface IDropTarget { [PreserveSig] - UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + UnmanagedMethods.HRESULT DragEnter([MarshalAs(UnmanagedType.Interface)] [In] IDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); [PreserveSig] UnmanagedMethods.HRESULT DragOver([MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); [PreserveSig] UnmanagedMethods.HRESULT DragLeave(); [PreserveSig] - UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IOleDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); + UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); } } diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index 6c86cd03e0..bf6f3d98c3 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -6,14 +6,15 @@ using System.Runtime.InteropServices.ComTypes; using System.Text; using Avalonia.Controls.DragDrop; using Avalonia.Win32.Interop; +using IDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; namespace Avalonia.Win32 { - class OleDataObject : IDragData + class OleDataObject : Avalonia.Controls.DragDrop.IDataObject { - private IOleDataObject _wrapped; + private IDataObject _wrapped; - public OleDataObject(IOleDataObject wrapped) + public OleDataObject(IDataObject wrapped) { _wrapped = wrapped; } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index e3725d3bbf..925f9b7046 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -1,11 +1,8 @@ -using System; -using System.Runtime.InteropServices.ComTypes; -using Avalonia.Controls; -using Avalonia.Controls.DragDrop; +using Avalonia.Controls.DragDrop; using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.VisualTree; using Avalonia.Win32.Interop; +using IDataObject = Avalonia.Controls.DragDrop.IDataObject; +using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; namespace Avalonia.Win32 { @@ -14,7 +11,7 @@ namespace Avalonia.Win32 private readonly IDragDispatcher _dragDispatcher; private readonly IInputElement _target; - private IDragData _currentDrag = null; + private IDataObject _currentDrag = null; public OleDropTarget(IInputElement target) { From 4c6a341b73a5198d1539506c057c4e1b392e9f76 Mon Sep 17 00:00:00 2001 From: boombuler Date: Sun, 4 Mar 2018 21:13:02 +0100 Subject: [PATCH 051/211] reworked Drag event dispatching --- src/Avalonia.Controls/Application.cs | 3 +- ...DefaultDragDispatcher.cs => DragDevice.cs} | 45 ++++++++--- .../DragDrop/IDragDispatcher.cs | 15 ---- .../DragDrop/Raw/IDragDevice.cs | 8 ++ .../DragDrop/Raw/RawDragEvent.cs | 31 ++++++++ .../DragDrop/Raw/RawDragEventType.cs | 10 +++ src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 65 +++++----------- src/Windows/Avalonia.Win32/OleDropTarget.cs | 76 +++++++++++++------ src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 9 files changed, 158 insertions(+), 97 deletions(-) rename src/Avalonia.Controls/DragDrop/{DefaultDragDispatcher.cs => DragDevice.cs} (62%) delete mode 100644 src/Avalonia.Controls/DragDrop/IDragDispatcher.cs create mode 100644 src/Avalonia.Controls/DragDrop/Raw/IDragDevice.cs create mode 100644 src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs create mode 100644 src/Avalonia.Controls/DragDrop/Raw/RawDragEventType.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 63b17530ff..b751aeca60 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -13,6 +13,7 @@ using Avalonia.Styling; using Avalonia.Threading; using System.Reactive.Concurrency; using Avalonia.Controls.DragDrop; +using Avalonia.Controls.DragDrop.Raw; namespace Avalonia { @@ -236,7 +237,7 @@ namespace Avalonia .Bind().ToSingleton() .Bind().ToConstant(this) .Bind().ToConstant(AvaloniaScheduler.Instance) - .Bind().ToConstant(DefaultDragDispatcher.Instance); + .Bind().ToConstant(DragDevice.Instance); } } } diff --git a/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/DragDevice.cs similarity index 62% rename from src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs rename to src/Avalonia.Controls/DragDrop/DragDevice.cs index 2db227248b..25f82ca3bc 100644 --- a/src/Avalonia.Controls/DragDrop/DefaultDragDispatcher.cs +++ b/src/Avalonia.Controls/DragDrop/DragDevice.cs @@ -2,19 +2,17 @@ using Avalonia.Interactivity; using Avalonia.VisualTree; using System.Linq; +using Avalonia.Controls.DragDrop.Raw; +using Avalonia.Input.Raw; namespace Avalonia.Controls.DragDrop { - class DefaultDragDispatcher : IDragDispatcher + class DragDevice : IDragDevice { - public static readonly DefaultDragDispatcher Instance = new DefaultDragDispatcher(); - + public static readonly DragDevice Instance = new DragDevice(); + private Interactive _lastTarget = null; - private DefaultDragDispatcher() - { - } - private Interactive GetTarget(IInputElement root, Point local) { var target = root.InputHitTest(local)?.GetSelfAndVisualAncestors()?.OfType()?.FirstOrDefault(); @@ -36,13 +34,13 @@ namespace Avalonia.Controls.DragDrop return args.DragEffects; } - public DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) { _lastTarget = GetTarget(inputRoot, point); return RaiseDragEvent(_lastTarget, DragDrop.DragEnterEvent, effects, data); } - public DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) { var target = GetTarget(inputRoot, point); @@ -61,7 +59,7 @@ namespace Avalonia.Controls.DragDrop } } - public void DragLeave(IInputElement inputRoot) + private void DragLeave(IInputElement inputRoot) { if (_lastTarget == null) return; @@ -75,7 +73,7 @@ namespace Avalonia.Controls.DragDrop } } - public DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) + private DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects) { try { @@ -86,5 +84,30 @@ namespace Avalonia.Controls.DragDrop _lastTarget = null; } } + + public void ProcessRawEvent(RawInputEventArgs e) + { + if (!e.Handled && e is RawDragEvent margs) + ProcessRawEvent(margs); + } + + private void ProcessRawEvent(RawDragEvent e) + { + switch (e.Type) + { + case RawDragEventType.DragEnter: + e.Effects = DragEnter(e.InputRoot, e.Location, e.Data, e.Effects); + break; + case RawDragEventType.DragOver: + e.Effects = DragOver(e.InputRoot, e.Location, e.Data, e.Effects); + break; + case RawDragEventType.DragLeave: + DragLeave(e.InputRoot); + break; + case RawDragEventType.Drop: + e.Effects = Drop(e.InputRoot, e.Location, e.Data, e.Effects); + break; + } + } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs b/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs deleted file mode 100644 index 538ffa171b..0000000000 --- a/src/Avalonia.Controls/DragDrop/IDragDispatcher.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Input; - -namespace Avalonia.Controls.DragDrop -{ - /// - /// Dispatches Drag+Drop events to the correct visual targets, based on the input root and the drag location. - /// - public interface IDragDispatcher - { - DragDropEffects DragEnter(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects); - DragDropEffects DragOver(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects); - void DragLeave(IInputElement inputRoot); - DragDropEffects Drop(IInputElement inputRoot, Point point, IDataObject data, DragDropEffects effects); - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/Raw/IDragDevice.cs b/src/Avalonia.Controls/DragDrop/Raw/IDragDevice.cs new file mode 100644 index 0000000000..d1c20fae55 --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/Raw/IDragDevice.cs @@ -0,0 +1,8 @@ +using Avalonia.Input; + +namespace Avalonia.Controls.DragDrop.Raw +{ + public interface IDragDevice : IInputDevice + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs b/src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs new file mode 100644 index 0000000000..76a2c16b3b --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.Controls.DragDrop.Raw +{ + public class RawDragEvent : RawInputEventArgs + { + public IInputElement InputRoot { get; } + public Point Location { get; } + public IDataObject Data { get; } + public DragDropEffects Effects { get; set; } + public RawDragEventType Type { get; } + + public RawDragEvent(IDragDevice inputDevice, RawDragEventType type, + IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects) + :base(inputDevice, GetTimeStamp()) + { + Type = type; + InputRoot = inputRoot; + Location = location; + Data = data; + Effects = effects; + } + + private static uint GetTimeStamp() + { + return (uint)0; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/Raw/RawDragEventType.cs b/src/Avalonia.Controls/DragDrop/Raw/RawDragEventType.cs new file mode 100644 index 0000000000..44c4bbd595 --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/Raw/RawDragEventType.cs @@ -0,0 +1,10 @@ +namespace Avalonia.Controls.DragDrop.Raw +{ + public enum RawDragEventType + { + DragEnter, + DragOver, + DragLeave, + Drop + } +} \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 4398b6bddb..b767a86490 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Controls.DragDrop; +using Avalonia.Controls.DragDrop.Raw; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; @@ -19,6 +20,7 @@ namespace Avalonia.MonoMac { public TopLevelView View { get; } private readonly IMouseDevice _mouse = AvaloniaLocator.Current.GetService(); + private readonly IDragDevice _dragDevice = AvaloniaLocator.Current.GetService(); protected TopLevelImpl() { View = new TopLevelView(this); @@ -37,7 +39,6 @@ namespace Avalonia.MonoMac bool _isLeftPressed, _isRightPressed, _isMiddlePressed; private readonly IMouseDevice _mouse; private readonly IKeyboardDevice _keyboard; - private readonly IDragDispatcher _dragDispatcher; private NSTrackingArea _area; private NSCursor _cursor; private bool _nonUiRedrawQueued; @@ -54,7 +55,6 @@ namespace Avalonia.MonoMac _tl = tl; _mouse = AvaloniaLocator.Current.GetService(); _keyboard = AvaloniaLocator.Current.GetService(); - _dragDispatcher = AvaloniaLocator.Current.GetService(); RegisterForDraggedTypes(new string[] { "public.data" // register for any kind of data. @@ -151,72 +151,45 @@ namespace Avalonia.MonoMac UpdateCursor(); } - public override NSDragOperation DraggingEntered(NSDraggingInfo sender) + private NSDragOperation SendRawDragEvent(NSDraggingInfo sender, RawDragEventType type) { + Action input = _tl.Input; + IDragDevice dragDevice = _tl._dragDevice; IInputRoot root = _tl?.InputRoot; - if (root == null || _dragDispatcher == null) + if (root == null || dragDevice == null || input == null) return NSDragOperation.None; - + var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); DraggingInfo info = new DraggingInfo(sender); var pt = TranslateLocalPoint(info.Location); - - dragOp = _dragDispatcher.DragEnter(root, pt, info, dragOp); - - return DraggingInfo.ConvertDragOperation(dragOp); + var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp); + input(args); + return DraggingInfo.ConvertDragOperation(args.Effects); } - public override NSDragOperation DraggingUpdated(NSDraggingInfo sender) + public override NSDragOperation DraggingEntered(NSDraggingInfo sender) { - IInputRoot root = _tl?.InputRoot; - if (root == null || _dragDispatcher == null) - return NSDragOperation.None; + return SendRawDragEvent(sender, RawDragEventType.DragEnter); + } - var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); - DraggingInfo info = new DraggingInfo(sender); - var pt = TranslateLocalPoint(info.Location); - - dragOp = _dragDispatcher.DragOver(root, pt, info, dragOp); - - return DraggingInfo.ConvertDragOperation(dragOp); + public override NSDragOperation DraggingUpdated(NSDraggingInfo sender) + { + return SendRawDragEvent(sender, RawDragEventType.DragOver); } public override void DraggingExited(NSDraggingInfo sender) { - IInputRoot root = _tl?.InputRoot; - if (root == null || _dragDispatcher == null) - return; - _dragDispatcher.DragLeave(root); + SendRawDragEvent(sender, RawDragEventType.DragLeave); } public override bool PrepareForDragOperation(NSDraggingInfo sender) { - IInputRoot root = _tl?.InputRoot; - if (root == null || _dragDispatcher == null) - return false; - - var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); - DraggingInfo info = new DraggingInfo(sender); - var pt = TranslateLocalPoint(info.Location); - - dragOp = _dragDispatcher.DragOver(root, pt, info, dragOp); - - return dragOp != DragDropEffects.None; + return SendRawDragEvent(sender, RawDragEventType.DragOver) != NSDragOperation.None; } public override bool PerformDragOperation(NSDraggingInfo sender) { - IInputRoot root = _tl?.InputRoot; - if (root == null || _dragDispatcher == null) - return false; - - var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask); - DraggingInfo info = new DraggingInfo(sender); - var pt = TranslateLocalPoint(info.Location); - - dragOp = _dragDispatcher.Drop(root, pt, info, dragOp); - - return dragOp != DragDropEffects.None; + return SendRawDragEvent(sender, RawDragEventType.Drop) != NSDragOperation.None; } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 925f9b7046..c16c5058f3 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -1,5 +1,7 @@ using Avalonia.Controls.DragDrop; +using Avalonia.Controls.DragDrop.Raw; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Win32.Interop; using IDataObject = Avalonia.Controls.DragDrop.IDataObject; using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; @@ -8,14 +10,16 @@ namespace Avalonia.Win32 { class OleDropTarget : IDropTarget { - private readonly IDragDispatcher _dragDispatcher; private readonly IInputElement _target; + private readonly ITopLevelImpl _tl; + private readonly IDragDevice _dragDevice; private IDataObject _currentDrag = null; - public OleDropTarget(IInputElement target) + public OleDropTarget(ITopLevelImpl tl, IInputElement target) { - _dragDispatcher = AvaloniaLocator.Current.GetService(); + _dragDevice = AvaloniaLocator.Current.GetService(); + _tl = tl; _target = target; } @@ -42,38 +46,50 @@ namespace Avalonia.Win32 result |= DragDropEffects.Link; return result; } - + UnmanagedMethods.HRESULT IDropTarget.DragEnter(IOleDataObject pDataObj, int grfKeyState, long pt, ref DropEffect pdwEffect) { - if (_dragDispatcher == null) + var dispatch = _tl?.Input; + if (dispatch == null) { pdwEffect = DropEffect.None; return UnmanagedMethods.HRESULT.S_OK; } _currentDrag = new OleDataObject(pDataObj); - var dragLocation = GetDragLocation(pt); - - var operation = ConvertDropEffect(pdwEffect); - operation = _dragDispatcher.DragEnter(_target, dragLocation, _currentDrag, operation); - pdwEffect = ConvertDropEffect(operation); + var args = new RawDragEvent( + _dragDevice, + RawDragEventType.DragEnter, + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(pdwEffect) + ); + dispatch(args); + pdwEffect = ConvertDropEffect(args.Effects); return UnmanagedMethods.HRESULT.S_OK; } UnmanagedMethods.HRESULT IDropTarget.DragOver(int grfKeyState, long pt, ref DropEffect pdwEffect) { - if (_dragDispatcher == null) + var dispatch = _tl?.Input; + if (dispatch == null) { pdwEffect = DropEffect.None; return UnmanagedMethods.HRESULT.S_OK; } - var dragLocation = GetDragLocation(pt); - - var operation = ConvertDropEffect(pdwEffect); - operation = _dragDispatcher.DragOver(_target, dragLocation, _currentDrag, operation); - pdwEffect = ConvertDropEffect(operation); + var args = new RawDragEvent( + _dragDevice, + RawDragEventType.DragOver, + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(pdwEffect) + ); + dispatch(args); + pdwEffect = ConvertDropEffect(args.Effects); return UnmanagedMethods.HRESULT.S_OK; } @@ -82,7 +98,14 @@ namespace Avalonia.Win32 { try { - _dragDispatcher?.DragLeave(_target); + _tl?.Input(new RawDragEvent( + _dragDevice, + RawDragEventType.DragLeave, + _target, + default(Point), + null, + DragDropEffects.None + )); return UnmanagedMethods.HRESULT.S_OK; } finally @@ -95,18 +118,25 @@ namespace Avalonia.Win32 { try { - if (_dragDispatcher == null) + var dispatch = _tl?.Input; + if (dispatch == null) { pdwEffect = DropEffect.None; return UnmanagedMethods.HRESULT.S_OK; } _currentDrag= new OleDataObject(pDataObj); - var dragLocation = GetDragLocation(pt); - - var operation = ConvertDropEffect(pdwEffect); - operation = _dragDispatcher.Drop(_target, dragLocation, _currentDrag, operation); - pdwEffect = ConvertDropEffect(operation); + + var args = new RawDragEvent( + _dragDevice, + RawDragEventType.Drop, + _target, + GetDragLocation(pt), + _currentDrag, + ConvertDropEffect(pdwEffect) + ); + dispatch(args); + pdwEffect = ConvertDropEffect(args.Effects); return UnmanagedMethods.HRESULT.S_OK; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ed224a6525..85e5b5b4b9 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -693,7 +693,7 @@ namespace Avalonia.Win32 private void CreateDropTarget() { - OleDropTarget odt = new OleDropTarget(_owner); + OleDropTarget odt = new OleDropTarget(this, _owner); if (OleContext.Current?.RegisterDragDrop(Handle, odt) ?? false) _dropTarget = odt; } From 87d7f65eb8bcf4b631170b349a660df77a4ae26d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 4 Mar 2018 21:34:19 +0100 Subject: [PATCH 052/211] 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 053/211] 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 054/211] 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 055/211] [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 056/211] 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 057/211] 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 058/211] 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 059/211] 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 060/211] 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 061/211] 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 +} From ee8ae91f72f99957e2dfb20ac623452b4cec8ca9 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Mon, 12 Feb 2018 14:17:56 -0500 Subject: [PATCH 062/211] Ported the AutoCompleteBox control from Silverlight --- samples/ControlCatalog/ControlCatalog.csproj | 6 + samples/ControlCatalog/MainView.xaml | 5 +- .../Pages/AutoCompleteBoxPage.xaml | 55 + .../Pages/AutoCompleteBoxPage.xaml.cs | 125 + src/Avalonia.Controls/AutoCompleteBox.cs | 2669 +++++++++++++++++ .../Utils/ISelectionAdapter.cs | 64 + .../SelectingItemsControlSelectionAdapter.cs | 342 +++ .../AutoCompleteBox.xaml | 43 + src/Avalonia.Themes.Default/DefaultTheme.xaml | 3 +- 9 files changed, 3309 insertions(+), 3 deletions(-) create mode 100644 samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml create mode 100644 samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs create mode 100644 src/Avalonia.Controls/AutoCompleteBox.cs create mode 100644 src/Avalonia.Controls/Utils/ISelectionAdapter.cs create mode 100644 src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs create mode 100644 src/Avalonia.Themes.Default/AutoCompleteBox.xaml diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 37f9da0c43..f6d5627555 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -38,6 +38,9 @@ Designer + + Designer + Designer @@ -110,6 +113,9 @@ BorderPage.xaml + + AutoCompleteBoxPage.xaml + ButtonPage.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 060369e404..e8b87e99b0 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -5,9 +5,10 @@ + - + @@ -25,4 +26,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml new file mode 100644 index 0000000000..491f41ecbf --- /dev/null +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -0,0 +1,55 @@ + + + AutoCompleteBox + A control into which the user can input text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs new file mode 100644 index 0000000000..9f181d44f2 --- /dev/null +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -0,0 +1,125 @@ +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Markup; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Data; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ControlCatalog.Pages +{ + public class AutoCompleteBoxPage : UserControl + { + public class StateData + { + public string Name { get; private set; } + public string Abbreviation { get; private set; } + public string Capital { get; private set; } + + public StateData(string name, string abbreviatoin, string capital) + { + Name = name; + Abbreviation = abbreviatoin; + Capital = capital; + } + + public override string ToString() + { + return Name; + } + } + + private StateData[] BuildAllStates() + { + return new StateData[] + { + new StateData("Alabama","AL","Montgomery"), + new StateData("Alaska","AK","Juneau"), + new StateData("Arizona","AZ","Phoenix"), + new StateData("Arkansas","AR","Little Rock"), + new StateData("California","CA","Sacramento"), + new StateData("Colorado","CO","Denver"), + new StateData("Connecticut","CT","Hartford"), + new StateData("Delaware","DE","Dover"), + new StateData("Florida","FL","Tallahassee"), + new StateData("Georgia","GA","Atlanta"), + new StateData("Hawaii","HI","Honolulu"), + new StateData("Idaho","ID","Boise"), + new StateData("Illinois","IL","Springfield"), + new StateData("Indiana","IN","Indianapolis"), + new StateData("Iowa","IA","Des Moines"), + new StateData("Kansas","KS","Topeka"), + new StateData("Kentucky","KY","Frankfort"), + new StateData("Louisiana","LA","Baton Rouge"), + new StateData("Maine","ME","Augusta"), + new StateData("Maryland","MD","Annapolis"), + new StateData("Massachusetts","MA","Boston"), + new StateData("Michigan","MI","Lansing"), + new StateData("Minnesota","MN","St. Paul"), + new StateData("Mississippi","MS","Jackson"), + new StateData("Missouri","MO","Jefferson City"), + new StateData("Montana","MT","Helena"), + new StateData("Nebraska","NE","Lincoln"), + new StateData("Nevada","NV","Carson City"), + new StateData("New Hampshire","NH","Concord"), + new StateData("New Jersey","NJ","Trenton"), + new StateData("New Mexico","NM","Santa Fe"), + new StateData("New York","NY","Albany"), + new StateData("North Carolina","NC","Raleigh"), + new StateData("North Dakota","ND","Bismarck"), + new StateData("Ohio","OH","Columbus"), + new StateData("Oklahoma","OK","Oklahoma City"), + new StateData("Oregon","OR","Salem"), + new StateData("Pennsylvania","PA","Harrisburg"), + new StateData("Rhode Island","RI","Providence"), + new StateData("South Carolina","SC","Columbia"), + new StateData("South Dakota","SD","Pierre"), + new StateData("Tennessee","TN","Nashville"), + new StateData("Texas","TX","Austin"), + new StateData("Utah","UT","Salt Lake City"), + new StateData("Vermont","VT","Montpelier"), + new StateData("Virginia","VA","Richmond"), + new StateData("Washington","WA","Olympia"), + new StateData("West Virginia","WV","Charleston"), + new StateData("Wisconsin","WI","Madison"), + new StateData("Wyoming","WY","Cheyenne"), + }; + } + public StateData[] States { get; private set; } + + public AutoCompleteBoxPage() + { + this.InitializeComponent(); + + States = BuildAllStates(); + + foreach (AutoCompleteBox box in GetAllAutoCompleteBox()) + { + box.Items = States; + } + + var converter = new FuncMultiValueConverter(parts => + { + return String.Format("{0} ({1})", parts.ToArray()); + }); + var binding = new MultiBinding { Converter = converter }; + binding.Bindings.Add(new Binding("Name")); + binding.Bindings.Add(new Binding("Abbreviation")); + + var multibindingBox = this.FindControl("MultiBindingBox"); + multibindingBox.ValueMemberBinding = binding; + } + private IEnumerable GetAllAutoCompleteBox() + { + return + this.GetLogicalDescendants() + .OfType(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs new file mode 100644 index 0000000000..0e8b93146c --- /dev/null +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -0,0 +1,2669 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Utils; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Collections; +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections; +using System.Collections.ObjectModel; +using System.Diagnostics; +using Avalonia.Threading; +using Avalonia.Controls.Templates; +using Avalonia.VisualTree; +using Avalonia.Utilities; +using System.Globalization; +using System.Collections.Specialized; +using System.Reactive.Disposables; + +namespace Avalonia.Controls +{ + public class CancelableEventArgs : EventArgs + { + public bool Cancel { get; set; } + + public CancelableEventArgs() + : base() + { } + } + + /// + /// Provides data for the + /// + /// event. + /// + public class PopulatedEventArgs : EventArgs + { + /// + /// Gets the list of possible matches added to the drop-down portion of + /// the + /// control. + /// + /// The list of possible matches added to the + /// . + public IEnumerable Data { get; private set; } + + /// + /// Initializes a new instance of the + /// . + /// + /// The list of possible matches added to the + /// drop-down portion of the + /// control. + public PopulatedEventArgs(IEnumerable data) + { + Data = data; + } + } + + /// + /// Provides data for the + /// + /// event. + /// + /// Stable + public class PopulatingEventArgs : CancelableEventArgs + { + /// + /// Gets the text that is used to determine which items to display in + /// the + /// control. + /// + /// The text that is used to determine which items to display in + /// the . + public string Parameter { get; private set; } + + /// + /// Initializes a new instance of the + /// . + /// + /// The value of the + /// + /// property, which is used to filter items for the + /// control. + public PopulatingEventArgs(string parameter) + { + Parameter = parameter; + } + } + + /// + /// Represents the filter used by the + /// control to + /// determine whether an item is a possible match for the specified text. + /// + /// true to indicate is a possible match + /// for ; otherwise false. + /// The string used as the basis for filtering. + /// The item that is compared with the + /// parameter. + /// The type used for filtering the + /// . This type can + /// be either a string or an object. + /// Stable + public delegate bool AutoCompleteFilterPredicate(string search, T item); + + /// + /// Specifies how text in the text box portion of the + /// control is used + /// to filter items specified by the + /// + /// property for display in the drop-down. + /// + /// Stable + public enum AutoCompleteFilterMode + { + /// + /// Specifies that no filter is used. All items are returned. + /// + None = 0, + + /// + /// Specifies a culture-sensitive, case-insensitive filter where the + /// returned items start with the specified text. The filter uses the + /// + /// method, specifying + /// as + /// the string comparison criteria. + /// + StartsWith = 1, + + /// + /// Specifies a culture-sensitive, case-sensitive filter where the + /// returned items start with the specified text. The filter uses the + /// + /// method, specifying + /// as the string + /// comparison criteria. + /// + StartsWithCaseSensitive = 2, + + /// + /// Specifies an ordinal, case-insensitive filter where the returned + /// items start with the specified text. The filter uses the + /// + /// method, specifying + /// as the + /// string comparison criteria. + /// + StartsWithOrdinal = 3, + + /// + /// Specifies an ordinal, case-sensitive filter where the returned items + /// start with the specified text. The filter uses the + /// + /// method, specifying as + /// the string comparison criteria. + /// + StartsWithOrdinalCaseSensitive = 4, + + /// + /// Specifies a culture-sensitive, case-insensitive filter where the + /// returned items contain the specified text. + /// + Contains = 5, + + /// + /// Specifies a culture-sensitive, case-sensitive filter where the + /// returned items contain the specified text. + /// + ContainsCaseSensitive = 6, + + /// + /// Specifies an ordinal, case-insensitive filter where the returned + /// items contain the specified text. + /// + ContainsOrdinal = 7, + + /// + /// Specifies an ordinal, case-sensitive filter where the returned items + /// contain the specified text. + /// + ContainsOrdinalCaseSensitive = 8, + + /// + /// Specifies a culture-sensitive, case-insensitive filter where the + /// returned items equal the specified text. The filter uses the + /// + /// method, specifying + /// as + /// the search comparison criteria. + /// + Equals = 9, + + /// + /// Specifies a culture-sensitive, case-sensitive filter where the + /// returned items equal the specified text. The filter uses the + /// + /// method, specifying + /// as the string + /// comparison criteria. + /// + EqualsCaseSensitive = 10, + + /// + /// Specifies an ordinal, case-insensitive filter where the returned + /// items equal the specified text. The filter uses the + /// + /// method, specifying + /// as the + /// string comparison criteria. + /// + EqualsOrdinal = 11, + + /// + /// Specifies an ordinal, case-sensitive filter where the returned items + /// equal the specified text. The filter uses the + /// + /// method, specifying as + /// the string comparison criteria. + /// + EqualsOrdinalCaseSensitive = 12, + + /// + /// Specifies that a custom filter is used. This mode is used when the + /// + /// or + /// + /// properties are set. + /// + Custom = 13, + } + + /// + /// Represents a control that provides a text box for user input and a + /// drop-down that contains possible matches based on the input in the text + /// box. + /// + public class AutoCompleteBox : TemplatedControl + { + /// + /// Specifies the name of the selection adapter TemplatePart. + /// + private const string ElementSelectionAdapter = "PART_SelectionAdapter"; + + /// + /// Specifies the name of the Selector TemplatePart. + /// + private const string ElementSelector = "PART_SelectingItemsControl"; + + /// + /// Specifies the name of the Popup TemplatePart. + /// + private const string ElementPopup = "PART_Popup"; + + /// + /// The name for the text box part. + /// + private const string ElementTextBox = "PART_TextBox"; + + private IEnumerable _itemsEnumerable; + + /// + /// Gets or sets a local cached copy of the items data. + /// + private List _items; + + /// + /// Gets or sets the observable collection that contains references to + /// all of the items in the generated view of data that is provided to + /// the selection-style control adapter. + /// + private AvaloniaList _view; + + /// + /// Gets or sets a value to ignore a number of pending change handlers. + /// The value is decremented after each use. This is used to reset the + /// value of properties without performing any of the actions in their + /// change handlers. + /// + /// The int is important as a value because the TextBox + /// TextChanged event does not immediately fire, and this will allow for + /// nested property changes to be ignored. + private int _ignoreTextPropertyChange; + + /// + /// Gets or sets a value indicating whether to ignore calling a pending + /// change handlers. + /// + private bool _ignorePropertyChange; + + /// + /// Gets or sets a value indicating whether to ignore the selection + /// changed event. + /// + private bool _ignoreTextSelectionChange; + + /// + /// Gets or sets a value indicating whether to skip the text update + /// processing when the selected item is updated. + /// + private bool _skipSelectedItemTextUpdate; + + /// + /// Gets or sets the last observed text box selection start location. + /// + private int _textSelectionStart; + + /// + /// Gets or sets a value indicating whether the user initiated the + /// current populate call. + /// + private bool _userCalledPopulate; + + /// + /// A value indicating whether the popup has been opened at least once. + /// + private bool _popupHasOpened; + + /// + /// Gets or sets the DispatcherTimer used for the MinimumPopulateDelay + /// condition for auto completion. + /// + private DispatcherTimer _delayTimer; + + /// + /// Gets or sets a value indicating whether a read-only dependency + /// property change handler should allow the value to be set. This is + /// used to ensure that read-only properties cannot be changed via + /// SetValue, etc. + /// + private bool _allowWrite; + + /// + /// The TextBox template part. + /// + private TextBox _textBox; + private IDisposable _textBoxSubscriptions; + + /// + /// The SelectionAdapter. + /// + private ISelectionAdapter _adapter; + + /// + /// A control that can provide updated string values from a binding. + /// + private BindingEvaluator _valueBindingEvaluator; + + /// + /// A weak subscription for the collection changed event. + /// + private IDisposable _collectionChangeSubscription; + + private IMemberSelector _valueMemberSelector; + + private bool _itemTemplateIsFromValueMemeberBinding = true; + private bool _settingItemTemplateFromValueMemeberBinding; + + private object _selectedItem; + private bool _isDropDownOpen; + private bool _isFocused = false; + + private string _text = string.Empty; + private string _searchText = string.Empty; + + private AutoCompleteFilterPredicate _itemFilter; + private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); + + public static readonly RoutedEvent SelectionChangedEvent = + RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); + + public static readonly StyledProperty WatermarkProperty = + TextBox.WatermarkProperty.AddOwner(); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty MinimumPrefixLengthProperty = + AvaloniaProperty.Register( + nameof(MinimumPrefixLength), 1, + validate: ValidateMinimumPrefixLength); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty MinimumPopulateDelayProperty = + AvaloniaProperty.Register( + nameof(MinimumPopulateDelay), + TimeSpan.Zero, + validate: ValidateMinimumPopulateDelay); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty MaxDropDownHeightProperty = + AvaloniaProperty.Register( + nameof(MaxDropDownHeight), + double.PositiveInfinity, + validate: ValidateMaxDropDownHeight); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty IsTextCompletionEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextCompletionEnabled)); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty IsDropDownOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsDropDownOpen), + o => o.IsDropDownOpen, + (o, v) => o.IsDropDownOpen = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier the + /// + /// dependency property. + public static readonly DirectProperty SelectedItemProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedItem), + o => o.SelectedItem, + (o, v) => o.SelectedItem = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect( + nameof(Text), + o => o.Text, + (o, v) => o.Text = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty SearchTextProperty = + AvaloniaProperty.RegisterDirect( + nameof(SearchText), + o => o.SearchText, + unsetValue: string.Empty); + + /// + /// Gets the identifier for the + /// + /// dependency property. + /// + public static readonly StyledProperty FilterModeProperty = + AvaloniaProperty.Register( + nameof(FilterMode), + defaultValue: AutoCompleteFilterMode.StartsWith, + validate: ValidateFilterMode); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> ItemFilterProperty = + AvaloniaProperty.RegisterDirect>( + nameof(ItemFilter), + o => o.ItemFilter, + (o, v) => o.ItemFilter = v); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty> TextFilterProperty = + AvaloniaProperty.RegisterDirect>( + nameof(TextFilter), + o => o.TextFilter, + (o, v) => o.TextFilter = v, + unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + + /// + /// Identifies the + /// + /// dependency property. + /// + /// The identifier for the + /// + /// dependency property. + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, + (o, v) => o.Items = v); + + public static readonly DirectProperty ValueMemberSelectorProperty = + AvaloniaProperty.RegisterDirect( + nameof(ValueMemberSelector), + o => o.ValueMemberSelector, + (o, v) => o.ValueMemberSelector = v); + + private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value) + { + Contract.Requires(value >= -1); + + return value; + } + + private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value) + { + Contract.Requires(value.TotalMilliseconds >= 0.0); + + return value; + } + + private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value) + { + Contract.Requires(value >= 0.0); + + return value; + } + + private static bool IsValidFilterMode(AutoCompleteFilterMode mode) + { + switch (mode) + { + case AutoCompleteFilterMode.None: + case AutoCompleteFilterMode.StartsWith: + case AutoCompleteFilterMode.StartsWithCaseSensitive: + case AutoCompleteFilterMode.StartsWithOrdinal: + case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive: + case AutoCompleteFilterMode.Contains: + case AutoCompleteFilterMode.ContainsCaseSensitive: + case AutoCompleteFilterMode.ContainsOrdinal: + case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive: + case AutoCompleteFilterMode.Equals: + case AutoCompleteFilterMode.EqualsCaseSensitive: + case AutoCompleteFilterMode.EqualsOrdinal: + case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive: + case AutoCompleteFilterMode.Custom: + return true; + default: + return false; + } + } + private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value) + { + Contract.Requires(IsValidFilterMode(value)); + + return value; + } + + /// + /// Handle the change of the IsEnabled property. + /// + /// The event data. + private void OnControlIsEnabledChanged(AvaloniaPropertyChangedEventArgs e) + { + bool isEnabled = (bool)e.NewValue; + if (!isEnabled) + { + IsDropDownOpen = false; + } + } + + /// + /// MinimumPopulateDelayProperty property changed handler. Any current + /// dispatcher timer will be stopped. The timer will not be restarted + /// until the next TextUpdate call by the user. + /// + /// Event arguments. + private void OnMinimumPopulateDelayChanged(AvaloniaPropertyChangedEventArgs e) + { + var newValue = (TimeSpan)e.NewValue; + + // Stop any existing timer + if (_delayTimer != null) + { + _delayTimer.Stop(); + + if (newValue == TimeSpan.Zero) + { + _delayTimer = null; + } + } + + if (newValue > TimeSpan.Zero) + { + // Create or clear a dispatcher timer instance + if (_delayTimer == null) + { + _delayTimer = new DispatcherTimer(); + _delayTimer.Tick += PopulateDropDown; + } + + // Set the new tick interval + _delayTimer.Interval = newValue; + } + } + + /// + /// IsDropDownOpenProperty property changed handler. + /// + /// Event arguments. + private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e) + { + // Ignore the change if requested + if (_ignorePropertyChange) + { + _ignorePropertyChange = false; + return; + } + + bool oldValue = (bool)e.OldValue; + bool newValue = (bool)e.NewValue; + + if (newValue) + { + TextUpdated(Text, true); + } + else + { + ClosingDropDown(oldValue); + } + + UpdatePseudoClasses(); + } + + private void OnSelectedItemPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (_ignorePropertyChange) + { + _ignorePropertyChange = false; + return; + } + + // Update the text display + if (_skipSelectedItemTextUpdate) + { + _skipSelectedItemTextUpdate = false; + } + else + { + OnSelectedItemChanged(e.NewValue); + } + + // Fire the SelectionChanged event + List removed = new List(); + if (e.OldValue != null) + { + removed.Add(e.OldValue); + } + + List added = new List(); + if (e.NewValue != null) + { + added.Add(e.NewValue); + } + + OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added)); + } + + /// + /// TextProperty property changed handler. + /// + /// Event arguments. + private void OnTextPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + TextUpdated((string)e.NewValue, false); + } + + private void OnSearchTextPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (_ignorePropertyChange) + { + _ignorePropertyChange = false; + return; + } + + // Ensure the property is only written when expected + if (!_allowWrite) + { + // Reset the old value before it was incorrectly written + _ignorePropertyChange = true; + SetValue(e.Property, e.OldValue); + + throw new InvalidOperationException("Cannot set read-only property SearchText."); + } + } + + /// + /// FilterModeProperty property changed handler. + /// + /// Event arguments. + private void OnFilterModePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue; + + // Sets the filter predicate for the new value + TextFilter = AutoCompleteSearch.GetFilter(mode); + } + + /// + /// ItemFilterProperty property changed handler. + /// + /// Event arguments. + private void OnItemFilterPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + AutoCompleteFilterPredicate value = e.NewValue as AutoCompleteFilterPredicate; + + // If null, revert to the "None" predicate + if (value == null) + { + FilterMode = AutoCompleteFilterMode.None; + } + else + { + FilterMode = AutoCompleteFilterMode.Custom; + TextFilter = null; + } + } + + /// + /// ItemsSourceProperty property changed handler. + /// + /// Event arguments. + private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + OnItemsChanged((IEnumerable)e.NewValue); + } + + private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!_settingItemTemplateFromValueMemeberBinding) + _itemTemplateIsFromValueMemeberBinding = false; + } + private void OnValueMemberBindingChanged(IBinding value) + { + if(_itemTemplateIsFromValueMemeberBinding) + { + var template = + new FuncDataTemplate( + typeof(object), + o => + { + var control = new ContentControl(); + control.Bind(ContentControl.ContentProperty, value); + return control; + }); + + _settingItemTemplateFromValueMemeberBinding = true; + ItemTemplate = template; + _settingItemTemplateFromValueMemeberBinding = false; + } + } + + static AutoCompleteBox() + { + FocusableProperty.OverrideDefaultValue(true); + + MinimumPopulateDelayProperty.Changed.AddClassHandler(x => x.OnMinimumPopulateDelayChanged); + IsDropDownOpenProperty.Changed.AddClassHandler(x => x.OnIsDropDownOpenChanged); + SelectedItemProperty.Changed.AddClassHandler(x => x.OnSelectedItemPropertyChanged); + TextProperty.Changed.AddClassHandler(x => x.OnTextPropertyChanged); + SearchTextProperty.Changed.AddClassHandler(x => x.OnSearchTextPropertyChanged); + FilterModeProperty.Changed.AddClassHandler(x => x.OnFilterModePropertyChanged); + ItemFilterProperty.Changed.AddClassHandler(x => x.OnItemFilterPropertyChanged); + ItemsProperty.Changed.AddClassHandler(x => x.OnItemsPropertyChanged); + IsEnabledProperty.Changed.AddClassHandler(x => x.OnControlIsEnabledChanged); + } + + /// + /// Initializes a new instance of the + /// class. + /// + public AutoCompleteBox() + { + ClearView(); + } + + /// + /// Gets or sets the minimum number of characters required to be entered + /// in the text box before the + /// displays + /// possible matches. + /// matches. + /// + /// + /// The minimum number of characters to be entered in the text box + /// before the + /// displays possible matches. The default is 1. + /// + /// + /// If you set MinimumPrefixLength to -1, the AutoCompleteBox will + /// not provide possible matches. There is no maximum value, but + /// setting MinimumPrefixLength to value that is too large will + /// prevent the AutoCompleteBox from providing possible matches as well. + /// + public int MinimumPrefixLength + { + get { return GetValue(MinimumPrefixLengthProperty); } + set { SetValue(MinimumPrefixLengthProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the first possible match + /// found during the filtering process will be displayed automatically + /// in the text box. + /// + /// + /// True if the first possible match found will be displayed + /// automatically in the text box; otherwise, false. The default is + /// false. + /// + public bool IsTextCompletionEnabled + { + get { return GetValue(IsTextCompletionEnabledProperty); } + set { SetValue(IsTextCompletionEnabledProperty, value); } + } + + /// + /// Gets or sets the used + /// to display each item in the drop-down portion of the control. + /// + /// The used to + /// display each item in the drop-down. The default is null. + /// + /// You use the ItemTemplate property to specify the visualization + /// of the data objects in the drop-down portion of the AutoCompleteBox + /// control. If your AutoCompleteBox is bound to a collection and you + /// do not provide specific display instructions by using a + /// DataTemplate, the resulting UI of each item is a string + /// representation of each object in the underlying collection. + /// + public IDataTemplate ItemTemplate + { + get { return GetValue(ItemTemplateProperty); } + set { SetValue(ItemTemplateProperty, value); } + } + + /// + /// Gets or sets the minimum delay, after text is typed + /// in the text box before the + /// control + /// populates the list of possible matches in the drop-down. + /// + /// The minimum delay, after text is typed in + /// the text box, but before the + /// populates + /// the list of possible matches in the drop-down. The default is 0. + public TimeSpan MinimumPopulateDelay + { + get { return GetValue(MinimumPopulateDelayProperty); } + set { SetValue(MinimumPopulateDelayProperty, value); } + } + + /// + /// Gets or sets the maximum height of the drop-down portion of the + /// control. + /// + /// The maximum height of the drop-down portion of the + /// control. + /// The default is . + /// The specified value is less than 0. + public double MaxDropDownHeight + { + get { return GetValue(MaxDropDownHeightProperty); } + set { SetValue(MaxDropDownHeightProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the drop-down portion of + /// the control is open. + /// + /// + /// True if the drop-down is open; otherwise, false. The default is + /// false. + /// + public bool IsDropDownOpen + { + get { return _isDropDownOpen; } + set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } + } + + /// + /// Gets or sets the that + /// is used to get the values for display in the text portion of + /// the + /// control. + /// + /// The object used + /// when binding to a collection property. + [AssignBinding] + public IBinding ValueMemberBinding + { + get { return _valueBindingEvaluator?.ValueBinding; } + set + { + if (ValueMemberBinding != value) + { + _valueBindingEvaluator = new BindingEvaluator(value); + OnValueMemberBindingChanged(value); + } + } + } + + /// + /// Gets or sets the MemberSelector that is used to get values for + /// display in the text portion of the + /// control. + /// + /// The MemberSelector that is used to get values for display in + /// the text portion of the + /// control. + public IMemberSelector ValueMemberSelector + { + get { return _valueMemberSelector; } + set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); } + } + + /// + /// Gets or sets the selected item in the drop-down. + /// + /// The selected item in the drop-down. + /// + /// If the IsTextCompletionEnabled property is true and text typed by + /// the user matches an item in the ItemsSource collection, which is + /// then displayed in the text box, the SelectedItem property will be + /// a null reference. + /// + public object SelectedItem + { + get { return _selectedItem; } + set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); } + } + + /// + /// Gets or sets the text in the text box portion of the + /// control. + /// + /// The text in the text box portion of the + /// control. + public string Text + { + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } + } + + /// + /// Gets the text that is used to filter items in the + /// + /// item collection. + /// + /// The text that is used to filter items in the + /// + /// item collection. + /// + /// The SearchText value is typically the same as the + /// Text property, but is set after the TextChanged event occurs + /// and before the Populating event. + /// + public string SearchText + { + get { return _searchText; } + private set + { + try + { + _allowWrite = true; + SetAndRaise(SearchTextProperty, ref _searchText, value); + } + finally + { + _allowWrite = false; + } + } + } + + /// + /// Gets or sets how the text in the text box is used to filter items + /// specified by the + /// + /// property for display in the drop-down. + /// + /// One of the + /// + /// values The default is + /// . + /// The specified value is + /// not a valid + /// . + /// + /// Use the FilterMode property to specify how possible matches are + /// filtered. For example, possible matches can be filtered in a + /// predefined or custom way. The search mode is automatically set to + /// Custom if you set the ItemFilter property. + /// + public AutoCompleteFilterMode FilterMode + { + get { return GetValue(FilterModeProperty); } + set { SetValue(FilterModeProperty, value); } + } + + public string Watermark + { + get { return GetValue(WatermarkProperty); } + set { SetValue(WatermarkProperty, value); } + } + + /// + /// Gets or sets the custom method that uses user-entered text to filter + /// the items specified by the + /// + /// property for display in the drop-down. + /// + /// The custom method that uses the user-entered text to filter + /// the items specified by the + /// + /// property. The default is null. + /// + /// The filter mode is automatically set to Custom if you set the + /// ItemFilter property. + /// + public AutoCompleteFilterPredicate ItemFilter + { + get { return _itemFilter; } + set { SetAndRaise(ItemFilterProperty, ref _itemFilter, value); } + } + + /// + /// Gets or sets the custom method that uses the user-entered text to + /// filter items specified by the + /// + /// property in a text-based way for display in the drop-down. + /// + /// The custom method that uses the user-entered text to filter + /// items specified by the + /// + /// property in a text-based way for display in the drop-down. + /// + /// The search mode is automatically set to Custom if you set the + /// TextFilter property. + /// + public AutoCompleteFilterPredicate TextFilter + { + get { return _textFilter; } + set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } + } + + /// + /// Gets or sets a collection that is used to generate the items for the + /// drop-down portion of the + /// control. + /// + /// The collection that is used to generate the items of the + /// drop-down portion of the + /// control. + public IEnumerable Items + { + get { return _itemsEnumerable; } + set { SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); } + } + + /// + /// Gets or sets the drop down popup control. + /// + private Popup DropDownPopup { get; set; } + + /// + /// Gets or sets the Text template part. + /// + private TextBox TextBox + { + get { return _textBox; } + set + { + _textBoxSubscriptions?.Dispose(); + _textBox = value; + + // Attach handlers + if (_textBox != null) + { + _textBoxSubscriptions = + _textBox.GetObservable(TextBox.TextProperty) + .Subscribe(_ => OnTextBoxTextChanged()); + + if (Text != null) + { + UpdateTextValue(Text); + } + } + } + } + + private int TextBoxSelectionStart + { + get + { + if (TextBox != null) + { + return Math.Min(TextBox.SelectionStart, TextBox.SelectionEnd); + } + else + { + return 0; + } + } + } + private int TextBoxSelectionLength + { + get + { + if (TextBox != null) + { + return Math.Abs(TextBox.SelectionEnd - TextBox.SelectionStart); + } + else + { + return 0; + } + } + } + + /// + /// Gets or sets the selection adapter used to populate the drop-down + /// with a list of selectable items. + /// + /// The selection adapter used to populate the drop-down with a + /// list of selectable items. + /// + /// You can use this property when you create an automation peer to + /// use with AutoCompleteBox or deriving from AutoCompleteBox to + /// create a custom control. + /// + protected ISelectionAdapter SelectionAdapter + { + get { return _adapter; } + set + { + if (_adapter != null) + { + _adapter.SelectionChanged -= OnAdapterSelectionChanged; + _adapter.Commit -= OnAdapterSelectionComplete; + _adapter.Cancel -= OnAdapterSelectionCanceled; + _adapter.Cancel -= OnAdapterSelectionComplete; + _adapter.Items = null; + } + + _adapter = value; + + if (_adapter != null) + { + _adapter.SelectionChanged += OnAdapterSelectionChanged; + _adapter.Commit += OnAdapterSelectionComplete; + _adapter.Cancel += OnAdapterSelectionCanceled; + _adapter.Cancel += OnAdapterSelectionComplete; + _adapter.Items = _view; + } + } + } + + /// + /// Returns the + /// part, if + /// possible. + /// + /// + /// A object, + /// if possible. Otherwise, null. + /// + protected virtual ISelectionAdapter GetSelectionAdapterPart(INameScope nameScope) + { + ISelectionAdapter adapter = null; + SelectingItemsControl selector = nameScope.Find(ElementSelector); + if (selector != null) + { + // Check if it is already an IItemsSelector + adapter = selector as ISelectionAdapter; + if (adapter == null) + { + // Built in support for wrapping a Selector control + adapter = new SelectingItemsControlSelectionAdapter(selector); + } + } + if (adapter == null) + { + adapter = nameScope.Find(ElementSelectionAdapter); + } + return adapter; + } + + /// + /// Builds the visual tree for the + /// control + /// when a new template is applied. + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + + if (DropDownPopup != null) + { + DropDownPopup.Closed -= DropDownPopup_Closed; + DropDownPopup = null; + } + + // Set the template parts. Individual part setters remove and add + // any event handlers. + Popup popup = e.NameScope.Find(ElementPopup); + if (popup != null) + { + DropDownPopup = popup; + DropDownPopup.Closed += DropDownPopup_Closed; + } + + SelectionAdapter = GetSelectionAdapterPart(e.NameScope); + TextBox = e.NameScope.Find(ElementTextBox); + + // If the drop down property indicates that the popup is open, + // flip its value to invoke the changed handler. + if (IsDropDownOpen && DropDownPopup != null && !DropDownPopup.IsOpen) + { + OpeningDropDown(false); + } + + base.OnTemplateApplied(e); + } + + /// + /// Provides handling for the + /// event. + /// + /// A + /// that contains the event data. + protected override void OnKeyDown(KeyEventArgs e) + { + Contract.Requires(e != null); + + base.OnKeyDown(e); + + if (e.Handled || !IsEnabled) + { + return; + } + + // The drop down is open, pass along the key event arguments to the + // selection adapter. If it isn't handled by the adapter's logic, + // then we handle some simple navigation scenarios for controlling + // the drop down. + if (IsDropDownOpen) + { + if (SelectionAdapter != null) + { + SelectionAdapter.HandleKeyDown(e); + if (e.Handled) + { + return; + } + } + + if (e.Key == Key.Escape) + { + OnAdapterSelectionCanceled(this, new RoutedEventArgs()); + e.Handled = true; + } + } + else + { + // The drop down is not open, the Down key will toggle it open. + if (e.Key == Key.Down) + { + IsDropDownOpen = true; + e.Handled = true; + } + } + + // Standard drop down navigation + switch (e.Key) + { + case Key.F4: + IsDropDownOpen = !IsDropDownOpen; + e.Handled = true; + break; + + case Key.Enter: + OnAdapterSelectionComplete(this, new RoutedEventArgs()); + e.Handled = true; + break; + + default: + break; + } + } + + /// + /// Provides handling for the + /// event. + /// + /// A + /// that contains the event data. + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + FocusChanged(HasFocus()); + } + + /// + /// Provides handling for the + /// event. + /// + /// A + /// that contains the event data. + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + FocusChanged(HasFocus()); + } + + /// + /// Determines whether the text box or drop-down portion of the + /// control has + /// focus. + /// + /// true to indicate the + /// has focus; + /// otherwise, false. + protected bool HasFocus() + { + IVisual focused = FocusManager.Instance.Current; + + while (focused != null) + { + if (object.ReferenceEquals(focused, this)) + { + return true; + } + + // This helps deal with popups that may not be in the same + // visual tree + IVisual parent = focused.GetVisualParent(); + if (parent == null) + { + // Try the logical parent. + IControl element = focused as IControl; + if (element != null) + { + parent = element.Parent; + } + } + focused = parent; + } + return false; + } + + /// + /// Handles the FocusChanged event. + /// + /// A value indicating whether the control + /// currently has the focus. + private void FocusChanged(bool hasFocus) + { + // The OnGotFocus & OnLostFocus are asynchronously and cannot + // reliably tell you that have the focus. All they do is let you + // know that the focus changed sometime in the past. To determine + // if you currently have the focus you need to do consult the + // FocusManager (see HasFocus()). + + bool wasFocused = _isFocused; + _isFocused = hasFocus; + + if (hasFocus) + { + + if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0) + { + TextBox.Focus(); + TextBox.SelectionStart = 0; + TextBox.SelectionEnd = TextBox.Text?.Length ?? 0; + } + } + else + { + IsDropDownOpen = false; + _userCalledPopulate = false; + ClearTextBoxSelection(); + } + + _isFocused = hasFocus; + } + + /// + /// Occurs when the text in the text box portion of the + /// changes. + /// + public event EventHandler TextChanged; + + /// + /// Occurs when the + /// is + /// populating the drop-down with possible matches based on the + /// + /// property. + /// + /// + /// If the event is canceled, by setting the PopulatingEventArgs.Cancel + /// property to true, the AutoCompleteBox will not automatically + /// populate the selection adapter contained in the drop-down. + /// In this case, if you want possible matches to appear, you must + /// provide the logic for populating the selection adapter. + /// + public event EventHandler Populating; + + /// + /// Occurs when the + /// has + /// populated the drop-down with possible matches based on the + /// + /// property. + /// + public event EventHandler Populated; + + /// + /// Occurs when the value of the + /// + /// property is changing from false to true. + /// + public event EventHandler DropDownOpening; + + /// + /// Occurs when the value of the + /// + /// property has changed from false to true and the drop-down is open. + /// + public event EventHandler DropDownOpened; + + /// + /// Occurs when the + /// + /// property is changing from true to false. + /// + public event EventHandler DropDownClosing; + + /// + /// Occurs when the + /// + /// property was changed from true to false and the drop-down is open. + /// + public event EventHandler DropDownClosed; + + /// + /// Occurs when the selected item in the drop-down portion of the + /// has + /// changed. + /// + public event EventHandler SelectionChanged + { + add { AddHandler(SelectionChangedEvent, value); } + remove { RemoveHandler(SelectionChangedEvent, value); } + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// that + /// contains the event data. + protected virtual void OnPopulating(PopulatingEventArgs e) + { + Populating?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnPopulated(PopulatedEventArgs e) + { + Populated?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnSelectionChanged(SelectionChangedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnDropDownOpening(CancelableEventArgs e) + { + DropDownOpening?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnDropDownOpened(EventArgs e) + { + DropDownOpened?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// that contains the event data. + protected virtual void OnDropDownClosing(CancelableEventArgs e) + { + DropDownClosing?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// + /// which contains the event data. + protected virtual void OnDropDownClosed(EventArgs e) + { + DropDownClosed?.Invoke(this, e); + } + + /// + /// Raises the + /// + /// event. + /// + /// A + /// that contains the event data. + protected virtual void OnTextChanged(RoutedEventArgs e) + { + TextChanged?.Invoke(this, e); + } + + /// + /// Begin closing the drop-down. + /// + /// The original value. + private void ClosingDropDown(bool oldValue) + { + var args = new CancelableEventArgs(); + OnDropDownClosing(args); + + if (args.Cancel) + { + _ignorePropertyChange = true; + SetValue(IsDropDownOpenProperty, oldValue); + } + else + { + CloseDropDown(); + } + + UpdatePseudoClasses(); + } + + /// + /// Begin opening the drop down by firing cancelable events, opening the + /// drop-down or reverting, depending on the event argument values. + /// + /// The original value, if needed for a revert. + private void OpeningDropDown(bool oldValue) + { + var args = new CancelableEventArgs(); + + // Opening + OnDropDownOpening(args); + + if (args.Cancel) + { + _ignorePropertyChange = true; + SetValue(IsDropDownOpenProperty, oldValue); + } + else + { + OpenDropDown(); + } + + UpdatePseudoClasses(); + } + + /// + /// Connects to the DropDownPopup Closed event. + /// + /// The source object. + /// The event data. + private void DropDownPopup_Closed(object sender, EventArgs e) + { + // Force the drop down dependency property to be false. + if (IsDropDownOpen) + { + IsDropDownOpen = false; + } + + // Fire the DropDownClosed event + if (_popupHasOpened) + { + OnDropDownClosed(EventArgs.Empty); + } + } + + /// + /// Handles the timer tick when using a populate delay. + /// + /// The source object. + /// The event arguments. + private void PopulateDropDown(object sender, EventArgs e) + { + if (_delayTimer != null) + { + _delayTimer.Stop(); + } + + // Update the prefix/search text. + SearchText = Text; + + // The Populated event enables advanced, custom filtering. The + // client needs to directly update the ItemsSource collection or + // call the Populate method on the control to continue the + // display process if Cancel is set to true. + PopulatingEventArgs populating = new PopulatingEventArgs(SearchText); + OnPopulating(populating); + if (!populating.Cancel) + { + PopulateComplete(); + } + } + + /// + /// Private method that directly opens the popup, checks the expander + /// button, and then fires the Opened event. + /// + private void OpenDropDown() + { + if (DropDownPopup != null) + { + DropDownPopup.IsOpen = true; + } + _popupHasOpened = true; + OnDropDownOpened(EventArgs.Empty); + } + + /// + /// Private method that directly closes the popup, flips the Checked + /// value, and then fires the Closed event. + /// + private void CloseDropDown() + { + if (_popupHasOpened) + { + if (SelectionAdapter != null) + { + SelectionAdapter.SelectedItem = null; + } + if (DropDownPopup != null) + { + DropDownPopup.IsOpen = false; + } + OnDropDownClosed(EventArgs.Empty); + } + } + + /// + /// Formats an Item for text comparisons based on Converter + /// and ConverterCulture properties. + /// + /// The object to format. + /// A value indicating whether to clear + /// the data context after the lookup is performed. + /// Formatted Value. + private string FormatValue(object value, bool clearDataContext) + { + string result = FormatValue(value); + if(clearDataContext && _valueBindingEvaluator != null) + { + _valueBindingEvaluator.ClearDataContext(); + } + + return result; + } + + /// + /// Converts the specified object to a string by using the + /// and + /// values + /// of the binding object specified by the + /// + /// property. + /// + /// The object to format as a string. + /// The string representation of the specified object. + /// + /// Override this method to provide a custom string conversion. + /// + protected virtual string FormatValue(object value) + { + if (_valueBindingEvaluator != null) + { + return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty; + } + + if (_valueMemberSelector != null) + { + value = _valueMemberSelector.Select(value); + } + + return value == null ? String.Empty : value.ToString(); + } + + /// + /// Handle the TextChanged event that is directly attached to the + /// TextBox part. This ensures that only user initiated actions will + /// result in an AutoCompleteBox suggestion and operation. + /// + private void OnTextBoxTextChanged() + { + //Uses Dispatcher.Post to allow the TextBox selection to update before processing + Dispatcher.UIThread.Post(() => + { + // Call the central updated text method as a user-initiated action + TextUpdated(_textBox.Text, true); + }); + } + + /// + /// Updates both the text box value and underlying text dependency + /// property value if and when they change. Automatically fires the + /// text changed events when there is a change. + /// + /// The new string value. + private void UpdateTextValue(string value) + { + UpdateTextValue(value, null); + } + + /// + /// Updates both the text box value and underlying text dependency + /// property value if and when they change. Automatically fires the + /// text changed events when there is a change. + /// + /// The new string value. + /// A nullable bool value indicating whether + /// the action was user initiated. In a user initiated mode, the + /// underlying text dependency property is updated. In a non-user + /// interaction, the text box value is updated. When user initiated is + /// null, all values are updated. + private void UpdateTextValue(string value, bool? userInitiated) + { + bool callTextChanged = false; + // Update the Text dependency property + if ((userInitiated == null || userInitiated == true) && Text != value) + { + _ignoreTextPropertyChange++; + Text = value; + callTextChanged = true; + } + + // Update the TextBox's Text dependency property + if ((userInitiated == null || userInitiated == false) && TextBox != null && TextBox.Text != value) + { + _ignoreTextPropertyChange++; + TextBox.Text = value ?? string.Empty; + + // Text dependency property value was set, fire event + if (!callTextChanged && (Text == value || Text == null)) + { + callTextChanged = true; + } + } + + if (callTextChanged) + { + OnTextChanged(new RoutedEventArgs()); + } + } + + /// + /// Handle the update of the text for the control from any source, + /// including the TextBox part and the Text dependency property. + /// + /// The new text. + /// A value indicating whether the update + /// is a user-initiated action. This should be a True value when the + /// TextUpdated method is called from a TextBox event handler. + private void TextUpdated(string newText, bool userInitiated) + { + // Only process this event if it is coming from someone outside + // setting the Text dependency property directly. + if (_ignoreTextPropertyChange > 0) + { + _ignoreTextPropertyChange--; + return; + } + + if (newText == null) + { + newText = string.Empty; + } + + // The TextBox.TextChanged event was not firing immediately and + // was causing an immediate update, even with wrapping. If there is + // a selection currently, no update should happen. + if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length) + { + return; + } + + // Evaluate the conditions needed for completion. + // 1. Minimum prefix length + // 2. If a delay timer is in use, use it + bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0; + _userCalledPopulate = populateReady ? userInitiated : false; + + // Update the interface and values only as necessary + UpdateTextValue(newText, userInitiated); + + if (populateReady) + { + _ignoreTextSelectionChange = true; + + if (_delayTimer != null) + { + _delayTimer.Start(); + } + else + { + PopulateDropDown(this, EventArgs.Empty); + } + } + else + { + SearchText = string.Empty; + if (SelectedItem != null) + { + _skipSelectedItemTextUpdate = true; + } + SelectedItem = null; + if (IsDropDownOpen) + { + IsDropDownOpen = false; + } + } + } + + /// + /// A simple helper method to clear the view and ensure that a view + /// object is always present and not null. + /// + private void ClearView() + { + if (_view == null) + { + _view = new AvaloniaList(); + } + else + { + _view.Clear(); + } + } + + /// + /// Walks through the items enumeration. Performance is not going to be + /// perfect with the current implementation. + /// + private void RefreshView() + { + if (_items == null) + { + ClearView(); + return; + } + + // Cache the current text value + string text = Text ?? string.Empty; + + // Determine if any filtering mode is on + bool stringFiltering = TextFilter != null; + bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null; + + int view_index = 0; + int view_count = _view.Count; + List items = _items; + foreach (object item in items) + { + bool inResults = !(stringFiltering || objectFiltering); + if (!inResults) + { + inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item); + } + + if (view_count > view_index && inResults && _view[view_index] == item) + { + // Item is still in the view + view_index++; + } + else if (inResults) + { + // Insert the item + if (view_count > view_index && _view[view_index] != item) + { + // Replace item + // Unfortunately replacing via index throws a fatal + // exception: View[view_index] = item; + // Cost: O(n) vs O(1) + _view.RemoveAt(view_index); + _view.Insert(view_index, item); + view_index++; + } + else + { + // Add the item + if (view_index == view_count) + { + // Constant time is preferred (Add). + _view.Add(item); + } + else + { + _view.Insert(view_index, item); + } + view_index++; + view_count++; + } + } + else if (view_count > view_index && _view[view_index] == item) + { + // Remove the item + _view.RemoveAt(view_index); + view_count--; + } + } + + // Clear the evaluator to discard a reference to the last item + if (_valueBindingEvaluator != null) + { + _valueBindingEvaluator.ClearDataContext(); + } + } + + /// + /// Handle any change to the ItemsSource dependency property, update + /// the underlying ObservableCollection view, and set the selection + /// adapter's ItemsSource to the view if appropriate. + /// + /// The new enumerable reference. + private void OnItemsChanged(IEnumerable newValue) + { + // Remove handler for oldValue.CollectionChanged (if present) + _collectionChangeSubscription?.Dispose(); + _collectionChangeSubscription = null; + + // Add handler for newValue.CollectionChanged (if possible) + if (newValue is INotifyCollectionChanged newValueINotifyCollectionChanged) + { + _collectionChangeSubscription = newValueINotifyCollectionChanged.WeakSubscribe(ItemsCollectionChanged); + } + + // Store a local cached copy of the data + _items = newValue == null ? null : new List(newValue.Cast().ToList()); + + // Clear and set the view on the selection adapter + ClearView(); + if (SelectionAdapter != null && SelectionAdapter.Items != _view) + { + SelectionAdapter.Items = _view; + } + if (IsDropDownOpen) + { + RefreshView(); + } + } + + /// + /// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property. + /// + /// The object that raised the event. + /// The event data. + private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // Update the cache + if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null) + { + for (int index = 0; index < e.OldItems.Count; index++) + { + _items.RemoveAt(e.OldStartingIndex); + } + } + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null && _items.Count >= e.NewStartingIndex) + { + for (int index = 0; index < e.NewItems.Count; index++) + { + _items.Insert(e.NewStartingIndex + index, e.NewItems[index]); + } + } + if (e.Action == NotifyCollectionChangedAction.Replace && e.NewItems != null && e.OldItems != null) + { + for (int index = 0; index < e.NewItems.Count; index++) + { + _items[e.NewStartingIndex] = e.NewItems[index]; + } + } + + // Update the view + if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace) + { + for (int index = 0; index < e.OldItems.Count; index++) + { + _view.Remove(e.OldItems[index]); + } + } + + if (e.Action == NotifyCollectionChangedAction.Reset) + { + // Significant changes to the underlying data. + ClearView(); + if (Items != null) + { + _items = new List(Items.Cast().ToList()); + } + } + + // Refresh the observable collection used in the selection adapter. + RefreshView(); + } + + /// + /// Notifies the + /// that the + /// + /// property has been set and the data can be filtered to provide + /// possible matches in the drop-down. + /// + /// + /// Call this method when you are providing custom population of + /// the drop-down portion of the AutoCompleteBox, to signal the control + /// that you are done with the population process. + /// Typically, you use PopulateComplete when the population process + /// is a long-running process and you want to cancel built-in filtering + /// of the ItemsSource items. In this case, you can handle the + /// Populated event and set PopulatingEventArgs.Cancel to true. + /// When the long-running process has completed you call + /// PopulateComplete to indicate the drop-down is populated. + /// + public void PopulateComplete() + { + // Apply the search filter + RefreshView(); + + // Fire the Populated event containing the read-only view data. + PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view)); + OnPopulated(populated); + + if (SelectionAdapter != null && SelectionAdapter.Items != _view) + { + SelectionAdapter.Items = _view; + } + + bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0); + if (isDropDownOpen != IsDropDownOpen) + { + _ignorePropertyChange = true; + IsDropDownOpen = isDropDownOpen; + } + if (IsDropDownOpen) + { + OpeningDropDown(false); + } + else + { + ClosingDropDown(true); + } + + UpdateTextCompletion(_userCalledPopulate); + } + + /// + /// Performs text completion, if enabled, and a lookup on the underlying + /// item values for an exact match. Will update the SelectedItem value. + /// + /// A value indicating whether the operation + /// was user initiated. Text completion will not be performed when not + /// directly initiated by the user. + private void UpdateTextCompletion(bool userInitiated) + { + // By default this method will clear the selected value + object newSelectedItem = null; + string text = Text; + + // Text search is StartsWith explicit and only when enabled, in + // line with WPF's ComboBox lookup. When in use it will associate + // a Value with the Text if it is found in ItemsSource. This is + // only valid when there is data and the user initiated the action. + if (_view.Count > 0) + { + if (IsTextCompletionEnabled && TextBox != null && userInitiated) + { + int currentLength = TextBox.Text.Length; + int selectionStart = TextBoxSelectionStart; + if (selectionStart == text.Length && selectionStart > _textSelectionStart) + { + // When the FilterMode dependency property is set to + // either StartsWith or StartsWithCaseSensitive, the + // first item in the view is used. This will improve + // performance on the lookup. It assumes that the + // FilterMode the user has selected is an acceptable + // case sensitive matching function for their scenario. + object top = FilterMode == AutoCompleteFilterMode.StartsWith || FilterMode == AutoCompleteFilterMode.StartsWithCaseSensitive + ? _view[0] + : TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); + + // If the search was successful, update SelectedItem + if (top != null) + { + newSelectedItem = top; + string topString = FormatValue(top, true); + + // Only replace partially when the two words being the same + int minLength = Math.Min(topString.Length, Text.Length); + if (AutoCompleteSearch.Equals(Text.Substring(0, minLength), topString.Substring(0, minLength))) + { + // Update the text + UpdateTextValue(topString); + + // Select the text past the user's caret + TextBox.SelectionStart = currentLength; + TextBox.SelectionEnd = topString.Length; + } + } + } + } + else + { + // Perform an exact string lookup for the text. This is a + // design change from the original Toolkit release when the + // IsTextCompletionEnabled property behaved just like the + // WPF ComboBox's IsTextSearchEnabled property. + // + // This change provides the behavior that most people expect + // to find: a lookup for the value is always performed. + newSelectedItem = TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)); + } + } + + // Update the selected item property + + if (SelectedItem != newSelectedItem) + { + _skipSelectedItemTextUpdate = true; + } + SelectedItem = newSelectedItem; + + // Restore updates for TextSelection + if (_ignoreTextSelectionChange) + { + _ignoreTextSelectionChange = false; + if (TextBox != null) + { + _textSelectionStart = TextBoxSelectionStart; + } + } + } + + /// + /// Attempts to look through the view and locate the specific exact + /// text match. + /// + /// The search text. + /// The view reference. + /// The predicate to use for the partial or + /// exact match. + /// Returns the object or null. + private object TryGetMatch(string searchText, AvaloniaList view, AutoCompleteFilterPredicate predicate) + { + if (view != null && view.Count > 0) + { + foreach (object o in view) + { + if (predicate(searchText, FormatValue(o))) + { + return o; + } + } + } + + return null; + } + + private void UpdatePseudoClasses() + { + PseudoClasses.Set(":dropdownopen", IsDropDownOpen); + } + + private void ClearTextBoxSelection() + { + if (TextBox != null) + { + int length = TextBox.Text?.Length ?? 0; + TextBox.SelectionStart = length; + TextBox.SelectionEnd = length; + } + } + + /// + /// Called when the selected item is changed, updates the text value + /// that is displayed in the text box part. + /// + /// The new item. + private void OnSelectedItemChanged(object newItem) + { + string text; + + if (newItem == null) + { + text = SearchText; + } + else + { + text = FormatValue(newItem, true); + } + + // Update the Text property and the TextBox values + UpdateTextValue(text); + + // Move the caret to the end of the text box + ClearTextBoxSelection(); + } + + /// + /// Handles the SelectionChanged event of the selection adapter. + /// + /// The source object. + /// The selection changed event data. + private void OnAdapterSelectionChanged(object sender, SelectionChangedEventArgs e) + { + SelectedItem = _adapter.SelectedItem; + } + + //TODO Check UpdateTextCompletion + /// + /// Handles the Commit event on the selection adapter. + /// + /// The source object. + /// The event data. + private void OnAdapterSelectionComplete(object sender, RoutedEventArgs e) + { + IsDropDownOpen = false; + + // Completion will update the selected value + //UpdateTextCompletion(false); + + // Text should not be selected + ClearTextBoxSelection(); + + TextBox.Focus(); + } + + /// + /// Handles the Cancel event on the selection adapter. + /// + /// The source object. + /// The event data. + private void OnAdapterSelectionCanceled(object sender, RoutedEventArgs e) + { + UpdateTextValue(SearchText); + + // Completion will update the selected value + UpdateTextCompletion(false); + } + + /// + /// A predefined set of filter functions for the known, built-in + /// AutoCompleteFilterMode enumeration values. + /// + private static class AutoCompleteSearch + { + /// + /// Index function that retrieves the filter for the provided + /// AutoCompleteFilterMode. + /// + /// The built-in search mode. + /// Returns the string-based comparison function. + public static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode FilterMode) + { + switch (FilterMode) + { + case AutoCompleteFilterMode.Contains: + return Contains; + + case AutoCompleteFilterMode.ContainsCaseSensitive: + return ContainsCaseSensitive; + + case AutoCompleteFilterMode.ContainsOrdinal: + return ContainsOrdinal; + + case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive: + return ContainsOrdinalCaseSensitive; + + case AutoCompleteFilterMode.Equals: + return Equals; + + case AutoCompleteFilterMode.EqualsCaseSensitive: + return EqualsCaseSensitive; + + case AutoCompleteFilterMode.EqualsOrdinal: + return EqualsOrdinal; + + case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive: + return EqualsOrdinalCaseSensitive; + + case AutoCompleteFilterMode.StartsWith: + return StartsWith; + + case AutoCompleteFilterMode.StartsWithCaseSensitive: + return StartsWithCaseSensitive; + + case AutoCompleteFilterMode.StartsWithOrdinal: + return StartsWithOrdinal; + + case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive: + return StartsWithOrdinalCaseSensitive; + + case AutoCompleteFilterMode.None: + case AutoCompleteFilterMode.Custom: + default: + return null; + } + } + + /// + /// An implementation of the Contains member of string that takes in a + /// string comparison. The traditional .NET string Contains member uses + /// StringComparison.Ordinal. + /// + /// The string. + /// The string value to search for. + /// The string comparison type. + /// Returns true when the substring is found. + private static bool Contains(string s, string value, StringComparison comparison) + { + return s.IndexOf(value, comparison) >= 0; + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWith(string text, string value) + { + return value.StartsWith(text, StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWithCaseSensitive(string text, string value) + { + return value.StartsWith(text, StringComparison.CurrentCulture); + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWithOrdinal(string text, string value) + { + return value.StartsWith(text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Check if the string value begins with the text. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool StartsWithOrdinalCaseSensitive(string text, string value) + { + return value.StartsWith(text, StringComparison.Ordinal); + } + + /// + /// Check if the prefix is contained in the string value. The current + /// culture's case insensitive string comparison operator is used. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool Contains(string text, string value) + { + return Contains(value, text, StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// Check if the prefix is contained in the string value. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool ContainsCaseSensitive(string text, string value) + { + return Contains(value, text, StringComparison.CurrentCulture); + } + + /// + /// Check if the prefix is contained in the string value. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool ContainsOrdinal(string text, string value) + { + return Contains(value, text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Check if the prefix is contained in the string value. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool ContainsOrdinalCaseSensitive(string text, string value) + { + return Contains(value, text, StringComparison.Ordinal); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool Equals(string text, string value) + { + return value.Equals(text, StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool EqualsCaseSensitive(string text, string value) + { + return value.Equals(text, StringComparison.CurrentCulture); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool EqualsOrdinal(string text, string value) + { + return value.Equals(text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Check if the string values are equal. + /// + /// The AutoCompleteBox prefix text. + /// The item's string value. + /// Returns true if the condition is met. + public static bool EqualsOrdinalCaseSensitive(string text, string value) + { + return value.Equals(text, StringComparison.Ordinal); + } + } + + /// + /// A framework element that permits a binding to be evaluated in a new data + /// context leaf node. + /// + /// The type of dynamic binding to return. + public class BindingEvaluator : Control + { + /// + /// Gets or sets the string value binding used by the control. + /// + private IBinding _binding; + + #region public T Value + + /// + /// Identifies the Value dependency property. + /// + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register, T>(nameof(Value)); + + /// + /// Gets or sets the data item value. + /// + public T Value + { + get { return GetValue(ValueProperty); } + set { SetValue(ValueProperty, value); } + } + + #endregion public string Value + + /// + /// Gets or sets the value binding. + /// + public IBinding ValueBinding + { + get { return _binding; } + set + { + _binding = value; + AvaloniaObjectExtensions.Bind(this, ValueProperty, value); + } + } + + /// + /// Initializes a new instance of the BindingEvaluator class. + /// + public BindingEvaluator() + { } + + /// + /// Initializes a new instance of the BindingEvaluator class, + /// setting the initial binding to the provided parameter. + /// + /// The initial string value binding. + public BindingEvaluator(IBinding binding) + : this() + { + ValueBinding = binding; + } + + /// + /// Clears the data context so that the control does not keep a + /// reference to the last-looked up item. + /// + public void ClearDataContext() + { + DataContext = null; + } + + /// + /// Updates the data context of the framework element and returns the + /// updated binding value. + /// + /// The object to use as the data context. + /// If set to true, this parameter will + /// clear the data context immediately after retrieving the value. + /// Returns the evaluated T value of the bound dependency + /// property. + public T GetDynamicValue(object o, bool clearDataContext) + { + DataContext = o; + T value = Value; + if (clearDataContext) + { + DataContext = null; + } + return value; + } + + /// + /// Updates the data context of the framework element and returns the + /// updated binding value. + /// + /// The object to use as the data context. + /// Returns the evaluated T value of the bound dependency + /// property. + public T GetDynamicValue(object o) + { + DataContext = o; + return Value; + } + } + } +} diff --git a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs new file mode 100644 index 0000000000..3c1006a12e --- /dev/null +++ b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs @@ -0,0 +1,64 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections; +using Avalonia.Interactivity; +using Avalonia.Input; + +namespace Avalonia.Controls.Utils +{ + /// + /// Defines an item collection, selection members, and key handling for the + /// selection adapter contained in the drop-down portion of an + /// control. + /// + public interface ISelectionAdapter + { + /// + /// Gets or sets the selected item. + /// + /// The currently selected item. + object SelectedItem { get; set; } + + /// + /// Occurs when the + /// + /// property value changes. + /// + event EventHandler SelectionChanged; + + /// + /// Gets or sets a collection that is used to generate content for the + /// selection adapter. + /// + /// The collection that is used to generate content for the + /// selection adapter. + IEnumerable Items { get; set; } + + /// + /// Occurs when a selected item is not cancelled and is committed as the + /// selected item. + /// + event EventHandler Commit; + + /// + /// Occurs when a selection has been canceled. + /// + event EventHandler Cancel; + + /// + /// Provides handling for the + /// event that occurs + /// when a key is pressed while the drop-down portion of the + /// has focus. + /// + /// A + /// that contains data about the + /// event. + void HandleKeyDown(KeyEventArgs e); + } + +} diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs new file mode 100644 index 0000000000..43c8a5aa6c --- /dev/null +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -0,0 +1,342 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Avalonia.Input; +using Avalonia.LogicalTree; +using System.Collections; +using System.Diagnostics; + +namespace Avalonia.Controls.Utils +{ + /// + /// Represents the selection adapter contained in the drop-down portion of + /// an control. + /// + public class SelectingItemsControlSelectionAdapter : ISelectionAdapter + { + /// + /// The SelectingItemsControl instance. + /// + private SelectingItemsControl _selector; + + /// + /// Gets or sets a value indicating whether the selection change event + /// should not be fired. + /// + private bool IgnoringSelectionChanged { get; set; } + + /// + /// Gets or sets the underlying + /// + /// control. + /// + /// The underlying + /// + /// control. + public SelectingItemsControl SelectorControl + { + get { return _selector; } + + set + { + if (_selector != null) + { + _selector.SelectionChanged -= OnSelectionChanged; + _selector.PointerReleased -= OnSelectorPointerReleased; + } + + _selector = value; + + if (_selector != null) + { + _selector.SelectionChanged += OnSelectionChanged; + _selector.PointerReleased += OnSelectorPointerReleased; + } + } + } + + /// + /// Occurs when the + /// + /// property value changes. + /// + public event EventHandler SelectionChanged; + + /// + /// Occurs when an item is selected and is committed to the underlying + /// + /// control. + /// + public event EventHandler Commit; + + /// + /// Occurs when a selection is canceled before it is committed. + /// + public event EventHandler Cancel; + + /// + /// Initializes a new instance of the + /// + /// class. + /// + public SelectingItemsControlSelectionAdapter() + { + + } + + /// + /// Initializes a new instance of the + /// + /// class with the specified + /// + /// control. + /// + /// The + /// control + /// to wrap as a + /// . + public SelectingItemsControlSelectionAdapter(SelectingItemsControl selector) + { + SelectorControl = selector; + } + + /// + /// Gets or sets the selected item of the selection adapter. + /// + /// The selected item of the underlying selection adapter. + public object SelectedItem + { + get + { + return SelectorControl?.SelectedItem; + } + + set + { + IgnoringSelectionChanged = true; + if (SelectorControl != null) + { + SelectorControl.SelectedItem = value; + } + + // Attempt to reset the scroll viewer's position + if (value == null) + { + ResetScrollViewer(); + } + + IgnoringSelectionChanged = false; + } + } + + /// + /// Gets or sets a collection that is used to generate the content of + /// the selection adapter. + /// + /// The collection used to generate content for the selection + /// adapter. + public IEnumerable Items + { + get + { + return SelectorControl?.Items; + } + set + { + if (SelectorControl != null) + { + SelectorControl.Items = value; + } + } + } + + /// + /// If the control contains a ScrollViewer, this will reset the viewer + /// to be scrolled to the top. + /// + private void ResetScrollViewer() + { + if (SelectorControl != null) + { + ScrollViewer sv = SelectorControl.GetLogicalDescendants().OfType().FirstOrDefault(); + if (sv != null) + { + sv.Offset = new Vector(0, 0); + } + } + } + + /// + /// Handles the mouse left button up event on the selector control. + /// + /// The source object. + /// The event data. + private void OnSelectorPointerReleased(object sender, PointerReleasedEventArgs e) + { + if (e.MouseButton == MouseButton.Left) + { + OnCommit(); + } + } + + /// + /// Handles the SelectionChanged event on the SelectingItemsControl control. + /// + /// The source object. + /// The selection changed event data. + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (IgnoringSelectionChanged) + { + return; + } + + SelectionChanged?.Invoke(sender, e); + } + + /// + /// Increments the + /// + /// property of the underlying + /// + /// control. + /// + protected void SelectedIndexIncrement() + { + if (SelectorControl != null) + { + SelectorControl.SelectedIndex = SelectorControl.SelectedIndex + 1 >= SelectorControl.ItemCount ? -1 : SelectorControl.SelectedIndex + 1; + } + } + + /// + /// Decrements the + /// + /// property of the underlying + /// + /// control. + /// + protected void SelectedIndexDecrement() + { + if (SelectorControl != null) + { + int index = SelectorControl.SelectedIndex; + if (index >= 0) + { + SelectorControl.SelectedIndex--; + } + else if (index == -1) + { + SelectorControl.SelectedIndex = SelectorControl.ItemCount - 1; + } + } + } + + /// + /// Provides handling for the + /// event that occurs + /// when a key is pressed while the drop-down portion of the + /// has focus. + /// + /// A + /// that contains data about the + /// event. + public void HandleKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + OnCommit(); + e.Handled = true; + break; + + case Key.Up: + SelectedIndexDecrement(); + e.Handled = true; + break; + + case Key.Down: + if ((e.Modifiers & InputModifiers.Alt) == InputModifiers.None) + { + SelectedIndexIncrement(); + e.Handled = true; + } + break; + + case Key.Escape: + OnCancel(); + e.Handled = true; + break; + + default: + break; + } + } + + /// + /// Raises the + /// + /// event. + /// + protected virtual void OnCommit() + { + OnCommit(this, new RoutedEventArgs()); + } + + /// + /// Fires the Commit event. + /// + /// The source object. + /// The event data. + private void OnCommit(object sender, RoutedEventArgs e) + { + Commit?.Invoke(sender, e); + + AfterAdapterAction(); + } + + /// + /// Raises the + /// + /// event. + /// + protected virtual void OnCancel() + { + OnCancel(this, new RoutedEventArgs()); + } + + /// + /// Fires the Cancel event. + /// + /// The source object. + /// The event data. + private void OnCancel(object sender, RoutedEventArgs e) + { + Cancel?.Invoke(sender, e); + + AfterAdapterAction(); + } + + /// + /// Change the selection after the actions are complete. + /// + private void AfterAdapterAction() + { + IgnoringSelectionChanged = true; + if (SelectorControl != null) + { + SelectorControl.SelectedItem = null; + SelectorControl.SelectedIndex = -1; + } + IgnoringSelectionChanged = false; + } + } +} diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml new file mode 100644 index 0000000000..82dbf6064b --- /dev/null +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 5b6f4b78fd..7d3090de66 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -23,7 +23,7 @@ - + @@ -42,4 +42,5 @@ + From e2ff0a02584303b7c7f2308a23f020b848ab7bb8 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 28 Feb 2018 20:09:11 -0500 Subject: [PATCH 063/211] Fixed an off by one bug --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3ec3d6ed5b..27410c5948 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -788,7 +788,7 @@ namespace Avalonia.Controls int pos = 0; int i; - for (i = 0; i < lines.Count; ++i) + for (i = 0; i < lines.Count - 1; ++i) { var line = lines[i]; pos += line.Length; From c5608bd386ddfed5ba1cc27b43326f53a846dc98 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 28 Feb 2018 20:10:35 -0500 Subject: [PATCH 064/211] Moved the setting of FocusedElement This allows the LostFocus event to check the newly focused element --- src/Avalonia.Input/KeyboardDevice.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index d815f8082b..2a1cdec1c0 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -46,13 +46,13 @@ namespace Avalonia.Input if (element != FocusedElement) { var interactive = FocusedElement as IInteractive; + FocusedElement = element; interactive?.RaiseEvent(new RoutedEventArgs { RoutedEvent = InputElement.LostFocusEvent, }); - FocusedElement = element; interactive = element as IInteractive; interactive?.RaiseEvent(new GotFocusEventArgs From 5927e7acfe17dc0e47e2a8fab5dbdbba50ffb7bb Mon Sep 17 00:00:00 2001 From: sdoroff Date: Mon, 5 Mar 2018 12:09:56 -0500 Subject: [PATCH 065/211] Added Unit Tests --- .../AutoCompleteBoxTests.cs | 1042 +++++++++++++++++ 1 file changed, 1042 insertions(+) create mode 100644 tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs new file mode 100644 index 0000000000..f9da2ab6f3 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -0,0 +1,1042 @@ +// 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.ComponentModel; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Moq; +using Xunit; +using System.Collections.ObjectModel; + +namespace Avalonia.Controls.UnitTests +{ + public class AutoCompleteBoxTests + { + [Fact] + public void Search_Filters() + { + Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("am", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.Contains)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.Contains)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsCaseSensitive)("hello", "name")); + + Assert.Null(GetFilter(AutoCompleteFilterMode.Custom)); + Assert.Null(GetFilter(AutoCompleteFilterMode.None)); + + Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "na")); + Assert.True(GetFilter(AutoCompleteFilterMode.Equals)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.Equals)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "na")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("na", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWith)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWith)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithCaseSensitive)("hello", "name")); + } + + [Fact] + public void Ordinal_Search_Filters() + { + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("am", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinal)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("AME", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.ContainsOrdinalCaseSensitive)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "na")); + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinal)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "na")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("na", "NA")); + Assert.False(GetFilter(AutoCompleteFilterMode.EqualsOrdinalCaseSensitive)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("na", "name")); + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinal)("hello", "name")); + + Assert.True(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("na", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("NAM", "name")); + Assert.False(GetFilter(AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)("hello", "name")); + } + + [Fact] + public void Fires_DropDown_Events() + { + RunTest((control, textbox) => + { + bool openEvent = false; + bool closeEvent = false; + control.DropDownOpened += (s, e) => openEvent = true; + control.DropDownClosed += (s, e) => closeEvent = true; + control.Items = CreateSimpleStringArray(); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.SearchText == "a"); + Assert.True(control.IsDropDownOpen); + Assert.True(openEvent); + + textbox.Text = String.Empty; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.SearchText == String.Empty); + Assert.False(control.IsDropDownOpen); + Assert.True(closeEvent); + }); + } + + [Fact] + public void Text_Completion_Via_Text_Property() + { + RunTest((control, textbox) => + { + control.IsTextCompletionEnabled = true; + + Assert.Equal(String.Empty, control.Text); + control.Text = "close"; + Assert.NotNull(control.SelectedItem); + }); + } + + [Fact] + public void Text_Completion_Selects_Text() + { + RunTest((control, textbox) => + { + control.IsTextCompletionEnabled = true; + + textbox.Text = "ac"; + textbox.SelectionEnd = textbox.SelectionStart = 2; + Dispatcher.UIThread.RunJobs(); + + Assert.True(control.IsDropDownOpen); + Assert.True(Math.Abs(textbox.SelectionEnd - textbox.SelectionStart) > 2); + }); + } + + [Fact] + public void TextChanged_Event_Fires() + { + RunTest((control, textbox) => + { + bool textChanged = false; + control.TextChanged += (s, e) => textChanged = true; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(textChanged); + + textChanged = false; + control.Text = "conversati"; + Dispatcher.UIThread.RunJobs(); + Assert.True(textChanged); + + textChanged = false; + control.Text = null; + Dispatcher.UIThread.RunJobs(); + Assert.True(textChanged); + }); + } + + [Fact] + public void MinimumPrefixLength_Works() + { + RunTest((control, textbox) => + { + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.IsDropDownOpen); + + + textbox.Text = String.Empty; + Dispatcher.UIThread.RunJobs(); + Assert.False(control.IsDropDownOpen); + + control.MinimumPrefixLength = 3; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.False(control.IsDropDownOpen); + + textbox.Text = "acc"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.IsDropDownOpen); + }); + } + + [Fact] + public void Can_Cancel_DropDown_Opening() + { + RunTest((control, textbox) => + { + control.DropDownOpening += (s, e) => e.Cancel = true; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.False(control.IsDropDownOpen); + }); + } + + [Fact] + public void Can_Cancel_DropDown_Closing() + { + RunTest((control, textbox) => + { + control.DropDownClosing += (s, e) => e.Cancel = true; + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.True(control.IsDropDownOpen); + + control.IsDropDownOpen = false; + Assert.True(control.IsDropDownOpen); + }); + } + + [Fact] + public void Can_Cancel_Population() + { + RunTest((control, textbox) => + { + bool populating = false; + bool populated = false; + control.FilterMode = AutoCompleteFilterMode.None; + control.Populating += (s, e) => + { + e.Cancel = true; + populating = true; + }; + control.Populated += (s, e) => populated = true; + + textbox.Text = "accounti"; + Dispatcher.UIThread.RunJobs(); + + Assert.True(populating); + Assert.False(populated); + }); + } + + [Fact] + public void Custom_Population_Supported() + { + RunTest((control, textbox) => + { + string custom = "Custom!"; + string search = "accounti"; + bool populated = false; + bool populatedOk = false; + control.FilterMode = AutoCompleteFilterMode.None; + control.Populating += (s, e) => + { + control.Items = new string[] { custom }; + Assert.Equal(search, e.Parameter); + }; + control.Populated += (s, e) => + { + populated = true; + ReadOnlyCollection collection = e.Data as ReadOnlyCollection; + populatedOk = collection != null && collection.Count == 1; + }; + + textbox.Text = search; + Dispatcher.UIThread.RunJobs(); + + Assert.True(populated); + Assert.True(populatedOk); + }); + } + + [Fact] + public void Text_Completion() + { + RunTest((control, textbox) => + { + control.IsTextCompletionEnabled = true; + textbox.Text = "accounti"; + textbox.SelectionStart = textbox.SelectionEnd = textbox.Text.Length; + Dispatcher.UIThread.RunJobs(); + Assert.Equal("accounti", control.SearchText); + Assert.Equal("accounting", textbox.Text); + }); + } + + [Fact] + public void String_Search() + { + RunTest((control, textbox) => + { + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "acc"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = ""; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "accept"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + }); + } + + [Fact] + public void Item_Search() + { + RunTest((control, textbox) => + { + control.FilterMode = AutoCompleteFilterMode.Custom; + control.ItemFilter = (search, item) => + { + string s = item as string; + return s == null ? false : true; + }; + + // Just set to null briefly to exercise that code path + AutoCompleteFilterPredicate filter = control.ItemFilter; + Assert.NotNull(filter); + control.ItemFilter = null; + Assert.Null(control.ItemFilter); + control.ItemFilter = filter; + Assert.NotNull(control.ItemFilter); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "acc"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "a"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = ""; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "accept"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + + textbox.Text = "cook"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal(textbox.Text, control.Text); + }); + } + + /// + /// Retrieves a defined predicate filter through a new AutoCompleteBox + /// control instance. + /// + /// The FilterMode of interest. + /// Returns the predicate instance. + private static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode mode) + { + return new AutoCompleteBox { FilterMode = mode } + .TextFilter; + } + + /// + /// Creates a large list of strings for AutoCompleteBox testing. + /// + /// Returns a new List of string values. + private IList CreateSimpleStringArray() + { + return new List + { + "a", + "abide", + "able", + "about", + "above", + "absence", + "absurd", + "accept", + "acceptance", + "accepted", + "accepting", + "access", + "accessed", + "accessible", + "accident", + "accidentally", + "accordance", + "account", + "accounting", + "accounts", + "accusation", + "accustomed", + "ache", + "across", + "act", + "active", + "actual", + "actually", + "ada", + "added", + "adding", + "addition", + "additional", + "additions", + "address", + "addressed", + "addresses", + "addressing", + "adjourn", + "adoption", + "advance", + "advantage", + "adventures", + "advice", + "advisable", + "advise", + "affair", + "affectionately", + "afford", + "afore", + "afraid", + "after", + "afterwards", + "again", + "against", + "age", + "aged", + "agent", + "ago", + "agony", + "agree", + "agreed", + "agreement", + "ah", + "ahem", + "air", + "airs", + "ak", + "alarm", + "alarmed", + "alas", + "alice", + "alive", + "all", + "allow", + "almost", + "alone", + "along", + "aloud", + "already", + "also", + "alteration", + "altered", + "alternate", + "alternately", + "altogether", + "always", + "am", + "ambition", + "among", + "an", + "ancient", + "and", + "anger", + "angrily", + "angry", + "animal", + "animals", + "ann", + "annoy", + "annoyed", + "another", + "answer", + "answered", + "answers", + "antipathies", + "anxious", + "anxiously", + "any", + "anyone", + "anything", + "anywhere", + "appealed", + "appear", + "appearance", + "appeared", + "appearing", + "appears", + "applause", + "apple", + "apples", + "applicable", + "apply", + "approach", + "arch", + "archbishop", + "arches", + "archive", + "are", + "argue", + "argued", + "argument", + "arguments", + "arise", + "arithmetic", + "arm", + "arms", + "around", + "arranged", + "array", + "arrived", + "arrow", + "arrum", + "as", + "ascii", + "ashamed", + "ask", + "askance", + "asked", + "asking", + "asleep", + "assembled", + "assistance", + "associated", + "at", + "ate", + "atheling", + "atom", + "attached", + "attempt", + "attempted", + "attempts", + "attended", + "attending", + "attends", + "audibly", + "australia", + "author", + "authority", + "available", + "avoid", + "away", + "awfully", + "axes", + "axis", + "b", + "baby", + "back", + "backs", + "bad", + "bag", + "baked", + "balanced", + "bank", + "banks", + "banquet", + "bark", + "barking", + "barley", + "barrowful", + "based", + "bat", + "bathing", + "bats", + "bawled", + "be", + "beak", + "bear", + "beast", + "beasts", + "beat", + "beating", + "beau", + "beauti", + "beautiful", + "beautifully", + "beautify", + "became", + "because", + "become", + "becoming", + "bed", + "beds", + "bee", + "been", + "before", + "beg", + "began", + "begged", + "begin", + "beginning", + "begins", + "begun", + "behead", + "beheaded", + "beheading", + "behind", + "being", + "believe", + "believed", + "bells", + "belong", + "belongs", + "beloved", + "below", + "belt", + "bend", + "bent", + "besides", + "best", + "better", + "between", + "bill", + "binary", + "bird", + "birds", + "birthday", + "bit", + "bite", + "bitter", + "blacking", + "blades", + "blame", + "blasts", + "bleeds", + "blew", + "blow", + "blown", + "blows", + "body", + "boldly", + "bone", + "bones", + "book", + "books", + "boon", + "boots", + "bore", + "both", + "bother", + "bottle", + "bottom", + "bough", + "bound", + "bowed", + "bowing", + "box", + "boxed", + "boy", + "brain", + "branch", + "branches", + "brandy", + "brass", + "brave", + "breach", + "bread", + "break", + "breath", + "breathe", + "breeze", + "bright", + "brightened", + "bring", + "bringing", + "bristling", + "broke", + "broken", + "brother", + "brought", + "brown", + "brush", + "brushing", + "burn", + "burning", + "burnt", + "burst", + "bursting", + "busily", + "business", + "business@pglaf", + "busy", + "but", + "butter", + "buttercup", + "buttered", + "butterfly", + "buttons", + "by", + "bye", + "c", + "cackled", + "cake", + "cakes", + "calculate", + "calculated", + "call", + "called", + "calling", + "calmly", + "came", + "camomile", + "can", + "canary", + "candle", + "cannot", + "canterbury", + "canvas", + "capering", + "capital", + "card", + "cardboard", + "cards", + "care", + "carefully", + "cares", + "carried", + "carrier", + "carroll", + "carry", + "carrying", + "cart", + "cartwheels", + "case", + "cat", + "catch", + "catching", + "caterpillar", + "cats", + "cattle", + "caucus", + "caught", + "cauldron", + "cause", + "caused", + "cautiously", + "cease", + "ceiling", + "centre", + "certain", + "certainly", + "chain", + "chains", + "chair", + "chance", + "chanced", + "change", + "changed", + "changes", + "changing", + "chapter", + "character", + "charge", + "charges", + "charitable", + "charities", + "chatte", + "cheap", + "cheated", + "check", + "checked", + "checks", + "cheeks", + "cheered", + "cheerfully", + "cherry", + "cheshire", + "chief", + "child", + "childhood", + "children", + "chimney", + "chimneys", + "chin", + "choice", + "choke", + "choked", + "choking", + "choose", + "choosing", + "chop", + "chorus", + "chose", + "christmas", + "chrysalis", + "chuckled", + "circle", + "circumstances", + "city", + "civil", + "claim", + "clamour", + "clapping", + "clasped", + "classics", + "claws", + "clean", + "clear", + "cleared", + "clearer", + "clearly", + "clever", + "climb", + "clinging", + "clock", + "close", + "closed", + "closely", + "closer", + "clubs", + "coast", + "coaxing", + "codes", + "coils", + "cold", + "collar", + "collected", + "collection", + "come", + "comes", + "comfits", + "comfort", + "comfortable", + "comfortably", + "coming", + "commercial", + "committed", + "common", + "commotion", + "company", + "compilation", + "complained", + "complaining", + "completely", + "compliance", + "comply", + "complying", + "compressed", + "computer", + "computers", + "concept", + "concerning", + "concert", + "concluded", + "conclusion", + "condemn", + "conduct", + "confirmation", + "confirmed", + "confused", + "confusing", + "confusion", + "conger", + "conqueror", + "conquest", + "consented", + "consequential", + "consider", + "considerable", + "considered", + "considering", + "constant", + "consultation", + "contact", + "contain", + "containing", + "contempt", + "contemptuous", + "contemptuously", + "content", + "continued", + "contract", + "contradicted", + "contributions", + "conversation", + "conversations", + "convert", + "cook", + "cool", + "copied", + "copies", + "copy", + "copying", + "copyright", + "corner", + "corners", + "corporation", + "corrupt", + "cost", + "costs", + "could", + "couldn", + "counting", + "countries", + "country", + "couple", + "couples", + "courage", + "course", + "court", + "courtiers", + "coward", + "crab", + "crash", + "crashed", + "crawled", + "crawling", + "crazy", + "created", + "creating", + "creation", + "creature", + "creatures", + "credit", + "creep", + "crept", + "cried", + "cries", + "crimson", + "critical", + "crocodile", + "croquet", + "croqueted", + "croqueting", + "cross", + "crossed", + "crossly", + "crouched", + "crowd", + "crowded", + "crown", + "crumbs", + "crust", + "cry", + "crying", + "cucumber", + "cunning", + "cup", + "cupboards", + "cur", + "curiosity", + "curious", + "curiouser", + "curled", + "curls", + "curly", + "currants", + "current", + "curtain", + "curtsey", + "curtseying", + "curving", + "cushion", + "custard", + "custody", + "cut", + "cutting", + }; + } + private void RunTest(Action test) + { + using (UnitTestApplication.Start(Services)) + { + AutoCompleteBox control = CreateControl(); + control.Items = CreateSimpleStringArray(); + TextBox textBox = GetTextBox(control); + Dispatcher.UIThread.RunJobs(); + test.Invoke(control, textBox); + } + } + + private static TestServices Services => TestServices.StyledWindow; + + /*private static TestServices Services => TestServices.MockThreadingInterface.With( + standardCursorFactory: Mock.Of(), + windowingPlatform: new MockWindowingPlatform());*/ + + private AutoCompleteBox CreateControl() + { + var datePicker = + new AutoCompleteBox + { + Template = CreateTemplate() + }; + + datePicker.ApplyTemplate(); + return datePicker; + } + private TextBox GetTextBox(AutoCompleteBox control) + { + return control.GetTemplateChildren() + .OfType() + .First(); + } + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate(control => + { + var textBox = + new TextBox + { + Name = "PART_TextBox" + }; + var listbox = + new ListBox + { + Name = "PART_SelectingItemsControl" + }; + var popup = + new Popup + { + Name = "PART_Popup" + }; + + var panel = new Panel(); + panel.Children.Add(textBox); + panel.Children.Add(popup); + panel.Children.Add(listbox); + + return panel; + }); + } + } +} From be68b32440a4d96eb6268abee1c390dbca6863a8 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 07:25:46 +0100 Subject: [PATCH 066/211] reworked the dataobject for drag+drop --- src/Avalonia.Controls/DragDrop/IDataObject.cs | 2 + src/OSX/Avalonia.MonoMac/DraggingInfo.cs | 13 +- .../Avalonia.Win32/ClipboardFormats.cs | 20 +- src/Windows/Avalonia.Win32/DataObject.cs | 312 ++++++++++++++++++ .../Interop/UnmanagedMethods.cs | 53 ++- src/Windows/Avalonia.Win32/OleDataObject.cs | 5 + src/Windows/Avalonia.Win32/OleDragSource.cs | 41 +++ src/Windows/Avalonia.Win32/OleDropTarget.cs | 11 +- 8 files changed, 437 insertions(+), 20 deletions(-) create mode 100644 src/Windows/Avalonia.Win32/DataObject.cs create mode 100644 src/Windows/Avalonia.Win32/OleDragSource.cs diff --git a/src/Avalonia.Controls/DragDrop/IDataObject.cs b/src/Avalonia.Controls/DragDrop/IDataObject.cs index 5ffecc13ed..c8fd720c48 100644 --- a/src/Avalonia.Controls/DragDrop/IDataObject.cs +++ b/src/Avalonia.Controls/DragDrop/IDataObject.cs @@ -11,5 +11,7 @@ namespace Avalonia.Controls.DragDrop string GetText(); IEnumerable GetFileNames(); + + object Get(string dataFormat); } } \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs index ef4d9d99f6..2fb28a50f5 100644 --- a/src/OSX/Avalonia.MonoMac/DraggingInfo.cs +++ b/src/OSX/Avalonia.MonoMac/DraggingInfo.cs @@ -15,8 +15,7 @@ namespace Avalonia.MonoMac { _info = info; } - - + internal static NSDragOperation ConvertDragOperation(DragDropEffects d) { NSDragOperation result = NSDragOperation.None; @@ -77,5 +76,15 @@ namespace Avalonia.MonoMac { return GetDataFormats().Any(f => f == dataFormat); } + + public object Get(string dataFormat) + { + if (dataFormat == DataFormats.Text) + return GetText(); + if (dataFormat == DataFormats.FileNames) + return GetFileNames(); + + return _info.DraggingPasteboard.GetDataForType(dataFormat).ToArray(); + } } } \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index 254facf81a..a59c6a1664 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -10,34 +10,34 @@ namespace Avalonia.Win32 { static class ClipboardFormats { + private const int MAX_FORMAT_NAME_LENGTH = 260; + class ClipboardFormat { public short Format { get; private set; } public string Name { get; private set; } + public short[] Synthesized { get; private set; } - public ClipboardFormat(string name, short format) + public ClipboardFormat(string name, short format, params short[] synthesized) { Format = format; Name = name; + Synthesized = synthesized; } } private static readonly List FormatList = new List() { - new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT), + new ClipboardFormat(DataFormats.Text, (short)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (short)UnmanagedMethods.ClipboardFormat.CF_TEXT), new ClipboardFormat(DataFormats.FileNames, (short)UnmanagedMethods.ClipboardFormat.CF_HDROP), }; private static string QueryFormatName(short format) { - int len = UnmanagedMethods.GetClipboardFormatName(format, null, 0); - if (len > 0) - { - StringBuilder sb = new StringBuilder(len); - if (UnmanagedMethods.GetClipboardFormatName(format, sb, len) <= len) - return sb.ToString(); - } + StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH); + if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0) + return sb.ToString(); return null; } @@ -45,7 +45,7 @@ namespace Avalonia.Win32 { lock (FormatList) { - var pd = FormatList.FirstOrDefault(f => f.Format == format); + var pd = FormatList.FirstOrDefault(f => f.Format == format || Array.IndexOf(f.Synthesized, format) >= 0); if (pd == null) { string name = QueryFormatName(format); diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs new file mode 100644 index 0000000000..6248afd7bb --- /dev/null +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -0,0 +1,312 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Text; +using Avalonia.Controls.DragDrop; +using Avalonia.Win32.Interop; +using IDataObject = Avalonia.Controls.DragDrop.IDataObject; +using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; + +namespace Avalonia.Win32 +{ + class DataObject : IDataObject, IOleDataObject + { + class FormatEnumerator : IEnumFORMATETC + { + private FORMATETC[] _formats; + private int _current; + + private FormatEnumerator(FORMATETC[] formats, int current) + { + _formats = formats; + _current = current; + } + + public FormatEnumerator(IDataObject dataobj) + { + _formats = dataobj.GetDataFormats().Select(ConvertToFormatEtc).ToArray(); + _current = 0; + } + + private FORMATETC ConvertToFormatEtc(string aFormatName) + { + FORMATETC result = default(FORMATETC); + result.cfFormat = ClipboardFormats.GetFormat(aFormatName); + result.dwAspect = DVASPECT.DVASPECT_CONTENT; + result.ptd = IntPtr.Zero; + result.lindex = -1; + result.tymed = TYMED.TYMED_HGLOBAL; + return result; + } + + public void Clone(out IEnumFORMATETC newEnum) + { + newEnum = new FormatEnumerator(_formats, _current); + } + + public int Next(int celt, FORMATETC[] rgelt, int[] pceltFetched) + { + if (rgelt == null) + return unchecked((int)UnmanagedMethods.HRESULT.E_INVALIDARG); + + int i = 0; + while (i < celt && _current < _formats.Length) + { + rgelt[i] = _formats[_current]; + _current++; + i++; + } + if (pceltFetched != null) + pceltFetched[0] = i; + + if (i != celt) + return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + + public int Reset() + { + _current = 0; + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + + public int Skip(int celt) + { + _current += Math.Min(celt, int.MaxValue - _current); + if (_current >= _formats.Length) + return unchecked((int)UnmanagedMethods.HRESULT.S_FALSE); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + } + + private const int DV_E_TYMED = unchecked((int)0x80040069); + private const int DV_E_DVASPECT = unchecked((int)0x8004006B); + private const int DV_E_FORMATETC = unchecked((int)0x80040064); + private const int OLE_E_ADVISENOTSUPPORTED = unchecked((int)0x80040003); + private const int STG_E_MEDIUMFULL = unchecked((int)0x80030070); + private const int GMEM_ZEROINIT = 0x0040; + private const int GMEM_MOVEABLE = 0x0002; + + + IDataObject _wrapped; + + public DataObject(IDataObject wrapped) + { + _wrapped = wrapped; + } + + #region IDataObject + bool IDataObject.Contains(string dataFormat) + { + return _wrapped.Contains(dataFormat); + } + + IEnumerable IDataObject.GetDataFormats() + { + return _wrapped.GetDataFormats(); + } + + IEnumerable IDataObject.GetFileNames() + { + return _wrapped.GetFileNames(); + } + + string IDataObject.GetText() + { + return _wrapped.GetText(); + } + + object IDataObject.Get(string dataFormat) + { + return _wrapped.Get(dataFormat); + } + #endregion + + #region IOleDataObject + + int IOleDataObject.DAdvise(ref FORMATETC pFormatetc, ADVF advf, IAdviseSink adviseSink, out int connection) + { + if (_wrapped is IOleDataObject ole) + return ole.DAdvise(ref pFormatetc, advf, adviseSink, out connection); + connection = 0; + return OLE_E_ADVISENOTSUPPORTED; + } + + void IOleDataObject.DUnadvise(int connection) + { + if (_wrapped is IOleDataObject ole) + ole.DUnadvise(connection); + Marshal.ThrowExceptionForHR(OLE_E_ADVISENOTSUPPORTED); + } + + int IOleDataObject.EnumDAdvise(out IEnumSTATDATA enumAdvise) + { + if (_wrapped is IOleDataObject ole) + return ole.EnumDAdvise(out enumAdvise); + + enumAdvise = null; + return OLE_E_ADVISENOTSUPPORTED; + } + + IEnumFORMATETC IOleDataObject.EnumFormatEtc(DATADIR direction) + { + if (_wrapped is IOleDataObject ole) + return ole.EnumFormatEtc(direction); + if (direction == DATADIR.DATADIR_GET) + return new FormatEnumerator(_wrapped); + throw new NotSupportedException(); + } + + int IOleDataObject.GetCanonicalFormatEtc(ref FORMATETC formatIn, out FORMATETC formatOut) + { + if (_wrapped is IOleDataObject ole) + return ole.GetCanonicalFormatEtc(ref formatIn, out formatOut); + + formatOut = new FORMATETC(); + formatOut.ptd = IntPtr.Zero; + return unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL); + } + + void IOleDataObject.GetData(ref FORMATETC format, out STGMEDIUM medium) + { + if (_wrapped is IOleDataObject ole) + { + ole.GetData(ref format, out medium); + return; + } + if(!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + Marshal.ThrowExceptionForHR(DV_E_TYMED); + + if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + + string fmt = ClipboardFormats.GetFormat(format.cfFormat); + if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) + Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + + medium = default(STGMEDIUM); + medium.tymed = TYMED.TYMED_HGLOBAL; + int result = WriteDataToHGlobal(fmt, ref medium.unionmember); + Marshal.ThrowExceptionForHR(result); + } + + void IOleDataObject.GetDataHere(ref FORMATETC format, ref STGMEDIUM medium) + { + if (_wrapped is IOleDataObject ole) + { + ole.GetDataHere(ref format, ref medium); + return; + } + + if (medium.tymed != TYMED.TYMED_HGLOBAL || !format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + Marshal.ThrowExceptionForHR(DV_E_TYMED); + + if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + Marshal.ThrowExceptionForHR(DV_E_DVASPECT); + + string fmt = ClipboardFormats.GetFormat(format.cfFormat); + if (string.IsNullOrEmpty(fmt) || !_wrapped.Contains(fmt)) + Marshal.ThrowExceptionForHR(DV_E_FORMATETC); + + if (medium.unionmember == IntPtr.Zero) + Marshal.ThrowExceptionForHR(STG_E_MEDIUMFULL); + + int result = WriteDataToHGlobal(fmt, ref medium.unionmember); + Marshal.ThrowExceptionForHR(result); + } + + int IOleDataObject.QueryGetData(ref FORMATETC format) + { + if (_wrapped is IOleDataObject ole) + return ole.QueryGetData(ref format); + if (format.dwAspect != DVASPECT.DVASPECT_CONTENT) + return DV_E_DVASPECT; + if (!format.tymed.HasFlag(TYMED.TYMED_HGLOBAL)) + return DV_E_TYMED; + + string dataFormat = ClipboardFormats.GetFormat(format.cfFormat); + if (!string.IsNullOrEmpty(dataFormat) && _wrapped.Contains(dataFormat)) + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + return DV_E_FORMATETC; + } + + void IOleDataObject.SetData(ref FORMATETC formatIn, ref STGMEDIUM medium, bool release) + { + if (_wrapped is IOleDataObject ole) + { + ole.SetData(ref formatIn, ref medium, release); + return; + } + Marshal.ThrowExceptionForHR(unchecked((int)UnmanagedMethods.HRESULT.E_NOTIMPL)); + } + + private int WriteDataToHGlobal(string dataFormat, ref IntPtr hGlobal) + { + object data = _wrapped.Get(dataFormat); + if (dataFormat == DataFormats.Text || data is string) + return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data)); + if (dataFormat == DataFormats.FileNames && data is IEnumerable files) + return WriteFileListToHGlobal(ref hGlobal, files); + return DV_E_TYMED; + } + + private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) + { + if (!files?.Any() ?? false) + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + + char[] filesStr = (string.Join("\0", files) + "\0\0").ToCharArray(); + _DROPFILES df = new _DROPFILES(); + df.pFiles = Marshal.SizeOf<_DROPFILES>(); + df.fWide = true; + + int required = (filesStr.Length * sizeof(char)) + Marshal.SizeOf<_DROPFILES>(); + if (hGlobal == IntPtr.Zero) + hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required); + + long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + if (required > available) + return STG_E_MEDIUMFULL; + + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + Marshal.StructureToPtr(df, ptr, false); + + Marshal.Copy(filesStr, 0, ptr + Marshal.SizeOf<_DROPFILES>(), filesStr.Length); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + private int WriteStringToHGlobal(ref IntPtr hGlobal, string data) + { + int required = (data.Length + 1) * sizeof(char); + if (hGlobal == IntPtr.Zero) + hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE|GMEM_ZEROINIT, required); + + long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + if (required > available) + return STG_E_MEDIUMFULL; + + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + char[] chars = (data + '\0').ToCharArray(); + Marshal.Copy(chars, 0, ptr, chars.Length); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } + } + + #endregion + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index f43cdb98cc..aa86ab0f8d 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -973,7 +973,11 @@ namespace Avalonia.Win32.Interop [DllImport("shell32.dll", BestFitMapping = false, CharSet = CharSet.Auto)] public static extern int DragQueryFile(IntPtr hDrop, int iFile, StringBuilder lpszFile, int cch); - + [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)] + public static extern void DoDragDrop(IDataObject dataObject, IDropSource dropSource, int allowedEffects, int[] finalEffect); + + + public enum MONITOR { MONITOR_DEFAULTTONULL = 0x00000000, @@ -1013,11 +1017,28 @@ namespace Avalonia.Win32.Interop MDT_DEFAULT = MDT_EFFECTIVE_DPI } - public enum ClipboardFormat + public enum ClipboardFormat { + /// + /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text. + /// CF_TEXT = 1, + /// + /// A handle to a bitmap + /// + CF_BITMAP = 2, + /// + /// A memory object containing a BITMAPINFO structure followed by the bitmap bits. + /// + CF_DIB = 3, + /// + /// Unicode text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. + /// CF_UNICODETEXT = 13, - CF_HDROP = 15 + /// + /// A handle to type HDROP that identifies a list of files. + /// + CF_HDROP = 15, } public struct MSG @@ -1160,7 +1181,9 @@ namespace Avalonia.Win32.Interop S_FALSE = 0x0001, S_OK = 0x0000, E_INVALIDARG = 0x80070057, - E_OUTOFMEMORY = 0x8007000E + E_OUTOFMEMORY = 0x8007000E, + E_NOTIMPL = 0x80004001, + E_UNEXPECTED = 0x8000FFFF, } public enum Icons @@ -1351,4 +1374,26 @@ namespace Avalonia.Win32.Interop [PreserveSig] UnmanagedMethods.HRESULT Drop([MarshalAs(UnmanagedType.Interface)] [In] IDataObject pDataObj, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState, [MarshalAs(UnmanagedType.U8)] [In] long pt, [In] [Out] ref DropEffect pdwEffect); } + + [ComImport] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("00000121-0000-0000-C000-000000000046")] + internal interface IDropSource + { + [PreserveSig] + int QueryContinueDrag(int fEscapePressed, [MarshalAs(UnmanagedType.U4)] [In] int grfKeyState); + [PreserveSig] + int GiveFeedback([MarshalAs(UnmanagedType.U4)] [In] int dwEffect); + } + + + [StructLayoutAttribute(LayoutKind.Sequential)] + internal struct _DROPFILES + { + public Int32 pFiles; + public Int32 X; + public Int32 Y; + public bool fNC; + public bool fWide; + } } diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index bf6f3d98c3..fe6faa162c 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -39,6 +39,11 @@ namespace Avalonia.Win32 return GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT) as IEnumerable; } + public object Get(string dataFormat) + { + return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); + } + private object GetDataFromOleHGLOBAL(string format, DVASPECT aspect) { FORMATETC formatEtc = new FORMATETC(); diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs new file mode 100644 index 0000000000..1bff984087 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OleDragSource.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class OleDragSource : IDropSource + { + private const int DRAGDROP_S_USEDEFAULTCURSORS = 0x00040102; + private const int DRAGDROP_S_DROP = 0x00040100; + private const int DRAGDROP_S_CANCEL = 0x00040101; + + private const int KEYSTATE_LEFTMB = 1; + private const int KEYSTATE_MIDDLEMB = 16; + private const int KEYSTATE_RIGHTMB = 2; + private static readonly int[] MOUSE_BUTTONS = new int[] { KEYSTATE_LEFTMB, KEYSTATE_MIDDLEMB, KEYSTATE_RIGHTMB }; + + public int QueryContinueDrag(int fEscapePressed, int grfKeyState) + { + if (fEscapePressed != 0) + return DRAGDROP_S_CANCEL; + + int pressedMouseButtons = MOUSE_BUTTONS.Where(mb => (grfKeyState & mb) == mb).Count(); + + if (pressedMouseButtons >= 2) + return DRAGDROP_S_CANCEL; + if (pressedMouseButtons == 0) + return DRAGDROP_S_DROP; + + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + + public int GiveFeedback(int dwEffect) + { + if (dwEffect != 0) + return DRAGDROP_S_USEDEFAULTCURSORS; + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + } +} diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index c16c5058f3..6bc298951e 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -55,8 +55,9 @@ namespace Avalonia.Win32 pdwEffect = DropEffect.None; return UnmanagedMethods.HRESULT.S_OK; } - - _currentDrag = new OleDataObject(pDataObj); + _currentDrag = pDataObj as IDataObject; + if (_currentDrag == null) + _currentDrag = new OleDataObject(pDataObj); var args = new RawDragEvent( _dragDevice, RawDragEventType.DragEnter, @@ -124,8 +125,10 @@ namespace Avalonia.Win32 pdwEffect = DropEffect.None; return UnmanagedMethods.HRESULT.S_OK; } - - _currentDrag= new OleDataObject(pDataObj); + + _currentDrag = pDataObj as IDataObject; + if (_currentDrag == null) + _currentDrag= new OleDataObject(pDataObj); var args = new RawDragEvent( _dragDevice, From f57fbdc2da30ff6ab7b713e5fae2c7cb867b79bc Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 07:31:54 +0100 Subject: [PATCH 067/211] renamed DragDevice to DragDropDevice --- .../DragDrop/{DragDevice.cs => DragDropDevice.cs} | 4 ++-- .../DragDrop/Raw/{IDragDevice.cs => IDragDropDevice.cs} | 2 +- src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs | 2 +- src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 4 ++-- src/Windows/Avalonia.Win32/OleDropTarget.cs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename src/Avalonia.Controls/DragDrop/{DragDevice.cs => DragDropDevice.cs} (96%) rename src/Avalonia.Controls/DragDrop/Raw/{IDragDevice.cs => IDragDropDevice.cs} (60%) diff --git a/src/Avalonia.Controls/DragDrop/DragDevice.cs b/src/Avalonia.Controls/DragDrop/DragDropDevice.cs similarity index 96% rename from src/Avalonia.Controls/DragDrop/DragDevice.cs rename to src/Avalonia.Controls/DragDrop/DragDropDevice.cs index 25f82ca3bc..0aadda79f4 100644 --- a/src/Avalonia.Controls/DragDrop/DragDevice.cs +++ b/src/Avalonia.Controls/DragDrop/DragDropDevice.cs @@ -7,9 +7,9 @@ using Avalonia.Input.Raw; namespace Avalonia.Controls.DragDrop { - class DragDevice : IDragDevice + class DragDropDevice : IDragDropDevice { - public static readonly DragDevice Instance = new DragDevice(); + public static readonly DragDropDevice Instance = new DragDropDevice(); private Interactive _lastTarget = null; diff --git a/src/Avalonia.Controls/DragDrop/Raw/IDragDevice.cs b/src/Avalonia.Controls/DragDrop/Raw/IDragDropDevice.cs similarity index 60% rename from src/Avalonia.Controls/DragDrop/Raw/IDragDevice.cs rename to src/Avalonia.Controls/DragDrop/Raw/IDragDropDevice.cs index d1c20fae55..6e2db67ac3 100644 --- a/src/Avalonia.Controls/DragDrop/Raw/IDragDevice.cs +++ b/src/Avalonia.Controls/DragDrop/Raw/IDragDropDevice.cs @@ -2,7 +2,7 @@ namespace Avalonia.Controls.DragDrop.Raw { - public interface IDragDevice : IInputDevice + public interface IDragDropDevice : IInputDevice { } } \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs b/src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs index 76a2c16b3b..e9c1119e44 100644 --- a/src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs +++ b/src/Avalonia.Controls/DragDrop/Raw/RawDragEvent.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.DragDrop.Raw public DragDropEffects Effects { get; set; } public RawDragEventType Type { get; } - public RawDragEvent(IDragDevice inputDevice, RawDragEventType type, + public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type, IInputElement inputRoot, Point location, IDataObject data, DragDropEffects effects) :base(inputDevice, GetTimeStamp()) { diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index b767a86490..20d255a608 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.MonoMac { public TopLevelView View { get; } private readonly IMouseDevice _mouse = AvaloniaLocator.Current.GetService(); - private readonly IDragDevice _dragDevice = AvaloniaLocator.Current.GetService(); + private readonly IDragDropDevice _dragDevice = AvaloniaLocator.Current.GetService(); protected TopLevelImpl() { View = new TopLevelView(this); @@ -154,7 +154,7 @@ namespace Avalonia.MonoMac private NSDragOperation SendRawDragEvent(NSDraggingInfo sender, RawDragEventType type) { Action input = _tl.Input; - IDragDevice dragDevice = _tl._dragDevice; + IDragDropDevice dragDevice = _tl._dragDevice; IInputRoot root = _tl?.InputRoot; if (root == null || dragDevice == null || input == null) return NSDragOperation.None; diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 6bc298951e..31db5f88a9 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -12,13 +12,13 @@ namespace Avalonia.Win32 { private readonly IInputElement _target; private readonly ITopLevelImpl _tl; - private readonly IDragDevice _dragDevice; + private readonly IDragDropDevice _dragDevice; private IDataObject _currentDrag = null; public OleDropTarget(ITopLevelImpl tl, IInputElement target) { - _dragDevice = AvaloniaLocator.Current.GetService(); + _dragDevice = AvaloniaLocator.Current.GetService(); _tl = tl; _target = target; } From 30b5a1fd4df156180b2151562d5689f9a3ab4185 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 07:43:51 +0100 Subject: [PATCH 068/211] fixed locator setup --- src/Avalonia.Controls/Application.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index b751aeca60..0a9bf4eab2 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -237,7 +237,7 @@ namespace Avalonia .Bind().ToSingleton() .Bind().ToConstant(this) .Bind().ToConstant(AvaloniaScheduler.Instance) - .Bind().ToConstant(DragDevice.Instance); + .Bind().ToConstant(DragDropDevice.Instance); } } } From c1abc0535534e66697b08b0cd3e58530189ef297 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 07:45:55 +0100 Subject: [PATCH 069/211] Modified the ControlCatalog examples to run in STA ApartmentState --- samples/ControlCatalog.Desktop/Program.cs | 1 + samples/ControlCatalog.NetCore/Program.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index b151cabf43..a2048005a4 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -10,6 +10,7 @@ namespace ControlCatalog { internal class Program { + [STAThread] static void Main(string[] args) { // TODO: Make this work with GTK/Skia/Cairo depending on command-line args diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 346535d39d..b45a93455e 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -9,8 +9,10 @@ namespace ControlCatalog.NetCore { static class Program { + static void Main(string[] args) { + Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); if (args.Contains("--wait-for-attach")) { Console.WriteLine("Attach debugger and use 'Set next statement'"); From 411e3c8860b1756ce8b01943a1091d5481493b02 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 07:46:28 +0100 Subject: [PATCH 070/211] started dragging API --- src/Avalonia.Controls/DragDrop/DataObject.cs | 43 ++++++++++++++++++++ src/Avalonia.Controls/DragDrop/DragDrop.cs | 10 +++++ 2 files changed, 53 insertions(+) create mode 100644 src/Avalonia.Controls/DragDrop/DataObject.cs diff --git a/src/Avalonia.Controls/DragDrop/DataObject.cs b/src/Avalonia.Controls/DragDrop/DataObject.cs new file mode 100644 index 0000000000..e0e7b6d069 --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/DataObject.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.DragDrop +{ + public class DataObject : IDataObject + { + private readonly Dictionary _items = new Dictionary(); + + public bool Contains(string dataFormat) + { + return _items.ContainsKey(dataFormat); + } + + public object Get(string dataFormat) + { + if (_items.ContainsKey(dataFormat)) + return _items[dataFormat]; + return null; + } + + public IEnumerable GetDataFormats() + { + return _items.Keys; + } + + public IEnumerable GetFileNames() + { + return Get(DataFormats.FileNames) as IEnumerable; + } + + public string GetText() + { + return Get(DataFormats.Text) as string; + } + + public void Set(string dataFormat, object value) + { + _items[dataFormat] = value; + } + } +} diff --git a/src/Avalonia.Controls/DragDrop/DragDrop.cs b/src/Avalonia.Controls/DragDrop/DragDrop.cs index 317a903d3c..af003516b1 100644 --- a/src/Avalonia.Controls/DragDrop/DragDrop.cs +++ b/src/Avalonia.Controls/DragDrop/DragDrop.cs @@ -20,5 +20,15 @@ namespace Avalonia.Controls.DragDrop { interactive.SetValue(AcceptDragProperty, value); } + + /// + /// Starts a dragging operation with the given and returns the applied drop effect from the target. + /// + /// + public static DragDropEffects DoDragDrop(this Interactive source, IDataObject data, DragDropEffects allowedEffects) + { + + return DragDropEffects.None; + } } } \ No newline at end of file From b2461f4b1f3a99ad574132e63ba6f6cbcc5f57b0 Mon Sep 17 00:00:00 2001 From: Manuel Allenspach Date: Tue, 20 Feb 2018 17:16:58 +0100 Subject: [PATCH 071/211] Implement Closing event --- src/Avalonia.Controls/Platform/IWindowImpl.cs | 6 +++ src/Avalonia.Controls/Window.cs | 43 ++++++++++++++++--- .../Remote/PreviewerWindowImpl.cs | 1 + src/Avalonia.DesignerSupport/Remote/Stubs.cs | 1 + src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs | 8 ++++ src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs | 8 ++++ src/Windows/Avalonia.Win32/WindowImpl.cs | 10 +++++ 7 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 37637b1624..1f84574318 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -44,5 +44,11 @@ namespace Avalonia.Platform /// Enables or disables the taskbar icon /// void ShowTaskbarIcon(bool value); + + /// + /// Gets or sets a method called before the underlying implementation is destroyed. + /// Return true to prevent the underlying implementation from closing. + /// + Func Closing { get; set; } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9db7db365d..c66209d3c6 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -13,6 +13,7 @@ using Avalonia.Styling; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using System.ComponentModel; namespace Avalonia.Controls { @@ -129,6 +130,7 @@ namespace Avalonia.Controls public Window(IWindowImpl impl) : base(impl) { + impl.Closing = HandleClosing; _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); Screens = new Screens(PlatformImpl?.Screen); } @@ -230,20 +232,23 @@ namespace Avalonia.Controls /// Type IStyleable.StyleKey => typeof(Window); + /// + /// Fired before a window is closed. + /// + public event EventHandler Closing; + /// /// Closes the window. /// public void Close() { - s_windows.Remove(this); - PlatformImpl?.Dispose(); - IsVisible = false; + Close(false); } protected override void HandleApplicationExiting() { base.HandleApplicationExiting(); - Close(); + Close(true); } /// @@ -258,7 +263,35 @@ namespace Avalonia.Controls public void Close(object dialogResult) { _dialogResult = dialogResult; - Close(); + Close(false); + } + + internal void Close(bool ignoreCancel) + { + var cancelClosing = false; + try + { + cancelClosing = HandleClosing(); + } + finally + { + if (ignoreCancel || !cancelClosing) + { + s_windows.Remove(this); + PlatformImpl?.Dispose(); + IsVisible = false; + } + } + } + + /// + /// Handles a closing notification from . + /// + protected virtual bool HandleClosing() + { + var args = new CancelEventArgs(); + Closing?.Invoke(this, args); + return args.Cancel; } /// diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 3c7ef86d5d..fc9541abb7 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -39,6 +39,7 @@ namespace Avalonia.DesignerSupport.Remote public Action PositionChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } + public Func Closing { get; set; } public IPlatformHandle Handle { get; } public WindowState WindowState { get; set; } public Size MaxClientSize { get; } = new Size(4096, 4096); diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 2ed434a2dc..560425286e 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -26,6 +26,7 @@ namespace Avalonia.DesignerSupport.Remote public Action Paint { get; set; } public Action Resized { get; set; } public Action ScalingChanged { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); public Point Position { get; set; } diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 5aac4466b2..a42c8a19b9 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -54,6 +54,7 @@ namespace Avalonia.Gtk3 ConnectEvent("key-press-event", OnKeyEvent); ConnectEvent("key-release-event", OnKeyEvent); ConnectEvent("leave-notify-event", OnLeaveNotifyEvent); + ConnectEvent("delete-event", OnClosingEvent); Connect("destroy", OnDestroy); Native.GtkWidgetRealize(gtkWidget); GdkWindowHandle = this.Handle.Handle; @@ -125,6 +126,12 @@ namespace Avalonia.Gtk3 return rv; } + private unsafe bool OnClosingEvent(IntPtr w, IntPtr ev, IntPtr userdata) + { + bool? preventClosing = Closing?.Invoke(); + return preventClosing ?? false; + } + private unsafe bool OnButton(IntPtr w, IntPtr ev, IntPtr userdata) { var evnt = (GdkEventButton*)ev; @@ -343,6 +350,7 @@ namespace Avalonia.Gtk3 string IPlatformHandle.HandleDescriptor => "HWND"; public Action Activated { get; set; } + public Func Closing { get; set; } public Action Closed { get; set; } public Action Deactivated { get; set; } public Action Input { get; set; } diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index 9ce1756aae..e5ba285f4f 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Input.Raw; using Avalonia.Platform; using MonoMac.AppKit; using MonoMac.CoreGraphics; +using MonoMac.Foundation; using MonoMac.ObjCRuntime; namespace Avalonia.MonoMac @@ -69,6 +70,12 @@ namespace Avalonia.MonoMac _impl.PositionChanged?.Invoke(_impl.Position); } + public override bool WindowShouldClose(NSObject sender) + { + bool? preventClose = _impl.Closing?.Invoke(); + return preventClose != true; + } + public override void WillClose(global::MonoMac.Foundation.NSNotification notification) { _impl.Window.Dispose(); @@ -107,6 +114,7 @@ namespace Avalonia.MonoMac public Action PositionChanged { get; set; } public Action Deactivated { get; set; } public Action Activated { get; set; } + public Func Closing { get; set; } public override Size ClientSize => Window.ContentRectFor(Window.Frame).Size.ToAvaloniaSize(); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 3ba926b42a..a67362d59f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -56,6 +56,8 @@ namespace Avalonia.Win32 public Action Activated { get; set; } + public Func Closing { get; set; } + public Action Closed { get; set; } public Action Deactivated { get; set; } @@ -431,6 +433,14 @@ namespace Avalonia.Win32 return IntPtr.Zero; + case UnmanagedMethods.WindowsMessage.WM_CLOSE: + bool? preventClosing = Closing?.Invoke(); + if (preventClosing == true) + { + return IntPtr.Zero; + } + break; + case UnmanagedMethods.WindowsMessage.WM_DESTROY: //Window doesn't exist anymore _hwnd = IntPtr.Zero; From 7b3942685e4495231a658afb22d204508d995a9d Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 14:02:06 +0100 Subject: [PATCH 072/211] Basic generic + Windows Drag support. --- src/Avalonia.Controls/Application.cs | 4 +- src/Avalonia.Controls/DragDrop/DragDrop.cs | 10 +- src/Avalonia.Controls/DragDrop/DragSource.cs | 129 ++++++++++++++++++ .../Platform/IPlatformDragSource.cs | 14 ++ src/Windows/Avalonia.Win32/DragSource.cs | 28 ++++ src/Windows/Avalonia.Win32/OleDragSource.cs | 4 +- src/Windows/Avalonia.Win32/OleDropTarget.cs | 4 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 3 +- 8 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 src/Avalonia.Controls/DragDrop/DragSource.cs create mode 100644 src/Avalonia.Controls/Platform/IPlatformDragSource.cs create mode 100644 src/Windows/Avalonia.Win32/DragSource.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 0a9bf4eab2..6ed797dc51 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -14,6 +14,7 @@ using Avalonia.Threading; using System.Reactive.Concurrency; using Avalonia.Controls.DragDrop; using Avalonia.Controls.DragDrop.Raw; +using Avalonia.Controls.Platform; namespace Avalonia { @@ -237,7 +238,8 @@ namespace Avalonia .Bind().ToSingleton() .Bind().ToConstant(this) .Bind().ToConstant(AvaloniaScheduler.Instance) - .Bind().ToConstant(DragDropDevice.Instance); + .Bind().ToConstant(DragDropDevice.Instance) + .Bind().ToTransient(); } } } diff --git a/src/Avalonia.Controls/DragDrop/DragDrop.cs b/src/Avalonia.Controls/DragDrop/DragDrop.cs index af003516b1..bbac80e38f 100644 --- a/src/Avalonia.Controls/DragDrop/DragDrop.cs +++ b/src/Avalonia.Controls/DragDrop/DragDrop.cs @@ -1,4 +1,6 @@ -using Avalonia.Interactivity; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; +using Avalonia.Interactivity; namespace Avalonia.Controls.DragDrop { @@ -25,10 +27,10 @@ namespace Avalonia.Controls.DragDrop /// Starts a dragging operation with the given and returns the applied drop effect from the target. /// /// - public static DragDropEffects DoDragDrop(this Interactive source, IDataObject data, DragDropEffects allowedEffects) + public static Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) { - - return DragDropEffects.None; + var src = AvaloniaLocator.Current.GetService(); + return src?.DoDragDrop(data, allowedEffects) ?? Task.FromResult(DragDropEffects.None); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/DragDrop/DragSource.cs b/src/Avalonia.Controls/DragDrop/DragSource.cs new file mode 100644 index 0000000000..f899eabfbd --- /dev/null +++ b/src/Avalonia.Controls/DragDrop/DragSource.cs @@ -0,0 +1,129 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.DragDrop.Raw; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.DragDrop +{ + class DragSource : IPlatformDragSource + { + private const InputModifiers MOUSE_INPUTMODIFIERS = InputModifiers.LeftMouseButton|InputModifiers.MiddleMouseButton|InputModifiers.RightMouseButton; + private readonly IDragDropDevice _dragDrop; + private readonly IInputManager _inputManager; + + + private readonly Subject _result = new Subject(); + private IDataObject _draggedData; + private IInputRoot _lastRoot; + private InputModifiers? _initialInputModifiers; + + public DragSource() + { + _inputManager = AvaloniaLocator.Current.GetService(); + _dragDrop = AvaloniaLocator.Current.GetService(); + } + + public async Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + { + Dispatcher.UIThread.VerifyAccess(); + if (_draggedData == null) + { + _draggedData = data; + _lastRoot = null; + + using (_inputManager.PreProcess.OfType().Subscribe(e => ProcessMouseEvents(e, allowedEffects))) + { + var effect = await _result.FirstAsync(); + return effect; + } + } + return DragDropEffects.None; + } + + private DragDropEffects RaiseDragEvent(RawDragEventType type, IInputElement root, Point pt, DragDropEffects allowedEffects) + { + RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, allowedEffects); + var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + tl.PlatformImpl.Input(rawEvent); + return rawEvent.Effects; + } + + private void ProcessMouseEvents(RawMouseEventArgs e, DragDropEffects allowedEffects) + { + if (!_initialInputModifiers.HasValue) + _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS; + + void CancelDragging() + { + if (_lastRoot != null) + RaiseDragEvent(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), allowedEffects); + _result.OnNext(DragDropEffects.None); + e.Handled = true; + } + void AcceptDragging() + { + var result = RaiseDragEvent(RawDragEventType.Drop, e.Root, e.Position, allowedEffects) & allowedEffects; + _result.OnNext(result); + e.Handled = true; + } + + switch (e.Type) + { + case RawMouseEventType.LeftButtonDown: + case RawMouseEventType.RightButtonDown: + case RawMouseEventType.MiddleButtonDown: + case RawMouseEventType.NonClientLeftButtonDown: + CancelDragging(); + return; + case RawMouseEventType.LeaveWindow: + RaiseDragEvent(RawDragEventType.DragLeave, e.Root, e.Position, allowedEffects); + break; + case RawMouseEventType.LeftButtonUp: + if (_initialInputModifiers.Value.HasFlag(InputModifiers.LeftMouseButton)) + AcceptDragging(); + else + CancelDragging(); + return; + case RawMouseEventType.MiddleButtonUp: + if (_initialInputModifiers.Value.HasFlag(InputModifiers.MiddleMouseButton)) + AcceptDragging(); + else + CancelDragging(); + return; + case RawMouseEventType.RightButtonUp: + if (_initialInputModifiers.Value.HasFlag(InputModifiers.RightMouseButton)) + AcceptDragging(); + else + CancelDragging(); + return; + case RawMouseEventType.Move: + var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS; + if (_initialInputModifiers.Value != mods) + { + CancelDragging(); + return; + } + + if (e.Root != _lastRoot) + { + if (_lastRoot != null) + RaiseDragEvent(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), allowedEffects); + RaiseDragEvent(RawDragEventType.DragEnter, e.Root, e.Position, allowedEffects); + _lastRoot = e.Root; + } + else + RaiseDragEvent(RawDragEventType.DragOver, e.Root, e.Position, allowedEffects); + return; + } + } + } +} diff --git a/src/Avalonia.Controls/Platform/IPlatformDragSource.cs b/src/Avalonia.Controls/Platform/IPlatformDragSource.cs new file mode 100644 index 0000000000..667e1f005c --- /dev/null +++ b/src/Avalonia.Controls/Platform/IPlatformDragSource.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.DragDrop; +using Avalonia.Interactivity; + +namespace Avalonia.Controls.Platform +{ + public interface IPlatformDragSource + { + Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects); + } +} diff --git a/src/Windows/Avalonia.Win32/DragSource.cs b/src/Windows/Avalonia.Win32/DragSource.cs new file mode 100644 index 0000000000..937471f9ad --- /dev/null +++ b/src/Windows/Avalonia.Win32/DragSource.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.DragDrop; +using Avalonia.Controls.Platform; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + class DragSource : IPlatformDragSource + { + public Task DoDragDrop(IDataObject data, DragDropEffects allowedEffects) + { + Dispatcher.UIThread.VerifyAccess(); + + OleDragSource src = new OleDragSource(); + DataObject dataObject = new DataObject(data); + int allowed = (int)OleDropTarget.ConvertDropEffect(allowedEffects); + + int[] finalEffect = new int[1]; + UnmanagedMethods.DoDragDrop(dataObject, src, allowed, finalEffect); + + return Task.FromResult(OleDropTarget.ConvertDropEffect((DropEffect)finalEffect[0]));} + } +} diff --git a/src/Windows/Avalonia.Win32/OleDragSource.cs b/src/Windows/Avalonia.Win32/OleDragSource.cs index 1bff984087..522014abc0 100644 --- a/src/Windows/Avalonia.Win32/OleDragSource.cs +++ b/src/Windows/Avalonia.Win32/OleDragSource.cs @@ -33,9 +33,7 @@ namespace Avalonia.Win32 public int GiveFeedback(int dwEffect) { - if (dwEffect != 0) - return DRAGDROP_S_USEDEFAULTCURSORS; - return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + return DRAGDROP_S_USEDEFAULTCURSORS; } } } diff --git a/src/Windows/Avalonia.Win32/OleDropTarget.cs b/src/Windows/Avalonia.Win32/OleDropTarget.cs index 31db5f88a9..747006673a 100644 --- a/src/Windows/Avalonia.Win32/OleDropTarget.cs +++ b/src/Windows/Avalonia.Win32/OleDropTarget.cs @@ -23,7 +23,7 @@ namespace Avalonia.Win32 _target = target; } - static DropEffect ConvertDropEffect(DragDropEffects operation) + public static DropEffect ConvertDropEffect(DragDropEffects operation) { DropEffect result = DropEffect.None; if (operation.HasFlag(DragDropEffects.Copy)) @@ -35,7 +35,7 @@ namespace Avalonia.Win32 return result; } - static DragDropEffects ConvertDropEffect(DropEffect effect) + public static DragDropEffects ConvertDropEffect(DropEffect effect) { DragDropEffects result = DragDropEffects.None; if (effect.HasFlag(DropEffect.Copy)) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 4e1ba618a8..ef1c4e987d 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -84,7 +84,8 @@ namespace Avalonia.Win32 .Bind().ToConstant(new RenderLoop(60)) .Bind().ToSingleton() .Bind().ToConstant(s_instance) - .Bind().ToConstant(s_instance); + .Bind().ToConstant(s_instance) + .Bind().ToSingleton(); UseDeferredRendering = deferredRendering; _uiThread = UnmanagedMethods.GetCurrentThreadId(); From 81aaa0938309fab331725189c841b5b5bd35c247 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 18:43:07 +0100 Subject: [PATCH 073/211] set the cursor for the drop-effects --- src/Avalonia.Controls/DragDrop/DragSource.cs | 47 ++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/DragDrop/DragSource.cs b/src/Avalonia.Controls/DragDrop/DragSource.cs index f899eabfbd..2048a0c0da 100644 --- a/src/Avalonia.Controls/DragDrop/DragSource.cs +++ b/src/Avalonia.Controls/DragDrop/DragSource.cs @@ -23,8 +23,9 @@ namespace Avalonia.Controls.DragDrop private readonly Subject _result = new Subject(); private IDataObject _draggedData; - private IInputRoot _lastRoot; + private IInputElement _lastRoot; private InputModifiers? _initialInputModifiers; + private object _lastCursor; public DragSource() { @@ -54,9 +55,48 @@ namespace Avalonia.Controls.DragDrop RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, allowedEffects); var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); tl.PlatformImpl.Input(rawEvent); + + SetCursor(root, rawEvent.Effects); return rawEvent.Effects; } + private Cursor GetCursorForDropEffect(DragDropEffects effects) + { + // Todo. Needs to choose cursor by effect. + if (effects == DragDropEffects.None) + return new Cursor(StandardCursorType.No); + return new Cursor(StandardCursorType.Hand); + } + + private void SetCursor(IInputElement root, DragDropEffects effect) + { + if (_lastRoot != root) + { + if (_lastRoot is InputElement ieLast) + { + if (_lastCursor == AvaloniaProperty.UnsetValue) + ieLast.ClearValue(InputElement.CursorProperty); + else + ieLast.Cursor = _lastCursor as Cursor; + } + + if (root is InputElement ieNew) + { + if (!ieNew.IsSet(InputElement.CursorProperty)) + _lastCursor = AvaloniaProperty.UnsetValue; + else + _lastCursor = root.Cursor; + } + else + _lastCursor = null; + + _lastRoot = root; + } + + if (root is InputElement ie) + ie.Cursor = GetCursorForDropEffect(effect); + } + private void ProcessMouseEvents(RawMouseEventArgs e, DragDropEffects allowedEffects) { if (!_initialInputModifiers.HasValue) @@ -66,16 +106,18 @@ namespace Avalonia.Controls.DragDrop { if (_lastRoot != null) RaiseDragEvent(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), allowedEffects); + SetCursor(null, DragDropEffects.None); _result.OnNext(DragDropEffects.None); e.Handled = true; } void AcceptDragging() { var result = RaiseDragEvent(RawDragEventType.Drop, e.Root, e.Position, allowedEffects) & allowedEffects; + SetCursor(null, DragDropEffects.None); _result.OnNext(result); e.Handled = true; } - + switch (e.Type) { case RawMouseEventType.LeftButtonDown: @@ -118,7 +160,6 @@ namespace Avalonia.Controls.DragDrop if (_lastRoot != null) RaiseDragEvent(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), allowedEffects); RaiseDragEvent(RawDragEventType.DragEnter, e.Root, e.Position, allowedEffects); - _lastRoot = e.Root; } else RaiseDragEvent(RawDragEventType.DragOver, e.Root, e.Position, allowedEffects); From 7f2f713ff0d549488ad8304547ec2680362af348 Mon Sep 17 00:00:00 2001 From: boombuler Date: Wed, 7 Mar 2018 22:21:19 +0100 Subject: [PATCH 074/211] fixed not updating cursor on osx --- src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 667ee12fa0..9dc6c45745 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -133,7 +133,10 @@ namespace Avalonia.MonoMac { ResetCursorRects(); if (_cursor != null) + { AddCursorRect(Frame, _cursor); + _cursor.Set(); + } } static readonly NSCursor ArrowCursor = NSCursor.ArrowCursor; From a3dd74fafab6ef7b95acbe56fad8f32a7e198748 Mon Sep 17 00:00:00 2001 From: boombuler Date: Thu, 8 Mar 2018 07:27:35 +0100 Subject: [PATCH 075/211] only set the cursor if the mouse is over the toplevel --- src/OSX/Avalonia.MonoMac/TopLevelImpl.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 9dc6c45745..a655bc1ec5 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -39,6 +39,7 @@ namespace Avalonia.MonoMac private NSTrackingArea _area; private NSCursor _cursor; private bool _nonUiRedrawQueued; + private bool _isMouseOver; public CGSize PixelSize { get; set; } @@ -135,7 +136,8 @@ namespace Avalonia.MonoMac if (_cursor != null) { AddCursorRect(Frame, _cursor); - _cursor.Set(); + if (_isMouseOver) + _cursor.Set(); } } @@ -302,10 +304,17 @@ namespace Avalonia.MonoMac public override void MouseExited(NSEvent theEvent) { + _isMouseOver = false; MouseEvent(theEvent, RawMouseEventType.LeaveWindow); base.MouseExited(theEvent); } + public override void MouseEntered(NSEvent theEvent) + { + _isMouseOver = true; + base.MouseEntered(theEvent); + } + void KeyboardEvent(RawKeyEventType type, NSEvent ev) { var code = KeyTransform.TransformKeyCode(ev.KeyCode); From e967a8eb7276542166080c2652fe4c6f5b3fa300 Mon Sep 17 00:00:00 2001 From: ahopper Date: Thu, 8 Mar 2018 12:34:47 +0000 Subject: [PATCH 076/211] rename WritableBitmap --- src/Avalonia.Controls/Remote/RemoteWidget.cs | 4 ++-- .../Imaging/{WritableBitmap.cs => WriteableBitmap.cs} | 10 +++++----- .../Platform/IPlatformRenderInterface.cs | 6 +++--- ...{IWritableBitmapImpl.cs => IWriteableBitmapImpl.cs} | 4 ++-- src/Skia/Avalonia.Skia/BitmapImpl.cs | 2 +- src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 2 +- src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs | 4 ++-- ...tableWicBitmapImpl.cs => WriteableWicBitmapImpl.cs} | 4 ++-- tests/Avalonia.RenderTests/Media/BitmapTests.cs | 10 +++++----- .../Avalonia.UnitTests/MockPlatformRenderInterface.cs | 2 +- .../VisualTree/MockRenderInterface.cs | 2 +- 11 files changed, 25 insertions(+), 25 deletions(-) rename src/Avalonia.Visuals/Media/Imaging/{WritableBitmap.cs => WriteableBitmap.cs} (51%) rename src/Avalonia.Visuals/Platform/{IWritableBitmapImpl.cs => IWriteableBitmapImpl.cs} (75%) rename src/Windows/Avalonia.Direct2D1/Media/Imaging/{WritableWicBitmapImpl.cs => WriteableWicBitmapImpl.cs} (86%) diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 83360a0010..ea8c3ebe52 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Remote { private readonly IAvaloniaRemoteTransportConnection _connection; private FrameMessage _lastFrame; - private WritableBitmap _bitmap; + private WriteableBitmap _bitmap; public RemoteWidget(IAvaloniaRemoteTransportConnection connection) { _connection = connection; @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Remote var fmt = (PixelFormat) _lastFrame.Format; if (_bitmap == null || _bitmap.PixelWidth != _lastFrame.Width || _bitmap.PixelHeight != _lastFrame.Height) - _bitmap = new WritableBitmap(_lastFrame.Width, _lastFrame.Height, fmt); + _bitmap = new WriteableBitmap(_lastFrame.Width, _lastFrame.Height, fmt); using (var l = _bitmap.Lock()) { var lineLen = (fmt == PixelFormat.Rgb565 ? 2 : 4) * _lastFrame.Width; diff --git a/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs similarity index 51% rename from src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs rename to src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs index df3e71dc85..af6fde6876 100644 --- a/src/Avalonia.Visuals/Media/Imaging/WritableBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/WriteableBitmap.cs @@ -9,15 +9,15 @@ using Avalonia.Utilities; namespace Avalonia.Media.Imaging { /// - /// Holds a writable bitmap image. + /// Holds a writeable bitmap image. /// - public class WritableBitmap : Bitmap + public class WriteableBitmap : Bitmap { - public WritableBitmap(int width, int height, PixelFormat? format = null) - : base(AvaloniaLocator.Current.GetService().CreateWritableBitmap(width, height, format)) + public WriteableBitmap(int width, int height, PixelFormat? format = null) + : base(AvaloniaLocator.Current.GetService().CreateWriteableBitmap(width, height, format)) { } - public ILockedFramebuffer Lock() => ((IWritableBitmapImpl) PlatformImpl.Item).Lock(); + public ILockedFramebuffer Lock() => ((IWriteableBitmapImpl) PlatformImpl.Item).Lock(); } } diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs index aab8521f6d..cc17efd2bb 100644 --- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs @@ -61,13 +61,13 @@ namespace Avalonia.Platform double dpiY); /// - /// Creates a writable bitmap implementation. + /// Creates a writeable bitmap implementation. /// /// The width of the bitmap. /// The height of the bitmap. /// Pixel format (optional). - /// An . - IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null); + /// An . + IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null); /// /// Loads a bitmap implementation from a file.. diff --git a/src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs b/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs similarity index 75% rename from src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs rename to src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs index b736c11dab..7ab5a7c100 100644 --- a/src/Avalonia.Visuals/Platform/IWritableBitmapImpl.cs +++ b/src/Avalonia.Visuals/Platform/IWriteableBitmapImpl.cs @@ -7,9 +7,9 @@ using System.Threading.Tasks; namespace Avalonia.Platform { /// - /// Defines the platform-specific interface for a . + /// Defines the platform-specific interface for a . /// - public interface IWritableBitmapImpl : IBitmapImpl + public interface IWriteableBitmapImpl : IBitmapImpl { ILockedFramebuffer Lock(); } diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index 00ab770e01..ccc5a37105 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -6,7 +6,7 @@ using SkiaSharp; namespace Avalonia.Skia { - class BitmapImpl : IRenderTargetBitmapImpl, IWritableBitmapImpl + class BitmapImpl : IRenderTargetBitmapImpl, IWriteableBitmapImpl { private Vector _dpi; diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index bd3769e4a5..50e65f45dc 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -88,7 +88,7 @@ namespace Avalonia.Skia return new FramebufferRenderTarget(fb); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) { return new BitmapImpl(width, height, new Vector(96, 96), format); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index a47c871f5a..296edcb2d9 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -168,9 +168,9 @@ namespace Avalonia.Direct2D1 dpiY); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = null) { - return new WritableWicBitmapImpl(s_imagingFactory, width, height, format); + return new WriteableWicBitmapImpl(s_imagingFactory, width, height, format); } public IStreamGeometryImpl CreateStreamGeometry() diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs similarity index 86% rename from src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs rename to src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index 5dc07e06c4..fc931c32db 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WritableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -9,9 +9,9 @@ using PixelFormat = Avalonia.Platform.PixelFormat; namespace Avalonia.Direct2D1.Media.Imaging { - class WritableWicBitmapImpl : WicBitmapImpl, IWritableBitmapImpl + class WriteableWicBitmapImpl : WicBitmapImpl, IWriteableBitmapImpl { - public WritableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) + public WriteableWicBitmapImpl(ImagingFactory factory, int width, int height, PixelFormat? pixelFormat) : base(factory, width, height, pixelFormat) { } diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index a7cd06a894..089579a0a0 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -106,9 +106,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Theory] [InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgba8888)] - public void WritableBitmapShouldBeUsable(PixelFormat fmt) + public void WriteableBitmapShouldBeUsable(PixelFormat fmt) { - var writableBitmap = new WritableBitmap(256, 256, fmt); + var writeableBitmap = new WriteableBitmap(256, 256, fmt); var data = new int[256 * 256]; for (int y = 0; y < 256; y++) @@ -116,7 +116,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media data[y * 256 + x] =(int)((uint)(x + (y << 8)) | 0xFF000000u); - using (var l = writableBitmap.Lock()) + using (var l = writeableBitmap.Lock()) { for(var r = 0; r<256; r++) { @@ -125,9 +125,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media } - var name = nameof(WritableBitmapShouldBeUsable) + "_" + fmt; + var name = nameof(WriteableBitmapShouldBeUsable) + "_" + fmt; - writableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png")); + writeableBitmap.Save(System.IO.Path.Combine(OutputPath, name + ".out.png")); CompareImagesNoRenderer(name); } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 8c6c949e07..de2b517956 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -39,7 +39,7 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?)) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? format = default(PixelFormat?)) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 54bb5d72d0..93b5a8a764 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -49,7 +49,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree throw new NotImplementedException(); } - public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? fmt) + public IWriteableBitmapImpl CreateWriteableBitmap(int width, int height, PixelFormat? fmt) { throw new NotImplementedException(); } From 74163ff668d744a1c3b06917cdeef63977df89fd Mon Sep 17 00:00:00 2001 From: boombuler Date: Thu, 8 Mar 2018 15:03:51 +0100 Subject: [PATCH 077/211] Added D+D StandardCursors --- src/Avalonia.Input/Cursors.cs | 5 +++- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 5 +++- src/OSX/Avalonia.MonoMac/Cursor.cs | 4 +++ src/Windows/Avalonia.Win32/CursorFactory.cs | 27 +++++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index e3860e58e5..02a026c998 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,7 +38,10 @@ namespace Avalonia.Input TopLeftCorner, TopRightCorner, BottomLeftCorner, - BottomRightCorner + BottomRightCorner, + DragMove, + DragCopy, + DragLink, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index ac547b8bc2..d6a3c1f260 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -32,7 +32,10 @@ namespace Avalonia.Gtk3 {StandardCursorType.TopLeftCorner, CursorType.TopLeftCorner}, {StandardCursorType.TopRightCorner, CursorType.TopRightCorner}, {StandardCursorType.BottomLeftCorner, CursorType.BottomLeftCorner}, - {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner} + {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, + {StandardCursorType.DragCopy, CursorType.CenterPtr}, + {StandardCursorType.DragMove, CursorType.Fleur}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = diff --git a/src/OSX/Avalonia.MonoMac/Cursor.cs b/src/OSX/Avalonia.MonoMac/Cursor.cs index 10445e62e2..d9370e527b 100644 --- a/src/OSX/Avalonia.MonoMac/Cursor.cs +++ b/src/OSX/Avalonia.MonoMac/Cursor.cs @@ -51,6 +51,10 @@ namespace Avalonia.MonoMac [StandardCursorType.TopSide] = NSCursor.ResizeUpCursor, [StandardCursorType.UpArrow] = NSCursor.ResizeUpCursor, [StandardCursorType.Wait] = NSCursor.ArrowCursor, //TODO + [StandardCursorType.DragMove] = NSCursor.DragCopyCursor, // TODO + [StandardCursorType.DragCopy] = NSCursor.DragCopyCursor, + [StandardCursorType.DragLink] = NSCursor.DragLinkCursor, + }; } diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index 0d529d6b91..fa2fbe4810 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Platform; +using System.Runtime.InteropServices; namespace Avalonia.Win32 { @@ -20,6 +21,27 @@ namespace Avalonia.Win32 { } + static CursorFactory() + { + LoadModuleCursor(StandardCursorType.DragMove, "ole32.dll", 2); + LoadModuleCursor(StandardCursorType.DragCopy, "ole32.dll", 3); + LoadModuleCursor(StandardCursorType.DragLink, "ole32.dll", 4); + } + + private static void LoadModuleCursor(StandardCursorType cursorType, string module, int id) + { + IntPtr mh = UnmanagedMethods.GetModuleHandle(module); + if (mh != IntPtr.Zero) + { + IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id)); + if (cursor != IntPtr.Zero) + { + PlatformHandle phCursor = new PlatformHandle(cursor, PlatformConstants.CursorHandleType); + Cache.Add(cursorType, phCursor); + } + } + } + private static readonly Dictionary CursorTypeMapping = new Dictionary { @@ -47,6 +69,11 @@ namespace Avalonia.Win32 //Using SizeNorthEastSouthWest {StandardCursorType.TopRightCorner, 32643}, {StandardCursorType.BottomLeftCorner, 32643}, + + // Fallback, should have been loaded from ole32.dll + {StandardCursorType.DragMove, 32516}, + {StandardCursorType.DragCopy, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From 97f581af496fc8172b8e1863bb6fae89e8f544dc Mon Sep 17 00:00:00 2001 From: boombuler Date: Thu, 8 Mar 2018 15:05:13 +0100 Subject: [PATCH 078/211] Improved DragSource the drag source now gets the preferred drag effect and handles keyboard inputs too --- src/Avalonia.Controls/DragDrop/DragSource.cs | 129 ++++++++++++------- 1 file changed, 81 insertions(+), 48 deletions(-) diff --git a/src/Avalonia.Controls/DragDrop/DragSource.cs b/src/Avalonia.Controls/DragDrop/DragSource.cs index 2048a0c0da..fd3d8ef143 100644 --- a/src/Avalonia.Controls/DragDrop/DragSource.cs +++ b/src/Avalonia.Controls/DragDrop/DragSource.cs @@ -19,13 +19,14 @@ namespace Avalonia.Controls.DragDrop private const InputModifiers MOUSE_INPUTMODIFIERS = InputModifiers.LeftMouseButton|InputModifiers.MiddleMouseButton|InputModifiers.RightMouseButton; private readonly IDragDropDevice _dragDrop; private readonly IInputManager _inputManager; - - private readonly Subject _result = new Subject(); + + private DragDropEffects _allowedEffects; private IDataObject _draggedData; private IInputElement _lastRoot; - private InputModifiers? _initialInputModifiers; + private Point _lastPosition; private object _lastCursor; + private InputModifiers? _initialInputModifiers; public DragSource() { @@ -40,35 +41,58 @@ namespace Avalonia.Controls.DragDrop { _draggedData = data; _lastRoot = null; + _lastPosition = default(Point); + _allowedEffects = allowedEffects; - using (_inputManager.PreProcess.OfType().Subscribe(e => ProcessMouseEvents(e, allowedEffects))) + using (_inputManager.PreProcess.OfType().Subscribe(ProcessMouseEvents)) { - var effect = await _result.FirstAsync(); - return effect; + using (_inputManager.PreProcess.OfType().Subscribe(ProcessKeyEvents)) + { + var effect = await _result.FirstAsync(); + return effect; + } } } return DragDropEffects.None; } - private DragDropEffects RaiseDragEvent(RawDragEventType type, IInputElement root, Point pt, DragDropEffects allowedEffects) + + private DragDropEffects RaiseEventAndUpdateCursor(RawDragEventType type, IInputElement root, Point pt, InputModifiers modifiers) { - RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, allowedEffects); + _lastPosition = pt; + + RawDragEvent rawEvent = new RawDragEvent(_dragDrop, type, root, pt, _draggedData, _allowedEffects); var tl = root.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); tl.PlatformImpl.Input(rawEvent); - - SetCursor(root, rawEvent.Effects); - return rawEvent.Effects; + + var effect = GetPreferredEffect(rawEvent.Effects & _allowedEffects, modifiers); + UpdateCursor(root, effect); + return effect; + } + + private DragDropEffects GetPreferredEffect(DragDropEffects effect, InputModifiers modifiers) + { + if (effect == DragDropEffects.Copy || effect == DragDropEffects.Move || effect == DragDropEffects.Link || effect == DragDropEffects.None) + return effect; // No need to check for the modifiers. + if (effect.HasFlag(DragDropEffects.Link) && modifiers.HasFlag(InputModifiers.Alt)) + return DragDropEffects.Link; + if (effect.HasFlag(DragDropEffects.Copy) && modifiers.HasFlag(InputModifiers.Control)) + return DragDropEffects.Copy; + return DragDropEffects.Move; } private Cursor GetCursorForDropEffect(DragDropEffects effects) { - // Todo. Needs to choose cursor by effect. - if (effects == DragDropEffects.None) - return new Cursor(StandardCursorType.No); - return new Cursor(StandardCursorType.Hand); + if (effects.HasFlag(DragDropEffects.Copy)) + return new Cursor(StandardCursorType.DragCopy); + if (effects.HasFlag(DragDropEffects.Move)) + return new Cursor(StandardCursorType.DragMove); + if (effects.HasFlag(DragDropEffects.Link)) + return new Cursor(StandardCursorType.DragLink); + return new Cursor(StandardCursorType.No); } - private void SetCursor(IInputElement root, DragDropEffects effect) + private void UpdateCursor(IInputElement root, DragDropEffects effect) { if (_lastRoot != root) { @@ -96,25 +120,45 @@ namespace Avalonia.Controls.DragDrop if (root is InputElement ie) ie.Cursor = GetCursorForDropEffect(effect); } - - private void ProcessMouseEvents(RawMouseEventArgs e, DragDropEffects allowedEffects) + + private void CancelDragging() { - if (!_initialInputModifiers.HasValue) - _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS; + if (_lastRoot != null) + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, InputModifiers.None); + UpdateCursor(null, DragDropEffects.None); + _result.OnNext(DragDropEffects.None); + } - void CancelDragging() + private void ProcessKeyEvents(RawKeyEventArgs e) + { + if (e.Type == RawKeyEventType.KeyDown && e.Key == Key.Escape) { if (_lastRoot != null) - RaiseDragEvent(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), allowedEffects); - SetCursor(null, DragDropEffects.None); + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastPosition, e.Modifiers); + UpdateCursor(null, DragDropEffects.None); _result.OnNext(DragDropEffects.None); e.Handled = true; } - void AcceptDragging() + else if (e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl || e.Key == Key.LeftAlt || e.Key == Key.RightAlt) + RaiseEventAndUpdateCursor(RawDragEventType.DragOver, _lastRoot, _lastPosition, e.Modifiers); + } + + private void ProcessMouseEvents(RawMouseEventArgs e) + { + if (!_initialInputModifiers.HasValue) + _initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS; + + + void CheckDraggingAccepted(InputModifiers changedMouseButton) { - var result = RaiseDragEvent(RawDragEventType.Drop, e.Root, e.Position, allowedEffects) & allowedEffects; - SetCursor(null, DragDropEffects.None); - _result.OnNext(result); + if (_initialInputModifiers.Value.HasFlag(changedMouseButton)) + { + var result = RaiseEventAndUpdateCursor(RawDragEventType.Drop, e.Root, e.Position, e.InputModifiers); + UpdateCursor(null, DragDropEffects.None); + _result.OnNext(result); + } + else + CancelDragging(); e.Handled = true; } @@ -125,45 +169,34 @@ namespace Avalonia.Controls.DragDrop case RawMouseEventType.MiddleButtonDown: case RawMouseEventType.NonClientLeftButtonDown: CancelDragging(); + e.Handled = true; return; case RawMouseEventType.LeaveWindow: - RaiseDragEvent(RawDragEventType.DragLeave, e.Root, e.Position, allowedEffects); - break; + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position, e.InputModifiers); break; case RawMouseEventType.LeftButtonUp: - if (_initialInputModifiers.Value.HasFlag(InputModifiers.LeftMouseButton)) - AcceptDragging(); - else - CancelDragging(); - return; + CheckDraggingAccepted(InputModifiers.LeftMouseButton); break; case RawMouseEventType.MiddleButtonUp: - if (_initialInputModifiers.Value.HasFlag(InputModifiers.MiddleMouseButton)) - AcceptDragging(); - else - CancelDragging(); - return; + CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break; case RawMouseEventType.RightButtonUp: - if (_initialInputModifiers.Value.HasFlag(InputModifiers.RightMouseButton)) - AcceptDragging(); - else - CancelDragging(); - return; + CheckDraggingAccepted(InputModifiers.RightMouseButton); break; case RawMouseEventType.Move: var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS; if (_initialInputModifiers.Value != mods) { CancelDragging(); + e.Handled = true; return; } if (e.Root != _lastRoot) { if (_lastRoot != null) - RaiseDragEvent(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), allowedEffects); - RaiseDragEvent(RawDragEventType.DragEnter, e.Root, e.Position, allowedEffects); + RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, _lastRoot, _lastRoot.PointToClient(e.Root.PointToScreen(e.Position)), e.InputModifiers); + RaiseEventAndUpdateCursor(RawDragEventType.DragEnter, e.Root, e.Position, e.InputModifiers); } else - RaiseDragEvent(RawDragEventType.DragOver, e.Root, e.Position, allowedEffects); - return; + RaiseEventAndUpdateCursor(RawDragEventType.DragOver, e.Root, e.Position, e.InputModifiers); + break; } } } From c6a7611150f07a21aa52e3e8364502c93ca39393 Mon Sep 17 00:00:00 2001 From: ahopper Date: Thu, 8 Mar 2018 14:39:29 +0000 Subject: [PATCH 079/211] render test fixed --- ...eableBitmapShouldBeUsable_Bgra8888.expected.png} | Bin ...eableBitmapShouldBeUsable_Rgba8888.expected.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/TestFiles/Skia/Media/Bitmap/{WritableBitmapShouldBeUsable_Bgra8888.expected.png => WriteableBitmapShouldBeUsable_Bgra8888.expected.png} (100%) rename tests/TestFiles/Skia/Media/Bitmap/{WritableBitmapShouldBeUsable_Rgba8888.expected.png => WriteableBitmapShouldBeUsable_Rgba8888.expected.png} (100%) diff --git a/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png similarity index 100% rename from tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png rename to tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png diff --git a/tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png b/tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png similarity index 100% rename from tests/TestFiles/Skia/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png rename to tests/TestFiles/Skia/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png From c06ac12de709f7e337e78b29b0bced3478e4c4af Mon Sep 17 00:00:00 2001 From: ahopper Date: Thu, 8 Mar 2018 14:56:47 +0000 Subject: [PATCH 080/211] fix d2d render tests --- ...eableBitmapShouldBeUsable_Bgra8888.expected.png} | Bin ...eableBitmapShouldBeUsable_Rgba8888.expected.png} | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/TestFiles/Direct2D1/Media/Bitmap/{WritableBitmapShouldBeUsable_Bgra8888.expected.png => WriteableBitmapShouldBeUsable_Bgra8888.expected.png} (100%) rename tests/TestFiles/Direct2D1/Media/Bitmap/{WritableBitmapShouldBeUsable_Rgba8888.expected.png => WriteableBitmapShouldBeUsable_Rgba8888.expected.png} (100%) diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png similarity index 100% rename from tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Bgra8888.expected.png rename to tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Bgra8888.expected.png diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png similarity index 100% rename from tests/TestFiles/Direct2D1/Media/Bitmap/WritableBitmapShouldBeUsable_Rgba8888.expected.png rename to tests/TestFiles/Direct2D1/Media/Bitmap/WriteableBitmapShouldBeUsable_Rgba8888.expected.png From ed4a85fef992f16e5c84554c2a2c8caee8c8ffcf Mon Sep 17 00:00:00 2001 From: boombuler Date: Fri, 9 Mar 2018 16:41:41 +0100 Subject: [PATCH 081/211] Allow Winforms + WPF compatible object drag --- src/Windows/Avalonia.Win32/DataObject.cs | 51 ++++++++++++++++++++- src/Windows/Avalonia.Win32/OleDataObject.cs | 26 ++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 6248afd7bb..aa3ee91ef9 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -8,11 +8,16 @@ using Avalonia.Controls.DragDrop; using Avalonia.Win32.Interop; using IDataObject = Avalonia.Controls.DragDrop.IDataObject; using IOleDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; namespace Avalonia.Win32 { class DataObject : IDataObject, IOleDataObject { + // Compatibility with WinForms + WPF... + internal static readonly byte[] SerializedObjectGUID = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray(); + class FormatEnumerator : IEnumFORMATETC { private FORMATETC[] _formats; @@ -249,7 +254,51 @@ namespace Avalonia.Win32 return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data)); if (dataFormat == DataFormats.FileNames && data is IEnumerable files) return WriteFileListToHGlobal(ref hGlobal, files); - return DV_E_TYMED; + if (data is Stream stream) + { + byte[] buffer = new byte[stream.Length - stream.Position]; + stream.Read(buffer, 0, buffer.Length); + return WriteBytesToHGlobal(ref hGlobal, buffer); + } + if (data is IEnumerable bytes) + { + var byteArr = bytes is byte[] ? (byte[])bytes : bytes.ToArray(); + return WriteBytesToHGlobal(ref hGlobal, byteArr); + } + return WriteBytesToHGlobal(ref hGlobal, SerializeObject(data)); + } + + private byte[] SerializeObject(object data) + { + using (var ms = new MemoryStream()) + { + ms.Write(SerializedObjectGUID, 0, SerializedObjectGUID.Length); + BinaryFormatter binaryFormatter = new BinaryFormatter(); + binaryFormatter.Serialize(ms, data); + return ms.ToArray(); + } + } + + private int WriteBytesToHGlobal(ref IntPtr hGlobal, byte[] data) + { + int required = data.Length; + if (hGlobal == IntPtr.Zero) + hGlobal = UnmanagedMethods.GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, required); + + long available = UnmanagedMethods.GlobalSize(hGlobal).ToInt64(); + if (required > available) + return STG_E_MEDIUMFULL; + + IntPtr ptr = UnmanagedMethods.GlobalLock(hGlobal); + try + { + Marshal.Copy(data, 0, ptr, data.Length); + return unchecked((int)UnmanagedMethods.HRESULT.S_OK); + } + finally + { + UnmanagedMethods.GlobalUnlock(hGlobal); + } } private int WriteFileListToHGlobal(ref IntPtr hGlobal, IEnumerable files) diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index fe6faa162c..60527bb7d6 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; +using System.Runtime.Serialization.Formatters.Binary; using System.Text; using Avalonia.Controls.DragDrop; using Avalonia.Win32.Interop; @@ -62,7 +64,19 @@ namespace Avalonia.Win32 return ReadStringFromHGlobal(medium.unionmember); if (format == DataFormats.FileNames) return ReadFileNamesFromHGlobal(medium.unionmember); - return ReadBytesFromHGlobal(medium.unionmember); + + byte[] data = ReadBytesFromHGlobal(medium.unionmember); + + if (IsSerializedObject(data)) + { + using (var ms = new MemoryStream(data)) + { + ms.Position = DataObject.SerializedObjectGUID.Length; + BinaryFormatter binaryFormatter = new BinaryFormatter(); + return binaryFormatter.Deserialize(ms); + } + } + return data; } } finally @@ -73,6 +87,16 @@ namespace Avalonia.Win32 return null; } + private bool IsSerializedObject(byte[] data) + { + if (data.Length < DataObject.SerializedObjectGUID.Length) + return false; + for (int i = 0; i < DataObject.SerializedObjectGUID.Length; i++) + if (data[i] != DataObject.SerializedObjectGUID[i]) + return false; + return true; + } + private static IEnumerable ReadFileNamesFromHGlobal(IntPtr hGlobal) { List files = new List(); From cfb3924c0fa645c6256b32af93a8000a3a936b92 Mon Sep 17 00:00:00 2001 From: boombuler Date: Fri, 9 Mar 2018 16:42:06 +0100 Subject: [PATCH 082/211] Prevent setting the cursor to often. --- src/Avalonia.Controls/DragDrop/DragSource.cs | 33 +++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/DragDrop/DragSource.cs b/src/Avalonia.Controls/DragDrop/DragSource.cs index fd3d8ef143..ceb962ad98 100644 --- a/src/Avalonia.Controls/DragDrop/DragSource.cs +++ b/src/Avalonia.Controls/DragDrop/DragSource.cs @@ -25,7 +25,8 @@ namespace Avalonia.Controls.DragDrop private IDataObject _draggedData; private IInputElement _lastRoot; private Point _lastPosition; - private object _lastCursor; + private StandardCursorType _lastCursorType; + private object _originalCursor; private InputModifiers? _initialInputModifiers; public DragSource() @@ -81,15 +82,15 @@ namespace Avalonia.Controls.DragDrop return DragDropEffects.Move; } - private Cursor GetCursorForDropEffect(DragDropEffects effects) + private StandardCursorType GetCursorForDropEffect(DragDropEffects effects) { if (effects.HasFlag(DragDropEffects.Copy)) - return new Cursor(StandardCursorType.DragCopy); + return StandardCursorType.DragCopy; if (effects.HasFlag(DragDropEffects.Move)) - return new Cursor(StandardCursorType.DragMove); + return StandardCursorType.DragMove; if (effects.HasFlag(DragDropEffects.Link)) - return new Cursor(StandardCursorType.DragLink); - return new Cursor(StandardCursorType.No); + return StandardCursorType.DragLink; + return StandardCursorType.No; } private void UpdateCursor(IInputElement root, DragDropEffects effect) @@ -98,27 +99,35 @@ namespace Avalonia.Controls.DragDrop { if (_lastRoot is InputElement ieLast) { - if (_lastCursor == AvaloniaProperty.UnsetValue) + if (_originalCursor == AvaloniaProperty.UnsetValue) ieLast.ClearValue(InputElement.CursorProperty); else - ieLast.Cursor = _lastCursor as Cursor; + ieLast.Cursor = _originalCursor as Cursor; } if (root is InputElement ieNew) { if (!ieNew.IsSet(InputElement.CursorProperty)) - _lastCursor = AvaloniaProperty.UnsetValue; + _originalCursor = AvaloniaProperty.UnsetValue; else - _lastCursor = root.Cursor; + _originalCursor = root.Cursor; } else - _lastCursor = null; + _originalCursor = null; + _lastCursorType = StandardCursorType.Arrow; _lastRoot = root; } if (root is InputElement ie) - ie.Cursor = GetCursorForDropEffect(effect); + { + var ct = GetCursorForDropEffect(effect); + if (ct != _lastCursorType) + { + _lastCursorType = ct; + ie.Cursor = new Cursor(ct); + } + } } private void CancelDragging() From c2cecbb85a8436abd7a78f7a846178bb60ae258b Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Fri, 9 Mar 2018 19:28:46 +0300 Subject: [PATCH 083/211] ButtonSpinner control is ported from ExtendedWFPToolkit --- samples/ControlCatalog/ControlCatalog.csproj | 6 + samples/ControlCatalog/MainView.xaml | 1 + .../Pages/ButtonSpinnerPage.xaml | 19 ++ .../Pages/ButtonSpinnerPage.xaml.cs | 54 ++++ src/Avalonia.Controls/ButtonSpinner.cs | 235 ++++++++++++++++++ src/Avalonia.Controls/Spinner.cs | 174 +++++++++++++ .../ButtonSpinner.xaml | 54 ++++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + 8 files changed, 544 insertions(+) create mode 100644 samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml create mode 100644 samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs create mode 100644 src/Avalonia.Controls/ButtonSpinner.cs create mode 100644 src/Avalonia.Controls/Spinner.cs create mode 100644 src/Avalonia.Themes.Default/ButtonSpinner.xaml diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 37f9da0c43..a3d7a0cdce 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -35,6 +35,9 @@ Designer + + Designer + Designer @@ -164,6 +167,9 @@ ToolTipPage.xaml + + ButtonSpinnerPage.xaml + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 060369e404..142d0d42b1 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -7,6 +7,7 @@ + diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml new file mode 100644 index 0000000000..760760e3f4 --- /dev/null +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -0,0 +1,19 @@ + + + + ButtonSpinner + The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element. + + + AllowSpin + ShowButtonSpinner + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs new file mode 100644 index 0000000000..1f753ab3ea --- /dev/null +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml.cs @@ -0,0 +1,54 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ButtonSpinnerPage : UserControl + { + public ButtonSpinnerPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnSpin(object sender, SpinEventArgs e) + { + var spinner = (ButtonSpinner)sender; + var txtBox = (TextBlock)spinner.Content; + + int value = Array.IndexOf(_mountains, txtBox.Text); + if (e.Direction == SpinDirection.Increase) + value++; + else + value--; + + if (value < 0) + value = _mountains.Length - 1; + else if (value >= _mountains.Length) + value = 0; + + txtBox.Text = _mountains[value]; + } + + private readonly string[] _mountains = new[] + { + "Everest", + "K2 (Mount Godwin Austen)", + "Kangchenjunga", + "Lhotse", + "Makalu", + "Cho Oyu", + "Dhaulagiri", + "Manaslu", + "Nanga Parbat", + "Annapurna" + }; + } +} diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs new file mode 100644 index 0000000000..c7b31a8c6c --- /dev/null +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -0,0 +1,235 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Represents a spinner control that includes two Buttons. + /// + public class ButtonSpinner : Spinner + { + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowSpinProperty = + AvaloniaProperty.Register(nameof(AllowSpin), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowButtonSpinnerProperty = + AvaloniaProperty.Register(nameof(ShowButtonSpinner), true); + + private Button _decreaseButton; + /// + /// Gets or sets the DecreaseButton template part. + /// + private Button DecreaseButton + { + get { return _decreaseButton; } + set + { + if (_decreaseButton != null) + { + _decreaseButton.Click -= OnButtonClick; + } + _decreaseButton = value; + if (_decreaseButton != null) + { + _decreaseButton.Click += OnButtonClick; + } + } + } + + private Button _increaseButton; + /// + /// Gets or sets the IncreaseButton template part. + /// + private Button IncreaseButton + { + get + { + return _increaseButton; + } + set + { + if (_increaseButton != null) + { + _increaseButton.Click -= OnButtonClick; + } + _increaseButton = value; + if (_increaseButton != null) + { + _increaseButton.Click += OnButtonClick; + } + } + } + + /// + /// Initializes static members of the class. + /// + static ButtonSpinner() + { + AllowSpinProperty.Changed.Subscribe(AllowSpinChanged); + } + + /// + /// Gets or sets a value indicating whether the should allow to spin. + /// + public bool AllowSpin + { + get { return GetValue(AllowSpinProperty); } + set { SetValue(AllowSpinProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the spin buttons should be shown. + /// + public bool ShowButtonSpinner + { + get { return GetValue(ShowButtonSpinnerProperty); } + set { SetValue(ShowButtonSpinnerProperty, value); } + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + IncreaseButton = e.NameScope.Find