Browse Source

Merge branch 'master' into infra/artifacts_controlcatalog

pull/5481/head
Max Katz 5 years ago
committed by GitHub
parent
commit
54a4a040c4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      build/ReactiveUI.props
  2. 2
      native/Avalonia.Native/src/OSX/common.h
  3. 4
      native/Avalonia.Native/src/OSX/main.mm
  4. 9
      native/Avalonia.Native/src/OSX/menu.h
  5. 34
      native/Avalonia.Native/src/OSX/menu.mm
  6. 7
      native/Avalonia.Native/src/OSX/window.mm
  7. 4
      nukebuild/Build.cs
  8. 2
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  9. 2
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  10. 4
      samples/ControlCatalog/MainWindow.xaml
  11. 47
      src/Android/Avalonia.Android/ActivityTracker.cs
  12. 32
      src/Android/Avalonia.Android/AndroidPlatform.cs
  13. 15
      src/Android/Avalonia.Android/AndroidThreadingInterface.cs
  14. 2
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  15. 11
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  16. 35
      src/Android/Avalonia.Android/AvaloniaView.cs
  17. 101
      src/Android/Avalonia.Android/ChoreographerTimer.cs
  18. 17
      src/Android/Avalonia.Android/CursorFactory.cs
  19. 6
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  20. 13
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  21. 14
      src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
  22. 6
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  23. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
  24. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  25. 48
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  26. 91
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  27. 11
      src/Android/Avalonia.Android/app.config
  28. 4
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  29. 2
      src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
  30. 28
      src/Avalonia.Animation/Animatable.cs
  31. 9
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  32. 9
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  33. 2
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  34. 49
      src/Avalonia.Base/ValueStore.cs
  35. 12
      src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
  36. 4
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  37. 5
      src/Avalonia.Controls/ApiCompatBaseline.txt
  38. 8
      src/Avalonia.Controls/AutoCompleteBox.cs
  39. 1
      src/Avalonia.Controls/Button.cs
  40. 2
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  41. 22
      src/Avalonia.Controls/NativeControlHost.cs
  42. 33
      src/Avalonia.Controls/NativeMenu.cs
  43. 16
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  44. 10
      src/Avalonia.Controls/NativeMenuItemSeperator.cs
  45. 18
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  46. 2
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  47. 22
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  48. 4
      src/Avalonia.Controls/Slider.cs
  49. 43
      src/Avalonia.Controls/TextBox.cs
  50. 2
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  51. 2
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  52. 26
      src/Avalonia.Native/IAvnMenu.cs
  53. 9
      src/Avalonia.Native/avn.idl
  54. 18
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  55. 2
      src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
  56. 5
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs
  57. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  58. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  59. 3
      src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs
  60. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs
  61. 8
      src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
  62. 8
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  63. 31
      tests/Avalonia.Animation.UnitTests/AnimatableTests.cs
  64. 122
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  65. 32
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  66. 64
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  67. 29
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  68. 26
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  69. 95
      tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs
  70. 62
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  71. 25
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

2
build/ReactiveUI.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="ReactiveUI" Version="12.1.1" /> <PackageReference Include="ReactiveUI" Version="13.2.10" />
</ItemGroup> </ItemGroup>
</Project> </Project>

2
native/Avalonia.Native/src/OSX/common.h

@ -23,7 +23,7 @@ extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay(); extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeperator(); extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
extern IAvnMenu* GetAppMenu (); extern IAvnMenu* GetAppMenu ();

4
native/Avalonia.Native/src/OSX/main.mm

@ -253,9 +253,9 @@ public:
return S_OK; return S_OK;
} }
virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override virtual HRESULT CreateMenuItemSeparator (IAvnMenuItem** ppv) override
{ {
*ppv = ::CreateAppMenuItemSeperator(); *ppv = ::CreateAppMenuItemSeparator();
return S_OK; return S_OK;
} }

9
native/Avalonia.Native/src/OSX/menu.h

@ -31,13 +31,13 @@ private:
NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
IAvnActionCallback* _callback; IAvnActionCallback* _callback;
IAvnPredicateCallback* _predicate; IAvnPredicateCallback* _predicate;
bool _isSeperator; bool _isSeparator;
bool _isCheckable; bool _isCheckable;
public: public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
AvnAppMenuItem(bool isSeperator); AvnAppMenuItem(bool isSeparator);
NSMenuItem* GetNative(); NSMenuItem* GetNative();
@ -60,7 +60,6 @@ public:
void RaiseOnClicked(); void RaiseOnClicked();
}; };
class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu> class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
{ {
private: private:
@ -71,10 +70,12 @@ public:
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
AvnAppMenu(IAvnMenuEvents* events); AvnAppMenu(IAvnMenuEvents* events);
AvnMenu* GetNative(); AvnMenu* GetNative();
void RaiseNeedsUpdate (); void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override; virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;

34
native/Avalonia.Native/src/OSX/menu.mm

@ -71,12 +71,12 @@
} }
@end @end
AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) AvnAppMenuItem::AvnAppMenuItem(bool isSeparator)
{ {
_isCheckable = false; _isCheckable = false;
_isSeperator = isSeperator; _isSeparator = isSeparator;
if(isSeperator) if(isSeparator)
{ {
_native = [NSMenuItem separatorItem]; _native = [NSMenuItem separatorItem];
} }
@ -298,6 +298,23 @@ void AvnAppMenu::RaiseNeedsUpdate()
} }
} }
void AvnAppMenu::RaiseOpening()
{
if(_baseEvents != nullptr)
{
_baseEvents->Opening();
}
}
void AvnAppMenu::RaiseClosed()
{
if(_baseEvents != nullptr)
{
_baseEvents->Closed();
}
}
HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
{ {
@autoreleasepool @autoreleasepool
@ -382,6 +399,15 @@ HRESULT AvnAppMenu::Clear()
_parent->RaiseNeedsUpdate(); _parent->RaiseNeedsUpdate();
} }
- (void)menuWillOpen:(NSMenu *)menu
{
_parent->RaiseOpening();
}
- (void)menuDidClose:(NSMenu *)menu
{
_parent->RaiseClosed();
}
@end @end
@ -401,7 +427,7 @@ extern IAvnMenuItem* CreateAppMenuItem()
} }
} }
extern IAvnMenuItem* CreateAppMenuItemSeperator() extern IAvnMenuItem* CreateAppMenuItemSeparator()
{ {
@autoreleasepool @autoreleasepool
{ {

7
native/Avalonia.Native/src/OSX/window.mm

@ -2231,9 +2231,12 @@ protected:
{ {
@autoreleasepool @autoreleasepool
{ {
[Window setContentSize:NSSize{x, y}]; if (Window != nullptr)
{
[Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))]; [Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
}
return S_OK; return S_OK;
} }

4
nukebuild/Build.cs

@ -89,10 +89,6 @@ partial class Build : NukeBuild
Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit(); Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit();
} }
ExecWait("dotnet version:", "dotnet", "--version"); ExecWait("dotnet version:", "dotnet", "--version");
if (Parameters.IsRunningOnUnix)
ExecWait("Mono version:", "mono", "--version");
} }
IReadOnlyCollection<Output> MsBuildCommon( IReadOnlyCollection<Output> MsBuildCommon(

2
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile> <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies> <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk> <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion> <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

2
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
<uses-sdk android:targetSdkVersion="29" /> <uses-sdk android:targetSdkVersion="30" />
<application android:label="ControlCatalog.Android"></application> <application android:label="ControlCatalog.Android"></application>
</manifest> </manifest>

4
samples/ControlCatalog/MainWindow.xaml

@ -18,11 +18,11 @@
<NativeMenuItem Header="File"> <NativeMenuItem Header="File">
<NativeMenu> <NativeMenu>
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/> <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/> <NativeMenuItemSeperator/><!-- Uses incorrect spelling to demonstrate backwards compatibility -->
<NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent"> <NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenu/> <NativeMenu/>
</NativeMenuItem> </NativeMenuItem>
<NativeMenuItemSeperator/> <NativeMenuItemSeparator/>
<NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}" <NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}" Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" /> Clicked="OnCloseClicked" />

47
src/Android/Avalonia.Android/ActivityTracker.cs

@ -1,47 +0,0 @@
using Android.App;
using Android.OS;
namespace Avalonia.Android
{
internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks
{
public static Activity Current { get; private set; }
public void OnActivityCreated(Activity activity, Bundle savedInstanceState)
{
Current = activity;
}
public void OnActivityDestroyed(Activity activity)
{
if (Current == activity)
Current = null;
}
public void OnActivityPaused(Activity activity)
{
if (Current == activity)
Current = null;
}
public void OnActivityResumed(Activity activity)
{
Current = activity;
}
public void OnActivitySaveInstanceState(Activity activity, Bundle outState)
{
Current = activity;
}
public void OnActivityStarted(Activity activity)
{
Current = activity;
}
public void OnActivityStopped(Activity activity)
{
if (Current == activity)
Current = null;
}
}
}

32
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -29,60 +29,42 @@ namespace Avalonia
namespace Avalonia.Android namespace Avalonia.Android
{ {
class AndroidPlatform : IPlatformSettings, IWindowingPlatform class AndroidPlatform : IPlatformSettings
{ {
public static readonly AndroidPlatform Instance = new AndroidPlatform(); public static readonly AndroidPlatform Instance = new AndroidPlatform();
public static AndroidPlatformOptions Options { get; private set; }
public Size DoubleClickSize => new Size(4, 4); public Size DoubleClickSize => new Size(4, 4);
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200); public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200);
public double RenderScalingFactor => _scalingFactor;
public double LayoutScalingFactor => _scalingFactor;
private readonly double _scalingFactor = 1;
public AndroidPlatform()
{
_scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
}
public static void Initialize(Type appType, AndroidPlatformOptions options) public static void Initialize(Type appType, AndroidPlatformOptions options)
{ {
Options = options;
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToTransient<ClipboardImpl>() .Bind<IClipboard>().ToTransient<ClipboardImpl>()
.Bind<IStandardCursorFactory>().ToTransient<CursorFactory>() .Bind<ICursorFactory>().ToTransient<CursorFactory>()
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>() .Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToConstant(Instance) .Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface()) .Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>() .Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>() .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60)) .Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(new RenderLoop()) .Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly)); .Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly));
SkiaPlatform.Initialize(); SkiaPlatform.Initialize();
((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
.RegisterActivityLifecycleCallbacks(new ActivityTracker());
if (options.UseGpu) if (options.UseGpu)
{ {
EglPlatformOpenGlInterface.TryInitialize(); EglPlatformOpenGlInterface.TryInitialize();
} }
} }
public IWindowImpl CreateWindow()
{
throw new NotSupportedException();
}
public IWindowImpl CreateEmbeddableWindow()
{
throw new NotSupportedException();
}
} }
public sealed class AndroidPlatformOptions public sealed class AndroidPlatformOptions
{ {
public bool UseDeferredRendering { get; set; } = true;
public bool UseGpu { get; set; } = true; public bool UseGpu { get; set; } = true;
} }
} }

