Browse Source

Merge branch 'master' into compiledbinding-methods

pull/7246/head
Dan Walmsley 5 years ago
committed by GitHub
parent
commit
5f0979b6e4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 41
      Avalonia.sln
  2. 1
      build/CoreLibraries.props
  3. 2
      build/SharedVersion.props
  4. 5
      build/SourceLink.props
  5. 31
      native/Avalonia.Native/src/OSX/window.mm
  6. 4
      samples/ControlCatalog.Android/Assets/AboutAssets.txt
  7. 1
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  8. 1
      samples/ControlCatalog/App.xaml
  9. 33
      samples/ControlCatalog/App.xaml.cs
  10. 2
      samples/ControlCatalog/MainView.xaml.cs
  11. 15
      samples/ControlCatalog/Models/Person.cs
  12. 5
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  13. 12
      samples/ControlCatalog/Pages/DataGridPage.xaml
  14. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  15. 220
      samples/ControlCatalog/Pages/PointersPage.cs
  16. 37
      samples/ControlCatalog/Pages/ScreenPage.cs
  17. 22
      samples/ControlCatalog/Pages/ViewboxPage.xaml
  18. 19
      samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
  19. 13
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  20. 3
      samples/RenderDemo/MainWindow.xaml
  21. 54
      samples/RenderDemo/Pages/ClippingPage.xaml
  22. 17
      samples/RenderDemo/Pages/ClippingPage.xaml.cs
  23. 13
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  24. 7
      samples/RenderDemo/Pages/FormattedTextPage.axaml
  25. 60
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  26. 7
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  27. 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  28. 2
      src/Android/Avalonia.Android/AppBuilder.cs
  29. 4
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  30. 18
      src/Android/Avalonia.Android/RuntimeInfo.cs
  31. 16
      src/Avalonia.Animation/Animatable.cs
  32. 46
      src/Avalonia.Animation/Animation.cs
  33. 33
      src/Avalonia.Animation/AnimationInstance`1.cs
  34. 20
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  35. 9
      src/Avalonia.Animation/Animators/Animator`1.cs
  36. 4
      src/Avalonia.Animation/Avalonia.Animation.csproj
  37. 2
      src/Avalonia.Animation/Clock.cs
  38. 8
      src/Avalonia.Animation/Cue.cs
  39. 8
      src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs
  40. 4
      src/Avalonia.Animation/Easing/Easing.cs
  41. 4
      src/Avalonia.Animation/Easing/EasingTypeConverter.cs
  42. 2
      src/Avalonia.Animation/IAnimation.cs
  43. 4
      src/Avalonia.Animation/IAnimationSetter.cs
  44. 4
      src/Avalonia.Animation/IAnimator.cs
  45. 2
      src/Avalonia.Animation/ITransition.cs
  46. 2
      src/Avalonia.Animation/IterationCount.cs
  47. 4
      src/Avalonia.Animation/IterationCountTypeConverter.cs
  48. 4
      src/Avalonia.Animation/KeyFrame.cs
  49. 4
      src/Avalonia.Animation/KeySplineTypeConverter.cs
  50. 21
      src/Avalonia.Animation/Transition.cs
  51. 6
      src/Avalonia.Animation/TransitionInstance.cs
  52. 1
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  53. 21
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  54. 10
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  55. 20
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  56. 24
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  57. 5
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  58. 2
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  59. 7
      src/Avalonia.Base/Threading/Dispatcher.cs
  60. 15
      src/Avalonia.Base/Threading/JobRunner.cs
  61. 12
      src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs
  62. 2
      src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
  63. 187
      src/Avalonia.Base/Utilities/WeakEvent.cs
  64. 40
      src/Avalonia.Base/Utilities/WeakEvents.cs
  65. 35
      src/Avalonia.Base/Utilities/WeakObservable.cs
  66. 1
      src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs
  67. 10
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  68. 4
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  69. 72
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  70. 21
      src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs
  71. 12
      src/Avalonia.Controls/ApiCompatBaseline.txt
  72. 96
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  73. 6
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  74. 197
      src/Avalonia.Controls/Button.cs
  75. 11
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  76. 19
      src/Avalonia.Controls/Control.cs
  77. 13
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  78. 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  79. 10
      src/Avalonia.Controls/NativeMenuItem.cs
  80. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  81. 34
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  82. 493
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  83. 22
      src/Avalonia.Controls/Primitives/Popup.cs
  84. 8
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  85. 1
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  86. 32
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  87. 52
      src/Avalonia.Controls/ScrollViewer.cs
  88. 52
      src/Avalonia.Controls/TextBlock.cs
  89. 346
      src/Avalonia.Controls/TextBox.cs
  90. 24
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  91. 24
      src/Avalonia.Controls/TopLevel.cs
  92. 15
      src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs
  93. 53
      src/Avalonia.Controls/Utils/StringUtils.cs
  94. 344
      src/Avalonia.Controls/ValidatingToplevel.cs
  95. 11
      src/Avalonia.Controls/Window.cs
  96. 9
      src/Avalonia.Controls/WindowBase.cs
  97. 3
      src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt
  98. 2
      src/Avalonia.DesktopRuntime/AppBuilder.cs
  99. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  100. 40
      src/Avalonia.DesktopRuntime/RuntimeInfo.cs

41
Avalonia.sln

@ -60,20 +60,17 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skia", "Skia", "{3743B0F2-CC41-4F14-A8C8-267F579BF91E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}"
@ -235,15 +232,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "sampl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5
src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
Ad-Hoc|iPhone = Ad-Hoc|iPhone
@ -2169,6 +2160,30 @@ Global
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhone.Build.0 = Release|Any CPU
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.Build.0 = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.ActiveCfg = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.Build.0 = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -2187,11 +2202,9 @@ Global
{EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{3C4C0CB4-0C0F-4450-A37B-148C84FF905F} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}

1
build/CoreLibraries.props

@ -17,5 +17,6 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.MicroCom/Avalonia.MicroCom.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj" Condition="'$(TargetFramework)' != 'netstandard2.0'" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj" />
</ItemGroup>
</Project>

2
build/SharedVersion.props

@ -3,7 +3,7 @@
<PropertyGroup>
<Product>Avalonia</Product>
<Version>0.10.999</Version>
<Copyright>Copyright 2021 &#169; The AvaloniaUI Project</Copyright>
<Copyright>Copyright 2022 &#169; The AvaloniaUI Project</Copyright>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>

5
build/SourceLink.props

@ -3,7 +3,6 @@
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>false</IncludeSymbols>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
@ -15,6 +14,10 @@
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<DebugType Condition="$(ContinuousIntegrationBuild) == 'true'">embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
</ItemGroup>

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

@ -1627,6 +1627,19 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return;
}
}
else if (type == Magnify)
{
delta.X = delta.Y = [event magnification];
}
else if (type == Rotate)
{
delta.X = delta.Y = [event rotation];
}
else if (type == Swipe)
{
delta.X = [event deltaX];
delta.Y = [event deltaY];
}
auto timestamp = [event timestamp] * 1000;
auto modifiers = [self getModifiers:[event modifierFlags]];
@ -1753,6 +1766,24 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[super scrollWheel:event];
}
- (void)magnifyWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Magnify];
[super magnifyWithEvent:event];
}
- (void)rotateWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Rotate];
[super rotateWithEvent:event];
}
- (void)swipeWithEvent:(NSEvent *)event
{
[self mouseEvent:event withType:Swipe];
[super swipeWithEvent:event];
}
- (void)mouseEntered:(NSEvent *)event
{
_isMouseOver = true;

4
samples/ControlCatalog.Android/Assets/AboutAssets.txt

@ -1,7 +1,7 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories) and given a Build Action of "AndroidAsset".
These files will be deployed with you package and will be accessible using Android's
These files will be deployed with your package and will be accessible using Android's
AssetManager, like this:
public class ReadAsset : Activity
@ -16,4 +16,4 @@ public class ReadAsset : Activity
Additionally, some Android functions will automatically load asset files:
Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf");
Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf");

1
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<WasmBuildNative>True</WasmBuildNative>
</PropertyGroup>
<ItemGroup>

1
samples/ControlCatalog/App.xaml

@ -3,6 +3,7 @@
xmlns:vm="using:ControlCatalog.ViewModels"
x:DataType="vm:ApplicationViewModel"
x:CompileBindings="True"
Name="Avalonia ControlCatalog"
x:Class="ControlCatalog.App">
<Application.Styles>
<Style Selector="TextBlock.h1, TextBlock.h2, TextBlock.h3">

33
samples/ControlCatalog/App.xaml.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling;
using Avalonia.Themes.Default;
using Avalonia.Themes.Fluent;
using ControlCatalog.ViewModels;
@ -29,6 +30,8 @@ namespace ControlCatalog
public static FluentTheme Fluent = new FluentTheme(new Uri("avares://ControlCatalog/Styles"));
public static SimpleTheme Default = new SimpleTheme(new Uri("avares://ControlCatalog/Styles"));
public static Styles DefaultLight = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
@ -43,14 +46,7 @@ namespace ControlCatalog
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
}
Default
};
public static Styles DefaultDark = new Styles
@ -67,14 +63,7 @@ namespace ControlCatalog
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml")
}
Default
};
public override void Initialize()
@ -89,16 +78,18 @@ namespace ControlCatalog
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView();
}
base.OnFrameworkInitializationCompleted();
this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
{
StartupScreenIndex = 1,
});
}
}
}

2
samples/ControlCatalog/MainView.xaml.cs

@ -63,11 +63,13 @@ namespace ControlCatalog
}
else if (theme == CatalogTheme.DefaultLight)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light;
Application.Current.Styles[0] = App.DefaultLight;
Application.Current.Styles[1] = App.DataGridDefault;
}
else if (theme == CatalogTheme.DefaultDark)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark;
Application.Current.Styles[0] = App.DefaultDark;
Application.Current.Styles[1] = App.DataGridDefault;
}

15
samples/ControlCatalog/Models/Person.cs

@ -16,6 +16,7 @@ namespace ControlCatalog.Models
string _firstName;
string _lastName;
bool _isBanned;
private int _age;
public string FirstName
{
@ -59,6 +60,20 @@ namespace ControlCatalog.Models
}
}
/// <summary>
/// Gets or sets the age of the person
/// </summary>
public int Age
{
get => _age;
set
{
_age = value;
OnPropertyChanged(nameof(Age));
}
}
Dictionary<string, List<string>> _errorLookup = new Dictionary<string, List<string>>();
void SetError(string propertyName, string error)

5
samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml

@ -1,5 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
x:DataType="vm:MainWindowViewModel"
x:Class="ControlCatalog.Pages.CalendarDatePickerPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
@ -39,6 +41,9 @@
<TextBlock Text="Disabled"/>
<CalendarDatePicker IsEnabled="False"/>
<TextBlock Text="Validation Example"/>
<CalendarDatePicker SelectedDate="{CompiledBinding ValidatedDateExample, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>

12
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -64,6 +64,18 @@
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" />
<DataGridTemplateColumn Header="Age" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person">
<TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:Person">
<NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1" Name="btnAdd" Margin="12,0,12,12" Content="Add" HorizontalAlignment="Right" />

22
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -6,7 +6,9 @@ using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.Models;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Threading;
namespace ControlCatalog.Pages
{
@ -48,9 +50,9 @@ namespace ControlCatalog.Pages
var items = new List<Person>
{
new Person { FirstName = "John", LastName = "Doe" },
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true },
new Person { FirstName = "Zack", LastName = "Ward" }
new Person { FirstName = "John", LastName = "Doe" , Age = 30},
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 },
new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 }
};
var collectionView3 = new DataGridCollectionView(items);
@ -84,5 +86,19 @@ namespace ControlCatalog.Pages
return Comparer.Default.Compare(x, y);
}
}
private void NumericUpDown_OnTemplateApplied(object sender, TemplateAppliedEventArgs e)
{
// We want to focus the TextBox of the NumericUpDown. To do so we search for this control when the template
// is applied, but we postpone the action until the control is actually loaded.
if (e.NameScope.Find<TextBox>("PART_TextBox") is {} textBox)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
textBox.Focus();
textBox.SelectAll();
}, DispatcherPriority.Loaded);
}
}
}
}

220
samples/ControlCatalog/Pages/PointersPage.cs

@ -1,15 +1,37 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace ControlCatalog.Pages
namespace ControlCatalog.Pages;
public class PointersPage : Decorator
{
public class PointersPage : Control
public PointersPage()
{
Child = new TabControl
{
Items = new[]
{
new TabItem() { Header = "Contacts", Content = new PointerContactsTab() },
new TabItem() { Header = "IntermediatePoints", Content = new PointerIntermediatePointsTab() }
}
};
}
class PointerContactsTab : Control
{
class PointerInfo
{
@ -45,7 +67,7 @@ namespace ControlCatalog.Pages
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
public PointersPage()
public PointerContactsTab()
{
ClipToBounds = true;
}
@ -104,4 +126,196 @@ namespace ControlCatalog.Pages
}
}
}
public class PointerIntermediatePointsTab : Decorator
{
public PointerIntermediatePointsTab()
{
this[TextBlock.ForegroundProperty] = Brushes.Black;
var slider = new Slider
{
Margin = new Thickness(5),
Minimum = 0,
Maximum = 500
};
var status = new TextBlock()
{
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
};
Child = new Grid
{
Children =
{
new PointerCanvas(slider, status),
new Border
{
Background = Brushes.LightYellow,
Child = new StackPanel
{
Children =
{
new StackPanel
{
Orientation = Orientation.Horizontal,
Children =
{
new TextBlock { Text = "Thread sleep:" },
new TextBlock()
{
[!TextBlock.TextProperty] =slider.GetObservable(Slider.ValueProperty)
.Select(x=>x.ToString()).ToBinding()
}
}
},
slider
}
},
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Width = 300,
Height = 60
},
status
}
};
}
class PointerCanvas : Control
{
private readonly Slider _slider;
private readonly TextBlock _status;
private int _events;
private Stopwatch _stopwatch = Stopwatch.StartNew();
private Dictionary<int, PointerPoints> _pointers = new();
class PointerPoints
{
struct CanvasPoint
{
public IBrush Brush;
public Point Point;
public double Radius;
}
readonly CanvasPoint[] _points = new CanvasPoint[1000];
int _index;
public void Render(DrawingContext context)
{
CanvasPoint? prev = null;
for (var c = 0; c < _points.Length; c++)
{
var i = (c + _index) % _points.Length;
var pt = _points[i];
if (prev.HasValue && prev.Value.Brush != null && pt.Brush != null)
context.DrawLine(new Pen(Brushes.Black), prev.Value.Point, pt.Point);
prev = pt;
if (pt.Brush != null)
context.DrawEllipse(pt.Brush, null, pt.Point, pt.Radius, pt.Radius);
}
}
void AddPoint(Point pt, IBrush brush, double radius)
{
_points[_index] = new CanvasPoint { Point = pt, Brush = brush, Radius = radius };
_index = (_index + 1) % _points.Length;
}
public void HandleEvent(PointerEventArgs e, Visual v)
{
e.Handled = true;
if (e.RoutedEvent == PointerPressedEvent)
AddPoint(e.GetPosition(v), Brushes.Green, 10);
else if (e.RoutedEvent == PointerReleasedEvent)
AddPoint(e.GetPosition(v), Brushes.Red, 10);
else
{
var pts = e.GetIntermediatePoints(v);
for (var c = 0; c < pts.Count; c++)
{
var pt = pts[c];
AddPoint(pt.Position, c == pts.Count - 1 ? Brushes.Blue : Brushes.Black,
c == pts.Count - 1 ? 5 : 2);
}
}
}
}
public PointerCanvas(Slider slider, TextBlock status)
{
_slider = slider;
_status = status;
DispatcherTimer.Run(() =>
{
if (_stopwatch.Elapsed.TotalSeconds > 1)
{
_status.Text = "Events per second: " + (_events / _stopwatch.Elapsed.TotalSeconds);
_stopwatch.Restart();
_events = 0;
}
return this.GetVisualRoot() != null;
}, TimeSpan.FromMilliseconds(10));
}
void HandleEvent(PointerEventArgs e)
{
_events++;
Thread.Sleep((int)_slider.Value);
InvalidateVisual();
if (e.RoutedEvent == PointerReleasedEvent && e.Pointer.Type == PointerType.Touch)
{
_pointers.Remove(e.Pointer.Id);
return;
}
if (!_pointers.TryGetValue(e.Pointer.Id, out var pt))
_pointers[e.Pointer.Id] = pt = new PointerPoints();
pt.HandleEvent(e, this);
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.White, Bounds);
foreach(var pt in _pointers.Values)
pt.Render(context);
base.Render(context);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.ClickCount == 2)
{
_pointers.Clear();
InvalidateVisual();
return;
}
HandleEvent(e);
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
HandleEvent(e);
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
HandleEvent(e);
base.OnPointerReleased(e);
}
}
}
}

