Browse Source

Merge branch 'master' into feature/ui-automation

ui-automation-test
Steven Kirk 5 years ago
parent
commit
a4751575f7
  1. 4
      .github/PULL_REQUEST_TEMPLATE.md
  2. 2
      Avalonia.sln
  3. 22
      Documentation/build.md
  4. 2
      build/ReactiveUI.props
  5. 2
      native/Avalonia.Native/src/OSX/common.h
  6. 4
      native/Avalonia.Native/src/OSX/main.mm
  7. 9
      native/Avalonia.Native/src/OSX/menu.h
  8. 34
      native/Avalonia.Native/src/OSX/menu.mm
  9. 27
      native/Avalonia.Native/src/OSX/window.mm
  10. 58
      nukebuild/Build.cs
  11. 7
      nukebuild/BuildParameters.cs
  12. 1
      nukebuild/_build.csproj
  13. 6
      readme.md
  14. 6
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  15. 8
      samples/ControlCatalog/App.xaml.cs
  16. 2
      samples/ControlCatalog/ControlCatalog.csproj
  17. 6
      samples/ControlCatalog/MainView.xaml
  18. 4
      samples/ControlCatalog/MainWindow.xaml
  19. 102
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  20. 45
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
  21. 1
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  22. 264
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  23. 81
      samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
  24. 2
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  25. 6
      samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
  26. 76
      samples/ControlCatalog/Pages/ViewboxPage.xaml
  27. 21
      samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
  28. 78
      samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs
  29. 3
      samples/RenderDemo/MainWindow.xaml
  30. 7
      samples/RenderDemo/Pages/AnimationsPage.xaml
  31. 89
      samples/RenderDemo/Pages/PathMeasurementPage.cs
  32. 65
      samples/RenderDemo/Pages/TransitionsPage.xaml
  33. 96
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  34. 4
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  35. 12
      src/Android/Avalonia.Android/IInitEditorInfo.cs
  36. 6
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  37. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
  38. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  39. 72
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  40. 83
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  41. 42
      src/Android/Avalonia.Android/SoftKeyboardListner.cs
  42. 28
      src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
  43. 28
      src/Avalonia.Animation/Animatable.cs
  44. 11
      src/Avalonia.Animation/Animation.cs
  45. 20
      src/Avalonia.Animation/AnimatorDrivenTransition.cs
  46. 32
      src/Avalonia.Animation/AnimatorTransitionObservable.cs
  47. 8
      src/Avalonia.Animation/Animators/Animator`1.cs
  48. 9
      src/Avalonia.Animation/Clock.cs
  49. 12
      src/Avalonia.Animation/ClockBase.cs
  50. 6
      src/Avalonia.Animation/Transition.cs
  51. 60
      src/Avalonia.Animation/TransitionInstance.cs
  52. 58
      src/Avalonia.Animation/TransitionObservableBase.cs
  53. 15
      src/Avalonia.Animation/Transitions/DoubleTransition.cs
  54. 12
      src/Avalonia.Animation/Transitions/FloatTransition.cs
  55. 12
      src/Avalonia.Animation/Transitions/IntegerTransition.cs
  56. 39
      src/Avalonia.Base/AvaloniaObject.cs
  57. 6
      src/Avalonia.Base/Data/BindingValue.cs
  58. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  59. 14
      src/Avalonia.Base/Data/Optional.cs
  60. 4
      src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
  61. 54
      src/Avalonia.Base/EnumExtensions.cs
  62. 9
      src/Avalonia.Base/IAvaloniaObject.cs
  63. 15
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  64. 13
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  65. 4
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  66. 4
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  67. 2
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  68. 15
      src/Avalonia.Base/Utilities/MathUtilities.cs
  69. 4
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  70. 49
      src/Avalonia.Base/ValueStore.cs
  71. 12
      src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
  72. 4
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  73. 2
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  74. 37
      src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
  75. 13
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  76. 16
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  77. 6
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  78. 4
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  79. 9
      src/Avalonia.Controls/ApiCompatBaseline.txt
  80. 2
      src/Avalonia.Controls/AutoCompleteBox.cs
  81. 52
      src/Avalonia.Controls/Button.cs
  82. 73
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  83. 2
      src/Avalonia.Controls/ComboBox.cs
  84. 22
      src/Avalonia.Controls/Control.cs
  85. 44
      src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs
  86. 32
      src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs
  87. 16
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  88. 2
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  89. 21
      src/Avalonia.Controls/Expander.cs
  90. 50
      src/Avalonia.Controls/Flyouts/Flyout.cs
  91. 504
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  92. 77
      src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs
  93. 33
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  94. 24
      src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs
  95. 75
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  96. 55
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  97. 12
      src/Avalonia.Controls/Grid.cs
  98. 22
      src/Avalonia.Controls/GridSplitter.cs
  99. 4
      src/Avalonia.Controls/IControl.cs
  100. 6
      src/Avalonia.Controls/IMenuItem.cs

4
.github/PULL_REQUEST_TEMPLATE.md

@ -18,11 +18,13 @@
- [ ] Added unit tests (if possible)?
- [ ] Added XML documentation to any related classes?
- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation
- [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Documentation with user documentation
## Breaking changes
<!--- List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes -->
## Obsoletions / Deprecations
<!--- Obsolete and Deprecated attributes on APIs MUST only be included when discussed with Core team. @grokys, @kekekeks & @danwalmsley -->
## Fixed issues
<!--- If the pull request fixes issue(s) list them like this:

2
Avalonia.sln

@ -222,8 +222,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicroComGenerator", "src\tools\MicroComGenerator\MicroComGenerator.csproj", "{AEC9031E-06EA-4A9E-9E7F-7D7C719404DD}"

22
Documentation/build.md

@ -9,10 +9,24 @@ git clone https://github.com/AvaloniaUI/Avalonia.git
git submodule update --init
```
### Install the required version of the .NET Core SDK
Go to https://dotnet.microsoft.com/download/visual-studio-sdks and install the latest version of the .NET Core SDK compatible with Avalonia UI. Make sure to download the SDK (not just the "runtime") package. The version compatible is indicated within the [global.json](https://github.com/AvaloniaUI/Avalonia/blob/master/global.json) file. Note that Avalonia UI does not always use the latest version and is hardcoded to use the last version known to be compatible (SDK releases may break the builds from time-to-time).
### Open in Visual Studio
Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community
edition works fine. Run the `Samples\ControlCatalog.Desktop` project to see the sample application.
Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application.
### Troubleshooting
* **Error CS0006: Avalonia.DesktopRuntime.dll could not be found**
It is common for the first build to fail with the errors below (also discussed in [#4257](https://github.com/AvaloniaUI/Avalonia/issues/4257)).
```
>CSC : error CS0006: Metadata file 'C:\...\Avalonia\src\Avalonia.DesktopRuntime\bin\Debug\netcoreapp2.0\Avalonia.DesktopRuntime.dll' could not be found
>CSC : error CS0006: Metadata file 'C:\...\Avalonia\packages\Avalonia\bin\Debug\netcoreapp2.0\Avalonia.dll' could not be found
```
To correct this, right click on the `Avalonia.DesktopRuntime` project then press `Build` to build the project manually. Afterwards the solution should build normally and the ControlCatalog can be run.
# Linux/macOS
@ -20,9 +34,9 @@ It's *not* possible to build the *whole* project on Linux/macOS. You can only bu
MonoDevelop, Xamarin Studio and Visual Studio for Mac aren't capable of properly opening our solution. You can use Rider (at least 2017.2 EAP) or VSCode instead. They will fail to load most of platform specific projects, but you don't need them to run on .NET Core.
### Install the latest version of .NET Core
### Install the latest version of the .NET Core SDK
Go to https://www.microsoft.com/net/core and follow instructions for your OS. You need SDK (not just "runtime") package.
Go to https://www.microsoft.com/net/core and follow the instructions for your OS. Make sure to download the SDK (not just the "runtime") package.
### Additional requirements for macOS

2
build/ReactiveUI.props

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

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

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

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

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

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

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

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

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

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

@ -50,7 +50,6 @@ public:
[Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false];
[Window setContentView: StandardContainer];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
@ -112,6 +111,9 @@ public:
{
SetPosition(lastPositionSet);
UpdateStyle();
[Window setContentView: StandardContainer];
if(ShouldTakeFocusOnShow() && activate)
{
[Window makeKeyAndOrderFront:Window];
@ -124,7 +126,7 @@ public:
[Window setTitle:_lastTitle];
_shown = true;
return S_OK;
}
}
@ -191,9 +193,11 @@ public:
{
if(ret == nullptr)
return E_POINTER;
auto frame = [View frame];
ret->Width = frame.size.width;
ret->Height = frame.size.height;
return S_OK;
}
}
@ -254,6 +258,12 @@ public:
y = maxSize.height;
}
if(!_shown)
{
BaseEvents->Resized(AvnSize{x,y});
}
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
return S_OK;
@ -1936,6 +1946,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
[NSApp setMenu:_menu];
}
else
{
[self showAppMenuOnly];
}
}
-(void) showAppMenuOnly
@ -1992,7 +2006,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastScaling = [self backingScaleFactor];
[self setOpaque:NO];
[self setBackgroundColor: [NSColor clearColor]];
[self invalidateShadow];
_isExtended = false;
return self;
}
@ -2231,9 +2244,13 @@ protected:
{
@autoreleasepool
{
[Window setContentSize:NSSize{x, y}];
if (Window != nullptr)
{
[StandardContainer setFrameSize:NSSize{x,y}];
[Window setContentSize:NSSize{x, y}];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
[Window setFrameTopLeftPoint:ToNSPoint(ConvertPointY(lastPositionSet))];
}
return S_OK;
}

58
nukebuild/Build.cs

@ -16,7 +16,6 @@ using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using Pharmacist.Core;
using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
@ -89,10 +88,6 @@ partial class Build : NukeBuild
Process.Start(new ProcessStartInfo(command, args) {UseShellExecute = false}).WaitForExit();
}
ExecWait("dotnet version:", "dotnet", "--version");
if (Parameters.IsRunningOnUnix)
ExecWait("Mono version:", "mono", "--version");
}
IReadOnlyCollection<Output> MsBuildCommon(
@ -167,44 +162,8 @@ partial class Build : NukeBuild
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
await CompileReactiveEvents();
});
async Task CompileReactiveEvents()
{
var avaloniaBuildOutput = Path.Combine(RootDirectory, "packages", "Avalonia", "bin", Parameters.Configuration);
var avaloniaAssemblies = GlobFiles(avaloniaBuildOutput, "**/Avalonia*.dll")
.Where(file => !file.Contains("Avalonia.Build.Tasks") &&
!file.Contains("Avalonia.Remote.Protocol"));
var eventsDirectory = GlobDirectories($"{RootDirectory}/src/**/Avalonia.ReactiveUI.Events").First();
var eventsBuildFile = Path.Combine(eventsDirectory, "Events_Avalonia.cs");
if (File.Exists(eventsBuildFile))
File.Delete(eventsBuildFile);
using (var stream = File.Create(eventsBuildFile))
using (var writer = new StreamWriter(stream))
{
await ObservablesForEventGenerator.ExtractEventsFromAssemblies(
writer, avaloniaAssemblies, new string[0], "netstandard2.0"
);
}
var eventsProject = Path.Combine(eventsDirectory, "Avalonia.ReactiveUI.Events.csproj");
if (Parameters.IsRunningOnWindows)
MsBuildCommon(eventsProject, c => c
.SetProcessArgumentConfigurator(a => a.Add("/r"))
.AddTargets("Build")
);
else
DotNetBuild(c => c
.SetProjectFile(eventsProject)
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
}
void RunCoreTest(string projectName)
{
Information($"Running tests from {projectName}");
@ -305,14 +264,19 @@ partial class Build : NukeBuild
.Executes(() =>
{
var data = Parameters;
var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore";
var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish";
DotNetPublish(c => c
.SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj")
.EnableNoBuild()
.SetConfiguration(data.Configuration)
.AddProperty("PackageVersion", data.Version)
.AddProperty("PublishDir", pathToPublish));
Zip(data.ZipCoreArtifacts, data.BinRoot);
Zip(data.ZipNuGetArtifacts, data.NugetRoot);
Zip(data.ZipTargetControlCatalogDesktopDir,
GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dll").Concat(
GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.config")).Concat(
GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.so")).Concat(
GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dylib")).Concat(
GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.exe")));
Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish);
});
Target CreateIntermediateNugetPackages => _ => _

7
nukebuild/BuildParameters.cs

@ -58,8 +58,7 @@ public partial class Build
public string FileZipSuffix { get; }
public AbsolutePath ZipCoreArtifacts { get; }
public AbsolutePath ZipNuGetArtifacts { get; }
public AbsolutePath ZipSourceControlCatalogDesktopDir { get; }
public AbsolutePath ZipTargetControlCatalogDesktopDir { get; }
public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; }
public BuildParameters(Build b)
@ -129,9 +128,7 @@ public partial class Build
FileZipSuffix = Version + ".zip";
ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix);
ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix);
ZipSourceControlCatalogDesktopDir =
RootDirectory / ("samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461");
ZipTargetControlCatalogDesktopDir = ZipRoot / ("ControlCatalog.Desktop-" + FileZipSuffix);
ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix);
}
string GetVersion()