15
src/Android/Avalonia.Android/AndroidThreadingInterface.cs

@ -1,25 +1,26 @@
using System; using System;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Threading; using System.Threading;
using Android.OS; using Android.OS;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using App = Android.App.Application;
namespace Avalonia.Android namespace Avalonia.Android
{ {
class AndroidThreadingInterface : IPlatformThreadingInterface internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
{ {
private Handler _handler; private Handler _handler;
public AndroidThreadingInterface() public AndroidThreadingInterface()
{ {
_handler = new Handler(global::Android.App.Application.Context.MainLooper); _handler = new Handler(App.Context.MainLooper);
} }
public void RunLoop(CancellationToken cancellationToken) public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
{
return;
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{ {
@ -57,7 +58,7 @@ namespace Avalonia.Android
}); });
} }
}, null, TimeSpan.Zero, interval); }, null, TimeSpan.Zero, interval);
return Disposable.Create(() => return Disposable.Create(() =>
{ {
lock (l) lock (l)

2
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -1,6 +1,6 @@
<Project Sdk="MSBuild.Sdk.Extras"> <Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup> <PropertyGroup>
<TargetFramework>monoandroid90</TargetFramework> <TargetFramework>monoandroid11.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

11
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -1,4 +1,3 @@
using Android.App; using Android.App;
using Android.OS; using Android.OS;
using Android.Views; using Android.Views;
@ -7,15 +6,13 @@ namespace Avalonia.Android
{ {
public abstract class AvaloniaActivity : Activity public abstract class AvaloniaActivity : Activity
{ {
internal AvaloniaView View; internal AvaloniaView View;
object _content; object _content;
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
{ {
RequestWindowFeature(WindowFeatures.NoTitle);
View = new AvaloniaView(this); View = new AvaloniaView(this);
if(_content != null) if (_content != null)
View.Content = _content; View.Content = _content;
SetContentView(View); SetContentView(View);
TakeKeyEvents(true); TakeKeyEvents(true);
@ -36,9 +33,7 @@ namespace Avalonia.Android
} }
} }
public override bool DispatchKeyEvent(KeyEvent e) public override bool DispatchKeyEvent(KeyEvent e) =>
{ View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e);
return View.DispatchKeyEvent(e);
}
} }
} }

35
src/Android/Avalonia.Android/AvaloniaView.cs

@ -1,11 +1,12 @@
using System; using System;
using Android.Content; using Android.Content;
using Android.Runtime;
using Android.Views; using Android.Views;
using Android.Widget; using Android.Widget;
using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Embedding; using Avalonia.Controls.Embedding;
using Avalonia.Platform; using Avalonia.Rendering;
namespace Avalonia.Android namespace Avalonia.Android
{ {
@ -14,6 +15,8 @@ namespace Avalonia.Android
private readonly EmbeddableControlRoot _root; private readonly EmbeddableControlRoot _root;
private readonly ViewImpl _view; private readonly ViewImpl _view;
private IDisposable? _timerSubscription;
public AvaloniaView(Context context) : base(context) public AvaloniaView(Context context) : base(context)
{ {
_view = new ViewImpl(context); _view = new ViewImpl(context);
@ -33,6 +36,36 @@ namespace Avalonia.Android
return _view.View.DispatchKeyEvent(e); return _view.View.DispatchKeyEvent(e);
} }
public override void OnVisibilityAggregated(bool isVisible)
{
base.OnVisibilityAggregated(isVisible);
OnVisibilityChanged(isVisible);
}
protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility)
{
base.OnVisibilityChanged(changedView, visibility);
OnVisibilityChanged(visibility == ViewStates.Visible);
}
private void OnVisibilityChanged(bool isVisible)
{
if (isVisible)
{
if (AvaloniaLocator.Current.GetService<IRenderTimer>() is ChoreographerTimer timer)
{
_timerSubscription = timer.SubscribeView(this);
}
_root.Renderer.Start();
}
else
{
_root.Renderer.Stop();
_timerSubscription?.Dispose();
}
}
class ViewImpl : TopLevelImpl class ViewImpl : TopLevelImpl
{ {
public ViewImpl(Context context) : base(context) public ViewImpl(Context context) : base(context)

101
src/Android/Avalonia.Android/ChoreographerTimer.cs

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Android.OS;
using Android.Views;
using Avalonia.Rendering;
using Java.Lang;
namespace Avalonia.Android
{
internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback
{
private readonly object _lock = new object();
private readonly Thread _thread;
private readonly TaskCompletionSource<Choreographer> _choreographer = new TaskCompletionSource<Choreographer>();
private readonly ISet<AvaloniaView> _views = new HashSet<AvaloniaView>();
private Action<TimeSpan> _tick;
private int _count;
public ChoreographerTimer()
{
_thread = new Thread(Loop);
_thread.Start();
}
public event Action<TimeSpan> Tick
{
add
{
lock (_lock)
{
_tick += value;
_count++;
if (_count == 1)
{
_choreographer.Task.Result.PostFrameCallback(this);
}
}
}
remove
{
lock (_lock)
{
_tick -= value;
_count--;
}
}
}
internal IDisposable SubscribeView(AvaloniaView view)
{
lock (_lock)
{
_views.Add(view);
if (_views.Count == 1)
{
_choreographer.Task.Result.PostFrameCallback(this);
}
}
return Disposable.Create(
() =>
{
lock (_lock)
{
_views.Remove(view);
}
}
);
}
private void Loop()
{
Looper.Prepare();
_choreographer.SetResult(Choreographer.Instance);
Looper.Loop();
}
public void DoFrame(long frameTimeNanos)
{
_tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100));
lock (_lock)
{
if (_count > 0 && _views.Count > 0)
{
Choreographer.Instance.PostFrameCallback(this);
}
}
}
}
}

17
src/Android/Avalonia.Android/CursorFactory.cs

@ -1,12 +1,21 @@
using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Android namespace Avalonia.Android
{ {
internal class CursorFactory : IStandardCursorFactory internal class CursorFactory : ICursorFactory
{ {
public IPlatformHandle GetCursor(StandardCursorType cursorType) public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor;
=> new PlatformHandle(IntPtr.Zero, "ZeroCursor");
public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor;
private sealed class CursorImpl : ICursorImpl
{
public static CursorImpl ZeroCursor { get; } = new CursorImpl();
private CursorImpl() { }
public void Dispose() { }
}
} }
} }

6
src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs

@ -1,6 +1,4 @@
using System.Linq; using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces; using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Android.OpenGL namespace Avalonia.Android.OpenGL
@ -17,7 +15,7 @@ namespace Avalonia.Android.OpenGL
} }
public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle)); new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info) public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
{ {

13
src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs

@ -1,23 +1,30 @@
using Avalonia.OpenGL.Egl; using System;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces; using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Android.OpenGL namespace Avalonia.Android.OpenGL
{ {
internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo
{ {
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglSurface _surface; private readonly EglSurface _surface;
private readonly IntPtr _handle;
public GlRenderTarget( public GlRenderTarget(
EglPlatformOpenGlInterface egl, EglPlatformOpenGlInterface egl,
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
EglSurface surface) EglSurface surface,
IntPtr handle)
: base(egl) : base(egl)
{ {
_info = info; _info = info;
_surface = surface; _surface = surface;
_handle = handle;
} }
public bool IsCorrupted => _handle != _info.Handle;
public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
} }
} }

14
src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs

@ -1,14 +0,0 @@
using Avalonia.Input;
namespace Avalonia.Android.Platform.Input
{
public class AndroidMouseDevice : MouseDevice
{
public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice();
public AndroidMouseDevice()
{
}
}
}

6
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@ -10,7 +10,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{ {
private IntPtr _window; private IntPtr _window;
public AndroidFramebuffer(Surface surface) public AndroidFramebuffer(Surface surface, double scaling)
{ {
if(surface == null) if(surface == null)
throw new ArgumentNullException(nameof(surface)); throw new ArgumentNullException(nameof(surface));
@ -31,6 +31,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4); RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
Address = buffer.bits; Address = buffer.bits;
Dpi = scaling * new Vector(96, 96);
} }
public void Dispose() public void Dispose()
@ -44,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IntPtr Address { get; set; } public IntPtr Address { get; set; }
public PixelSize Size { get; } public PixelSize Size { get; }
public int RowBytes { get; } public int RowBytes { get; }
public Vector Dpi { get; } = new Vector(96, 96); public Vector Dpi { get; }
public PixelFormat Format { get; } public PixelFormat Format { get; }
[DllImport("android")] [DllImport("android")]

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs

@ -12,6 +12,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_topLevel = topLevel; _topLevel = topLevel;
} }
public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface); public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling);
} }
} }

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@ -43,11 +43,13 @@ namespace Avalonia.Android
} }
} }
[Obsolete("deprecated")]
public override void Invalidate(global::Android.Graphics.Rect dirty) public override void Invalidate(global::Android.Graphics.Rect dirty)
{ {
Invalidate(); Invalidate();
} }
[Obsolete("deprecated")]
public override void Invalidate(int l, int t, int r, int b) public override void Invalidate(int l, int t, int r, int b)
{ {
Invalidate(); Invalidate();

48
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -6,7 +6,6 @@ using Android.Runtime;
using Android.Views; using Android.Views;
using Avalonia.Android.OpenGL; using Avalonia.Android.OpenGL;
using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Controls; using Avalonia.Controls;
@ -35,16 +34,16 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view = new ViewImpl(context, this, placeOnTop); _view = new ViewImpl(context, this, placeOnTop);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this); _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot, _touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
p => GetAvaloniaPointFromEvent(p)); GetAvaloniaPointFromEvent);
_gl = GlPlatformSurface.TryCreate(this); _gl = GlPlatformSurface.TryCreate(this);
_framebuffer = new FramebufferManager(this); _framebuffer = new FramebufferManager(this);
MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels, RenderScaling = (int)_view.Resources.DisplayMetrics.Density;
_view.Resources.DisplayMetrics.HeightPixels);
}
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
}
private bool _handleEvents; private bool _handleEvents;
@ -58,25 +57,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform
} }
} }
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e) => new Point(e.GetX(), e.GetY()); public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
public IInputRoot InputRoot { get; private set; } public IInputRoot InputRoot { get; private set; }
public virtual Size ClientSize public virtual Size ClientSize => Size.ToSize(RenderScaling);
{
get
{
if (_view == null)
return new Size(0, 0);
return new Size(_view.Width, _view.Height);
}
set
{
}
}
public IMouseDevice MouseDevice => AndroidMouseDevice.Instance; public IMouseDevice MouseDevice { get; } = new MouseDevice();
public Action Closed { get; set; } public Action Closed { get; set; }
@ -98,10 +86,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer }; public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer };
public IRenderer CreateRenderer(IRenderRoot root) public IRenderer CreateRenderer(IRenderRoot root) =>
{ AndroidPlatform.Options.UseDeferredRendering
return new ImmediateRenderer(root); ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
} : new ImmediateRenderer(root);
public virtual void Hide() public virtual void Hide()
{ {
@ -115,15 +103,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Point PointToClient(PixelPoint point) public Point PointToClient(PixelPoint point)
{ {
return point.ToPoint(1); return point.ToPoint(RenderScaling);
} }
public PixelPoint PointToScreen(Point point) public PixelPoint PointToScreen(Point point)
{ {
return PixelPoint.FromPoint(point, 1); return PixelPoint.FromPoint(point, RenderScaling);
} }
public void SetCursor(IPlatformHandle cursor) public void SetCursor(ICursorImpl cursor)
{ {
//still not implemented //still not implemented
} }
@ -138,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Visibility = ViewStates.Visible; _view.Visibility = ViewStates.Visible;
} }
public double RenderScaling => 1; public double RenderScaling { get; }
void Draw() void Draw()
{ {
@ -193,7 +181,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height) void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{ {
var newSize = new Size(width, height); var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
if (newSize != _oldSize) if (newSize != _oldSize)
{ {

91
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@ -11,7 +11,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
private TView _view; private TView _view;
public bool HandleEvents { get; set; } public bool HandleEvents { get; set; }
public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, Point> getPointfunc) public AndroidTouchEventsHelper(TView view, Func<IInputRoot> getInputRoot, Func<MotionEvent, int, Point> getPointfunc)
{ {
this._view = view; this._view = view;
HandleEvents = true; HandleEvents = true;
@ -19,11 +19,9 @@ namespace Avalonia.Android.Platform.Specific.Helpers
_getInputRoot = getInputRoot; _getInputRoot = getInputRoot;
} }
private DateTime _lastTouchMoveEventTime = DateTime.Now; private TouchDevice _touchDevice = new TouchDevice();
private Point? _lastTouchMovePoint; private Func<MotionEvent, int, Point> _getPointFunc;
private Func<MotionEvent, Point> _getPointFunc;
private Func<IInputRoot> _getInputRoot; private Func<IInputRoot> _getInputRoot;
private Point _point;
public bool? DispatchTouchEvent(MotionEvent e, out bool callBase) public bool? DispatchTouchEvent(MotionEvent e, out bool callBase)
{ {
@ -33,89 +31,44 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return null; return null;
} }
RawPointerEventType? mouseEventType = null;
var eventTime = DateTime.Now; var eventTime = DateTime.Now;
//Basic touch support //Basic touch support
switch (e.Action) var pointerEventType = e.Action switch
{ {
case MotionEventActions.Move: MotionEventActions.Down => RawPointerEventType.TouchBegin,
//may be bot flood the evnt system with too many event especially on not so powerfull mobile devices MotionEventActions.Up => RawPointerEventType.TouchEnd,
if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10) MotionEventActions.Cancel => RawPointerEventType.TouchCancel,
{ _ => RawPointerEventType.TouchUpdate
mouseEventType = RawPointerEventType.Move; };
}
break;
case MotionEventActions.Down:
mouseEventType = RawPointerEventType.LeftButtonDown;
break; if (e.Action.HasFlag(MotionEventActions.PointerDown))
{
pointerEventType = RawPointerEventType.TouchBegin;
}
case MotionEventActions.Up: if (e.Action.HasFlag(MotionEventActions.PointerUp))
mouseEventType = RawPointerEventType.LeftButtonUp; {
break; pointerEventType = RawPointerEventType.TouchEnd;
} }
if (mouseEventType != null) for (int i = 0; i < e.PointerCount; i++)
{ {
//if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event //if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event
_point = _getPointFunc(e); var point = _getPointFunc(e, i);
double x = _view.View.GetX(); double x = _view.View.GetX();
double y = _view.View.GetY(); double y = _view.View.GetY();
double r = x + _view.View.Width; double r = x + _view.View.Width;
double b = y + _view.View.Height; double b = y + _view.View.Height;
if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y) if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y)
{ {
var inputRoot = _getInputRoot(); var inputRoot = _getInputRoot();
var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance;
//in order the controls to work in a predictable way
//we need to generate mouse move before first mouse down event
//as this is the way buttons are working every time
//otherwise there is a problem sometimes
if (mouseEventType == RawPointerEventType.LeftButtonDown)
{
var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
RawPointerEventType.Move, _point, RawInputModifiers.None);
_view.Input(me);
}
var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot, var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot,
mouseEventType.Value, _point, RawInputModifiers.LeftMouseButton); i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i));
_view.Input(mouseEvent); _view.Input(mouseEvent);
if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null)
{
if (_lastTouchMovePoint != null)
{
//raise mouse scroll event so the scrollers
//are moving with the cursor
double vectorX = _point.X - _lastTouchMovePoint.Value.X;
double vectorY = _point.Y - _lastTouchMovePoint.Value.Y;
//based on test correction of 0.02 is working perfect
double correction = 0.02;
var ps = AndroidPlatform.Instance.LayoutScalingFactor;
var mouseWheelEvent = new RawMouseWheelEventArgs(
mouseDevice,
(uint)eventTime.Ticks,
inputRoot,
_point,
new Vector(vectorX * correction / ps, vectorY * correction / ps), RawInputModifiers.LeftMouseButton);
_view.Input(mouseWheelEvent);
}
_lastTouchMovePoint = _point;
_lastTouchMoveEventTime = eventTime;
}
else if (e.Action == MotionEventActions.Down)
{
_lastTouchMovePoint = _point;
}
else
{
_lastTouchMovePoint = null;
}
} }
} }