37
samples/ControlCatalog/Pages/ScreenPage.cs

@ -1,4 +1,5 @@
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
@ -49,25 +50,33 @@ namespace ControlCatalog.Pages
context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect);
var text = new FormattedText() { Typeface = new Typeface("Arial"), FontSize = 18 };
text.Text = $"Bounds: {screen.Bounds.TopLeft} {screen.Bounds.Width}:{screen.Bounds.Height}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text);
text.Text = $"WorkArea: {screen.WorkingArea.TopLeft} {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
text.Text = $"Scaling: {screen.PixelDensity * 100}%";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text);
text.Text = $"Primary: {screen.Primary}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text);
text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
formattedText =
CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
formattedText = CreateFormattedText($"Scaling: {screen.PixelDensity * 100}%");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
formattedText = CreateFormattedText($"Primary: {screen.Primary}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
formattedText =
CreateFormattedText(
$"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
}
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
}
private FormattedText CreateFormattedText(string textToFormat)
{
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
Typeface.Default, 12, Brushes.Green);
}
}
}

22
samples/ControlCatalog/Pages/ViewboxPage.xaml

@ -1,5 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:collections="clr-namespace:System.Collections;assembly=netstandard"
x:Class="ControlCatalog.Pages.ViewboxPage">
<Grid RowDefinitions="Auto,*,*">
@ -12,8 +13,8 @@
<Border HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="Orange" Width="200" Height="200">
<Border VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="CornflowerBlue" Width="{Binding #WidthSlider.Value}" Height="{Binding #HeightSlider.Value}" >
<Viewbox
Stretch="{Binding #StretchSelector.SelectedItem}"
StretchDirection="{Binding #StretchDirectionSelector.SelectedItem}">
Stretch="{Binding #StretchSelector.SelectedItem, FallbackValue={x:Static Stretch.Uniform}}"
StretchDirection="{Binding #StretchDirectionSelector.SelectedItem, FallbackValue={x:Static StretchDirection.Both}}">
<Ellipse Width="50" Height="50" Fill="CornflowerBlue" />
</Viewbox>
</Border>
@ -25,9 +26,22 @@
<TextBlock Text="Height" />
<Slider Minimum="10" Maximum="200" Value="100" x:Name="HeightSlider" TickFrequency="25" TickPlacement="TopLeft" />
<TextBlock Text="Stretch" />
<ComboBox x:Name="StretchSelector" HorizontalAlignment="Stretch" Margin="0,0,0,2" />
<ComboBox x:Name="StretchSelector" HorizontalAlignment="Stretch" SelectedIndex="0" Margin="0,0,0,2">
<collections:ArrayList>
<Stretch>Uniform</Stretch>
<Stretch>UniformToFill</Stretch>
<Stretch>Fill</Stretch>
<Stretch>None</Stretch>
</collections:ArrayList>
</ComboBox>
<TextBlock Text="Stretch Direction" />
<ComboBox x:Name="StretchDirectionSelector" HorizontalAlignment="Stretch" />
<ComboBox x:Name="StretchDirectionSelector" SelectedIndex="0" HorizontalAlignment="Stretch">
<collections:ArrayList>
<StretchDirection>Both</StretchDirection>
<StretchDirection>DownOnly</StretchDirection>
<StretchDirection>UpOnly</StretchDirection>
</collections:ArrayList>
</ComboBox>
</StackPanel>
</Grid>

19
samples/ControlCatalog/Pages/ViewboxPage.xaml.cs

