瀏覽代碼

Merge branch 'master' into scenegraph

Steven Kirk 9 年之前
父節點
當前提交
8ed9823c5a

+ 5 - 0
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

+ 26 - 6
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);
     }
 });
 

+ 4 - 0
samples/BindingTest/MainWindow.xaml

@@ -41,6 +41,10 @@
             <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" 
                      Text="{Binding #first.Text, Mode=TwoWay}"/>
           </StackPanel>
+          <StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
+            <TextBlock FontSize="16" Text="Scheduler"/>
+            <TextBox Watermark="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/>
+          </StackPanel>
         </StackPanel>
       </StackPanel>
     </TabItem>

+ 18 - 0
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<TestItem> 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<object> StringValueCommand { get; }
 
         public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel();

+ 8 - 2
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<IScheduler>() ?? 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);

+ 25 - 6
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
         /// <inheritdoc/>
         public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> 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;
         }
     }
 }

+ 3 - 1
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<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
                 .Bind<IStyler>().ToConstant(_styler)
                 .Bind<ILayoutManager>().ToSingleton<LayoutManager>()
-                .Bind<IApplicationLifecycle>().ToConstant(this);
+                .Bind<IApplicationLifecycle>().ToConstant(this)
+                .Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
         }
     }
 }

+ 4 - 2
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)
             {

+ 17 - 1
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);
         }
 
         /// <summary>
@@ -45,6 +45,22 @@ namespace Avalonia
             return p.Item1.PointToScreen(point + p.Item2);
         }
 
+        /// <summary>
+        /// 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
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <param name="point">The point value, as relative to this visual.</param>
+        /// <param name="relativeTo">The visual to translate the given point into.</param>
+        /// <returns>A point value, now relative to the target visual rather than this source element.</returns>
+        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);
+        }
+
         /// <summary>
         /// Gets the root of the control's visual tree and the position of the control 
         /// in the root's coordinate space.

+ 1 - 4
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());
         }
 

+ 2 - 2
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);
             }
         }
 

+ 1 - 0
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)

+ 30 - 0
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<object>();
+            var currentThreadId = Thread.CurrentThread.ManagedThreadId;
+
+            var threadingInterfaceMock = new Mock<IPlatformThreadingInterface>();
+            threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread)
+                .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId);
+
+            using (AvaloniaLocator.EnterScope())
+            {
+                AvaloniaLocator.CurrentMutable.Bind<IPlatformThreadingInterface>().ToConstant(threadingInterfaceMock.Object);
+                AvaloniaLocator.CurrentMutable.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
+
+                target.Bind(Class1.QuxProperty, source);
+
+                await Task.Run(() => source.OnNext(6.7));
+            }
+
+        }
 
         /// <summary>
         /// Returns an observable that returns a single value but does not complete.

+ 29 - 0
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<IStandardCursorFactory>());
 

+ 1 - 0
tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

@@ -99,6 +99,7 @@
     <Compile Include="AvaloniaObjectTests.cs" />
     <Compile Include="ControlTests.cs" />
     <Compile Include="ExpressionObserverTests.cs" />
+    <Compile Include="MemberSelectorTests.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>
   <ItemGroup>

+ 47 - 0
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; }
+        }
+    }
+}

+ 0 - 26
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()
         {