11
src/Android/Avalonia.Android/app.config

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.InteropServices.WindowsRuntime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

4
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile> <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies> <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk> <AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion> <TargetFrameworkVersion>v11.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest> <AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -150,4 +150,4 @@
<Import Project="..\..\..\build\System.Memory.props" /> <Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\AndroidWorkarounds.props" /> <Import Project="..\..\..\build\AndroidWorkarounds.props" />
<Import Project="..\..\..\build\LegacyProject.targets" /> <Import Project="..\..\..\build\LegacyProject.targets" />
</Project> </Project>

2
src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto">
<uses-sdk android:targetSdkVersion="29" /> <uses-sdk android:targetSdkVersion="30" />
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application> <application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

28
src/Avalonia.Animation/Animatable.cs

@ -2,6 +2,7 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Data; using Avalonia.Data;
#nullable enable #nullable enable
@ -93,16 +94,35 @@ namespace Avalonia.Animation
var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>(); var oldTransitions = change.OldValue.GetValueOrDefault<Transitions>();
var newTransitions = change.NewValue.GetValueOrDefault<Transitions>(); var newTransitions = change.NewValue.GetValueOrDefault<Transitions>();
// When transitions are replaced, we add the new transitions before removing the old
// transitions, so that when the old transition being disposed causes the value to
// change, there is a corresponding entry in `_transitionStates`. This means that we
// need to account for any transitions present in both the old and new transitions
// collections.
if (newTransitions is object) if (newTransitions is object)
{ {
var toAdd = (IList)newTransitions;
if (newTransitions.Count > 0 && oldTransitions?.Count > 0)
{
toAdd = newTransitions.Except(oldTransitions).ToList();
}
newTransitions.CollectionChanged += TransitionsCollectionChanged; newTransitions.CollectionChanged += TransitionsCollectionChanged;
AddTransitions(newTransitions); AddTransitions(toAdd);
} }
if (oldTransitions is object) if (oldTransitions is object)
{ {
var toRemove = (IList)oldTransitions;
if (oldTransitions.Count > 0 && newTransitions?.Count > 0)
{
toRemove = oldTransitions.Except(newTransitions).ToList();
}
oldTransitions.CollectionChanged -= TransitionsCollectionChanged; oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
RemoveTransitions(oldTransitions); RemoveTransitions(toRemove);
} }
} }
else if (_transitionsEnabled && else if (_transitionsEnabled &&
@ -115,9 +135,9 @@ namespace Avalonia.Animation
{ {
var transition = Transitions[i]; var transition = Transitions[i];
if (transition.Property == change.Property) if (transition.Property == change.Property &&
_transitionState.TryGetValue(transition, out var state))
{ {
var state = _transitionState[transition];
var oldValue = state.BaseValue; var oldValue = state.BaseValue;
var newValue = GetAnimationBaseValue(transition.Property); var newValue = GetAnimationBaseValue(transition.Property);

9
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -65,7 +65,6 @@ namespace Avalonia.PropertyStore
{ {
_subscription?.Dispose(); _subscription?.Dispose();
_subscription = null; _subscription = null;
_isSubscribed = false;
OnCompleted(); OnCompleted();
} }
@ -74,6 +73,7 @@ namespace Avalonia.PropertyStore
var oldValue = _value; var oldValue = _value;
_value = default; _value = default;
Priority = BindingPriority.Unset; Priority = BindingPriority.Unset;
_isSubscribed = false;
_sink.Completed(Property, this, oldValue); _sink.Completed(Property, this, oldValue);
} }
@ -104,8 +104,11 @@ namespace Avalonia.PropertyStore
public void Start(bool ignoreBatchUpdate) public void Start(bool ignoreBatchUpdate)
{ {
// We can't use _subscription to check whether we're subscribed because it won't be set // We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy. // until Subscribe has finished, which will be too late to prevent reentrancy. In addition
if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate)) // don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
if (!_isSubscribed &&
Priority != BindingPriority.Unset &&
(!_batchUpdate || ignoreBatchUpdate))
{ {
_isSubscribed = true; _isSubscribed = true;
_subscription = Source.Subscribe(this); _subscription = Source.Subscribe(this);

9
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@ -6,12 +6,19 @@ using Avalonia.Data;
namespace Avalonia.PropertyStore namespace Avalonia.PropertyStore
{ {
/// <summary>
/// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
/// </summary>
internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
{
}
/// <summary> /// <summary>
/// Stores a value with a priority in a <see cref="ValueStore"/> or /// Stores a value with a priority in a <see cref="ValueStore"/> or
/// <see cref="PriorityValue{T}"/>. /// <see cref="PriorityValue{T}"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The property type.</typeparam> /// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
{ {
private IValueSink _sink; private IValueSink _sink;
private Optional<T> _value; private Optional<T> _value;

2
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -94,7 +94,7 @@ namespace Avalonia.Utilities
return (0, false); return (0, false);
} }
public bool TryGetValue(AvaloniaProperty property, [MaybeNull] out TValue value) public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{ {
(int index, bool found) = TryFindEntry(property.Id); (int index, bool found) = TryFindEntry(property.Id);
if (!found) if (!found)

49
src/Avalonia.Base/ValueStore.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.PropertyStore; using Avalonia.PropertyStore;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -56,7 +57,7 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property) public bool IsAnimating(AvaloniaProperty property)
{ {
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
return slot.Priority < BindingPriority.LocalValue; return slot.Priority < BindingPriority.LocalValue;
} }
@ -66,7 +67,7 @@ namespace Avalonia
public bool IsSet(AvaloniaProperty property) public bool IsSet(AvaloniaProperty property)
{ {
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
return slot.GetValue().HasValue; return slot.GetValue().HasValue;
} }
@ -79,7 +80,7 @@ namespace Avalonia
BindingPriority maxPriority, BindingPriority maxPriority,
out T value) out T value)
{ {
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
var v = ((IValue<T>)slot).GetValue(maxPriority); var v = ((IValue<T>)slot).GetValue(maxPriority);
@ -103,7 +104,7 @@ namespace Avalonia
IDisposable? result = null; IDisposable? result = null;
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
result = SetExisting(slot, property, value, priority); result = SetExisting(slot, property, value, priority);
} }
@ -138,7 +139,7 @@ namespace Avalonia
IObservable<BindingValue<T>> source, IObservable<BindingValue<T>> source,
BindingPriority priority) BindingPriority priority)
{ {
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
return BindExisting(slot, property, source, priority); return BindExisting(slot, property, source, priority);
} }
@ -160,7 +161,7 @@ namespace Avalonia
public void ClearLocalValue<T>(StyledPropertyBase<T> property) public void ClearLocalValue<T>(StyledPropertyBase<T> property)
{ {
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
if (slot is PriorityValue<T> p) if (slot is PriorityValue<T> p)
{ {
@ -173,7 +174,7 @@ namespace Avalonia
// During batch update values can't be removed immediately because they're needed to raise // During batch update values can't be removed immediately because they're needed to raise
// a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
// by setting their priority to Unset. // by setting their priority to Unset.
if (_batchUpdate is null) if (!IsBatchUpdating())
{ {
_values.Remove(property); _values.Remove(property);
} }
@ -198,7 +199,7 @@ namespace Avalonia
public void CoerceValue<T>(StyledPropertyBase<T> property) public void CoerceValue<T>(StyledPropertyBase<T> property)
{ {
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
if (slot is PriorityValue<T> p) if (slot is PriorityValue<T> p)
{ {
@ -209,7 +210,7 @@ namespace Avalonia
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property) public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{ {
if (_values.TryGetValue(property, out var slot)) if (TryGetValue(property, out var slot))
{ {
var slotValue = slot.GetValue(); var slotValue = slot.GetValue();
return new Diagnostics.AvaloniaPropertyValue( return new Diagnostics.AvaloniaPropertyValue(
@ -242,6 +243,7 @@ namespace Avalonia
IPriorityValueEntry entry, IPriorityValueEntry entry,
Optional<T> oldValue) Optional<T> oldValue)
{ {
// We need to include remove sentinels here so call `_values.TryGetValue` directly.
if (_values.TryGetValue(property, out var slot) && slot == entry) if (_values.TryGetValue(property, out var slot) && slot == entry)
{ {
if (_batchUpdate is null) if (_batchUpdate is null)
@ -285,7 +287,7 @@ namespace Avalonia
else else
{ {
var priorityValue = new PriorityValue<T>(_owner, property, this, l); var priorityValue = new PriorityValue<T>(_owner, property, this, l);
if (_batchUpdate is object) if (IsBatchUpdating())
priorityValue.BeginBatchUpdate(); priorityValue.BeginBatchUpdate();
result = priorityValue.SetValue(value, priority); result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue); _values.SetValue(property, priorityValue);
@ -311,7 +313,7 @@ namespace Avalonia
{ {
priorityValue = new PriorityValue<T>(_owner, property, this, e); priorityValue = new PriorityValue<T>(_owner, property, this, e);
if (_batchUpdate is object) if (IsBatchUpdating())
{ {
priorityValue.BeginBatchUpdate(); priorityValue.BeginBatchUpdate();
} }
@ -338,7 +340,7 @@ namespace Avalonia
private void AddValue(AvaloniaProperty property, IValue value) private void AddValue(AvaloniaProperty property, IValue value)
{ {
_values.AddValue(property, value); _values.AddValue(property, value);
if (_batchUpdate is object && value is IBatchUpdate batch) if (IsBatchUpdating() && value is IBatchUpdate batch)
batch.BeginBatchUpdate(); batch.BeginBatchUpdate();
value.Start(); value.Start();
} }
@ -364,6 +366,21 @@ namespace Avalonia
} }
} }
private bool IsBatchUpdating() => _batchUpdate?.IsBatchUpdating == true;
private bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out IValue value)
{
return _values.TryGetValue(property, out value) && !IsRemoveSentinel(value);
}
private static bool IsRemoveSentinel(IValue value)
{
// Local value entries are optimized and contain only a single value field to save space,
// so there's no way to mark them for removal at the end of a batch update. Instead a
// ConstantValueEntry with a priority of Unset is used as a sentinel value.
return value is IConstantValueEntry t && t.Priority == BindingPriority.Unset;
}
private class BatchUpdate private class BatchUpdate
{ {
private ValueStore _owner; private ValueStore _owner;
@ -373,6 +390,8 @@ namespace Avalonia
public BatchUpdate(ValueStore owner) => _owner = owner; public BatchUpdate(ValueStore owner) => _owner = owner;
public bool IsBatchUpdating => _batchUpdateCount > 0;
public void Begin() public void Begin()
{ {
if (_batchUpdateCount++ == 0) if (_batchUpdateCount++ == 0)
@ -437,8 +456,10 @@ namespace Avalonia
// During batch update values can't be removed immediately because they're needed to raise // During batch update values can't be removed immediately because they're needed to raise
// the _sink.ValueChanged notification. They instead mark themselves for removal by setting // the _sink.ValueChanged notification. They instead mark themselves for removal by setting
// their priority to Unset. // their priority to Unset. We need to re-read the slot here because raising ValueChanged
if (slot.Priority == BindingPriority.Unset) // could have caused it to be updated.
if (values.TryGetValue(entry.property, out var updatedSlot) &&
updatedSlot.Priority == BindingPriority.Unset)
{ {
values.Remove(entry.property); values.Remove(entry.property);
} }

12
src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs

@ -0,0 +1,12 @@
using System;
using XamlX.Transform;
namespace Avalonia.Build.Tasks
{
public class DeterministicIdGenerator : IXamlIdentifierGenerator
{
private int _nextId = 1;
public string GenerateIdentifierPart() => (_nextId++).ToString();
}
}

4
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -22,7 +22,6 @@ using XamlX.IL;
namespace Avalonia.Build.Tasks namespace Avalonia.Build.Tasks
{ {
public static partial class XamlCompilerTaskExecutor public static partial class XamlCompilerTaskExecutor
{ {
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage),
AvaloniaXamlIlLanguage.CustomValueConverter, AvaloniaXamlIlLanguage.CustomValueConverter,
new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)), new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)),
new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure))); new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)),
new DeterministicIdGenerator());
var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext",

5
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,7 +1,10 @@
Compat issues with assembly Avalonia.Controls: Compat issues with assembly Avalonia.Controls:
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 5 Total Issues: 7

8
src/Avalonia.Controls/AutoCompleteBox.cs

@ -483,7 +483,9 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>( AvaloniaProperty.RegisterDirect<AutoCompleteBox, object>(
nameof(SelectedItem), nameof(SelectedItem),
o => o.SelectedItem, o => o.SelectedItem,
(o, v) => o.SelectedItem = v); (o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
/// <summary> /// <summary>
/// Identifies the /// Identifies the
@ -1333,7 +1335,7 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
} }
/// <summary> /// <summary>
/// Called to update the validation state for properties for which data validation is /// Called to update the validation state for properties for which data validation is
/// enabled. /// enabled.
@ -1342,7 +1344,7 @@ namespace Avalonia.Controls
/// <param name="value">The new binding value for the property.</param> /// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value) protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{ {
if (property == TextProperty) if (property == TextProperty || property == SelectedItemProperty)
{ {
DataValidationErrors.SetError(this, value.Error); DataValidationErrors.SetError(this, value.Error);
} }

1
src/Avalonia.Controls/Button.cs

@ -218,6 +218,7 @@ namespace Avalonia.Controls
if (Command != null) if (Command != null)
{ {
Command.CanExecuteChanged += CanExecuteChanged; Command.CanExecuteChanged += CanExecuteChanged;
CanExecuteChanged(this, EventArgs.Empty);
} }
} }

2
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

@ -3,5 +3,7 @@ namespace Avalonia.Controls
public interface INativeMenuExporterEventsImplBridge public interface INativeMenuExporterEventsImplBridge
{ {
void RaiseNeedsUpdate (); void RaiseNeedsUpdate ();
void RaiseOpening();
void RaiseClosed();
} }
} }

22
src/Avalonia.Controls/NativeControlHost.cs

@ -16,30 +16,16 @@ namespace Avalonia.Controls
private bool _queuedForDestruction; private bool _queuedForDestruction;
private bool _queuedForMoveResize; private bool _queuedForMoveResize;
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>(); private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
static NativeControlHost()
{
IsVisibleProperty.Changed.AddClassHandler<NativeControlHost>(OnVisibleChanged);
}
public NativeControlHost()
{
_propertyChangedHandler = PropertyChangedHandler;
}
private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2)
=> host.UpdateHost();
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
_currentRoot = e.Root as TopLevel; _currentRoot = e.Root as TopLevel;
var visual = (IVisual)this; var visual = (IVisual)this;
while (visual != _currentRoot) while (visual != null)
{ {
if (visual is Visual v) if (visual is Visual v)
{ {
v.PropertyChanged += _propertyChangedHandler; v.PropertyChanged += PropertyChangedHandler;
_propertyChangedSubscriptions.Add(v); _propertyChangedSubscriptions.Add(v);
} }
@ -51,7 +37,7 @@ namespace Avalonia.Controls
private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
{ {
if (e.IsEffectiveValueChange && e.Property == BoundsProperty) if (e.IsEffectiveValueChange && (e.Property == BoundsProperty || e.Property == IsVisibleProperty))
EnqueueForMoveResize(); EnqueueForMoveResize();
} }
@ -61,7 +47,7 @@ namespace Avalonia.Controls
if (_propertyChangedSubscriptions != null) if (_propertyChangedSubscriptions != null)
{ {
foreach (var v in _propertyChangedSubscriptions) foreach (var v in _propertyChangedSubscriptions)
v.PropertyChanged -= _propertyChangedHandler; v.PropertyChanged -= PropertyChangedHandler;
_propertyChangedSubscriptions.Clear(); _propertyChangedSubscriptions.Clear();
} }
UpdateHost(); UpdateHost();

33
src/Avalonia.Controls/NativeMenu.cs

@ -12,13 +12,34 @@ namespace Avalonia.Controls
private readonly AvaloniaList<NativeMenuItemBase> _items = private readonly AvaloniaList<NativeMenuItemBase> _items =
new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove }; new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
private NativeMenuItem _parent; private NativeMenuItem _parent;
[Content] [Content]
public IList<NativeMenuItemBase> Items => _items; public IList<NativeMenuItemBase> Items => _items;
/// <summary> /// <summary>
/// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically. /// Raised when the menu requests an update.
/// </summary>
/// <remarks>
/// Use this event to add, remove or modify menu items before a menu is
/// shown or a hotkey is pressed.
/// </remarks>
public event EventHandler<EventArgs> NeedsUpdate;
/// <summary>
/// Raised before the menu is opened.
/// </summary> /// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Opening; public event EventHandler<EventArgs> Opening;
/// <summary>
/// Raised after the menu is closed.
/// </summary>
/// <remarks>
/// Do not update the menu in this event; use <see cref="NeedsUpdate"/>.
/// </remarks>
public event EventHandler<EventArgs> Closed;
public NativeMenu() public NativeMenu()
{ {
@ -27,10 +48,20 @@ namespace Avalonia.Controls
} }
void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate() void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
{
NeedsUpdate?.Invoke(this, EventArgs.Empty);
}
void INativeMenuExporterEventsImplBridge.RaiseOpening()
{ {
Opening?.Invoke(this, EventArgs.Empty); Opening?.Invoke(this, EventArgs.Empty);
} }
void INativeMenuExporterEventsImplBridge.RaiseClosed()
{
Closed?.Invoke(this, EventArgs.Empty);
}
private void Validator(NativeMenuItemBase obj) private void Validator(NativeMenuItemBase obj)
{ {
if (obj.Parent != null) if (obj.Parent != null)

16
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@ -0,0 +1,16 @@
using System;
namespace Avalonia.Controls
{
[Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")]
public class NativeMenuItemSeperator : NativeMenuItemSeparator
{
}
public class NativeMenuItemSeparator : NativeMenuItemBase
{
[Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
public string Header => "-";
}
}

10
src/Avalonia.Controls/NativeMenuItemSeperator.cs

@ -1,10 +0,0 @@
using System;
namespace Avalonia.Controls
{
public class NativeMenuItemSeperator : NativeMenuItemBase
{
[Obsolete("This is a temporary hack to make our MenuItem recognize this as a separator, don't use", true)]
public string Header => "-";
}
}

18
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -91,14 +91,14 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public static readonly DirectProperty<NumericUpDown, string> TextProperty = public static readonly DirectProperty<NumericUpDown, string> TextProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v, AvaloniaProperty.RegisterDirect<NumericUpDown, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary> /// <summary>
/// Defines the <see cref="Value"/> property. /// Defines the <see cref="Value"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<NumericUpDown, double> ValueProperty = public static readonly DirectProperty<NumericUpDown, double> ValueProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value, AvaloniaProperty.RegisterDirect<NumericUpDown, double>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary> /// <summary>
/// Defines the <see cref="Watermark"/> property. /// Defines the <see cref="Watermark"/> property.
@ -370,6 +370,20 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == TextProperty || property == ValueProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
/// <summary> /// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed. /// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary> /// </summary>

2
src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs

@ -16,7 +16,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// The default for the platform. /// The default for the platform.
/// </summary> /// </summary>
Default = SystemChrome, Default = PreferSystemChrome,
/// <summary> /// <summary>
/// Use SystemChrome /// Use SystemChrome

22
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives
nameof(SelectedItem), nameof(SelectedItem),
o => o.SelectedItem, o => o.SelectedItem,
(o, v) => o.SelectedItem = v, (o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay); defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary> /// <summary>
/// Defines the <see cref="SelectedItems"/> property. /// Defines the <see cref="SelectedItems"/> property.
@ -466,6 +466,20 @@ namespace Avalonia.Controls.Primitives
EndUpdating(); EndUpdating();
} }
/// <summary>
/// Called to update the validation state for properties for which data validation is
/// enabled.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new binding value for the property.</param>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
if (property == SelectedItemProperty)
{
DataValidationErrors.SetError(this, value.Error);
}
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
@ -707,7 +721,7 @@ namespace Avalonia.Controls.Primitives
_oldSelectedItem = SelectedItem; _oldSelectedItem = SelectedItem;
} }
else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
{ {
RaisePropertyChanged( RaisePropertyChanged(
SelectedItemsProperty, SelectedItemsProperty,
@ -977,7 +991,7 @@ namespace Avalonia.Controls.Primitives
public Optional<ISelectionModel> Selection { get; set; } public Optional<ISelectionModel> Selection { get; set; }
public Optional<IList?> SelectedItems { get; set; } public Optional<IList?> SelectedItems { get; set; }
public Optional<int> SelectedIndex public Optional<int> SelectedIndex
{ {
get => _selectedIndex; get => _selectedIndex;
set set
@ -996,6 +1010,6 @@ namespace Avalonia.Controls.Primitives
_selectedIndex = default; _selectedIndex = default;
} }
} }
} }
} }
} }

4
src/Avalonia.Controls/Slider.cs

@ -341,7 +341,9 @@ namespace Avalonia.Controls
var pointNum = orient ? x.Position.X : x.Position.Y; var pointNum = orient ? x.Position.X : x.Position.Y;
var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d); var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);
var invert = orient ? 0 : 1; var invert = orient ?
IsDirectionReversed ? 1 : 0 :
IsDirectionReversed ? 0 : 1;
var calcVal = Math.Abs(invert - logicalPos); var calcVal = Math.Abs(invert - logicalPos);
var range = Maximum - Minimum; var range = Maximum - Minimum;
var finalValue = calcVal * range + Minimum; var finalValue = calcVal * range + Minimum;

43
src/Avalonia.Controls/TextBox.cs

@ -514,21 +514,36 @@ namespace Avalonia.Controls
private void HandleTextInput(string input) private void HandleTextInput(string input)
{ {
if (!IsReadOnly) if (IsReadOnly)
{ {
input = RemoveInvalidCharacters(input); return;
string text = Text ?? string.Empty; }
int caretIndex = CaretIndex;
if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength)) input = RemoveInvalidCharacters(input);
{
DeleteSelection(); if (string.IsNullOrEmpty(input))
caretIndex = CaretIndex; {
text = Text ?? string.Empty; return;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); }
CaretIndex += input.Length;
ClearSelection(); string text = Text ?? string.Empty;
_undoRedoHelper.DiscardRedo(); int caretIndex = CaretIndex;
} int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
if (MaxLength > 0 && newLength > MaxLength)
{
input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
}
if (!string.IsNullOrEmpty(input))
{
DeleteSelection();
caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
_undoRedoHelper.DiscardRedo();
} }
} }

2
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -169,7 +169,7 @@ namespace Avalonia.DesignerSupport.Remote
if (entryPoint == null) if (entryPoint == null)
throw Die($"Assembly {args.AppPath} doesn't have an entry point"); throw Die($"Assembly {args.AppPath} doesn't have an entry point");
var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName, var builderMethod = entryPoint.DeclaringType.GetMethod(BuilderMethodName,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Array.Empty<Type>(), null);
if (builderMethod == null) if (builderMethod == null)
throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}"); throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}");
Design.IsDesignMode = true; Design.IsDesignMode = true;