@ -1,6 +1,5 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace ControlCatalog.Pages
{
@ -9,24 +8,6 @@ namespace ControlCatalog.Pages
public ViewboxPage()
{
InitializeComponent();
var stretchSelector = this.FindControl<ComboBox>("StretchSelector");
stretchSelector.Items = new[]
{
Stretch.Uniform, Stretch.UniformToFill, Stretch.Fill, Stretch.None
};
stretchSelector.SelectedIndex = 0;
var stretchDirectionSelector = this.FindControl<ComboBox>("StretchDirectionSelector");
stretchDirectionSelector.Items = new[]
{
StretchDirection.Both, StretchDirection.DownOnly, StretchDirection.UpOnly
};
stretchDirectionSelector.SelectedIndex = 0;
}
private void InitializeComponent()

13
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -5,6 +5,7 @@ using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
using Avalonia.Platform;
using System;
using System.ComponentModel.DataAnnotations;
using MiniMvvm;
namespace ControlCatalog.ViewModels
@ -164,5 +165,17 @@ namespace ControlCatalog.ViewModels
public MiniCommand ExitCommand { get; }
public MiniCommand ToggleMenuItemCheckedCommand { get; }
private DateTime? _validatedDateExample;
/// <summary>
/// A required DateTime which should demonstrate validation for the DateTimePicker
/// </summary>
[Required]
public DateTime? ValidatedDateExample
{
get => _validatedDateExample;
set => this.RaiseAndSetIfChanged(ref _validatedDateExample, value);
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -57,6 +57,9 @@
<TabItem Header="GlyphRun">
<pages:GlyphRunPage />
</TabItem>
<TabItem Header="FormattedText">
<pages:FormattedTextPage />
</TabItem>
<TabItem Header="LineBounds">
<pages:LineBoundsPage />
</TabItem>

54
samples/RenderDemo/Pages/ClippingPage.xaml

@ -19,30 +19,36 @@
</Style>
</Styles>
</Grid.Styles>
<Border Name="clipped"
Background="Yellow"
Width="100"
Height="100"
Clip="M 58.625 0.07421875
C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703
C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594
C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312
C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875
C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422
C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125
C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172
C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438
C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953
C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078
C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594
C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859
C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766
C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359
C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531
C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609
C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344
C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812
C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z ">
<Border Name="clipped"
Background="Yellow"
Width="100"
Height="100"
Classes.clip="{Binding #useMask.IsChecked}">
<Border.Styles>
<Style Selector="Border.clip">
<Setter Property="Clip"
Value="M 58.625 0.07421875
C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703
C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594
C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312
C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875
C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422
C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125
C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172
C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438
C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953
C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078
C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594
C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859
C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766
C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359
C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531
C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609
C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344
C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812
C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z " />
</Style>
</Border.Styles>
<Border Name="clipChild" Background="Red" Margin="4">
<!-- Setting opacity puts the TextBox on a new layer -->
<TextBox Text="Avalonia" Opacity="0.9" VerticalAlignment="Center"/>

17
samples/RenderDemo/Pages/ClippingPage.xaml.cs

@ -1,35 +1,18 @@
using System;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace RenderDemo.Pages
{
public class ClippingPage : UserControl
{
private Geometry _clip;
public ClippingPage()
{
InitializeComponent();
WireUpCheckbox();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void WireUpCheckbox()
{
var useMask = this.FindControl<CheckBox>("useMask");
var clipped = this.FindControl<Border>("clipped");
_clip = clipped.Clip;
useMask.Click += (s, e) => clipped.Clip = clipped.Clip == null ? _clip : null;
}
}
}

13
samples/RenderDemo/Pages/CustomSkiaPage.cs

@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@ -41,7 +42,10 @@ namespace RenderDemo.Pages
{
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
if (canvas == null)
context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl);
using (var c = new DrawingContext(context, false))
{
c.DrawText(_noSkia, new Point());
}
else
{
canvas.Save();
@ -108,10 +112,9 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context)
{
var noSkia = new FormattedText()
{
Text = "Current rendering API is not Skia"
};
var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture,
FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black);
context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
}

7
samples/RenderDemo/Pages/FormattedTextPage.axaml

@ -0,0 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="RenderDemo.Pages.FormattedTextPage">
</UserControl>

60
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@ -0,0 +1,60 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace RenderDemo.Pages
{
public class FormattedTextPage : UserControl
{
public FormattedTextPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public override void Render(DrawingContext context)
{
const string testString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor";
// Create the initial formatted text string.
var formattedText = new FormattedText(
testString,
CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface("Verdana"),
32,
Brushes.Black) { MaxTextWidth = 300, MaxTextHeight = 240 };
// Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears.
// Use a larger font size beginning at the first (zero-based) character and continuing for 5 characters.
// The font size is calculated in terms of points -- not as device-independent pixels.
formattedText.SetFontSize(36 * (96.0 / 72.0), 0, 5);
// Use a Bold font weight beginning at the 6th character and continuing for 11 characters.
formattedText.SetFontWeight(FontWeight.Bold, 6, 11);
var gradient = new LinearGradientBrush
{
GradientStops =
new GradientStops { new GradientStop(Colors.Orange, 0), new GradientStop(Colors.Teal, 1) },
StartPoint = new RelativePoint(0,0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0,1, RelativeUnit.Relative)
};
// Use a linear gradient brush beginning at the 6th character and continuing for 11 characters.
formattedText.SetForegroundBrush(gradient, 6, 11);
// Use an Italic font style beginning at the 28th character and continuing for 28 characters.
formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
context.DrawText(formattedText, new Point(10, 0));
}
}
}

7
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -13,6 +13,7 @@ namespace RenderDemo.Pages
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
private char[] _characters = new char[1];
private float _fontSize = 20;
private int _direction = 10;
@ -38,7 +39,7 @@ namespace RenderDemo.Pages
private void UpdateGlyphRun()
{
var c = (uint)_rand.Next(65, 90);
var c = (char)_rand.Next(65, 90);
if (_fontSize + _direction > 200)
{
@ -54,6 +55,8 @@ namespace RenderDemo.Pages
_glyphIndices[0] = _glyphTypeface.GetGlyph(c);
_characters[0] = c;
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
var drawingGroup = new DrawingGroup();
@ -61,7 +64,7 @@ namespace RenderDemo.Pages
var glyphRunDrawing = new GlyphRunDrawing
{
Foreground = Brushes.Black,
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices),
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
};
drawingGroup.Children.Add(glyphRunDrawing);

5
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -10,7 +10,7 @@ using Avalonia.Input.Platform;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Shared.PlatformSupport;
using Avalonia.PlatformSupport;
using Avalonia.Skia;
namespace Avalonia
@ -59,8 +59,7 @@ namespace Avalonia.Android
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly));
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
SkiaPlatform.Initialize();

2
src/Android/Avalonia.Android/AppBuilder.cs

@ -1,5 +1,5 @@
using Avalonia.Controls;
using Avalonia.Shared.PlatformSupport;
using Avalonia.PlatformSupport;
namespace Avalonia
{

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

@ -5,9 +5,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj">
<SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<Import Project="..\..\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\AndroidWorkarounds.props" />
</Project>

18
src/Android/Avalonia.Android/RuntimeInfo.cs

@ -1,18 +0,0 @@
using Avalonia.Platform;
namespace Avalonia.Shared.PlatformSupport
{
internal partial class StandardRuntimePlatform
{
public RuntimePlatformInfo GetRuntimeInfo() => new RuntimePlatformInfo
{
IsCoreClr = false,
IsDesktop = false,
IsMobile = true,
IsDotNetFramework = false,
IsMono = true,
IsUnix = true,
OperatingSystem = OperatingSystemType.Android
};
}
}

16
src/Avalonia.Animation/Animatable.cs

@ -157,7 +157,7 @@ namespace Avalonia.Animation
state.Instance?.Dispose();
state.Instance = transition.Apply(
this,
Clock ?? AvaloniaLocator.Current.GetService<IGlobalClock>(),
Clock ?? AvaloniaLocator.Current.GetRequiredService<IGlobalClock>(),
oldValue,
newValue);
return;
@ -169,7 +169,7 @@ namespace Avalonia.Animation
base.OnPropertyChangedCore(change);
}
private void TransitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
private void TransitionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!_transitionsEnabled)
{
@ -179,14 +179,14 @@ namespace Avalonia.Animation
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddTransitions(e.NewItems);
AddTransitions(e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
RemoveTransitions(e.OldItems);
RemoveTransitions(e.OldItems!);
break;
case NotifyCollectionChangedAction.Replace:
RemoveTransitions(e.OldItems);
AddTransitions(e.NewItems);
RemoveTransitions(e.OldItems!);
AddTransitions(e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
throw new NotSupportedException("Transitions collection cannot be reset.");
@ -204,7 +204,7 @@ namespace Avalonia.Animation
for (var i = 0; i < items.Count; ++i)
{
var t = (ITransition)items[i];
var t = (ITransition)items[i]!;
_transitionState.Add(t, new TransitionState
{
@ -222,7 +222,7 @@ namespace Avalonia.Animation
for (var i = 0; i < items.Count; ++i)
{
var t = (ITransition)items[i];
var t = (ITransition)items[i]!;
if (_transitionState.TryGetValue(t, out var state))
{

46
src/Avalonia.Animation/Animation.cs

@ -203,7 +203,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <returns>The property animator type.</returns>
public static Type GetAnimator(IAnimationSetter setter)
public static Type? GetAnimator(IAnimationSetter setter)
{
if (s_animators.TryGetValue(setter, out var type))
{
@ -254,7 +254,7 @@ namespace Avalonia.Animation
Animators.Insert(0, (condition, typeof(TAnimator)));
}
private static Type GetAnimatorType(AvaloniaProperty property)
private static Type? GetAnimatorType(AvaloniaProperty property)
{
foreach (var (condition, type) in Animators)
{
@ -276,6 +276,11 @@ namespace Avalonia.Animation
{
foreach (var setter in keyframe.Setters)
{
if (setter.Property is null)
{
throw new InvalidOperationException("No Setter property assigned.");
}
var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
@ -305,7 +310,7 @@ namespace Avalonia.Animation
foreach (var (handlerType, property) in handlerList)
{
var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
var newInstance = (IAnimator)Activator.CreateInstance(handlerType)!;
newInstance.Property = property;
newAnimatorInstances.Add(newInstance);
}
@ -321,32 +326,43 @@ namespace Avalonia.Animation
}
/// <inheritdoc/>
public IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
public IDisposable Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
if (animators.Count == 1)
{
subscriptions.Add(animators[0].Apply(this, control, clock, match, onComplete));
var subscription = animators[0].Apply(this, control, clock, match, onComplete);
if (subscription is not null)
{
subscriptions.Add(subscription);
}
}
else
{
var completionTasks = onComplete != null ? new List<Task>() : null;
foreach (IAnimator animator in animators)
{
Action animatorOnComplete = null;
Action? animatorOnComplete = null;
if (onComplete != null)
{
var tcs = new TaskCompletionSource<object>();
var tcs = new TaskCompletionSource<object?>();
animatorOnComplete = () => tcs.SetResult(null);
completionTasks.Add(tcs.Task);
completionTasks!.Add(tcs.Task);
}
var subscription = animator.Apply(this, control, clock, match, animatorOnComplete);
if (subscription is not null)
{
subscriptions.Add(subscription);
}
subscriptions.Add(animator.Apply(this, control, clock, match, animatorOnComplete));
}
if (onComplete != null)
{
Task.WhenAll(completionTasks).ContinueWith(
(_, state) => ((Action)state).Invoke(),
Task.WhenAll(completionTasks!).ContinueWith(
(_, state) => ((Action)state!).Invoke(),
onComplete);
}
}
@ -354,25 +370,25 @@ namespace Avalonia.Animation
}
/// <inheritdoc/>
public Task RunAsync(Animatable control, IClock clock = null)
public Task RunAsync(Animatable control, IClock? clock = null)
{
return RunAsync(control, clock, default);
}
/// <inheritdoc/>
public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default)
public Task RunAsync(Animatable control, IClock? clock = null, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return Task.CompletedTask;
}
var run = new TaskCompletionSource<object>();
var run = new TaskCompletionSource<object?>();
if (this.IterationCount == IterationCount.Infinite)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
IDisposable subscriptions = null, cancellation = null;
IDisposable? subscriptions = null, cancellation = null;
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
run.TrySetResult(null);

33
src/Avalonia.Animation/AnimationInstance`1.cs

@ -31,15 +31,15 @@ namespace Avalonia.Animation
private TimeSpan _initialDelay;
private TimeSpan _iterationDelay;
private TimeSpan _duration;
private Easings.Easing _easeFunc;
private Action _onCompleteAction;
private Easings.Easing? _easeFunc;
private Action? _onCompleteAction;
private Func<double, T, T> _interpolator;
private IDisposable _timerSub;
private IDisposable? _timerSub;
private readonly IClock _baseClock;
private IClock _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedDelegate;
private IClock? _clock;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChangedDelegate;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action? OnComplete, Func<double, T, T> Interpolator)
{
_animator = animator;
_animation = animation;
@ -47,6 +47,9 @@ namespace Avalonia.Animation
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
_lastInterpValue = default!;
_firstKFValue = default!;
_neutralValue = default!;
FetchProperties();
}
@ -82,7 +85,7 @@ namespace Avalonia.Animation
_targetControl.PropertyChanged -= _propertyChangedDelegate;
_timerSub?.Dispose();
_clock.PlayState = PlayState.Stop;
_clock!.PlayState = PlayState.Stop;
}
protected override void Subscribed()
@ -108,6 +111,8 @@ namespace Avalonia.Animation
private void ApplyFinalFill()
{
if (_animator.Property is null)
throw new InvalidOperationException("Animator has no property specified.");
if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both)
_targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue);
}
@ -130,12 +135,12 @@ namespace Avalonia.Animation
private void DoPlayStates()
{
if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
if (_clock!.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
DoComplete();
if (!_gotFirstKFValue)
{
_firstKFValue = (T)_animator.First().Value;
_firstKFValue = (T)_animator.First().Value!;
_gotFirstKFValue = true;
}
}
@ -169,7 +174,7 @@ namespace Avalonia.Animation
// and snap the last iteration value to exact values.
if ((_currentIteration + 1) > _iterationCount)
{
var easedTime = _easeFunc.Ease(_playbackReversed ? 0.0 : 1.0);
var easedTime = _easeFunc!.Ease(_playbackReversed ? 0.0 : 1.0);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
DoComplete();
}
@ -203,7 +208,7 @@ namespace Avalonia.Animation
normalizedTime = 1 - normalizedTime;
// Ease and interpolate
var easedTime = _easeFunc.Ease(normalizedTime);
var easedTime = _easeFunc!.Ease(normalizedTime);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
PublishNext(_lastInterpValue);
@ -223,14 +228,14 @@ namespace Avalonia.Animation
private void UpdateNeutralValue()
{
var property = _animator.Property;
var property = _animator.Property ?? throw new InvalidOperationException("Animator has no property specified.");
var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue);
_neutralValue = baseValue != AvaloniaProperty.UnsetValue ?
(T)baseValue : (T)_targetControl.GetValue(property);
(T)baseValue! : (T)_targetControl.GetValue(property)!;
}
private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation)
{

20
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -12,22 +12,22 @@ namespace Avalonia.Animation
/// </summary>
public class AnimatorKeyFrame : AvaloniaObject
{
public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public static readonly DirectProperty<AnimatorKeyFrame, object?> ValueProperty =
AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object?>(nameof(Value), k => k.Value, (k, v) => k.Value = v);
public AnimatorKeyFrame()
{
}
public AnimatorKeyFrame(Type animatorType, Cue cue)
public AnimatorKeyFrame(Type? animatorType, Cue cue)
{
AnimatorType = animatorType;
Cue = cue;
KeySpline = null;
}
public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline)
public AnimatorKeyFrame(Type? animatorType, Cue cue, KeySpline? keySpline)
{
AnimatorType = animatorType;
Cue = cue;
@ -35,14 +35,14 @@ namespace Avalonia.Animation
}
internal bool isNeutral;
public Type AnimatorType { get; }
public Type? AnimatorType { get; }
public Cue Cue { get; }
public KeySpline KeySpline { get; }
public AvaloniaProperty Property { get; private set; }
public KeySpline? KeySpline { get; }
public AvaloniaProperty? Property { get; private set; }
private object _value;
private object? _value;
public object Value
public object? Value
{
get => _value;
set => SetAndRaise(ValueProperty, ref _value, value);
@ -80,7 +80,7 @@ namespace Avalonia.Animation
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
return (T)typeConv.ConvertTo(Value, typeof(T));
return (T)typeConv.ConvertTo(Value, typeof(T))!;
}
}
}

9
src/Avalonia.Animation/Animators/Animator`1.cs

@ -24,7 +24,7 @@ namespace Avalonia.Animation.Animators
/// <summary>
/// Gets or sets the target property for the keyframe.
/// </summary>
public AvaloniaProperty Property { get; set; }
public AvaloniaProperty? Property { get; set; }
public Animator()
{
@ -33,7 +33,7 @@ namespace Avalonia.Animation.Animators
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
public virtual IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete)
{
if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames();
@ -106,13 +106,16 @@ namespace Avalonia.Animation.Animators
public virtual IDisposable BindAnimation(Animatable control, IObservable<T> instance)
{
if (Property is null)
throw new InvalidOperationException("Animator has no property specified.");
return control.Bind((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete)
internal IDisposable Run(Animation animation, Animatable control, IClock? clock, Action? onComplete)
{
var instance = new AnimationInstance<T>(
animation,

4
src/Avalonia.Animation/Avalonia.Animation.csproj

@ -2,9 +2,13 @@
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project>

2
src/Avalonia.Animation/Clock.cs

@ -4,7 +4,7 @@ namespace Avalonia.Animation
{
public class Clock : ClockBase
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
public static IClock GlobalClock => AvaloniaLocator.Current.GetRequiredService<IGlobalClock>();
private readonly IDisposable _parentSubscription;

8
src/Avalonia.Animation/Cue.cs

@ -30,7 +30,7 @@ namespace Avalonia.Animation
/// <summary>
/// Parses a string to a <see cref="Cue"/> object.
/// </summary>
public static Cue Parse(string value, CultureInfo culture)
public static Cue Parse(string value, CultureInfo? culture)
{
string v = value;
@ -72,14 +72,14 @@ namespace Avalonia.Animation
public class CueTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return Cue.Parse((string)value, culture);
}
}
}
}

8
src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs

@ -8,15 +8,15 @@ namespace Avalonia.Animation
/// </summary>
internal class DisposeAnimationInstanceSubject<T> : IObserver<bool>, IDisposable
{
private IDisposable _lastInstance;
private IDisposable? _lastInstance;
private bool _lastMatch;
private Animator<T> _animator;
private Animation _animation;
private Animatable _control;
private Action _onComplete;
private IClock _clock;
private Action? _onComplete;
private IClock? _clock;
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock clock, Action onComplete)
public DisposeAnimationInstanceSubject(Animator<T> animator, Animation animation, Animatable control, IClock? clock, Action? onComplete)
{
this._animator = animator;
this._animation = animation;

4
src/Avalonia.Animation/Easing/Easing.cs

@ -15,7 +15,7 @@ namespace Avalonia.Animation.Easings
/// <inheritdoc/>
public abstract double Ease(double progress);
static Dictionary<string, Type> _easingTypes;
static Dictionary<string, Type>? _easingTypes;
static readonly Type s_thisType = typeof(Easing);
@ -48,7 +48,7 @@ namespace Avalonia.Animation.Easings
if (_easingTypes.ContainsKey(e))
{
var type = _easingTypes[e];
return (Easing)Activator.CreateInstance(type);
return (Easing)Activator.CreateInstance(type)!;
}
else
{

4
src/Avalonia.Animation/Easing/EasingTypeConverter.cs

@ -6,12 +6,12 @@ namespace Avalonia.Animation.Easings
{
public class EasingTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return Easing.Parse((string)value);
}

2
src/Avalonia.Animation/IAnimation.cs

@ -12,7 +12,7 @@ namespace Avalonia.Animation
/// <summary>
/// Apply the animation to the specified control and run it when <paramref name="match" /> produces <c>true</c>.
/// </summary>
IDisposable Apply(Animatable control, IClock clock, IObservable<bool> match, Action onComplete = null);
IDisposable Apply(Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete = null);
/// <summary>
/// Run the animation on the specified control.

4
src/Avalonia.Animation/IAnimationSetter.cs

@ -2,7 +2,7 @@ namespace Avalonia.Animation
{
public interface IAnimationSetter
{
AvaloniaProperty Property { get; set; }
object Value { get; set; }
AvaloniaProperty? Property { get; set; }
object? Value { get; set; }
}
}

4
src/Avalonia.Animation/IAnimator.cs

@ -11,11 +11,11 @@ namespace Avalonia.Animation
/// <summary>
/// The target property.
/// </summary>
AvaloniaProperty Property {get; set;}
AvaloniaProperty? Property {get; set;}
/// <summary>
/// Applies the current KeyFrame group to the specified control.
/// </summary>
IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete);
IDisposable? Apply(Animation animation, Animatable control, IClock? clock, IObservable<bool> match, Action? onComplete);
}
}

2
src/Avalonia.Animation/ITransition.cs

@ -10,7 +10,7 @@ namespace Avalonia.Animation
/// <summary>
/// Applies the transition to the specified <see cref="Animatable"/>.
/// </summary>
IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue);
IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue);
/// <summary>
/// Gets the property to be animated.

2
src/Avalonia.Animation/IterationCount.cs

@ -97,7 +97,7 @@ namespace Avalonia.Animation
/// </summary>
/// <param name="o">The object with which to test equality.</param>
/// <returns>True if the objects are equal, otherwise false.</returns>
public override bool Equals(object o)
public override bool Equals(object? o)
{
if (o == null)
{

4
src/Avalonia.Animation/IterationCountTypeConverter.cs

@ -6,12 +6,12 @@ namespace Avalonia.Animation
{
public class IterationCountTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return IterationCount.Parse((string)value);
}

4
src/Avalonia.Animation/KeyFrame.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
{
private TimeSpan _ktimeSpan;
private Cue _kCue;
private KeySpline _kKeySpline;
private KeySpline? _kKeySpline;
public KeyFrame()
{
@ -79,7 +79,7 @@ namespace Avalonia.Animation
/// Gets or sets the KeySpline of this <see cref="KeyFrame"/>.
/// </summary>
/// <value>The key spline.</value>
public KeySpline KeySpline
public KeySpline? KeySpline
{
get
{

4
src/Avalonia.Animation/KeySplineTypeConverter.cs

@ -12,12 +12,12 @@ namespace Avalonia.Animation
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
return KeySpline.Parse((string)value, CultureInfo.InvariantCulture);
}

21
src/Avalonia.Animation/Transition.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
@ -8,7 +9,7 @@ namespace Avalonia.Animation
/// </summary>
public abstract class Transition<T> : AvaloniaObject, ITransition
{
private AvaloniaProperty _prop;
private AvaloniaProperty? _prop;
/// <summary>
/// Gets or sets the duration of the transition.
@ -26,7 +27,8 @@ namespace Avalonia.Animation
public Easing Easing { get; set; } = new LinearEasing();
/// <inheritdocs/>
public AvaloniaProperty Property
[DisallowNull]
public AvaloniaProperty? Property
{
get
{
@ -42,16 +44,25 @@ namespace Avalonia.Animation
}
}
AvaloniaProperty ITransition.Property
{
get => Property ?? throw new InvalidOperationException("Transition has no property specified.");
set => Property = value;
}
/// <summary>
/// Apply interpolation to the property.
/// </summary>
public abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue);
/// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue)
public virtual IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue)
{
var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue);
if (Property is null)
throw new InvalidOperationException("Transition has no property specified.");
var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue!, (T)newValue!);
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}
}
}

6
src/Avalonia.Animation/TransitionInstance.cs

@ -10,11 +10,11 @@ namespace Avalonia.Animation
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
{
private IDisposable _timerSubscription;
private IDisposable? _timerSubscription;
private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
private TransitionClock _clock;
private TransitionClock? _clock;
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
@ -67,7 +67,7 @@ namespace Avalonia.Animation
protected override void Unsubscribed()
{
_timerSubscription?.Dispose();
_clock.PlayState = PlayState.Stop;
_clock!.PlayState = PlayState.Stop;
}
protected override void Subscribed()

1
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@ -146,6 +146,7 @@ namespace Avalonia.Collections
{
if (_inner.TryGetValue(key, out var value))
{
_inner.Remove(key);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));

21
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@ -59,7 +59,7 @@ namespace Avalonia.Collections
}
private class WeakCollectionChangedObservable : LightweightObservableBase<NotifyCollectionChangedEventArgs>,
IWeakSubscriber<NotifyCollectionChangedEventArgs>
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
private WeakReference<INotifyCollectionChanged> _sourceReference;
@ -68,31 +68,22 @@ namespace Avalonia.Collections
_sourceReference = source;
}
public void OnEvent(object? sender, NotifyCollectionChangedEventArgs e)
public void OnEvent(object? sender,
WeakEvent ev,
NotifyCollectionChangedEventArgs e)
{
PublishNext(e);
}
protected override void Initialize()
{
if (_sourceReference.TryGetTarget(out var instance))
{
WeakSubscriptionManager.Subscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
WeakEvents.CollectionChanged.Subscribe(instance, this);
}
protected override void Deinitialize()
{
if (_sourceReference.TryGetTarget(out var instance))
{
WeakSubscriptionManager.Unsubscribe(
instance,
nameof(instance.CollectionChanged),
this);
}
WeakEvents.CollectionChanged.Unsubscribe(instance, this);
}
}
}

10
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -23,18 +23,16 @@ namespace Avalonia.Data.Core
if (incc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
incc,
nameof(incc.CollectionChanged))
inputs.Add(WeakObservable.FromEventPattern(
incc, WeakEvents.CollectionChanged)
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}
if (inpc != null)
{
inputs.Add(WeakObservable.FromEventPattern<INotifyPropertyChanged, PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged))
inputs.Add(WeakObservable.FromEventPattern(
inpc, WeakEvents.PropertyChanged)
.Where(x => ShouldUpdate(x.Sender, x.EventArgs))
.Select(_ => GetValue(target)));
}

20
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -11,6 +11,12 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
public class IndeiValidationPlugin : IDataValidationPlugin
{
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(
(s, h) => s.ErrorsChanged += h,
(s, h) => s.ErrorsChanged -= h
);
/// <inheritdoc/>
public bool Match(WeakReference<object?> reference, string memberName)
{
@ -25,7 +31,7 @@ namespace Avalonia.Data.Core.Plugins
return new Validator(reference, name, accessor);
}
private class Validator : DataValidationBase, IWeakSubscriber<DataErrorsChangedEventArgs>
private class Validator : DataValidationBase, IWeakEventSubscriber<DataErrorsChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly string _name;
@ -37,7 +43,7 @@ namespace Avalonia.Data.Core.Plugins
_name = name;
}
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? sender, DataErrorsChangedEventArgs e)
void IWeakEventSubscriber<DataErrorsChangedEventArgs>.OnEvent(object? notifyDataErrorInfo, WeakEvent ev, DataErrorsChangedEventArgs e)
{
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{
@ -51,10 +57,7 @@ namespace Avalonia.Data.Core.Plugins
if (target != null)
{
WeakSubscriptionManager.Subscribe(
target,
nameof(target.ErrorsChanged),
this);
ErrorsChangedWeakEvent.Subscribe(target, this);
}
base.SubscribeCore();
@ -66,10 +69,7 @@ namespace Avalonia.Data.Core.Plugins
if (target != null)
{
WeakSubscriptionManager.Unsubscribe(
target,
nameof(target.ErrorsChanged),
this);
ErrorsChangedWeakEvent.Unsubscribe(target, this);
}
base.UnsubscribeCore();

24
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Reflection;
using Avalonia.Utilities;
@ -85,7 +86,7 @@ namespace Avalonia.Data.Core.Plugins
return found;
}
private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference<object?> _reference;
private readonly PropertyInfo _property;
@ -129,7 +130,8 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object? sender, PropertyChangedEventArgs e)
void IWeakEventSubscriber<PropertyChangedEventArgs>.
OnEvent(object? notifyPropertyChanged, WeakEvent ev, PropertyChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{
@ -148,13 +150,8 @@ namespace Avalonia.Data.Core.Plugins
{
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Unsubscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
if (inpc != null)
WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
}
private object? GetReferenceTarget()
@ -178,13 +175,8 @@ namespace Avalonia.Data.Core.Plugins
{
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Subscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
if (inpc != null)
WeakEvents.PropertyChanged.Subscribe(inpc, this);
}
}
}

5
src/Avalonia.Base/Platform/IRuntimePlatform.cs

@ -1,5 +1,4 @@
using System;
using System.Reflection;
namespace Avalonia.Platform
{
@ -23,6 +22,7 @@ namespace Avalonia.Platform
public OperatingSystemType OperatingSystem { get; set; }
public bool IsDesktop { get; set; }
public bool IsMobile { get; set; }
public bool IsBrowser { get; set; }
public bool IsCoreClr { get; set; }
public bool IsMono { get; set; }
public bool IsDotNetFramework { get; set; }
@ -36,6 +36,7 @@ namespace Avalonia.Platform
Linux,
OSX,
Android,
iOS
iOS,
Browser
}
}

2
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@ -30,7 +30,7 @@ namespace Avalonia.Threading
/// <inheritdoc/>
public override void Post(SendOrPostCallback d, object? state)
{
Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Send);
Dispatcher.UIThread.Post(() => d(state), DispatcherPriority.Background);
}
/// <inheritdoc/>

7
src/Avalonia.Base/Threading/Dispatcher.cs

@ -74,6 +74,13 @@ namespace Avalonia.Threading
/// </summary>
/// <param name="minimumPriority"></param>
public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority);
/// <summary>
/// Use this method to check if there are more prioritized tasks
/// </summary>
/// <param name="minimumPriority"></param>
public bool HasJobsWithPriority(DispatcherPriority minimumPriority) =>
_jobRunner.HasJobsWithPriority(minimumPriority);
/// <inheritdoc/>
public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)

15
src/Avalonia.Base/Threading/JobRunner.cs

@ -121,6 +121,21 @@ namespace Avalonia.Threading
return null;
}
public bool HasJobsWithPriority(DispatcherPriority minimumPriority)
{
for (int c = (int)minimumPriority; c < (int)DispatcherPriority.MaxValue; c++)
{
var q = _queues[c];
lock (q)
{
if (q.Count > 0)
return true;
}
}
return false;
}
private interface IJob
{
/// <summary>

12
src/Avalonia.Base/Utilities/IWeakEventSubscriber.cs

@ -0,0 +1,12 @@
using System;
namespace Avalonia.Utilities;
/// <summary>
/// Defines a listener to a event subscribed vis the <see cref="WeakEvent{TTarget, TEventArgs}"/>.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
public interface IWeakEventSubscriber<in TEventArgs> where TEventArgs : EventArgs
{
void OnEvent(object? sender, WeakEvent ev, TEventArgs e);
}

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

@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Avalonia.Utilities
{
public struct ImmutableReadOnlyListStructEnumerator<T> : IEnumerator, IEnumerator<T>
public struct ImmutableReadOnlyListStructEnumerator<T> : IEnumerator<T>
{
private readonly IReadOnlyList<T> _readOnlyList;
private int _pos;

187
src/Avalonia.Base/Utilities/WeakEvent.cs

@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Threading;
namespace Avalonia.Utilities;
/// <summary>
/// Manages subscriptions to events using weak listeners.
/// </summary>
public class WeakEvent<TSender, TEventArgs> : WeakEvent where TEventArgs : EventArgs where TSender : class
{
private readonly Func<TSender, EventHandler<TEventArgs>, Action> _subscribe;
readonly ConditionalWeakTable<object, Subscription> _subscriptions = new();
internal WeakEvent(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe)
{
_subscribe = (t, s) =>
{
subscribe(t, s);
return () => unsubscribe(t, s);
};
}
internal WeakEvent(Func<TSender, EventHandler<TEventArgs>, Action> subscribe)
{
_subscribe = subscribe;
}
public void Subscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (!_subscriptions.TryGetValue(target, out var subscription))
_subscriptions.Add(target, subscription = new Subscription(this, target));
subscription.Add(new WeakReference<IWeakEventSubscriber<TEventArgs>>(subscriber));
}
public void Unsubscribe(TSender target, IWeakEventSubscriber<TEventArgs> subscriber)
{
if (_subscriptions.TryGetValue(target, out var subscription))
subscription.Remove(subscriber);
}
private class Subscription
{
private readonly WeakEvent<TSender, TEventArgs> _ev;
private readonly TSender _target;
private readonly Action _compact;
private WeakReference<IWeakEventSubscriber<TEventArgs>>?[] _data =
new WeakReference<IWeakEventSubscriber<TEventArgs>>[16];
private int _count;
private readonly Action _unsubscribe;
private bool _compactScheduled;
public Subscription(WeakEvent<TSender, TEventArgs> ev, TSender target)
{
_ev = ev;
_target = target;
_compact = Compact;
_unsubscribe = ev._subscribe(target, OnEvent);
}
void Destroy()
{
_unsubscribe();
_ev._subscriptions.Remove(_target);
}
public void Add(WeakReference<IWeakEventSubscriber<TEventArgs>> s)
{
if (_count == _data.Length)
{
//Extend capacity
var extendedData = new WeakReference<IWeakEventSubscriber<TEventArgs>>?[_data.Length * 2];
Array.Copy(_data, extendedData, _data.Length);
_data = extendedData;
}
_data[_count] = s;
_count++;
}
public void Remove(IWeakEventSubscriber<TEventArgs> s)
{
var removed = false;
for (int c = 0; c < _count; ++c)
{
var reference = _data[c];
if (reference != null && reference.TryGetTarget(out var instance) && instance == s)
{
_data[c] = null;
removed = true;
}
}
if (removed)
{
ScheduleCompact();
}
}
void ScheduleCompact()
{
if(_compactScheduled)
return;
_compactScheduled = true;
Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background);
}
void Compact()
{
_compactScheduled = false;
int empty = -1;
for (var c = 0; c < _count; c++)
{
var r = _data[c];
//Mark current index as first empty
if (r == null && empty == -1)
empty = c;
//If current element isn't null and we have an empty one
if (r != null && empty != -1)
{
_data[c] = null;
_data[empty] = r;
empty++;
}
}
if (empty != -1)
_count = empty;
if (_count == 0)
Destroy();
}
void OnEvent(object? sender, TEventArgs eventArgs)
{
var needCompact = false;
for (var c = 0; c < _count; c++)
{
var r = _data[c];
if (r?.TryGetTarget(out var sub) == true)
sub!.OnEvent(_target, _ev, eventArgs);
else
needCompact = true;
}
if (needCompact)
ScheduleCompact();
}
}
}
public class WeakEvent
{
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Action<TSender, EventHandler<TEventArgs>> subscribe,
Action<TSender, EventHandler<TEventArgs>> unsubscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe, unsubscribe);
}
public static WeakEvent<TSender, TEventArgs> Register<TSender, TEventArgs>(
Func<TSender, EventHandler<TEventArgs>, Action> subscribe) where TSender : class where TEventArgs : EventArgs
{
return new WeakEvent<TSender, TEventArgs>(subscribe);
}
public static WeakEvent<TSender, EventArgs> Register<TSender>(
Action<TSender, EventHandler> subscribe,
Action<TSender, EventHandler> unsubscribe) where TSender : class
{
return Register<TSender, EventArgs>((s, h) =>
{
EventHandler handler = (_, e) => h(s, e);
subscribe(s, handler);
return () => unsubscribe(s, handler);
});
}
}

40
src/Avalonia.Base/Utilities/WeakEvents.cs

@ -0,0 +1,40 @@
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Input;
namespace Avalonia.Utilities;
public class WeakEvents
{
/// <summary>
/// Represents CollectionChanged event from <see cref="INotifyCollectionChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>
CollectionChanged = WeakEvent.Register<INotifyCollectionChanged, NotifyCollectionChangedEventArgs>(
(c, s) =>
{
NotifyCollectionChangedEventHandler handler = (_, e) => s(c, e);
c.CollectionChanged += handler;
return () => c.CollectionChanged -= handler;
});
/// <summary>
/// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/>
/// </summary>
public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs>
PropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>(
(s, h) =>
{
PropertyChangedEventHandler handler = (_, e) => h(s, e);
s.PropertyChanged += handler;
return () => s.PropertyChanged -= handler;
});
/// <summary>
/// Represents CanExecuteChanged event from <see cref="ICommand"/>
/// </summary>
public static readonly WeakEvent<ICommand, EventArgs> CommandCanExecuteChanged =
WeakEvent.Register<ICommand>((s, h) => s.CanExecuteChanged += h,
(s, h) => s.CanExecuteChanged -= h);
}

35
src/Avalonia.Base/Utilities/WeakObservable.cs

@ -18,6 +18,7 @@ namespace Avalonia.Utilities
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="eventName">Name of the event to convert.</param>
/// <returns></returns>
[Obsolete("Use WeakEvent-based overload")]
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target,
string eventName)
@ -34,7 +35,9 @@ namespace Avalonia.Utilities
}).Publish().RefCount();
}
private class Handler<TEventArgs> : IWeakSubscriber<TEventArgs> where TEventArgs : EventArgs
private class Handler<TEventArgs>
: IWeakSubscriber<TEventArgs>,
IWeakEventSubscriber<TEventArgs> where TEventArgs : EventArgs
{
private IObserver<EventPattern<object, TEventArgs>> _observer;
@ -47,6 +50,36 @@ namespace Avalonia.Utilities
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
public void OnEvent(object? sender, WeakEvent ev, TEventArgs e)
{
_observer.OnNext(new EventPattern<object, TEventArgs>(sender, e));
}
}
/// <summary>
/// Converts a WeakEvent conforming to the standard .NET event pattern into an observable
/// sequence, subscribing weakly.
/// </summary>
/// <typeparam name="TTarget">The type of target.</typeparam>
/// <typeparam name="TEventArgs">The type of the event args.</typeparam>
/// <param name="target">Object instance that exposes the event to convert.</param>
/// <param name="ev">The weak event to convert.</param>
/// <returns></returns>
public static IObservable<EventPattern<object, TEventArgs>> FromEventPattern<TTarget, TEventArgs>(
TTarget target, WeakEvent<TTarget, TEventArgs> ev)
where TEventArgs : EventArgs where TTarget : class
{
_ = target ?? throw new ArgumentNullException(nameof(target));
_ = ev ?? throw new ArgumentNullException(nameof(ev));
return Observable.Create<EventPattern<object, TEventArgs>>(observer =>
{
var handler = new Handler<TEventArgs>(observer);
ev.Subscribe(target, handler);
return () => ev.Unsubscribe(target, handler);
}).Publish().RefCount();
}
}
}