1
nukebuild/_build.csproj

@ -17,7 +17,6 @@
<PackageReference Include="ILRepack.NETStandard" Version="2.0.4" />
<!-- Keep in sync with Avalonia.Build.Tasks -->
<PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Pharmacist.Core" Version="1.8.1" />
</ItemGroup>
<ItemGroup>

6
readme.md

@ -2,8 +2,6 @@
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
<img alt="Avalonia" src="https://user-images.githubusercontent.com/6759207/84897744-cab6d800-b0ae-11ea-8214-e5174d71f5c8.png" width="400"/>
## 📖 About AvaloniaUI
Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
@ -16,7 +14,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith
## 🚀 Getting Started
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://avaloniaui.net/docs/quickstart/create-new-project).
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](https://docs.avaloniaui.net/docs/getting-started).
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
@ -54,7 +52,7 @@ We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using
## Documentation
Documentation can be found on our website at https://avaloniaui.net/docs/. We also have a [tutorial](https://avaloniaui.net/docs/tutorial/) over there for newcomers.
Documentation can be found at https://docs.avaloniaui.net. We also have a [tutorial](https://docs.avaloniaui.net/docs/getting-started/programming-with-avalonia) over there for newcomers.
## Building and Using

6
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
@ -15,6 +15,10 @@
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
</ItemGroup>
<PropertyGroup>
<!-- For Microsoft.CodeAnalysis -->
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />

8
samples/ControlCatalog/App.xaml.cs

@ -39,6 +39,10 @@ namespace ControlCatalog
public static Styles DefaultLight = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")
@ -60,6 +64,10 @@ namespace ControlCatalog
public static Styles DefaultDark = new Styles
{
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml")
},
new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
{
Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml")

2
samples/ControlCatalog/ControlCatalog.csproj

@ -27,6 +27,6 @@
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

6
samples/ControlCatalog/MainView.xaml

@ -21,6 +21,9 @@
<TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
<TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
<TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
<TabItem Header="ContextFlyout">
<pages:ContextFlyoutPage/>
</TabItem>
<TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
<TabItem Header="Cursor"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
@ -38,6 +41,9 @@
<pages:CalendarDatePickerPage/></TabItem>
<TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
<TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
<TabItem Header="Flyouts">
<pages:FlyoutsPage />
</TabItem>
<TabItem Header="Image"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">

4
samples/ControlCatalog/MainWindow.xaml

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

102
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

@ -0,0 +1,102 @@
<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="ControlCatalog.Pages.ContextFlyoutPage">
<UserControl.Styles>
<Style Selector="FlyoutPresenter.NoPadding">
<Setter Property="Padding" Value="0" />
</Style>
</UserControl.Styles>
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Context Flyout</TextBlock>
<TextBlock Classes="h2">A right click Flyout that can be applied to any control.</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<Border Background="{DynamicResource SystemAccentColor}"
Margin="16"
Padding="48,48,48,48">
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
<MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
</MenuItem>
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item with _Checkbox">
<MenuItem.Icon>
<CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Border.ContextFlyout>
<TextBlock Text="Defined in XAML"/>
</Border>
<Border Background="{DynamicResource SystemAccentColor}"
Margin="16"
Padding="48,48,48,48">
<Border.ContextMenu>
<ContextMenu Items="{Binding MenuItems}">
<ContextMenu.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="Items" Value="{Binding Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
</Style>
</ContextMenu.Styles>
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Dynamically Generated"/>
</Border>
</StackPanel>
<TextBlock Text="Custom ContextFlyout for TextBox" />
<TextBox Name="TextBox" Width="150" HorizontalAlignment="Center" ContextMenu="{x:Null}">
<TextBox.ContextFlyout>
<Flyout FlyoutPresenterClasses="NoPadding">
<StackPanel Orientation="Horizontal">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="40" />
<Setter Property="Width" Value="40" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Opacity" Value="0.5" />
</Style>
</StackPanel.Styles>
<Button Name="CutButton" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}">
<PathIcon Width="14" Height="14" Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
</Button>
<Button Name="CopyButton" Content="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}">
<PathIcon Width="14" Height="14" Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
</Button>
<Button Name="PasteButton" Content="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}">
<PathIcon Width="14" Height="14" Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
</Button>
<Button Name="ClearButton" Content="Clear" Command="{Binding $parent[TextBox].Clear}">
<PathIcon Width="14" Height="14" Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
</Button>
</StackPanel>
</Flyout>
</TextBox.ContextFlyout>
</TextBox>
</StackPanel>
</UserControl>

45
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs

@ -0,0 +1,45 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public class ContextFlyoutPage : UserControl
{
private TextBox _textBox;
public ContextFlyoutPage()
{
InitializeComponent();
var vm = new ContextFlyoutPageViewModel();
vm.View = this;
DataContext = vm;
_textBox = this.FindControl<TextBox>("TextBox");
var cutButton = this.FindControl<Button>("CutButton");
cutButton.Click += CloseFlyout;
var copyButton = this.FindControl<Button>("CopyButton");
copyButton.Click += CloseFlyout;
var pasteButton = this.FindControl<Button>("PasteButton");
pasteButton.Click += CloseFlyout;
var clearButton = this.FindControl<Button>("ClearButton");
clearButton.Click += CloseFlyout;
}
private void CloseFlyout(object sender, RoutedEventArgs e)
{
_textBox.ContextFlyout.Hide();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

1
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@ -31,6 +31,7 @@
<CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Menu Item that won't close on click" StaysOpenOnClick="True" />
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Defined in XAML"/>

264
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@ -0,0 +1,264 @@
<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="700"
x:Class="ControlCatalog.Pages.FlyoutsPage">
<UserControl.Resources>
<MenuFlyout x:Key="SharedMenuFlyout">
<MenuItem Header="Item 1">
<MenuItem Header="Subitem 1" />
<MenuItem Header="Subitem 2" />
<MenuItem Header="Subitem 3" />
</MenuItem>
<MenuItem Header="Item 2" InputGesture="Ctrl+A" />
<MenuItem Header="Item 3" />
</MenuFlyout>
<Flyout Placement="Bottom" x:Key="BasicFlyout">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</UserControl.Resources>
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
<StackPanel Spacing="10">
<TextBlock FontSize="18" Text="Button with a Flyout" />
<StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
BorderThickness="1" Padding="15">
<Button Content="Click Me!" Flyout="{StaticResource BasicFlyout}" />
</Border>
<Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
<TextBlock Name="ButtonFlyoutXamlText" Padding="15" />
</Panel>
</StackPanel>
<TextBlock FontSize="18" Text="MenuFlyout" />
<StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
BorderThickness="1" Padding="15">
<Button Content="Click Me!" Flyout="{StaticResource SharedMenuFlyout}" />
</Border>
<Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
<TextBlock Name="MenuFlyoutXamlText" Padding="15" />
</Panel>
</StackPanel>
<TextBlock FontSize="18" Text="Attached Flyouts" />
<StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
BorderThickness="1" Padding="15">
<Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
HorizontalAlignment="Left"
Height="100"
Name="AttachedFlyoutPanel">
<FlyoutBase.AttachedFlyout>
<Flyout>
<Panel Height="100">
<TextBlock Text="Attached Flyout!"
VerticalAlignment="Center"
Margin="10"/>
</Panel>
</Flyout>
</FlyoutBase.AttachedFlyout>
<TextBlock Text="Double click panel to launch AttachedFlyout"
VerticalAlignment="Center"
Margin="10"/>
</Panel>
</Border>
<Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
<TextBlock Name="AttachedFlyoutXamlText" Padding="15" />
</Panel>
</StackPanel>
<TextBlock FontSize="18" Text="Sharing Flyouts" />
<StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
BorderThickness="1" Padding="15">
<StackPanel Orientation="Horizontal" Spacing="30">
<Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
<Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
</StackPanel>
</Border>
<Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
<TextBlock Name="SharedFlyoutXamlText" Padding="15" />
</Panel>
</StackPanel>
<TextBlock FontSize="18" Text="Flyout Placements" />
<StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
BorderThickness="1" Padding="15">
<UniformGrid Columns="3">
<UniformGrid.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="10" />
</Style>
</UniformGrid.Styles>
<Button Content="Placement=Top">
<Button.Flyout>
<Flyout Placement="Top">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=Bottom">
<Button.Flyout>
<Flyout Placement="Bottom">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=Left">
<Button.Flyout>
<Flyout Placement="Left">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=Right">
<Button.Flyout>
<Flyout Placement="Right">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=TopEdgeAlignedLeft">
<Button.Flyout>
<Flyout Placement="TopEdgeAlignedLeft">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=TopEdgeAlignedRight">
<Button.Flyout>
<Flyout Placement="TopEdgeAlignedRight">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=BottomEdgeAlignedLeft">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedLeft">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=BottomEdgeAlignedRight">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=LeftEdgeAlignedTop">
<Button.Flyout>
<Flyout Placement="LeftEdgeAlignedTop">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=LeftEdgeAlignedBottom">
<Button.Flyout>
<Flyout Placement="LeftEdgeAlignedBottom">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=RightEdgeAlignedBottom">
<Button.Flyout>
<Flyout Placement="RightEdgeAlignedTop">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=RightEdgeAlignedBottom">
<Button.Flyout>
<Flyout Placement="RightEdgeAlignedBottom">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
</UniformGrid>
</Border>
</StackPanel>
<TextBlock FontSize="18" Text="Flyout ShowMode" />
<StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
BorderThickness="1" Padding="15">
<WrapPanel Orientation="Horizontal">
<WrapPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="4" />
</Style>
</WrapPanel.Styles>
<Button Content="ShowMode=Standard (default)">
<Button.Flyout>
<Flyout>
<StackPanel Width="200">
<TextBox />
<TextBlock Text="Standard ShowMode attempts to focus the Flyout when its opened" TextWrapping="Wrap"/>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="ShowMode=Transient">
<Button.Flyout>
<Flyout ShowMode="Transient">
<StackPanel Width="200">
<TextBox />
<TextBlock Text="Transient ShowMode does not focus the Flyout when opened" TextWrapping="Wrap"/>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="ShowMode=TransientWithDismissOnPointerMoveAway">
<Button.Flyout>
<Flyout ShowMode="TransientWithDismissOnPointerMoveAway">
<StackPanel Width="200">
<TextBox />
<TextBlock Text="Show in Transient mode (no focus), but closes the Flyout when the pointer moves away" TextWrapping="Wrap"/>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</WrapPanel>
</Border>
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

81
samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs

@ -0,0 +1,81 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
using Avalonia.Interactivity;
namespace ControlCatalog.Pages
{
public class FlyoutsPage : UserControl
{
public FlyoutsPage()
{
InitializeComponent();
var afp = this.FindControl<Panel>("AttachedFlyoutPanel");
if (afp != null)
{
afp.DoubleTapped += Afp_DoubleTapped;
}
SetXamlTexts();
}
private void Afp_DoubleTapped(object sender, RoutedEventArgs e)
{
if (sender is Panel p)
{
FlyoutBase.ShowAttachedFlyout(p);
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SetXamlTexts()
{
var bfxt = this.FindControl<TextBlock>("ButtonFlyoutXamlText");
bfxt.Text = "<Button Content=\"Click me!\">\n" +
" <Button.Flyout>\n" +
" <Flyout>\n" +
" <Panel Width=\"100\" Height=\"100\">\n" +
" <TextBlock Text=\"Flyout Content!\" />\n" +
" </Panel>\n" +
" </Flyout>\n" +
" </Button.Flyout>\n</Button>";
var mfxt = this.FindControl<TextBlock>("MenuFlyoutXamlText");
mfxt.Text = "<Button Content=\"Click me!\">\n" +
" <Button.Flyout>\n" +
" <MenuFlyout>\n" +
" <MenuItem Header=\"Item 1\">\n" +
" <MenuItem Header=\"Item 2\">\n" +
" </MenuFlyout>\n" +
" </Button.Flyout>\n</Button>";
var afxt = this.FindControl<TextBlock>("AttachedFlyoutXamlText");
afxt.Text = "<Panel Name=\"AttachedFlyoutPanel\">\n" +
" <FlyoutBase.AttachedFlyout>\n" +
" <Flyout>\n" +
" <Panel Height=\"100\">\n" +
" <TextBlock Text=\"Attached Flyout\" />\n" +
" </Panel>\n" +
" </Flyout>\n" +
" </FlyoutBase.AttachedFlyout>\n</Panel>" +
"\n\n In DoubleTapped handler:\n" +
"FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);";
var sfxt = this.FindControl<TextBlock>("SharedFlyoutXamlText");
sfxt.Text = "Declare a flyout in Resources:\n" +
"<Window.Resources>\n" +
" <Flyout x:Key=\"SharedFlyout\">\n" +
" <Panel Width=\"100\" Height=\"100\">\n" +
" <TextBlock Text=\"Flyout Content!\" />\n" +
" </Panel>\n" +
" </Flyout>\n</Window.Resources>\n\n" +
"Then attach the flyout where you want it:\n" +
"<Button Content=\"Launch Flyout here\" Flyout=\"{StaticResource SharedFlyout}\" />";
}
}
}

2
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -13,7 +13,7 @@
<StackPanel Orientation="Vertical" Spacing="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
<TextBox Width="200" Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" Watermark="Watermark" />
<TextBox Width="200" Watermark="Numeric Watermark" x:Name="numericWatermark"/>
<TextBox Width="200"
Watermark="Floating Watermark"
UseFloatingWatermark="True"

6
samples/ControlCatalog/Pages/TextBoxPage.xaml.cs

@ -13,6 +13,12 @@ namespace ControlCatalog.Pages
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
this.Get<TextBox>("numericWatermark")
.TextInputOptionsQuery += (s, a) =>
{
a.ContentType = Avalonia.Input.TextInput.TextInputContentType.Number;
};
}
}
}

76
samples/ControlCatalog/Pages/ViewboxPage.xaml

@ -1,66 +1,36 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ViewboxPage">
<UserControl.Resources>
<StreamGeometry x:Key="Acorn">
F1 M 16.6309,18.6563C 17.1309,
8.15625 29.8809,14.1563 29.8809,
14.1563C 30.8809,11.1563 34.1308,
11.4063 34.1308,11.4063C 33.5,12
34.6309,13.1563 34.6309,13.1563C
32.1309,13.1562 31.1309,14.9062
31.1309,14.9062C 41.1309,23.9062
32.6309,27.9063 32.6309,27.9062C
24.6309,24.9063 21.1309,22.1562
16.6309,18.6563 Z M 16.6309,19.9063C
21.6309,24.1563 25.1309,26.1562
31.6309,28.6562C 31.6309,28.6562
26.3809,39.1562 18.3809,36.1563C
18.3809,36.1563 18,38 16.3809,36.9063C
15,36 16.3809,34.9063 16.3809,34.9063C
16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z
</StreamGeometry>
</UserControl.Resources>
<Grid RowDefinitions="Auto,*">
<Grid RowDefinitions="Auto,*,*">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">Viewbox</TextBlock>
<TextBlock Classes="h2">A control used to scale single child.</TextBlock>
</StackPanel>
<Grid ColumnDefinitions="Auto,*,*"
RowDefinitions="*,*,*,*"
Grid.Row="1" Margin="48"
MaxWidth="400">
<TextBlock Grid.Row="0" VerticalAlignment="Center">None</TextBlock>
<TextBlock Grid.Row="1" VerticalAlignment="Center">Fill</TextBlock>
<TextBlock Grid.Row="2" VerticalAlignment="Center">Uniform</TextBlock>
<TextBlock Grid.Row="3" VerticalAlignment="Center">UniformToFill</TextBlock>
<Viewbox Grid.Row="0" Grid.Column="1" Stretch="None">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="1" Grid.Column="1" Stretch="Fill">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="2" Grid.Column="1" Stretch="Uniform">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Viewbox Grid.Row="3" Grid.Column="1" Stretch="UniformToFill">
<TextBlock>Hello World!</TextBlock>
</Viewbox>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto" HorizontalAlignment="Center" Margin="0,10,0,0">
<Viewbox Grid.Row="0" Grid.Column="2" Stretch="None">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="1" Grid.Column="2" Stretch="Fill">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="2" Grid.Column="2" Stretch="Uniform">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<Viewbox Grid.Row="3" Grid.Column="2" Stretch="UniformToFill">
<Path Fill="Blue" Data="{StaticResource Acorn}"/>
</Viewbox>
<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}">
<Ellipse Width="50" Height="50" Fill="CornflowerBlue" />
</Viewbox>
</Border>
</Border>
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Grid.Column="1" Margin="8,0,0,0" Width="150">
<TextBlock Text="Width" />
<Slider Minimum="10" Maximum="200" Value="100" x:Name="WidthSlider" TickFrequency="25" TickPlacement="TopLeft" />
<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" />
<TextBlock Text="Stretch Direction" />
<ComboBox x:Name="StretchDirectionSelector" HorizontalAlignment="Stretch" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

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

@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace ControlCatalog.Pages
{
@ -7,7 +8,25 @@ namespace ControlCatalog.Pages
{
public ViewboxPage()
{
this.InitializeComponent();
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()

78
samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs

@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Reactive;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.VisualTree;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class ContextFlyoutPageViewModel
{
public Control View { get; set; }
public ContextFlyoutPageViewModel()
{
OpenCommand = MiniCommand.CreateFromTask(Open);
SaveCommand = MiniCommand.Create(Save);
OpenRecentCommand = MiniCommand.Create<string>(OpenRecent);
MenuItems = new[]
{
new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
new MenuItemViewModel { Header = "Save", Command = SaveCommand },
new MenuItemViewModel { Header = "-" },
new MenuItemViewModel
{
Header = "Recent",
Items = new[]
{
new MenuItemViewModel
{
Header = "File1.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File1.txt"
},
new MenuItemViewModel
{
Header = "File2.txt",
Command = OpenRecentCommand,
CommandParameter = @"c:\foo\File2.txt"
},
}
},
};
}
public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
public MiniCommand OpenCommand { get; }
public MiniCommand SaveCommand { get; }
public MiniCommand OpenRecentCommand { get; }
public async Task Open()
{
var window = View?.GetVisualRoot() as Window;
if (window == null)
return;
var dialog = new OpenFileDialog();
var result = await dialog.ShowAsync(window);
if (result != null)
{
foreach (var path in result)
{
System.Diagnostics.Debug.WriteLine($"Opened: {path}");
}
}
}
public void Save()
{
System.Diagnostics.Debug.WriteLine("Save");
}
public void OpenRecent(string path)
{
System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -57,6 +57,9 @@
<TabItem Header="LineBounds">
<pages:LineBoundsPage />
</TabItem>
<TabItem Header="Path Measurement">
<pages:PathMeasurementPage />
</TabItem>
</TabControl>
</DockPanel>
</Window>

7
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -1,7 +1,8 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.AnimationsPage">
x:Class="RenderDemo.Pages.AnimationsPage"
MaxWidth="600">
<UserControl.Styles>
<Styles>
<Styles.Resources>
@ -167,8 +168,8 @@
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="20">
<TextBlock VerticalAlignment="Center">Hover to activate Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">

89
samples/RenderDemo/Pages/PathMeasurementPage.cs

@ -0,0 +1,89 @@
using System;
using System.Diagnostics;
using System.Drawing.Drawing2D;
using System.Security.Cryptography;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Threading;
using Avalonia.Visuals.Media.Imaging;
namespace RenderDemo.Pages
{
public class PathMeasurementPage : Control
{
static PathMeasurementPage()
{
AffectsRender<PathMeasurementPage>(BoundsProperty);
}
private RenderTargetBitmap _bitmap;
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_bitmap = new RenderTargetBitmap(new PixelSize(500, 500), new Vector(96, 96));
base.OnAttachedToLogicalTree(e);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_bitmap.Dispose();
_bitmap = null;
base.OnDetachedFromLogicalTree(e);
}
readonly IPen strokePen = new ImmutablePen(Brushes.DarkBlue, 10d, null, PenLineCap.Round, PenLineJoin.Round);
readonly IPen strokePen1 = new ImmutablePen(Brushes.Purple, 10d, null, PenLineCap.Round, PenLineJoin.Round);
readonly IPen strokePen2 = new ImmutablePen(Brushes.Green, 10d, null, PenLineCap.Round, PenLineJoin.Round);
readonly IPen strokePen3 = new ImmutablePen(Brushes.LightBlue, 10d, null, PenLineCap.Round, PenLineJoin.Round);
readonly IPen strokePen4 = new ImmutablePen(Brushes.Red, 1d, null, PenLineCap.Round, PenLineJoin.Round);
public override void Render(DrawingContext context)
{
using (var ctxi = _bitmap.CreateDrawingContext(null))
using (var bitmapCtx = new DrawingContext(ctxi, false))
{
ctxi.Clear(default);
var basePath = new PathGeometry();
using (var basePathCtx = basePath.Open())
{
basePathCtx.BeginFigure(new Point(20, 20), false);
basePathCtx.LineTo(new Point(400, 50));
basePathCtx.LineTo(new Point(80, 100));
basePathCtx.LineTo(new Point(300, 150));
basePathCtx.EndFigure(false);
}
bitmapCtx.DrawGeometry(null, strokePen, basePath);
var length = basePath.PlatformImpl.ContourLength;
if (basePath.PlatformImpl.TryGetSegment(length * 0.05, length * 0.2, true, out var dst1))
bitmapCtx.DrawGeometry(null, strokePen1, dst1);
if (basePath.PlatformImpl.TryGetSegment(length * 0.2, length * 0.8, true, out var dst2))
bitmapCtx.DrawGeometry(null, strokePen2, dst2);
if (basePath.PlatformImpl.TryGetSegment(length * 0.8, length * 0.95, true, out var dst3))
bitmapCtx.DrawGeometry(null, strokePen3, dst3);
var pathBounds = basePath.GetRenderBounds(strokePen);
bitmapCtx.DrawRectangle(null, strokePen4, pathBounds);
}
context.DrawImage(_bitmap,
new Rect(0, 0, 500, 500),
new Rect(0, 0, 500, 500));
base.Render(context);
}
}
}

65
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -1,7 +1,8 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.TransitionsPage">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.TransitionsPage"
MaxWidth="600">
<UserControl.Styles>
<Styles>
<Styles.Resources>
@ -90,6 +91,56 @@
<Setter Property="RenderTransform" Value="none" />
</Style>
<Style Selector="Border.Rect7">
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Height" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Rect7:pointerover">
<Setter Property="Height" Value="50" />
</Style>
<Style Selector="Border.Rect8">
<Setter Property="Transitions">
<Transitions>
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Rect8:pointerover">
<Setter Property="CornerRadius" Value="50" />
</Style>
<Style Selector="Border.Rect9">
<Setter Property="Transitions">
<Transitions>
<ThicknessTransition Property="Padding" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Rect9:pointerover">
<Setter Property="Padding" Value="10" />
</Style>
<Style Selector="Border.Shadow">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red, -15 -15 Green"/>
<Setter Property="Transitions">
<Transitions>
<BoxShadowsTransition Property="BoxShadow" Duration="0:0:0.5" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.Shadow:pointerover">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</Style>
</Styles>
</UserControl.Styles>
@ -98,8 +149,8 @@
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="20">
<TextBlock VerticalAlignment="Center">Hover to activate Transitions.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">
@ -109,6 +160,12 @@
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Orange"/>
<Border Classes="Test Rect7" Background="Gold"/>
<Border Classes="Test Rect8" Background="Gray" />
<Border Classes="Test Rect9" Background="Red" />
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
</WrapPanel>
</StackPanel>
</Grid>

96
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -0,0 +1,96 @@
using System;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Input;
using Avalonia.Input.TextInput;
namespace Avalonia.Android
{
class AndroidInputMethod<TView> : ITextInputMethodImpl
where TView: View, IInitEditorInfo
{
private readonly TView _host;
private readonly InputMethodManager _imm;
private IInputElement _inputElement;
public AndroidInputMethod(TView host)
{
if (host.OnCheckIsTextEditor() == false)
throw new InvalidOperationException("Host should return true from OnCheckIsTextEditor()");
_host = host;
_imm = host.Context.GetSystemService(Context.InputMethodService).JavaCast<InputMethodManager>();
_host.Focusable = true;
_host.FocusableInTouchMode = true;
_host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListner(_host));
}
public void Reset()
{
_imm.RestartInput(_host);
}
public void SetActive(bool active)
{
if (active)
{
_host.RequestFocus();
Reset();
_imm.ShowSoftInput(_host, ShowFlags.Implicit);
}
else
_imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
}
public void SetCursorRect(Rect rect)
{
}
public void SetOptions(TextInputOptionsQueryEventArgs options)
{
if (_inputElement != null)
{
_inputElement.PointerReleased -= RestoreSoftKeyboard;
}
_inputElement = options.Source as InputElement;
if (_inputElement == null)
{
_imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None);
}
_host.InitEditorInfo((outAttrs) =>
{
outAttrs.InputType = options.ContentType switch
{
TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress,
TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber,
TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword,
TextInputContentType.Phone => global::Android.Text.InputTypes.ClassPhone,
TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri,
_ => global::Android.Text.InputTypes.ClassText
};
if (options.AutoCapitalization)
{
outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences;
outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences;
}
if (options.Multiline)
outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine;
});
//_inputElement.PointerReleased += RestoreSoftKeyboard;
}
private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e)
{
_imm.ShowSoftInput(_host, ShowFlags.Implicit);
}
}
}

4
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -15,7 +15,6 @@ namespace Avalonia.Android
if (_content != null)
View.Content = _content;
SetContentView(View);
TakeKeyEvents(true);
base.OnCreate(savedInstanceState);
}
@ -32,8 +31,5 @@ namespace Avalonia.Android
View.Content = value;
}
}
public override bool DispatchKeyEvent(KeyEvent e) =>
View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e);
}
}