2
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -192,7 +192,7 @@ namespace Avalonia.FreeDesktop
{ {
var (it, menu) = i; var (it, menu) = i;
if (it is NativeMenuItemSeperator) if (it is NativeMenuItemSeparator)
{ {
if (name == "type") if (name == "type")
return "separator"; return "separator";

26
src/Avalonia.Native/IAvnMenu.cs

@ -20,11 +20,23 @@ namespace Avalonia.Native.Interop
{ {
_parent?.RaiseNeedsUpdate(); _parent?.RaiseNeedsUpdate();
} }
public void Opening()
{
_parent?.RaiseOpening();
}
public void Closed()
{
_parent?.RaiseClosed();
}
} }
partial interface IAvnMenu partial interface IAvnMenu
{ {
void RaiseNeedsUpdate(); void RaiseNeedsUpdate();
void RaiseOpening();
void RaiseClosed();
void Deinitialise(); void Deinitialise();
} }
} }
@ -45,6 +57,16 @@ namespace Avalonia.Native.Interop.Impl
_exporter.UpdateIfNeeded(); _exporter.UpdateIfNeeded();
} }
public void RaiseOpening()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseOpening();
}
public void RaiseClosed()
{
(ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseClosed();
}
internal NativeMenu ManagedMenu { get; private set; } internal NativeMenu ManagedMenu { get; private set; }
public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory) public static __MicroComIAvnMenuProxy Create(IAvaloniaNativeFactory factory)
@ -103,8 +125,8 @@ namespace Avalonia.Native.Interop.Impl
private __MicroComIAvnMenuItemProxy CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) private __MicroComIAvnMenuItemProxy CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item)
{ {
var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeperator ? var nativeItem = (__MicroComIAvnMenuItemProxy)(item is NativeMenuItemSeparator ?
factory.CreateMenuItemSeperator() : factory.CreateMenuItemSeparator() :
factory.CreateMenuItem()); factory.CreateMenuItem());
nativeItem.ManagedMenuItem = item; nativeItem.ManagedMenuItem = item;

9
src/Avalonia.Native/avn.idl

@ -397,7 +397,7 @@ enum AvnExtendClientAreaChromeHints
AvnSystemChrome = 0x01, AvnSystemChrome = 0x01,
AvnPreferSystemChrome = 0x02, AvnPreferSystemChrome = 0x02,
AvnOSXThickTitleBar = 0x08, AvnOSXThickTitleBar = 0x08,
AvnDefaultChrome = AvnSystemChrome, AvnDefaultChrome = AvnPreferSystemChrome,
} }
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
@ -417,7 +417,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT SetAppMenu(IAvnMenu* menu); HRESULT SetAppMenu(IAvnMenu* menu);
HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv);
HRESULT CreateMenuItemSeperator(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
} }
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -685,10 +685,9 @@ interface IAvnMenuItem : IUnknown
[uuid(0af7df53-7632-42f4-a650-0992c361b477)] [uuid(0af7df53-7632-42f4-a650-0992c361b477)]
interface IAvnMenuEvents : IUnknown interface IAvnMenuEvents : IUnknown
{ {
/**
* NeedsUpdate
*/
void NeedsUpdate(); void NeedsUpdate();
void Opening();
void Closed();
} }
[uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)] [uuid(5142bb41-66ab-49e7-bb37-cd079c000f27)]