1
src/Avalonia.Base/Utilities/WeakSubscriptionManager.cs

@ -19,6 +19,7 @@ namespace Avalonia.Utilities
/// <param name="target">The event source.</param>
/// <param name="eventName">The name of the event.</param>
/// <param name="subscriber">The subscriber.</param>
[Obsolete("Use WeakEvent")]
public static void Subscribe<TTarget, TEventArgs>(TTarget target, string eventName, IWeakSubscriber<TEventArgs> subscriber)
where TEventArgs : EventArgs
{

10
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -2215,7 +2215,14 @@ namespace Avalonia.Controls
/// <param name="e">PointerWheelEventArgs</param>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta);
if(UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta))
{
e.Handled = true;
}
else
{
e.Handled = e.Handled || !ScrollViewer.GetIsScrollChainingEnabled(this);
}
}
internal bool UpdateScroll(Vector delta)
@ -5751,6 +5758,7 @@ namespace Avalonia.Controls
return true;
}
// Unselect everything except the row that was clicked on
_noSelectionChangeCount++;
try
{
UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);

4
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -448,7 +448,7 @@ namespace Avalonia.Controls
internal set;
}
public bool IsReadOnly
public virtual bool IsReadOnly
{
get
{
@ -680,7 +680,7 @@ namespace Avalonia.Controls
public void ClearSort()
{
//InvokeProcessSort is already validating if sorting is possible
_headerCell?.InvokeProcessSort(Input.KeyModifiers.Control);
_headerCell?.InvokeProcessSort(KeyboardHelper.GetPlatformCtrlOrCmdKeyModifier());
}
/// <summary>

72
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -1,4 +1,4 @@
// (c) Copyright Microsoft Corporation.
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
@ -15,7 +15,7 @@ namespace Avalonia.Controls
{
public class DataGridTemplateColumn : DataGridColumn
{
IDataTemplate _cellTemplate;
private IDataTemplate _cellTemplate;
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
@ -30,17 +30,38 @@ namespace Avalonia.Controls
set { SetAndRaise(CellTemplateProperty, ref _cellTemplate, value); }
}
private IDataTemplate _cellEditingCellTemplate;
/// <summary>
/// Defines the <see cref="CellEditingTemplate"/> property.
/// </summary>
public static readonly DirectProperty<DataGridTemplateColumn, IDataTemplate> CellEditingTemplateProperty =
AvaloniaProperty.RegisterDirect<DataGridTemplateColumn, IDataTemplate>(
nameof(CellEditingTemplate),
o => o.CellEditingTemplate,
(o, v) => o.CellEditingTemplate = v);
/// <summary>
/// Gets or sets the <see cref="IDataTemplate"/> which is used for the editing mode of the current <see cref="DataGridCell"/>
/// </summary>
/// <value>
/// An <see cref="IDataTemplate"/> for the editing mode of the current <see cref="DataGridCell"/>
/// </value>
/// <remarks>
/// If this property is <see langword="null"/> the column is read-only.
/// </remarks>
public IDataTemplate CellEditingTemplate
{
get => _cellEditingCellTemplate;
set => SetAndRaise(CellEditingTemplateProperty, ref _cellEditingCellTemplate, value);
}
private void OnCellTemplateChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldValue = (IDataTemplate)e.OldValue;
var value = (IDataTemplate)e.NewValue;
}
public DataGridTemplateColumn()
{
IsReadOnly = true;
}
protected override IControl GenerateElement(DataGridCell cell, object dataItem)
{
if(CellTemplate != null)
@ -60,7 +81,22 @@ namespace Avalonia.Controls
protected override IControl GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding)
{
binding = null;
return GenerateElement(cell, dataItem);
if(CellEditingTemplate != null)
{
return CellEditingTemplate.Build(dataItem);
}
else if (CellTemplate != null)
{
return CellTemplate.Build(dataItem);
}
if (Design.IsDesignMode)
{
return null;
}
else
{
throw DataGridError.DataGridTemplateColumn.MissingTemplateForType(typeof(DataGridTemplateColumn));
}
}
protected override object PrepareCellForEdit(IControl editingElement, RoutedEventArgs editingEventArgs)
@ -70,12 +106,30 @@ namespace Avalonia.Controls
protected internal override void RefreshCellContent(IControl element, string propertyName)
{
if(propertyName == nameof(CellTemplate) && element.Parent is DataGridCell cell)
var cell = element.Parent as DataGridCell;
if(propertyName == nameof(CellTemplate) && cell is not null)
{
cell.Content = GenerateElement(cell, cell.DataContext);
}
base.RefreshCellContent(element, propertyName);
}
public override bool IsReadOnly
{
get
{
if (CellEditingTemplate is null)
{
return true;
}
return base.IsReadOnly;
}
set
{
base.IsReadOnly = value;
}
}
}
}

21
src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs

@ -4,22 +4,29 @@
// All other rights reserved.
using Avalonia.Input;
using Avalonia.Input.Platform;
namespace Avalonia.Controls.Utils
{
internal static class KeyboardHelper
{
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift)
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift)
{
ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control;
shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift;
ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier());
shift = modifiers.HasFlag(KeyModifiers.Shift);
}
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrl, out bool shift, out bool alt)
public static void GetMetaKeyState(KeyModifiers modifiers, out bool ctrlOrCmd, out bool shift, out bool alt)
{
ctrl = (modifiers & KeyModifiers.Control) == KeyModifiers.Control;
shift = (modifiers & KeyModifiers.Shift) == KeyModifiers.Shift;
alt = (modifiers & KeyModifiers.Alt) == KeyModifiers.Alt;
ctrlOrCmd = modifiers.HasFlag(GetPlatformCtrlOrCmdKeyModifier());
shift = modifiers.HasFlag(KeyModifiers.Shift);
alt = modifiers.HasFlag(KeyModifiers.Alt);
}
public static KeyModifiers GetPlatformCtrlOrCmdKeyModifier()
{
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
return keymap?.CommandModifiers ?? KeyModifiers.Control;
}
}
}

12
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -29,15 +29,25 @@ MembersMustExist : Member 'public void Avalonia.Controls.NumericUpDownValueChang
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.NewValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.TopLevel' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Window' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.WindowBase' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.TryShutdown(System.Int32)' is present in the implementation but not in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Embedding.EmbeddableControlRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' 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.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation.
MembersMustExist : Member 'protected Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.CreateFormattedText()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.FormattedText.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Int32 Avalonia.Controls.Presenters.TextPresenter.GetCaretIndex(Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.TextPresenter.InvalidateFormattedText()' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does 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 System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
@ -57,4 +67,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 58
Total Issues: 68

