diff --git a/appveyor.yml b/appveyor.yml index 2909da65b7..db7d129dd1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,6 +20,11 @@ build_script: - ps: .\build.ps1 -Target "AppVeyor" -Platform "$env:platform" -Configuration "$env:configuration" after_build: - .\packages\JetBrains.dotMemoryUnit.2.1.20150828.125449\tools\dotMemoryUnit.exe -targetExecutable="%xunit20%\xunit.console.x86.exe" -returnTargetExitCode --"tests\Avalonia.LeakTests\bin\Release\Avalonia.LeakTests.dll" + +- "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" +- pip install codecov +- codecov -f "./artifacts/coverage.xml" + test: off artifacts: - path: artifacts\nuget\*.nupkg diff --git a/build.cake b/build.cake index d128fbcc5e..a3d7cf64d2 100644 --- a/build.cake +++ b/build.cake @@ -10,6 +10,7 @@ /////////////////////////////////////////////////////////////////////////////// #tool "nuget:?package=xunit.runner.console&version=2.1.0" +#tool "nuget:?package=OpenCover" /////////////////////////////////////////////////////////////////////////////// // USINGS @@ -92,6 +93,7 @@ var artifactsDir = (DirectoryPath)Directory("./artifacts"); var nugetRoot = artifactsDir.Combine("nuget"); var zipRoot = artifactsDir.Combine("zip"); var binRoot = artifactsDir.Combine("bin"); +var testsRoot = artifactsDir.Combine("tests"); var dirSuffix = configuration; var dirSuffixSkia = (isPlatformAnyCPU ? "x86" : platform) + "/" + configuration; @@ -585,6 +587,7 @@ Task("Clean") CleanDirectory(nugetRoot); CleanDirectory(zipRoot); CleanDirectory(binRoot); + CleanDirectory(testsRoot); }); Task("Restore-NuGet-Packages") @@ -670,18 +673,35 @@ Task("Run-Unit-Tests") "./tools/xunit.runner.console/tools/xunit.console.x86.exe" : "./tools/xunit.runner.console/tools/xunit.console.exe"; - var settings = new XUnit2Settings + var xUnitSettings = new XUnit2Settings { ToolPath = toolPath, - Parallelism = ParallelismOption.None + Parallelism = ParallelismOption.None, + ShadowCopy = false }; - settings.NoAppDomain = !isRunningOnWindows; + xUnitSettings.NoAppDomain = !isRunningOnWindows; - foreach (var file in unitTests) + var openCoverOutput = artifactsDir.GetFilePath(new FilePath("./coverage.xml")); + var openCoverSettings = new OpenCoverSettings() + .WithFilter("+[Avalonia.*]* -[*Test*]* -[ControlCatalog*]*") + .WithFilter("-[Avalonia.*]OmniXaml.* -[Avalonia.*]Glass.*") + .WithFilter("-[Avalonia.HtmlRenderer]TheArtOfDev.HtmlRenderer.* +[Avalonia.HtmlRenderer]TheArtOfDev.HtmlRenderer.Avalonia.* -[Avalonia.ReactiveUI]*"); + + foreach(var test in unitTests) { - Information("Running test " + file.GetFilenameWithoutExtension()); - XUnit2(file.FullPath, settings); + CopyDirectory(test.GetDirectory(), testsRoot); + } + + if(isRunningOnWindows) + { + OpenCover(context => { + context.XUnit2(unitTests.Select(test => testsRoot.GetFilePath(test).FullPath), xUnitSettings); + }, openCoverOutput, openCoverSettings); + } + else + { + XUnit2(unitTests.Select(test => test.FullPath), xUnitSettings); } }); diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingTest/MainWindow.xaml index 02c364346d..95f671fd84 100644 --- a/samples/BindingTest/MainWindow.xaml +++ b/samples/BindingTest/MainWindow.xaml @@ -41,6 +41,10 @@ + + + + diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index 94f7ff595a..4b58bf2279 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,8 @@ using System.Collections.ObjectModel; using System.Linq; using ReactiveUI; using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Threading; namespace BindingTest.ViewModels { @@ -12,6 +14,7 @@ namespace BindingTest.ViewModels private double _doubleValue = 5.0; private string _stringValue = "Simple Binding"; private bool _booleanFlag = false; + private string _currentTime; public MainWindowViewModel() { @@ -37,6 +40,15 @@ namespace BindingTest.ViewModels BooleanFlag = !BooleanFlag; StringValue = param.ToString(); }); + + Task.Run(() => + { + while (true) + { + CurrentTime = DateTimeOffset.Now.ToString(); + Thread.Sleep(1000); + } + }); } public ObservableCollection Items { get; } @@ -67,6 +79,12 @@ namespace BindingTest.ViewModels set { this.RaiseAndSetIfChanged(ref _booleanFlag, value); } } + public string CurrentTime + { + get { return _currentTime; } + private set { this.RaiseAndSetIfChanged(ref _currentTime, value); } + } + public ReactiveCommand StringValueCommand { get; } public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel(); diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 48e937d6b2..409abfe8fa 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -12,6 +12,7 @@ using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; using Avalonia.Utilities; +using System.Reactive.Concurrency; namespace Avalonia { @@ -304,6 +305,11 @@ namespace Avalonia VerifyAccess(); + var description = GetDescription(source); + + var scheduler = AvaloniaLocator.Current.GetService() ?? ImmediateScheduler.Instance; + source = source.ObserveOn(scheduler); + if (property.IsDirect) { if (property.IsReadOnly) @@ -316,7 +322,7 @@ namespace Avalonia this, "Bound {Property} to {Binding} with priority LocalValue", property, - GetDescription(source)); + description); IDisposable subscription = null; @@ -358,7 +364,7 @@ namespace Avalonia this, "Bound {Property} to {Binding} with priority {Priority}", property, - GetDescription(source), + description, priority); return v.Add(source, (int)priority); diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index de32057a00..f9d67470c1 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Concurrency; +using System.Reactive.Disposables; namespace Avalonia.Threading { @@ -26,13 +27,31 @@ namespace Avalonia.Threading /// public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) { - return DispatcherTimer.Run( - () => + var composite = new CompositeDisposable(2); + if (dueTime == TimeSpan.Zero) + { + if (!Dispatcher.UIThread.CheckAccess()) { - action(this, state); - return false; - }, - dueTime); + var cancellation = new CancellationDisposable(); + Dispatcher.UIThread.InvokeAsync(() => + { + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add(action(this, state)); + } + }, DispatcherPriority.DataBind); + composite.Add(cancellation); + } + else + { + return action(this, state); + } + } + else + { + composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); + } + return composite; } } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 326556f629..3d13608226 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -11,6 +11,7 @@ using Avalonia.Layout; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; +using System.Reactive.Concurrency; namespace Avalonia { @@ -175,7 +176,8 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(_styler) .Bind().ToSingleton() - .Bind().ToConstant(this); + .Bind().ToConstant(this) + .Bind().ToConstant(AvaloniaScheduler.Instance); } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 7a271e8615..719e888dd4 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -202,6 +202,7 @@ namespace Avalonia.Controls { if (!_ignoreTextChanges) { + CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0); SetAndRaise(TextProperty, ref _text, value); } } @@ -558,10 +559,11 @@ namespace Avalonia.Controls return null; } - private int CoerceCaretIndex(int value) + private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0); + + private int CoerceCaretIndex(int value, int length) { var text = Text; - var length = text?.Length ?? 0; if (value < 0) { diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs index 6e55070702..d69785cc59 100644 --- a/src/Avalonia.Visuals/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualExtensions.cs @@ -30,7 +30,7 @@ namespace Avalonia public static Point PointToClient(this IVisual visual, Point point) { var p = GetRootAndPosition(visual); - return p.Item1.PointToClient(point + p.Item2); + return p.Item1.PointToClient(point - p.Item2); } /// @@ -45,6 +45,22 @@ namespace Avalonia return p.Item1.PointToScreen(point + p.Item2); } + /// + /// Translates a point relative to this visual to coordinates that are relative to the specified visual. + /// The visual and relativeTo should be descendants of the same root window + /// + /// The visual. + /// The point value, as relative to this visual. + /// The visual to translate the given point into. + /// A point value, now relative to the target visual rather than this source element. + public static Point TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) + { + var pos = GetRootAndPosition(visual); + var relToPos = GetRootAndPosition(relativeTo); + + return point - (relToPos.Item2 - pos.Item2); + } + /// /// Gets the root of the control's visual tree and the position of the control /// in the root's coordinate space. diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index e5ede1c6b4..5c02be7d5b 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -16,10 +16,7 @@ namespace Avalonia.Shared.PlatformSupport public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null); public IDisposable StartSystemTimer(TimeSpan interval, Action tick) { - var timer = new Timer(delegate - { - - }, null, interval, interval); + var timer = new Timer(_ => tick(), null, interval, interval); return Disposable.Create(() => timer.Dispose()); } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index e24b6053b0..51f4b2b724 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -522,7 +522,7 @@ namespace Avalonia.Skia _lineHeight = mDescent - mAscent; // Rendering is relative to baseline - _lineOffset = -metrics.Top; + _lineOffset = (-metrics.Ascent); string subString; @@ -590,7 +590,7 @@ namespace Avalonia.Skia else { var lastLine = _skiaLines[_skiaLines.Count - 1]; - _size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent); + _size = new Size(maxX, lastLine.Top + lastLine.Height); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 65e10cafda..5db3f69f85 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -682,6 +682,7 @@ namespace Avalonia.Win32 } UnmanagedMethods.ShowWindow(_hwnd, command); + UnmanagedMethods.SetFocus(_hwnd); } public void SetIcon(IWindowIconImpl icon) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 66fe3c7767..5e286305d2 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -11,6 +11,13 @@ using Avalonia.Data; using Avalonia.Logging; using Avalonia.UnitTests; using Xunit; +using System.Threading.Tasks; +using Avalonia.Platform; +using System.Threading; +using Moq; +using System.Reactive.Disposables; +using System.Reactive.Concurrency; +using Avalonia.Threading; namespace Avalonia.Base.UnitTests { @@ -356,6 +363,29 @@ namespace Avalonia.Base.UnitTests Assert.True(called); } } + + [Fact] + public async void Bind_With_Scheduler_Executes_On_Scheduler() + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + using (AvaloniaLocator.EnterScope()) + { + AvaloniaLocator.CurrentMutable.Bind().ToConstant(threadingInterfaceMock.Object); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(AvaloniaScheduler.Instance); + + target.Bind(Class1.QuxProperty, source); + + await Task.Run(() => source.OnNext(6.7)); + } + + } /// /// Returns an observable that returns a single value but does not complete. diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index cff49bc32e..26fc2a2461 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.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; +using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -200,6 +202,33 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_Text_Updates_CaretPosition() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Text = "Initial Text", + CaretIndex = 11 + }; + + var invoked = false; + + target.GetObservable(TextBox.TextProperty).Skip(1).Subscribe(_ => + { + // Caret index should be set before Text changed notification, as we don't want + // to notify with an invalid CaretIndex. + Assert.Equal(7, target.CaretIndex); + invoked = true; + }); + + target.Text = "Changed"; + + Assert.True(invoked); + } + } + private static TestServices Services => TestServices.MockThreadingInterface.With( standardCursorFactory: Mock.Of()); diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index b3734255d6..31e7d5ebea 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -99,6 +99,7 @@ + diff --git a/tests/Avalonia.LeakTests/MemberSelectorTests.cs b/tests/Avalonia.LeakTests/MemberSelectorTests.cs new file mode 100644 index 0000000000..d794e788fd --- /dev/null +++ b/tests/Avalonia.LeakTests/MemberSelectorTests.cs @@ -0,0 +1,47 @@ +using Avalonia.Markup.Xaml.Templates; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Avalonia.LeakTests +{ + public class MemberSelectorTests + { + [Fact] + public void Should_Not_Hold_Reference_To_Object() + { + WeakReference dataRef = null; + + var selector = new MemberSelector() { MemberName = "Child.StringValue" }; + + Action run = () => + { + var data = new Item() + { + Child = new Item() { StringValue = "Value1" } + }; + + Assert.Same("Value1", selector.Select(data)); + + dataRef = new WeakReference(data); + }; + + run(); + + GC.Collect(); + + Assert.False(dataRef.IsAlive); + } + + private class Item + { + public Item Child { get; set; } + public int IntValue { get; set; } + + public string StringValue { get; set; } + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs index 7082863c0d..49a88e8fae 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs @@ -9,32 +9,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Templates { public class MemberSelectorTests { - [Fact] - public void Should_Not_Hold_Reference_To_Object() - { - WeakReference dataRef = null; - - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - Action run = () => - { - var data = new Item() - { - Child = new Item() { StringValue = "Value1" } - }; - - Assert.Same("Value1", selector.Select(data)); - - dataRef = new WeakReference(data); - }; - - run(); - - GC.Collect(); - - Assert.False(dataRef.IsAlive); - } - [Fact] public void Should_Select_Child_Property_Value() {