18
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@ -9,18 +9,22 @@ namespace Avalonia.ReactiveUI
{ {
/// <summary> /// <summary>
/// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia
/// scheduler and Avalonia activation for view fetcher. Always remember to /// scheduler, an activation for view fetcher, a template binding hook. Remember
/// call this method if you are using ReactiveUI in your application. /// to call this method if you are using ReactiveUI in your application.
/// </summary> /// </summary>
public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder) public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new() where TAppBuilder : AppBuilderBase<TAppBuilder>, new() =>
{ builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() =>
return builder.AfterPlatformServicesSetup(_ =>
{ {
if (Locator.CurrentMutable is null)
{
return;
}
PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia);
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
}); }));
}
} }
} }

2
src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml

@ -35,7 +35,7 @@
<ItemsControl Items="{Binding}"> <ItemsControl Items="{Binding}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text="{Binding Message}" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" /> <TextBlock Text="{Binding }" Foreground="{DynamicResource SystemControlErrorTextForegroundBrush}" TextWrapping="Wrap" />
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>

5
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs

@ -14,8 +14,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
XamlXmlnsMappings xmlnsMappings, XamlXmlnsMappings xmlnsMappings,
XamlValueConverter customValueConverter, XamlValueConverter customValueConverter,
XamlIlClrPropertyInfoEmitter clrPropertyEmitter, XamlIlClrPropertyInfoEmitter clrPropertyEmitter,
XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter) XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter,
: base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) IXamlIdentifierGenerator identifierGenerator = null)
: base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator)
{ {
ClrPropertyEmitter = clrPropertyEmitter; ClrPropertyEmitter = clrPropertyEmitter;
AccessorFactoryEmitter = accessorFactoryEmitter; AccessorFactoryEmitter = accessorFactoryEmitter;

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit f3ca2028f4f64be3556a6afd22f192902de095e5 Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a

2
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates
public IControl Build(object data, IControl existing) public IControl Build(object data, IControl existing)
{ {
return existing ?? TemplateContent.Load(Content).Control; return existing ?? TemplateContent.Load(Content)?.Control;
} }
} }
} }