12
src/Android/Avalonia.Android/IInitEditorInfo.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using Android.Views.InputMethods;
namespace Avalonia.Android
{
interface IInitEditorInfo
{
void InitEditorInfo(Action<EditorInfo> init);
}
}

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

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

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

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

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

@ -13,7 +13,7 @@ namespace Avalonia.Android
bool _invalidateQueued;
readonly object _lock = new object();
private readonly Handler _handler;
public InvalidationAwareSurfaceView(Context context) : base(context)
{

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

@ -4,14 +4,16 @@ using Android.Content;
using Android.Graphics;
using Android.Runtime;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.OpenGL;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
@ -19,19 +21,20 @@ using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidTouchEventsHelper<TopLevelImpl> _touchHelper;
private readonly ITextInputMethodImpl _textInputMethod;
private ViewImpl _view;
public TopLevelImpl(Context context, bool placeOnTop = false)
{
_view = new ViewImpl(context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
GetAvaloniaPointFromEvent);
@ -39,20 +42,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_gl = GlPlatformSurface.TryCreate(this);
_framebuffer = new FramebufferManager(this);
MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels);
}
private bool _handleEvents;
RenderScaling = (int)_view.Resources.DisplayMetrics.Density;
public bool HandleEvents
{
get { return _handleEvents; }
set
{
_handleEvents = value;
_keyboardHelper.HandleEvents = _handleEvents;
}
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
}
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -60,19 +53,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IInputRoot InputRoot { get; private set; }
public virtual Size ClientSize
{
get
{
if (_view == null)
return new Size(0, 0);
return new Size(_view.Width, _view.Height);
}
set
{
}
}
public virtual Size ClientSize => Size.ToSize(RenderScaling);
public IMouseDevice MouseDevice { get; } = new MouseDevice();
@ -113,12 +94,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Point PointToClient(PixelPoint point)
{
return point.ToPoint(1);
return point.ToPoint(RenderScaling);
}
public PixelPoint PointToScreen(Point point)
{
return PixelPoint.FromPoint(point, 1);
return PixelPoint.FromPoint(point, RenderScaling);
}
public void SetCursor(ICursorImpl cursor)
@ -136,7 +117,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Visibility = ViewStates.Visible;
}
public double RenderScaling => 1;
public double RenderScaling { get; }
void Draw()
{
@ -154,7 +135,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
Resized?.Invoke(size);
}
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo
{
private readonly TopLevelImpl _tl;
private Size _oldSize;
@ -191,7 +172,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format, int width, int height)
{
var newSize = new Size(width, height);
var newSize = new PixelSize(width, height).ToSize(_tl.RenderScaling);
if (newSize != _oldSize)
{
@ -201,6 +182,27 @@ namespace Avalonia.Android.Platform.SkiaPlatform
base.SurfaceChanged(holder, format, width, height);
}
public sealed override bool OnCheckIsTextEditor()
{
return true;
}
private Action<EditorInfo> _initEditorInfo;
public void InitEditorInfo(Action<EditorInfo> init)
{
_initEditorInfo = init;
}
public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs)
{
if (_initEditorInfo != null)
_initEditorInfo(outAttrs);
return base.OnCreateInputConnection(outAttrs);
}
}
public IPopupImpl CreatePopup() => null;
@ -219,6 +221,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public double Scaling => RenderScaling;
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{
throw new NotImplementedException();

83
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs

@ -1,12 +1,7 @@
using System;
using System.ComponentModel;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Raw;
@ -14,14 +9,13 @@ namespace Avalonia.Android.Platform.Specific.Helpers
{
internal class AndroidKeyboardEventsHelper<TView> : IDisposable where TView : TopLevelImpl, IAndroidView
{
private TView _view;
private IInputElement _lastFocusedElement;
private readonly TView _view;
public bool HandleEvents { get; set; }
public AndroidKeyboardEventsHelper(TView view)
{
this._view = view;
_view = view;
HandleEvents = true;
}
@ -36,9 +30,20 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return DispatchKeyEventInternal(e, out callBase);
}
string? UnicodeTextInput(KeyEvent keyEvent)
{
return keyEvent.Action == KeyEventActions.Multiple
&& keyEvent.RepeatCount == 0
&& !string.IsNullOrEmpty(keyEvent?.Characters)
? keyEvent.Characters
: null;
}
private bool? DispatchKeyEventInternal(KeyEvent e, out bool callBase)
{
if (e.Action == KeyEventActions.Multiple)
var unicodeTextInput = UnicodeTextInput(e);
if (e.Action == KeyEventActions.Multiple && unicodeTextInput == null)
{
callBase = true;
return null;
@ -53,13 +58,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers
_view.Input(rawKeyEvent);
if (e.Action == KeyEventActions.Down && e.UnicodeChar >= 32)
if ((e.Action == KeyEventActions.Down && e.UnicodeChar >= 32)
|| unicodeTextInput != null)
{
var rawTextEvent = new RawTextInputEventArgs(
AndroidKeyboardDevice.Instance,
Convert.ToUInt32(e.EventTime),
_view.InputRoot,
Convert.ToChar(e.UnicodeChar).ToString()
unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString()
);
_view.Input(rawTextEvent);
}
@ -85,61 +91,6 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return rv;
}
private bool NeedsKeyboard(IInputElement element)
{
//may be some other elements
return element is TextBox;
}
private void TryShowHideKeyboard(IInputElement element, bool value)
{
var input = _view.View.Context.GetSystemService(Context.InputMethodService).JavaCast<InputMethodManager>();
if (value)
{
//show keyboard
//may be in the future different keyboards support e.g. normal, only digits etc.
//Android.Text.InputTypes
input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.ImplicitOnly);
}
else
{
//hide keyboard
input.HideSoftInputFromWindow(_view.View.WindowToken, HideSoftInputFlags.None);
}
}
public void UpdateKeyboardState(IInputElement element)
{
var focusedElement = element;
bool oldValue = NeedsKeyboard(_lastFocusedElement);
bool newValue = NeedsKeyboard(focusedElement);
if (newValue != oldValue || newValue)
{
TryShowHideKeyboard(focusedElement, newValue);
}
_lastFocusedElement = element;
}
public void ActivateAutoShowKeyboard()
{
var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged);
//just in case we've called more than once the method
kbDevice.PropertyChanged -= KeyboardDevice_PropertyChanged;
kbDevice.PropertyChanged += KeyboardDevice_PropertyChanged;
}
private void KeyboardDevice_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(KeyboardDevice.FocusedElement))
{
UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement);
}
}
public void Dispose()
{
HandleEvents = false;

42
src/Android/Avalonia.Android/SoftKeyboardListner.cs

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Text;
using Android.Content;
using Android.OS;
using Android.Util;
using Android.Views;
using Avalonia.Input;
namespace Avalonia.Android
{
class SoftKeyboardListner : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
{
private const int DefaultKeyboardHeightDP = 100;
private static readonly int EstimatedKeyboardDP = DefaultKeyboardHeightDP + (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop ? 48 : 0);
private readonly View _host;
private bool _wasKeyboard;
public SoftKeyboardListner(View view)
{
_host = view;
}
public void OnGlobalLayout()
{
int estimatedKeyboardHeight = (int)TypedValue.ApplyDimension(ComplexUnitType.Dip,
EstimatedKeyboardDP, _host.Resources.DisplayMetrics);
var rect = new global::Android.Graphics.Rect();
_host.GetWindowVisibleDisplayFrame(rect);
int heightDiff = _host.RootView.Height - (rect.Bottom - rect.Top);
var isKeyboard = heightDiff >= estimatedKeyboardHeight;
if (_wasKeyboard && !isKeyboard)
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
_wasKeyboard = isKeyboard;
}
}
}

28
src/Android/Avalonia.AndroidTestApplication/MainActivity.cs

@ -16,18 +16,18 @@ namespace Avalonia.AndroidTestApplication
Icon = "@drawable/icon",
LaunchMode = LaunchMode.SingleInstance/*,
ScreenOrientation = ScreenOrientation.Landscape*/)]
public class MainBaseActivity : Activity
public class MainBaseActivity : AvaloniaActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
if (Avalonia.Application.Current == null)
{
AppBuilder.Configure<App>()
.UseAndroid()
.SetupWithoutStarting();
}
SetContentView(new AvaloniaView(this) { Content = App.CreateSimpleWindow() });
base.OnCreate(savedInstanceState);
Content = App.CreateSimpleWindow();
}
}
@ -72,13 +72,33 @@ namespace Avalonia.AndroidTestApplication
Height = 40,
Background = Brushes.LightGreen,
Foreground = Brushes.Black
}
},
CreateTextBox(Input.TextInput.TextInputContentType.Normal),
CreateTextBox(Input.TextInput.TextInputContentType.Password),
CreateTextBox(Input.TextInput.TextInputContentType.Email),
CreateTextBox(Input.TextInput.TextInputContentType.Url),
CreateTextBox(Input.TextInput.TextInputContentType.Phone),
CreateTextBox(Input.TextInput.TextInputContentType.Number),
}
}
};
return window;
}
private static TextBox CreateTextBox(Input.TextInput.TextInputContentType contentType)
{
var textBox = new TextBox()
{
Margin = new Thickness(20, 10),
Watermark = contentType.ToString(),
BorderThickness = new Thickness(3),
FontSize = 20
};
textBox.TextInputOptionsQuery += (s, e) => { e.ContentType = contentType; };
return textBox;
}
}
}