96
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -76,36 +76,21 @@ namespace Avalonia.Controls.ApplicationLifetimes
return;
if (ShutdownMode == ShutdownMode.OnLastWindowClose && _windows.Count == 0)
Shutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && window == MainWindow)
Shutdown();
TryShutdown();
else if (ShutdownMode == ShutdownMode.OnMainWindowClose && ReferenceEquals(window, MainWindow))
TryShutdown();
}
public void Shutdown(int exitCode = 0)
{
if (_isShuttingDown)
throw new InvalidOperationException("Application is already shutting down.");
_exitCode = exitCode;
_isShuttingDown = true;
DoShutdown(new ShutdownRequestedEventArgs(), true, exitCode);
}
try
{
foreach (var w in Windows)
w.Close();
var e = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, e);
_exitCode = e.ApplicationExitCode;
}
finally
{
_cts?.Cancel();
_cts = null;
_isShuttingDown = false;
}
public bool TryShutdown(int exitCode = 0)
{
return DoShutdown(new ShutdownRequestedEventArgs(), false, exitCode);
}
public int Start(string[] args)
{
Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
@ -114,7 +99,10 @@ namespace Avalonia.Controls.ApplicationLifetimes
if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
{
((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
if (Application.Current is IApplicationPlatformEvents events)
{
events.RaiseUrlsOpened(args);
}
}
var lifetimeEvents = AvaloniaLocator.Current.GetService<IPlatformLifetimeEventsImpl>();
@ -145,23 +133,57 @@ namespace Avalonia.Controls.ApplicationLifetimes
if (_activeLifetime == this)
_activeLifetime = null;
}
private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e)
private bool DoShutdown(ShutdownRequestedEventArgs e, bool force = false, int exitCode = 0)
{
ShutdownRequested?.Invoke(this, e);
if (!force)
{
ShutdownRequested?.Invoke(this, e);
if (e.Cancel)
return;
if (e.Cancel)
return false;
if (_isShuttingDown)
throw new InvalidOperationException("Application is already shutting down.");
}
_exitCode = exitCode;
_isShuttingDown = true;
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
foreach (var w in Windows)
if (w.Owner is null)
w.Close();
if (Windows.Count > 0)
e.Cancel = true;
try
{
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
foreach (var w in Windows)
{
if (w.Owner is null)
{
w.Close();
}
}
if (!force && Windows.Count > 0)
{
e.Cancel = true;
return false;
}
var args = new ControlledApplicationLifetimeExitEventArgs(exitCode);
Exit?.Invoke(this, args);
_exitCode = args.ApplicationExitCode;
}
finally
{
_cts?.Cancel();
_cts = null;
_isShuttingDown = false;
}
return true;
}
private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e) => DoShutdown(e);
}
public class ClassicDesktopStyleApplicationLifetimeOptions

6
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@ -9,6 +9,12 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// </summary>
public interface IClassicDesktopStyleApplicationLifetime : IControlledApplicationLifetime
{
/// <summary>
/// Tries to Shutdown the application. <see cref="ShutdownRequested" /> event can be used to cancel the shutdown.
/// </summary>
/// <param name="exitCode">An integer exit code for an application. The default exit code is 0.</param>
bool TryShutdown(int exitCode = 0);
/// <summary>
/// Gets the arguments passed to the
/// <see cref="ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime{T}(T, string[], ShutdownMode)"/>

197
src/Avalonia.Controls/Button.cs

@ -59,13 +59,13 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Button, object>(nameof(CommandParameter));
/// <summary>
/// Defines the <see cref="IsDefaultProperty"/> property.
/// Defines the <see cref="IsDefault"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsDefaultProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsDefault));
/// <summary>
/// Defines the <see cref="IsCancelProperty"/> property.
/// Defines the <see cref="IsCancel"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsCancelProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsCancel));
@ -76,6 +76,9 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
RoutedEvent.Register<Button, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="IsPressed"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsPressedProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
@ -95,13 +98,12 @@ namespace Avalonia.Controls
static Button()
{
FocusableProperty.OverrideDefaultValue(typeof(Button), true);
CommandProperty.Changed.Subscribe(CommandChanged);
CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
IsCancelProperty.Changed.Subscribe(IsCancelChanged);
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>((lbl, args) => lbl.OnAccessKey(args));
}
/// <summary>
/// Initializes a new instance of the <see cref="Button"/> class.
/// </summary>
public Button()
{
UpdatePseudoClasses(IsPressed);
@ -112,8 +114,8 @@ namespace Avalonia.Controls
/// </summary>
public event EventHandler<RoutedEventArgs> Click
{
add { AddHandler(ClickEvent, value); }
remove { RemoveHandler(ClickEvent, value); }
add => AddHandler(ClickEvent, value);
remove => RemoveHandler(ClickEvent, value);
}
/// <summary>
@ -121,8 +123,8 @@ namespace Avalonia.Controls
/// </summary>
public ClickMode ClickMode
{
get { return GetValue(ClickModeProperty); }
set { SetValue(ClickModeProperty, value); }
get => GetValue(ClickModeProperty);
set => SetValue(ClickModeProperty, value);
}
/// <summary>
@ -130,8 +132,8 @@ namespace Avalonia.Controls
/// </summary>
public ICommand Command
{
get { return _command; }
set { SetAndRaise(CommandProperty, ref _command, value); }
get => _command;
set => SetAndRaise(CommandProperty, ref _command, value);
}
/// <summary>
@ -139,8 +141,8 @@ namespace Avalonia.Controls
/// </summary>
public KeyGesture HotKey
{
get { return GetValue(HotKeyProperty); }
set { SetValue(HotKeyProperty, value); }
get => GetValue(HotKeyProperty);
set => SetValue(HotKeyProperty, value);
}
/// <summary>
@ -148,8 +150,8 @@ namespace Avalonia.Controls
/// </summary>
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
get => GetValue(CommandParameterProperty);
set => SetValue(CommandParameterProperty, value);
}
/// <summary>
@ -158,8 +160,8 @@ namespace Avalonia.Controls
/// </summary>
public bool IsDefault
{
get { return GetValue(IsDefaultProperty); }
set { SetValue(IsDefaultProperty, value); }
get => GetValue(IsDefaultProperty);
set => SetValue(IsDefaultProperty, value);
}
/// <summary>
@ -168,18 +170,21 @@ namespace Avalonia.Controls
/// </summary>
public bool IsCancel
{
get { return GetValue(IsCancelProperty); }
set { SetValue(IsCancelProperty, value); }
get => GetValue(IsCancelProperty);
set => SetValue(IsCancelProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the button is currently pressed.
/// </summary>
public bool IsPressed
{
get { return GetValue(IsPressedProperty); }
private set { SetValue(IsPressedProperty, value); }
get => GetValue(IsPressedProperty);
private set => SetValue(IsPressedProperty, value);
}
/// <summary>
/// Gets or sets the Flyout that should be shown with this button
/// Gets or sets the Flyout that should be shown with this button.
/// </summary>
public FlyoutBase Flyout
{
@ -187,7 +192,8 @@ namespace Avalonia.Controls
set => SetValue(FlyoutProperty, value);
}
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
/// <inheritdoc/>
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
@ -224,6 +230,7 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control
@ -240,6 +247,7 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
// This will cause the hotkey manager to dispose the observer and the reference to this control
@ -358,12 +366,14 @@ namespace Avalonia.Controls
}
}
}
/// <inheritdoc/>
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
IsPressed = false;
}
/// <inheritdoc/>
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
@ -371,119 +381,93 @@ namespace Avalonia.Controls
IsPressed = false;
}
/// <inheritdoc/>
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == IsPressedProperty)
{
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
}
else if (change.Property == FlyoutProperty)
if (change.Property == CommandProperty)
{
// If flyout is changed while one is already open, make sure we
// close the old one first
if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout &&
oldFlyout.IsOpen)
if (((ILogical)this).IsAttachedToLogicalTree)
{
oldFlyout.Hide();
if (change.OldValue.GetValueOrDefault() is ICommand oldCommand)
{
oldCommand.CanExecuteChanged -= CanExecuteChanged;
}
if (change.NewValue.GetValueOrDefault() is ICommand newCommand)
{
newCommand.CanExecuteChanged += CanExecuteChanged;
}
}
}
}
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.UpdateDataValidation(property, value);
if (property == CommandProperty)
CanExecuteChanged(this, EventArgs.Empty);
}
else if (change.Property == CommandParameterProperty)
{
if (value.Type == BindingValueType.BindingError)
CanExecuteChanged(this, EventArgs.Empty);
}
else if (change.Property == IsCancelProperty)
{
var isCancel = change.NewValue.GetValueOrDefault<bool>();
if (VisualRoot is IInputElement inputRoot)
{
if (_commandCanExecute)
if (isCancel)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
ListenForCancel(inputRoot);
}
else
{
StopListeningForCancel(inputRoot);
}
}
}
}
/// <summary>
/// Called when the <see cref="Command"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is Button button)
else if (change.Property == IsDefaultProperty)
{
if (((ILogical)button).IsAttachedToLogicalTree)
var isDefault = change.NewValue.GetValueOrDefault<bool>();
if (VisualRoot is IInputElement inputRoot)
{
if (e.OldValue is ICommand oldCommand)
if (isDefault)
{
oldCommand.CanExecuteChanged -= button.CanExecuteChanged;
ListenForDefault(inputRoot);
}
if (e.NewValue is ICommand newCommand)
else
{
newCommand.CanExecuteChanged += button.CanExecuteChanged;
StopListeningForDefault(inputRoot);
}
}
button.CanExecuteChanged(button, EventArgs.Empty);
}
}
/// <summary>
/// Called when the <see cref="CommandParameter"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is Button button)
else if (change.Property == IsPressedProperty)
{
button.CanExecuteChanged(button, EventArgs.Empty);
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
}
}
/// <summary>
/// Called when the <see cref="IsDefault"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void IsDefaultChanged(AvaloniaPropertyChangedEventArgs e)
{
var button = e.Sender as Button;
var isDefault = (bool)e.NewValue;
if (button?.VisualRoot is IInputElement inputRoot)
else if (change.Property == FlyoutProperty)
{
if (isDefault)
{
button.ListenForDefault(inputRoot);
}
else
// If flyout is changed while one is already open, make sure we
// close the old one first
if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout &&
oldFlyout.IsOpen)
{
button.StopListeningForDefault(inputRoot);
oldFlyout.Hide();
}
}
}
/// <summary>
/// Called when the <see cref="IsCancel"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void IsCancelChanged(AvaloniaPropertyChangedEventArgs e)
/// <inheritdoc/>
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
var button = e.Sender as Button;
var isCancel = (bool)e.NewValue;
if (button?.VisualRoot is IInputElement inputRoot)
base.UpdateDataValidation(property, value);
if (property == CommandProperty)
{
if (isCancel)
{
button.ListenForCancel(inputRoot);
}
else
if (value.Type == BindingValueType.BindingError)
{
button.StopListeningForCancel(inputRoot);
if (_commandCanExecute)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
}
}
}
@ -566,6 +550,9 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Updates the visual state of the control by applying latest PseudoClasses.
/// </summary>
private void UpdatePseudoClasses(bool isPressed)
{
PseudoClasses.Set(":pressed", isPressed);

11
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -185,7 +185,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<CalendarDatePicker, DateTime?>(
nameof(SelectedDate),
o => o.SelectedDate,
(o, v) => o.SelectedDate = v);
(o, v) => o.SelectedDate = v,
enableDataValidation: true);
public static readonly StyledProperty<CalendarDatePickerFormat> SelectedDateFormatProperty =
AvaloniaProperty.Register<CalendarDatePicker, CalendarDatePickerFormat>(
@ -533,13 +534,11 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
{
base.OnPropertyChanged(change);
if (change.Property == SelectedDateProperty)
if (property == SelectedDateProperty)
{
DataValidationErrors.SetError(this, change.NewValue.Error);
DataValidationErrors.SetError(this, value.Error);
}
}

19
src/Avalonia.Controls/Control.cs

@ -1,10 +1,12 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.VisualTree;
@ -60,7 +62,13 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<ContextRequestedEventArgs> ContextRequestedEvent =
RoutedEvent.Register<Control, ContextRequestedEventArgs>(nameof(ContextRequested),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Control, Control, FlowDirection>(nameof(FlowDirection), inherits: true);
private DataTemplates? _dataTemplates;
private IControl? _focusAdorner;
@ -108,6 +116,15 @@ namespace Avalonia.Controls
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Occurs when the user has completed a context input gesture, such as a right-click.

13
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@ -43,5 +43,18 @@ namespace Avalonia.Controls
{
return new MenuItemContainerGenerator(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
foreach (var i in LogicalChildren)
{
if (i is MenuItem menuItem)
{
menuItem.IsSubMenuOpen = false;
}
}
}
}
}

2
src/Avalonia.Controls/LayoutTransformControl.cs

@ -18,7 +18,7 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<LayoutTransformControl, ITransform>(nameof(LayoutTransform));
public static readonly StyledProperty<bool> UseRenderTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(LayoutTransform));
AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(UseRenderTransform));
static LayoutTransformControl()
{

10
src/Avalonia.Controls/NativeMenuItem.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
}
class CanExecuteChangedSubscriber : IWeakSubscriber<EventArgs>
class CanExecuteChangedSubscriber : IWeakEventSubscriber<EventArgs>
{
private readonly NativeMenuItem _parent;
@ -42,7 +42,7 @@ namespace Avalonia.Controls
_parent = parent;
}
public void OnEvent(object sender, EventArgs e)
public void OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
_parent.CanExecuteChanged();
}
@ -160,14 +160,12 @@ namespace Avalonia.Controls
set
{
if (_command != null)
WeakSubscriptionManager.Unsubscribe(_command,
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
WeakEvents.CommandCanExecuteChanged.Unsubscribe(_command, _canExecuteChangedSubscriber);
SetAndRaise(CommandProperty, ref _command, value);
if (_command != null)
WeakSubscriptionManager.Subscribe(_command,
nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
WeakEvents.CommandCanExecuteChanged.Subscribe(_command, _canExecuteChangedSubscriber);
CanExecuteChanged();
}

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

@ -101,7 +101,7 @@ namespace Avalonia.Controls.Platform
root.Deactivated -= WindowDeactivated;
}
if (_root is TopLevel tl)
if (_root is TopLevel tl && tl.PlatformImpl != null)
tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
_inputManagerSubscription?.Dispose();

34
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -60,6 +60,12 @@ namespace Avalonia.Controls.Presenters
o => o.Viewport,
(o, v) => o.Viewport = v);
/// <summary>
/// Defines the <see cref="IsScrollChainingEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsScrollChainingEnabledProperty =
ScrollViewer.IsScrollChainingEnabledProperty.AddOwner<ScrollContentPresenter>();
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private bool _arranging;
@ -138,6 +144,20 @@ namespace Avalonia.Controls.Presenters
private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
}
/// <summary>
/// Gets or sets if scroll chaining is enabled. The default value is true.
/// </summary>
/// <remarks>
/// After a user hits a scroll limit on an element that has been nested within another scrollable element,
/// you can specify whether that parent element should continue the scrolling operation begun in its child element.
/// This is called scroll chaining.
/// </remarks>
public bool IsScrollChainingEnabled
{
get => GetValue(IsScrollChainingEnabledProperty);
set => SetValue(IsScrollChainingEnabledProperty, value);
}
/// <inheritdoc/>
IControl? IScrollAnchorProvider.CurrentAnchor
{
@ -405,8 +425,11 @@ namespace Avalonia.Controls.Presenters
_activeLogicalGestureScrolls[e.Id] = delta;
}
Offset = new Vector(x, y);
e.Handled = true;
Vector newOffset = new Vector(x, y);
bool offsetChanged = newOffset != Offset;
Offset = newOffset;
e.Handled = !IsScrollChainingEnabled || offsetChanged;
}
}
@ -440,8 +463,11 @@ namespace Avalonia.Controls.Presenters
x = Math.Min(x, Extent.Width - Viewport.Width);
}
Offset = new Vector(x, y);
e.Handled = true;
Vector newOffset = new Vector(x, y);
bool offsetChanged = newOffset != Offset;
Offset = newOffset;
e.Handled = !IsScrollChainingEnabled || offsetChanged;
}
}