3
src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs

@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent] [TemplateContent]
public object Content { get; set; } public object Content { get; set; }
public IPanel Build() public IPanel Build() => (IPanel)TemplateContent.Load(Content)?.Control;
=> (IPanel)TemplateContent.Load(Content).Control;
object ITemplate.Build() => Build(); object ITemplate.Build() => Build();
} }

2
src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs

@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates
[TemplateContent] [TemplateContent]
public object Content { get; set; } public object Content { get; set; }
public IControl Build() => TemplateContent.Load(Content).Control; public IControl Build() => TemplateContent.Load(Content)?.Control;
object ITemplate.Build() => Build(); object ITemplate.Build() => Build();
} }

8
src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs

@ -1,6 +1,4 @@
using System; using System;
using Avalonia.Controls;
using System.Collections.Generic;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
namespace Avalonia.Markup.Xaml.Templates namespace Avalonia.Markup.Xaml.Templates
@ -14,6 +12,12 @@ namespace Avalonia.Markup.Xaml.Templates
{ {
return (ControlTemplateResult)direct(null); return (ControlTemplateResult)direct(null);
} }
if (templateContent is null)
{
return null;
}
throw new ArgumentException(nameof(templateContent)); throw new ArgumentException(nameof(templateContent));
} }
} }