28
src/Avalonia.Animation/Animatable.cs

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

11
src/Avalonia.Animation/Animation.cs

@ -209,6 +209,17 @@ namespace Avalonia.Animation
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
};
/// <summary>
/// Registers a <see cref="Animator{T}"/> that can handle
/// a value type that matches the specified condition.
/// </summary>
/// <param name="condition">
/// The condition to which the <see cref="Animator{T}"/>
/// is to be activated and used.
/// </param>
/// <typeparam name="TAnimator">
/// The type of the animator to instantiate.
/// </typeparam>
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
where TAnimator : IAnimator
{

20
src/Avalonia.Animation/AnimatorDrivenTransition.cs

@ -0,0 +1,20 @@
using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// <see cref="Transition{T}"/> using an <see cref="Animator{T}"/> to transition between values.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
/// <typeparam name="TAnimator">Type of the animator.</typeparam>
public abstract class AnimatorDrivenTransition<T, TAnimator> : Transition<T> where TAnimator : Animator<T>, new()
{
private static readonly TAnimator s_animator = new TAnimator();
public override IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue)
{
return new AnimatorTransitionObservable<T, TAnimator>(s_animator, progress, Easing, oldValue, newValue);
}
}
}

32
src/Avalonia.Animation/AnimatorTransitionObservable.cs

@ -0,0 +1,32 @@
using System;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
{
/// <summary>
/// Transition observable based on an <see cref="Animator{T}"/> producing a value.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
/// <typeparam name="TAnimator">Type of the animator.</typeparam>
public class AnimatorTransitionObservable<T, TAnimator> : TransitionObservableBase<T> where TAnimator : Animator<T>
{
private readonly TAnimator _animator;
private readonly Easing _easing;
private readonly T _oldValue;
private readonly T _newValue;
public AnimatorTransitionObservable(TAnimator animator, IObservable<double> progress, Easing easing, T oldValue, T newValue) : base(progress, easing)
{
_animator = animator;
_easing = easing;
_oldValue = oldValue;
_newValue = newValue;
}
protected override T ProduceValue(double progress)
{
return _animator.Interpolate(progress, _oldValue, _newValue);
}
}
}

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

