Browse Source

Merge branch 'master' into scenegraph

scenegraph-after-breakage
Steven Kirk 10 years ago
parent
commit
8ed9823c5a
  1. 5
      appveyor.yml
  2. 32
      build.cake
  3. 4
      samples/BindingTest/MainWindow.xaml
  4. 18
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  5. 10
      src/Avalonia.Base/AvaloniaObject.cs
  6. 31
      src/Avalonia.Base/Threading/AvaloniaScheduler.cs
  7. 4
      src/Avalonia.Controls/Application.cs
  8. 6
      src/Avalonia.Controls/TextBox.cs
  9. 18
      src/Avalonia.Visuals/VisualExtensions.cs
  10. 5
      src/Shared/PlatformSupport/StandardRuntimePlatform.cs
  11. 4
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  12. 1
      src/Windows/Avalonia.Win32/WindowImpl.cs
  13. 30
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  14. 29
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  15. 1
      tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
  16. 47
      tests/Avalonia.LeakTests/MemberSelectorTests.cs
  17. 26
      tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs

5
appveyor.yml

@ -20,6 +20,11 @@ build_script:
- ps: .\build.ps1 -Target "AppVeyor" -Platform "$env:platform" -Configuration "$env:configuration" - ps: .\build.ps1 -Target "AppVeyor" -Platform "$env:platform" -Configuration "$env:configuration"
after_build: 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" - .\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 test: off
artifacts: artifacts:
- path: artifacts\nuget\*.nupkg - path: artifacts\nuget\*.nupkg

32
build.cake

@ -10,6 +10,7 @@
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
#tool "nuget:?package=xunit.runner.console&version=2.1.0" #tool "nuget:?package=xunit.runner.console&version=2.1.0"
#tool "nuget:?package=OpenCover"
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// USINGS // USINGS
@ -92,6 +93,7 @@ var artifactsDir = (DirectoryPath)Directory("./artifacts");
var nugetRoot = artifactsDir.Combine("nuget"); var nugetRoot = artifactsDir.Combine("nuget");
var zipRoot = artifactsDir.Combine("zip"); var zipRoot = artifactsDir.Combine("zip");
var binRoot = artifactsDir.Combine("bin"); var binRoot = artifactsDir.Combine("bin");
var testsRoot = artifactsDir.Combine("tests");
var dirSuffix = configuration; var dirSuffix = configuration;
var dirSuffixSkia = (isPlatformAnyCPU ? "x86" : platform) + "/" + configuration; var dirSuffixSkia = (isPlatformAnyCPU ? "x86" : platform) + "/" + configuration;
@ -585,6 +587,7 @@ Task("Clean")
CleanDirectory(nugetRoot); CleanDirectory(nugetRoot);
CleanDirectory(zipRoot); CleanDirectory(zipRoot);
CleanDirectory(binRoot); CleanDirectory(binRoot);
CleanDirectory(testsRoot);
}); });
Task("Restore-NuGet-Packages") 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.x86.exe" :
"./tools/xunit.runner.console/tools/xunit.console.exe"; "./tools/xunit.runner.console/tools/xunit.console.exe";
var settings = new XUnit2Settings var xUnitSettings = new XUnit2Settings
{ {
ToolPath = toolPath, 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()); CopyDirectory(test.GetDirectory(), testsRoot);
XUnit2(file.FullPath, settings); }
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
samples/BindingTest/MainWindow.xaml

@ -41,6 +41,10 @@
<TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True"
Text="{Binding #first.Text, Mode=TwoWay}"/> Text="{Binding #first.Text, Mode=TwoWay}"/>
</StackPanel> </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>
</StackPanel> </StackPanel>
</TabItem> </TabItem>

18
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -3,6 +3,8 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using ReactiveUI; using ReactiveUI;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Threading;
namespace BindingTest.ViewModels namespace BindingTest.ViewModels
{ {
@ -12,6 +14,7 @@ namespace BindingTest.ViewModels
private double _doubleValue = 5.0; private double _doubleValue = 5.0;
private string _stringValue = "Simple Binding"; private string _stringValue = "Simple Binding";
private bool _booleanFlag = false; private bool _booleanFlag = false;
private string _currentTime;
public MainWindowViewModel() public MainWindowViewModel()
{ {
@ -37,6 +40,15 @@ namespace BindingTest.ViewModels
BooleanFlag = !BooleanFlag; BooleanFlag = !BooleanFlag;
StringValue = param.ToString(); StringValue = param.ToString();
}); });
Task.Run(() =>
{
while (true)
{
CurrentTime = DateTimeOffset.Now.ToString();
Thread.Sleep(1000);
}
});
} }
public ObservableCollection<TestItem> Items { get; } public ObservableCollection<TestItem> Items { get; }
@ -67,6 +79,12 @@ namespace BindingTest.ViewModels
set { this.RaiseAndSetIfChanged(ref _booleanFlag, value); } 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 ReactiveCommand<object> StringValueCommand { get; }
public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel(); public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel();

10
src/Avalonia.Base/AvaloniaObject.cs

@ -12,6 +12,7 @@ using Avalonia.Diagnostics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using System.Reactive.Concurrency;
namespace Avalonia namespace Avalonia
{ {
@ -304,6 +305,11 @@ namespace Avalonia
VerifyAccess(); VerifyAccess();
var description = GetDescription(source);
var scheduler = AvaloniaLocator.Current.GetService<IScheduler>() ?? ImmediateScheduler.Instance;
source = source.ObserveOn(scheduler);
if (property.IsDirect) if (property.IsDirect)
{ {
if (property.IsReadOnly) if (property.IsReadOnly)
@ -316,7 +322,7 @@ namespace Avalonia
this, this,
"Bound {Property} to {Binding} with priority LocalValue", "Bound {Property} to {Binding} with priority LocalValue",
property, property,
GetDescription(source)); description);
IDisposable subscription = null; IDisposable subscription = null;
@ -358,7 +364,7 @@ namespace Avalonia
this, this,
"Bound {Property} to {Binding} with priority {Priority}", "Bound {Property} to {Binding} with priority {Priority}",
property, property,
GetDescription(source), description,
priority); priority);
return v.Add(source, (int)priority); return v.Add(source, (int)priority);