8
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@ -51,8 +51,12 @@ namespace Avalonia.Markup.Xaml.Templates
public IControl Build(object data) public IControl Build(object data)
{ {
var visualTreeForItem = TemplateContent.Load(Content).Control; var visualTreeForItem = TemplateContent.Load(Content)?.Control;
visualTreeForItem.DataContext = data; if (visualTreeForItem != null)
{
visualTreeForItem.DataContext = data;
}
return visualTreeForItem; return visualTreeForItem;
} }
} }

31
tests/Avalonia.Animation.UnitTests/AnimatableTests.cs

@ -330,6 +330,37 @@ namespace Avalonia.Animation.UnitTests
} }
} }
[Fact]
public void Transitions_Can_Be_Changed_To_Collection_That_Contains_The_Same_Transitions()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
control.Transitions = new Transitions { target.Object };
}
[Fact]
public void Transitions_Can_Re_Set_During_Batch_Update()
{
var target = CreateTarget();
var control = CreateControl(target.Object);
// Assigning and then clearing Transitions ensures we have a transition state
// collection created.
control.Transitions = null;
control.BeginBatchUpdate();
// Setting opacity then Transitions means that we receive the Transitions change
// after the Opacity change when EndBatchUpdate is called.
control.Opacity = 0.5;
control.Transitions = new Transitions { target.Object };
// Which means that the transition state hasn't been initialized with the new
// Transitions when the Opacity change notification gets raised here.
control.EndBatchUpdate();
}
private static Mock<ITransition> CreateTarget() private static Mock<ITransition> CreateTarget()
{ {
return CreateTransition(Visual.OpacityProperty); return CreateTransition(Visual.OpacityProperty);

122
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@ -53,6 +53,21 @@ namespace Avalonia.Base.UnitTests
Assert.Empty(raised); Assert.Empty(raised);
} }
[Fact]
public void Binding_Disposal_Should_Not_Raise_Property_Changes_During_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<string>();
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
target.BeginBatchUpdate();
sub.Dispose();
Assert.Empty(raised);
}
[Fact] [Fact]
public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1() public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
{ {
@ -240,6 +255,27 @@ namespace Avalonia.Base.UnitTests
Assert.Equal(BindingPriority.Unset, raised[0].Priority); Assert.Equal(BindingPriority.Unset, raised[0].Priority);
} }
[Fact]
public void Binding_Disposal_Should_Be_Raised_After_Batch_Update()
{
var target = new TestClass();
var observable = new TestObservable<string>("foo");
var raised = new List<AvaloniaPropertyChangedEventArgs>();
var sub = target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
target.PropertyChanged += (s, e) => raised.Add(e);
target.BeginBatchUpdate();
sub.Dispose();
target.EndBatchUpdate();
Assert.Equal(1, raised.Count);
Assert.Null(target.Foo);
Assert.Equal("foo", raised[0].OldValue);
Assert.Null(raised[0].NewValue);
Assert.Equal(BindingPriority.Unset, raised[0].Priority);
}
[Fact] [Fact]
public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1() public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
{ {
@ -449,6 +485,92 @@ namespace Avalonia.Base.UnitTests
Assert.Null(raised[1].NewValue); Assert.Null(raised[1].NewValue);
} }
[Fact]
public void Can_Set_Cleared_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
target.Foo = "foo";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Foo = "bar";
++raised;
}
};
target.EndBatchUpdate();
Assert.Equal("bar", target.Foo);
Assert.Equal(1, raised);
}
[Fact]
public void Can_Bind_Cleared_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
target.Foo = "foo";
target.BeginBatchUpdate();
target.ClearValue(TestClass.FooProperty);
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Bind(TestClass.FooProperty, new TestObservable<string>("bar"));
++raised;
}
notifications.Add(e);
};
target.EndBatchUpdate();
Assert.Equal("bar", target.Foo);
Assert.Equal(1, raised);
Assert.Equal(2, notifications.Count);
Assert.Equal(null, notifications[0].NewValue);
Assert.Equal("bar", notifications[1].NewValue);
}
[Fact]
public void Can_Bind_Completed_Binding_Back_To_Original_Value_When_Ending_Batch_Update()
{
var target = new TestClass();
var raised = 0;
var notifications = new List<AvaloniaPropertyChangedEventArgs>();
var observable1 = new TestObservable<string>("foo");
var observable2 = new TestObservable<string>("foo");
target.Bind(TestClass.FooProperty, observable1);
target.BeginBatchUpdate();
observable1.OnCompleted();
target.PropertyChanged += (sender, e) =>
{
if (e.Property == TestClass.FooProperty && e.NewValue is null)
{
target.Bind(TestClass.FooProperty, observable2);
++raised;
}
notifications.Add(e);
};
target.EndBatchUpdate();
Assert.Equal("foo", target.Foo);
Assert.Equal(1, raised);
Assert.Equal(2, notifications.Count);
Assert.Equal(null, notifications[0].NewValue);
Assert.Equal("foo", notifications[1].NewValue);
}
public class TestClass : AvaloniaObject public class TestClass : AvaloniaObject
{ {
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =

32
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -14,6 +14,8 @@ using Avalonia.UnitTests;
using Moq; using Moq;
using Xunit; using Xunit;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
{ {
@ -396,6 +398,36 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(control.Text, control.ItemSelector(input, selectedItem)); Assert.Equal(control.Text, control.ItemSelector(input, selectedItem));
}); });
} }
[Fact]
public void Text_Validation()
{
RunTest((control, textbox) =>
{
var exception = new InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
control.Bind(AutoCompleteBox.TextProperty, textObservable);
Dispatcher.UIThread.RunJobs();
Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
});
}
[Fact]
public void SelectedItem_Validation()
{
RunTest((control, textbox) =>
{
var exception = new InvalidCastException("failed validation");
var itemObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
control.Bind(AutoCompleteBox.SelectedItemProperty, itemObservable);
Dispatcher.UIThread.RunJobs();
Assert.Equal(DataValidationErrors.GetHasErrors(control), true);
Assert.Equal(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }), true);
});
}
/// <summary> /// <summary>
/// Retrieves a defined predicate filter through a new AutoCompleteBox /// Retrieves a defined predicate filter through a new AutoCompleteBox