493
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Linq;
using Avalonia.Input.TextInput;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using Avalonia.Layout;
using Avalonia.Media.Immutable;
@ -75,28 +78,18 @@ namespace Avalonia.Controls.Presenters
private int _selectionEnd;
private bool _caretBlink;
private string _text;
private FormattedText _formattedText;
private Size _constraint;
private TextLayout _textLayout;
private Size _constraint = Size.Infinity;
static TextPresenter()
{
AffectsRender<TextPresenter>(SelectionBrushProperty, TextBlock.ForegroundProperty,
SelectionForegroundBrushProperty, CaretBrushProperty,
SelectionStartProperty, SelectionEndProperty);
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty, RevealPasswordProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
private CharacterHit _lastCharacterHit;
private Rect _caretBounds;
private Point _navigationPosition;
Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
SelectionStartProperty.Changed, SelectionEndProperty.Changed,
SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed, RevealPasswordProperty.Changed
).AddClassHandler<TextPresenter>((x, _) => x.InvalidateFormattedText());
private ScrollViewer _scrollViewer;
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
static TextPresenter()
{
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty);
}
public TextPresenter()
@ -106,6 +99,8 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick += CaretTimerTick;
}
public event EventHandler CaretBoundsChanged;
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
@ -189,13 +184,22 @@ namespace Avalonia.Controls.Presenters
}
/// <summary>
/// Gets the <see cref="FormattedText"/> used to render the text.
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
public FormattedText FormattedText
public TextLayout TextLayout
{
get
{
return _formattedText ?? (_formattedText = CreateFormattedText());
if (_textLayout != null)
{
return _textLayout;
}
_textLayout = CreateTextLayout();
UpdateCaret(_lastCharacterHit);
return _textLayout;
}
}
@ -205,11 +209,12 @@ namespace Avalonia.Controls.Presenters
{
return _caretIndex;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
if (value != _caretIndex)
{
MoveCaretToTextPosition(value);
}
}
}
@ -271,37 +276,25 @@ namespace Avalonia.Controls.Presenters
}
}
public int GetCaretIndex(Point point)
{
var hit = FormattedText.HitTestPoint(point);
return hit.TextPosition + (hit.IsTrailing ? 1 : 0);
}
/// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text.
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns>
private FormattedText CreateFormattedTextInternal(Size constraint, string text)
{
return new FormattedText
{
Constraint = constraint,
Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize,
Text = text ?? string.Empty,
TextAlignment = TextAlignment,
TextWrapping = TextWrapping,
};
}
/// <param name="typeface"></param>
/// <param name="textStyleOverrides"></param>
/// <returns>A <see cref="TextLayout"/> object.</returns>
private TextLayout CreateTextLayoutInternal(Size constraint, string text, Typeface typeface,
IReadOnlyList<ValueSpan<TextRunProperties>> textStyleOverrides)
{
var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height;
var textLayout = new TextLayout(text ?? string.Empty, typeface, FontSize, Foreground, TextAlignment,
TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
flowDirection: FlowDirection);
/// <summary>
/// Invalidates <see cref="FormattedText"/>.
/// </summary>
protected void InvalidateFormattedText()
{
_formattedText = null;
return textLayout;
}
/// <summary>
@ -317,31 +310,36 @@ namespace Avalonia.Controls.Presenters
context.FillRectangle(background, new Rect(Bounds.Size));
}
double top = 0;
var textSize = FormattedText.Bounds.Size;
var top = 0d;
var left = 0.0;
var (_, textHeight) = TextLayout.Size;
if (Bounds.Height < textSize.Height)
if (Bounds.Height < textHeight)
{
switch (VerticalAlignment)
{
case VerticalAlignment.Center:
top += (Bounds.Height - textSize.Height) / 2;
top += (Bounds.Height - textHeight) / 2;
break;
case VerticalAlignment.Bottom:
top += (Bounds.Height - textSize.Height);
top += (Bounds.Height - textHeight);
break;
}
}
context.DrawText(Foreground, new Point(0, top), FormattedText);
TextLayout.Draw(context, new Point(left, top));
}
public override void Render(DrawingContext context)
{
FormattedText.Constraint = Bounds.Size;
_constraint = Bounds.Size;
if (double.IsPositiveInfinity (_constraint.Width))
{
_constraint = _scrollViewer?.Viewport ?? Size.Infinity;
InvalidateTextLayout();
}
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
@ -351,7 +349,7 @@ namespace Avalonia.Controls.Presenters
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
var rects = FormattedText.HitTestTextRange(start, length);
var rects = TextLayout.HitTestTextRange(start, length);
foreach (var rect in rects)
{
@ -361,40 +359,47 @@ namespace Avalonia.Controls.Presenters
RenderInternal(context);
if (selectionStart == selectionEnd && _caretBlink)
if (selectionStart != selectionEnd || !_caretBlink)
{
return;
}
var caretBrush = CaretBrush?.ToImmutable();
if (caretBrush is null)
{
var caretBrush = CaretBrush?.ToImmutable();
var backgroundColor = (Background as ISolidColorBrush)?.Color;
if (caretBrush is null)
if (backgroundColor.HasValue)
{
var backgroundColor = (Background as ISolidColorBrush)?.Color;
if (backgroundColor.HasValue)
{
byte red = (byte)~(backgroundColor.Value.R);
byte green = (byte)~(backgroundColor.Value.G);
byte blue = (byte)~(backgroundColor.Value.B);
var red = (byte)~(backgroundColor.Value.R);
var green = (byte)~(backgroundColor.Value.G);
var blue = (byte)~(backgroundColor.Value.B);
caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue));
}
else
{
caretBrush = Brushes.Black;
}
caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue));
}
else
{
caretBrush = Brushes.Black;
}
var (p1, p2) = GetCaretPoints();
context.DrawLine(
new ImmutablePen(caretBrush, 1),
p1, p2);
}
}
(Point, Point) GetCaretPoints()
var (p1, p2) = GetCaretPoints();
context.DrawLine(new ImmutablePen(caretBrush), p1, p2);
}
private (Point, Point) GetCaretPoints()
{
var charPos = FormattedText.HitTestTextPosition(CaretIndex);
var x = Math.Floor(charPos.X) + 0.5;
var y = Math.Floor(charPos.Y) + 0.5;
var b = Math.Ceiling(charPos.Bottom) - 0.5;
var x = Math.Floor(_caretBounds.X) + 0.5;
var y = Math.Floor(_caretBounds.Y) + 0.5;
var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
if (x >= Bounds.Width)
{
x = Math.Floor(_caretBounds.X - 1) + 0.5;
}
return (new Point(x, y), new Point(x, b));
}
@ -412,7 +417,7 @@ namespace Avalonia.Controls.Presenters
InvalidateVisual();
}
internal void CaretIndexChanged(int caretIndex)
internal void CaretChanged()
{
if (this.GetVisualParent() != null)
{
@ -432,8 +437,7 @@ namespace Avalonia.Controls.Presenters
if (IsMeasureValid)
{
var rect = FormattedText.HitTestTextPosition(caretIndex);
this.BringIntoView(rect);
this.BringIntoView(_caretBounds);
}
else
{
@ -443,8 +447,7 @@ namespace Avalonia.Controls.Presenters
Dispatcher.UIThread.Post(
() =>
{
var rect = FormattedText.HitTestTextPosition(caretIndex);
this.BringIntoView(rect);
this.BringIntoView(_caretBounds);
},
DispatcherPriority.Render);
}
@ -452,104 +455,310 @@ namespace Avalonia.Controls.Presenters
}
/// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text.
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="FormattedText"/> object.</returns>
protected virtual FormattedText CreateFormattedText()
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout()
{
FormattedText result = null;
TextLayout result;
var text = Text;
if (PasswordChar != default(char) && !RevealPassword)
{
result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
}
else
{
result = CreateFormattedTextInternal(_constraint, text);
}
var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
IReadOnlyList<ValueSpan<TextRunProperties>> textStyleOverrides = null;
if (length > 0)
{
result.Spans = new[]
textStyleOverrides = new[]
{
new FormattedTextStyleSpan(start, length, SelectionForegroundBrush),
new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: SelectionForegroundBrush ?? Brushes.White))
};
}
if (PasswordChar != default(char) && !RevealPassword)
{
result = CreateTextLayoutInternal(_constraint, new string(PasswordChar, text?.Length ?? 0), typeface,
textStyleOverrides);
}
else
{
result = CreateTextLayoutInternal(_constraint, text, typeface, textStyleOverrides);
}
return result;
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns>
private Size MeasureInternal(Size availableSize)
protected virtual void InvalidateTextLayout()
{
_textLayout = null;
InvalidateMeasure();
}
protected override Size MeasureOverride(Size availableSize)
{
if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint)
{
_constraint = availableSize;
InvalidateTextLayout();
}
return TextLayout.Size;
}
private int CoerceCaretIndex(int value)
{
var text = Text;
var length = text?.Length ?? 0;
return Math.Max(0, Math.Min(length, value));
}
private void CaretTimerTick(object sender, EventArgs e)
{
_caretBlink = !_caretBlink;
InvalidateVisual();
}
public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)
{
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge);
var textLine = TextLayout.TextLines[lineIndex];
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition));
var nextCaretCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
if (nextCaretCharacterHit.FirstCharacterIndex <= textPosition)
{
characterHit = nextCaretCharacterHit;
}
if (textPosition == characterHit.FirstCharacterIndex + characterHit.TrailingLength)
{
UpdateCaret(characterHit);
}
else
{
UpdateCaret(trailingEdge ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex));
}
_navigationPosition = _caretBounds.Position;
}
public void MoveCaretToPoint(Point point)
{
var hit = TextLayout.HitTestPoint(point);
UpdateCaret(hit.CharacterHit);
_navigationPosition = _caretBounds.Position;
}
public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
{
if (!string.IsNullOrEmpty(Text))
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0);
if (lineIndex < 0)
{
return;
}
var (currentX, currentY) = _navigationPosition;
if (direction == LogicalDirection.Forward)
{
if (TextWrapping == TextWrapping.Wrap)
if (lineIndex + 1 > TextLayout.TextLines.Count - 1)
{
_constraint = new Size(availableSize.Width, double.PositiveInfinity);
return;
}
else
var textLine = TextLayout.TextLines[lineIndex];
currentY += textLine.Height;
}
else
{
if (lineIndex - 1 < 0)
{
_constraint = Size.Infinity;
return;
}
_formattedText = null;
var textLine = TextLayout.TextLines[--lineIndex];
return FormattedText.Bounds.Size;
currentY -= textLine.Height;
}
return new Size();
var navigationPosition = _navigationPosition;
MoveCaretToPoint(new Point(currentX, currentY));
_navigationPosition = navigationPosition.WithY(_caretBounds.Y);
}
protected override Size MeasureOverride(Size availableSize)
public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
{
var text = Text;
var characterHit = _lastCharacterHit;
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
if (!string.IsNullOrEmpty(text))
if (lineIndex < 0)
{
return MeasureInternal(availableSize);
return;
}
if (direction == LogicalDirection.Forward)
{
while (lineIndex < TextLayout.TextLines.Count)
{
var textLine = TextLayout.TextLines[lineIndex];
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length)
{
characterHit = new CharacterHit(caretIndex);
}
if (caretIndex >= Text.Length)
{
characterHit = new CharacterHit(Text.Length);
break;
}
if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length)
{
break;
}
if (caretIndex <= CaretIndex)
{
lineIndex++;
continue;
}
break;
}
}
else
{
return new FormattedText
while (lineIndex >= 0)
{
Text = "X",
Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize,
TextAlignment = TextAlignment,
Constraint = availableSize,
}.Bounds.Size;
var textLine = TextLayout.TextLines[lineIndex];
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (caretIndex >= CaretIndex)
{
lineIndex--;
continue;
}
break;
}
}
UpdateCaret(characterHit);
_navigationPosition = _caretBounds.Position;
}
private int CoerceCaretIndex(int value)
private void UpdateCaret(CharacterHit characterHit)
{
var text = Text;
var length = text?.Length ?? 0;
return Math.Max(0, Math.Min(length, value));
_lastCharacterHit = characterHit;
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0);
var textLine = TextLayout.TextLines[lineIndex];
var distanceX = textLine.GetDistanceFromCharacterHit(characterHit);
var distanceY = 0d;
for (var i = 0; i < lineIndex; i++)
{
var currentLine = TextLayout.TextLines[i];
distanceY += currentLine.Height;
}
var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height);
if (caretBounds != _caretBounds)
{
_caretBounds = caretBounds;
CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
}
CaretChanged();
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
}
private void CaretTimerTick(object sender, EventArgs e)
internal Rect GetCursorRectangle()
{
_caretBlink = !_caretBlink;
InvalidateVisual();
return _caretBounds;
}
internal Rect GetCursorRectangle()
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
var (p1, p2) = GetCaretPoints();
return new Rect(p1, p2);
base.OnAttachedToVisualTree(e);
_scrollViewer = this.FindAncestorOfType<ScrollViewer>();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_scrollViewer = null;
_caretTimer.Stop();
_caretTimer.Tick -= CaretTimerTick;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof (TextBlock.Foreground):
case nameof (TextBlock.FontSize):
case nameof (TextBlock.FontStyle):
case nameof (TextBlock.FontWeight):
case nameof (TextBlock.FontFamily):
case nameof (Text):
case nameof (TextAlignment):
case nameof (TextWrapping):
case nameof (SelectionStart):
case nameof (SelectionEnd):
case nameof (SelectionForegroundBrush):
case nameof (PasswordChar):
case nameof (RevealPassword):
{
InvalidateTextLayout();
break;
}
}
}
}
}

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

@ -429,16 +429,20 @@ namespace Avalonia.Controls.Primitives
(x, handler) => x.LostFocus += handler,
(x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup);
SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
if (placementTarget is Layoutable layoutTarget)
// Recalculate popup position on parent moved/resized, but not if placement was on pointer
if (PlacementMode != PlacementMode.Pointer)
{
// If the placement target is moved, update the popup position
SubscribeToEventHandler<Layoutable, EventHandler>(layoutTarget, PlacementTargetLayoutUpdated,
(x, handler) => x.LayoutUpdated += handler,
(x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup);
SubscribeToEventHandler<IWindowImpl, Action<PixelPoint>>(window.PlatformImpl, WindowPositionChanged,
(x, handler) => x.PositionChanged += handler,
(x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup);
if (placementTarget is Layoutable layoutTarget)
{
// If the placement target is moved, update the popup position
SubscribeToEventHandler<Layoutable, EventHandler>(layoutTarget, PlacementTargetLayoutUpdated,
(x, handler) => x.LayoutUpdated += handler,
(x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup);
}
}
}
else if (topLevel is PopupRoot parentPopupRoot)

8
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives
/// The dependency resolver to use. If null the default dependency resolver will be used.
/// </param>
public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver dependencyResolver)
: base(impl, dependencyResolver)
: base(ValidatingPopupImpl.Wrap(impl), dependencyResolver)
{
_parent = parent;
}
@ -74,7 +74,11 @@ namespace Avalonia.Controls.Primitives
IStyleHost IStyleHost.StylingParent => Parent;
/// <inheritdoc/>
public void Dispose() => PlatformImpl?.Dispose();
public void Dispose()
{
PlatformImpl?.Dispose();
HandleClosed();
}
private void UpdatePosition()
{

1
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@ -3,6 +3,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]

32
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -20,7 +20,7 @@ namespace Avalonia.Controls
/// Represents a data-driven collection control that incorporates a flexible layout system,
/// custom views, and virtualization.
/// </summary>
public class ItemsRepeater : Panel, IChildIndexProvider
public class ItemsRepeater : Panel, IChildIndexProvider, IWeakEventSubscriber<EventArgs>
{
/// <summary>
/// Defines the <see cref="HorizontalCacheLength"/> property.
@ -723,14 +723,8 @@ namespace Avalonia.Controls
{
oldValue.UninitializeForContext(LayoutContext);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Unsubscribe<EventArgs, ItemsRepeater>(
oldValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
AttachedLayout.MeasureInvalidatedWeakEvent.Unsubscribe(oldValue, this);
AttachedLayout.ArrangeInvalidatedWeakEvent.Unsubscribe(oldValue, this);
// Walk through all the elements and make sure they are cleared
foreach (var element in Children)
@ -748,14 +742,8 @@ namespace Avalonia.Controls
{
newValue.InitializeForContext(LayoutContext);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.MeasureInvalidated),
InvalidateMeasureForLayout);
WeakEventHandlerManager.Subscribe<AttachedLayout, EventArgs, ItemsRepeater>(
newValue,
nameof(AttachedLayout.ArrangeInvalidated),
InvalidateArrangeForLayout);
AttachedLayout.MeasureInvalidatedWeakEvent.Subscribe(newValue, this);
AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, this);
}
bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout;
@ -806,9 +794,13 @@ namespace Avalonia.Controls
_viewportManager.OnBringIntoViewRequested(e);
}
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();
void IWeakEventSubscriber<EventArgs>.OnEvent(object? sender, WeakEvent ev, EventArgs e)
{
if(ev == AttachedLayout.ArrangeInvalidatedWeakEvent)
InvalidateArrange();
else if (ev == AttachedLayout.MeasureInvalidatedWeakEvent)
InvalidateMeasure();
}
private VirtualizingLayoutContext GetLayoutContext()
{

52
src/Avalonia.Controls/ScrollViewer.cs

@ -181,6 +181,14 @@ namespace Avalonia.Controls
nameof(AllowAutoHide),
true);
/// <summary>
/// Defines the <see cref="IsScrollChainingEnabled"/> property.
/// </summary>
public static readonly AttachedProperty<bool> IsScrollChainingEnabledProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, bool>(
nameof(IsScrollChainingEnabled),
defaultValue: true);
/// <summary>
/// Defines the <see cref="ScrollChanged"/> event.
/// </summary>
@ -418,6 +426,20 @@ namespace Avalonia.Controls
set => SetValue(AllowAutoHideProperty, value);
}
/// <summary>
/// Gets or sets if scroll chaining is enabled. The default value is true.
/// </summary>
/// <remarks>
/// After a user hits a scroll limit on an element that has been nested within another scrollable element,
/// you can specify whether that parent element should continue the scrolling operation begun in its child element.
/// This is called scroll chaining.
/// </remarks>
public bool IsScrollChainingEnabled
{
get => GetValue(IsScrollChainingEnabledProperty);
set => SetValue(IsScrollChainingEnabledProperty, value);
}
/// <summary>
/// Scrolls the content up one line.
/// </summary>
@ -548,6 +570,36 @@ namespace Avalonia.Controls
return control.GetValue(AllowAutoHideProperty);
}
/// <summary>
/// Sets the value of the IsScrollChainingEnabled attached property.
/// </summary>
/// <param name="control">The control to set the value on.</param>
/// <param name="value">The value of the property.</param>
/// <remarks>
/// After a user hits a scroll limit on an element that has been nested within another scrollable element,
/// you can specify whether that parent element should continue the scrolling operation begun in its child element.
/// This is called scroll chaining.
/// </remarks>
public static void SetIsScrollChainingEnabled(Control control, bool value)
{
control.SetValue(IsScrollChainingEnabledProperty, value);
}
/// <summary>
/// Gets the value of the IsScrollChainingEnabled attached property.
/// </summary>
/// <param name="control">The control to read the value from.</param>
/// <returns>The value of the property.</returns>
/// <remarks>
/// After a user hits a scroll limit on an element that has been nested within another scrollable element,
/// you can specify whether that parent element should continue the scrolling operation begun in its child element.
/// This is called scroll chaining.
/// </remarks>
public static bool GetIsScrollChainingEnabled(Control control)
{
return control.GetValue(IsScrollChainingEnabledProperty);
}
/// <summary>
/// Gets the value of the VerticalScrollBarVisibility attached property.
/// </summary>