@ -104,6 +104,11 @@ namespace Avalonia.Animation.Animators
throw new Exception("Index time is out of keyframe time range.");
}
public virtual IDisposable BindAnimation(Animatable control, IObservable<T> instance)
{
return control.Bind((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
@ -116,7 +121,8 @@ namespace Avalonia.Animation.Animators
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler);
return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
return BindAnimation(control, instance);
}
/// <summary>

9
src/Avalonia.Animation/Clock.cs

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
@ -10,10 +6,9 @@ namespace Avalonia.Animation
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService<IGlobalClock>();
private IDisposable _parentSubscription;
private readonly IDisposable _parentSubscription;
public Clock()
:this(GlobalClock)
public Clock() : this(GlobalClock)
{
}

12
src/Avalonia.Animation/ClockBase.cs

@ -1,16 +1,11 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
public class ClockBase : IClock
{
private ClockObservable _observable;
private IObservable<TimeSpan> _connectedObservable;
private readonly ClockObservable _observable;
private TimeSpan? _previousTime;
private TimeSpan _internalTime;
@ -18,7 +13,6 @@ namespace Avalonia.Animation
protected ClockBase()
{
_observable = new ClockObservable();
_connectedObservable = _observable.Publish().RefCount();
}
protected bool HasSubscriptions => _observable.HasSubscriptions;
@ -58,10 +52,10 @@ namespace Avalonia.Animation
public IDisposable Subscribe(IObserver<TimeSpan> observer)
{
return _connectedObservable.Subscribe(observer);
return _observable.Subscribe(observer);
}
private class ClockObservable : LightweightObservableBase<TimeSpan>
private sealed class ClockObservable : LightweightObservableBase<TimeSpan>
{
public bool HasSubscriptions { get; private set; }
public void Pulse(TimeSpan time) => PublishNext(time);

6
src/Avalonia.Animation/Transition`1.cs → src/Avalonia.Animation/Transition.cs

@ -1,7 +1,5 @@
using System;
using System.Reactive.Linq;
using System;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
namespace Avalonia.Animation
{
@ -56,4 +54,4 @@ namespace Avalonia.Animation
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}
}
}

60
src/Avalonia.Animation/TransitionInstance.cs

@ -1,8 +1,5 @@
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
using System.Runtime.ExceptionServices;
using Avalonia.Reactive;
using Avalonia.Utilities;
@ -11,13 +8,13 @@ namespace Avalonia.Animation
/// <summary>
/// Handles the timing and lifetime of a <see cref="Transition{T}"/>.
/// </summary>
internal class TransitionInstance : SingleSubscriberObservableBase<double>
internal class TransitionInstance : SingleSubscriberObservableBase<double>, IObserver<TimeSpan>
{
private IDisposable _timerSubscription;
private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
private IClock _clock;
private TransitionClock _clock;
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
@ -75,9 +72,56 @@ namespace Avalonia.Animation
protected override void Subscribed()
{
_clock = new Clock(_baseClock);
_timerSubscription = _clock.Subscribe(TimerTick);
_clock = new TransitionClock(_baseClock);
_timerSubscription = _clock.Subscribe(this);
PublishNext(0.0d);
}
void IObserver<TimeSpan>.OnCompleted()
{
PublishCompleted();
}
void IObserver<TimeSpan>.OnError(Exception error)
{
PublishError(error);
}
void IObserver<TimeSpan>.OnNext(TimeSpan value)
{
TimerTick(value);
}
/// <summary>
/// TODO: This clock is still fairly expensive due to <see cref="ClockBase"/> implementation.
/// </summary>
private sealed class TransitionClock : ClockBase, IObserver<TimeSpan>
{
private readonly IDisposable _parentSubscription;
public TransitionClock(IClock parent)
{
_parentSubscription = parent.Subscribe(this);
}
protected override void Stop()
{
_parentSubscription.Dispose();
}
void IObserver<TimeSpan>.OnNext(TimeSpan value)
{
Pulse(value);
}
void IObserver<TimeSpan>.OnCompleted()
{
}
void IObserver<TimeSpan>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
}
}
}

58
src/Avalonia.Animation/TransitionObservableBase.cs

@ -0,0 +1,58 @@
using System;
using Avalonia.Animation.Easings;
using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Animation
{
/// <summary>
/// Provides base for observables implementing transitions.
/// </summary>
/// <typeparam name="T">Type of the transitioned value.</typeparam>
public abstract class TransitionObservableBase<T> : SingleSubscriberObservableBase<T>, IObserver<double>
{
private readonly Easing _easing;
private readonly IObservable<double> _progress;
private IDisposable? _progressSubscription;
protected TransitionObservableBase(IObservable<double> progress, Easing easing)
{
_progress = progress;
_easing = easing;
}
/// <summary>
/// Produces value at given progress time point.
/// </summary>
/// <param name="progress">Transition progress.</param>
protected abstract T ProduceValue(double progress);
protected override void Subscribed()
{
_progressSubscription = _progress.Subscribe(this);
}
protected override void Unsubscribed()
{
_progressSubscription?.Dispose();
}
void IObserver<double>.OnCompleted()
{
PublishCompleted();
}
void IObserver<double>.OnError(Exception error)
{
PublishError(error);
}
void IObserver<double>.OnNext(double value)
{
double progress = _easing.Ease(value);
PublishNext(ProduceValue(progress));
}
}
}

15
src/Avalonia.Animation/Transitions/DoubleTransition.cs

@ -1,22 +1,11 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types.
/// </summary>
public class DoubleTransition : Transition<double>
public class DoubleTransition : AnimatorDrivenTransition<double, DoubleAnimator>
{
/// <inheritdocs/>
public override IObservable<double> DoTransition(IObservable<double> progress, double oldValue, double newValue)
{
return progress
.Select(p =>
{
var f = Easing.Ease(p);
return ((newValue - oldValue) * f) + oldValue;
});
}
}
}

12
src/Avalonia.Animation/Transitions/FloatTransition.cs

@ -1,19 +1,11 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="float"/> types.
/// </summary>
public class FloatTransition : Transition<float>
public class FloatTransition : AnimatorDrivenTransition<float, FloatAnimator>
{
/// <inheritdocs/>
public override IObservable<float> DoTransition(IObservable<double> progress, float oldValue, float newValue)
{
var delta = newValue - oldValue;
return progress
.Select(p => (float)Easing.Ease(p) * delta + oldValue);
}
}
}

12
src/Avalonia.Animation/Transitions/IntegerTransition.cs

@ -1,19 +1,11 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="int"/> types.
/// </summary>
public class IntegerTransition : Transition<int>
public class IntegerTransition : AnimatorDrivenTransition<int, Int32Animator>
{
/// <inheritdocs/>
public override IObservable<int> DoTransition(IObservable<double> progress, int oldValue, int newValue)
{
var delta = newValue - oldValue;
return progress
.Select(p => (int)(Easing.Ease(p) * delta + oldValue));
}
}
}

39
src/Avalonia.Base/AvaloniaObject.cs

@ -7,6 +7,8 @@ using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Threading;
#nullable enable
namespace Avalonia
{
/// <summary>
@ -17,12 +19,12 @@ namespace Avalonia
/// </remarks>
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
{
private IAvaloniaObject _inheritanceParent;
private List<IDisposable> _directBindings;
private PropertyChangedEventHandler _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
private List<IAvaloniaObject> _inheritanceChildren;
private ValueStore _values;
private IAvaloniaObject? _inheritanceParent;
private List<IDisposable>? _directBindings;
private PropertyChangedEventHandler? _inpcChanged;
private EventHandler<AvaloniaPropertyChangedEventArgs>? _propertyChanged;
private List<IAvaloniaObject>? _inheritanceChildren;
private ValueStore? _values;
private bool _batchUpdate;
/// <summary>
@ -36,7 +38,7 @@ namespace Avalonia
/// <summary>
/// Raised when a <see cref="AvaloniaProperty"/> value changes on this object.
/// </summary>
public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged
public event EventHandler<AvaloniaPropertyChangedEventArgs>? PropertyChanged
{
add { _propertyChanged += value; }
remove { _propertyChanged -= value; }
@ -58,7 +60,7 @@ namespace Avalonia
/// <value>
/// The inheritance parent.
/// </value>
protected IAvaloniaObject InheritanceParent
protected IAvaloniaObject? InheritanceParent
{
get
{
@ -289,7 +291,8 @@ namespace Avalonia
/// <returns>True if the property is animating, otherwise false.</returns>
public bool IsAnimating(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
return _values?.IsAnimating(property) ?? false;
@ -306,7 +309,8 @@ namespace Avalonia
/// </remarks>
public bool IsSet(AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(property != null);
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
return _values?.IsSet(property) ?? false;
@ -320,7 +324,7 @@ namespace Avalonia
/// <param name="priority">The priority of the value.</param>
public void SetValue(
AvaloniaProperty property,
object value,
object? value,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
@ -338,7 +342,7 @@ namespace Avalonia
/// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
public IDisposable SetValue<T>(
public IDisposable? SetValue<T>(
StyledPropertyBase<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
@ -497,7 +501,7 @@ namespace Avalonia
}
/// <inheritdoc/>
Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{
return _propertyChanged?.GetInvocationList();
}
@ -723,7 +727,8 @@ namespace Avalonia
{
var values = o._values;
if (values?.TryGetValue(property, maxPriority, out value) == true)
if (values != null
&& values.TryGetValue(property, maxPriority, out value) == true)
{
return value;
}
@ -873,7 +878,7 @@ namespace Avalonia
}
else
{
LogBindingError(property, value.Error);
LogBindingError(property, value.Error!);
}
}
}
@ -907,14 +912,14 @@ namespace Avalonia
{
_owner = owner;
_property = property;
_owner._directBindings.Add(this);
_owner._directBindings!.Add(this);
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription.Dispose();
_owner._directBindings.Remove(this);
_owner._directBindings!.Remove(this);
}
public void OnCompleted() => Dispose();

6
src/Avalonia.Base/Data/BindingValue.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities;
@ -108,12 +109,12 @@ namespace Avalonia.Data
/// Gets a value indicating whether the binding value represents either a binding or data
/// validation error.
/// </summary>
public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
public bool HasError => Type.HasAllFlags(BindingValueType.HasError);
/// <summary>
/// Gets a value indicating whether the binding value has a value.
/// </summary>
public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
public bool HasValue => Type.HasAllFlags(BindingValueType.HasValue);
/// <summary>
/// Gets the type of the binding value.
@ -358,6 +359,7 @@ namespace Avalonia.Data
e);
}
[Conditional("DEBUG")]
private static void ValidateValue([AllowNull] T value)
{
if (value is UnsetValueType)

2
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -20,7 +20,7 @@ namespace Avalonia.Data.Core
protected override void StartListeningCore(WeakReference<object> reference)
{
GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
_subscription = GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
}
protected override void StopListeningCore()

14
src/Avalonia.Base/Data/Optional.cs

@ -153,4 +153,18 @@ namespace Avalonia.Data
/// </summary>
public static Optional<T> Empty => default;
}
public static class OptionalExtensions
{
/// <summary>
/// Casts the type of an <see cref="Optional{T}"/> using only the C# cast operator.
/// </summary>
/// <typeparam name="T">The target type.</typeparam>
/// <param name="value">The binding value.</param>
/// <returns>The cast value.</returns>
public static Optional<T> Cast<T>(this Optional<object> value)
{
return value.HasValue ? new Optional<T>((T)value.Value) : Optional<T>.Empty;
}
}
}

4
src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs

@ -1,5 +1,7 @@
using System;
#nullable enable
namespace Avalonia.Diagnostics
{
/// <summary>
@ -14,6 +16,6 @@ namespace Avalonia.Diagnostics
/// <returns>
/// The subscribers or null if no subscribers.
/// </returns>
Delegate[] GetPropertyChangedSubscribers();
Delegate[]? GetPropertyChangedSubscribers();
}
}

54
src/Avalonia.Base/EnumExtensions.cs

@ -9,31 +9,67 @@ namespace Avalonia
public static class EnumExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
[Obsolete("This method is obsolete. Use HasAllFlags instead.")]
public static bool HasFlagCustom<T>(this T value, T flag) where T : unmanaged, Enum
=> value.HasAllFlags(flag);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool HasAllFlags<T>(this T value, T flags) where T : unmanaged, Enum
{
if (sizeof(T) == 1)
{
var byteValue = Unsafe.As<T, byte>(ref value);
var byteFlags = Unsafe.As<T, byte>(ref flags);
return (byteValue & byteFlags) == byteFlags;
}
else if (sizeof(T) == 2)
{
var shortValue = Unsafe.As<T, short>(ref value);
var shortFlags = Unsafe.As<T, short>(ref flags);
return (shortValue & shortFlags) == shortFlags;
}
else if (sizeof(T) == 4)
{
var intValue = Unsafe.As<T, int>(ref value);
var intFlags = Unsafe.As<T, int>(ref flags);
return (intValue & intFlags) == intFlags;
}
else if (sizeof(T) == 8)
{
var longValue = Unsafe.As<T, long>(ref value);
var longFlags = Unsafe.As<T, long>(ref flags);
return (longValue & longFlags) == longFlags;
}
else
throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf<T>() + " are not supported");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool HasAnyFlag<T>(this T value, T flags) where T : unmanaged, Enum
{
if (sizeof(T) == 1)
{
var byteValue = Unsafe.As<T, byte>(ref value);
var byteFlag = Unsafe.As<T, byte>(ref flag);
return (byteValue & byteFlag) == byteFlag;
var byteFlags = Unsafe.As<T, byte>(ref flags);
return (byteValue & byteFlags) != 0;
}
else if (sizeof(T) == 2)
{
var shortValue = Unsafe.As<T, short>(ref value);
var shortFlag = Unsafe.As<T, short>(ref flag);
return (shortValue & shortFlag) == shortFlag;
var shortFlags = Unsafe.As<T, short>(ref flags);
return (shortValue & shortFlags) != 0;
}
else if (sizeof(T) == 4)
{
var intValue = Unsafe.As<T, int>(ref value);
var intFlag = Unsafe.As<T, int>(ref flag);
return (intValue & intFlag) == intFlag;
var intFlags = Unsafe.As<T, int>(ref flags);
return (intValue & intFlags) != 0;
}
else if (sizeof(T) == 8)
{
var longValue = Unsafe.As<T, long>(ref value);
var longFlag = Unsafe.As<T, long>(ref flag);
return (longValue & longFlag) == longFlag;
var longFlags = Unsafe.As<T, long>(ref flags);
return (longValue & longFlags) != 0;
}
else
throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf<T>() + " are not supported");

9
src/Avalonia.Base/IAvaloniaObject.cs

@ -1,6 +1,8 @@
using System;
using Avalonia.Data;
#nullable enable
namespace Avalonia
{
/// <summary>
@ -11,7 +13,7 @@ namespace Avalonia
/// <summary>
/// Raised when a <see cref="AvaloniaProperty"/> value changes on this object.
/// </summary>
event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged;
event EventHandler<AvaloniaPropertyChangedEventArgs>? PropertyChanged;
/// <summary>
/// Clears an <see cref="AvaloniaProperty"/>'s local value.
@ -75,7 +77,10 @@ namespace Avalonia
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <param name="priority">The priority of the value.</param>
IDisposable SetValue<T>(
/// <returns>
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
IDisposable? SetValue<T>(
StyledPropertyBase<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue);

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

@ -65,7 +65,6 @@ namespace Avalonia.PropertyStore
{
_subscription?.Dispose();
_subscription = null;
_isSubscribed = false;
OnCompleted();
}
@ -74,12 +73,13 @@ namespace Avalonia.PropertyStore
var oldValue = _value;
_value = default;
Priority = BindingPriority.Unset;
_isSubscribed = false;
_sink.Completed(Property, this, oldValue);
}
public void OnError(Exception error)
{
throw new NotImplementedException();
throw new NotImplementedException("BindingEntry.OnError is not implemented", error);
}
public void OnNext(BindingValue<T> value)
@ -104,8 +104,11 @@ namespace Avalonia.PropertyStore
public void Start(bool ignoreBatchUpdate)
{
// We can't use _subscription to check whether we're subscribed because it won't be set
// until Subscribe has finished, which will be too late to prevent reentrancy.
if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
// until Subscribe has finished, which will be too late to prevent reentrancy. In addition
// don't re-subscribe to completed/disposed bindings (indicated by Unset priority).
if (!_isSubscribed &&
Priority != BindingPriority.Unset &&
(!_batchUpdate || ignoreBatchUpdate))
{
_isSubscribed = true;
_subscription = Source.Subscribe(this);
@ -124,8 +127,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}

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

@ -6,12 +6,19 @@ using Avalonia.Data;
namespace Avalonia.PropertyStore
{
/// <summary>
/// Represents an untyped interface to <see cref="ConstantValueEntry{T}"/>.
/// </summary>
internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
{
}
/// <summary>
/// Stores a value with a priority in a <see cref="ValueStore"/> or
/// <see cref="PriorityValue{T}"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IDisposable
internal class ConstantValueEntry<T> : IPriorityValueEntry<T>, IConstantValueEntry
{
private IValueSink _sink;
private Optional<T> _value;
@ -58,8 +65,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}
}

4
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@ -36,8 +36,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
BindingPriority.LocalValue));
}
}

4
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -197,8 +197,8 @@ namespace Avalonia.PropertyStore
sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
owner,
(AvaloniaProperty<T>)property,
oldValue.GetValueOrDefault<T>(),
newValue.GetValueOrDefault<T>(),
oldValue.Cast<T>(),
newValue.Cast<T>(),
Priority));
}

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

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

15
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -30,6 +30,21 @@ namespace Avalonia.Utilities
return (-eps < delta) && (eps > delta);
}
/// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
/// <param name="eps"> The fixed epsilon value used to compare.</param>
public static bool AreClose(double value1, double value2, double eps)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
/// <summary>
/// AreClose - Returns whether or not two floats are "close". That is, whether or
/// not they are within epsilon of each other.

4
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -372,8 +372,8 @@ namespace Avalonia.Utilities
const string implicitName = "op_Implicit";
const string explicitName = "op_Explicit";
bool allowImplicit = operatorType.HasFlagCustom(OperatorType.Implicit);
bool allowExplicit = operatorType.HasFlagCustom(OperatorType.Explicit);
bool allowImplicit = operatorType.HasAllFlags(OperatorType.Implicit);
bool allowExplicit = operatorType.HasAllFlags(OperatorType.Explicit);
foreach (MethodInfo method in fromType.GetMethods())
{

49
src/Avalonia.Base/ValueStore.cs

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

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

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

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

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

2
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@ -2595,7 +2595,7 @@ namespace Avalonia.Collections
/// <returns>Whether the specified flag is set</returns>
private bool CheckFlag(CollectionViewFlags flags)
{
return _flags.HasFlagCustom(flags);
return _flags.HasAllFlags(flags);
}
/// <summary>

37
src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs

@ -265,6 +265,43 @@ namespace Avalonia.Collections
{
return new DataGridPathSortDescription(propertyPath, direction, comparer, null);
}
public static DataGridSortDescription FromComparer(IComparer comparer, ListSortDirection direction = ListSortDirection.Ascending)
{
return new DataGridComparerSortDesctiption(comparer, direction);
}
}
public class DataGridComparerSortDesctiption : DataGridSortDescription
{
private readonly IComparer _innerComparer;
private readonly ListSortDirection _direction;
private readonly IComparer<object> _comparer;
public IComparer SourceComparer => _innerComparer;
public override IComparer<object> Comparer => _comparer;
public override ListSortDirection Direction => _direction;
public DataGridComparerSortDesctiption(IComparer comparer, ListSortDirection direction)
{
_innerComparer = comparer;
_direction = direction;
_comparer = Comparer<object>.Create((x, y) => Compare(x, y));
}
private int Compare(object x, object y)
{
int result = _innerComparer.Compare(x, y);
if (Direction == ListSortDirection.Descending)
return -result;
else
return result;
}
public override DataGridSortDescription SwitchSortDirection()
{
var newDirection = _direction == ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
return new DataGridComparerSortDesctiption(_innerComparer, newDirection);
}
}
public class DataGridSortDescriptionCollection : AvaloniaList<DataGridSortDescription>

13
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -535,7 +535,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<DataGrid, int>(
nameof(SelectedIndex),
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v);
(o, v) => o.SelectedIndex = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the index of the current selection.
@ -553,7 +554,8 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<DataGrid, object>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
(o, v) => o.SelectedItem = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Gets or sets the data item corresponding to the selected row.
@ -910,6 +912,11 @@ namespace Avalonia.Controls
// Clear all row selections
ClearRowSelection(resetAnchorSlot: true);
if (DataConnection.CollectionView != null)
{
DataConnection.CollectionView.MoveCurrentTo(null);
}
}
else
{
@ -5352,7 +5359,7 @@ namespace Avalonia.Controls
_focusedRow = null;
}
private void SelectAll()
public void SelectAll()
{
SetRowsSelection(0, SlotCount - 1);
}

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

@ -1009,6 +1009,14 @@ namespace Avalonia.Controls
get;
set;
}
/// <summary>
/// Holds a Comparer to use for sorting, if not using the default.
/// </summary>
public System.Collections.IComparer CustomSortComparer
{
get;
set;
}
/// <summary>
/// We get the sort description from the data source. We don't worry whether we can modify sort -- perhaps the sort description
@ -1020,6 +1028,14 @@ namespace Avalonia.Controls
&& OwningGrid.DataConnection != null
&& OwningGrid.DataConnection.SortDescriptions != null)
{
if(CustomSortComparer != null)
{
return
OwningGrid.DataConnection.SortDescriptions
.OfType<DataGridComparerSortDesctiption>()
.FirstOrDefault(s => s.SourceComparer == CustomSortComparer);
}
string propertyName = GetSortPropertyName();
return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName);

6
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -274,6 +274,12 @@ namespace Avalonia.Controls
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
}
else if (OwningColumn.CustomSortComparer != null)
{
newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer);
owningGrid.DataConnection.SortDescriptions.Add(newSort);
}
else
{
string propertyName = OwningColumn.GetSortPropertyName();

4
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -163,6 +163,10 @@
<Style Selector="DataGridCell:invalid /template/ Rectangle#InvalidVisualElement">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="DataGridCell > TextBox DataValidationErrors">
<Setter Property="Template" Value="{DynamicResource TooltipDataValidationContentTemplate}" />
<Setter Property="ErrorTemplate" Value="{DynamicResource TooltipDataValidationErrorTemplate}" />
</Style>
<Style Selector="DataGridColumnHeader">
<Setter Property="Foreground" Value="{DynamicResource DataGridColumnHeaderForegroundBrush}" />

9
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,7 +1,14 @@
Compat issues with assembly Avalonia.Controls:
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseClosed()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public 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.
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.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 5
Total Issues: 12

2
src/Avalonia.Controls/AutoCompleteBox.cs

@ -31,7 +31,6 @@ namespace Avalonia.Controls
/// <see cref="E:Avalonia.Controls.AutoCompleteBox.Populated" />
/// event.
/// </summary>
[PseudoClasses(":dropdownopen")]
public class PopulatedEventArgs : EventArgs
{
/// <summary>
@ -253,6 +252,7 @@ namespace Avalonia.Controls
/// drop-down that contains possible matches based on the input in the text
/// box.
/// </summary>
[PseudoClasses(":dropdownopen")]
public class AutoCompleteBox : TemplatedControl
{
/// <summary>

52
src/Avalonia.Controls/Button.cs

@ -4,6 +4,7 @@ using System.Windows.Input;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Platform;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
@ -80,6 +81,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsPressedProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
/// <summary>
/// Defines the <see cref="Flyout"/> property
/// </summary>
public static readonly StyledProperty<FlyoutBase> FlyoutProperty =
AvaloniaProperty.Register<Button, FlyoutBase>(nameof(Flyout));
private ICommand _command;
private bool _commandCanExecute = true;
private KeyGesture _hotkey;
@ -91,6 +98,7 @@ namespace Avalonia.Controls
{
FocusableProperty.OverrideDefaultValue(typeof(Button), true);
CommandProperty.Changed.Subscribe(CommandChanged);
CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
IsCancelProperty.Changed.Subscribe(IsCancelChanged);
}
@ -171,6 +179,15 @@ namespace Avalonia.Controls
private set { SetValue(IsPressedProperty, value); }
}
/// <summary>
/// Gets or sets the Flyout that should be shown with this button
/// </summary>
public FlyoutBase Flyout
{
get => GetValue(FlyoutProperty);
set => SetValue(FlyoutProperty, value);
}
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
/// <inheritdoc/>
@ -220,6 +237,7 @@ namespace Avalonia.Controls
if (Command != null)
{
Command.CanExecuteChanged += CanExecuteChanged;
CanExecuteChanged(this, EventArgs.Empty);
}
}
@ -257,6 +275,11 @@ namespace Avalonia.Controls
IsPressed = true;
e.Handled = true;
}
else if (e.Key == Key.Escape && Flyout != null)
{
// If Flyout doesn't have focusable content, close the flyout here
Flyout.Hide();
}
base.OnKeyDown(e);
}
@ -280,6 +303,8 @@ namespace Avalonia.Controls
/// </summary>
protected virtual void OnClick()
{
OpenFlyout();
var e = new RoutedEventArgs(ClickEvent);
RaiseEvent(e);
@ -290,6 +315,11 @@ namespace Avalonia.Controls
}
}
protected virtual void OpenFlyout()
{
Flyout?.ShowAt(this);
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
@ -338,6 +368,16 @@ namespace Avalonia.Controls
{
UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
}
else if (change.Property == FlyoutProperty)
{
// 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)
{
oldFlyout.Hide();
}
}
}
protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
@ -388,6 +428,18 @@ namespace Avalonia.Controls
}
}
/// <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)
{
button.CanExecuteChanged(button, EventArgs.Empty);
}
}
/// <summary>
/// Called when the <see cref="IsDefault"/> property changes.
/// </summary>

73
src/Avalonia.Controls/Chrome/CaptionButtons.cs

@ -14,17 +14,21 @@ namespace Avalonia.Controls.Chrome
public class CaptionButtons : TemplatedControl
{
private CompositeDisposable? _disposables;
private Window? _hostWindow;
public void Attach(Window hostWindow)
/// <summary>
/// Currently attached window.
/// </summary>
protected Window? HostWindow { get; private set; }
public virtual void Attach(Window hostWindow)
{
if (_disposables == null)
{
_hostWindow = hostWindow;
HostWindow = hostWindow;
_disposables = new CompositeDisposable
{
_hostWindow.GetObservable(Window.WindowStateProperty)
HostWindow.GetObservable(Window.WindowStateProperty)
.Subscribe(x =>
{
PseudoClasses.Set(":minimized", x == WindowState.Minimized);
@ -36,14 +40,45 @@ namespace Avalonia.Controls.Chrome
}
}
public void Detach()
public virtual void Detach()
{
if (_disposables != null)
{
_disposables.Dispose();
_disposables = null;
_hostWindow = null;
HostWindow = null;
}
}
protected virtual void OnClose()
{
HostWindow?.Close();
}
protected virtual void OnRestore()
{
if (HostWindow != null)
{
HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
}
protected virtual void OnMinimize()
{
if (HostWindow != null)
{
HostWindow.WindowState = WindowState.Minimized;
}
}
protected virtual void OnToggleFullScreen()
{
if (HostWindow != null)
{
HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen
? WindowState.Normal
: WindowState.FullScreen;
}
}
@ -56,31 +91,13 @@ namespace Avalonia.Controls.Chrome
var minimiseButton = e.NameScope.Get<Panel>("PART_MinimiseButton");
var fullScreenButton = e.NameScope.Get<Panel>("PART_FullScreenButton");
closeButton.PointerReleased += (sender, e) => _hostWindow?.Close();
closeButton.PointerReleased += (sender, e) => OnClose();
restoreButton.PointerReleased += (sender, e) =>
{
if (_hostWindow != null)
{
_hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
}
};
restoreButton.PointerReleased += (sender, e) => OnRestore();
minimiseButton.PointerReleased += (sender, e) =>
{
if (_hostWindow != null)
{
_hostWindow.WindowState = WindowState.Minimized;
}
};
minimiseButton.PointerReleased += (sender, e) => OnMinimize();
fullScreenButton.PointerReleased += (sender, e) =>
{
if (_hostWindow != null)
{
_hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen;
}
};
fullScreenButton.PointerReleased += (sender, e) => OnToggleFullScreen();
}
}
}

2
src/Avalonia.Controls/ComboBox.cs

@ -208,7 +208,7 @@ namespace Avalonia.Controls
return;
if (e.Key == Key.F4 ||
((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasFlagCustom(KeyModifiers.Alt)))
((e.Key == Key.Down || e.Key == Key.Up) && e.KeyModifiers.HasAllFlags(KeyModifiers.Alt)))
{
IsDropDownOpen = !IsDropDownOpen;
e.Handled = true;

22
src/Avalonia.Controls/Control.cs

@ -42,6 +42,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
/// <summary>
/// Defines the <see cref="ContextFlyout"/> property
/// </summary>
public static readonly StyledProperty<FlyoutBase?> ContextFlyoutProperty =
AvaloniaProperty.Register<Control, FlyoutBase?>(nameof(ContextFlyout));
/// <summary>
/// Event raised when an element wishes to be scrolled into view.
/// </summary>
@ -79,6 +85,15 @@ namespace Avalonia.Controls
set => SetValue(ContextMenuProperty, value);
}
/// <summary>
/// Gets or sets a context flyout to the control
/// </summary>
public FlyoutBase? ContextFlyout
{
get => GetValue(ContextFlyoutProperty);
set => SetValue(ContextFlyoutProperty, value);
}
/// <summary>
/// Gets or sets a user-defined object attached to the control.
/// </summary>
@ -96,6 +111,11 @@ namespace Avalonia.Controls
/// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter)
{
if (setter is Setter s && s.Property == ContextFlyoutProperty)
{
return; // Allow ContextFlyout to not need wrapping in <Template>
}
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
@ -184,7 +204,7 @@ namespace Avalonia.Controls
{
base.OnLostFocus(e);
if (_focusAdorner != null)
if (_focusAdorner?.Parent != null)
{
var adornerLayer = (IPanel)_focusAdorner.Parent;
adornerLayer.Children.Remove(_focusAdorner);

44
src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs

@ -0,0 +1,44 @@
#nullable enable
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts an existing CornerRadius struct to a new CornerRadius struct,
/// with filters applied to extract only the specified fields, leaving the others set to 0.
/// </summary>
public class CornerRadiusFilterConverter : IValueConverter
{
/// <summary>
/// Gets or sets the type of the filter applied to the <see cref="CornerRadiusFilterConverter"/>.
/// </summary>
public CornerRadiusFilterKinds Filter { get; set; }
/// <summary>
/// Gets or sets the scale multiplier applied to the <see cref="CornerRadiusFilterConverter"/>.
/// </summary>
public double Scale { get; set; } = 1;
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (!(value is CornerRadius radius))
{
return value;
}
return new CornerRadius(
Filter.HasAllFlags(CornerRadiusFilterKinds.TopLeft) ? radius.TopLeft * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.TopRight) ? radius.TopRight * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.BottomRight) ? radius.BottomRight * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.BottomLeft) ? radius.BottomLeft * Scale : 0);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

32
src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs

@ -0,0 +1,32 @@
using System;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Defines constants that specify the filter type for a <see cref="CornerRadiusFilterConverter"/> instance.
/// </summary>
[Flags]
public enum CornerRadiusFilterKinds
{
/// <summary>
/// No filter applied.
/// </summary>
None,
/// <summary>
/// Filters TopLeft value.
/// </summary>
TopLeft = 1,
/// <summary>
/// Filters TopRight value.
/// </summary>
TopRight = 2,
/// <summary>
/// Filters BottomLeft value.
/// </summary>
BottomLeft = 4,
/// <summary>
/// Filters BottomRight value.
/// </summary>
BottomRight = 8
}
}

16
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@ -72,24 +72,24 @@ namespace Avalonia.Controls.Converters
}
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control))
{
s.Append("Ctrl");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
{
Plus(s);
s.Append("Shift");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
{
Plus(s);
s.Append("Alt");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
{
Plus(s);
s.Append(meta);
@ -105,22 +105,22 @@ namespace Avalonia.Controls.Converters
{
var s = new StringBuilder();
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control))
{
s.Append('⌃');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
{
s.Append('⌥');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
{
s.Append('⇧');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
{
s.Append('⌘');
}

2
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -8,7 +8,7 @@ using System.Globalization;
namespace Avalonia.Controls
{
/// <summary>
/// A control to allow the user to select a time
/// A control to allow the user to select a time.
/// </summary>
[PseudoClasses(":hasnotime")]
public class TimePicker : TemplatedControl

21
src/Avalonia.Controls/Expander.cs

@ -4,14 +4,35 @@ using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
/// <summary>
/// Direction in which an <see cref="Expander"/> control opens.
/// </summary>
public enum ExpandDirection
{
/// <summary>
/// Opens down.
/// </summary>
Down,
/// <summary>
/// Opens up.
/// </summary>
Up,
/// <summary>
/// Opens left.
/// </summary>
Left,
/// <summary>
/// Opens right.
/// </summary>
Right
}
/// <summary>
/// A control with a header that has a collapsible content section.
/// </summary>
[PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
public class Expander : HeaderedContentControl
{

50
src/Avalonia.Controls/Flyouts/Flyout.cs

@ -0,0 +1,50 @@
using Avalonia.Controls.Primitives;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Controls
{
public class Flyout : FlyoutBase
{
/// <summary>
/// Defines the <see cref="Content"/> property
/// </summary>
public static readonly StyledProperty<object> ContentProperty =
AvaloniaProperty.Register<Flyout, object>(nameof(Content));
/// <summary>
/// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting
/// </summary>
public Classes FlyoutPresenterClasses => _classes ??= new Classes();
private Classes? _classes;
/// <summary>
/// Gets or sets the content to display in this flyout
/// </summary>
[Content]
public object Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
protected override Control CreatePresenter()
{
return new FlyoutPresenter
{
[!ContentControl.ContentProperty] = this[!ContentProperty]
};
}
protected override void OnOpened()
{
if (_classes != null)
{
SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
}
base.OnOpened();
}
}
}

504
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -0,0 +1,504 @@
using System;
using System.ComponentModel;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
#nullable enable
namespace Avalonia.Controls.Primitives
{
public abstract class FlyoutBase : AvaloniaObject
{
static FlyoutBase()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
/// <summary>
/// Defines the <see cref="IsOpen"/> property
/// </summary>
private static readonly DirectProperty<FlyoutBase, bool> IsOpenProperty =
AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen),
x => x.IsOpen);
/// <summary>
/// Defines the <see cref="Target"/> property
/// </summary>
public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty =
AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target);
/// <summary>
/// Defines the <see cref="Placement"/> property
/// </summary>
public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
/// <summary>
/// Defines the <see cref="ShowMode"/> property
/// </summary>
public static readonly DirectProperty<FlyoutBase, FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutShowMode>(nameof(ShowMode),
x => x.ShowMode, (x, v) => x.ShowMode = v);
/// <summary>
/// Defines the AttachedFlyout property
/// </summary>
public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
private bool _isOpen;
private Control? _target;
private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
private Rect? enlargedPopupRect;
private IDisposable? transientDisposable;
protected Popup? Popup { get; private set; }
/// <summary>
/// Gets whether this Flyout is currently Open
/// </summary>
public bool IsOpen
{
get => _isOpen;
private set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
}
/// <summary>
/// Gets or sets the desired placement
/// </summary>
public FlyoutPlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets the desired ShowMode
/// </summary>
public FlyoutShowMode ShowMode
{
get => _showMode;
set => SetAndRaise(ShowModeProperty, ref _showMode, value);
}
/// <summary>
/// Gets the Target used for showing the Flyout
/// </summary>
public Control? Target
{
get => _target;
private set => SetAndRaise(TargetProperty, ref _target, value);
}
public event EventHandler? Closed;
public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opened;
public event EventHandler? Opening;
public static FlyoutBase? GetAttachedFlyout(Control element)
{
return element.GetValue(AttachedFlyoutProperty);
}
public static void SetAttachedFlyout(Control element, FlyoutBase? value)
{
element.SetValue(AttachedFlyoutProperty, value);
}
public static void ShowAttachedFlyout(Control flyoutOwner)
{
var flyout = GetAttachedFlyout(flyoutOwner);
flyout?.ShowAt(flyoutOwner);
}
/// <summary>
/// Shows the Flyout at the given Control
/// </summary>
/// <param name="placementTarget">The control to show the Flyout at</param>
public void ShowAt(Control placementTarget)
{
ShowAtCore(placementTarget);
}
/// <summary>
/// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
/// </summary>
/// <param name="placementTarget">The target control</param>
/// <param name="showAtPointer">True to show at pointer</param>
public void ShowAt(Control placementTarget, bool showAtPointer)
{
ShowAtCore(placementTarget, showAtPointer);
}
/// <summary>
/// Hides the Flyout
/// </summary>
public void Hide()
{
HideCore();
}
protected virtual void HideCore(bool canCancel = true)
{
if (!IsOpen)
{
return;
}
if (canCancel)
{
bool cancel = false;
var closing = new CancelEventArgs();
Closing?.Invoke(this, closing);
if (cancel || closing.Cancel)
{
return;
}
}
IsOpen = false;
Popup.IsOpen = false;
// Ensure this isn't active
transientDisposable?.Dispose();
transientDisposable = null;
OnClosed();
}
protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false)
{
if (placementTarget == null)
throw new ArgumentNullException("placementTarget cannot be null");
if (Popup == null)
{
InitPopup();
}
if (IsOpen)
{
if (placementTarget == Target)
{
return;
}
else // Close before opening a new one
{
HideCore(false);
}
}
if (Popup.Parent != null && Popup.Parent != placementTarget)
{
((ISetLogicalParent)Popup).SetParent(null);
}
if (Popup.PlacementTarget != placementTarget)
{
Popup.PlacementTarget = Target = placementTarget;
((ISetLogicalParent)Popup).SetParent(placementTarget);
}
if (Popup.Child == null)
{
Popup.Child = CreatePresenter();
}
OnOpening();
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
OnOpened();
if (ShowMode == FlyoutShowMode.Standard)
{
// Try and focus content inside Flyout
if (Popup.Child.Focusable)
{
FocusManager.Instance?.Focus(Popup.Child);
}
else
{
var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
if (nextFocus != null)
{
FocusManager.Instance?.Focus(nextFocus);
}
}
}
else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
{
transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
}
}
private void HandleTransientDismiss(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
{
// In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
// shown as long as the pointer is within a certain px distance from the
// flyout itself. I'm not sure what WinUI uses, but I'm defaulting to
// 100px, which seems about right
// enlargedPopupRect is the Flyout bounds enlarged 100px
// For windowed popups, enlargedPopupRect is in screen coordinates,
// for overlay popups, its in OverlayLayer coordinates
if (enlargedPopupRect == null)
{
// Only do this once when the Flyout opens & cache the result
if (Popup?.Host is PopupRoot root)
{
// Get the popup root bounds and convert to screen coordinates
var tmp = root.Bounds.Inflate(100);
var scPt = root.PointToScreen(tmp.TopLeft);
enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height);
}
else if (Popup?.Host is OverlayPopupHost host)
{
// Overlay popups are in OverlayLayer coordinates, just use that
enlargedPopupRect = host.Bounds.Inflate(100);
}
return;
}
if (Popup?.Host is PopupRoot)
{
// As long as the pointer stays within the enlargedPopupRect
// the flyout stays open. If it leaves, close it
// Despite working in screen coordinates, leaving the TopLevel
// window will not close this (as pointer events stop), which
// does match UWP
var pt = pArgs.Root.PointToScreen(pArgs.Position);
if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false)
{
HideCore(false);
enlargedPopupRect = null;
transientDisposable?.Dispose();
transientDisposable = null;
}
}
else if (Popup?.Host is OverlayPopupHost)
{
// Same as above here, but just different coordinate space
// so we don't need to translate
if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false)
{
HideCore(false);
enlargedPopupRect = null;
transientDisposable?.Dispose();
transientDisposable = null;
}
}
}
}
protected virtual void OnOpening()
{
Opening?.Invoke(this, null);
}
protected virtual void OnOpened()
{
Opened?.Invoke(this, null);
}
protected virtual void OnClosing(CancelEventArgs args)
{
Closing?.Invoke(this, args);
}
protected virtual void OnClosed()
{
Closed?.Invoke(this, null);
}
/// <summary>
/// Used to create the content the Flyout displays
/// </summary>
/// <returns></returns>
protected abstract Control CreatePresenter();
private void InitPopup()
{
Popup = new Popup();
Popup.WindowManagerAddShadowHint = false;
Popup.IsLightDismissEnabled = true;
Popup.Opened += OnPopupOpened;
Popup.Closed += OnPopupClosed;
}
private void OnPopupOpened(object sender, EventArgs e)
{
IsOpen = true;
}
private void OnPopupClosed(object sender, EventArgs e)
{
HideCore();
}
private void PositionPopup(bool showAtPointer)
{
Size sz;
if(Popup.Child.DesiredSize == Size.Empty)
{
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
}
else
{
sz = Popup.Child.DesiredSize;
}
if (showAtPointer)
{
Popup.PlacementMode = PlacementMode.Pointer;
}
else
{
Popup.PlacementMode = PlacementMode.AnchorAndGravity;
Popup.PlacementConstraintAdjustment =
PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
}
var trgtBnds = Target?.Bounds ?? Rect.Empty;
switch (Placement)
{
case FlyoutPlacementMode.Top: //Above & centered
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
break;
case FlyoutPlacementMode.TopEdgeAlignedLeft:
Popup.PlacementRect = new Rect(0, 0, 0, 0);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
break;
case FlyoutPlacementMode.TopEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
break;
case FlyoutPlacementMode.RightEdgeAlignedTop:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.Right: //Right & centered
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, trgtBnds.Height);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Right;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.RightEdgeAlignedBottom:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.Bottom: //Below & centered
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, trgtBnds.Width, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Bottom;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.BottomEdgeAlignedLeft:
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.BottomEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.LeftEdgeAlignedTop:
Popup.PlacementRect = new Rect(0, 0, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
break;
case FlyoutPlacementMode.Left: //Left & centered
Popup.PlacementRect = new Rect(0, 0, 1, trgtBnds.Height);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Left;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
break;
case FlyoutPlacementMode.LeftEdgeAlignedBottom:
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.BottomLeft;
break;
//includes Auto (not sure what determines that)...
default:
//This is just FlyoutPlacementMode.Top behavior (above & centered)
Popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
break;
}
}
private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
if (args.Sender is Control c)
{
if (args.OldValue is FlyoutBase)
{
c.PointerReleased -= OnControlWithContextFlyoutPointerReleased;
}
if (args.NewValue is FlyoutBase)
{
c.PointerReleased += OnControlWithContextFlyoutPointerReleased;
}
}
}
private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e)
{
if (sender is Control c)
{
if (e.InitialPressMouseButton == MouseButton.Right &&
e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased)
{
if (c.ContextFlyout != null)
{
if (c.ContextMenu != null)
{
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(c, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
c.ContextFlyout.ShowAt(c, true);
}
}
}
}
internal static void SetPresenterClasses(IControl presenter, Classes classes)
{
//Remove any classes no longer in use, ignoring pseudoclasses
for (int i = presenter.Classes.Count - 1; i >= 0; i--)
{
if (!classes.Contains(presenter.Classes[i]) &&
!presenter.Classes[i].Contains(":"))
{
presenter.Classes.RemoveAt(i);
}
}
//Add new classes
presenter.Classes.AddRange(classes);
}
}
}

77
src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs

@ -0,0 +1,77 @@
namespace Avalonia.Controls
{
public enum FlyoutPlacementMode
{
/// <summary>
/// Preferred location is above the target element
/// </summary>
Top = 0,
/// <summary>
/// Preferred location is below the target element
/// </summary>
Bottom = 1,
/// <summary>
/// Preferred location is to the left of the target element
/// </summary>
Left = 2,
/// <summary>
/// Preferred location is to the right of the target element
/// </summary>
Right = 3,
//TODO
// <summary>
// Preferred location is centered on the screen
// </summary>
//Full = 4,
/// <summary>
/// Preferred location is above the target element, with the left edge of the flyout
/// aligned with the left edge of the target element
/// </summary>
TopEdgeAlignedLeft = 5,
/// <summary>
/// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element.
/// </summary>
TopEdgeAlignedRight = 6,
/// <summary>
/// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element.
/// </summary>
BottomEdgeAlignedLeft = 7,
/// <summary>
/// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element.
/// </summary>
BottomEdgeAlignedRight = 8,
/// <summary>
/// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element.
/// </summary>
LeftEdgeAlignedTop = 9,
/// <summary>
/// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
/// </summary>
LeftEdgeAlignedBottom = 10,
/// <summary>
/// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element.
/// </summary>
RightEdgeAlignedTop = 11,
/// <summary>
/// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
/// </summary>
RightEdgeAlignedBottom = 12,
/// <summary>
/// Preferred location is determined automatically.
/// </summary>
Auto = 13
}
}

33
src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs

@ -0,0 +1,33 @@
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
{
public class FlyoutPresenter : ContentControl
{
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
public CornerRadius CornerRadius
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
var host = this.FindLogicalAncestorOfType<Popup>();
if (host != null)
{
host.IsOpen = false;
e.Handled = true;
}
}
base.OnKeyDown(e);
}
}
}

24
src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs

@ -0,0 +1,24 @@
namespace Avalonia.Controls
{
// Note: FlyoutShowMode.Auto was removed. MS Docs just say:
// The show mode is determined automatically based on the method used to show the flyout.
// and AFAICT Flyouts generally open with "Standard" behavior
public enum FlyoutShowMode
{
/// <summary>
/// Behavior is typical of a flyout shown reactively, like a context menu. The open flyout takes focus. For a CommandBarFlyout, it opens in it's expanded state.
/// </summary>
Standard,
/// <summary>
/// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state.
/// </summary>
Transient,
/// <summary>
/// The flyout exhibits Transient behavior while the cursor is close to it, but is dismissed when the cursor moves away.
/// </summary>
TransientWithDismissOnPointerMoveAway
}
}

75
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -0,0 +1,75 @@
using System.Collections;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Controls
{
public class MenuFlyout : FlyoutBase
{
public MenuFlyout()
{
_items = new AvaloniaList<object>();
}
/// <summary>
/// Defines the <see cref="Items"/> property
/// </summary>
public static readonly DirectProperty<MenuFlyout, IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items,
(x, v) => x.Items = v);
/// <summary>
/// Defines the <see cref="ItemTemplate"/> property
/// </summary>
public static readonly DirectProperty<MenuFlyout, IDataTemplate?> ItemTemplateProperty =
AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate),
x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
public Classes FlyoutPresenterClasses => _classes ??= new Classes();
/// <summary>
/// Gets or sets the items of the MenuFlyout
/// </summary>
[Content]
public IEnumerable Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
}
/// <summary>
/// Gets or sets the template used for the items
/// </summary>
public IDataTemplate? ItemTemplate
{
get => _itemTemplate;
set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
}
private Classes? _classes;
private IEnumerable _items;
private IDataTemplate? _itemTemplate;
protected override Control CreatePresenter()
{
return new MenuFlyoutPresenter
{
[!ItemsControl.ItemsProperty] = this[!ItemsProperty],
[!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty]
};
}
protected override void OnOpened()
{
if (_classes != null)
{
SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
}
base.OnOpened();
}
}
}

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

@ -0,0 +1,55 @@
using System;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
{
public class MenuFlyoutPresenter : MenuBase
{
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
public CornerRadius CornerRadius
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public MenuFlyoutPresenter()
:base(new DefaultMenuInteractionHandler(true))
{
}
public override void Close()
{
// DefaultMenuInteractionHandler calls this
var host = this.FindLogicalAncestorOfType<Popup>();
if (host != null)
{
for (int i = 0; i < LogicalChildren.Count; i++)
{
if (LogicalChildren[i] is MenuItem item)
{
item.IsSubMenuOpen = false;
}
}
SelectedIndex = -1;
host.IsOpen = false;
}
}
public override void Open()
{
throw new NotSupportedException("Use MenuFlyout.ShowAt(Control) instead");
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
}
}

12
src/Avalonia.Controls/Grid.cs

@ -17,7 +17,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// Grid
/// Defines a flexible grid area that consists of columns and rows.
/// </summary>
public class Grid : Panel
{
@ -2355,7 +2355,7 @@ namespace Avalonia.Controls
/// </summary>
private bool CheckFlags(Flags flags)
{
return _flags.HasFlagCustom(flags);
return _flags.HasAllFlags(flags);
}
private static void OnShowGridLinesPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
@ -2790,10 +2790,10 @@ namespace Avalonia.Controls
internal LayoutTimeSizeType SizeTypeU;
internal LayoutTimeSizeType SizeTypeV;
internal int Next;
internal bool IsStarU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Star);
internal bool IsAutoU => SizeTypeU.HasFlagCustom(LayoutTimeSizeType.Auto);
internal bool IsStarV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Star);
internal bool IsAutoV => SizeTypeV.HasFlagCustom(LayoutTimeSizeType.Auto);
internal bool IsStarU => SizeTypeU.HasAllFlags(LayoutTimeSizeType.Star);
internal bool IsAutoU => SizeTypeU.HasAllFlags(LayoutTimeSizeType.Auto);
internal bool IsStarV => SizeTypeV.HasAllFlags(LayoutTimeSizeType.Star);
internal bool IsAutoV => SizeTypeV.HasAllFlags(LayoutTimeSizeType.Auto);
}
/// <summary>

22
src/Avalonia.Controls/GridSplitter.cs

@ -609,10 +609,20 @@ namespace Avalonia.Controls
private void MoveSplitter(double horizontalChange, double verticalChange)
{
Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter");
// Calculate the offset to adjust the splitter.
var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange;
// Calculate the offset to adjust the splitter. If layout rounding is enabled, we
// need to round to an integer physical pixel value to avoid round-ups of children that
// expand the bounds of the Grid. In practice this only happens in high dpi because
// horizontal/vertical offsets here are never fractional (they correspond to mouse movement
// across logical pixels). Rounding error only creeps in when converting to a physical
// display with something other than the logical 96 dpi.
double delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange;
if (UseLayoutRounding)
{
delta = LayoutHelper.RoundLayoutValue(delta, LayoutHelper.GetLayoutScale(this));
}
DefinitionBase definition1 = _resizeData.Definition1;
DefinitionBase definition2 = _resizeData.Definition2;
@ -622,11 +632,11 @@ namespace Avalonia.Controls
double actualLength2 = GetActualLength(definition2);
// When splitting, Check to see if the total pixels spanned by the definitions
// is the same asbefore starting resize. If not cancel the drag
// is the same as before starting resize. If not cancel the drag.
if (_resizeData.SplitBehavior == SplitBehavior.Split &&
!MathUtilities.AreClose(
actualLength1 + actualLength2,
_resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength))
_resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, LayoutHelper.LayoutEpsilon))
{
CancelResize();

4
src/Avalonia.Controls/IControl.cs

@ -3,6 +3,8 @@ using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -15,6 +17,6 @@ namespace Avalonia.Controls
INamed,
IStyledElement
{
new IControl Parent { get; }
new IControl? Parent { get; }
}
}

6
src/Avalonia.Controls/IMenuItem.cs

@ -23,6 +23,12 @@ namespace Avalonia.Controls
/// </summary>
bool IsSubMenuOpen { get; set; }
/// <summary>
/// Gets or sets a value that indicates the submenu that this <see cref="MenuItem"/> is
/// within should not close when this item is clicked.
/// </summary>
bool StaysOpenOnClick { get; set; }
/// <summary>
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level main menu item.
/// </summary>

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

Loading…
Cancel
Save