31
src/Avalonia.Base/Threading/AvaloniaScheduler.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Reactive.Concurrency; using System.Reactive.Concurrency;
using System.Reactive.Disposables;
namespace Avalonia.Threading namespace Avalonia.Threading
{ {
@ -26,13 +27,31 @@ namespace Avalonia.Threading
/// <inheritdoc/> /// <inheritdoc/>
public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action) 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); var cancellation = new CancellationDisposable();
return false; Dispatcher.UIThread.InvokeAsync(() =>
}, {
dueTime); 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;
} }
} }
} }

4
src/Avalonia.Controls/Application.cs

@ -11,6 +11,7 @@ using Avalonia.Layout;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading; using Avalonia.Threading;
using System.Reactive.Concurrency;
namespace Avalonia namespace Avalonia
{ {
@ -175,7 +176,8 @@ namespace Avalonia
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>() .Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
.Bind<IStyler>().ToConstant(_styler) .Bind<IStyler>().ToConstant(_styler)
.Bind<ILayoutManager>().ToSingleton<LayoutManager>() .Bind<ILayoutManager>().ToSingleton<LayoutManager>()
.Bind<IApplicationLifecycle>().ToConstant(this); .Bind<IApplicationLifecycle>().ToConstant(this)
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance);
} }
} }
} }

6
src/Avalonia.Controls/TextBox.cs

@ -202,6 +202,7 @@ namespace Avalonia.Controls
{ {
if (!_ignoreTextChanges) if (!_ignoreTextChanges)
{ {
CaretIndex = CoerceCaretIndex(CaretIndex, value?.Length ?? 0);
SetAndRaise(TextProperty, ref _text, value); SetAndRaise(TextProperty, ref _text, value);
} }
} }
@ -558,10 +559,11 @@ namespace Avalonia.Controls
return null; 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 text = Text;
var length = text?.Length ?? 0;
if (value < 0) if (value < 0)
{ {

18
src/Avalonia.Visuals/VisualExtensions.cs

@ -30,7 +30,7 @@ namespace Avalonia
public static Point PointToClient(this IVisual visual, Point point) public static Point PointToClient(this IVisual visual, Point point)
{ {
var p = GetRootAndPosition(visual); var p = GetRootAndPosition(visual);
return p.Item1.PointToClient(point + p.Item2); return p.Item1.PointToClient(point - p.Item2);
} }
/// <summary> /// <summary>
@ -45,6 +45,22 @@ namespace Avalonia
return p.Item1.PointToScreen(point + p.Item2); 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> /// <summary>
/// Gets the root of the control's visual tree and the position of the control /// Gets the root of the control's visual tree and the position of the control
/// in the root's coordinate space. /// in the root's coordinate space.

5
src/Shared/PlatformSupport/StandardRuntimePlatform.cs

@ -16,10 +16,7 @@ namespace Avalonia.Shared.PlatformSupport
public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null); public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null);
public IDisposable StartSystemTimer(TimeSpan interval, Action tick) public IDisposable StartSystemTimer(TimeSpan interval, Action tick)
{ {
var timer = new Timer(delegate var timer = new Timer(_ => tick(), null, interval, interval);
{
}, null, interval, interval);
return Disposable.Create(() => timer.Dispose()); return Disposable.Create(() => timer.Dispose());
} }

4
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -522,7 +522,7 @@ namespace Avalonia.Skia
_lineHeight = mDescent - mAscent; _lineHeight = mDescent - mAscent;
// Rendering is relative to baseline // Rendering is relative to baseline
_lineOffset = -metrics.Top; _lineOffset = (-metrics.Ascent);
string subString; string subString;
@ -590,7 +590,7 @@ namespace Avalonia.Skia
else else
{ {
var lastLine = _skiaLines[_skiaLines.Count - 1]; var lastLine = _skiaLines[_skiaLines.Count - 1];
_size = new Size(maxX, lastLine.Top + lastLine.Height + lastLineDescent); _size = new Size(maxX, lastLine.Top + lastLine.Height);
} }
} }

1
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -682,6 +682,7 @@ namespace Avalonia.Win32
} }
UnmanagedMethods.ShowWindow(_hwnd, command); UnmanagedMethods.ShowWindow(_hwnd, command);
UnmanagedMethods.SetFocus(_hwnd);
} }
public void SetIcon(IWindowIconImpl icon) public void SetIcon(IWindowIconImpl icon)

30
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -11,6 +11,13 @@ using Avalonia.Data;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; 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 namespace Avalonia.Base.UnitTests
{ {
@ -356,6 +363,29 @@ namespace Avalonia.Base.UnitTests
Assert.True(called); 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> /// <summary>
/// Returns an observable that returns a single value but does not complete. /// Returns an observable that returns a single value but does not complete.

29
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // 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.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; 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( private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<IStandardCursorFactory>()); standardCursorFactory: Mock.Of<IStandardCursorFactory>());

1
tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

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

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

26
tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs

@ -9,32 +9,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Templates
{ {
public class MemberSelectorTests 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] [Fact]
public void Should_Select_Child_Property_Value() public void Should_Select_Child_Property_Value()
{ {

Loading…
Cancel
Save