52
src/Avalonia.Controls/TextBlock.cs

@ -131,21 +131,8 @@ namespace Avalonia.Controls
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty,
TextAlignmentProperty, TextDecorationsProperty);
AffectsMeasure<TextBlock>(FontSizeProperty, FontWeightProperty,
FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty);
Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextTrimmingProperty.Changed, FontSizeProperty.Changed,
FontStyleProperty.Changed, FontWeightProperty.Changed,
FontFamilyProperty.Changed, TextDecorationsProperty.Changed,
PaddingProperty.Changed, MaxLinesProperty.Changed, LineHeightProperty.Changed
).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
}
/// <summary>
@ -460,6 +447,7 @@ namespace Avalonia.Controls
TextWrapping,
TextTrimming,
TextDecorations,
FlowDirection,
constraint.Width,
constraint.Height,
maxLines: MaxLines,
@ -472,6 +460,8 @@ namespace Avalonia.Controls
protected void InvalidateTextLayout()
{
_textLayout = null;
InvalidateMeasure();
}
/// <summary>
@ -507,12 +497,40 @@ namespace Avalonia.Controls
base.OnAttachedToLogicalTree(e);
InvalidateTextLayout();
InvalidateMeasure();
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof (FontSize):
case nameof (FontWeight):
case nameof (FontStyle):
case nameof (FontFamily):
case nameof (TextWrapping):
case nameof (TextTrimming):
case nameof (TextAlignment):
case nameof (FlowDirection):
case nameof (Padding):
case nameof (LineHeight):
case nameof (MaxLines):
case nameof (Text):
case nameof (TextDecorations):
case nameof (Foreground):
{
InvalidateTextLayout();
break;
}
}
}
}
}

346
src/Avalonia.Controls/TextBox.cs

@ -14,6 +14,8 @@ using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Utilities;
using Avalonia.Controls.Metadata;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls
{
@ -250,6 +252,7 @@ namespace Avalonia.Controls
{
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state;
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState();
@ -301,7 +304,8 @@ namespace Avalonia.Controls
{
UpdateCommandStates();
}
if (SelectionStart == SelectionEnd)
if (value == SelectionEnd)
{
CaretIndex = SelectionStart;
}
@ -319,13 +323,15 @@ namespace Avalonia.Controls
{
value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
if (changed)
{
UpdateCommandStates();
}
if (SelectionStart == SelectionEnd)
if (value == SelectionStart)
{
CaretIndex = SelectionEnd;
CaretIndex = value;
}
}
}
@ -345,6 +351,7 @@ namespace Avalonia.Controls
if (!_ignoreTextChanges)
{
var caretIndex = CaretIndex;
SelectionStart = CoerceCaretIndex(SelectionStart, value);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
@ -533,13 +540,27 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_imClient.SetPresenter(_presenter, this);
if (IsFocused)
{
_presenter?.ShowCaret();
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_imClient.SetPresenter(null, null);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
@ -631,9 +652,8 @@ namespace Avalonia.Controls
_selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false);
string text = Text ?? string.Empty;
int caretIndex = CaretIndex;
int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
var text = Text ?? string.Empty;
var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
if (MaxLength > 0 && newLength > MaxLength)
{
@ -649,11 +669,11 @@ namespace Avalonia.Controls
try
{
DeleteSelection(false);
caretIndex = CaretIndex;
var caretIndex = CaretIndex;
text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection();
if (IsUndoEnabled)
{
_undoRedoHelper.DiscardRedo();
@ -663,6 +683,8 @@ namespace Avalonia.Controls
{
RaisePropertyChanged(TextProperty, oldText, _text);
}
CaretIndex = caretIndex + input.Length;
}
finally
{
@ -684,6 +706,7 @@ namespace Avalonia.Controls
public async void Cut()
{
var text = GetSelection();
if (string.IsNullOrEmpty(text))
{
return;
@ -703,6 +726,7 @@ namespace Avalonia.Controls
public async void Copy()
{
var text = GetSelection();
if (string.IsNullOrEmpty(text))
{
return;
@ -739,11 +763,16 @@ namespace Avalonia.Controls
protected override void OnKeyDown(KeyEventArgs e)
{
string text = Text ?? string.Empty;
int caretIndex = CaretIndex;
bool movement = false;
bool selection = false;
bool handled = false;
if (_presenter == null)
{
return;
}
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
var movement = false;
var selection = false;
var handled = false;
var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
@ -884,46 +913,85 @@ namespace Avalonia.Controls
break;
case Key.Up:
movement = MoveVertical(-1);
{
selection = DetectSelection();
break;
_presenter.MoveCaretVertical(LogicalDirection.Backward);
if (caretIndex != _presenter.CaretIndex)
{
movement = true;
}
if (selection)
{
SelectionEnd = _presenter.CaretIndex;
}
break;
}
case Key.Down:
movement = MoveVertical(1);
{
selection = DetectSelection();
_presenter?.MoveCaretVertical();
if (caretIndex != _presenter.CaretIndex)
{
movement = true;
}
if (selection)
{
SelectionEnd = _presenter.CaretIndex;
}
break;
}
case Key.Back:
{
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlBackspace();
}
if (!DeleteSelection() && CaretIndex > 0)
if (!DeleteSelection() && caretIndex > 0)
{
var removedCharacters = 1;
// handle deleting /r/n
// you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if
// a /r should also be deleted.
if (CaretIndex > 1 &&
text[CaretIndex - 1] == '\n' &&
text[CaretIndex - 2] == '\r')
var removedCharacters = 0;
// \r\n needs special treatment here
if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r')
{
removedCharacters = 2;
}
else
{
Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters);
}
if (removedCharacters == 0)
{
return;
}
var length = Math.Max(0, caretIndex - removedCharacters);
SetTextInternal(text.Substring(0, caretIndex - removedCharacters) +
SetTextInternal(text.Substring(0, length) +
text.Substring(caretIndex));
CaretIndex -= removedCharacters;
CaretIndex = caretIndex - removedCharacters;
ClearSelection();
}
handled = true;
break;
}
case Key.Delete:
SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{
SetSelectionForControlDelete();
@ -931,21 +999,18 @@ namespace Avalonia.Controls
if (!DeleteSelection() && caretIndex < text.Length)
{
var removedCharacters = 1;
// handle deleting /r/n
// you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if
// a /r should also be deleted.
if (CaretIndex < text.Length - 1 &&
text[caretIndex + 1] == '\n' &&
text[caretIndex] == '\r')
{
removedCharacters = 2;
}
_presenter.MoveCaretHorizontal();
var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex);
SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters));
CaretIndex = caretIndex;
}
SnapshotUndoRedo();
handled = true;
break;
@ -983,11 +1048,7 @@ namespace Avalonia.Controls
}
}
if (movement && selection)
{
SelectionEnd = CaretIndex;
}
else if (movement)
if (movement && !selection)
{
ClearSelection();
}
@ -1000,19 +1061,28 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (_presenter == null)
{
return;
}
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border))
if (text != null && clickInfo.Properties.IsLeftButtonPressed &&
!(clickInfo.Pointer?.Captured is Border))
{
var point = e.GetPosition(_presenter);
var index = _presenter.GetCaretIndex(point);
var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (!clickToSelect)
{
CaretIndex = index;
}
var oldIndex = CaretIndex;
_presenter.MoveCaretToPoint(point);
var index = _presenter.CaretIndex;
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
SetAndRaise(CaretIndexProperty, ref _caretIndex, index);
#pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount)
@ -1021,13 +1091,14 @@ namespace Avalonia.Controls
case 1:
if (clickToSelect)
{
SelectionStart = Math.Min(index, CaretIndex);
SelectionEnd = Math.Max(index, CaretIndex);
SelectionStart = Math.Min(oldIndex, index);
SelectionEnd = Math.Max(oldIndex, index);
}
else
{
SelectionStart = SelectionEnd = index;
}
break;
case 2:
if (!StringUtils.IsStartOfWord(text, index))
@ -1049,8 +1120,13 @@ namespace Avalonia.Controls
protected override void OnPointerMoved(PointerEventArgs e)
{
if (_presenter == null)
{
return;
}
// selection should not change during pointer move if the user right clicks
if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(_presenter);
@ -1058,32 +1134,45 @@ namespace Avalonia.Controls
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
CaretIndex = SelectionEnd = _presenter.GetCaretIndex(point);
_presenter.MoveCaretToPoint(point);
SelectionEnd = _presenter.CaretIndex;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (_presenter != null && e.Pointer.Captured == _presenter)
if (_presenter == null)
{
if (e.InitialPressMouseButton == MouseButton.Right)
return;
}
if (e.Pointer.Captured != _presenter)
{
return;
}
if (e.InitialPressMouseButton == MouseButton.Right)
{
var point = e.GetPosition(_presenter);
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
var point = e.GetPosition(_presenter);
var caretIndex = _presenter.GetCaretIndex(point);
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
CaretIndex = SelectionEnd = SelectionStart = caretIndex;
}
CaretIndex = SelectionEnd = SelectionStart = caretIndex;
}
e.Pointer.Capture(null);
}
e.Pointer.Capture(null);
}
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
@ -1127,106 +1216,46 @@ namespace Avalonia.Controls
Text = string.Empty;
}
private int DeleteCharacter(int index)
{
var start = index + 1;
var text = Text;
var c = text[index];
var result = 1;
if (c == '\n' && index > 0 && text[index - 1] == '\r')
{
--index;
++result;
}
else if (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n')
{
++start;
++result;
}
Text = text.Substring(0, index) + text.Substring(start);
return result;
}
private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
{
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
var selectionStart = SelectionStart;
if (!wholeWord)
{
if (SelectionStart != SelectionEnd && !isSelecting)
if (_presenter == null)
{
var start = Math.Min(SelectionStart, SelectionEnd);
var end = Math.Max(SelectionStart, SelectionEnd);
CaretIndex = direction < 0 ? start : end;
return;
}
_presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward);
var index = caretIndex + direction;
if (index < 0 || index > text.Length)
if (isSelecting)
{
return;
}
else if (index == text.Length)
{
CaretIndex = index;
return;
}
var c = text[index];
if (direction > 0)
{
CaretIndex += (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n') ? 2 : 1;
SelectionEnd = _presenter.CaretIndex;
}
else
{
CaretIndex -= (c == '\n' && index > 0 && text[index - 1] == '\r') ? 2 : 1;
SelectionStart = SelectionEnd = _presenter.CaretIndex;
}
}
else
{
if (direction > 0)
{
CaretIndex += StringUtils.NextWord(text, caretIndex) - caretIndex;
var offset = StringUtils.NextWord(text, selectionStart) - selectionStart;
CaretIndex += offset;
}
else
{
CaretIndex += StringUtils.PreviousWord(text, caretIndex) - caretIndex;
var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart;
CaretIndex += offset;
}
}
}
private bool MoveVertical(int count)
{
if (_presenter is null)
{
return false;
}
var formattedText = _presenter.FormattedText;
var lines = formattedText.GetLines().ToList();
var caretIndex = CaretIndex;
var lineIndex = GetLine(caretIndex, lines) + count;
if (lineIndex >= 0 && lineIndex < lines.Count)
{
var line = lines[lineIndex];
var rect = formattedText.HitTestTextPosition(caretIndex);
var y = count < 0 ? rect.Y : rect.Bottom;
var point = new Point(rect.X, y + (count * (line.Height / 2)));
var hit = formattedText.HitTestPoint(point);
CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
return true;
}
return false;
}
private void MoveHome(bool document)
{
if (_presenter is null)
@ -1243,17 +1272,17 @@ namespace Avalonia.Controls
}
else
{
var lines = _presenter.FormattedText.GetLines();
var lines = _presenter.TextLayout.TextLines;
var pos = 0;
foreach (var line in lines)
{
if (pos + line.Length > caretIndex || pos + line.Length == text.Length)
if (pos + line.TextRange.Length > caretIndex || pos + line.TextRange.Length == text.Length)
{
break;
}
pos += line.Length;
pos += line.TextRange.Length;
}
caretIndex = pos;
@ -1278,12 +1307,12 @@ namespace Avalonia.Controls
}
else
{
var lines = _presenter.FormattedText.GetLines();
var lines = _presenter.TextLayout.TextLines;
var pos = 0;
foreach (var line in lines)
{
pos += line.Length;
pos += line.TextRange.Length;
if (pos > caretIndex)
{
@ -1360,25 +1389,6 @@ namespace Avalonia.Controls
return text.Substring(start, end - start);
}
private int GetLine(int caretIndex, IList<FormattedTextLine> lines)
{
int pos = 0;
int i;
for (i = 0; i < lines.Count - 1; ++i)
{
var line = lines[i];
pos += line.Length;
if (pos > caretIndex)
{
break;
}
}
return i;
}
private void SetTextInternal(string value, bool raiseTextChanged = true)
{
if (raiseTextChanged)

24
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
@ -10,7 +11,7 @@ namespace Avalonia.Controls
{
private InputElement _parent;
private TextPresenter _presenter;
private IDisposable _subscription;
public Rect CursorRectangle
{
get
@ -20,11 +21,15 @@ namespace Avalonia.Controls
return default;
}
var transform = _presenter.TransformToVisual(_parent);
if (transform == null)
{
return default;
}
return _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
return rect;
}
}
@ -40,20 +45,25 @@ namespace Avalonia.Controls
public string TextBeforeCursor => null;
public string TextAfterCursor => null;
private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
private void OnCaretBoundsChanged(object sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
public void SetPresenter(TextPresenter presenter, InputElement parent)
{
_parent = parent;
_subscription?.Dispose();
_subscription = null;
if (_presenter != null)
{
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
}
_presenter = presenter;
if (_presenter != null)
{
_subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty)
.Subscribe(OnCaretIndexChanged);
_presenter.CaretBoundsChanged += OnCaretBoundsChanged;
}
TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
}

24
src/Avalonia.Controls/TopLevel.cs

@ -34,7 +34,7 @@ namespace Avalonia.Controls
IStyleHost,
ILogicalRoot,
ITextInputMethodRoot,
IWeakSubscriber<ResourcesChangedEventArgs>
IWeakEventSubscriber<ResourcesChangedEventArgs>
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -74,6 +74,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> TransparencyBackgroundFallbackProperty =
AvaloniaProperty.Register<TopLevel, IBrush>(nameof(TransparencyBackgroundFallback), Brushes.White);
private static readonly WeakEvent<IResourceHost, ResourcesChangedEventArgs>
ResourcesChangedWeakEvent = WeakEvent.Register<IResourceHost, ResourcesChangedEventArgs>(
(s, h) => s.ResourcesChanged += h,
(s, h) => s.ResourcesChanged -= h
);
private readonly IInputManager _inputManager;
private readonly IAccessKeyHandler _accessKeyHandler;
private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
@ -128,6 +134,8 @@ namespace Avalonia.Controls
"Could not create window implementation: maybe no windowing subsystem was initialized?");
}
impl = ValidatingToplevelImpl.Wrap(impl);
PlatformImpl = impl;
_actualTransparencyLevel = PlatformImpl.TransparencyLevel;
@ -178,10 +186,7 @@ namespace Avalonia.Controls
if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
{
WeakSubscriptionManager.Subscribe(
applicationResources,
nameof(IResourceHost.ResourcesChanged),
this);
ResourcesChangedWeakEvent.Subscribe(applicationResources, this);
}
impl.LostFocus += PlatformImpl_LostFocus;
@ -286,7 +291,7 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
void IWeakSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, ResourcesChangedEventArgs e)
void IWeakEventSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, WeakEvent ev, ResourcesChangedEventArgs e)
{
((ILogical)this).NotifyResourcesChanged(e);
}
@ -364,14 +369,15 @@ namespace Avalonia.Controls
Renderer?.Dispose();
Renderer = null;
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
var visualArgs = new VisualTreeAttachmentEventArgs(this, this);
OnDetachedFromVisualTreeCore(visualArgs);
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
OnClosed(EventArgs.Empty);
LayoutManager?.Dispose();