64
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -1,10 +1,14 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.UnitTests;
using Xunit; using Xunit;
namespace Avalonia.Controls.UnitTests namespace Avalonia.Controls.UnitTests
@ -77,9 +81,9 @@ namespace Avalonia.Controls.UnitTests
{ {
var items = new ObservableCollection<string> var items = new ObservableCollection<string>
{ {
"Foo", "Foo",
"Bar", "Bar",
"FooBar" "FooBar"
}; };
var target = new Carousel var target = new Carousel
@ -113,9 +117,9 @@ namespace Avalonia.Controls.UnitTests
{ {
var items = new ObservableCollection<string> var items = new ObservableCollection<string>
{ {
"Foo", "Foo",
"Bar", "Bar",
"FooBar" "FooBar"
}; };
var target = new Carousel var target = new Carousel
@ -172,9 +176,9 @@ namespace Avalonia.Controls.UnitTests
{ {
var items = new ObservableCollection<string> var items = new ObservableCollection<string>
{ {
"Foo", "Foo",
"Bar", "Bar",
"FooBar" "FooBar"
}; };
var target = new Carousel var target = new Carousel
@ -206,9 +210,9 @@ namespace Avalonia.Controls.UnitTests
{ {
var items = new ObservableCollection<string> var items = new ObservableCollection<string>
{ {
"Foo", "Foo",
"Bar", "Bar",
"FooBar" "FooBar"
}; };
var target = new Carousel var target = new Carousel
@ -235,9 +239,9 @@ namespace Avalonia.Controls.UnitTests
{ {
var items = new ObservableCollection<string> var items = new ObservableCollection<string>
{ {
"Foo", "Foo",
"Bar", "Bar",
"FooBar" "FooBar"
}; };
var target = new Carousel var target = new Carousel
@ -269,9 +273,9 @@ namespace Avalonia.Controls.UnitTests
{ {
var items = new ObservableCollection<string> var items = new ObservableCollection<string>
{ {
"Foo", "Foo",
"Bar", "Bar",
"FooBar" "FooBar"
}; };
var target = new Carousel var target = new Carousel
@ -311,5 +315,29 @@ namespace Avalonia.Controls.UnitTests
contentPresenter.UpdateChild(); contentPresenter.UpdateChild();
return Assert.IsType<TextBlock>(contentPresenter.Child); return Assert.IsType<TextBlock>(contentPresenter.Child);
} }
[Fact]
public void SelectedItem_Validation()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate), IsVirtualized = false
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var exception = new System.InvalidCastException("failed validation");
var textObservable =
new BehaviorSubject<BindingNotification>(new BindingNotification(exception,
BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
} }
} }

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

@ -1,11 +1,14 @@
using System.Linq; using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -173,5 +176,31 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(expectedSelectedIndex, target.SelectedIndex); Assert.Equal(expectedSelectedIndex, target.SelectedIndex);
} }
} }
[Fact]
public void SelectedItem_Validation()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new ComboBox
{
Template = GetTemplate(),
VirtualizationMode = ItemVirtualizationMode.None
};
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
var exception = new System.InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
} }
} }

26
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -1,11 +1,14 @@
using System.Linq; using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Xunit; using Xunit;
@ -559,5 +562,28 @@ namespace Avalonia.Controls.UnitTests
public string Value { get; } public string Value { get; }
} }
[Fact]
public void SelectedItem_Validation()
{
var target = new ListBox
{
Template = ListBoxTemplate(),
Items = new[] { "Foo" },
ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
SelectionMode = SelectionMode.AlwaysSelected,
VirtualizationMode = ItemVirtualizationMode.None
};
Prepare(target);
var exception = new System.InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
} }
} }

95
tests/Avalonia.Controls.UnitTests/NumericUpDownTests.cs

@ -0,0 +1,95 @@
using System;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class NumericUpDownTests
{
private static TestServices Services => TestServices.StyledWindow;
[Fact]
public void Text_Validation()
{
RunTest((control, textbox) =>
{
var exception = new InvalidCastException("failed validation");
var textObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
control.Bind(NumericUpDown.TextProperty, textObservable);
Dispatcher.UIThread.RunJobs();
Assert.True(DataValidationErrors.GetHasErrors(control));
Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }));
});
}
[Fact]
public void Value_Validation()
{
RunTest((control, textbox) =>
{
var exception = new InvalidCastException("failed validation");
var valueObservable = new BehaviorSubject<BindingNotification>(new BindingNotification(exception, BindingErrorType.DataValidationError));
control.Bind(NumericUpDown.ValueProperty, valueObservable);
Dispatcher.UIThread.RunJobs();
Assert.True(DataValidationErrors.GetHasErrors(control));
Assert.True(DataValidationErrors.GetErrors(control).SequenceEqual(new[] { exception }));
});
}
private void RunTest(Action<NumericUpDown, TextBox> test)
{
using (UnitTestApplication.Start(Services))
{
var control = CreateControl();
TextBox textBox = GetTextBox(control);
var window = new Window { Content = control };
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
test.Invoke(control, textBox);
}
}
private NumericUpDown CreateControl()
{
var control = new NumericUpDown
{
Template = CreateTemplate()
};
control.ApplyTemplate();
return control;
}
private TextBox GetTextBox(NumericUpDown control)
{
return control.GetTemplateChildren()
.OfType<ButtonSpinner>()
.Select(b => b.Content)
.OfType<TextBox>()
.First();
}
private IControlTemplate CreateTemplate()
{
return new FuncControlTemplate<NumericUpDown>((control, scope) =>
{
var textBox =
new TextBox
{
Name = "PART_TextBox"
}.RegisterInNameScope(scope);
return new ButtonSpinner
{
Name = "PART_Spinner",
Content = textBox,
}.RegisterInNameScope(scope);
});
}
}
}

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

@ -646,22 +646,49 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(target.Text); Assert.Null(target.Text);
} }
} }
[Fact] [Theory]
public void Text_Box_MaxLength_Work_Properly() [InlineData("abc", "d", 3, 0, 0, false, "abc")]
[InlineData("abc", "dd", 4, 3, 3, false, "abcd")]
[InlineData("abc", "ddd", 3, 0, 2, true, "ddc")]
[InlineData("abc", "dddd", 4, 1, 3, true, "addd")]
[InlineData("abc", "ddddd", 5, 3, 3, true, "abcdd")]
public void MaxLength_Works_Properly(
string initalText,
string textInput,
int maxLength,
int selectionStart,
int selectionEnd,
bool fromClipboard,
string expected)
{ {
using (UnitTestApplication.Start(Services)) using (UnitTestApplication.Start(Services))
{ {
var target = new TextBox var target = new TextBox
{ {
Template = CreateTemplate(), Template = CreateTemplate(),
Text = "abc", Text = initalText,
MaxLength = 3, MaxLength = maxLength,
SelectionStart = selectionStart,
SelectionEnd = selectionEnd
}; };
RaiseKeyEvent(target, Key.D, KeyModifiers.None); if (fromClipboard)
{
Assert.Equal("abc", target.Text); AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
var clipboard = AvaloniaLocator.CurrentMutable.GetService<IClipboard>();
clipboard.SetTextAsync(textInput).GetAwaiter().GetResult();
RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
clipboard.ClearAsync().GetAwaiter().GetResult();
}
else
{
RaiseTextEvent(target, textInput);
}
Assert.Equal(expected, target.Text);
} }
} }
@ -758,11 +785,22 @@ namespace Avalonia.Controls.UnitTests
private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
{ {
public Task<string> GetTextAsync() => Task.FromResult(""); private string _text;
public Task<string> GetTextAsync() => Task.FromResult(_text);
public Task SetTextAsync(string text) => Task.CompletedTask; public Task SetTextAsync(string text)
{
_text = text;
return Task.CompletedTask;
}
public Task ClearAsync() => Task.CompletedTask; public Task ClearAsync()
{
_text = null;
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>()); public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());

25
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

@ -7,6 +7,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{ {
public class DataTemplateTests : XamlTestBase public class DataTemplateTests : XamlTestBase
{ {
[Fact]
public void DataTemplate_Can_Be_Empty()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.DataTemplates>
<DataTemplate DataType='{x:Type sys:String}' />
</Window.DataTemplates>
<ContentControl Name='target' Content='Foo'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
Assert.Null(target.Presenter.Child);
}
}
[Fact] [Fact]
public void DataTemplate_Can_Contain_Name() public void DataTemplate_Can_Contain_Name()
{ {

Loading…
Cancel
Save