15
src/Avalonia.Controls/Utils/CollectionChangedEventManager.cs

@ -83,7 +83,7 @@ namespace Avalonia.Controls.Utils
"Collection listener not registered for this collection/listener combination.");
}
private class Entry : IWeakSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
private class Entry : IWeakEventSubscriber<NotifyCollectionChangedEventArgs>, IDisposable
{
private INotifyCollectionChanged _collection;
@ -91,23 +91,18 @@ namespace Avalonia.Controls.Utils
{
_collection = collection;
Listeners = new List<WeakReference<ICollectionChangedListener>>();
WeakSubscriptionManager.Subscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Subscribe(_collection, this);
}
public List<WeakReference<ICollectionChangedListener>> Listeners { get; }
public void Dispose()
{
WeakSubscriptionManager.Unsubscribe(
_collection,
nameof(INotifyCollectionChanged.CollectionChanged),
this);
WeakEvents.CollectionChanged.Unsubscribe(_collection, this);
}
void IWeakSubscriber<NotifyCollectionChangedEventArgs>.OnEvent(object? sender, NotifyCollectionChangedEventArgs e)
void IWeakEventSubscriber<NotifyCollectionChangedEventArgs>.
OnEvent(object? notifyCollectionChanged, WeakEvent ev, NotifyCollectionChangedEventArgs e)
{
static void Notify(
INotifyCollectionChanged incc,

53
src/Avalonia.Controls/Utils/StringUtils.cs

@ -1,4 +1,5 @@
using System.Globalization;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls.Utils
{
@ -23,26 +24,38 @@ namespace Avalonia.Controls.Utils
return false;
}
var codepoint = new Codepoint(text[index]);
// A 'word' starts with an AlphaNumeric or some punctuation symbols immediately
// preceeded by lwsp.
if (index > 0 && !char.IsWhiteSpace(text[index - 1]))
if (index > 0)
{
return false;
var previousCodepoint = new Codepoint(text[index - 1]);
if (!previousCodepoint.IsWhiteSpace)
{
return false;
}
if (previousCodepoint.IsBreakChar)
{
return true;
}
}
switch (CharUnicodeInfo.GetUnicodeCategory(text[index]))
switch (codepoint.GeneralCategory)
{
case UnicodeCategory.LowercaseLetter:
case UnicodeCategory.TitlecaseLetter:
case UnicodeCategory.UppercaseLetter:
case UnicodeCategory.DecimalDigitNumber:
case UnicodeCategory.LetterNumber:
case UnicodeCategory.OtherNumber:
case UnicodeCategory.DashPunctuation:
case UnicodeCategory.InitialQuotePunctuation:
case UnicodeCategory.OpenPunctuation:
case UnicodeCategory.CurrencySymbol:
case UnicodeCategory.MathSymbol:
case GeneralCategory.LowercaseLetter:
case GeneralCategory.TitlecaseLetter:
case GeneralCategory.UppercaseLetter:
case GeneralCategory.DecimalNumber:
case GeneralCategory.LetterNumber:
case GeneralCategory.OtherNumber:
case GeneralCategory.DashPunctuation:
case GeneralCategory.InitialPunctuation:
case GeneralCategory.OpenPunctuation:
case GeneralCategory.CurrencySymbol:
case GeneralCategory.MathSymbol:
return true;
// TODO: How do you do this in .NET?
@ -56,6 +69,11 @@ namespace Avalonia.Controls.Utils
public static int PreviousWord(string text, int cursor)
{
if (string.IsNullOrEmpty(text))
{
return 0;
}
int begin;
int i;
int cr;
@ -107,7 +125,12 @@ namespace Avalonia.Controls.Utils
cr = LineEnd(text, cursor);
if (cr < text.Length && text[cr] == '\r' && text[cr + 1] == '\n')
if (cursor >= text.Length)
{
return cursor;
}
if (cr < text.Length && text[cr] == '\r' && cr + 1 < text.Length && text[cr + 1] == '\n')
{
lf = cr + 1;
}

344
src/Avalonia.Controls/ValidatingToplevel.cs

@ -0,0 +1,344 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia.Controls;
internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost,
ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod
{
private readonly ITopLevelImpl _impl;
private bool _disposed;
public ValidatingToplevelImpl(ITopLevelImpl impl)
{
_impl = impl ?? throw new InvalidOperationException(
"Could not create TopLevel implementation: maybe no windowing subsystem was initialized?");
}
public void Dispose()
{
_disposed = true;
_impl.Dispose();
}
protected void CheckDisposed()
{
if (_disposed)
throw new ObjectDisposedException(_impl.GetType().FullName);
}
protected ITopLevelImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static ITopLevelImpl Wrap(ITopLevelImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingToplevelImpl(impl);
#else
return impl;
#endif
}
public Size ClientSize => Inner.ClientSize;
public Size? FrameSize => Inner.FrameSize;
public double RenderScaling => Inner.RenderScaling;
public IEnumerable<object> Surfaces => Inner.Surfaces;
public Action<RawInputEventArgs> Input
{
get => Inner.Input;
set => Inner.Input = value;
}
public Action<Rect> Paint
{
get => Inner.Paint;
set => Inner.Paint = value;
}
public Action<Size, PlatformResizeReason> Resized
{
get => Inner.Resized;
set => Inner.Resized = value;
}
public Action<double> ScalingChanged
{
get => Inner.ScalingChanged;
set => Inner.ScalingChanged = value;
}
public Action<WindowTransparencyLevel> TransparencyLevelChanged
{
get => Inner.TransparencyLevelChanged;
set => Inner.TransparencyLevelChanged = value;
}
public IRenderer CreateRenderer(IRenderRoot root) => Inner.CreateRenderer(root);
public void Invalidate(Rect rect) => Inner.Invalidate(rect);
public void SetInputRoot(IInputRoot inputRoot) => Inner.SetInputRoot(inputRoot);
public Point PointToClient(PixelPoint point) => Inner.PointToClient(point);
public PixelPoint PointToScreen(Point point) => Inner.PointToScreen(point);
public void SetCursor(ICursorImpl cursor) => Inner.SetCursor(cursor);
public Action Closed
{
get => Inner.Closed;
set => Inner.Closed = value;
}
public Action LostFocus
{
get => Inner.LostFocus;
set => Inner.LostFocus = value;
}
// Exception: for some reason we are notifying platform mouse device from TopLevel.cs
public IMouseDevice MouseDevice => _impl.MouseDevice;
public IPopupImpl CreatePopup() => Inner.CreatePopup();
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) =>
Inner.SetTransparencyLevelHint(transparencyLevel);
public WindowTransparencyLevel TransparencyLevel => Inner.TransparencyLevel;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => Inner.AcrylicCompensationLevels;
public INativeControlHostImpl NativeControlHost => (Inner as ITopLevelImplWithNativeControlHost)?.NativeControlHost;
public ITopLevelNativeMenuExporter NativeMenuExporter =>
(Inner as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter;
public ITextInputMethodImpl TextInputMethod => (Inner as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
}
internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImpl
{
private readonly IWindowBaseImpl _impl;
public ValidatingWindowBaseImpl(IWindowBaseImpl impl) : base(impl)
{
_impl = impl;
}
protected new IWindowBaseImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static IWindowBaseImpl Wrap(IWindowBaseImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingWindowBaseImpl(impl);
#else
return impl;
#endif
}
public void Show(bool activate, bool isDialog) => Inner.Show(activate, isDialog);
public void Hide() => Inner.Hide();
public double DesktopScaling => Inner.DesktopScaling;
public PixelPoint Position => Inner.Position;
public Action<PixelPoint> PositionChanged
{
get => Inner.PositionChanged;
set => Inner.PositionChanged = value;
}
public void Activate() => Inner.Activate();
public Action Deactivated
{
get => Inner.Deactivated;
set => Inner.Deactivated = value;
}
public Action Activated
{
get => Inner.Activated;
set => Inner.Activated = value;
}
public IPlatformHandle Handle => Inner.Handle;
public Size MaxAutoSizeHint => Inner.MaxAutoSizeHint;
public void SetTopmost(bool value) => Inner.SetTopmost(value);
public IScreenImpl Screen => Inner.Screen;
}
internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl
{
private readonly IWindowImpl _impl;
public ValidatingWindowImpl(IWindowImpl impl) : base(impl)
{
_impl = impl;
}
protected new IWindowImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static IWindowImpl Unwrap(IWindowImpl impl)
{
if (impl is ValidatingWindowImpl v)
return v.Inner;
return impl;
}
public static IWindowImpl Wrap(IWindowImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingWindowImpl(impl);
#else
return impl;
#endif
}
public WindowState WindowState
{
get => Inner.WindowState;
set => Inner.WindowState = value;
}
public Action<WindowState> WindowStateChanged
{
get => Inner.WindowStateChanged;
set => Inner.WindowStateChanged = value;
}
public void SetTitle(string title) => Inner.SetTitle(title);
public void SetParent(IWindowImpl parent)
{
//Workaround. SetParent will cast IWindowImpl to WindowImpl but ValidatingWindowImpl isn't actual WindowImpl so it will fail with InvalidCastException.
if (parent is ValidatingWindowImpl validatingToplevelImpl)
{
Inner.SetParent(validatingToplevelImpl.Inner);
}
else
{
Inner.SetParent(parent);
}
}
public void SetEnabled(bool enable) => Inner.SetEnabled(enable);
public Action GotInputWhenDisabled
{
get => Inner.GotInputWhenDisabled;
set => Inner.GotInputWhenDisabled = value;
}
public void SetSystemDecorations(SystemDecorations enabled) => Inner.SetSystemDecorations(enabled);
public void SetIcon(IWindowIconImpl icon) => Inner.SetIcon(icon);
public void ShowTaskbarIcon(bool value) => Inner.ShowTaskbarIcon(value);
public void CanResize(bool value) => Inner.CanResize(value);
public Func<bool> Closing
{
get => Inner.Closing;
set => Inner.Closing = value;
}
public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations;
public Action<bool> ExtendClientAreaToDecorationsChanged
{
get => Inner.ExtendClientAreaToDecorationsChanged;
set => Inner.ExtendClientAreaToDecorationsChanged = value;
}
public bool NeedsManagedDecorations => Inner.NeedsManagedDecorations;
public Thickness ExtendedMargins => Inner.ExtendedMargins;
public Thickness OffScreenMargin => Inner.OffScreenMargin;
public void BeginMoveDrag(PointerPressedEventArgs e) => Inner.BeginMoveDrag(e);
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => Inner.BeginResizeDrag(edge, e);
public void Resize(Size clientSize, PlatformResizeReason reason) =>
Inner.Resize(clientSize, reason);
public void Move(PixelPoint point) => Inner.Move(point);
public void SetMinMaxSize(Size minSize, Size maxSize) => Inner.SetMinMaxSize(minSize, maxSize);
public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) =>
Inner.SetExtendClientAreaToDecorationsHint(extendIntoClientAreaHint);
public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) =>
Inner.SetExtendClientAreaChromeHints(hints);
public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) =>
Inner.SetExtendClientAreaTitleBarHeightHint(titleBarHeight);
}
internal class ValidatingPopupImpl : ValidatingWindowBaseImpl, IPopupImpl
{
private readonly IPopupImpl _impl;
public ValidatingPopupImpl(IPopupImpl impl) : base(impl)
{
_impl = impl;
}
protected new IPopupImpl Inner
{
get
{
CheckDisposed();
return _impl;
}
}
public static IPopupImpl Wrap(IPopupImpl impl)
{
#if DEBUG
if (impl is ValidatingToplevelImpl)
return impl;
return new ValidatingPopupImpl(impl);
#else
return impl;
#endif
}
public IPopupPositioner PopupPositioner => Inner.PopupPositioner;
public void SetWindowManagerAddShadowHint(bool enabled) => Inner.SetWindowManagerAddShadowHint(enabled);
}

11
src/Avalonia.Controls/Window.cs

@ -237,13 +237,14 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="impl">The window implementation.</param>
public Window(IWindowImpl impl)
: base(impl)
: base(ValidatingWindowImpl.Wrap(impl))
{
impl.Closing = HandleClosing;
impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
impl.WindowStateChanged = HandleWindowStateChanged;
var wrapped = (IWindowImpl)base.PlatformImpl!;
wrapped.Closing = HandleClosing;
wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled;
wrapped.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);

9
src/Avalonia.Controls/WindowBase.cs

@ -57,12 +57,13 @@ namespace Avalonia.Controls
{
}
public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver) : base(impl, dependencyResolver)
public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver)
{
Screens = new Screens(PlatformImpl?.Screen);
impl.Activated = HandleActivated;
impl.Deactivated = HandleDeactivated;
impl.PositionChanged = HandlePositionChanged;
var wrapped = PlatformImpl!;
wrapped.Activated = HandleActivated;
wrapped.Deactivated = HandleDeactivated;
wrapped.PositionChanged = HandlePositionChanged;
}
/// <summary>

3
src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt

@ -0,0 +1,3 @@
Compat issues with assembly Avalonia.DesktopRuntime:
TypesMustExist : Type 'Avalonia.Shared.PlatformSupport.AssetLoader' does not exist in the implementation but it does exist in the contract.
Total Issues: 1

2
src/Avalonia.DesktopRuntime/AppBuilder.cs

@ -4,7 +4,7 @@ using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport;
using Avalonia.PlatformSupport;
namespace Avalonia
{

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -8,6 +8,7 @@
<ProjectReference Include="../Avalonia.Base/Avalonia.Base.csproj" />
<ProjectReference Include="../Avalonia.Visuals/Avalonia.Visuals.csproj" />
<ProjectReference Include="../Avalonia.Controls/Avalonia.Controls.csproj" />
<ProjectReference Include="../Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj" />
</ItemGroup>
<ItemGroup>
@ -16,6 +17,5 @@
<Import Project="..\..\build\NetCore.props" />
<Import Project="..\..\build\NetFX.props" />
<Import Project="..\Shared\PlatformSupport\PlatformSupport.projitems" />
<Import Project="..\..\build\ApiDiff.props" />
</Project>

40
src/Avalonia.DesktopRuntime/RuntimeInfo.cs

@ -1,40 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Platform;
namespace Avalonia.Shared.PlatformSupport
{
internal partial class StandardRuntimePlatform
{
private static readonly Lazy<RuntimePlatformInfo> Info = new Lazy<RuntimePlatformInfo>(() =>
{
OperatingSystemType os;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
os = OperatingSystemType.OSX;
else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
os = OperatingSystemType.Linux;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
os = OperatingSystemType.WinNT;
else
throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription);
return new RuntimePlatformInfo
{
#if NETCOREAPP2_0
IsCoreClr = true,
#elif NET461
IsDotNetFramework = false,
#endif
IsDesktop = true,
IsMono = false,
IsMobile = false,
IsUnix = os != OperatingSystemType.WinNT,
OperatingSystem = os,
};
});
public RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save