diff --git a/.editorconfig b/.editorconfig
index f6bce9cb767..c7a381b730b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -156,6 +156,9 @@ indent_size = 2
[*.{props,targets,config,nuspec}]
indent_size = 2
+[*.json]
+indent_size = 2
+
# Shell scripts
[*.sh]
end_of_line = lf
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000000..4e34d4b1325
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,32 @@
+---
+name: Bug report
+about: Create a report to help us improve Avalonia
+title: ''
+labels: bug
+assignees: ''
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+
+- OS: [e.g. Windows, Mac, Linux (State distribution)]
+- Version [e.g. 0.10.0-rc1 or 0.9.12]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000000..687355d825e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Questions, Discussions, Ideas
+ url: https://github.com/AvaloniaUI/Avalonia/discussions/new
+ about: Please ask and answer questions here.
+ - name: Avalonia Community Support on Gitter
+ url: https://gitter.im/AvaloniaUI/Avalonia
+ about: Please ask and answer questions here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000000..5f0a04cee3a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,19 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index acff8cc117e..46e8665945a 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.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
+## Obsoletions / Deprecations
+
## Fixed issues
+ false
diff --git a/Documentation/build.md b/Documentation/build.md
index 8c2ef57b549..a7d68eb5990 100644
--- a/Documentation/build.md
+++ b/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
@@ -30,7 +44,7 @@ The build process needs [Xcode](https://developer.apple.com/xcode/) to build the
Linux operating systems ship with their own respective package managers however we will use [Homebrew](https://brew.sh/) to manage packages on macOS. To install follow the instructions [here](https://docs.brew.sh/Installation).
-### Install CastXML
+### Install CastXML (pre Nov 2020)
Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processing during the build process. The easiest way to install this is via the operating system's package managers, such as below.
@@ -60,14 +74,10 @@ git submodule update --init --recursive
### Build native libraries (macOS only)
-On macOS it is necessary to build and manually install the respective native libraries using [Xcode](https://developer.apple.com/xcode/). The steps to get this working correctly are:
-- Navigate to the Avalonia/native/Avalonia.Native/src/OSX folder and open the `Avalonia.Native.OSX.xcodeproj` project
-- Build the library via the Product->Build menu. This will generate binaries in your local path under ~/Library/Developer/Xcode/DerivedData/Avalonia.Native.OSX-*guid* where "guid" is uniquely generated every time you build.
-- Manually install the native library by copying it from the build artifacts folder into the shared dynamic library path:
+On macOS it is necessary to build and manually install the respective native libraries using [Xcode](https://developer.apple.com/xcode/). Execute the build script in the root project with the `CompileNative` task. It will build the headers, build the libraries, and place them in the appropriate place to allow .NET to find them at compilation and run time.
-```
-cd ~/Library/Developer/Xcode/DerivedData/Avalonia.Native.OSX-[guid]/Build/Products/Debug
-cp libAvalonia.Native.OSX.dylib /usr/local/lib/libAvaloniaNative.dylib
+```bash
+./build.sh CompileNative
```
### Build and Run Avalonia
diff --git a/NuGet.Config b/NuGet.Config
index 3abd236d420..7a1f28bea71 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -4,6 +4,6 @@
-
+
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 721a0415f48..fbd85071931 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -3,13 +3,6 @@ jobs:
pool:
vmImage: 'ubuntu-16.04'
steps:
- - task: CmdLine@2
- displayName: 'Install CastXML'
- inputs:
- script: |
- sudo apt-get update
- sudo apt-get install castxml
-
- task: CmdLine@2
displayName: 'Install Nuke'
inputs:
@@ -31,8 +24,10 @@ jobs:
condition: not(canceled())
- job: macOS
+ variables:
+ SolutionDir: '$(Build.SourcesDirectory)'
pool:
- vmImage: 'macOS-10.14'
+ vmImage: 'macOS-10.15'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
@@ -46,14 +41,20 @@ jobs:
curl -o ./mono.pkg https://download.mono-project.com/archive/5.18.0/macos-10-universal/MonoFramework-MDK-5.18.0.225.macos10.xamarin.universal.pkg
sudo installer -verbose -pkg ./mono.pkg -target /
+ - task: CmdLine@2
+ displayName: 'Generate avalonia-native'
+ inputs:
+ script: |
+ cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h
+
- task: Xcode@5
inputs:
actions: 'build'
scheme: ''
- sdk: 'macosx10.14'
+ sdk: 'macosx11.1'
configuration: 'Release'
xcWorkspacePath: '**/*.xcodeproj/project.xcworkspace'
- xcodeVersion: '10' # Options: 8, 9, default, specifyPath
+ xcodeVersion: '12' # Options: 8, 9, default, specifyPath
args: '-derivedDataPath ./'
- task: CmdLine@2
@@ -97,6 +98,8 @@ jobs:
- job: Windows
pool:
vmImage: 'windows-2019'
+ variables:
+ SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.1.401'
diff --git a/build.ps1 b/build.ps1
index 57e2f800758..985e8abcee9 100644
--- a/build.ps1
+++ b/build.ps1
@@ -43,7 +43,7 @@ if (Test-Path $DotNetGlobalFile) {
}
# If dotnet is installed locally, and expected version is not set or installation matches the expected version
-if ((Get-Command "dotnet" -ErrorAction SilentlyContinue) -ne $null -and `
+if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
(!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) {
$env:DOTNET_EXE = (Get-Command "dotnet").Path
}
@@ -53,7 +53,7 @@ else {
# Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
- md -force $TempDirectory > $null
+ mkdir -force $TempDirectory > $null
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
# Install by channel or version
@@ -62,6 +62,8 @@ else {
} else {
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
+
+ $env:PATH="$DotNetDirectory;$env:PATH"
}
Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
diff --git a/build.sh b/build.sh
index a40e00f815b..bd162fab9bd 100755
--- a/build.sh
+++ b/build.sh
@@ -47,7 +47,7 @@ if [ -f "$DOTNET_GLOBAL_FILE" ]; then
fi
# If dotnet is installed locally, and expected version is not set or installation matches the expected version
-if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then
+if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") || "$SKIP_DOTNET_DOWNLOAD" == "1" ]]; then
export DOTNET_EXE="$(command -v dotnet)"
else
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props
index 67947296b34..de86acc6dec 100644
--- a/build/AndroidWorkarounds.props
+++ b/build/AndroidWorkarounds.props
@@ -2,7 +2,7 @@
-
+
diff --git a/build/ApiDiff.props b/build/ApiDiff.props
index da82fbcc513..666417addfd 100644
--- a/build/ApiDiff.props
+++ b/build/ApiDiff.props
@@ -1,12 +1,12 @@
- 0.10.0-preview3
+ 0.10.0
$(PackageId)
Avalonia
-
+
diff --git a/build/Assets/Icon.png b/build/Assets/Icon.png
new file mode 100644
index 00000000000..41a2a618fb0
Binary files /dev/null and b/build/Assets/Icon.png differ
diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props
index d17eec01350..fff00041c3c 100644
--- a/build/CoreLibraries.props
+++ b/build/CoreLibraries.props
@@ -15,6 +15,7 @@
+
diff --git a/build/EmbedXaml.props b/build/EmbedXaml.props
index 7ce0366dea8..0bb8da4f47a 100644
--- a/build/EmbedXaml.props
+++ b/build/EmbedXaml.props
@@ -4,8 +4,9 @@
%(Filename)
+
Designer
-
\ No newline at end of file
+
diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index e636461ad99..13419eb173a 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/build/MicroCom.targets b/build/MicroCom.targets
new file mode 100644
index 00000000000..49d2cdce726
--- /dev/null
+++ b/build/MicroCom.targets
@@ -0,0 +1,34 @@
+
+
+
+
+
+ false
+ all
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_AvaloniaPatchComInterop>true
+
+
+
diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props
index d8e86e917e1..c3b136d41df 100644
--- a/build/ReactiveUI.props
+++ b/build/ReactiveUI.props
@@ -1,5 +1,5 @@
-
+
diff --git a/build/Rx.props b/build/Rx.props
index 8a15ccd6a9a..fde1f80ea1d 100644
--- a/build/Rx.props
+++ b/build/Rx.props
@@ -1,5 +1,5 @@
-
+
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index d3cebef4183..75bada4bfc9 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -3,17 +3,24 @@
Avalonia
0.10.999
- Copyright 2020 © The AvaloniaUI Project
+ Copyright 2021 © The AvaloniaUI Project
https://avaloniaui.net
https://github.com/AvaloniaUI/Avalonia/
true
CS1591
latest
MIT
- https://avatars2.githubusercontent.com/u/14075148?s=200
+ Icon.png
Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS.
avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin
https://github.com/AvaloniaUI/Avalonia/releases
git
+ $(MSBuildThisFileDirectory)\avalonia.snk
+ true
+ $(DefineConstants);SIGNED_BUILD
+
+
+
+
diff --git a/build/SourceLink.props b/build/SourceLink.props
index 0c9b6a34f82..1e007e01eb7 100644
--- a/build/SourceLink.props
+++ b/build/SourceLink.props
@@ -1,5 +1,26 @@
+
+ true
+ false
+ true
+ embedded
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+ true
+
+
+
+ true
+
+
-
+
-
\ No newline at end of file
+
+
+
+
+
+
diff --git a/build/XUnit.props b/build/XUnit.props
index 079565d184d..a75e1bac863 100644
--- a/build/XUnit.props
+++ b/build/XUnit.props
@@ -11,4 +11,8 @@
+
+ $(MSBuildThisFileDirectory)\avalonia.snk
+ False
+
diff --git a/build/avalonia.snk b/build/avalonia.snk
new file mode 100644
index 00000000000..10b49deb317
Binary files /dev/null and b/build/avalonia.snk differ
diff --git a/dirs.proj b/dirs.proj
index bf32abef723..594f2c22d3f 100644
--- a/dirs.proj
+++ b/dirs.proj
@@ -21,6 +21,7 @@
+
diff --git a/native/Avalonia.Native/inc/.gitignore b/native/Avalonia.Native/inc/.gitignore
new file mode 100644
index 00000000000..e7aa7fc6a50
--- /dev/null
+++ b/native/Avalonia.Native/inc/.gitignore
@@ -0,0 +1 @@
+avalonia-native.h
diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h
deleted file mode 100644
index 9ff6130e5fa..00000000000
--- a/native/Avalonia.Native/inc/avalonia-native.h
+++ /dev/null
@@ -1,516 +0,0 @@
-#include "com.h"
-#include "key.h"
-#include "stddef.h"
-
-#define AVNCOM(name, id) COMINTERFACE(name, 2e2cda0a, 9ae5, 4f1b, 8e, 20, 08, 1a, 04, 27, 9f, id)
-
-struct IAvnWindowEvents;
-struct IAvnWindow;
-struct IAvnPopup;
-struct IAvnMacOptions;
-struct IAvnPlatformThreadingInterface;
-struct IAvnSystemDialogEvents;
-struct IAvnSystemDialogs;
-struct IAvnScreens;
-struct IAvnClipboard;
-struct IAvnCursor;
-struct IAvnCursorFactory;
-struct IAvnGlFeature;
-struct IAvnGlContext;
-struct IAvnGlDisplay;
-struct IAvnGlSurfaceRenderTarget;
-struct IAvnGlSurfaceRenderingSession;
-struct IAvnMenu;
-struct IAvnMenuItem;
-struct IAvnStringArray;
-struct IAvnDndResultCallback;
-struct IAvnGCHandleDeallocatorCallback;
-struct IAvnMenuEvents;
-struct IAvnNativeControlHost;
-struct IAvnNativeControlHostTopLevelAttachment;
-enum SystemDecorations {
- SystemDecorationsNone = 0,
- SystemDecorationsBorderOnly = 1,
- SystemDecorationsFull = 2,
-};
-
-struct AvnSize
-{
- double Width, Height;
-};
-
-struct AvnPixelSize
-{
- int Width, Height;
-};
-
-struct AvnRect
-{
- double X, Y, Width, Height;
-};
-
-struct AvnVector
-{
- double X, Y;
-};
-
-struct AvnPoint
-{
- double X, Y;
-};
-
-struct AvnScreen
-{
- AvnRect Bounds;
- AvnRect WorkingArea;
- float PixelDensity;
- bool Primary;
-};
-
-enum AvnPixelFormat
-{
- kAvnRgb565,
- kAvnRgba8888,
- kAvnBgra8888
-};
-
-struct AvnFramebuffer
-{
- void* Data;
- int Width;
- int Height;
- int Stride;
- AvnVector Dpi;
- AvnPixelFormat PixelFormat;
-};
-
-struct AvnColor
-{
- unsigned char Alpha;
- unsigned char Red;
- unsigned char Green;
- unsigned char Blue;
-};
-
-enum AvnRawMouseEventType
-{
- LeaveWindow,
- LeftButtonDown,
- LeftButtonUp,
- RightButtonDown,
- RightButtonUp,
- MiddleButtonDown,
- MiddleButtonUp,
- XButton1Down,
- XButton1Up,
- XButton2Down,
- XButton2Up,
- Move,
- Wheel,
- NonClientLeftButtonDown,
- TouchBegin,
- TouchUpdate,
- TouchEnd,
- TouchCancel
-};
-
-enum AvnRawKeyEventType
-{
- KeyDown,
- KeyUp
-};
-
-enum AvnInputModifiers
-{
- AvnInputModifiersNone = 0,
- Alt = 1,
- Control = 2,
- Shift = 4,
- Windows = 8,
- LeftMouseButton = 16,
- RightMouseButton = 32,
- MiddleMouseButton = 64,
- XButton1MouseButton = 128,
- XButton2MouseButton = 256
-};
-
-enum class AvnDragDropEffects
-{
- None = 0,
- Copy = 1,
- Move = 2,
- Link = 4,
-};
-
-enum class AvnDragEventType
-{
- Enter,
- Over,
- Leave,
- Drop
-};
-
-enum AvnWindowState
-{
- Normal,
- Minimized,
- Maximized,
- FullScreen,
-};
-
-enum AvnStandardCursorType
-{
- CursorArrow,
- CursorIbeam,
- CursorWait,
- CursorCross,
- CursorUpArrow,
- CursorSizeWestEast,
- CursorSizeNorthSouth,
- CursorSizeAll,
- CursorNo,
- CursorHand,
- CursorAppStarting,
- CursorHelp,
- CursorTopSide,
- CursorBottomSize,
- CursorLeftSide,
- CursorRightSide,
- CursorTopLeftCorner,
- CursorTopRightCorner,
- CursorBottomLeftCorner,
- CursorBottomRightCorner,
- CursorDragMove,
- CursorDragCopy,
- CursorDragLink,
- CursorNone
-};
-
-enum AvnWindowEdge
-{
- WindowEdgeNorthWest,
- WindowEdgeNorth,
- WindowEdgeNorthEast,
- WindowEdgeWest,
- WindowEdgeEast,
- WindowEdgeSouthWest,
- WindowEdgeSouth,
- WindowEdgeSouthEast
-};
-
-enum AvnMenuItemToggleType
-{
- None,
- CheckMark,
- Radio
-};
-
-enum AvnExtendClientAreaChromeHints
-{
- AvnNoChrome = 0,
- AvnSystemChrome = 0x01,
- AvnPreferSystemChrome = 0x02,
- AvnOSXThickTitleBar = 0x08,
- AvnDefaultChrome = AvnSystemChrome,
-};
-
-AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
-{
-public:
- virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0;
- virtual IAvnMacOptions* GetMacOptions() = 0;
- virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0;
- virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0;
- virtual HRESULT CreatePlatformThreadingInterface(IAvnPlatformThreadingInterface** ppv) = 0;
- virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0;
- virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0;
- virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
- virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0;
- virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
- virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
- virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0;
- virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0;
- virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0;
- virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0;
-};
-
-AVNCOM(IAvnString, 17) : IUnknown
-{
- virtual HRESULT Pointer(void**retOut) = 0;
- virtual HRESULT Length(int*ret) = 0;
-};
-
-AVNCOM(IAvnWindowBase, 02) : IUnknown
-{
- virtual HRESULT Show() = 0;
- virtual HRESULT Hide () = 0;
- virtual HRESULT Close() = 0;
- virtual HRESULT Activate () = 0;
- virtual HRESULT GetClientSize(AvnSize*ret) = 0;
- virtual HRESULT GetScaling(double*ret)=0;
- virtual HRESULT SetMinMaxSize(AvnSize minSize, AvnSize maxSize) = 0;
- virtual HRESULT Resize(double width, double height) = 0;
- virtual HRESULT Invalidate (AvnRect rect) = 0;
- virtual HRESULT BeginMoveDrag () = 0;
- virtual HRESULT BeginResizeDrag (AvnWindowEdge edge) = 0;
- virtual HRESULT GetPosition (AvnPoint*ret) = 0;
- virtual HRESULT SetPosition (AvnPoint point) = 0;
- virtual HRESULT PointToClient (AvnPoint point, AvnPoint*ret) = 0;
- virtual HRESULT PointToScreen (AvnPoint point, AvnPoint*ret) = 0;
- virtual HRESULT ThreadSafeSetSwRenderedFrame(AvnFramebuffer* fb, IUnknown* dispose) = 0;
- virtual HRESULT SetTopMost (bool value) = 0;
- virtual HRESULT SetCursor(IAvnCursor* cursor) = 0;
- virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0;
- virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0;
- virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
- virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
- virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
- virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
- virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) = 0;
- virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
- IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0;
- virtual HRESULT SetBlurEnabled (bool enable) = 0;
-};
-
-AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
-{
-
-};
-
-AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
-{
- virtual HRESULT SetEnabled (bool enable) = 0;
- virtual HRESULT SetParent (IAvnWindow* parent) = 0;
- virtual HRESULT SetCanResize(bool value) = 0;
- virtual HRESULT SetDecorations(SystemDecorations value) = 0;
- virtual HRESULT SetTitle (void* utf8Title) = 0;
- virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
- virtual HRESULT SetWindowState(AvnWindowState state) = 0;
- virtual HRESULT GetWindowState(AvnWindowState*ret) = 0;
- virtual HRESULT TakeFocusFromChildren() = 0;
- virtual HRESULT SetExtendClientArea (bool enable) = 0;
- virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) = 0;
- virtual HRESULT GetExtendTitleBarHeight (double*ret) = 0;
- virtual HRESULT SetExtendTitleBarHeight (double value) = 0;
-};
-
-AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown
-{
- virtual HRESULT Paint() = 0;
- virtual void Closed() = 0;
- virtual void Activated() = 0;
- virtual void Deactivated() = 0;
- virtual void Resized(const AvnSize& size) = 0;
- virtual void PositionChanged (AvnPoint position) = 0;
- virtual void RawMouseEvent (AvnRawMouseEventType type,
- unsigned int timeStamp,
- AvnInputModifiers modifiers,
- AvnPoint point,
- AvnVector delta) = 0;
- virtual bool RawKeyEvent (AvnRawKeyEventType type, unsigned int timeStamp, AvnInputModifiers modifiers, unsigned int key) = 0;
- virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0;
- virtual void ScalingChanged(double scaling) = 0;
- virtual void RunRenderPriorityJobs() = 0;
- virtual void LostFocus() = 0;
- virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position,
- AvnInputModifiers modifiers, AvnDragDropEffects effects,
- IAvnClipboard* clipboard, void* dataObjectHandle) = 0;
-};
-
-
-AVNCOM(IAvnWindowEvents, 06) : IAvnWindowBaseEvents
-{
- /**
- * Closing Event
- * Called when the user presses the OS window close button.
- * return true to allow the close, return false to prevent close.
- */
- virtual bool Closing () = 0;
-
- virtual void WindowStateChanged (AvnWindowState state) = 0;
-
- virtual void GotInputWhenDisabled () = 0;
-};
-
-AVNCOM(IAvnMacOptions, 07) : IUnknown
-{
- virtual HRESULT SetShowInDock(int show) = 0;
- virtual HRESULT SetApplicationTitle (void* utf8string) = 0;
-};
-
-AVNCOM(IAvnActionCallback, 08) : IUnknown
-{
- virtual void Run() = 0;
-};
-
-AVNCOM(IAvnSignaledCallback, 09) : IUnknown
-{
- virtual void Signaled(int priority, bool priorityContainsMeaningfulValue) = 0;
-};
-
-AVNCOM(IAvnLoopCancellation, 0a) : IUnknown
-{
- virtual void Cancel() = 0;
-};
-
-AVNCOM(IAvnPlatformThreadingInterface, 0b) : IUnknown
-{
- virtual bool GetCurrentThreadIsLoopThread() = 0;
- virtual void SetSignaledCallback(IAvnSignaledCallback* cb) = 0;
- virtual IAvnLoopCancellation* CreateLoopCancellation() = 0;
- virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) = 0;
- // Can't pass int* to sharpgentools for some reason
- virtual void Signal(int priority) = 0;
- virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) = 0;
-};
-
-AVNCOM(IAvnSystemDialogEvents, 0c) : IUnknown
-{
- virtual void OnCompleted (int numResults, void* ptrFirstResult) = 0;
-};
-
-AVNCOM(IAvnSystemDialogs, 0d) : IUnknown
-{
- virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
- IAvnSystemDialogEvents* events,
- const char* title,
- const char* initialPath) = 0;
-
- virtual void OpenFileDialog (IAvnWindow* parentWindowHandle,
- IAvnSystemDialogEvents* events,
- bool allowMultiple,
- const char* title,
- const char* initialDirectory,
- const char* initialFile,
- const char* filters) = 0;
-
- virtual void SaveFileDialog (IAvnWindow* parentWindowHandle,
- IAvnSystemDialogEvents* events,
- const char* title,
- const char* initialDirectory,
- const char* initialFile,
- const char* filters) = 0;
-};
-
-AVNCOM(IAvnScreens, 0e) : IUnknown
-{
- virtual HRESULT GetScreenCount (int* ret) = 0;
- virtual HRESULT GetScreen (int index, AvnScreen* ret) = 0;
-};
-
-AVNCOM(IAvnClipboard, 0f) : IUnknown
-{
- virtual HRESULT GetText (char* type, IAvnString**ppv) = 0;
- virtual HRESULT SetText (char* type, void* utf8Text) = 0;
- virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0;
- virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0;
- virtual HRESULT SetBytes(char* type, void* utf8Text, int len) = 0;
- virtual HRESULT GetBytes(char* type, IAvnString**ppv) = 0;
-
- virtual HRESULT Clear() = 0;
-};
-
-AVNCOM(IAvnCursor, 10) : IUnknown
-{
-};
-
-AVNCOM(IAvnCursorFactory, 11) : IUnknown
-{
- virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) = 0;
-};
-
-AVNCOM(IAvnGlDisplay, 13) : IUnknown
-{
- virtual HRESULT CreateContext(IAvnGlContext* share, IAvnGlContext**ppv) = 0;
- virtual void LegacyClearCurrentContext() = 0;
- virtual HRESULT WrapContext(void* native, IAvnGlContext**ppv) = 0;
- virtual void* GetProcAddress(char* proc) = 0;
-};
-
-AVNCOM(IAvnGlContext, 14) : IUnknown
-{
- virtual HRESULT MakeCurrent(IUnknown** ppv) = 0;
- virtual HRESULT LegacyMakeCurrent() = 0;
- virtual int GetSampleCount() = 0;
- virtual int GetStencilSize() = 0;
- virtual void* GetNativeHandle() = 0;
-};
-
-AVNCOM(IAvnGlSurfaceRenderTarget, 15) : IUnknown
-{
- virtual HRESULT BeginDrawing(IAvnGlSurfaceRenderingSession** ret) = 0;
-};
-
-AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown
-{
- virtual HRESULT GetPixelSize(AvnPixelSize* ret) = 0;
- virtual HRESULT GetScaling(double* ret) = 0;
-};
-
-AVNCOM(IAvnMenu, 17) : IUnknown
-{
- virtual HRESULT InsertItem (int index, IAvnMenuItem* item) = 0;
- virtual HRESULT RemoveItem (IAvnMenuItem* item) = 0;
- virtual HRESULT SetTitle (void* utf8String) = 0;
- virtual HRESULT Clear () = 0;
-};
-
-AVNCOM(IAvnPredicateCallback, 18) : IUnknown
-{
- virtual bool Evaluate() = 0;
-};
-
-AVNCOM(IAvnMenuItem, 19) : IUnknown
-{
- virtual HRESULT SetSubMenu (IAvnMenu* menu) = 0;
- virtual HRESULT SetTitle (void* utf8String) = 0;
- virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0;
- virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0;
- virtual HRESULT SetIsChecked (bool isChecked) = 0;
- virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) = 0;
- virtual HRESULT SetIcon (void* data, size_t length) = 0;
-};
-
-AVNCOM(IAvnMenuEvents, 1A) : IUnknown
-{
- /**
- * NeedsUpdate
- */
- virtual void NeedsUpdate () = 0;
-};
-
-AVNCOM(IAvnStringArray, 20) : IUnknown
-{
- virtual unsigned int GetCount() = 0;
- virtual HRESULT Get(unsigned int index, IAvnString**ppv) = 0;
-};
-
-AVNCOM(IAvnDndResultCallback, 21) : IUnknown
-{
- virtual void OnDragAndDropComplete(AvnDragDropEffects effecct) = 0;
-};
-
-AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown
-{
- virtual void FreeGCHandle(void* handle) = 0;
-};
-
-AVNCOM(IAvnNativeControlHost, 20) : IUnknown
-{
- virtual HRESULT CreateDefaultChild(void* parent, void** retOut) = 0;
- virtual IAvnNativeControlHostTopLevelAttachment* CreateAttachment() = 0;
- virtual void DestroyDefaultChild(void* child) = 0;
-};
-
-AVNCOM(IAvnNativeControlHostTopLevelAttachment, 21) : IUnknown
-{
- virtual void* GetParentHandle() = 0;
- virtual HRESULT InitializeWithChildHandle(void* child) = 0;
- virtual HRESULT AttachTo(IAvnNativeControlHost* host) = 0;
- virtual void ShowInBounds(float x, float y, float width, float height) = 0;
- virtual void HideWithSize(float width, float height) = 0;
- virtual void ReleaseChild() = 0;
-};
-
-
-extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();
diff --git a/native/Avalonia.Native/inc/comimpl.h b/native/Avalonia.Native/inc/comimpl.h
index 0ff64b72158..47b0a3c5f27 100644
--- a/native/Avalonia.Native/inc/comimpl.h
+++ b/native/Avalonia.Native/inc/comimpl.h
@@ -8,8 +8,109 @@
#include
+/**
+ START_COM_CALL causes AddRef to be called at the beggining of a function.
+ When a function is exited, it causes ReleaseRef to be called.
+ This ensures that the object cannot be destroyed whilst the function is running.
+ For example: Window Show is called, which triggers an event, and user calls Close inside the event
+ causing the refcount to reach 0, and the object to be destroyed. Function then continues and this pointer
+ will now be invalid.
+
+ START_COM_CALL protects against this scenario.
+ */
+#define START_COM_CALL auto r = this->UnknownSelf()
+
__IID_DEF(IUnknown, 0, 0, 0, C0, 00, 00, 00, 00, 00, 00, 46);
+template
+class ComPtr
+{
+private:
+ TInterface* _obj;
+public:
+ ComPtr()
+ {
+ _obj = 0;
+ }
+
+ ComPtr(TInterface* pObj)
+ {
+ _obj = 0;
+
+ if (pObj)
+ {
+ _obj = pObj;
+ _obj->AddRef();
+ }
+ }
+
+ ComPtr(const ComPtr& ptr)
+ {
+ _obj = 0;
+
+ if (ptr._obj)
+ {
+ _obj = ptr._obj;
+ _obj->AddRef();
+ }
+
+ }
+
+ ComPtr& operator=(ComPtr other)
+ {
+ if(_obj != NULL)
+ _obj->Release();
+ _obj = other._obj;
+ if(_obj != NULL)
+ _obj->AddRef();
+ return *this;
+ }
+
+ ~ComPtr()
+ {
+ if (_obj)
+ {
+ _obj->Release();
+ _obj = 0;
+ }
+ }
+
+ TInterface* getRaw()
+ {
+ return _obj;
+ }
+
+ TInterface* getRetainedReference()
+ {
+ if(_obj == NULL)
+ return NULL;
+ _obj->AddRef();
+ return _obj;
+ }
+
+ TInterface** getPPV()
+ {
+ return &_obj;
+ }
+
+ operator TInterface*() const
+ {
+ return _obj;
+ }
+ TInterface& operator*() const
+ {
+ return *_obj;
+ }
+ TInterface** operator&()
+ {
+ return &_obj;
+ }
+ TInterface* operator->() const
+ {
+ return _obj;
+ }
+};
+
class ComObject : public virtual IUnknown
{
private:
@@ -58,6 +159,12 @@ class ComObject : public virtual IUnknown
_refCount++;
return S_OK;
}
+
+protected:
+ ComPtr UnknownSelf()
+ {
+ return this;
+ }
};
@@ -104,94 +211,5 @@ template class ComSingleObject : public ComO
virtual ~ComSingleObject(){}
};
-template
-class ComPtr
-{
-private:
- TInterface* _obj;
-public:
- ComPtr()
- {
- _obj = 0;
- }
-
- ComPtr(TInterface* pObj)
- {
- _obj = 0;
-
- if (pObj)
- {
- _obj = pObj;
- _obj->AddRef();
- }
- }
-
- ComPtr(const ComPtr& ptr)
- {
- _obj = 0;
-
- if (ptr._obj)
- {
- _obj = ptr._obj;
- _obj->AddRef();
- }
-
- }
-
- ComPtr& operator=(ComPtr other)
- {
- if(_obj != NULL)
- _obj->Release();
- _obj = other._obj;
- if(_obj != NULL)
- _obj->AddRef();
- return *this;
- }
-
- ~ComPtr()
- {
- if (_obj)
- {
- _obj->Release();
- _obj = 0;
- }
- }
-
- TInterface* getRaw()
- {
- return _obj;
- }
-
- TInterface* getRetainedReference()
- {
- if(_obj == NULL)
- return NULL;
- _obj->AddRef();
- return _obj;
- }
-
- TInterface** getPPV()
- {
- return &_obj;
- }
-
- operator TInterface*() const
- {
- return _obj;
- }
- TInterface& operator*() const
- {
- return *_obj;
- }
- TInterface** operator&()
- {
- return &_obj;
- }
- TInterface* operator->() const
- {
- return _obj;
- }
-};
-
#endif // COMIMPL_H_INCLUDED
#pragma clang diagnostic pop
diff --git a/native/Avalonia.Native/inc/key.h b/native/Avalonia.Native/inc/key.h
deleted file mode 100644
index 12d283cc171..00000000000
--- a/native/Avalonia.Native/inc/key.h
+++ /dev/null
@@ -1,1020 +0,0 @@
-#ifndef _KEY_H_
-#define _KEY_H_
-
-///
-/// Defines the keys available on a keyboard.
-///
-enum AvnKey
-{
- ///
- /// No key pressed.
- ///
- AvnKeyNone = 0,
-
- ///
- /// The Cancel key.
- ///
- AvnKeyCancel = 1,
-
- ///
- /// The Back key.
- ///
- AvnKeyBack = 2,
-
- ///
- /// The Tab key.
- ///
- AvnKeyTab = 3,
-
- ///
- /// The Linefeed key.
- ///
- AvnKeyLineFeed = 4,
-
- ///
- /// The Clear key.
- ///
- AvnKeyClear = 5,
-
- ///
- /// The Return key.
- ///
- AvnKeyReturn = 6,
-
- ///
- /// The Enter key.
- ///
- AvnKeyEnter = 6,
-
- ///
- /// The Pause key.
- ///
- AvnKeyPause = 7,
-
- ///
- /// The Caps Lock key.
- ///
- AvnKeyCapsLock = 8,
-
- ///
- /// The Caps Lock key.
- ///
- AvnKeyCapital = 8,
-
- ///
- /// The IME Hangul mode key.
- ///
- AvnKeyHangulMode = 9,
-
- ///
- /// The IME Kana mode key.
- ///
- AvnKeyKanaMode = 9,
-
- ///
- /// The IME Junja mode key.
- ///
- AvnKeyJunjaMode = 10,
-
- ///
- /// The IME Final mode key.
- ///
- AvnKeyFinalMode = 11,
-
- ///
- /// The IME Kanji mode key.
- ///
- AvnKeyKanjiMode = 12,
-
- ///
- /// The IME Hanja mode key.
- ///
- HanjaMode = 12,
-
- ///
- /// The Escape key.
- ///
- Escape = 13,
-
- ///
- /// The IME Convert key.
- ///
- ImeConvert = 14,
-
- ///
- /// The IME NonConvert key.
- ///
- ImeNonConvert = 15,
-
- ///
- /// The IME Accept key.
- ///
- ImeAccept = 16,
-
- ///
- /// The IME Mode change key.
- ///
- ImeModeChange = 17,
-
- ///
- /// The space bar.
- ///
- Space = 18,
-
- ///
- /// The Page Up key.
- ///
- PageUp = 19,
-
- ///
- /// The Page Up key.
- ///
- Prior = 19,
-
- ///
- /// The Page Down key.
- ///
- PageDown = 20,
-
- ///
- /// The Page Down key.
- ///
- Next = 20,
-
- ///
- /// The End key.
- ///
- End = 21,
-
- ///
- /// The Home key.
- ///
- Home = 22,
-
- ///
- /// The Left arrow key.
- ///
- Left = 23,
-
- ///
- /// The Up arrow key.
- ///
- Up = 24,
-
- ///
- /// The Right arrow key.
- ///
- Right = 25,
-
- ///
- /// The Down arrow key.
- ///
- Down = 26,
-
- ///
- /// The Select key.
- ///
- Select = 27,
-
- ///
- /// The Print key.
- ///
- Print = 28,
-
- ///
- /// The Execute key.
- ///
- Execute = 29,
-
- ///
- /// The Print Screen key.
- ///
- Snapshot = 30,
-
- ///
- /// The Print Screen key.
- ///
- PrintScreen = 30,
-
- ///
- /// The Insert key.
- ///
- Insert = 31,
-
- ///
- /// The Delete key.
- ///
- Delete = 32,
-
- ///
- /// The Help key.
- ///
- Help = 33,
-
- ///
- /// The 0 key.
- ///
- D0 = 34,
-
- ///
- /// The 1 key.
- ///
- D1 = 35,
-
- ///
- /// The 2 key.
- ///
- D2 = 36,
-
- ///
- /// The 3 key.
- ///
- D3 = 37,
-
- ///
- /// The 4 key.
- ///
- D4 = 38,
-
- ///
- /// The 5 key.
- ///
- D5 = 39,
-
- ///
- /// The 6 key.
- ///
- D6 = 40,
-
- ///
- /// The 7 key.
- ///
- D7 = 41,
-
- ///
- /// The 8 key.
- ///
- D8 = 42,
-
- ///
- /// The 9 key.
- ///
- D9 = 43,
-
- ///
- /// The A key.
- ///
- A = 44,
-
- ///
- /// The B key.
- ///
- B = 45,
-
- ///
- /// The C key.
- ///
- C = 46,
-
- ///
- /// The D key.
- ///
- D = 47,
-
- ///
- /// The E key.
- ///
- E = 48,
-
- ///
- /// The F key.
- ///
- F = 49,
-
- ///
- /// The G key.
- ///
- G = 50,
-
- ///
- /// The H key.
- ///
- H = 51,
-
- ///
- /// The I key.
- ///
- I = 52,
-
- ///
- /// The J key.
- ///
- J = 53,
-
- ///
- /// The K key.
- ///
- AvnKeyK = 54,
-
- ///
- /// The L key.
- ///
- L = 55,
-
- ///
- /// The M key.
- ///
- M = 56,
-
- ///
- /// The N key.
- ///
- N = 57,
-
- ///
- /// The O key.
- ///
- O = 58,
-
- ///
- /// The P key.
- ///
- P = 59,
-
- ///
- /// The Q key.
- ///
- Q = 60,
-
- ///
- /// The R key.
- ///
- R = 61,
-
- ///
- /// The S key.
- ///
- S = 62,
-
- ///
- /// The T key.
- ///
- T = 63,
-
- ///
- /// The U key.
- ///
- U = 64,
-
- ///
- /// The V key.
- ///
- V = 65,
-
- ///
- /// The W key.
- ///
- W = 66,
-
- ///
- /// The X key.
- ///
- X = 67,
-
- ///
- /// The Y key.
- ///
- Y = 68,
-
- ///
- /// The Z key.
- ///
- Z = 69,
-
- ///
- /// The left Windows key.
- ///
- LWin = 70,
-
- ///
- /// The right Windows key.
- ///
- RWin = 71,
-
- ///
- /// The Application key.
- ///
- Apps = 72,
-
- ///
- /// The Sleep key.
- ///
- Sleep = 73,
-
- ///
- /// The 0 key on the numeric keypad.
- ///
- NumPad0 = 74,
-
- ///
- /// The 1 key on the numeric keypad.
- ///
- NumPad1 = 75,
-
- ///
- /// The 2 key on the numeric keypad.
- ///
- NumPad2 = 76,
-
- ///
- /// The 3 key on the numeric keypad.
- ///
- NumPad3 = 77,
-
- ///
- /// The 4 key on the numeric keypad.
- ///
- NumPad4 = 78,
-
- ///
- /// The 5 key on the numeric keypad.
- ///
- NumPad5 = 79,
-
- ///
- /// The 6 key on the numeric keypad.
- ///
- NumPad6 = 80,
-
- ///
- /// The 7 key on the numeric keypad.
- ///
- NumPad7 = 81,
-
- ///
- /// The 8 key on the numeric keypad.
- ///
- NumPad8 = 82,
-
- ///
- /// The 9 key on the numeric keypad.
- ///
- NumPad9 = 83,
-
- ///
- /// The Multiply key.
- ///
- Multiply = 84,
-
- ///
- /// The Add key.
- ///
- Add = 85,
-
- ///
- /// The Separator key.
- ///
- Separator = 86,
-
- ///
- /// The Subtract key.
- ///
- Subtract = 87,
-
- ///
- /// The Decimal key.
- ///
- Decimal = 88,
-
- ///
- /// The Divide key.
- ///
- Divide = 89,
-
- ///
- /// The F1 key.
- ///
- F1 = 90,
-
- ///
- /// The F2 key.
- ///
- F2 = 91,
-
- ///
- /// The F3 key.
- ///
- F3 = 92,
-
- ///
- /// The F4 key.
- ///
- F4 = 93,
-
- ///
- /// The F5 key.
- ///
- F5 = 94,
-
- ///
- /// The F6 key.
- ///
- F6 = 95,
-
- ///
- /// The F7 key.
- ///
- F7 = 96,
-
- ///
- /// The F8 key.
- ///
- F8 = 97,
-
- ///
- /// The F9 key.
- ///
- F9 = 98,
-
- ///
- /// The F10 key.
- ///
- F10 = 99,
-
- ///
- /// The F11 key.
- ///
- F11 = 100,
-
- ///
- /// The F12 key.
- ///
- F12 = 101,
-
- ///
- /// The F13 key.
- ///
- F13 = 102,
-
- ///
- /// The F14 key.
- ///
- F14 = 103,
-
- ///
- /// The F15 key.
- ///
- F15 = 104,
-
- ///
- /// The F16 key.
- ///
- F16 = 105,
-
- ///
- /// The F17 key.
- ///
- F17 = 106,
-
- ///
- /// The F18 key.
- ///
- F18 = 107,
-
- ///
- /// The F19 key.
- ///
- F19 = 108,
-
- ///
- /// The F20 key.
- ///
- F20 = 109,
-
- ///
- /// The F21 key.
- ///
- F21 = 110,
-
- ///
- /// The F22 key.
- ///
- F22 = 111,
-
- ///
- /// The F23 key.
- ///
- F23 = 112,
-
- ///
- /// The F24 key.
- ///
- F24 = 113,
-
- ///
- /// The Numlock key.
- ///
- NumLock = 114,
-
- ///
- /// The Scroll key.
- ///
- Scroll = 115,
-
- ///
- /// The left Shift key.
- ///
- LeftShift = 116,
-
- ///
- /// The right Shift key.
- ///
- RightShift = 117,
-
- ///
- /// The left Ctrl key.
- ///
- LeftCtrl = 118,
-
- ///
- /// The right Ctrl key.
- ///
- RightCtrl = 119,
-
- ///
- /// The left Alt key.
- ///
- LeftAlt = 120,
-
- ///
- /// The right Alt key.
- ///
- RightAlt = 121,
-
- ///
- /// The browser Back key.
- ///
- BrowserBack = 122,
-
- ///
- /// The browser Forward key.
- ///
- BrowserForward = 123,
-
- ///
- /// The browser Refresh key.
- ///
- BrowserRefresh = 124,
-
- ///
- /// The browser Stop key.
- ///
- BrowserStop = 125,
-
- ///
- /// The browser Search key.
- ///
- BrowserSearch = 126,
-
- ///
- /// The browser Favorites key.
- ///
- BrowserFavorites = 127,
-
- ///
- /// The browser Home key.
- ///
- BrowserHome = 128,
-
- ///
- /// The Volume Mute key.
- ///
- VolumeMute = 129,
-
- ///
- /// The Volume Down key.
- ///
- VolumeDown = 130,
-
- ///
- /// The Volume Up key.
- ///
- VolumeUp = 131,
-
- ///
- /// The media Next Track key.
- ///
- MediaNextTrack = 132,
-
- ///
- /// The media Previous Track key.
- ///
- MediaPreviousTrack = 133,
-
- ///
- /// The media Stop key.
- ///
- MediaStop = 134,
-
- ///
- /// The media Play/Pause key.
- ///
- MediaPlayPause = 135,
-
- ///
- /// The Launch Mail key.
- ///
- LaunchMail = 136,
-
- ///
- /// The Select Media key.
- ///
- SelectMedia = 137,
-
- ///
- /// The Launch Application 1 key.
- ///
- LaunchApplication1 = 138,
-
- ///
- /// The Launch Application 2 key.
- ///
- LaunchApplication2 = 139,
-
- ///
- /// The OEM Semicolon key.
- ///
- OemSemicolon = 140,
-
- ///
- /// The OEM 1 key.
- ///
- Oem1 = 140,
-
- ///
- /// The OEM Plus key.
- ///
- OemPlus = 141,
-
- ///
- /// The OEM Comma key.
- ///
- OemComma = 142,
-
- ///
- /// The OEM Minus key.
- ///
- OemMinus = 143,
-
- ///
- /// The OEM Period key.
- ///
- OemPeriod = 144,
-
- ///
- /// The OEM Question Mark key.
- ///
- OemQuestion = 145,
-
- ///
- /// The OEM 2 key.
- ///
- Oem2 = 145,
-
- ///
- /// The OEM Tilde key.
- ///
- OemTilde = 146,
-
- ///
- /// The OEM 3 key.
- ///
- Oem3 = 146,
-
- ///
- /// The ABNT_C1 (Brazilian) key.
- ///
- AbntC1 = 147,
-
- ///
- /// The ABNT_C2 (Brazilian) key.
- ///
- AbntC2 = 148,
-
- ///
- /// The OEM Open Brackets key.
- ///
- OemOpenBrackets = 149,
-
- ///
- /// The OEM 4 key.
- ///
- Oem4 = 149,
-
- ///
- /// The OEM Pipe key.
- ///
- OemPipe = 150,
-
- ///
- /// The OEM 5 key.
- ///
- Oem5 = 150,
-
- ///
- /// The OEM Close Brackets key.
- ///
- OemCloseBrackets = 151,
-
- ///
- /// The OEM 6 key.
- ///
- Oem6 = 151,
-
- ///
- /// The OEM Quotes key.
- ///
- OemQuotes = 152,
-
- ///
- /// The OEM 7 key.
- ///
- Oem7 = 152,
-
- ///
- /// The OEM 8 key.
- ///
- Oem8 = 153,
-
- ///
- /// The OEM Backslash key.
- ///
- OemBackslash = 154,
-
- ///
- /// The OEM 3 key.
- ///
- Oem102 = 154,
-
- ///
- /// A special key masking the real key being processed by an IME.
- ///
- ImeProcessed = 155,
-
- ///
- /// A special key masking the real key being processed as a system key.
- ///
- System = 156,
-
- ///
- /// The OEM ATTN key.
- ///
- OemAttn = 157,
-
- ///
- /// The DBE_ALPHANUMERIC key.
- ///
- DbeAlphanumeric = 157,
-
- ///
- /// The OEM Finish key.
- ///
- OemFinish = 158,
-
- ///
- /// The DBE_KATAKANA key.
- ///
- DbeKatakana = 158,
-
- ///
- /// The DBE_HIRAGANA key.
- ///
- DbeHiragana = 159,
-
- ///
- /// The OEM Copy key.
- ///
- OemCopy = 159,
-
- ///
- /// The DBE_SBCSCHAR key.
- ///
- DbeSbcsChar = 160,
-
- ///
- /// The OEM Auto key.
- ///
- OemAuto = 160,
-
- ///
- /// The DBE_DBCSCHAR key.
- ///
- DbeDbcsChar = 161,
-
- ///
- /// The OEM ENLW key.
- ///
- OemEnlw = 161,
-
- ///
- /// The OEM BackTab key.
- ///
- OemBackTab = 162,
-
- ///
- /// The DBE_ROMAN key.
- ///
- DbeRoman = 162,
-
- ///
- /// The DBE_NOROMAN key.
- ///
- DbeNoRoman = 163,
-
- ///
- /// The ATTN key.
- ///
- Attn = 163,
-
- ///
- /// The CRSEL key.
- ///
- CrSel = 164,
-
- ///
- /// The DBE_ENTERWORDREGISTERMODE key.
- ///
- DbeEnterWordRegisterMode = 164,
-
- ///
- /// The EXSEL key.
- ///
- ExSel = 165,
-
- ///
- /// The DBE_ENTERIMECONFIGMODE key.
- ///
- DbeEnterImeConfigureMode = 165,
-
- ///
- /// The ERASE EOF Key.
- ///
- EraseEof = 166,
-
- ///
- /// The DBE_FLUSHSTRING key.
- ///
- DbeFlushString = 166,
-
- ///
- /// The Play key.
- ///
- Play = 167,
-
- ///
- /// The DBE_CODEINPUT key.
- ///
- DbeCodeInput = 167,
-
- ///
- /// The DBE_NOCODEINPUT key.
- ///
- DbeNoCodeInput = 168,
-
- ///
- /// The Zoom key.
- ///
- Zoom = 168,
-
- ///
- /// Reserved for future use.
- ///
- NoName = 169,
-
- ///
- /// The DBE_DETERMINESTRING key.
- ///
- DbeDetermineString = 169,
-
- ///
- /// The DBE_ENTERDLGCONVERSIONMODE key.
- ///
- DbeEnterDialogConversionMode = 170,
-
- ///
- /// The PA1 key.
- ///
- Pa1 = 170,
-
- ///
- /// The OEM Clear key.
- ///
- OemClear = 171,
-
- ///
- /// The key is used with another key to create a single combined character.
- ///
- DeadCharProcessed = 172,
-};
-
-#endif
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index d5cad4d1caf..dba3ee6d31d 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -8,19 +8,20 @@
/* Begin PBXBuildFile section */
1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; };
+ 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */; };
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
- 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A1852DB23E05814008F0DED /* deadlock.mm */; };
- 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; };
+ 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1AFD334023E03C4F0042899B /* controlhost.mm */; };
37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; };
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; };
+ 522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; };
5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
@@ -32,13 +33,13 @@
/* Begin PBXFileReference section */
1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; };
+ 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; };
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = rendertarget.mm; sourceTree = ""; };
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; };
- 1A1852DB23E05814008F0DED /* deadlock.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = deadlock.mm; sourceTree = ""; };
- 1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = ""; };
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; };
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; };
+ 1AFD334023E03C4F0042899B /* controlhost.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = controlhost.mm; sourceTree = ""; };
37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; };
379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; };
37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; };
@@ -49,6 +50,7 @@
37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = ""; };
37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; };
520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; };
+ 522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; };
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; };
5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; };
@@ -69,6 +71,7 @@
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */,
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */,
AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */,
+ 522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */,
AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -79,6 +82,7 @@
AB661C1C2148230E00291242 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ 522D5958258159C1006F7F7A /* Carbon.framework */,
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */,
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */,
AB1E522B217613570091CD71 /* OpenGL.framework */,
diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h
index 5d299374e5c..3ce83d370a7 100644
--- a/native/Avalonia.Native/src/OSX/AvnString.h
+++ b/native/Avalonia.Native/src/OSX/AvnString.h
@@ -11,6 +11,7 @@
extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray* array);
+extern IAvnStringArray* CreateAvnStringArray(NSArray* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
extern IAvnString* CreateByteArray(void* data, int len);
#endif /* AvnString_h */
diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm
index 00b748ef63f..cd0e2cdf941 100644
--- a/native/Avalonia.Native/src/OSX/AvnString.mm
+++ b/native/Avalonia.Native/src/OSX/AvnString.mm
@@ -43,6 +43,8 @@
virtual HRESULT Pointer(void**retOut) override
{
+ START_COM_CALL;
+
@autoreleasepool
{
if(retOut == nullptr)
@@ -58,14 +60,19 @@ virtual HRESULT Pointer(void**retOut) override
virtual HRESULT Length(int*retOut) override
{
- if(retOut == nullptr)
+ START_COM_CALL;
+
+ @autoreleasepool
{
- return E_POINTER;
+ if(retOut == nullptr)
+ {
+ return E_POINTER;
+ }
+
+ *retOut = _length;
+
+ return S_OK;
}
-
- *retOut = _length;
-
- return S_OK;
}
};
@@ -85,6 +92,16 @@ virtual HRESULT Length(int*retOut) override
}
}
+ AvnStringArrayImpl(NSArray* array)
+ {
+ for(int c = 0; c < [array count]; c++)
+ {
+ ComPtr s;
+ *s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString);
+ _list.push_back(s);
+ }
+ }
+
AvnStringArrayImpl(NSString* string)
{
ComPtr s;
@@ -99,10 +116,15 @@ virtual unsigned int GetCount() override
virtual HRESULT Get(unsigned int index, IAvnString**ppv) override
{
- if(_list.size() <= index)
- return E_INVALIDARG;
- *ppv = _list[index].getRetainedReference();
- return S_OK;
+ START_COM_CALL;
+
+ @autoreleasepool
+ {
+ if(_list.size() <= index)
+ return E_INVALIDARG;
+ *ppv = _list[index].getRetainedReference();
+ return S_OK;
+ }
}
};
@@ -117,6 +139,11 @@ virtual HRESULT Get(unsigned int index, IAvnString**ppv) override
return new AvnStringArrayImpl(array);
}
+IAvnStringArray* CreateAvnStringArray(NSArray * array)
+{
+ return new AvnStringArrayImpl(array);
+}
+
IAvnStringArray* CreateAvnStringArray(NSString* string)
{
return new AvnStringArrayImpl(string);
diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.h b/native/Avalonia.Native/src/OSX/KeyTransform.h
index ea4fbecd5c6..2f434570c9e 100644
--- a/native/Avalonia.Native/src/OSX/KeyTransform.h
+++ b/native/Avalonia.Native/src/OSX/KeyTransform.h
@@ -1,9 +1,14 @@
#ifndef keytransform_h
#define keytransform_h
#include "common.h"
-#include "key.h"
#include
-
- Designer
-
+
-
+
+
-
+
+ Resources\drawable\Icon.png
+
diff --git a/samples/ControlCatalog.Android/MainActivity.cs b/samples/ControlCatalog.Android/MainActivity.cs
index 40d001a1950..2ab03551b60 100644
--- a/samples/ControlCatalog.Android/MainActivity.cs
+++ b/samples/ControlCatalog.Android/MainActivity.cs
@@ -1,31 +1,18 @@
-using System;
-using Android.App;
+using Android.App;
using Android.OS;
using Android.Content.PM;
using Avalonia.Android;
-using Avalonia.Controls;
-using Avalonia.Controls.Templates;
-using Avalonia.Markup.Xaml;
-using Avalonia.Media;
-using Avalonia.Styling;
-using Avalonia.Themes.Default;
-using Avalonia;
namespace ControlCatalog.Android
{
- [Activity(Label = "ControlCatalog.Android", MainLauncher = true, Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)]
+ [Activity(Label = "ControlCatalog.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance)]
public class MainActivity : AvaloniaActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
- if (Avalonia.Application.Current == null)
- {
- AppBuilder.Configure()
- .UseAndroid()
- .SetupWithoutStarting();
- Content = new MainView();
- }
base.OnCreate(savedInstanceState);
+
+ Content = new MainView();
}
}
}
diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
index e39ec39f1ca..9effda7e797 100644
--- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
+++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs
index 96f0e76fd81..b1ca548e2c7 100644
--- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs
+++ b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs
@@ -2,7 +2,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -15,7 +14,7 @@ namespace ControlCatalog.Android
{
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
public partial class Resource
{
@@ -26,8 +25,6 @@ static Resource()
public static void UpdateIdValues()
{
- global::Avalonia.Android.Resource.String.ApplicationName = global::ControlCatalog.Android.Resource.String.ApplicationName;
- global::Avalonia.Android.Resource.String.Hello = global::ControlCatalog.Android.Resource.String.Hello;
}
public partial class Attribute
@@ -43,69 +40,59 @@ private Attribute()
}
}
- public partial class Drawable
+ public partial class Color
{
- // aapt resource value: 0x7f020000
- public const int Icon = 2130837504;
+ // aapt resource value: 0x7F010000
+ public const int splash_background = 2130771968;
- static Drawable()
+ static Color()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private Drawable()
+ private Color()
{
}
}
- public partial class Id
+ public partial class Drawable
{
- // aapt resource value: 0x7f050000
- public const int MyButton = 2131034112;
-
- static Id()
- {
- global::Android.Runtime.ResourceIdManager.UpdateIdValues();
- }
-
- private Id()
- {
- }
- }
-
- public partial class Layout
- {
+ // aapt resource value: 0x7F020000
+ public const int Icon = 2130837504;
- // aapt resource value: 0x7f030000
- public const int Main = 2130903040;
+ // aapt resource value: 0x7F020001
+ public const int splash_screen = 2130837505;
- static Layout()
+ static Drawable()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private Layout()
+ private Drawable()
{
}
}
- public partial class String
+ public partial class Style
{
- // aapt resource value: 0x7f040001
- public const int ApplicationName = 2130968577;
+ // aapt resource value: 0x7F030000
+ public const int MyTheme = 2130903040;
+
+ // aapt resource value: 0x7F030001
+ public const int MyTheme_NoActionBar = 2130903041;
- // aapt resource value: 0x7f040000
- public const int Hello = 2130968576;
+ // aapt resource value: 0x7F030002
+ public const int MyTheme_Splash = 2130903042;
- static String()
+ static Style()
{
global::Android.Runtime.ResourceIdManager.UpdateIdValues();
}
- private String()
+ private Style()
{
}
}
diff --git a/samples/ControlCatalog.Android/Resources/drawable/Icon.png b/samples/ControlCatalog.Android/Resources/drawable/Icon.png
deleted file mode 100644
index 8074c4c571b..00000000000
Binary files a/samples/ControlCatalog.Android/Resources/drawable/Icon.png and /dev/null differ
diff --git a/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml b/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml
new file mode 100644
index 00000000000..2e920b4b3be
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/drawable/splash_screen.xml
@@ -0,0 +1,13 @@
+
+
+
+ -
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Android/Resources/layout/Main.axml b/samples/ControlCatalog.Android/Resources/layout/Main.axml
deleted file mode 100644
index 570c96ad728..00000000000
--- a/samples/ControlCatalog.Android/Resources/layout/Main.axml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
diff --git a/samples/ControlCatalog.Android/Resources/values/Strings.xml b/samples/ControlCatalog.Android/Resources/values/Strings.xml
deleted file mode 100644
index 95221a08a91..00000000000
--- a/samples/ControlCatalog.Android/Resources/values/Strings.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- Hello World, Click Me!
- ControlCatalog.Android
-
diff --git a/samples/ControlCatalog.Android/Resources/values/colors.xml b/samples/ControlCatalog.Android/Resources/values/colors.xml
new file mode 100644
index 00000000000..59279d5d32e
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
diff --git a/samples/ControlCatalog.Android/Resources/values/styles.xml b/samples/ControlCatalog.Android/Resources/values/styles.xml
new file mode 100644
index 00000000000..e017b6facf9
--- /dev/null
+++ b/samples/ControlCatalog.Android/Resources/values/styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Android/SplashActivity.cs b/samples/ControlCatalog.Android/SplashActivity.cs
new file mode 100644
index 00000000000..6d7c6bc1161
--- /dev/null
+++ b/samples/ControlCatalog.Android/SplashActivity.cs
@@ -0,0 +1,32 @@
+using Android.App;
+using Android.Content;
+using Android.OS;
+using Application = Android.App.Application;
+
+using Avalonia;
+
+namespace ControlCatalog.Android
+{
+ [Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)]
+ public class SplashActivity : Activity
+ {
+ protected override void OnCreate(Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ }
+
+ protected override void OnResume()
+ {
+ base.OnResume();
+
+ if (Avalonia.Application.Current == null)
+ {
+ AppBuilder.Configure()
+ .UseAndroid()
+ .SetupWithoutStarting();
+ }
+
+ StartActivity(new Intent(Application.Context, typeof(MainActivity)));
+ }
+ }
+}
diff --git a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
index 1a112d0d7d6..c6405dabb6d 100644
--- a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
+++ b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj
@@ -3,6 +3,7 @@
Exe
net461
+ x64
diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs
index b2df1953f5d..7b8b27fff7f 100644
--- a/samples/ControlCatalog.Desktop/Program.cs
+++ b/samples/ControlCatalog.Desktop/Program.cs
@@ -3,28 +3,22 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform;
-using Avalonia.ReactiveUI;
namespace ControlCatalog
{
internal class Program
{
[STAThread]
- static void Main(string[] args)
- {
- // TODO: Make this work with GTK/Skia/Cairo depending on command-line args
- // again.
- BuildAvaloniaApp().Start();
- }
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
///
/// This method is needed for IDE previewer infrastructure
///
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
- .LogToDebug()
- .UsePlatformDetect()
- .UseReactiveUI();
+ .LogToTrace()
+ .UsePlatformDetect();
private static void ConfigureAssetAssembly(AppBuilder builder)
{
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index 28b0257eda3..3c2d2ee359c 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -1,7 +1,7 @@
- Exe
+ WinExe
netcoreapp3.1
true
@@ -11,11 +11,14 @@
-
-
+
+
+
+ en
+
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 142736a0bbf..0c8fd9465cf 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -7,12 +7,10 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Dialogs;
using Avalonia.Headless;
using Avalonia.LogicalTree;
-using Avalonia.Skia;
-using Avalonia.ReactiveUI;
using Avalonia.Threading;
-using Avalonia.Dialogs;
namespace ControlCatalog.NetCore
{
@@ -111,17 +109,16 @@ public static AppBuilder BuildAvaloniaApp()
.With(new X11PlatformOptions
{
EnableMultiTouch = true,
- UseDBusMenu = true
+ UseDBusMenu = true,
+ EnableIme = true,
})
.With(new Win32PlatformOptions
{
- EnableMultitouch = true,
- AllowEglInitialization = true
+ EnableMultitouch = true
})
.UseSkia()
- .UseReactiveUI()
.UseManagedSystemDialogs()
- .LogToDebug();
+ .LogToTrace();
static void SilenceConsole()
{
diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index 9bac320c790..6aad44c0d50 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -12,6 +12,16 @@
+
+
+
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index 22f4e9be1fb..f3ec7b48aaa 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -23,7 +23,7 @@ public class App : Application
{
new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
- Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentDark.xaml")
+ Source = new Uri("avares://Avalonia.Themes.Fluent/FluentDark.xaml")
},
DataGridFluent
};
@@ -32,13 +32,17 @@ public class App : Application
{
new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
- Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentLight.xaml")
+ Source = new Uri("avares://Avalonia.Themes.Fluent/FluentLight.xaml")
},
DataGridFluent
};
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 @@ public class App : Application
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")
diff --git a/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf
new file mode 100644
index 00000000000..61e2583a6c2
Binary files /dev/null and b/samples/ControlCatalog/Assets/Fonts/WenQuanYiMicroHei-01.ttf differ
diff --git a/samples/ControlCatalog/Assets/avalonia-32.png b/samples/ControlCatalog/Assets/avalonia-32.png
new file mode 100644
index 00000000000..7b443e7a250
Binary files /dev/null and b/samples/ControlCatalog/Assets/avalonia-32.png differ
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 8a88b89b489..53ad213d924 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -24,9 +24,9 @@
-
+
-
+
diff --git a/samples/ControlCatalog/DecoratedWindow.xaml b/samples/ControlCatalog/DecoratedWindow.xaml
index 8e4c97b7f03..5251a2fa552 100644
--- a/samples/ControlCatalog/DecoratedWindow.xaml
+++ b/samples/ControlCatalog/DecoratedWindow.xaml
@@ -6,25 +6,21 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index bd5beafe297..6537c470d5a 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -21,7 +21,14 @@
+
+
+
+
+
+
@@ -34,6 +41,9 @@
+
+
+
@@ -44,6 +54,7 @@
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
+
diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml
index 97bd88f5e43..a107ee2163d 100644
--- a/samples/ControlCatalog/MainWindow.xaml
+++ b/samples/ControlCatalog/MainWindow.xaml
@@ -16,47 +16,39 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs
index a316321cd18..2446c0e1c99 100644
--- a/samples/ControlCatalog/MainWindow.xaml.cs
+++ b/samples/ControlCatalog/MainWindow.xaml.cs
@@ -17,7 +17,10 @@ public class MainWindow : Window
public MainWindow()
{
this.InitializeComponent();
- this.AttachDevTools();
+ this.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()
+ {
+ StartupScreenIndex = 1,
+ });
//Renderer.DrawFps = true;
//Renderer.DrawDirtyRects = Renderer.DrawFps = true;
@@ -67,7 +70,7 @@ private void InitializeComponent()
if (Application.Current.Styles.Contains(App.FluentDark)
|| Application.Current.Styles.Contains(App.FluentLight))
{
- var theme = new Avalonia.Themes.Fluent.FluentTheme();
+ var theme = new Avalonia.Themes.Fluent.Controls.FluentControls();
theme.TryGetResource("Button", out _);
}
else
diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml
index 96cfcc5288f..7635e1ccc34 100644
--- a/samples/ControlCatalog/Pages/AcrylicPage.xaml
+++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml
@@ -16,13 +16,13 @@
-
-
+
+
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
index a49616e543f..1a53217842a 100644
--- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
@@ -1,5 +1,6 @@
AutoCompleteBox
@@ -56,6 +57,16 @@
Width="200"
Margin="0,0,0,8"
FilterMode="None"/>
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
index c3f9f65dd97..4ac2330403d 100644
--- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml
@@ -1,6 +1,7 @@
+ x:Class="ControlCatalog.Pages.ButtonSpinnerPage"
+ xmlns:sys="clr-namespace:System;assembly=netstandard">
ButtonSpinner
@@ -19,6 +20,14 @@
ShowButtonSpinner="{Binding #showSpinCheck.IsChecked}">
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
index 369f7037186..025b85492cd 100644
--- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml
@@ -1,6 +1,7 @@
+ x:Class="ControlCatalog.Pages.ComboBoxPage"
+ xmlns:sys="clr-namespace:System;assembly=netstandard">
ComboBox
A drop-down list.
@@ -35,6 +36,16 @@
+
+
+ Inline Items
+ Inline Item 2
+ Inline Item 3
+ Inline Item 4
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
new file mode 100644
index 00000000000..0d9026bdd5b
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+ Context Flyout
+ A right click Flyout that can be applied to any control.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hello world
+
+
+ Inner context flyout
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
new file mode 100644
index 00000000000..10f9ee1de8f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
@@ -0,0 +1,91 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+using Avalonia.Interactivity;
+using System;
+using System.ComponentModel;
+
+namespace ControlCatalog.Pages
+{
+ public class ContextFlyoutPage : UserControl
+ {
+ private TextBox _textBox;
+
+ public ContextFlyoutPage()
+ {
+ InitializeComponent();
+
+ DataContext = new ContextPageViewModel();
+
+ _textBox = this.FindControl("TextBox");
+
+ var cutButton = this.FindControl
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
index f861bfab33d..da016e9a75d 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
@@ -1,5 +1,8 @@
using System;
+using System.ComponentModel;
+
using Avalonia.Controls;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
@@ -10,21 +13,54 @@ public class ContextMenuPage : UserControl
public ContextMenuPage()
{
this.InitializeComponent();
- DataContext = new ContextMenuPageViewModel();
+ DataContext = new ContextPageViewModel();
+
+ var customContextRequestedBorder = this.FindControl("CustomContextRequestedBorder");
+ customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
+
+ var cancellableContextBorder = this.FindControl("CancellableContextBorder");
+ cancellableContextBorder.ContextMenu!.ContextMenuClosing += ContextFlyoutPage_Closing;
+ cancellableContextBorder.ContextMenu!.ContextMenuOpening += ContextFlyoutPage_Opening;
}
- private ContextMenuPageViewModel _model;
+ private ContextPageViewModel _model;
protected override void OnDataContextChanged(EventArgs e)
{
if (_model != null)
_model.View = null;
- _model = DataContext as ContextMenuPageViewModel;
+ _model = DataContext as ContextPageViewModel;
if (_model != null)
_model.View = this;
base.OnDataContextChanged(e);
}
+ private void ContextFlyoutPage_Closing(object sender, CancelEventArgs e)
+ {
+ var cancelCloseCheckBox = this.FindControl("CancelCloseCheckBox");
+ e.Cancel = cancelCloseCheckBox.IsChecked ?? false;
+ }
+
+ private void ContextFlyoutPage_Opening(object sender, EventArgs e)
+ {
+ if (e is CancelEventArgs cancelArgs)
+ {
+ var cancelCloseCheckBox = this.FindControl("CancelOpenCheckBox");
+ cancelArgs.Cancel = cancelCloseCheckBox.IsChecked ?? false;
+ }
+ }
+
+ public void CustomContextRequested(object sender, ContextRequestedEventArgs e)
+ {
+ var border = (Border)sender;
+ var textBlock = (TextBlock)border.Child;
+
+ textBlock.Text = e.TryGetPosition(border, out var point)
+ ? $"Context was requested with pointer at: {point.X:N0}, {point.Y:N0}"
+ : "Context was requested without pointer";
+ e.Handled = true;
+ }
+
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml
new file mode 100644
index 00000000000..a28039ea3f2
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CursorPage.xaml
@@ -0,0 +1,29 @@
+
+
+
+ Cursor
+ Defines a cursor (mouse pointer)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Custom Cursor
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml.cs b/samples/ControlCatalog/Pages/CursorPage.xaml.cs
new file mode 100644
index 00000000000..9e9e9ba8b96
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CursorPage.xaml.cs
@@ -0,0 +1,20 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+ public class CursorPage : UserControl
+ {
+ public CursorPage()
+ {
+ this.InitializeComponent();
+ DataContext = new CursorPageViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml
index cacc2204bde..340b3376f54 100644
--- a/samples/ControlCatalog/Pages/DataGridPage.xaml
+++ b/samples/ControlCatalog/Pages/DataGridPage.xaml
@@ -1,5 +1,5 @@
@@ -23,23 +23,29 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs
index 2a30f4d91bf..dc5cc49a902 100644
--- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs
@@ -24,8 +24,10 @@ public DataGridPage()
dg1.LoadingRow += Dg1_LoadingRow;
dg1.Sorting += (s, a) =>
{
- var property = ((a.Column as DataGridBoundColumn)?.Binding as Binding).Path;
- if (property == dataGridSortDescription.PropertyPath
+ var binding = (a.Column as DataGridBoundColumn)?.Binding as Binding;
+
+ if (binding?.Path is string property
+ && property == dataGridSortDescription.PropertyPath
&& !collectionView1.SortDescriptions.Contains(dataGridSortDescription))
{
collectionView1.SortDescriptions.Add(dataGridSortDescription);
diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
index af6b6e86055..45056a9a765 100644
--- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
@@ -2,6 +2,7 @@
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"
+ xmlns:sys="clr-namespace:System;assembly=netstandard"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.DateTimePickerPage">
@@ -30,6 +31,16 @@
+
+
+
+
+
+
+
+
+
A DatePicker with day formatted and year hidden.
+
+
+
+
+
+
+
+
A TimePicker with a header and minute increments specified.
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index cf6c771e34b..49921fb7f68 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
@@ -4,6 +4,7 @@
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Dialogs;
+using Avalonia.Layout;
using Avalonia.Markup.Xaml;
#pragma warning disable 4014
@@ -112,11 +113,29 @@ List GetFilters()
private Window CreateSampleWindow()
{
- var window = new Window();
- window.Height = 200;
- window.Width = 200;
- window.Content = new TextBlock { Text = "Hello world!" };
- window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ Button button;
+
+ var window = new Window
+ {
+ Height = 200,
+ Width = 200,
+ Content = new StackPanel
+ {
+ Spacing = 4,
+ Children =
+ {
+ new TextBlock { Text = "Hello world!" },
+ (button = new Button
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ Content = "Click to close"
+ })
+ }
+ },
+ WindowStartupLocation = WindowStartupLocation.CenterOwner
+ };
+
+ button.Click += (_, __) => window.Close();
return window;
}
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
new file mode 100644
index 00000000000..c4d0bc3e67e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
new file mode 100644
index 00000000000..0803d178b9b
--- /dev/null
+++ b/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("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("ButtonFlyoutXamlText");
+ bfxt.Text = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n";
+
+ var mfxt = this.FindControl("MenuFlyoutXamlText");
+ mfxt.Text = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n";
+
+ var afxt = this.FindControl("AttachedFlyoutXamlText");
+ afxt.Text = "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ "\n\n In DoubleTapped handler:\n" +
+ "FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);";
+
+ var sfxt = this.FindControl("SharedFlyoutXamlText");
+ sfxt.Text = "Declare a flyout in Resources:\n" +
+ "\n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n\n\n" +
+ "Then attach the flyout where you want it:\n" +
+ "";
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/LabelsPage.axaml b/samples/ControlCatalog/Pages/LabelsPage.axaml
new file mode 100644
index 00000000000..961e7ba7e6e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/LabelsPage.axaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+ Save
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/LabelsPage.axaml.cs b/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
new file mode 100644
index 00000000000..a14978fd2c9
--- /dev/null
+++ b/samples/ControlCatalog/Pages/LabelsPage.axaml.cs
@@ -0,0 +1,42 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.Models;
+
+namespace ControlCatalog.Pages
+{
+ public class LabelsPage : UserControl
+ {
+ private Person _person;
+
+ public LabelsPage()
+ {
+ CreateDefaultPerson();
+ this.InitializeComponent();
+ }
+
+ private void CreateDefaultPerson()
+ {
+ DataContext = _person = new Person
+ {
+ FirstName = "John",
+ LastName = "Doe",
+ IsBanned = true,
+ };
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public void DoSave()
+ {
+
+ }
+ public void DoCancel()
+ {
+ CreateDefaultPerson();
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index 3521ad71a97..f515db84d40 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -20,6 +20,6 @@
+ SelectionMode="{Binding SelectionMode^}"/>
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
index 46dbe3dcada..5999510b6c8 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
@@ -6,7 +6,6 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
-using ReactiveUI;
namespace ControlCatalog.Pages
{
diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
index 0d7e5da17fa..2f422e2d64b 100644
--- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
+++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml
@@ -1,7 +1,9 @@
-
+
Numeric up-down control
Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.
@@ -67,15 +69,36 @@
+
-
- Usage of NumericUpDown:
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
index 92da64d87ee..5856194430f 100644
--- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
@@ -6,7 +6,7 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.Pages
{
@@ -26,17 +26,32 @@ private void InitializeComponent()
}
- public class NumbersPageViewModel : ReactiveObject
+ public class NumbersPageViewModel : ViewModelBase
{
private IList _formats;
private FormatObject _selectedFormat;
private IList _spinnerLocations;
+ private double _doubleValue;
+ private decimal _decimalValue;
+
public NumbersPageViewModel()
{
SelectedFormat = Formats.FirstOrDefault();
}
+ public double DoubleValue
+ {
+ get { return _doubleValue; }
+ set { this.RaiseAndSetIfChanged(ref _doubleValue, value); }
+ }
+
+ public decimal DecimalValue
+ {
+ get { return _decimalValue; }
+ set { this.RaiseAndSetIfChanged(ref _decimalValue, value); }
+ }
+
public IList Formats
{
get
diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
index 2ec0b48c76b..da8ef6cf078 100644
--- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml
+++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml
@@ -15,6 +15,13 @@
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml b/samples/ControlCatalog/Pages/RelativePanelPage.axaml
index 3657d52bd9d..4dcdcc195fa 100644
--- a/samples/ControlCatalog/Pages/RelativePanelPage.axaml
+++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml
@@ -23,16 +23,16 @@
-
+
-
+
-
+
@@ -41,19 +41,19 @@
-
+
-
+
-
+
-
+
-
+
diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs
index c39f414b446..4edb0f137ad 100644
--- a/samples/ControlCatalog/Pages/ScreenPage.cs
+++ b/samples/ControlCatalog/Pages/ScreenPage.cs
@@ -29,7 +29,7 @@ public override void Render(DrawingContext context)
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;
- var drawBrush = Brushes.Green;
+ var drawBrush = Brushes.Black;
Pen p = new Pen(drawBrush);
if (screens != null)
foreach (Screen screen in screens)
@@ -45,18 +45,16 @@ public override void Render(DrawingContext context)
screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f);
+
context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect);
-
- FormattedText text = new FormattedText()
- {
- Typeface = Typeface.Default
- };
- text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}";
+ var text = new FormattedText() { Typeface = new Typeface("Arial"), FontSize = 18 };
+
+ text.Text = $"Bounds: {screen.Bounds.TopLeft} {screen.Bounds.Width}:{screen.Bounds.Height}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text);
- text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
+ text.Text = $"WorkArea: {screen.WorkingArea.TopLeft} {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
text.Text = $"Scaling: {screen.PixelDensity * 100}%";
@@ -69,7 +67,7 @@ public override void Render(DrawingContext context)
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text);
}
- context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10));
+ context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
}
}
}
diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
index 36d3768b131..dcd7a88a56a 100644
--- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
@@ -2,11 +2,11 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Markup.Xaml;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.Pages
{
- public class ScrollViewerPageViewModel : ReactiveObject
+ public class ScrollViewerPageViewModel : ViewModelBase
{
private bool _allowAutoHide;
private ScrollBarVisibility _horizontalScrollVisibility;
diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml
index ea31ed00502..b4901ec780d 100644
--- a/samples/ControlCatalog/Pages/SliderPage.xaml
+++ b/samples/ControlCatalog/Pages/SliderPage.xaml
@@ -1,5 +1,6 @@
Slider
@@ -21,6 +22,35 @@
IsSnapToTickEnabled="True"
Ticks="0,20,25,40,75,100"
Width="300" />
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs
index a38a3ab4cb8..f49b13091b2 100644
--- a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs
@@ -6,7 +6,7 @@
using Avalonia.Media.Imaging;
using Avalonia.Platform;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.Pages
{
@@ -56,7 +56,7 @@ private IBitmap LoadBitmap(string uri)
return new Bitmap(assets.Open(new Uri(uri)));
}
- private class PageViewModel : ReactiveObject
+ private class PageViewModel : ViewModelBase
{
private Dock _tabPlacement;
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index 4a1c196917a..d4f72f161a1 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -18,8 +18,8 @@
-
-
+
+
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml
index 481a459159a..1ac447ea693 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml
@@ -1,9 +1,10 @@
+ x:Class="ControlCatalog.Pages.TextBoxPage"
+ xmlns:sys="clr-namespace:System;assembly=netstandard">
- TextBox
- A control into which the user can input text
+
+
-
-
+
+
+
+
+
+
+
+
-
+
-
- resm fonts
-
-
-
-
-
-
-
- res fonts
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs b/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
index cd5f7903127..9eeefebb023 100644
--- a/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
@@ -13,6 +13,12 @@ public TextBoxPage()
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
+
+ this.Get("numericWatermark")
+ .TextInputOptionsQuery += (s, a) =>
+ {
+ a.ContentType = Avalonia.Input.TextInput.TextInputContentType.Number;
+ };
}
}
}
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index ec073d48a9b..de23c7a169d 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -6,7 +6,7 @@
ToolTip
A control which pops up a hint when a control is hovered
-
@@ -38,6 +38,31 @@
ToolTip bottom placement
+
+
+
+
+ Moving offset
+
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml b/samples/ControlCatalog/Pages/ViewboxPage.xaml
index e78cf2bc224..ef802db33ee 100644
--- a/samples/ControlCatalog/Pages/ViewboxPage.xaml
+++ b/samples/ControlCatalog/Pages/ViewboxPage.xaml
@@ -1,66 +1,36 @@
-
-
- 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
-
-
-
+
Viewbox
A control used to scale single child.
-
- None
- Fill
- Uniform
- UniformToFill
-
- Hello World!
-
-
- Hello World!
-
-
- Hello World!
-
-
- Hello World!
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs b/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
index 1b5f4bc7f46..94b3f3ea14c 100644
--- a/samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
+++ b/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 @@ public class ViewboxPage : UserControl
{
public ViewboxPage()
{
- this.InitializeComponent();
+ InitializeComponent();
+
+ var stretchSelector = this.FindControl("StretchSelector");
+
+ stretchSelector.Items = new[]
+ {
+ Stretch.Uniform, Stretch.UniformToFill, Stretch.Fill, Stretch.None
+ };
+
+ stretchSelector.SelectedIndex = 0;
+
+ var stretchDirectionSelector = this.FindControl("StretchDirectionSelector");
+
+ stretchDirectionSelector.Items = new[]
+ {
+ StretchDirection.Both, StretchDirection.DownOnly, StretchDirection.UpOnly
+ };
+
+ stretchDirectionSelector.SelectedIndex = 0;
}
private void InitializeComponent()
diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml
index 7c911e91e9e..2b5215a3feb 100644
--- a/samples/ControlCatalog/SideBar.xaml
+++ b/samples/ControlCatalog/SideBar.xaml
@@ -16,7 +16,6 @@
diff --git a/samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
similarity index 80%
rename from samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs
rename to samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
index 5c2f74d2d5f..b46ffeb0074 100644
--- a/samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/ContextPageViewModel.cs
@@ -3,18 +3,18 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.VisualTree;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
- public class ContextMenuPageViewModel
+ public class ContextPageViewModel
{
public Control View { get; set; }
- public ContextMenuPageViewModel()
+ public ContextPageViewModel()
{
- OpenCommand = ReactiveCommand.CreateFromTask(Open);
- SaveCommand = ReactiveCommand.Create(Save);
- OpenRecentCommand = ReactiveCommand.Create(OpenRecent);
+ OpenCommand = MiniCommand.CreateFromTask(Open);
+ SaveCommand = MiniCommand.Create(Save);
+ OpenRecentCommand = MiniCommand.Create(OpenRecent);
MenuItems = new[]
{
@@ -44,9 +44,9 @@ public ContextMenuPageViewModel()
}
public IReadOnlyList MenuItems { get; set; }
- public ReactiveCommand OpenCommand { get; }
- public ReactiveCommand SaveCommand { get; }
- public ReactiveCommand OpenRecentCommand { get; }
+ public MiniCommand OpenCommand { get; }
+ public MiniCommand SaveCommand { get; }
+ public MiniCommand OpenRecentCommand { get; }
public async Task Open()
{
diff --git a/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
new file mode 100644
index 00000000000..f1cc0637dc6
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/CursorPageViewModel.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia;
+using Avalonia.Input;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+ public class CursorPageViewModel : ViewModelBase
+ {
+ public CursorPageViewModel()
+ {
+ StandardCursors = Enum.GetValues(typeof(StandardCursorType))
+ .Cast()
+ .Select(x => new StandardCursorModel(x))
+ .ToList();
+
+ var loader = AvaloniaLocator.Current.GetService();
+ var s = loader.Open(new Uri("avares://ControlCatalog/Assets/avalonia-32.png"));
+ var bitmap = new Bitmap(s);
+ CustomCursor = new Cursor(bitmap, new PixelPoint(16, 16));
+ }
+
+ public IEnumerable StandardCursors { get; }
+
+ public Cursor CustomCursor { get; }
+
+ public class StandardCursorModel
+ {
+ public StandardCursorModel(StandardCursorType type)
+ {
+ Type = type;
+ Cursor = new Cursor(type);
+ }
+
+ public StandardCursorType Type { get; }
+
+ public Cursor Cursor { get; }
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
index f893a6e28e9..ee1fa6ae777 100644
--- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
@@ -2,11 +2,11 @@
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Media;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
- public class ItemsRepeaterPageViewModel : ReactiveObject
+ public class ItemsRepeaterPageViewModel : ViewModelBase
{
private int _newItemIndex = 1;
private int _newGenerationIndex = 0;
@@ -59,7 +59,7 @@ private ObservableCollection- CreateItems()
}));
}
- public class Item : ReactiveObject
+ public class Item : ViewModelBase
{
private double _height = double.NaN;
diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
index f75bc32105e..7f2d6e95727 100644
--- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
@@ -4,18 +4,18 @@
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Controls.Selection;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
- public class ListBoxPageViewModel : ReactiveObject
+ public class ListBoxPageViewModel : ViewModelBase
{
private bool _multiple;
private bool _toggle;
private bool _alwaysSelected;
private bool _autoScrollToSelectedItem = true;
private int _counter;
- private ObservableAsPropertyHelper _selectionMode;
+ private IObservable _selectionMode;
public ListBoxPageViewModel()
{
@@ -29,14 +29,13 @@ public ListBoxPageViewModel()
x => x.Toggle,
x => x.AlwaysSelected,
(m, t, a) =>
- (m ? SelectionMode.Multiple : 0) |
- (t ? SelectionMode.Toggle : 0) |
- (a ? SelectionMode.AlwaysSelected : 0))
- .ToProperty(this, x => x.SelectionMode);
+ (m ? Avalonia.Controls.SelectionMode.Multiple : 0) |
+ (t ? Avalonia.Controls.SelectionMode.Toggle : 0) |
+ (a ? Avalonia.Controls.SelectionMode.AlwaysSelected : 0));
- AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
+ AddItemCommand = MiniCommand.Create(() => Items.Add(GenerateItem()));
- RemoveItemCommand = ReactiveCommand.Create(() =>
+ RemoveItemCommand = MiniCommand.Create(() =>
{
var items = Selection.SelectedItems.ToList();
@@ -46,7 +45,7 @@ public ListBoxPageViewModel()
}
});
- SelectRandomItemCommand = ReactiveCommand.Create(() =>
+ SelectRandomItemCommand = MiniCommand.Create(() =>
{
var random = new Random();
@@ -60,7 +59,7 @@ public ListBoxPageViewModel()
public ObservableCollection Items { get; }
public SelectionModel Selection { get; }
- public SelectionMode SelectionMode => _selectionMode.Value;
+ public IObservable SelectionMode => _selectionMode;
public bool Multiple
{
@@ -86,9 +85,9 @@ public bool AutoScrollToSelectedItem
set => this.RaiseAndSetIfChanged(ref _autoScrollToSelectedItem, value);
}
- public ReactiveCommand AddItemCommand { get; }
- public ReactiveCommand RemoveItemCommand { get; }
- public ReactiveCommand SelectRandomItemCommand { get; }
+ public MiniCommand AddItemCommand { get; }
+ public MiniCommand RemoveItemCommand { get; }
+ public MiniCommand SelectRandomItemCommand { get; }
private string GenerateItem() => $"Item {_counter++.ToString()}";
}
diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
index 4356a032fad..4b3cfa9c9d0 100644
--- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
@@ -5,11 +5,11 @@
using Avalonia.Dialogs;
using Avalonia.Platform;
using System;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
- class MainWindowViewModel : ReactiveObject
+ class MainWindowViewModel : ViewModelBase
{
private IManagedNotificationManager _notificationManager;
@@ -27,22 +27,22 @@ public MainWindowViewModel(IManagedNotificationManager notificationManager)
{
_notificationManager = notificationManager;
- ShowCustomManagedNotificationCommand = ReactiveCommand.Create(() =>
+ ShowCustomManagedNotificationCommand = MiniCommand.Create(() =>
{
NotificationManager.Show(new NotificationViewModel(NotificationManager) { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" });
});
- ShowManagedNotificationCommand = ReactiveCommand.Create(() =>
+ ShowManagedNotificationCommand = MiniCommand.Create(() =>
{
NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Welcome", "Avalonia now supports Notifications.", NotificationType.Information));
});
- ShowNativeNotificationCommand = ReactiveCommand.Create(() =>
+ ShowNativeNotificationCommand = MiniCommand.Create(() =>
{
NotificationManager.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error));
});
- AboutCommand = ReactiveCommand.CreateFromTask(async () =>
+ AboutCommand = MiniCommand.CreateFromTask(async () =>
{
var dialog = new AboutAvaloniaDialog();
@@ -51,12 +51,12 @@ public MainWindowViewModel(IManagedNotificationManager notificationManager)
await dialog.ShowDialog(mainWindow);
});
- ExitCommand = ReactiveCommand.Create(() =>
+ ExitCommand = MiniCommand.Create(() =>
{
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
});
- ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
+ ToggleMenuItemCheckedCommand = MiniCommand.Create(() =>
{
IsMenuItemChecked = !IsMenuItemChecked;
});
@@ -153,16 +153,16 @@ public bool IsMenuItemChecked
set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); }
}
- public ReactiveCommand ShowCustomManagedNotificationCommand { get; }
+ public MiniCommand ShowCustomManagedNotificationCommand { get; }
- public ReactiveCommand ShowManagedNotificationCommand { get; }
+ public MiniCommand ShowManagedNotificationCommand { get; }
- public ReactiveCommand ShowNativeNotificationCommand { get; }
+ public MiniCommand ShowNativeNotificationCommand { get; }
- public ReactiveCommand AboutCommand { get; }
+ public MiniCommand AboutCommand { get; }
- public ReactiveCommand ExitCommand { get; }
+ public MiniCommand ExitCommand { get; }
- public ReactiveCommand ToggleMenuItemCheckedCommand { get; }
+ public MiniCommand ToggleMenuItemCheckedCommand { get; }
}
}
diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
index 9e7ae8b716f..ecbd59c5d73 100644
--- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
@@ -4,7 +4,7 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.VisualTree;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
@@ -13,9 +13,9 @@ public class MenuPageViewModel
public Control View { get; set; }
public MenuPageViewModel()
{
- OpenCommand = ReactiveCommand.CreateFromTask(Open);
- SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false));
- OpenRecentCommand = ReactiveCommand.Create(OpenRecent);
+ OpenCommand = MiniCommand.CreateFromTask(Open);
+ SaveCommand = MiniCommand.Create(Save);
+ OpenRecentCommand = MiniCommand.Create(OpenRecent);
var recentItems = new[]
{
@@ -65,9 +65,9 @@ public MenuPageViewModel()
public IReadOnlyList MenuItems { get; set; }
public IReadOnlyList RecentItems { get; set; }
- public ReactiveCommand OpenCommand { get; }
- public ReactiveCommand SaveCommand { get; }
- public ReactiveCommand OpenRecentCommand { get; }
+ public MiniCommand OpenCommand { get; }
+ public MiniCommand SaveCommand { get; }
+ public MiniCommand OpenRecentCommand { get; }
public async Task Open()
{
diff --git a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs
index 8724ba344b1..2052481015c 100644
--- a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs
@@ -1,6 +1,6 @@
using System.Reactive;
using Avalonia.Controls.Notifications;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
@@ -8,12 +8,12 @@ public class NotificationViewModel
{
public NotificationViewModel(INotificationManager manager)
{
- YesCommand = ReactiveCommand.Create(() =>
+ YesCommand = MiniCommand.Create(() =>
{
manager.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today."));
});
- NoCommand = ReactiveCommand.Create(() =>
+ NoCommand = MiniCommand.Create(() =>
{
manager.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today. To find out more visit..."));
});
@@ -22,9 +22,9 @@ public NotificationViewModel(INotificationManager manager)
public string Title { get; set; }
public string Message { get; set; }
- public ReactiveCommand YesCommand { get; }
+ public MiniCommand YesCommand { get; }
- public ReactiveCommand NoCommand { get; }
+ public MiniCommand NoCommand { get; }
}
}
diff --git a/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
index f27f605a8b5..9e6932bb76a 100644
--- a/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs
@@ -1,10 +1,10 @@
using System;
using Avalonia.Controls;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
- public class SplitViewPageViewModel : ReactiveObject
+ public class SplitViewPageViewModel : ViewModelBase
{
private bool _isLeft = true;
private int _displayMode = 3; //CompactOverlay
diff --git a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
index 210e281ed67..c03379330fb 100644
--- a/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
+++ b/samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
@@ -3,11 +3,11 @@
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
-using ReactiveUI;
+using MiniMvvm;
namespace ControlCatalog.ViewModels
{
- public class TreeViewPageViewModel : ReactiveObject
+ public class TreeViewPageViewModel : ViewModelBase
{
private readonly Node _root;
private SelectionMode _selectionMode;
@@ -19,16 +19,16 @@ public TreeViewPageViewModel()
Items = _root.Children;
SelectedItems = new ObservableCollection();
- AddItemCommand = ReactiveCommand.Create(AddItem);
- RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
- SelectRandomItemCommand = ReactiveCommand.Create(SelectRandomItem);
+ AddItemCommand = MiniCommand.Create(AddItem);
+ RemoveItemCommand = MiniCommand.Create(RemoveItem);
+ SelectRandomItemCommand = MiniCommand.Create(SelectRandomItem);
}
public ObservableCollection Items { get; }
public ObservableCollection SelectedItems { get; }
- public ReactiveCommand AddItemCommand { get; }
- public ReactiveCommand RemoveItemCommand { get; }
- public ReactiveCommand SelectRandomItemCommand { get; }
+ public MiniCommand AddItemCommand { get; }
+ public MiniCommand RemoveItemCommand { get; }
+ public MiniCommand SelectRandomItemCommand { get; }
public SelectionMode SelectionMode
{
diff --git a/samples/MiniMvvm/MiniCommand.cs b/samples/MiniMvvm/MiniCommand.cs
new file mode 100644
index 00000000000..c6a9273c201
--- /dev/null
+++ b/samples/MiniMvvm/MiniCommand.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows.Input;
+
+namespace MiniMvvm
+{
+ public sealed class MiniCommand : MiniCommand, ICommand
+ {
+ private readonly Action _cb;
+ private bool _busy;
+ private Func _acb;
+
+ public MiniCommand(Action cb)
+ {
+ _cb = cb;
+ }
+
+ public MiniCommand(Func cb)
+ {
+ _acb = cb;
+ }
+
+ private bool Busy
+ {
+ get => _busy;
+ set
+ {
+ _busy = value;
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+
+ public override event EventHandler CanExecuteChanged;
+ public override bool CanExecute(object parameter) => !_busy;
+
+ public override async void Execute(object parameter)
+ {
+ if(Busy)
+ return;
+ try
+ {
+ Busy = true;
+ if (_cb != null)
+ _cb((T)parameter);
+ else
+ await _acb((T)parameter);
+ }
+ finally
+ {
+ Busy = false;
+ }
+ }
+ }
+
+ public abstract class MiniCommand : ICommand
+ {
+ public static MiniCommand Create(Action cb) => new MiniCommand(_ => cb());
+ public static MiniCommand Create(Action cb) => new MiniCommand(cb);
+ public static MiniCommand CreateFromTask(Func cb) => new MiniCommand(_ => cb());
+
+ public abstract bool CanExecute(object parameter);
+ public abstract void Execute(object parameter);
+ public abstract event EventHandler CanExecuteChanged;
+ }
+}
diff --git a/samples/MiniMvvm/MiniMvvm.csproj b/samples/MiniMvvm/MiniMvvm.csproj
new file mode 100644
index 00000000000..6535b2bdbd0
--- /dev/null
+++ b/samples/MiniMvvm/MiniMvvm.csproj
@@ -0,0 +1,6 @@
+
+
+ netstandard2.0
+
+
+
diff --git a/samples/MiniMvvm/PropertyChangedExtensions.cs b/samples/MiniMvvm/PropertyChangedExtensions.cs
new file mode 100644
index 00000000000..f1065c75303
--- /dev/null
+++ b/samples/MiniMvvm/PropertyChangedExtensions.cs
@@ -0,0 +1,108 @@
+using System;
+using System.ComponentModel;
+using System.Linq.Expressions;
+using System.Reactive.Linq;
+using System.Reflection;
+
+namespace MiniMvvm
+{
+ public static class PropertyChangedExtensions
+ {
+ class PropertyObservable : IObservable
+ {
+ private readonly INotifyPropertyChanged _target;
+ private readonly PropertyInfo _info;
+
+ public PropertyObservable(INotifyPropertyChanged target, PropertyInfo info)
+ {
+ _target = target;
+ _info = info;
+ }
+
+ class Subscription : IDisposable
+ {
+ private readonly INotifyPropertyChanged _target;
+ private readonly PropertyInfo _info;
+ private readonly IObserver _observer;
+
+ public Subscription(INotifyPropertyChanged target, PropertyInfo info, IObserver observer)
+ {
+ _target = target;
+ _info = info;
+ _observer = observer;
+ _target.PropertyChanged += OnPropertyChanged;
+ _observer.OnNext((T)_info.GetValue(_target));
+ }
+
+ private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == _info.Name)
+ _observer.OnNext((T)_info.GetValue(_target));
+ }
+
+ public void Dispose()
+ {
+ _target.PropertyChanged -= OnPropertyChanged;
+ _observer.OnCompleted();
+ }
+ }
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ return new Subscription(_target, _info, observer);
+ }
+ }
+
+ public static IObservable WhenAnyValue(this TModel model,
+ Expression> expr) where TModel : INotifyPropertyChanged
+ {
+ var l = (LambdaExpression)expr;
+ var ma = (MemberExpression)l.Body;
+ var prop = (PropertyInfo)ma.Member;
+ return new PropertyObservable(model, prop);
+ }
+
+ public static IObservable WhenAnyValue(this TModel model,
+ Expression> v1,
+ Func cb
+ ) where TModel : INotifyPropertyChanged
+ {
+ return model.WhenAnyValue(v1).Select(cb);
+ }
+
+ public static IObservable WhenAnyValue(this TModel model,
+ Expression> v1,
+ Expression> v2,
+ Func cb
+ ) where TModel : INotifyPropertyChanged =>
+ Observable.CombineLatest(
+ model.WhenAnyValue(v1),
+ model.WhenAnyValue(v2),
+ cb);
+
+ public static IObservable> WhenAnyValue(this TModel model,
+ Expression> v1,
+ Expression> v2
+ ) where TModel : INotifyPropertyChanged =>
+ model.WhenAnyValue(v1, v2, (a1, a2) => (a1, a2));
+
+ public static IObservable WhenAnyValue(this TModel model,
+ Expression> v1,
+ Expression> v2,
+ Expression> v3,
+ Func cb
+ ) where TModel : INotifyPropertyChanged =>
+ Observable.CombineLatest(
+ model.WhenAnyValue(v1),
+ model.WhenAnyValue(v2),
+ model.WhenAnyValue(v3),
+ cb);
+
+ public static IObservable> WhenAnyValue(this TModel model,
+ Expression> v1,
+ Expression> v2,
+ Expression> v3
+ ) where TModel : INotifyPropertyChanged =>
+ model.WhenAnyValue(v1, v2, v3, (a1, a2, a3) => (a1, a2, a3));
+ }
+}
diff --git a/samples/MiniMvvm/ViewModelBase.cs b/samples/MiniMvvm/ViewModelBase.cs
new file mode 100644
index 00000000000..7256b05ceff
--- /dev/null
+++ b/samples/MiniMvvm/ViewModelBase.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reactive.Joins;
+using System.Runtime.CompilerServices;
+
+namespace MiniMvvm
+{
+ public class ViewModelBase : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null)
+ {
+ if (!EqualityComparer.Default.Equals(field, value))
+ {
+ field = value;
+ RaisePropertyChanged(propertyName);
+ return true;
+ }
+ return false;
+ }
+
+
+ protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/samples/Previewer/App.xaml.cs b/samples/Previewer/App.xaml.cs
index fffa987a278..ab83d45cd3d 100644
--- a/samples/Previewer/App.xaml.cs
+++ b/samples/Previewer/App.xaml.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Previewer
@@ -9,6 +10,13 @@ public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/samples/Previewer/Previewer.csproj b/samples/Previewer/Previewer.csproj
index cd3daf61e1e..cfedb7ad9ed 100644
--- a/samples/Previewer/Previewer.csproj
+++ b/samples/Previewer/Previewer.csproj
@@ -8,7 +8,6 @@
%(Filename)
-
diff --git a/samples/Previewer/Program.cs b/samples/Previewer/Program.cs
index 48363e27f2d..b12b93974a5 100644
--- a/samples/Previewer/Program.cs
+++ b/samples/Previewer/Program.cs
@@ -1,13 +1,14 @@
-using System;
-using Avalonia;
+using Avalonia;
namespace Previewer
{
class Program
{
- static void Main(string[] args)
- {
- AppBuilder.Configure().UsePlatformDetect().Start();
- }
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect();
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
-}
\ No newline at end of file
+}
diff --git a/samples/RenderDemo/App.xaml b/samples/RenderDemo/App.xaml
index 61e4d2385b1..7cdcea2e1de 100644
--- a/samples/RenderDemo/App.xaml
+++ b/samples/RenderDemo/App.xaml
@@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.App">
-
+
diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs
index 233160b0252..8054b069648 100644
--- a/samples/RenderDemo/App.xaml.cs
+++ b/samples/RenderDemo/App.xaml.cs
@@ -1,6 +1,6 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
-using Avalonia.ReactiveUI;
namespace RenderDemo
{
@@ -11,15 +11,26 @@ public override void Initialize()
AvaloniaXamlLoader.Load(this);
}
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
+
// TODO: Make this work with GTK/Skia/Cairo depending on command-line args
// again.
- static void Main(string[] args) => BuildAvaloniaApp().Start();
+ static void Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// App configuration, used by the entry point and previewer
static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
+ .With(new Win32PlatformOptions
+ {
+ OverlayPopups = true,
+ })
.UsePlatformDetect()
- .UseReactiveUI()
- .LogToDebug();
+ .LogToTrace();
}
}
diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml
index 93fbe5e412a..f37df56b737 100644
--- a/samples/RenderDemo/MainWindow.xaml
+++ b/samples/RenderDemo/MainWindow.xaml
@@ -36,6 +36,9 @@
+
+
+
@@ -57,6 +60,9 @@
+
+
+
diff --git a/samples/RenderDemo/MainWindow.xaml.cs b/samples/RenderDemo/MainWindow.xaml.cs
index b45a605e04d..877eb8016aa 100644
--- a/samples/RenderDemo/MainWindow.xaml.cs
+++ b/samples/RenderDemo/MainWindow.xaml.cs
@@ -3,7 +3,7 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using RenderDemo.ViewModels;
-using ReactiveUI;
+using MiniMvvm;
namespace RenderDemo
{
diff --git a/samples/RenderDemo/Pages/AnimationsPage.xaml b/samples/RenderDemo/Pages/AnimationsPage.xaml
index 12fb31ea59e..9043bac33e7 100644
--- a/samples/RenderDemo/Pages/AnimationsPage.xaml
+++ b/samples/RenderDemo/Pages/AnimationsPage.xaml
@@ -1,7 +1,8 @@
+ x:Class="RenderDemo.Pages.AnimationsPage"
+ MaxWidth="600">
@@ -160,6 +161,151 @@
+
+
+
+
+
+
+
+
@@ -167,8 +313,8 @@
-
- Hover to activate Transform Keyframe Animations.
+
+ Hover to activate Keyframe Animations.
@@ -180,6 +326,10 @@
+
+
+
+
diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml
new file mode 100644
index 00000000000..b386636caed
--- /dev/null
+++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs
new file mode 100644
index 00000000000..eed8ee29cea
--- /dev/null
+++ b/samples/RenderDemo/Pages/CustomAnimatorPage.xaml.cs
@@ -0,0 +1,27 @@
+using System.Reactive.Linq;
+using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using RenderDemo.ViewModels;
+
+namespace RenderDemo.Pages
+{
+ public class CustomAnimatorPage : UserControl
+ {
+ public CustomAnimatorPage()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/CustomStringAnimator.cs b/samples/RenderDemo/Pages/CustomStringAnimator.cs
new file mode 100644
index 00000000000..851a2d01877
--- /dev/null
+++ b/samples/RenderDemo/Pages/CustomStringAnimator.cs
@@ -0,0 +1,16 @@
+using Avalonia.Animation.Animators;
+
+namespace RenderDemo.Pages
+{
+ public class CustomStringAnimator : Animator
+ {
+ public override string Interpolate(double progress, string oldValue, string newValue)
+ {
+ if (newValue.Length == 0) return "";
+ var step = 1.0 / newValue.Length;
+ var length = (int)(progress / step);
+ var result = newValue.Substring(0, length + 1);
+ return result;
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml
index fb3e318a0ed..c2914e88475 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml
@@ -6,9 +6,9 @@
x:Class="RenderDemo.Pages.GlyphRunPage">
-
-
+
diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
index ddee880288b..857358f6b26 100644
--- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
+++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
@@ -9,7 +9,7 @@ namespace RenderDemo.Pages
{
public class GlyphRunPage : UserControl
{
- private DrawingPresenter _drawingPresenter;
+ private Image _imageControl;
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1];
@@ -25,7 +25,8 @@ private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
- _drawingPresenter = this.FindControl("drawingPresenter");
+ _imageControl = this.FindControl("imageControl");
+ _imageControl.Source = new DrawingImage();
DispatcherTimer.Run(() =>
{
@@ -73,7 +74,7 @@ private void UpdateGlyphRun()
drawingGroup.Children.Add(geometryDrawing);
- _drawingPresenter.Drawing = drawingGroup;
+ (_imageControl.Source as DrawingImage).Drawing = drawingGroup;
}
}
}
diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs
new file mode 100644
index 00000000000..212377deae6
--- /dev/null
+++ b/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(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);
+ }
+ }
+}
diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml
index d6da293ff35..71b6ea0713a 100644
--- a/samples/RenderDemo/Pages/TransitionsPage.xaml
+++ b/samples/RenderDemo/Pages/TransitionsPage.xaml
@@ -1,7 +1,8 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ x:Class="RenderDemo.Pages.TransitionsPage"
+ MaxWidth="600">
@@ -90,6 +91,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -98,8 +249,8 @@
-
- Hover to activate Transform Keyframe Animations.
+
+ Hover to activate Transitions.
@@ -109,6 +260,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/RenderDemo.csproj b/samples/RenderDemo/RenderDemo.csproj
index d1654f4b54d..0d33b4c1116 100644
--- a/samples/RenderDemo/RenderDemo.csproj
+++ b/samples/RenderDemo/RenderDemo.csproj
@@ -9,12 +9,11 @@
-
+
-
diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml
index fd23067f611..b82a7b0514a 100644
--- a/samples/RenderDemo/SideBar.xaml
+++ b/samples/RenderDemo/SideBar.xaml
@@ -7,7 +7,6 @@
diff --git a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
index 7b89b7944c2..f8ba01f3d1e 100644
--- a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
+++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
@@ -1,10 +1,10 @@
using System;
-using ReactiveUI;
+using MiniMvvm;
using Avalonia.Animation;
namespace RenderDemo.ViewModels
{
- public class AnimationsPageViewModel : ReactiveObject
+ public class AnimationsPageViewModel : ViewModelBase
{
private bool _isPlaying = true;
diff --git a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs
index eda5e80530b..19917c20dfe 100644
--- a/samples/RenderDemo/ViewModels/MainWindowViewModel.cs
+++ b/samples/RenderDemo/ViewModels/MainWindowViewModel.cs
@@ -1,11 +1,10 @@
using System.Reactive;
using System.Threading.Tasks;
-
-using ReactiveUI;
+using MiniMvvm;
namespace RenderDemo.ViewModels
{
- public class MainWindowViewModel : ReactiveObject
+ public class MainWindowViewModel : ViewModelBase
{
private bool drawDirtyRects = false;
private bool drawFps = true;
@@ -14,9 +13,9 @@ public class MainWindowViewModel : ReactiveObject
public MainWindowViewModel()
{
- ToggleDrawDirtyRects = ReactiveCommand.Create(() => DrawDirtyRects = !DrawDirtyRects);
- ToggleDrawFps = ReactiveCommand.Create(() => DrawFps = !DrawFps);
- ResizeWindow = ReactiveCommand.CreateFromTask(ResizeWindowAsync);
+ ToggleDrawDirtyRects = MiniCommand.Create(() => DrawDirtyRects = !DrawDirtyRects);
+ ToggleDrawFps = MiniCommand.Create(() => DrawFps = !DrawFps);
+ ResizeWindow = MiniCommand.CreateFromTask(ResizeWindowAsync);
}
public bool DrawDirtyRects
@@ -43,9 +42,9 @@ public double Height
set => this.RaiseAndSetIfChanged(ref height, value);
}
- public ReactiveCommand ToggleDrawDirtyRects { get; }
- public ReactiveCommand ToggleDrawFps { get; }
- public ReactiveCommand ResizeWindow { get; }
+ public MiniCommand ToggleDrawDirtyRects { get; }
+ public MiniCommand ToggleDrawFps { get; }
+ public MiniCommand ResizeWindow { get; }
private async Task ResizeWindowAsync()
{
diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml
index 699781eb946..f601f9f78fd 100644
--- a/samples/Sandbox/App.axaml
+++ b/samples/Sandbox/App.axaml
@@ -3,6 +3,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sandbox.App">
-
+
diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs
index b7222e043d6..3d54036d29d 100644
--- a/samples/Sandbox/MainWindow.axaml.cs
+++ b/samples/Sandbox/MainWindow.axaml.cs
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.Win32.WinRT.Composition;
namespace Sandbox
{
diff --git a/samples/Sandbox/Program.cs b/samples/Sandbox/Program.cs
index 4d7eda8d9fa..52321b46a93 100644
--- a/samples/Sandbox/Program.cs
+++ b/samples/Sandbox/Program.cs
@@ -1,17 +1,15 @@
using Avalonia;
-using Avalonia.ReactiveUI;
namespace Sandbox
{
public class Program
{
- static void Main(string[] args)
- {
+ static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure()
.UsePlatformDetect()
- .UseReactiveUI()
- .LogToDebug()
- .StartWithClassicDesktopLifetime(args);
- }
+ .LogToTrace();
}
}
diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj
index 1a0a8a7ce5e..0c19440a1e9 100644
--- a/samples/Sandbox/Sandbox.csproj
+++ b/samples/Sandbox/Sandbox.csproj
@@ -8,7 +8,6 @@
-
diff --git a/samples/VirtualizationDemo/App.xaml.cs b/samples/VirtualizationDemo/App.xaml.cs
index 5990dd282c4..81b80c1f402 100644
--- a/samples/VirtualizationDemo/App.xaml.cs
+++ b/samples/VirtualizationDemo/App.xaml.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace VirtualizationDemo
@@ -9,5 +10,12 @@ public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
}
}
diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs
index 93ea5e1b884..febda464501 100644
--- a/samples/VirtualizationDemo/Program.cs
+++ b/samples/VirtualizationDemo/Program.cs
@@ -1,19 +1,15 @@
-using System;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.ReactiveUI;
+using Avalonia;
namespace VirtualizationDemo
{
class Program
{
- static void Main(string[] args)
- {
- AppBuilder.Configure()
- .UsePlatformDetect()
- .UseReactiveUI()
- .LogToDebug()
- .Start();
- }
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace();
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
}
diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
index cf34980b40c..9ba505ffe53 100644
--- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
+++ b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs
@@ -1,9 +1,9 @@
using System;
-using ReactiveUI;
+using MiniMvvm;
namespace VirtualizationDemo.ViewModels
{
- internal class ItemViewModel : ReactiveObject
+ internal class ItemViewModel : ViewModelBase
{
private string _prefix;
private int _index;
diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
index 852c01399fc..514df691aed 100644
--- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
+++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
@@ -5,13 +5,13 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
-using ReactiveUI;
using Avalonia.Layout;
using Avalonia.Controls.Selection;
+using MiniMvvm;
namespace VirtualizationDemo.ViewModels
{
- internal class MainWindowViewModel : ReactiveObject
+ internal class MainWindowViewModel : ViewModelBase
{
private int _itemCount = 200;
private string _newItemString = "New Item";
@@ -26,15 +26,15 @@ internal class MainWindowViewModel : ReactiveObject
public MainWindowViewModel()
{
this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems);
- RecreateCommand = ReactiveCommand.Create(() => Recreate());
+ RecreateCommand = MiniCommand.Create(() => Recreate());
- AddItemCommand = ReactiveCommand.Create(() => AddItem());
+ AddItemCommand = MiniCommand.Create(() => AddItem());
- RemoveItemCommand = ReactiveCommand.Create(() => Remove());
+ RemoveItemCommand = MiniCommand.Create(() => Remove());
- SelectFirstCommand = ReactiveCommand.Create(() => SelectItem(0));
+ SelectFirstCommand = MiniCommand.Create(() => SelectItem(0));
- SelectLastCommand = ReactiveCommand.Create(() => SelectItem(Items.Count - 1));
+ SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1));
}
public string NewItemString
@@ -90,11 +90,11 @@ public ItemVirtualizationMode VirtualizationMode
public IEnumerable VirtualizationModes =>
Enum.GetValues(typeof(ItemVirtualizationMode)).Cast();
- public ReactiveCommand AddItemCommand { get; private set; }
- public ReactiveCommand RecreateCommand { get; private set; }
- public ReactiveCommand RemoveItemCommand { get; private set; }
- public ReactiveCommand SelectFirstCommand { get; private set; }
- public ReactiveCommand SelectLastCommand { get; private set; }
+ public MiniCommand AddItemCommand { get; private set; }
+ public MiniCommand RecreateCommand { get; private set; }
+ public MiniCommand RemoveItemCommand { get; private set; }
+ public MiniCommand SelectFirstCommand { get; private set; }
+ public MiniCommand SelectLastCommand { get; private set; }
public void RandomizeSize()
{
diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj
index 817023fd711..d898b737a98 100644
--- a/samples/VirtualizationDemo/VirtualizationDemo.csproj
+++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj
@@ -6,12 +6,11 @@
-
+
-
diff --git a/samples/interop/Direct3DInteropSample/App.paml.cs b/samples/interop/Direct3DInteropSample/App.paml.cs
index 1b6d5fd39c5..29365decfed 100644
--- a/samples/interop/Direct3DInteropSample/App.paml.cs
+++ b/samples/interop/Direct3DInteropSample/App.paml.cs
@@ -1,4 +1,5 @@
using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Direct3DInteropSample
@@ -9,5 +10,12 @@ public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.MainWindow = new MainWindow();
+ base.OnFrameworkInitializationCompleted();
+ }
}
}
diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
index bd6b6f170fd..cd9963a2e56 100644
--- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
+++ b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
@@ -22,9 +22,9 @@
-
+
diff --git a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs b/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs
index d39a21cd07a..21679a99c5d 100644
--- a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs
+++ b/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs
@@ -3,11 +3,11 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
-using ReactiveUI;
+using MiniMvvm;
namespace Direct3DInteropSample
{
- public class MainWindowViewModel : ReactiveObject
+ public class MainWindowViewModel : ViewModelBase
{
private double _rotationX;
diff --git a/samples/interop/Direct3DInteropSample/Program.cs b/samples/interop/Direct3DInteropSample/Program.cs
index 21302fa68af..bf8e76d7e47 100644
--- a/samples/interop/Direct3DInteropSample/Program.cs
+++ b/samples/interop/Direct3DInteropSample/Program.cs
@@ -1,19 +1,16 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Avalonia;
+using Avalonia;
namespace Direct3DInteropSample
{
class Program
{
- static void Main(string[] args)
- {
- AppBuilder.Configure()
- .With(new Win32PlatformOptions {UseDeferredRendering = false})
- .UseWin32().UseDirect2D1().Start();
- }
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .With(new Win32PlatformOptions { UseDeferredRendering = false })
+ .UseWin32()
+ .UseDirect2D1();
+
+ public static int Main(string[] args)
+ => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
}
diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
index cc831ef8aeb..c25442b52c1 100644
--- a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
+++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj
@@ -9,11 +9,10 @@
-
-
+
Designer
diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
index c067d385956..8394d7cb138 100644
--- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
+++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj
@@ -136,10 +136,6 @@
{42472427-4774-4c81-8aff-9f27b8e31721}
Avalonia.Layout
-
- {6417b24e-49c2-4985-8db2-3ab9d898ec91}
- Avalonia.ReactiveUI
-
{eb582467-6abb-43a1-b052-e981ba910e3a}
Avalonia.Visuals
@@ -190,4 +186,4 @@
-
\ No newline at end of file
+
diff --git a/src/Android/Avalonia.Android/ActivityTracker.cs b/src/Android/Avalonia.Android/ActivityTracker.cs
deleted file mode 100644
index 2ad1d9e3615..00000000000
--- a/src/Android/Avalonia.Android/ActivityTracker.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using Android.App;
-using Android.OS;
-
-namespace Avalonia.Android
-{
- internal class ActivityTracker : Java.Lang.Object, global::Android.App.Application.IActivityLifecycleCallbacks
- {
- public static Activity Current { get; private set; }
- public void OnActivityCreated(Activity activity, Bundle savedInstanceState)
- {
- Current = activity;
- }
-
- public void OnActivityDestroyed(Activity activity)
- {
- if (Current == activity)
- Current = null;
- }
-
- public void OnActivityPaused(Activity activity)
- {
- if (Current == activity)
- Current = null;
- }
-
- public void OnActivityResumed(Activity activity)
- {
- Current = activity;
- }
-
- public void OnActivitySaveInstanceState(Activity activity, Bundle outState)
- {
- Current = activity;
- }
-
- public void OnActivityStarted(Activity activity)
- {
- Current = activity;
- }
-
- public void OnActivityStopped(Activity activity)
- {
- if (Current == activity)
- Current = null;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs
new file mode 100644
index 00000000000..7e49cb5dfaa
--- /dev/null
+++ b/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 : 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();
+
+ _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);
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs
index 2e6f4a67c39..5e11d8eab26 100644
--- a/src/Android/Avalonia.Android/AndroidPlatform.cs
+++ b/src/Android/Avalonia.Android/AndroidPlatform.cs
@@ -1,11 +1,13 @@
using System;
+
+using Avalonia.Android;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.Input;
-using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
+using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Shared.PlatformSupport;
@@ -17,7 +19,8 @@ public static class AndroidApplicationExtensions
{
public static T UseAndroid(this T builder) where T : AppBuilderBase, new()
{
- builder.UseWindowingSubsystem(() => Android.AndroidPlatform.Initialize(builder.ApplicationType), "Android");
+ var options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions();
+ builder.UseWindowingSubsystem(() => AndroidPlatform.Initialize(builder.ApplicationType, options), "Android");
builder.UseSkia();
return builder;
}
@@ -26,50 +29,42 @@ public static class AndroidApplicationExtensions
namespace Avalonia.Android
{
- class AndroidPlatform : IPlatformSettings, IWindowingPlatform
+ class AndroidPlatform : IPlatformSettings
{
public static readonly AndroidPlatform Instance = new AndroidPlatform();
+ public static AndroidPlatformOptions Options { get; private set; }
public Size DoubleClickSize => new Size(4, 4);
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(200);
- public double RenderScalingFactor => _scalingFactor;
- public double LayoutScalingFactor => _scalingFactor;
-
- private readonly double _scalingFactor = 1;
- public AndroidPlatform()
+ public static void Initialize(Type appType, AndroidPlatformOptions options)
{
- _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity;
- }
+ Options = options;
- public static void Initialize(Type appType)
- {
AvaloniaLocator.CurrentMutable
.Bind().ToTransient()
- .Bind().ToTransient()
+ .Bind().ToTransient()
.Bind().ToSingleton()
.Bind().ToConstant(Instance)
.Bind().ToConstant(new AndroidThreadingInterface())
.Bind().ToTransient()
- .Bind().ToConstant(Instance)
.Bind().ToSingleton()
- .Bind().ToConstant(new DefaultRenderTimer(60))
+ .Bind().ToConstant(new ChoreographerTimer())
.Bind().ToConstant(new RenderLoop())
.Bind().ToSingleton()
.Bind().ToConstant(new AssetLoader(appType.Assembly));
SkiaPlatform.Initialize();
- ((global::Android.App.Application) global::Android.App.Application.Context.ApplicationContext)
- .RegisterActivityLifecycleCallbacks(new ActivityTracker());
- }
- public IWindowImpl CreateWindow()
- {
- throw new NotSupportedException();
+ if (options.UseGpu)
+ {
+ EglPlatformOpenGlInterface.TryInitialize();
+ }
}
+ }
- public IWindowImpl CreateEmbeddableWindow()
- {
- throw new NotSupportedException();
- }
+ public sealed class AndroidPlatformOptions
+ {
+ public bool UseDeferredRendering { get; set; } = true;
+ public bool UseGpu { get; set; } = true;
}
}
diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs
index 6fe77adca19..e72f0aed90c 100644
--- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs
+++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs
@@ -1,25 +1,26 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
+
using Android.OS;
+
using Avalonia.Platform;
using Avalonia.Threading;
+using App = Android.App.Application;
+
namespace Avalonia.Android
{
- class AndroidThreadingInterface : IPlatformThreadingInterface
+ internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface
{
private Handler _handler;
public AndroidThreadingInterface()
{
- _handler = new Handler(global::Android.App.Application.Context.MainLooper);
+ _handler = new Handler(App.Context.MainLooper);
}
- public void RunLoop(CancellationToken cancellationToken)
- {
- return;
- }
+ public void RunLoop(CancellationToken cancellationToken) => throw new NotSupportedException();
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
@@ -57,7 +58,7 @@ public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Ac
});
}
}, null, TimeSpan.Zero, interval);
-
+
return Disposable.Create(() =>
{
lock (l)
diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj
index c170e8449cb..8c6775733f2 100644
--- a/src/Android/Avalonia.Android/Avalonia.Android.csproj
+++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj
@@ -1,6 +1,6 @@
- monoandroid90
+ monoandroid11.0
true
diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs
index d3696aa41d0..3c9f373a661 100644
--- a/src/Android/Avalonia.Android/AvaloniaActivity.cs
+++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs
@@ -1,4 +1,3 @@
-
using Android.App;
using Android.OS;
using Android.Views;
@@ -7,18 +6,15 @@ namespace Avalonia.Android
{
public abstract class AvaloniaActivity : Activity
{
-
internal AvaloniaView View;
object _content;
protected override void OnCreate(Bundle savedInstanceState)
{
- RequestWindowFeature(WindowFeatures.NoTitle);
View = new AvaloniaView(this);
- if(_content != null)
+ if (_content != null)
View.Content = _content;
SetContentView(View);
- TakeKeyEvents(true);
base.OnCreate(savedInstanceState);
}
@@ -35,10 +31,5 @@ public object Content
View.Content = value;
}
}
-
- public override bool DispatchKeyEvent(KeyEvent e)
- {
- return View.DispatchKeyEvent(e);
- }
}
}
diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs
index 72732a1f959..8de36572830 100644
--- a/src/Android/Avalonia.Android/AvaloniaView.cs
+++ b/src/Android/Avalonia.Android/AvaloniaView.cs
@@ -1,11 +1,12 @@
using System;
using Android.Content;
+using Android.Runtime;
using Android.Views;
using Android.Widget;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
-using Avalonia.Platform;
+using Avalonia.Rendering;
namespace Avalonia.Android
{
@@ -14,6 +15,8 @@ public class AvaloniaView : FrameLayout
private readonly EmbeddableControlRoot _root;
private readonly ViewImpl _view;
+ private IDisposable? _timerSubscription;
+
public AvaloniaView(Context context) : base(context)
{
_view = new ViewImpl(context);
@@ -33,6 +36,36 @@ public override bool DispatchKeyEvent(KeyEvent e)
return _view.View.DispatchKeyEvent(e);
}
+ public override void OnVisibilityAggregated(bool isVisible)
+ {
+ base.OnVisibilityAggregated(isVisible);
+ OnVisibilityChanged(isVisible);
+ }
+
+ protected override void OnVisibilityChanged(View changedView, [GeneratedEnum] ViewStates visibility)
+ {
+ base.OnVisibilityChanged(changedView, visibility);
+ OnVisibilityChanged(visibility == ViewStates.Visible);
+ }
+
+ private void OnVisibilityChanged(bool isVisible)
+ {
+ if (isVisible)
+ {
+ if (AvaloniaLocator.Current.GetService() is ChoreographerTimer timer)
+ {
+ _timerSubscription = timer.SubscribeView(this);
+ }
+
+ _root.Renderer.Start();
+ }
+ else
+ {
+ _root.Renderer.Stop();
+ _timerSubscription?.Dispose();
+ }
+ }
+
class ViewImpl : TopLevelImpl
{
public ViewImpl(Context context) : base(context)
diff --git a/src/Android/Avalonia.Android/ChoreographerTimer.cs b/src/Android/Avalonia.Android/ChoreographerTimer.cs
new file mode 100644
index 00000000000..1d898261a35
--- /dev/null
+++ b/src/Android/Avalonia.Android/ChoreographerTimer.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Disposables;
+using System.Threading.Tasks;
+
+using Android.OS;
+using Android.Views;
+
+using Avalonia.Rendering;
+
+using Java.Lang;
+
+namespace Avalonia.Android
+{
+ internal sealed class ChoreographerTimer : Java.Lang.Object, IRenderTimer, Choreographer.IFrameCallback
+ {
+ private readonly object _lock = new object();
+
+ private readonly Thread _thread;
+ private readonly TaskCompletionSource _choreographer = new TaskCompletionSource();
+
+ private readonly ISet _views = new HashSet();
+
+ private Action _tick;
+ private int _count;
+
+ public ChoreographerTimer()
+ {
+ _thread = new Thread(Loop);
+ _thread.Start();
+ }
+
+ public event Action Tick
+ {
+ add
+ {
+ lock (_lock)
+ {
+ _tick += value;
+ _count++;
+
+ if (_count == 1)
+ {
+ _choreographer.Task.Result.PostFrameCallback(this);
+ }
+ }
+ }
+ remove
+ {
+ lock (_lock)
+ {
+ _tick -= value;
+ _count--;
+ }
+ }
+ }
+
+ internal IDisposable SubscribeView(AvaloniaView view)
+ {
+ lock (_lock)
+ {
+ _views.Add(view);
+
+ if (_views.Count == 1)
+ {
+ _choreographer.Task.Result.PostFrameCallback(this);
+ }
+ }
+
+ return Disposable.Create(
+ () =>
+ {
+ lock (_lock)
+ {
+ _views.Remove(view);
+ }
+ }
+ );
+ }
+
+ private void Loop()
+ {
+ Looper.Prepare();
+ _choreographer.SetResult(Choreographer.Instance);
+ Looper.Loop();
+ }
+
+ public void DoFrame(long frameTimeNanos)
+ {
+ _tick?.Invoke(TimeSpan.FromTicks(frameTimeNanos / 100));
+
+ lock (_lock)
+ {
+ if (_count > 0 && _views.Count > 0)
+ {
+ Choreographer.Instance.PostFrameCallback(this);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/CursorFactory.cs b/src/Android/Avalonia.Android/CursorFactory.cs
index 9eb28c67f98..6293637d4eb 100644
--- a/src/Android/Avalonia.Android/CursorFactory.cs
+++ b/src/Android/Avalonia.Android/CursorFactory.cs
@@ -1,12 +1,21 @@
-using System;
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Android
{
- internal class CursorFactory : IStandardCursorFactory
+ internal class CursorFactory : ICursorFactory
{
- public IPlatformHandle GetCursor(StandardCursorType cursorType)
- => new PlatformHandle(IntPtr.Zero, "ZeroCursor");
+ public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => CursorImpl.ZeroCursor;
+
+ public ICursorImpl GetCursor(StandardCursorType cursorType) => CursorImpl.ZeroCursor;
+
+ private sealed class CursorImpl : ICursorImpl
+ {
+ public static CursorImpl ZeroCursor { get; } = new CursorImpl();
+
+ private CursorImpl() { }
+
+ public void Dispose() { }
+ }
}
}
diff --git a/src/Android/Avalonia.Android/IInitEditorInfo.cs b/src/Android/Avalonia.Android/IInitEditorInfo.cs
new file mode 100644
index 00000000000..98fc4eafc1d
--- /dev/null
+++ b/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 init);
+ }
+}
diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
new file mode 100644
index 00000000000..a9710039f86
--- /dev/null
+++ b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
@@ -0,0 +1,30 @@
+using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Surfaces;
+
+namespace Avalonia.Android.OpenGL
+{
+ internal sealed class GlPlatformSurface : EglGlPlatformSurfaceBase
+ {
+ private readonly EglPlatformOpenGlInterface _egl;
+ private readonly IEglWindowGlPlatformSurfaceInfo _info;
+
+ private GlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info)
+ {
+ _egl = egl;
+ _info = info;
+ }
+
+ public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
+ new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
+
+ public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
+ {
+ if (EglPlatformOpenGlInterface.TryCreate() is EglPlatformOpenGlInterface egl)
+ {
+ return new GlPlatformSurface(egl, info);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
new file mode 100644
index 00000000000..f9071d9b274
--- /dev/null
+++ b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
@@ -0,0 +1,30 @@
+using System;
+
+using Avalonia.OpenGL.Egl;
+using Avalonia.OpenGL.Surfaces;
+
+namespace Avalonia.Android.OpenGL
+{
+ internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo
+ {
+ private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
+ private readonly EglSurface _surface;
+ private readonly IntPtr _handle;
+
+ public GlRenderTarget(
+ EglPlatformOpenGlInterface egl,
+ EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
+ EglSurface surface,
+ IntPtr handle)
+ : base(egl)
+ {
+ _info = info;
+ _surface = surface;
+ _handle = handle;
+ }
+
+ public bool IsCorrupted => _handle != _info.Handle;
+
+ public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
index 7802f336fb0..d1a116345bd 100644
--- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
@@ -1,7 +1,11 @@
+using System;
using System.Threading.Tasks;
+
using Android.Content;
using Android.Runtime;
using Android.Views;
+
+using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
deleted file mode 100644
index d52eeb15e43..00000000000
--- a/src/Android/Avalonia.Android/Platform/Input/AndroidMouseDevice.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Avalonia.Input;
-
-namespace Avalonia.Android.Platform.Input
-{
- public class AndroidMouseDevice : MouseDevice
- {
- public static AndroidMouseDevice Instance { get; } = new AndroidMouseDevice();
-
- public AndroidMouseDevice()
- {
-
- }
- }
-}
\ No newline at end of file
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
index 2afa4e83f1f..aabf8160f87 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
@@ -10,7 +10,7 @@ class AndroidFramebuffer : ILockedFramebuffer
{
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 @@ public AndroidFramebuffer(Surface surface)
RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
Address = buffer.bits;
+
+ Dpi = new Vector(96, 96) * scaling;
}
public void Dispose()
@@ -44,7 +46,7 @@ public void Dispose()
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")]
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
new file mode 100644
index 00000000000..56a4eb22d43
--- /dev/null
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/FramebufferManager.cs
@@ -0,0 +1,17 @@
+using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Platform;
+
+namespace Avalonia.Android.Platform.SkiaPlatform
+{
+ internal sealed class FramebufferManager : IFramebufferPlatformSurface
+ {
+ private readonly TopLevelImpl _topLevel;
+
+ public FramebufferManager(TopLevelImpl topLevel)
+ {
+ _topLevel = topLevel;
+ }
+
+ public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface, _topLevel.RenderScaling);
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
index 02ea702236b..34784612f10 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
@@ -13,7 +13,7 @@ public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolder
bool _invalidateQueued;
readonly object _lock = new object();
private readonly Handler _handler;
-
+
public InvalidationAwareSurfaceView(Context context) : base(context)
{
@@ -43,11 +43,13 @@ public override void Invalidate()
}
}
+ [Obsolete("deprecated")]
public override void Invalidate(global::Android.Graphics.Rect dirty)
{
Invalidate();
}
+ [Obsolete("deprecated")]
public override void Invalidate(int l, int t, int r, int b)
{
Invalidate();
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index 71dce93ce7a..0afb1db1413 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -2,71 +2,62 @@
using System.Collections.Generic;
using Android.Content;
using Android.Graphics;
+using Android.Runtime;
using Android.Views;
-using Avalonia.Android.Platform.Input;
+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;
using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform
{
- class TopLevelImpl : IAndroidView, ITopLevelImpl, IFramebufferPlatformSurface
+ class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod
{
+ private readonly IGlPlatformSurface _gl;
+ private readonly IFramebufferPlatformSurface _framebuffer;
+
private readonly AndroidKeyboardEventsHelper _keyboardHelper;
private readonly AndroidTouchEventsHelper _touchHelper;
-
+ private readonly ITextInputMethodImpl _textInputMethod;
private ViewImpl _view;
public TopLevelImpl(Context context, bool placeOnTop = false)
{
_view = new ViewImpl(context, this, placeOnTop);
+ _textInputMethod = new AndroidInputMethod(_view);
_keyboardHelper = new AndroidKeyboardEventsHelper(this);
_touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot,
- p => GetAvaloniaPointFromEvent(p));
-
- Surfaces = new object[] { this };
-
- MaxClientSize = new Size(_view.Resources.DisplayMetrics.WidthPixels,
- _view.Resources.DisplayMetrics.HeightPixels);
- }
-
+ GetAvaloniaPointFromEvent);
+ _gl = GlPlatformSurface.TryCreate(this);
+ _framebuffer = new FramebufferManager(this);
- 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) => new Point(e.GetX(), e.GetY());
+
+ public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
+ new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
public IInputRoot InputRoot { get; private set; }
- public 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 => AndroidMouseDevice.Instance;
+ public Size? FrameSize => null;
+
+ public IMouseDevice MouseDevice { get; } = new MouseDevice();
public Action Closed { get; set; }
@@ -76,26 +67,28 @@ public virtual Size ClientSize
public Action Paint { get; set; }
- public Action Resized { get; set; }
+ public Action Resized { get; set; }
public Action ScalingChanged { get; set; }
public View View => _view;
+ internal InvalidationAwareSurfaceView InternalView => _view;
+
public IPlatformHandle Handle => _view;
- public IEnumerable Surfaces { get; }
+ public IEnumerable Surfaces => new object[] { _gl, _framebuffer };
- public IRenderer CreateRenderer(IRenderRoot root)
- {
- return new ImmediateRenderer(root);
- }
+ public IRenderer CreateRenderer(IRenderRoot root) =>
+ AndroidPlatform.Options.UseDeferredRendering
+ ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) { RenderOnlyOnRenderThread = true }
+ : new ImmediateRenderer(root);
public virtual void Hide()
{
_view.Visibility = ViewStates.Invisible;
}
-
+
public void Invalidate(Rect rect)
{
if (_view.Holder?.Surface?.IsValid == true) _view.Invalidate();
@@ -103,15 +96,15 @@ public void Invalidate(Rect rect)
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(IPlatformHandle cursor)
+ public void SetCursor(ICursorImpl cursor)
{
//still not implemented
}
@@ -126,7 +119,7 @@ public virtual void Show()
_view.Visibility = ViewStates.Visible;
}
- public double RenderScaling => 1;
+ public double RenderScaling { get; }
void Draw()
{
@@ -141,10 +134,10 @@ public virtual void Dispose()
protected virtual void OnResized(Size size)
{
- Resized?.Invoke(size);
+ Resized?.Invoke(size, PlatformResizeReason.Unspecified);
}
- class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback
+ class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo
{
private readonly TopLevelImpl _tl;
private Size _oldSize;
@@ -181,7 +174,7 @@ public override bool DispatchKeyEvent(KeyEvent e)
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)
{
@@ -191,12 +184,50 @@ void ISurfaceHolderCallback.SurfaceChanged(ISurfaceHolder holder, Format format,
base.SurfaceChanged(holder, format, width, height);
}
+
+ public sealed override bool OnCheckIsTextEditor()
+ {
+ return true;
+ }
+
+ private Action _initEditorInfo;
+
+ public void InitEditorInfo(Action init)
+ {
+ _initEditorInfo = init;
+ }
+
+ public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs)
+ {
+ if (_initEditorInfo != null)
+ _initEditorInfo(outAttrs);
+
+ return base.OnCreateInputConnection(outAttrs);
+ }
+
}
public IPopupImpl CreatePopup() => null;
public Action LostFocus { get; set; }
+ public Action TransparencyLevelChanged { get; set; }
+
+ public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None;
+
+ public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
+
+ IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle =>
+ AndroidFramebuffer.ANativeWindow_fromSurface(JNIEnv.Handle, _view.Holder.Surface.Handle);
- ILockedFramebuffer IFramebufferPlatformSurface.Lock()=>new AndroidFramebuffer(_view.Holder.Surface);
+ public PixelSize Size => new PixelSize(_view.Holder.SurfaceFrame.Width(), _view.Holder.SurfaceFrame.Height());
+
+ public double Scaling => RenderScaling;
+
+ public ITextInputMethodImpl TextInputMethod => _textInputMethod;
+
+ public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
+ {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
index 1179ce9235c..2b2a9dd2b4e 100644
--- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
+++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
@@ -1,27 +1,21 @@
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.Controls;
+using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Input;
using Avalonia.Input.Raw;
-using Avalonia.Platform;
namespace Avalonia.Android.Platform.Specific.Helpers
{
- public class AndroidKeyboardEventsHelper : IDisposable where TView :ITopLevelImpl, IAndroidView
+ internal class AndroidKeyboardEventsHelper : 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 @@ public AndroidKeyboardEventsHelper(TView view)
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;
@@ -46,17 +51,21 @@ public AndroidKeyboardEventsHelper(TView view)
var rawKeyEvent = new RawKeyEventArgs(
AndroidKeyboardDevice.Instance,
- Convert.ToUInt32(e.EventTime),
+ Convert.ToUInt64(e.EventTime),
+ _view.InputRoot,
e.Action == KeyEventActions.Down ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
- AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e));
+ AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e));
+
_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),
- Convert.ToChar(e.UnicodeChar).ToString()
+ _view.InputRoot,
+ unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString()
);
_view.Input(rawTextEvent);
}
@@ -82,61 +91,6 @@ private static RawInputModifiers GetModifierKeys(KeyEvent e)
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();
-
- 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;
diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
index 0bfbb1c2df0..6142598514f 100644
--- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
+++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
@@ -11,7 +11,7 @@ public class AndroidTouchEventsHelper : IDisposable where TView : ITopLev
private TView _view;
public bool HandleEvents { get; set; }
- public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc)
+ public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func getPointfunc)
{
this._view = view;
HandleEvents = true;
@@ -19,11 +19,9 @@ public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func<
_getInputRoot = getInputRoot;
}
- private DateTime _lastTouchMoveEventTime = DateTime.Now;
- private Point? _lastTouchMovePoint;
- private Func _getPointFunc;
+ private TouchDevice _touchDevice = new TouchDevice();
+ private Func _getPointFunc;
private Func _getInputRoot;
- private Point _point;
public bool? DispatchTouchEvent(MotionEvent e, out bool callBase)
{
@@ -33,89 +31,44 @@ public AndroidTouchEventsHelper(TView view, Func getInputRoot, Func<
return null;
}
- RawPointerEventType? mouseEventType = null;
var eventTime = DateTime.Now;
+
//Basic touch support
- switch (e.Action)
+ var pointerEventType = e.Action switch
{
- case MotionEventActions.Move:
- //may be bot flood the evnt system with too many event especially on not so powerfull mobile devices
- if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10)
- {
- mouseEventType = RawPointerEventType.Move;
- }
- break;
-
- case MotionEventActions.Down:
- mouseEventType = RawPointerEventType.LeftButtonDown;
+ MotionEventActions.Down => RawPointerEventType.TouchBegin,
+ MotionEventActions.Up => RawPointerEventType.TouchEnd,
+ MotionEventActions.Cancel => RawPointerEventType.TouchCancel,
+ _ => RawPointerEventType.TouchUpdate
+ };
- break;
+ if (e.Action.HasFlag(MotionEventActions.PointerDown))
+ {
+ pointerEventType = RawPointerEventType.TouchBegin;
+ }
- case MotionEventActions.Up:
- mouseEventType = RawPointerEventType.LeftButtonUp;
- break;
+ if (e.Action.HasFlag(MotionEventActions.PointerUp))
+ {
+ pointerEventType = RawPointerEventType.TouchEnd;
}
- if (mouseEventType != null)
+ for (int i = 0; i < e.PointerCount; i++)
{
//if point is in view otherwise it's possible avalonia not to find the proper window to dispatch the event
- _point = _getPointFunc(e);
+ var point = _getPointFunc(e, i);
double x = _view.View.GetX();
double y = _view.View.GetY();
double r = x + _view.View.Width;
double b = y + _view.View.Height;
- if (x <= _point.X && r >= _point.X && y <= _point.Y && b >= _point.Y)
+ if (x <= point.X && r >= point.X && y <= point.Y && b >= point.Y)
{
var inputRoot = _getInputRoot();
- var mouseDevice = Avalonia.Android.Platform.Input.AndroidMouseDevice.Instance;
-
- //in order the controls to work in a predictable way
- //we need to generate mouse move before first mouse down event
- //as this is the way buttons are working every time
- //otherwise there is a problem sometimes
- if (mouseEventType == RawPointerEventType.LeftButtonDown)
- {
- var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
- RawPointerEventType.Move, _point, RawInputModifiers.None);
- _view.Input(me);
- }
- var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
- mouseEventType.Value, _point, RawInputModifiers.LeftMouseButton);
+ var mouseEvent = new RawTouchEventArgs(_touchDevice, (uint)eventTime.Ticks, inputRoot,
+ i == e.ActionIndex ? pointerEventType : RawPointerEventType.TouchUpdate, point, RawInputModifiers.None, e.GetPointerId(i));
_view.Input(mouseEvent);
-
- if (e.Action == MotionEventActions.Move && mouseDevice.Captured == null)
- {
- if (_lastTouchMovePoint != null)
- {
- //raise mouse scroll event so the scrollers
- //are moving with the cursor
- double vectorX = _point.X - _lastTouchMovePoint.Value.X;
- double vectorY = _point.Y - _lastTouchMovePoint.Value.Y;
- //based on test correction of 0.02 is working perfect
- double correction = 0.02;
- var ps = AndroidPlatform.Instance.LayoutScalingFactor;
- var mouseWheelEvent = new RawMouseWheelEventArgs(
- mouseDevice,
- (uint)eventTime.Ticks,
- inputRoot,
- _point,
- new Vector(vectorX * correction / ps, vectorY * correction / ps), RawInputModifiers.LeftMouseButton);
- _view.Input(mouseWheelEvent);
- }
- _lastTouchMovePoint = _point;
- _lastTouchMoveEventTime = eventTime;
- }
- else if (e.Action == MotionEventActions.Down)
- {
- _lastTouchMovePoint = _point;
- }
- else
- {
- _lastTouchMovePoint = null;
- }
}
}
diff --git a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs b/src/Android/Avalonia.Android/Resources/Resource.Designer.cs
deleted file mode 100644
index 80cbbc51ec9..00000000000
--- a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-#pragma warning disable 1591
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-[assembly: global::Android.Runtime.ResourceDesignerAttribute("Avalonia.Android.Resource", IsApplication=false)]
-
-namespace Avalonia.Android
-{
-
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
- public partial class Resource
- {
-
- static Resource()
- {
- global::Android.Runtime.ResourceIdManager.UpdateIdValues();
- }
-
- public partial class Attribute
- {
-
- static Attribute()
- {
- global::Android.Runtime.ResourceIdManager.UpdateIdValues();
- }
-
- private Attribute()
- {
- }
- }
-
- public partial class String
- {
-
- // aapt resource value: 0x7f020001
- public static int ApplicationName = 2130837505;
-
- // aapt resource value: 0x7f020000
- public static int Hello = 2130837504;
-
- static String()
- {
- global::Android.Runtime.ResourceIdManager.UpdateIdValues();
- }
-
- private String()
- {
- }
- }
- }
-}
-#pragma warning restore 1591
diff --git a/src/Android/Avalonia.Android/SoftKeyboardListner.cs b/src/Android/Avalonia.Android/SoftKeyboardListner.cs
new file mode 100644
index 00000000000..df658f63148
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/SystemDialogImpl.cs b/src/Android/Avalonia.Android/SystemDialogImpl.cs
index a8d201d66e6..1ed1f688b14 100644
--- a/src/Android/Avalonia.Android/SystemDialogImpl.cs
+++ b/src/Android/Avalonia.Android/SystemDialogImpl.cs
@@ -2,18 +2,17 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
-using Avalonia.Platform;
namespace Avalonia.Android
{
internal class SystemDialogImpl : ISystemDialogImpl
{
- public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
+ public Task ShowFileDialogAsync(FileDialog dialog, Window parent)
{
throw new NotImplementedException();
}
- public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
+ public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
throw new NotImplementedException();
}
diff --git a/src/Android/Avalonia.Android/app.config b/src/Android/Avalonia.Android/app.config
deleted file mode 100644
index fc064cedfb2..00000000000
--- a/src/Android/Avalonia.Android/app.config
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
index b8697e0ca21..9104f1618cf 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
+++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
@@ -16,7 +16,7 @@
Resources\Resource.Designer.cs
Off
False
- v9.0
+ v11.0
Properties\AndroidManifest.xml
@@ -127,10 +127,6 @@
{42472427-4774-4c81-8aff-9f27b8e31721}
Avalonia.Layout
-
- {6417b24e-49c2-4985-8db2-3ab9d898ec91}
- Avalonia.ReactiveUI
-
{eb582467-6abb-43a1-b052-e981ba910e3a}
Avalonia.Visuals
diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
index ad2cec2ae3d..5f33cadf2e5 100644
--- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
+++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
@@ -4,7 +4,6 @@
using Android.OS;
using Avalonia.Android;
using Avalonia.Controls;
-using Avalonia.Controls.Templates;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Styling;
@@ -17,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()
.UseAndroid()
.SetupWithoutStarting();
}
- SetContentView(new AvaloniaView(this) { Content = App.CreateSimpleWindow() });
+ base.OnCreate(savedInstanceState);
+ Content = App.CreateSimpleWindow();
}
}
@@ -38,8 +37,7 @@ public override void Initialize()
{
Styles.Add(new DefaultTheme());
- var loader = new AvaloniaXamlLoader();
- var baseLight = (IStyle)loader.Load(
+ var baseLight = (IStyle)AvaloniaXamlLoader.Load(
new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"));
Styles.Add(baseLight);
@@ -74,13 +72,33 @@ public static ContentControl CreateSimpleWindow()
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;
+ }
}
}
diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
index 4792c8a1ec1..57ee5030058 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
+++ b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs
index e171dd61621..83db67fceef 100644
--- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs
+++ b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs
@@ -2,7 +2,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -15,7 +14,7 @@ namespace Avalonia.AndroidTestApplication
{
- [System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")]
public partial class Resource
{
@@ -26,8 +25,6 @@ static Resource()
public static void UpdateIdValues()
{
- global::Avalonia.Android.Resource.String.ApplicationName = global::Avalonia.AndroidTestApplication.Resource.String.ApplicationName;
- global::Avalonia.Android.Resource.String.Hello = global::Avalonia.AndroidTestApplication.Resource.String.Hello;
}
public partial class Attribute
@@ -46,8 +43,8 @@ private Attribute()
public partial class Drawable
{
- // aapt resource value: 0x7f020000
- public const int Icon = 2130837504;
+ // aapt resource value: 0x7F010000
+ public const int Icon = 2130771968;
static Drawable()
{
@@ -62,11 +59,11 @@ private Drawable()
public partial class String
{
- // aapt resource value: 0x7f030001
- public const int ApplicationName = 2130903041;
+ // aapt resource value: 0x7F020000
+ public const int ApplicationName = 2130837504;
- // aapt resource value: 0x7f030000
- public const int Hello = 2130903040;
+ // aapt resource value: 0x7F020001
+ public const int Hello = 2130837505;
static String()
{
diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs
index 067d9f462f4..a4150465139 100644
--- a/src/Avalonia.Animation/Animatable.cs
+++ b/src/Avalonia.Animation/Animatable.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Linq;
using Avalonia.Data;
#nullable enable
@@ -93,16 +94,35 @@ protected sealed override void OnPropertyChangedCore(AvaloniaPropertyChangedE
var oldTransitions = change.OldValue.GetValueOrDefault();
var newTransitions = change.NewValue.GetValueOrDefault();
+ // 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 @@ _transitionState is object &&
{
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);
diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs
index ca1d97290e0..172782c5a98 100644
--- a/src/Avalonia.Animation/Animation.cs
+++ b/src/Avalonia.Animation/Animation.cs
@@ -3,10 +3,11 @@
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
+using System.Threading;
using System.Threading.Tasks;
+
using Avalonia.Animation.Animators;
using Avalonia.Animation.Easings;
-using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Metadata;
@@ -22,7 +23,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty DurationProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_duration),
+ nameof(Duration),
o => o._duration,
(o, v) => o._duration = v);
@@ -31,7 +32,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty IterationCountProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_iterationCount),
+ nameof(IterationCount),
o => o._iterationCount,
(o, v) => o._iterationCount = v);
@@ -40,7 +41,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty PlaybackDirectionProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_playbackDirection),
+ nameof(PlaybackDirection),
o => o._playbackDirection,
(o, v) => o._playbackDirection = v);
@@ -49,7 +50,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty FillModeProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_fillMode),
+ nameof(FillMode),
o => o._fillMode,
(o, v) => o._fillMode = v);
@@ -58,7 +59,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty EasingProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_easing),
+ nameof(Easing),
o => o._easing,
(o, v) => o._easing = v);
@@ -67,7 +68,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty DelayProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_delay),
+ nameof(Delay),
o => o._delay,
(o, v) => o._delay = v);
@@ -76,7 +77,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty DelayBetweenIterationsProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_delayBetweenIterations),
+ nameof(DelayBetweenIterations),
o => o._delayBetweenIterations,
(o, v) => o._delayBetweenIterations = v);
@@ -85,7 +86,7 @@ public class Animation : AvaloniaObject, IAnimation
///
public static readonly DirectProperty SpeedRatioProperty =
AvaloniaProperty.RegisterDirect(
- nameof(_speedRatio),
+ nameof(SpeedRatio),
o => o._speedRatio,
(o, v) => o._speedRatio = v,
defaultBindingMode: BindingMode.TwoWay);
@@ -194,6 +195,33 @@ public string RepeatCount
[Content]
public KeyFrames Children { get; } = new KeyFrames();
+ // Store values for the Animator attached properties for IAnimationSetter objects.
+ private static readonly Dictionary s_animators = new Dictionary();
+
+ ///
+ /// Gets the value of the Animator attached property for a setter.
+ ///
+ /// The animation setter.
+ /// The property animator type.
+ public static Type GetAnimator(IAnimationSetter setter)
+ {
+ if (s_animators.TryGetValue(setter, out var type))
+ {
+ return type;
+ }
+ return null;
+ }
+
+ ///
+ /// Sets the value of the Animator attached property for a setter.
+ ///
+ /// The animation setter.
+ /// The property animator value.
+ public static void SetAnimator(IAnimationSetter setter, Type value)
+ {
+ s_animators[setter] = value;
+ }
+
private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)>
{
( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ),
@@ -209,6 +237,17 @@ public string RepeatCount
( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
};
+ ///
+ /// Registers a that can handle
+ /// a value type that matches the specified condition.
+ ///
+ ///
+ /// The condition to which the
+ /// is to be activated and used.
+ ///
+ ///
+ /// The type of the animator to instantiate.
+ ///
public static void RegisterAnimator(Func condition)
where TAnimator : IAnimator
{
@@ -237,7 +276,7 @@ private static Type GetAnimatorType(AvaloniaProperty property)
{
foreach (var setter in keyframe.Setters)
{
- var handler = GetAnimatorType(setter.Property);
+ var handler = Animation.GetAnimator(setter) ?? GetAnimatorType(setter.Property);
if (handler == null)
{
@@ -281,7 +320,7 @@ private static Type GetAnimatorType(AvaloniaProperty property)
return (newAnimatorInstances, subscriptions);
}
- ///
+ ///
public IDisposable Apply(Animatable control, IClock clock, IObservable match, Action onComplete)
{
var (animators, subscriptions) = InterpretKeyframes(control);
@@ -306,25 +345,40 @@ public IDisposable Apply(Animatable control, IClock clock, IObservable mat
if (onComplete != null)
{
- Task.WhenAll(completionTasks).ContinueWith(_ => onComplete());
+ Task.WhenAll(completionTasks).ContinueWith(
+ (_, state) => ((Action)state).Invoke(),
+ onComplete);
}
}
return new CompositeDisposable(subscriptions);
}
- ///
- public Task RunAsync(Animatable control, IClock clock = null)
+ ///
+ public Task RunAsync(Animatable control, IClock clock = null, CancellationToken cancellationToken = default)
{
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return Task.CompletedTask;
+ }
+
var run = new TaskCompletionSource();
if (this.IterationCount == IterationCount.Infinite)
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
- IDisposable subscriptions = null;
+ IDisposable subscriptions = null, cancellation = null;
subscriptions = this.Apply(control, clock, Observable.Return(true), () =>
{
- run.SetResult(null);
+ run.TrySetResult(null);
+ subscriptions?.Dispose();
+ cancellation?.Dispose();
+ });
+
+ cancellation = cancellationToken.Register(() =>
+ {
+ run.TrySetResult(null);
subscriptions?.Dispose();
+ cancellation?.Dispose();
});
return run.Task;
diff --git a/src/Avalonia.Animation/AnimationInstance`1.cs b/src/Avalonia.Animation/AnimationInstance`1.cs
index 6f601a3e136..cf796401500 100644
--- a/src/Avalonia.Animation/AnimationInstance`1.cs
+++ b/src/Avalonia.Animation/AnimationInstance`1.cs
@@ -5,6 +5,7 @@
using Avalonia.Animation.Utils;
using Avalonia.Data;
using Avalonia.Reactive;
+using JetBrains.Annotations;
namespace Avalonia.Animation
{
@@ -36,6 +37,7 @@ internal class AnimationInstance : SingleSubscriberObservableBase
private IDisposable _timerSub;
private readonly IClock _baseClock;
private IClock _clock;
+ private EventHandler _propertyChangedDelegate;
public AnimationInstance(Animation animation, Animatable control, Animator animator, IClock baseClock, Action OnComplete, Func Interpolator)
{
@@ -45,8 +47,6 @@ public AnimationInstance(Animation animation, Animatable control, Animator an
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
- _neutralValue = (T)_targetControl.GetValue(_animator.Property);
-
FetchProperties();
}
@@ -80,6 +80,7 @@ protected override void Unsubscribed()
// Animation may have been stopped before it has finished.
ApplyFinalFill();
+ _targetControl.PropertyChanged -= _propertyChangedDelegate;
_timerSub?.Dispose();
_clock.PlayState = PlayState.Stop;
}
@@ -88,6 +89,9 @@ protected override void Subscribed()
{
_clock = new Clock(_baseClock);
_timerSub = _clock.Subscribe(Step);
+ _propertyChangedDelegate ??= ControlPropertyChanged;
+ _targetControl.PropertyChanged += _propertyChangedDelegate;
+ UpdateNeutralValue();
}
public void Step(TimeSpan frameTick)
@@ -216,5 +220,22 @@ private void InternalStep(TimeSpan time)
}
}
}
+
+ private void UpdateNeutralValue()
+ {
+ var property = _animator.Property;
+ var baseValue = _targetControl.GetBaseValue(property, BindingPriority.LocalValue);
+
+ _neutralValue = baseValue != AvaloniaProperty.UnsetValue ?
+ (T)baseValue : (T)_targetControl.GetValue(property);
+ }
+
+ private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == _animator.Property && e.Priority > BindingPriority.Animation)
+ {
+ UpdateNeutralValue();
+ }
+ }
}
}
diff --git a/src/Avalonia.Animation/AnimatorDrivenTransition.cs b/src/Avalonia.Animation/AnimatorDrivenTransition.cs
new file mode 100644
index 00000000000..88c8ec5ec1f
--- /dev/null
+++ b/src/Avalonia.Animation/AnimatorDrivenTransition.cs
@@ -0,0 +1,20 @@
+using System;
+using Avalonia.Animation.Animators;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// using an to transition between values.
+ ///
+ /// Type of the transitioned value.
+ /// Type of the animator.
+ public abstract class AnimatorDrivenTransition : Transition where TAnimator : Animator, new()
+ {
+ private static readonly TAnimator s_animator = new TAnimator();
+
+ public override IObservable DoTransition(IObservable progress, T oldValue, T newValue)
+ {
+ return new AnimatorTransitionObservable(s_animator, progress, Easing, oldValue, newValue);
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/AnimatorTransitionObservable.cs b/src/Avalonia.Animation/AnimatorTransitionObservable.cs
new file mode 100644
index 00000000000..3cc185179bf
--- /dev/null
+++ b/src/Avalonia.Animation/AnimatorTransitionObservable.cs
@@ -0,0 +1,32 @@
+using System;
+using Avalonia.Animation.Animators;
+using Avalonia.Animation.Easings;
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Transition observable based on an producing a value.
+ ///
+ /// Type of the transitioned value.
+ /// Type of the animator.
+ public class AnimatorTransitionObservable : TransitionObservableBase where TAnimator : Animator
+ {
+ private readonly TAnimator _animator;
+ private readonly Easing _easing;
+ private readonly T _oldValue;
+ private readonly T _newValue;
+
+ public AnimatorTransitionObservable(TAnimator animator, IObservable 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);
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs
index 0660440e30e..d7842276203 100644
--- a/src/Avalonia.Animation/Animators/Animator`1.cs
+++ b/src/Avalonia.Animation/Animators/Animator`1.cs
@@ -104,6 +104,11 @@ private int FindClosestBeforeKeyFrame(double time)
throw new Exception("Index time is out of keyframe time range.");
}
+ public virtual IDisposable BindAnimation(Animatable control, IObservable instance)
+ {
+ return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+ }
+
///
/// Runs the KeyFrames Animation.
///
@@ -116,7 +121,8 @@ internal IDisposable Run(Animation animation, Animatable control, IClock clock,
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
InterpolationHandler);
- return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
+
+ return BindAnimation(control, instance);
}
///
diff --git a/src/Avalonia.Animation/ApiCompatBaseline.txt b/src/Avalonia.Animation/ApiCompatBaseline.txt
new file mode 100644
index 00000000000..58cb7830e7c
--- /dev/null
+++ b/src/Avalonia.Animation/ApiCompatBaseline.txt
@@ -0,0 +1,6 @@
+Compat issues with assembly Avalonia.Animation:
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.Animation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' is present in the contract but not in the implementation.
+MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IAnimation.RunAsync(Avalonia.Animation.Animatable, Avalonia.Animation.IClock, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
+Total Issues: 4
diff --git a/src/Avalonia.Animation/Clock.cs b/src/Avalonia.Animation/Clock.cs
index bea6c759826..5c2b7ce0dde 100644
--- a/src/Avalonia.Animation/Clock.cs
+++ b/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 @@ public class Clock : ClockBase
{
public static IClock GlobalClock => AvaloniaLocator.Current.GetService();
- private IDisposable _parentSubscription;
+ private readonly IDisposable _parentSubscription;
- public Clock()
- :this(GlobalClock)
+ public Clock() : this(GlobalClock)
{
}
diff --git a/src/Avalonia.Animation/ClockBase.cs b/src/Avalonia.Animation/ClockBase.cs
index a2b29e728e8..c6e5a363be1 100644
--- a/src/Avalonia.Animation/ClockBase.cs
+++ b/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 _connectedObservable;
+ private readonly ClockObservable _observable;
private TimeSpan? _previousTime;
private TimeSpan _internalTime;
@@ -18,7 +13,6 @@ public class ClockBase : IClock
protected ClockBase()
{
_observable = new ClockObservable();
- _connectedObservable = _observable.Publish().RefCount();
}
protected bool HasSubscriptions => _observable.HasSubscriptions;
@@ -58,10 +52,10 @@ protected virtual void Stop()
public IDisposable Subscribe(IObserver observer)
{
- return _connectedObservable.Subscribe(observer);
+ return _observable.Subscribe(observer);
}
- private class ClockObservable : LightweightObservableBase
+ private sealed class ClockObservable : LightweightObservableBase
{
public bool HasSubscriptions { get; private set; }
public void Pulse(TimeSpan time) => PublishNext(time);
diff --git a/src/Avalonia.Animation/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs
index ff85535d8ab..d037834630d 100644
--- a/src/Avalonia.Animation/IAnimation.cs
+++ b/src/Avalonia.Animation/IAnimation.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
namespace Avalonia.Animation
@@ -16,6 +17,6 @@ public interface IAnimation
///
/// Run the animation on the specified control.
///
- Task RunAsync(Animatable control, IClock clock);
+ Task RunAsync(Animatable control, IClock clock, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs
index d34fd06272e..221b51e95ad 100644
--- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs
@@ -6,5 +6,10 @@
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]
+#if SIGNED_BUILD
+[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+#else
[assembly: InternalsVisibleTo("Avalonia.LeakTests")]
[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")]
+#endif
diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition.cs
similarity index 96%
rename from src/Avalonia.Animation/Transition`1.cs
rename to src/Avalonia.Animation/Transition.cs
index 4542a137e5b..4115c95c0f5 100644
--- a/src/Avalonia.Animation/Transition`1.cs
+++ b/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 @@ public virtual IDisposable Apply(Animatable control, IClock clock, object oldVal
return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation);
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs
index 51843413246..b522d1961e5 100644
--- a/src/Avalonia.Animation/TransitionInstance.cs
+++ b/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
///
/// Handles the timing and lifetime of a .
///
- internal class TransitionInstance : SingleSubscriberObservableBase
+ internal class TransitionInstance : SingleSubscriberObservableBase, IObserver
{
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 @@ protected override void Unsubscribed()
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.OnCompleted()
+ {
+ PublishCompleted();
+ }
+
+ void IObserver.OnError(Exception error)
+ {
+ PublishError(error);
+ }
+
+ void IObserver.OnNext(TimeSpan value)
+ {
+ TimerTick(value);
+ }
+
+ ///
+ /// TODO: This clock is still fairly expensive due to implementation.
+ ///
+ private sealed class TransitionClock : ClockBase, IObserver
+ {
+ private readonly IDisposable _parentSubscription;
+
+ public TransitionClock(IClock parent)
+ {
+ _parentSubscription = parent.Subscribe(this);
+ }
+
+ protected override void Stop()
+ {
+ _parentSubscription.Dispose();
+ }
+
+ void IObserver.OnNext(TimeSpan value)
+ {
+ Pulse(value);
+ }
+
+ void IObserver.OnCompleted()
+ {
+ }
+
+ void IObserver.OnError(Exception error)
+ {
+ ExceptionDispatchInfo.Capture(error).Throw();
+ }
+ }
}
}
diff --git a/src/Avalonia.Animation/TransitionObservableBase.cs b/src/Avalonia.Animation/TransitionObservableBase.cs
new file mode 100644
index 00000000000..c4ac803135e
--- /dev/null
+++ b/src/Avalonia.Animation/TransitionObservableBase.cs
@@ -0,0 +1,58 @@
+using System;
+using Avalonia.Animation.Easings;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Animation
+{
+ ///
+ /// Provides base for observables implementing transitions.
+ ///
+ /// Type of the transitioned value.
+ public abstract class TransitionObservableBase : SingleSubscriberObservableBase, IObserver
+ {
+ private readonly Easing _easing;
+ private readonly IObservable _progress;
+ private IDisposable? _progressSubscription;
+
+ protected TransitionObservableBase(IObservable progress, Easing easing)
+ {
+ _progress = progress;
+ _easing = easing;
+ }
+
+ ///
+ /// Produces value at given progress time point.
+ ///
+ /// Transition progress.
+ protected abstract T ProduceValue(double progress);
+
+ protected override void Subscribed()
+ {
+ _progressSubscription = _progress.Subscribe(this);
+ }
+
+ protected override void Unsubscribed()
+ {
+ _progressSubscription?.Dispose();
+ }
+
+ void IObserver.OnCompleted()
+ {
+ PublishCompleted();
+ }
+
+ void IObserver.OnError(Exception error)
+ {
+ PublishError(error);
+ }
+
+ void IObserver.OnNext(double value)
+ {
+ double progress = _easing.Ease(value);
+
+ PublishNext(ProduceValue(progress));
+ }
+ }
+}
diff --git a/src/Avalonia.Animation/Transitions/DoubleTransition.cs b/src/Avalonia.Animation/Transitions/DoubleTransition.cs
index 8cae1e1f815..7232d878631 100644
--- a/src/Avalonia.Animation/Transitions/DoubleTransition.cs
+++ b/src/Avalonia.Animation/Transitions/DoubleTransition.cs
@@ -1,22 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with types.
///
- public class DoubleTransition : Transition
+ public class DoubleTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, double oldValue, double newValue)
- {
- return progress
- .Select(p =>
- {
- var f = Easing.Ease(p);
- return ((newValue - oldValue) * f) + oldValue;
- });
- }
}
}
diff --git a/src/Avalonia.Animation/Transitions/FloatTransition.cs b/src/Avalonia.Animation/Transitions/FloatTransition.cs
index 427563e5594..a96db8ba5b3 100644
--- a/src/Avalonia.Animation/Transitions/FloatTransition.cs
+++ b/src/Avalonia.Animation/Transitions/FloatTransition.cs
@@ -1,19 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with types.
///
- public class FloatTransition : Transition
+ public class FloatTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, float oldValue, float newValue)
- {
- var delta = newValue - oldValue;
- return progress
- .Select(p => (float)Easing.Ease(p) * delta + oldValue);
- }
}
}
diff --git a/src/Avalonia.Animation/Transitions/IntegerTransition.cs b/src/Avalonia.Animation/Transitions/IntegerTransition.cs
index 7a85bd75dc4..343da7b689a 100644
--- a/src/Avalonia.Animation/Transitions/IntegerTransition.cs
+++ b/src/Avalonia.Animation/Transitions/IntegerTransition.cs
@@ -1,19 +1,11 @@
-using System;
-using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
namespace Avalonia.Animation
{
///
/// Transition class that handles with types.
///
- public class IntegerTransition : Transition
+ public class IntegerTransition : AnimatorDrivenTransition
{
- ///
- public override IObservable DoTransition(IObservable progress, int oldValue, int newValue)
- {
- var delta = newValue - oldValue;
- return progress
- .Select(p => (int)(Easing.Ease(p) * delta + oldValue));
- }
}
}
diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt
deleted file mode 100644
index 4668a572c5f..00000000000
--- a/src/Avalonia.Base/ApiCompatBaseline.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Compat issues with assembly Avalonia.Base:
-CannotAddAbstractMembers : Member 'protected System.IObservable Avalonia.AvaloniaProperty.GetChanged()' is abstract in the implementation but is missing in the contract.
-Total Issues: 1
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 9bfd387ca97..0470dbfe894 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -5,9 +5,6 @@
Avalonia
True
-
-
-
diff --git a/src/Avalonia.Base/AvaloniaLocator.cs b/src/Avalonia.Base/AvaloniaLocator.cs
index f9bbe38bec8..3163d15c1b0 100644
--- a/src/Avalonia.Base/AvaloniaLocator.cs
+++ b/src/Avalonia.Base/AvaloniaLocator.cs
@@ -54,6 +54,23 @@ public AvaloniaLocator ToFunc(Func func) where TImlp : TService
return _locator;
}
+ public AvaloniaLocator ToLazy(Func func) where TImlp : TService
+ {
+ var constructed = false;
+ TImlp instance = default;
+ _locator._registry[typeof (TService)] = () =>
+ {
+ if (!constructed)
+ {
+ instance = func();
+ constructed = true;
+ }
+
+ return instance;
+ };
+ return _locator;
+ }
+
public AvaloniaLocator ToSingleton() where TImpl : class, TService, new()
{
TImpl instance = null;
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 6645d25b5d1..ce5b37043f5 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -7,6 +7,8 @@
using Avalonia.PropertyStore;
using Avalonia.Threading;
+#nullable enable
+
namespace Avalonia
{
///
@@ -17,13 +19,13 @@ namespace Avalonia
///
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
{
- private IAvaloniaObject _inheritanceParent;
- private List _directBindings;
- private PropertyChangedEventHandler _inpcChanged;
- private EventHandler _propertyChanged;
- private List _inheritanceChildren;
- private ValueStore _values;
- private ValueStore Values => _values ?? (_values = new ValueStore(this));
+ private IAvaloniaObject? _inheritanceParent;
+ private List? _directBindings;
+ private PropertyChangedEventHandler? _inpcChanged;
+ private EventHandler? _propertyChanged;
+ private List? _inheritanceChildren;
+ private ValueStore? _values;
+ private bool _batchUpdate;
///
/// Initializes a new instance of the class.
@@ -36,7 +38,7 @@ public AvaloniaObject()
///
/// Raised when a value changes on this object.
///
- public event EventHandler PropertyChanged
+ public event EventHandler? PropertyChanged
{
add { _propertyChanged += value; }
remove { _propertyChanged -= value; }
@@ -58,7 +60,7 @@ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
///
/// The inheritance parent.
///
- protected IAvaloniaObject InheritanceParent
+ protected IAvaloniaObject? InheritanceParent
{
get
{
@@ -117,6 +119,22 @@ public IBinding this[IndexerDescriptor binding]
set { this.Bind(binding.Property, value); }
}
+ private ValueStore Values
+ {
+ get
+ {
+ if (_values is null)
+ {
+ _values = new ValueStore(this);
+
+ if (_batchUpdate)
+ _values.BeginBatchUpdate();
+ }
+
+ return _values;
+ }
+ }
+
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@@ -273,7 +291,8 @@ public Optional GetBaseValue(StyledPropertyBase property, BindingPriori
/// True if the property is animating, otherwise false.
public bool IsAnimating(AvaloniaProperty property)
{
- Contract.Requires(property != null);
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
VerifyAccess();
return _values?.IsAnimating(property) ?? false;
@@ -290,7 +309,8 @@ public bool IsAnimating(AvaloniaProperty property)
///
public bool IsSet(AvaloniaProperty property)
{
- Contract.Requires(property != null);
+ property = property ?? throw new ArgumentNullException(nameof(property));
+
VerifyAccess();
return _values?.IsSet(property) ?? false;
@@ -304,7 +324,7 @@ public bool IsSet(AvaloniaProperty property)
/// The priority of the value.
public void SetValue(
AvaloniaProperty property,
- object value,
+ object? value,
BindingPriority priority = BindingPriority.LocalValue)
{
property = property ?? throw new ArgumentNullException(nameof(property));
@@ -322,7 +342,7 @@ public void SetValue(
///
/// An if setting the property can be undone, otherwise null.
///
- public IDisposable SetValue(
+ public IDisposable? SetValue(
StyledPropertyBase property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
@@ -434,6 +454,28 @@ public void CoerceValue(StyledPropertyBase property)
_values?.CoerceValue(property);
}
+ public void BeginBatchUpdate()
+ {
+ if (_batchUpdate)
+ {
+ throw new InvalidOperationException("Batch update already in progress.");
+ }
+
+ _batchUpdate = true;
+ _values?.BeginBatchUpdate();
+ }
+
+ public void EndBatchUpdate()
+ {
+ if (!_batchUpdate)
+ {
+ throw new InvalidOperationException("No batch update in progress.");
+ }
+
+ _batchUpdate = false;
+ _values?.EndBatchUpdate();
+ }
+
///
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{
@@ -459,7 +501,7 @@ void IAvaloniaObject.InheritedPropertyChanged(
}
///
- Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
+ Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
{
return _propertyChanged?.GetInvocationList();
}
@@ -685,7 +727,8 @@ private T GetValueOrInheritedOrDefault(
{
var values = o._values;
- if (values?.TryGetValue(property, maxPriority, out value) == true)
+ if (values != null
+ && values.TryGetValue(property, maxPriority, out value) == true)
{
return value;
}
@@ -818,7 +861,7 @@ private string GetDescription(object o)
}
///
- /// Logs a mesage if the notification represents a binding error.
+ /// Logs a message if the notification represents a binding error.
///
/// The property being bound.
/// The binding notification.
@@ -835,7 +878,7 @@ private void LogIfError(AvaloniaProperty property, BindingValue value)
}
else
{
- LogBindingError(property, value.Error);
+ LogBindingError(property, value.Error!);
}
}
}
@@ -869,14 +912,14 @@ public DirectBindingSubscription(
{
_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();
diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
index 173c5c1a942..ae61f8f6429 100644
--- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs
+++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
@@ -585,6 +585,30 @@ public static IDisposable AddClassHandler(
});
}
+ ///
+ /// Subscribes to a property changed notifications for changes that originate from a
+ /// .
+ ///
+ /// The type of the property change sender.
+ /// /// The type of the property..
+ /// The property changed observable.
+ ///
+ /// The method to call. The parameters are the sender and the event args.
+ ///
+ /// A disposable that can be used to terminate the subscription.
+ public static IDisposable AddClassHandler(
+ this IObservable> observable,
+ Action> action) where TTarget : AvaloniaObject
+ {
+ return observable.Subscribe(e =>
+ {
+ if (e.Sender is TTarget target)
+ {
+ action(target, e);
+ }
+ });
+ }
+
///
/// Subscribes to a property changed notifications for changes that originate from a
/// .
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index 3ae0445e9bd..94aefb8869b 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -17,9 +17,9 @@ public abstract class AvaloniaProperty : IEquatable, IProperty
public static readonly object UnsetValue = new UnsetValueType();
private static int s_nextId;
- private readonly PropertyMetadata _defaultMetadata;
- private readonly Dictionary _metadata;
- private readonly Dictionary _metadataCache = new Dictionary();
+ private readonly AvaloniaPropertyMetadata _defaultMetadata;
+ private readonly Dictionary _metadata;
+ private readonly Dictionary _metadataCache = new Dictionary();
private bool _hasMetadataOverrides;
@@ -35,7 +35,7 @@ protected AvaloniaProperty(
string name,
Type valueType,
Type ownerType,
- PropertyMetadata metadata,
+ AvaloniaPropertyMetadata metadata,
Action notifying = null)
{
Contract.Requires(name != null);
@@ -48,7 +48,7 @@ protected AvaloniaProperty(
throw new ArgumentException("'name' may not contain periods.");
}
- _metadata = new Dictionary();
+ _metadata = new Dictionary();
Name = name;
PropertyType = valueType;
@@ -69,12 +69,12 @@ protected AvaloniaProperty(
protected AvaloniaProperty(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
{
Contract.Requires(source != null);
Contract.Requires(ownerType != null);
- _metadata = new Dictionary();
+ _metadata = new Dictionary();
Name = source.Name;
PropertyType = source.PropertyType;
@@ -419,7 +419,7 @@ public override int GetHashCode()
///
/// The property metadata.
///
- public PropertyMetadata GetMetadata() where T : IAvaloniaObject
+ public AvaloniaPropertyMetadata GetMetadata() where T : IAvaloniaObject
{
return GetMetadata(typeof(T));
}
@@ -432,7 +432,7 @@ public PropertyMetadata GetMetadata() where T : IAvaloniaObject
/// The property metadata.
///
///
- public PropertyMetadata GetMetadata(Type type)
+ public AvaloniaPropertyMetadata GetMetadata(Type type)
{
if (!_hasMetadataOverrides)
{
@@ -465,9 +465,9 @@ public override string ToString()
/// Uses the visitor pattern to resolve an untyped property to a typed property.
///
/// The type of user data passed.
- /// The visitor which will accept the typed property.
+ /// The visitor which will accept the typed property.
/// The user data to pass.
- public abstract void Accept(IAvaloniaPropertyVisitor vistor, ref TData data)
+ public abstract void Accept(IAvaloniaPropertyVisitor visitor, ref TData data)
where TData : struct;
///
@@ -521,7 +521,7 @@ internal abstract IDisposable RouteBind(
///
/// The type.
/// The metadata.
- protected void OverrideMetadata(Type type, PropertyMetadata metadata)
+ protected void OverrideMetadata(Type type, AvaloniaPropertyMetadata metadata)
{
Contract.Requires(type != null);
Contract.Requires(metadata != null);
@@ -542,14 +542,14 @@ protected void OverrideMetadata(Type type, PropertyMetadata metadata)
protected abstract IObservable GetChanged();
- private PropertyMetadata GetMetadataWithOverrides(Type type)
+ private AvaloniaPropertyMetadata GetMetadataWithOverrides(Type type)
{
if (type is null)
{
throw new ArgumentNullException(nameof(type));
}
- if (_metadataCache.TryGetValue(type, out PropertyMetadata result))
+ if (_metadataCache.TryGetValue(type, out AvaloniaPropertyMetadata result))
{
return result;
}
diff --git a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
index c1a2832fdee..896d86e29da 100644
--- a/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
@@ -58,8 +58,8 @@ public AvaloniaPropertyChangedEventArgs(
///
/// This will usually be true, except in
///
- /// which recieves notifications for all changes to property values, whether a value with a higher
- /// priority is present or not. When this property is false, the change that is being signalled
+ /// which receives notifications for all changes to property values, whether a value with a higher
+ /// priority is present or not. When this property is false, the change that is being signaled
/// has not resulted in a change to the property value on the object.
///
public bool IsEffectiveValueChange { get; private set; }
diff --git a/src/Avalonia.Base/PropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
similarity index 85%
rename from src/Avalonia.Base/PropertyMetadata.cs
rename to src/Avalonia.Base/AvaloniaPropertyMetadata.cs
index 806051e1d16..2963567b14e 100644
--- a/src/Avalonia.Base/PropertyMetadata.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
@@ -5,15 +5,15 @@ namespace Avalonia
///
/// Base class for avalonia property metadata.
///
- public class PropertyMetadata
+ public class AvaloniaPropertyMetadata
{
private BindingMode _defaultBindingMode;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The default binding mode.
- public PropertyMetadata(
+ public AvaloniaPropertyMetadata(
BindingMode defaultBindingMode = BindingMode.Default)
{
_defaultBindingMode = defaultBindingMode;
@@ -37,7 +37,7 @@ public BindingMode DefaultBindingMode
/// The base metadata to merge.
/// The property to which the metadata is being applied.
public virtual void Merge(
- PropertyMetadata baseMetadata,
+ AvaloniaPropertyMetadata baseMetadata,
AvaloniaProperty property)
{
if (_defaultBindingMode == BindingMode.Default)
diff --git a/src/Avalonia.Base/AvaloniaProperty`1.cs b/src/Avalonia.Base/AvaloniaProperty`1.cs
index d5549e979b6..247ea0a25a5 100644
--- a/src/Avalonia.Base/AvaloniaProperty`1.cs
+++ b/src/Avalonia.Base/AvaloniaProperty`1.cs
@@ -23,7 +23,7 @@ public abstract class AvaloniaProperty : AvaloniaProperty
protected AvaloniaProperty(
string name,
Type ownerType,
- PropertyMetadata metadata,
+ AvaloniaPropertyMetadata metadata,
Action notifying = null)
: base(name, typeof(TValue), ownerType, metadata, notifying)
{
@@ -40,7 +40,7 @@ protected AvaloniaProperty(
protected AvaloniaProperty(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: this(source as AvaloniaProperty ?? throw new InvalidOperationException(), ownerType, metadata)
{
}
@@ -54,7 +54,7 @@ protected AvaloniaProperty(
protected AvaloniaProperty(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: base(source, ownerType, metadata)
{
_changed = source._changed;
diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs
index d43b4e04bb5..2c7f34c5be1 100644
--- a/src/Avalonia.Base/Collections/AvaloniaList.cs
+++ b/src/Avalonia.Base/Collections/AvaloniaList.cs
@@ -63,6 +63,15 @@ public AvaloniaList()
_inner = new List();
}
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// Initial list capacity.
+ public AvaloniaList(int capacity)
+ {
+ _inner = new List(capacity);
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -175,6 +184,15 @@ object IList.this[int index]
set { this[index] = (T)value; }
}
+ ///
+ /// Gets or sets the total number of elements the internal data structure can hold without resizing.
+ ///
+ public int Capacity
+ {
+ get => _inner.Capacity;
+ set => _inner.Capacity = value;
+ }
+
///
/// Adds an item to the collection.
///
@@ -436,6 +454,28 @@ public void MoveRange(int oldIndex, int count, int newIndex)
}
}
+ ///
+ /// Ensures that the capacity of the list is at least .
+ ///
+ /// The capacity.
+ public void EnsureCapacity(int capacity)
+ {
+ // Adapted from List implementation.
+ var currentCapacity = _inner.Capacity;
+
+ if (currentCapacity < capacity)
+ {
+ var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2;
+
+ if (newCapacity < capacity)
+ {
+ newCapacity = capacity;
+ }
+
+ _inner.Capacity = newCapacity;
+ }
+ }
+
///
/// Removes an item from the collection.
///
@@ -615,24 +655,6 @@ void ICollection.CopyTo(Array array, int index)
///
Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList();
- private void EnsureCapacity(int capacity)
- {
- // Adapted from List implementation.
- var currentCapacity = _inner.Capacity;
-
- if (currentCapacity < capacity)
- {
- var newCapacity = currentCapacity == 0 ? 4 : currentCapacity * 2;
-
- if (newCapacity < capacity)
- {
- newCapacity = capacity;
- }
-
- _inner.Capacity = newCapacity;
- }
- }
-
///
/// Raises the event with an add action.
///
diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
index e50e100d327..2cd9758f128 100644
--- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs
+++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs
@@ -1271,7 +1271,7 @@ public void Reverse()
/// Reverses the elements in a range of this list. Following a call to this
/// method, an element in the range given by index and count
/// which was previously located at index i will now be located at
- /// index index + (index + count - i - 1).
+ /// index + (index + count - i - 1).
///
public void Reverse(int index, int count)
{
diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs
index dc8421fb356..42f941da0c5 100644
--- a/src/Avalonia.Base/Data/BindingOperations.cs
+++ b/src/Avalonia.Base/Data/BindingOperations.cs
@@ -45,7 +45,7 @@ public static IDisposable Apply(
case BindingMode.OneWay:
return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority);
case BindingMode.TwoWay:
- return new CompositeDisposable(
+ return new TwoWayBindingDisposable(
target.Bind(property, binding.Subject, binding.Priority),
target.GetObservable(property).Subscribe(binding.Subject));
case BindingMode.OneTime:
@@ -88,6 +88,32 @@ public static IDisposable Apply(
throw new ArgumentException("Invalid binding mode.");
}
}
+
+ private sealed class TwoWayBindingDisposable : IDisposable
+ {
+ private readonly IDisposable _first;
+ private readonly IDisposable _second;
+ private bool _isDisposed;
+
+ public TwoWayBindingDisposable(IDisposable first, IDisposable second)
+ {
+ _first = first;
+ _second = second;
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _first.Dispose();
+ _second.Dispose();
+
+ _isDisposed = true;
+ }
+ }
}
public sealed class DoNothingType
diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs
index 6e3c9ae67b4..93948e54ee9 100644
--- a/src/Avalonia.Base/Data/BindingValue.cs
+++ b/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 @@ private BindingValue(BindingValueType type, [AllowNull] T value, Exception? erro
/// Gets a value indicating whether the binding value represents either a binding or data
/// validation error.
///
- public bool HasError => Type.HasFlagCustom(BindingValueType.HasError);
+ public bool HasError => Type.HasAllFlags(BindingValueType.HasError);
///
/// Gets a value indicating whether the binding value has a value.
///
- public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue);
+ public bool HasValue => Type.HasAllFlags(BindingValueType.HasValue);
///
/// Gets the type of the binding value.
@@ -358,6 +359,7 @@ public static BindingValue DataValidationError(Exception e, Optional fallb
e);
}
+ [Conditional("DEBUG")]
private static void ValidateValue([AllowNull] T value)
{
if (value is UnsetValueType)
diff --git a/src/Avalonia.Base/Data/Converters/BoolConverters.cs b/src/Avalonia.Base/Data/Converters/BoolConverters.cs
index 6740c2168f8..3985c5e32f4 100644
--- a/src/Avalonia.Base/Data/Converters/BoolConverters.cs
+++ b/src/Avalonia.Base/Data/Converters/BoolConverters.cs
@@ -12,5 +12,17 @@ public static class BoolConverters
///
public static readonly IMultiValueConverter And =
new FuncMultiValueConverter(x => x.All(y => y));
+
+ ///
+ /// A multi-value converter that returns true if any of the inputs is true.
+ ///
+ public static readonly IMultiValueConverter Or =
+ new FuncMultiValueConverter(x => x.Any(y => y));
+
+ ///
+ /// A value converter that returns true when input is false and false when input is true.
+ ///
+ public static readonly IValueConverter Not =
+ new FuncValueConverter(x => !x);
}
}
diff --git a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
index 7e7417d2f5a..7ff0a8ceca8 100644
--- a/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
+++ b/src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
@@ -63,7 +63,7 @@ public MethodToCommandConverter(Delegate action)
}
}
- void OnPropertyChanged(object sender,PropertyChangedEventArgs args)
+ void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.PropertyName)
|| dependencyProperties?.Contains(args.PropertyName) == true)
@@ -88,12 +88,7 @@ static Action CreateExecute(object target
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
-
+ var instance = ConvertTarget(target, method);
var call = Expression.Call
(
@@ -114,11 +109,7 @@ static Action CreateExecute(object target
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
+ var instance = ConvertTarget(target, method);
Expression body;
@@ -167,11 +158,7 @@ static Func CreateCanExecute(object target
, System.Reflection.MethodInfo method)
{
var parameter = Expression.Parameter(typeof(object), "parameter");
- var instance = Expression.Convert
- (
- Expression.Constant(target),
- method.DeclaringType
- );
+ var instance = ConvertTarget(target, method);
var call = Expression.Call
(
instance,
@@ -183,6 +170,8 @@ static Func CreateCanExecute(object target
.Compile();
}
+ private static Expression? ConvertTarget(object? target, MethodInfo method) =>
+ target is null ? null : Expression.Convert(Expression.Constant(target), method.DeclaringType);
internal class WeakPropertyChangedProxy
{
@@ -224,7 +213,7 @@ void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
else
Unsubscribe();
}
-
+
}
}
}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
index 324279e9f0b..2a580fe75fb 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
@@ -20,7 +20,7 @@ public interface IDataValidationPlugin
///
/// A weak reference to the object.
/// The property name.
- /// The inner property accessor used to aceess the property.
+ /// The inner property accessor used to access the property.
///
/// An interface through which future interactions with the
/// property will be made.
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index 8fc2a7b77c1..d600603d5c2 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using Avalonia.Utilities;
@@ -11,8 +12,11 @@ namespace Avalonia.Data.Core.Plugins
///
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
+ private readonly Dictionary<(Type, string), PropertyInfo> _propertyLookup =
+ new Dictionary<(Type, string), PropertyInfo>();
+
///
- public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null;
+ public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null;
///
/// Starts monitoring the value of a property on an object.
@@ -30,7 +34,7 @@ public IPropertyAccessor Start(WeakReference reference, string propertyN
reference.TryGetTarget(out object instance);
- var p = GetPropertyWithName(instance.GetType(), propertyName);
+ var p = GetFirstPropertyWithName(instance.GetType(), propertyName);
if (p != null)
{
@@ -44,12 +48,40 @@ public IPropertyAccessor Start(WeakReference reference, string propertyN
}
}
- private static PropertyInfo GetPropertyWithName(Type type, string propertyName)
+ private PropertyInfo GetFirstPropertyWithName(Type type, string propertyName)
{
- const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public |
- BindingFlags.Static | BindingFlags.Instance;
+ var key = (type, propertyName);
+
+ if (!_propertyLookup.TryGetValue(key, out PropertyInfo propertyInfo))
+ {
+ propertyInfo = TryFindAndCacheProperty(type, propertyName);
+ }
+
+ return propertyInfo;
+ }
+
+ private PropertyInfo TryFindAndCacheProperty(Type type, string propertyName)
+ {
+ PropertyInfo found = null;
+
+ const BindingFlags bindingFlags =
+ BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
+
+ var properties = type.GetProperties(bindingFlags);
+
+ foreach (PropertyInfo propertyInfo in properties)
+ {
+ if (propertyInfo.Name == propertyName)
+ {
+ found = propertyInfo;
+
+ break;
+ }
+ }
+
+ _propertyLookup.Add((type, propertyName), found);
- return type.GetProperty(propertyName, bindingFlags);
+ return found;
}
private class Accessor : PropertyAccessorBase, IWeakSubscriber
@@ -58,7 +90,7 @@ private class Accessor : PropertyAccessorBase, IWeakSubscriber reference, PropertyInfo property)
+ public Accessor(WeakReference reference, PropertyInfo property)
{
Contract.Requires(reference != null);
Contract.Requires(property != null);
diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs
index d0a918dc884..363d3da0eff 100644
--- a/src/Avalonia.Base/Data/Core/SettableNode.cs
+++ b/src/Avalonia.Base/Data/Core/SettableNode.cs
@@ -15,7 +15,8 @@ public bool SetTargetValue(object value, BindingPriority priority)
private bool ShouldNotSet(object value)
{
- if (PropertyType == null)
+ var propertyType = PropertyType;
+ if (propertyType == null)
{
return false;
}
@@ -37,7 +38,7 @@ private bool ShouldNotSet(object value)
return false;
}
- if (PropertyType.IsValueType)
+ if (propertyType.IsValueType)
{
return lastValue.Equals(value);
}
diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs
index 023999f5c51..e868b71fcd1 100644
--- a/src/Avalonia.Base/Data/Core/StreamNode.cs
+++ b/src/Avalonia.Base/Data/Core/StreamNode.cs
@@ -20,7 +20,7 @@ public StreamNode(IStreamPlugin customPlugin)
protected override void StartListeningCore(WeakReference reference)
{
- GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
+ _subscription = GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged);
}
protected override void StopListeningCore()
diff --git a/src/Avalonia.Base/Data/Core/TypeCastNode.cs b/src/Avalonia.Base/Data/Core/TypeCastNode.cs
new file mode 100644
index 00000000000..476fd5527f8
--- /dev/null
+++ b/src/Avalonia.Base/Data/Core/TypeCastNode.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Data.Core
+{
+ public class TypeCastNode : ExpressionNode
+ {
+ public override string Description => $"as {TargetType.FullName}";
+
+ public Type TargetType { get; }
+
+ public TypeCastNode(Type type)
+ {
+ TargetType = type;
+ }
+
+ protected virtual object Cast(object value)
+ {
+ return TargetType.IsInstanceOfType(value) ? value : null;
+ }
+
+ protected override void StartListeningCore(WeakReference reference)
+ {
+ if (reference.TryGetTarget(out object target))
+ {
+ target = Cast(target);
+ reference = target == null ? NullReference : new WeakReference(target);
+ }
+
+ base.StartListeningCore(reference);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs
index cc3baa45301..4d07b53dd28 100644
--- a/src/Avalonia.Base/Data/IndexerBinding.cs
+++ b/src/Avalonia.Base/Data/IndexerBinding.cs
@@ -1,6 +1,4 @@
-using System;
-
-namespace Avalonia.Data
+namespace Avalonia.Data
{
public class IndexerBinding : IBinding
{
@@ -24,23 +22,7 @@ public InstancedBinding Initiate(
object anchor = null,
bool enableDataValidation = false)
{
- var mode = Mode == BindingMode.Default ?
- targetProperty.GetMetadata(target.GetType()).DefaultBindingMode :
- Mode;
-
- switch (mode)
- {
- case BindingMode.OneTime:
- return InstancedBinding.OneTime(Source.GetObservable(Property));
- case BindingMode.OneWay:
- return InstancedBinding.OneWay(Source.GetObservable(Property));
- case BindingMode.OneWayToSource:
- return InstancedBinding.OneWayToSource(Source.GetSubject(Property));
- case BindingMode.TwoWay:
- return InstancedBinding.TwoWay(Source.GetSubject(Property));
- default:
- throw new NotSupportedException("Unsupported BindingMode.");
- }
+ return new InstancedBinding(Source.GetSubject(Property), Mode, BindingPriority.LocalValue);
}
}
}
diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs
index 8e044d7896b..9dec399e35b 100644
--- a/src/Avalonia.Base/Data/Optional.cs
+++ b/src/Avalonia.Base/Data/Optional.cs
@@ -153,4 +153,18 @@ public TResult GetValueOrDefault([AllowNull] TResult defaultValue)
///
public static Optional Empty => default;
}
+
+ public static class OptionalExtensions
+ {
+ ///
+ /// Casts the type of an using only the C# cast operator.
+ ///
+ /// The target type.
+ /// The binding value.
+ /// The cast value.
+ public static Optional Cast(this Optional value)
+ {
+ return value.HasValue ? new Optional((T)value.Value) : Optional.Empty;
+ }
+ }
}
diff --git a/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs b/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
index 7f094259050..4b9f12ddf8d 100644
--- a/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
+++ b/src/Avalonia.Base/Diagnostics/IAvaloniaObjectDebug.cs
@@ -1,5 +1,7 @@
using System;
+#nullable enable
+
namespace Avalonia.Diagnostics
{
///
@@ -14,6 +16,6 @@ public interface IAvaloniaObjectDebug
///
/// The subscribers or null if no subscribers.
///
- Delegate[] GetPropertyChangedSubscribers();
+ Delegate[]? GetPropertyChangedSubscribers();
}
}
diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs
index a2f113adb74..a057ad22544 100644
--- a/src/Avalonia.Base/DirectPropertyBase.cs
+++ b/src/Avalonia.Base/DirectPropertyBase.cs
@@ -13,7 +13,7 @@ namespace Avalonia
/// The type of the property's value.
///
/// Whereas is typed on the owner type, this base
- /// class provides a non-owner-typed interface to a direct poperty.
+ /// class provides a non-owner-typed interface to a direct property.
///
public abstract class DirectPropertyBase : AvaloniaProperty
{
@@ -26,7 +26,7 @@ public abstract class DirectPropertyBase : AvaloniaProperty
protected DirectPropertyBase(
string name,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: base(name, ownerType, metadata)
{
}
@@ -41,7 +41,7 @@ protected DirectPropertyBase(
protected DirectPropertyBase(
AvaloniaProperty source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: this(source as DirectPropertyBase ?? throw new InvalidOperationException(), ownerType, metadata)
{
}
@@ -55,7 +55,7 @@ protected DirectPropertyBase(
protected DirectPropertyBase(
DirectPropertyBase source,
Type ownerType,
- PropertyMetadata metadata)
+ AvaloniaPropertyMetadata metadata)
: base(source, ownerType, metadata)
{
}
@@ -123,9 +123,9 @@ public void OverrideMetadata(Type type, DirectPropertyMetadata metadata)
}
///
- public override void Accept(IAvaloniaPropertyVisitor vistor, ref TData data)
+ public override void Accept(IAvaloniaPropertyVisitor visitor, ref TData data)
{
- vistor.Visit(this, ref data);
+ visitor.Visit(this, ref data);
}
///
diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs
index 59a60507dc3..eabdef05ed6 100644
--- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs
+++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs
@@ -5,7 +5,7 @@ namespace Avalonia
///
/// Metadata for direct avalonia properties.
///
- public class DirectPropertyMetadata : PropertyMetadata, IDirectPropertyMetadata
+ public class DirectPropertyMetadata : AvaloniaPropertyMetadata, IDirectPropertyMetadata
{
///
/// Initializes a new instance of the class.
@@ -38,7 +38,7 @@ public DirectPropertyMetadata(
///
/// Data validation is validation performed at the target of a binding, for example in a
/// view model using the INotifyDataErrorInfo interface. Only certain properties on a
- /// control (such as a TextBox's Text property) will be interested in recieving data
+ /// control (such as a TextBox's Text property) will be interested in receiving data
/// validation messages so this feature must be explicitly enabled by setting this flag.
///
public bool? EnableDataValidation { get; private set; }
@@ -47,7 +47,7 @@ public DirectPropertyMetadata(
object IDirectPropertyMetadata.UnsetValue => UnsetValue;
///
- public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
+ public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property)
{
base.Merge(baseMetadata, property);
diff --git a/src/Avalonia.Base/EnumExtensions.cs b/src/Avalonia.Base/EnumExtensions.cs
index 1e4864283f9..19eb42a7009 100644
--- a/src/Avalonia.Base/EnumExtensions.cs
+++ b/src/Avalonia.Base/EnumExtensions.cs
@@ -9,12 +9,70 @@ namespace Avalonia
public static class EnumExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static unsafe bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum
+ [Obsolete("This method is obsolete. Use HasAllFlags instead.")]
+ public static bool HasFlagCustom(this T value, T flag) where T : unmanaged, Enum
+ => value.HasAllFlags(flag);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool HasAllFlags(this T value, T flags) where T : unmanaged, Enum
{
- var intValue = *(int*)&value;
- var intFlag = *(int*)&flag;
+ if (sizeof(T) == 1)
+ {
+ var byteValue = Unsafe.As(ref value);
+ var byteFlags = Unsafe.As(ref flags);
+ return (byteValue & byteFlags) == byteFlags;
+ }
+ else if (sizeof(T) == 2)
+ {
+ var shortValue = Unsafe.As(ref value);
+ var shortFlags = Unsafe.As(ref flags);
+ return (shortValue & shortFlags) == shortFlags;
+ }
+ else if (sizeof(T) == 4)
+ {
+ var intValue = Unsafe.As(ref value);
+ var intFlags = Unsafe.As(ref flags);
+ return (intValue & intFlags) == intFlags;
+ }
+ else if (sizeof(T) == 8)
+ {
+ var longValue = Unsafe.As(ref value);
+ var longFlags = Unsafe.As(ref flags);
+ return (longValue & longFlags) == longFlags;
+ }
+ else
+ throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf() + " are not supported");
+ }
- return (intValue & intFlag) == intFlag;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe bool HasAnyFlag(this T value, T flags) where T : unmanaged, Enum
+ {
+ if (sizeof(T) == 1)
+ {
+ var byteValue = Unsafe.As(ref value);
+ var byteFlags = Unsafe.As(ref flags);
+ return (byteValue & byteFlags) != 0;
+ }
+ else if (sizeof(T) == 2)
+ {
+ var shortValue = Unsafe.As(ref value);
+ var shortFlags = Unsafe.As(ref flags);
+ return (shortValue & shortFlags) != 0;
+ }
+ else if (sizeof(T) == 4)
+ {
+ var intValue = Unsafe.As(ref value);
+ var intFlags = Unsafe.As(ref flags);
+ return (intValue & intFlags) != 0;
+ }
+ else if (sizeof(T) == 8)
+ {
+ var longValue = Unsafe.As(ref value);
+ var longFlags = Unsafe.As(ref flags);
+ return (longValue & longFlags) != 0;
+ }
+ else
+ throw new NotSupportedException("Enum with size of " + Unsafe.SizeOf() + " are not supported");
}
}
}
diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs
index 0452f77d4ce..2e992f8616d 100644
--- a/src/Avalonia.Base/IAvaloniaObject.cs
+++ b/src/Avalonia.Base/IAvaloniaObject.cs
@@ -1,6 +1,8 @@
using System;
using Avalonia.Data;
+#nullable enable
+
namespace Avalonia
{
///
@@ -11,7 +13,7 @@ public interface IAvaloniaObject
///
/// Raised when a value changes on this object.
///
- event EventHandler PropertyChanged;
+ event EventHandler? PropertyChanged;
///
/// Clears an 's local value.
@@ -75,7 +77,10 @@ public interface IAvaloniaObject
/// The property.
/// The value.
/// The priority of the value.
- IDisposable SetValue(
+ ///
+ /// An if setting the property can be undone, otherwise null.
+ ///
+ IDisposable? SetValue(
StyledPropertyBase property,
T value,
BindingPriority priority = BindingPriority.LocalValue);
diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs
index 3c19b47a055..2ad220dddd7 100644
--- a/src/Avalonia.Base/Logging/LogArea.cs
+++ b/src/Avalonia.Base/Logging/LogArea.cs
@@ -34,5 +34,10 @@ public static class LogArea
/// The log event comes from the control system.
///
public const string Control = "Control";
+
+ ///
+ /// The log event comes from Win32Platform.
+ ///
+ public const string Win32Platform = nameof(Win32Platform);
}
}
diff --git a/src/Avalonia.Base/Logging/DebugLogSink.cs b/src/Avalonia.Base/Logging/TraceLogSink.cs
similarity index 92%
rename from src/Avalonia.Base/Logging/DebugLogSink.cs
rename to src/Avalonia.Base/Logging/TraceLogSink.cs
index 3695afa8605..f5978443788 100644
--- a/src/Avalonia.Base/Logging/DebugLogSink.cs
+++ b/src/Avalonia.Base/Logging/TraceLogSink.cs
@@ -6,12 +6,12 @@
namespace Avalonia.Logging
{
- public class DebugLogSink : ILogSink
+ public class TraceLogSink : ILogSink
{
private readonly LogEventLevel _level;
private readonly IList _areas;
- public DebugLogSink(
+ public TraceLogSink(
LogEventLevel minimumLevel,
IList areas = null)
{
@@ -28,7 +28,7 @@ public void Log(LogEventLevel level, string area, object source, string messageT
{
if (IsEnabled(level, area))
{
- Debug.WriteLine(Format(area, messageTemplate, source));
+ Trace.WriteLine(Format(area, messageTemplate, source));
}
}
@@ -36,7 +36,7 @@ public void Log(LogEventLevel level, string area, object source, string mess
{
if (IsEnabled(level, area))
{
- Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0));
+ Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0));
}
}
@@ -44,7 +44,7 @@ public void Log(LogEventLevel level, string area, object source, string
{
if (IsEnabled(level, area))
{
- Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1));
+ Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1));
}
}
@@ -52,7 +52,7 @@ public void Log(LogEventLevel level, string area, object source, str
{
if (IsEnabled(level, area))
{
- Debug.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2));
+ Trace.WriteLine(Format(area, messageTemplate, source, propertyValue0, propertyValue1, propertyValue2));
}
}
@@ -60,7 +60,7 @@ public void Log(LogEventLevel level, string area, object source, string messageT
{
if (IsEnabled(level, area))
{
- Debug.WriteLine(Format(area, messageTemplate, source, propertyValues));
+ Trace.WriteLine(Format(area, messageTemplate, source, propertyValues));
}
}
diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs
index 91f5e81863d..b6f0f3a47c5 100644
--- a/src/Avalonia.Base/Metadata/NullableAttributes.cs
+++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs
@@ -1,6 +1,5 @@
#pragma warning disable MA0048 // File name must match type name
#define INTERNAL_NULLABLE_ATTRIBUTES
-#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
@@ -10,6 +9,7 @@
namespace System.Diagnostics.CodeAnalysis
{
+#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
/// Specifies that null is allowed as an input even if the corresponding type disallows it.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
#if INTERNAL_NULLABLE_ATTRIBUTES
@@ -136,5 +136,82 @@ sealed class DoesNotReturnIfAttribute : Attribute
/// Gets the condition parameter value.
public bool ParameterValue { get; }
}
-}
+#endif // NETSTANDARD2_0 attributes
+
+#if NETSTANDARD2_1 || NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
+ ///
+ /// Specifies that the method or property will ensure that the listed field and property members have
+ /// not- values.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+#if INTERNAL_NULLABLE_ATTRIBUTES
+ internal
+#else
+ public
#endif
+ sealed class MemberNotNullAttribute : Attribute
+ {
+ /// Gets field or property member names.
+ public string[] Members { get; }
+
+ /// Initializes the attribute with a field or property member.
+ /// The field or property member that is promised to be not-null.
+ public MemberNotNullAttribute(string member)
+ {
+ Members = new[] { member };
+ }
+
+ /// Initializes the attribute with the list of field and property members.
+ /// The list of field and property members that are promised to be not-null.
+ public MemberNotNullAttribute(params string[] members)
+ {
+ Members = members;
+ }
+ }
+
+ ///
+ /// Specifies that the method or property will ensure that the listed field and property members have
+ /// non- values when returning with the specified return value condition.
+ ///
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
+#if INTERNAL_NULLABLE_ATTRIBUTES
+ internal
+#else
+ public
+#endif
+ sealed class MemberNotNullWhenAttribute : Attribute
+ {
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+
+ /// Gets field or property member names.
+ public string[] Members { get; }
+
+ /// Initializes the attribute with the specified return value condition and a field or property member.
+ ///
+ /// The return value condition. If the method returns this value,
+ /// the associated parameter will not be .
+ ///
+ /// The field or property member that is promised to be not-.
+ public MemberNotNullWhenAttribute(bool returnValue, string member)
+ {
+ ReturnValue = returnValue;
+ Members = new[] { member };
+ }
+
+ /// Initializes the attribute with the specified return value condition and list of field and property members.
+ ///
+ ///
+ /// The return value condition. If the method returns this value,
+ /// the associated parameter will not be .
+ ///
+ /// The list of field and property members that are promised to be not-null.
+ public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
+ {
+ ReturnValue = returnValue;
+ Members = members;
+ }
+ }
+#endif // NETSTANDARD2_1 attributes
+}
+
diff --git a/src/Avalonia.Base/Metadata/XmlnsPrefixAttribute.cs b/src/Avalonia.Base/Metadata/XmlnsPrefixAttribute.cs
new file mode 100644
index 00000000000..ad017822e4a
--- /dev/null
+++ b/src/Avalonia.Base/Metadata/XmlnsPrefixAttribute.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace Avalonia.Metadata
+{
+ ///
+ /// Use to predefine the prefix associated to an xml namespace in a xaml file
+ ///
+ ///
+ /// example:
+ /// [assembly: XmlnsPrefix("https://github.com/avaloniaui", "av")]
+ /// xaml:
+ /// xmlns:av="https://github.com/avaloniaui"
+ ///
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class XmlnsPrefixAttribute : Attribute
+ {
+ ///
+ /// Constructor
+ ///
+ /// XML namespce
+ /// recommended prefix
+ public XmlnsPrefixAttribute(string xmlNamespace, string prefix)
+ {
+ XmlNamespace = xmlNamespace ?? throw new ArgumentNullException(nameof(xmlNamespace));
+
+ Prefix = prefix ?? throw new ArgumentNullException(nameof(prefix));
+ }
+
+ ///
+ /// XML Namespace
+ ///
+ public string XmlNamespace { get; }
+
+ ///
+ /// New Xml Namespace
+ ///
+ public string Prefix { get; }
+ }
+}
diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs
index 13b247de515..b054c186ae2 100644
--- a/src/Avalonia.Base/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs
@@ -5,8 +5,18 @@
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
+#if SIGNED_BUILD
+[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
+[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+#else
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]
+[assembly: InternalsVisibleTo("Avalonia.Visuals")]
+#endif
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
index 0d563947e7d..362736eb064 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
@@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
///
/// Represents an untyped interface to .
///
- internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+ internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
{
+ void Start(bool ignoreBatchUpdate);
}
///
@@ -22,6 +23,8 @@ internal class BindingEntry : IBindingEntry, IPriorityValueEntry, IObserve
private readonly IAvaloniaObject _owner;
private IValueSink _sink;
private IDisposable? _subscription;
+ private bool _isSubscribed;
+ private bool _batchUpdate;
private Optional _value;
public BindingEntry(
@@ -39,10 +42,20 @@ public BindingEntry(
}
public StyledPropertyBase Property { get; }
- public BindingPriority Priority { get; }
+ public BindingPriority Priority { get; private set; }
public IObservable> Source { get; }
Optional IValue.GetValue() => _value.ToObject();
+ public void BeginBatchUpdate() => _batchUpdate = true;
+
+ public void EndBatchUpdate()
+ {
+ _batchUpdate = false;
+
+ if (_sink is ValueStore)
+ Start();
+ }
+
public Optional GetValue(BindingPriority maxPriority)
{
return Priority >= maxPriority ? _value : Optional.Empty;
@@ -52,14 +65,21 @@ public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
- _sink.Completed(Property, this, _value);
+ OnCompleted();
}
- public void OnCompleted() => _sink.Completed(Property, this, _value);
+ public void OnCompleted()
+ {
+ 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 value)
@@ -79,13 +99,39 @@ public void OnNext(BindingValue value)
}
}
- public void Start()
+ public void Start() => Start(false);
+
+ public void Start(bool ignoreBatchUpdate)
{
- _subscription = Source.Subscribe(this);
+ // 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. 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);
+ }
}
public void Reparent(IValueSink sink) => _sink = sink;
-
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ Priority));
+ }
+
private void UpdateValue(BindingValue value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
diff --git a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
index 46f6f9a1378..dc4a1d88c1c 100644
--- a/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
@@ -1,23 +1,31 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
#nullable enable
namespace Avalonia.PropertyStore
{
+ ///
+ /// Represents an untyped interface to .
+ ///
+ internal interface IConstantValueEntry : IPriorityValueEntry, IDisposable
+ {
+ }
+
///
/// Stores a value with a priority in a or
/// .
///
/// The property type.
- internal class ConstantValueEntry : IPriorityValueEntry, IDisposable
+ internal class ConstantValueEntry : IPriorityValueEntry, IConstantValueEntry
{
private IValueSink _sink;
private Optional _value;
public ConstantValueEntry(
StyledPropertyBase property,
- T value,
+ [AllowNull] T value,
BindingPriority priority,
IValueSink sink)
{
@@ -28,7 +36,7 @@ public ConstantValueEntry(
}
public StyledPropertyBase Property { get; }
- public BindingPriority Priority { get; }
+ public BindingPriority Priority { get; private set; }
Optional IValue.GetValue() => _value.ToObject();
public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@@ -36,7 +44,30 @@ public Optional GetValue(BindingPriority maxPriority = BindingPriority.Animat
return Priority >= maxPriority ? _value : Optional.Empty;
}
- public void Dispose() => _sink.Completed(Property, this, _value);
+ public void Dispose()
+ {
+ var oldValue = _value;
+ _value = default;
+ Priority = BindingPriority.Unset;
+ _sink.Completed(Property, this, oldValue);
+ }
+
public void Reparent(IValueSink sink) => _sink = sink;
+ public void Start() { }
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ Priority));
+ }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
new file mode 100644
index 00000000000..af4faf989c9
--- /dev/null
+++ b/src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
@@ -0,0 +1,8 @@
+namespace Avalonia.PropertyStore
+{
+ internal interface IBatchUpdate
+ {
+ void BeginBatchUpdate();
+ void EndBatchUpdate();
+ }
+}
diff --git a/src/Avalonia.Base/PropertyStore/IValue.cs b/src/Avalonia.Base/PropertyStore/IValue.cs
index 249cfc360c1..7f5245bb45d 100644
--- a/src/Avalonia.Base/PropertyStore/IValue.cs
+++ b/src/Avalonia.Base/PropertyStore/IValue.cs
@@ -9,8 +9,15 @@ namespace Avalonia.PropertyStore
///
internal interface IValue
{
- Optional GetValue();
BindingPriority Priority { get; }
+ Optional GetValue();
+ void Start();
+ void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue);
}
///
diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
index 859e9ba81cf..8fe2ad77941 100644
--- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
@@ -24,5 +24,21 @@ public Optional GetValue(BindingPriority maxPriority)
}
public void SetValue(T value) => _value = value;
+ public void Start() { }
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ BindingPriority.LocalValue));
+ }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
index 5e223cad604..556f1a62697 100644
--- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs
+++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
/// entries (sorted first by priority and then in the order
/// they were added) plus a local value.
///
- internal class PriorityValue : IValue, IValueSink
+ internal class PriorityValue : IValue, IValueSink, IBatchUpdate
{
private readonly IAvaloniaObject _owner;
private readonly IValueSink _sink;
@@ -26,6 +26,8 @@ internal class PriorityValue : IValue, IValueSink
private readonly Func? _coerceValue;
private Optional _localValue;
private Optional _value;
+ private bool _isCalculatingValue;
+ private bool _batchUpdate;
public PriorityValue(
IAvaloniaObject owner,
@@ -53,6 +55,18 @@ public PriorityValue(
existing.Reparent(this);
_entries.Add(existing);
+ if (existing is IBindingEntry binding &&
+ existing.Priority == BindingPriority.LocalValue)
+ {
+ // Bit of a special case here: if we have a local value binding that is being
+ // promoted to a priority value we need to make sure the binding is subscribed
+ // even if we've got a batch operation in progress because otherwise we don't know
+ // whether the binding or a subsequent SetValue with local priority will win. A
+ // notification won't be sent during batch update anyway because it will be
+ // caught and stored for later by the ValueStore.
+ binding.Start(ignoreBatchUpdate: true);
+ }
+
var v = existing.GetValue();
if (v.HasValue)
@@ -78,6 +92,28 @@ public PriorityValue(
public IReadOnlyList> Entries => _entries;
Optional IValue.GetValue() => _value.ToObject();
+ public void BeginBatchUpdate()
+ {
+ _batchUpdate = true;
+
+ foreach (var entry in _entries)
+ {
+ (entry as IBatchUpdate)?.BeginBatchUpdate();
+ }
+ }
+
+ public void EndBatchUpdate()
+ {
+ _batchUpdate = false;
+
+ foreach (var entry in _entries)
+ {
+ (entry as IBatchUpdate)?.EndBatchUpdate();
+ }
+
+ UpdateEffectiveValue(null);
+ }
+
public void ClearLocalValue()
{
UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs(
@@ -134,10 +170,37 @@ public BindingEntry AddBinding(IObservable> source, BindingPr
var binding = new BindingEntry(_owner, Property, source, priority, this);
var insert = FindInsertPoint(binding.Priority);
_entries.Insert(insert, binding);
+
+ if (_batchUpdate)
+ {
+ binding.BeginBatchUpdate();
+
+ if (priority == BindingPriority.LocalValue)
+ {
+ binding.Start(ignoreBatchUpdate: true);
+ }
+ }
+
return binding;
}
- public void CoerceValue() => UpdateEffectiveValue(null);
+ public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
+ public void Start() => UpdateEffectiveValue(null);
+
+ public void RaiseValueChanged(
+ IValueSink sink,
+ IAvaloniaObject owner,
+ AvaloniaProperty property,
+ Optional oldValue,
+ Optional newValue)
+ {
+ sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ owner,
+ (AvaloniaProperty)property,
+ oldValue.Cast(),
+ newValue.Cast(),
+ Priority));
+ }
void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change)
{
@@ -146,7 +209,7 @@ void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs ch
_localValue = default;
}
- if (change is AvaloniaPropertyChangedEventArgs c)
+ if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs c)
{
UpdateEffectiveValue(c);
}
@@ -188,41 +251,47 @@ private int FindInsertPoint(BindingPriority priority)
public (Optional, BindingPriority) CalculateValue(BindingPriority maxPriority)
{
- var reachedLocalValues = false;
+ _isCalculatingValue = true;
- for (var i = _entries.Count - 1; i >= 0; --i)
+ try
{
- var entry = _entries[i];
-
- if (entry.Priority < maxPriority)
+ for (var i = _entries.Count - 1; i >= 0; --i)
{
- continue;
+ var entry = _entries[i];
+
+ if (entry.Priority < maxPriority)
+ {
+ continue;
+ }
+
+ entry.Start();
+
+ if (entry.Priority >= BindingPriority.LocalValue &&
+ maxPriority <= BindingPriority.LocalValue &&
+ _localValue.HasValue)
+ {
+ return (_localValue, BindingPriority.LocalValue);
+ }
+
+ var entryValue = entry.GetValue();
+
+ if (entryValue.HasValue)
+ {
+ return (entryValue, entry.Priority);
+ }
}
- if (!reachedLocalValues &&
- entry.Priority >= BindingPriority.LocalValue &&
- maxPriority <= BindingPriority.LocalValue &&
- _localValue.HasValue)
+ if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
{
return (_localValue, BindingPriority.LocalValue);
}
- var entryValue = entry.GetValue();
-
- if (entryValue.HasValue)
- {
- return (entryValue, entry.Priority);
- }
+ return (default, BindingPriority.Unset);
}
-
- if (!reachedLocalValues &&
- maxPriority <= BindingPriority.LocalValue &&
- _localValue.HasValue)
+ finally
{
- return (_localValue, BindingPriority.LocalValue);
+ _isCalculatingValue = false;
}
-
- return (default, BindingPriority.Unset);
}
private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs? change)
diff --git a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
index 238aba5c962..6a3f9b0b30c 100644
--- a/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
+++ b/src/Avalonia.Base/Reactive/AvaloniaPropertyObservable.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
namespace Avalonia.Reactive
{
@@ -55,9 +56,9 @@ private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
newValue = (T)e.Sender.GetValue(e.Property);
}
- if (!Equals(newValue, _value))
+ if (!EqualityComparer.Default.Equals(newValue, _value))
{
- _value = (T)newValue;
+ _value = newValue;
PublishNext(_value);
}
}
diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
index cf0a0c34eca..ec7bff4dfb8 100644
--- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs
+++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
@@ -6,7 +6,7 @@ namespace Avalonia
///
/// Metadata for styled avalonia properties.
///
- public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata
+ public class StyledPropertyMetadata : AvaloniaPropertyMetadata, IStyledPropertyMetadata
{
private Optional _defaultValue;
@@ -39,7 +39,7 @@ public StyledPropertyMetadata(
object IStyledPropertyMetadata.DefaultValue => DefaultValue;
///
- public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
+ public override void Merge(AvaloniaPropertyMetadata baseMetadata, AvaloniaProperty property)
{
base.Merge(baseMetadata, property);
diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
index 40cf81358fe..1a787921736 100644
--- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
+++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
@@ -9,20 +9,6 @@ namespace Avalonia.Threading
///
public class AvaloniaSynchronizationContext : SynchronizationContext
{
- public interface INonPumpingPlatformWaitProvider
- {
- int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout);
- }
-
- private readonly INonPumpingPlatformWaitProvider _waitProvider;
-
- public AvaloniaSynchronizationContext(INonPumpingPlatformWaitProvider waitProvider)
- {
- _waitProvider = waitProvider;
- if (_waitProvider != null)
- SetWaitNotificationRequired();
- }
-
///
/// Controls if SynchronizationContext should be installed in InstallIfNeeded. Used by Designer.
///
@@ -38,8 +24,7 @@ public static void InstallIfNeeded()
return;
}
- SetSynchronizationContext(new AvaloniaSynchronizationContext(AvaloniaLocator.Current
- .GetService()));
+ SetSynchronizationContext(new AvaloniaSynchronizationContext());
}
///
@@ -57,12 +42,6 @@ public override void Send(SendOrPostCallback d, object state)
Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait();
}
- [PrePrepareMethod]
- public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
- {
- if (_waitProvider != null)
- return _waitProvider.Wait(waitHandles, waitAll, millisecondsTimeout);
- return base.Wait(waitHandles, waitAll, millisecondsTimeout);
- }
+
}
}
diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
index 0238446892e..c513f759627 100644
--- a/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
+++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
@@ -1,5 +1,8 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable enable
namespace Avalonia.Utilities
{
@@ -9,14 +12,19 @@ namespace Avalonia.Utilities
/// Stored value type.
internal sealed class AvaloniaPropertyValueStore
{
+ // The last item in the list is always int.MaxValue.
+ private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } };
+
private Entry[] _entries;
public AvaloniaPropertyValueStore()
{
- // The last item in the list is always int.MaxValue
- _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
+ _entries = s_emptyEntries;
}
+ public int Count => _entries.Length - 1;
+ public TValue this[int index] => _entries[index].Value;
+
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
@@ -86,7 +94,7 @@ public AvaloniaPropertyValueStore()
return (0, false);
}
- public bool TryGetValue(AvaloniaProperty property, out TValue value)
+ public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
@@ -132,7 +140,18 @@ public void Remove(AvaloniaProperty property)
if (found)
{
- Entry[] entries = new Entry[_entries.Length - 1];
+ var newLength = _entries.Length - 1;
+
+ // Special case - one element left means that value store is empty so we can just reuse our "empty" array.
+ if (newLength == 1)
+ {
+ _entries = s_emptyEntries;
+
+ return;
+ }
+
+ var entries = new Entry[newLength];
+
int ix = 0;
for (int i = 0; i < _entries.Length; ++i)
@@ -147,18 +166,6 @@ public void Remove(AvaloniaProperty property)
}
}
- public Dictionary ToDictionary()
- {
- var dict = new Dictionary(_entries.Length - 1);
-
- for (int i = 0; i < _entries.Length - 1; ++i)
- {
- dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
- }
-
- return dict;
- }
-
private struct Entry
{
internal int PropertyId;
diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index 2a92e75f587..596cbf1d7e3 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/src/Avalonia.Base/Utilities/MathUtilities.cs
@@ -5,7 +5,10 @@ namespace Avalonia.Utilities
///
/// Provides math utilities not provided in System.Math.
///
- public static class MathUtilities
+#if !BUILDTASK
+ public
+#endif
+ static class MathUtilities
{
// smallest such that 1.0+DoubleEpsilon != 1.0
internal static readonly double DoubleEpsilon = 2.2204460492503131e-016;
@@ -27,6 +30,21 @@ public static bool AreClose(double value1, double value2)
return (-eps < delta) && (eps > delta);
}
+ ///
+ /// AreClose - Returns whether or not two doubles are "close". That is, whether or
+ /// not they are within epsilon of each other.
+ ///
+ /// The first double to compare.
+ /// The second double to compare.
+ /// The fixed epsilon value used to compare.
+ 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);
+ }
+
///
/// AreClose - Returns whether or not two floats are "close". That is, whether or
/// not they are within epsilon of each other.
@@ -206,6 +224,34 @@ public static double Clamp(double val, double min, double max)
}
}
+ ///
+ /// Clamps a value between a minimum and maximum value.
+ ///
+ /// The value.
+ /// The minimum value.
+ /// The maximum value.
+ /// The clamped value.
+ public static decimal Clamp(decimal val, decimal min, decimal max)
+ {
+ if (min > max)
+ {
+ ThrowCannotBeGreaterThanException(min, max);
+ }
+
+ if (val < min)
+ {
+ return min;
+ }
+ else if (val > max)
+ {
+ return max;
+ }
+ else
+ {
+ return val;
+ }
+ }
+
///
/// Clamps a value between a minimum and maximum value.
///
@@ -263,8 +309,8 @@ public static double Turn2Rad(double angle)
{
return angle * 2 * Math.PI;
}
-
- private static void ThrowCannotBeGreaterThanException(double min, double max)
+
+ private static void ThrowCannotBeGreaterThanException(T min, T max)
{
throw new ArgumentException($"{min} cannot be greater than {max}.");
}
diff --git a/src/Avalonia.Base/Utilities/NonPumpingLockHelper.cs b/src/Avalonia.Base/Utilities/NonPumpingLockHelper.cs
new file mode 100644
index 00000000000..fd4e5d2acea
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/NonPumpingLockHelper.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Avalonia.Utilities
+{
+ public class NonPumpingLockHelper
+ {
+ public interface IHelperImpl
+ {
+ IDisposable Use();
+ }
+
+ public static IDisposable Use() => AvaloniaLocator.Current.GetService()?.Use();
+ }
+}
diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs
index a159bfb72ec..24e99febb91 100644
--- a/src/Avalonia.Base/Utilities/StringTokenizer.cs
+++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs
@@ -4,7 +4,10 @@
namespace Avalonia.Utilities
{
- public struct StringTokenizer : IDisposable
+#if !BUILDTASK
+ public
+#endif
+ struct StringTokenizer : IDisposable
{
private const char DefaultSeparatorChar = ',';
diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs
index d0d88166a77..9f2308a062c 100644
--- a/src/Avalonia.Base/Utilities/TypeUtilities.cs
+++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs
@@ -372,8 +372,8 @@ private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type t
const string implicitName = "op_Implicit";
const string explicitName = "op_Explicit";
- bool allowImplicit = (operatorType & OperatorType.Implicit) != 0;
- bool allowExplicit = (operatorType & OperatorType.Explicit) != 0;
+ bool allowImplicit = operatorType.HasAllFlags(OperatorType.Implicit);
+ bool allowExplicit = operatorType.HasAllFlags(OperatorType.Explicit);
foreach (MethodInfo method in fromType.GetMethods())
{
diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs
index 6b89fcbdb9b..495f13e1a9a 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/src/Avalonia.Base/ValueStore.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Utilities;
@@ -26,6 +28,7 @@ internal class ValueStore : IValueSink
private readonly AvaloniaObject _owner;
private readonly IValueSink _sink;
private readonly AvaloniaPropertyValueStore _values;
+ private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner)
{
@@ -33,9 +36,28 @@ public ValueStore(AvaloniaObject owner)
_values = new AvaloniaPropertyValueStore();
}
+ public void BeginBatchUpdate()
+ {
+ _batchUpdate ??= new BatchUpdate(this);
+ _batchUpdate.Begin();
+ }
+
+ public void EndBatchUpdate()
+ {
+ if (_batchUpdate is null)
+ {
+ throw new InvalidOperationException("No batch update in progress.");
+ }
+
+ if (_batchUpdate.End())
+ {
+ _batchUpdate = null;
+ }
+ }
+
public bool IsAnimating(AvaloniaProperty property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
return slot.Priority < BindingPriority.LocalValue;
}
@@ -45,7 +67,7 @@ public bool IsAnimating(AvaloniaProperty property)
public bool IsSet(AvaloniaProperty property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
return slot.GetValue().HasValue;
}
@@ -58,7 +80,7 @@ public bool TryGetValue(
BindingPriority maxPriority,
out T value)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
var v = ((IValue)slot).GetValue(maxPriority);
@@ -82,7 +104,7 @@ public bool TryGetValue(
IDisposable? result = null;
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
result = SetExisting(slot, property, value, priority);
}
@@ -90,23 +112,21 @@ public bool TryGetValue(
{
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue(_owner, property, this);
- _values.AddValue(property, entry);
+ AddValue(property, entry);
result = entry.SetValue(value, priority);
}
else
{
- var change = new AvaloniaPropertyChangedEventArgs(_owner, property, default, value, priority);
-
if (priority == BindingPriority.LocalValue)
{
- _values.AddValue(property, new LocalValueEntry(value));
- _sink.ValueChanged(change);
+ AddValue(property, new LocalValueEntry(value));
+ NotifyValueChanged(property, default, value, priority);
}
else
{
var entry = new ConstantValueEntry(property, value, priority, this);
- _values.AddValue(property, entry);
- _sink.ValueChanged(change);
+ AddValue(property, entry);
+ NotifyValueChanged(property, default, value, priority);
result = entry;
}
}
@@ -119,7 +139,7 @@ public IDisposable AddBinding(
IObservable> source,
BindingPriority priority)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
return BindExisting(slot, property, source, priority);
}
@@ -128,62 +148,69 @@ public IDisposable AddBinding(
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue(_owner, property, this);
var binding = entry.AddBinding(source, priority);
- _values.AddValue(property, entry);
- binding.Start();
+ AddValue(property, entry);
return binding;
}
else
{
var entry = new BindingEntry(_owner, property, source, priority, this);
- _values.AddValue(property, entry);
- entry.Start();
+ AddValue(property, entry);
return entry;
}
}
public void ClearLocalValue(StyledPropertyBase property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue p)
{
p.ClearLocalValue();
}
- else
+ else if (slot.Priority == BindingPriority.LocalValue)
{
- var remove = slot is ConstantValueEntry c ?
- c.Priority == BindingPriority.LocalValue :
- !(slot is IPriorityValueEntry);
+ var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
- if (remove)
+ // 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 (!IsBatchUpdating())
{
- var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
_values.Remove(property);
- _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
- _owner,
- property,
- new Optional(old),
- default,
- BindingPriority.Unset));
}
+ else if (slot is IDisposable d)
+ {
+ d.Dispose();
+ }
+ else
+ {
+ // 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 convert
+ // them to a constant value entry with Unset priority in the event of a local value being
+ // cleared during a batch update.
+ var sentinel = new ConstantValueEntry(property, default, BindingPriority.Unset, _sink);
+ _values.SetValue(property, sentinel);
+ }
+
+ NotifyValueChanged(property, old, default, BindingPriority.Unset);
}
}
}
public void CoerceValue(StyledPropertyBase property)
{
- if (_values.TryGetValue(property, out var slot))
+ if (TryGetValue(property, out var slot))
{
if (slot is PriorityValue p)
{
- p.CoerceValue();
+ p.UpdateEffectiveValue();
}
}
}
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(
@@ -198,7 +225,17 @@ public void CoerceValue(StyledPropertyBase property)
void IValueSink.ValueChanged(AvaloniaPropertyChangedEventArgs change)
{
- _sink.ValueChanged(change);
+ if (_batchUpdate is object)
+ {
+ if (change.IsEffectiveValueChange)
+ {
+ NotifyValueChanged(change.Property, change.OldValue, change.NewValue, change.Priority);
+ }
+ }
+ else
+ {
+ _sink.ValueChanged(change);
+ }
}
void IValueSink.Completed(
@@ -206,13 +243,18 @@ void IValueSink.Completed(
IPriorityValueEntry entry,
Optional oldValue)
{
- if (_values.TryGetValue(property, out var slot))
+ // We need to include remove sentinels here so call `_values.TryGetValue` directly.
+ if (_values.TryGetValue(property, out var slot) && slot == entry)
{
- if (slot == entry)
+ if (_batchUpdate is null)
{
_values.Remove(property);
_sink.Completed(property, entry, oldValue);
}
+ else
+ {
+ _batchUpdate.ValueChanged(property, oldValue.ToObject());
+ }
}
}
@@ -240,16 +282,13 @@ void IValueSink.Completed(
{
var old = l.GetValue(BindingPriority.LocalValue);
l.SetValue(value);
- _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
- _owner,
- property,
- old,
- value,
- priority));
+ NotifyValueChanged(property, old, value, priority);
}
else
{
var priorityValue = new PriorityValue(_owner, property, this, l);
+ if (IsBatchUpdating())
+ priorityValue.BeginBatchUpdate();
result = priorityValue.SetValue(value, priority);
_values.SetValue(property, priorityValue);
}
@@ -273,6 +312,11 @@ private IDisposable BindExisting(
if (slot is IPriorityValueEntry e)
{
priorityValue = new PriorityValue(_owner, property, this, e);
+
+ if (IsBatchUpdating())
+ {
+ priorityValue.BeginBatchUpdate();
+ }
}
else if (slot is PriorityValue p)
{
@@ -289,8 +333,181 @@ private IDisposable BindExisting(
var binding = priorityValue.AddBinding(source, priority);
_values.SetValue(property, priorityValue);
- binding.Start();
+ priorityValue.UpdateEffectiveValue();
return binding;
}
+
+ private void AddValue(AvaloniaProperty property, IValue value)
+ {
+ _values.AddValue(property, value);
+ if (IsBatchUpdating() && value is IBatchUpdate batch)
+ batch.BeginBatchUpdate();
+ value.Start();
+ }
+
+ private void NotifyValueChanged(
+ AvaloniaProperty property,
+ Optional oldValue,
+ BindingValue newValue,
+ BindingPriority priority)
+ {
+ if (_batchUpdate is null)
+ {
+ _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs(
+ _owner,
+ property,
+ oldValue,
+ newValue,
+ priority));
+ }
+ else
+ {
+ _batchUpdate.ValueChanged(property, oldValue.ToObject());
+ }
+ }
+
+ 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;
+ private List? _notifications;
+ private int _batchUpdateCount;
+ private int _iterator = -1;
+
+ public BatchUpdate(ValueStore owner) => _owner = owner;
+
+ public bool IsBatchUpdating => _batchUpdateCount > 0;
+
+ public void Begin()
+ {
+ if (_batchUpdateCount++ == 0)
+ {
+ var values = _owner._values;
+
+ for (var i = 0; i < values.Count; ++i)
+ {
+ (values[i] as IBatchUpdate)?.BeginBatchUpdate();
+ }
+ }
+ }
+
+ public bool End()
+ {
+ if (--_batchUpdateCount > 0)
+ return false;
+
+ var values = _owner._values;
+
+ // First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
+ // but notifications will still not be raised because the owner ValueStore will still have a reference
+ // to this batch update object.
+ for (var i = 0; i < values.Count; ++i)
+ {
+ (values[i] as IBatchUpdate)?.EndBatchUpdate();
+
+ // Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
+ // does, abort and continue batch updating.
+ if (_batchUpdateCount > 0)
+ return false;
+ }
+
+ if (_notifications is object)
+ {
+ // Raise all batched notifications. Doing this can cause other notifications to be added and even
+ // cause a new batch update to start, so we need to handle _notifications being modified by storing
+ // the index in field.
+ _iterator = 0;
+
+ for (; _iterator < _notifications.Count; ++_iterator)
+ {
+ var entry = _notifications[_iterator];
+
+ if (values.TryGetValue(entry.property, out var slot))
+ {
+ var oldValue = entry.oldValue;
+ var newValue = slot.GetValue();
+
+ // Raising this notification can cause a new batch update to be started, which in turn
+ // results in another change to the property. In this case we need to update the old value
+ // so that the *next* notification has an oldValue which follows on from the newValue
+ // raised here.
+ _notifications[_iterator] = new Notification
+ {
+ property = entry.property,
+ oldValue = newValue,
+ };
+
+ // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs.
+ slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue);
+
+ // 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. 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);
+ }
+ }
+ else
+ {
+ throw new AvaloniaInternalException("Value could not be found at the end of batch update.");
+ }
+
+ // If a new batch update was started while ending this one, abort.
+ if (_batchUpdateCount > 0)
+ return false;
+ }
+ }
+
+ _iterator = int.MaxValue - 1;
+ return true;
+ }
+
+ public void ValueChanged(AvaloniaProperty property, Optional oldValue)
+ {
+ _notifications ??= new List();
+
+ for (var i = 0; i < _notifications.Count; ++i)
+ {
+ if (_notifications[i].property == property)
+ {
+ oldValue = _notifications[i].oldValue;
+ _notifications.RemoveAt(i);
+
+ if (i <= _iterator)
+ --_iterator;
+ break;
+ }
+ }
+
+ _notifications.Add(new Notification
+ {
+ property = property,
+ oldValue = oldValue,
+ });
+ }
+
+ private struct Notification
+ {
+ public AvaloniaProperty property;
+ public Optional oldValue;
+ }
+ }
}
}
diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
index 490364c0d87..90f6abc873f 100644
--- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
+++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
@@ -1,7 +1,7 @@
netstandard2.0
- netstandard2.0;netcoreapp2.0
+ netstandard2.0;netcoreapp3.1
exe
false
tools
@@ -45,6 +45,12 @@
Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
Markup/%(RecursiveDir)%(FileName)%(Extension)
@@ -57,9 +63,36 @@
Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
-
+
diff --git a/src/Avalonia.Build.Tasks/ComInteropHelper.cs b/src/Avalonia.Build.Tasks/ComInteropHelper.cs
new file mode 100644
index 00000000000..007231417de
--- /dev/null
+++ b/src/Avalonia.Build.Tasks/ComInteropHelper.cs
@@ -0,0 +1,130 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using XamlX.TypeSystem;
+using MethodAttributes = Mono.Cecil.MethodAttributes;
+
+namespace Avalonia.Build.Tasks
+{
+ static class ComInteropHelper
+ {
+ public static void PatchAssembly(AssemblyDefinition asm, CecilTypeSystem typeSystem)
+ {
+ var classToRemoveList = new List();
+ var initializers = new List();
+ foreach (var type in asm.MainModule.Types)
+ {
+ var i = type.Methods.FirstOrDefault(m => m.Name == "__MicroComModuleInit");
+ if (i != null)
+ initializers.Add(i);
+
+ PatchType(type, classToRemoveList);
+ }
+
+ // Remove All Interop classes
+ foreach (var type in classToRemoveList)
+ asm.MainModule.Types.Remove(type);
+
+
+ // Patch automatic registrations
+ if (initializers.Count != 0)
+ {
+ var moduleType = asm.MainModule.Types.First(x => x.Name == "");
+
+ // Needed for compatibility with upcoming .NET 5 feature, look for existing initializer first
+ var staticCtor = moduleType.Methods.FirstOrDefault(m => m.Name == ".cctor");
+ if (staticCtor == null)
+ {
+ // Create a new static ctor if none exists
+ staticCtor = new MethodDefinition(".cctor",
+ MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName |
+ MethodAttributes.Static | MethodAttributes.Private,
+ asm.MainModule.TypeSystem.Void);
+ staticCtor.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
+ moduleType.Methods.Add(staticCtor);
+ }
+
+ foreach (var i in initializers)
+ staticCtor.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, i));
+ }
+
+ }
+
+
+
+ static void PatchMethod(MethodDefinition method)
+ {
+ if (method.HasBody)
+ {
+ var ilProcessor = method.Body.GetILProcessor();
+
+ var instructions = method.Body.Instructions;
+ for (int i = 0; i < instructions.Count; i++)
+ {
+ Instruction instruction = instructions[i];
+
+ if (instruction.OpCode == OpCodes.Call && instruction.Operand is MethodReference)
+ {
+ var methodDescription = (MethodReference)instruction.Operand;
+
+ if (methodDescription.Name.StartsWith("Calli") && methodDescription.DeclaringType.Name == "LocalInterop")
+ {
+ var callSite = new CallSite(methodDescription.ReturnType) { CallingConvention = MethodCallingConvention.StdCall };
+
+ if (methodDescription.Name.StartsWith("CalliCdecl"))
+ {
+ callSite.CallingConvention = MethodCallingConvention.C;
+ }
+ else if(methodDescription.Name.StartsWith("CalliThisCall"))
+ {
+ callSite.CallingConvention = MethodCallingConvention.ThisCall;
+ }
+ else if(methodDescription.Name.StartsWith("CalliStdCall"))
+ {
+ callSite.CallingConvention = MethodCallingConvention.StdCall;
+ }
+ else if(methodDescription.Name.StartsWith("CalliFastCall"))
+ {
+ callSite.CallingConvention = MethodCallingConvention.FastCall;
+ }
+
+ // Last parameter is the function ptr, so we don't add it as a parameter for calli
+ // as it is already an implicit parameter for calli
+ for (int j = 0; j < methodDescription.Parameters.Count - 1; j++)
+ {
+ var parameterDefinition = methodDescription.Parameters[j];
+ callSite.Parameters.Add(parameterDefinition);
+ }
+
+ // Create calli Instruction
+ var callIInstruction = ilProcessor.Create(OpCodes.Calli, callSite);
+
+ // Replace instruction
+ ilProcessor.Replace(instruction, callIInstruction);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Patches the type.
+ ///
+ /// The type.
+ static void PatchType(TypeDefinition type, List classToRemoveList)
+ {
+ // Patch methods
+ foreach (var method in type.Methods)
+ PatchMethod(method);
+
+ if (type.Name == "LocalInterop")
+ classToRemoveList.Add(type);
+
+ // Patch nested types
+ foreach (var typeDefinition in type.NestedTypes)
+ PatchType(typeDefinition, classToRemoveList);
+ }
+ }
+}
diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
index 95e59dde2bc..b85991fb770 100644
--- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
+++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
@@ -39,7 +39,9 @@ public bool Execute()
var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input,
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
- ProjectDirectory, OutputPath, VerifyIl, outputImportance);
+ ProjectDirectory, OutputPath, VerifyIl, outputImportance,
+ (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null,
+ EnableComInteropPatching, SkipXamlCompilation);
if (!res.Success)
return false;
if (!res.WrittenFile)
@@ -73,6 +75,13 @@ string GetPdbPath(string p)
public string OutputPath { get; set; }
public bool VerifyIl { get; set; }
+
+ public bool EnableComInteropPatching { get; set; }
+ public bool SkipXamlCompilation { get; set; }
+
+ public string AssemblyOriginatorKeyFile { get; set; }
+ public bool SignAssembly { get; set; }
+ public bool DelaySign { get; set; }
public string ReportImportance { get; set; }
diff --git a/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs
new file mode 100644
index 00000000000..f207b558a34
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs
index 1909c4c6ecc..b18c19cd63e 100644
--- a/src/Avalonia.Build.Tasks/Program.cs
+++ b/src/Avalonia.Build.Tasks/Program.cs
@@ -29,7 +29,8 @@ static int Main(string[] args)
OutputPath = args[2],
BuildEngine = new ConsoleBuildEngine(),
ProjectDirectory = Directory.GetCurrentDirectory(),
- VerifyIl = true
+ VerifyIl = true,
+ EnableComInteropPatching = true
}.Execute() ?
0 :
2;
diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs
index d5c406293ed..f8960f56ecf 100644
--- a/src/Avalonia.Build.Tasks/SpanCompat.cs
+++ b/src/Avalonia.Build.Tasks/SpanCompat.cs
@@ -1,3 +1,4 @@
+#if !NETCOREAPP3_1
namespace System
{
// This is a hack to enable our span code to work inside MSBuild task without referencing System.Memory
@@ -63,6 +64,8 @@ public ReadOnlySpan TrimStart()
}
public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length);
+
+ public static implicit operator ReadOnlySpan(char[] arr) => new ReadOnlySpan(new string(arr));
}
static class SpanCompatExtensions
@@ -71,3 +74,4 @@ static class SpanCompatExtensions
}
}
+#endif
diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
index c9c9c562bdb..508045dccb5 100644
--- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
+++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
@@ -22,7 +22,6 @@
namespace Avalonia.Build.Tasks
{
-
public static partial class XamlCompilerTaskExecutor
{
static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml")
@@ -40,17 +39,50 @@ public CompileResult(bool success, bool writtenFile = false)
WrittenFile = writtenFile;
}
}
+
+ public static CompileResult Compile(IBuildEngine engine, string input, string[] references,
+ string projectDirectory,
+ string output, bool verifyIl, MessageImportance logImportance, string strongNameKey, bool patchCom,
+ bool skipXamlCompilation)
+ {
+ var typeSystem = new CecilTypeSystem(references
+ .Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll"))
+ .Concat(new[] { input }), input);
+
+ var asm = typeSystem.TargetAssemblyDefinition;
+
+ if (!skipXamlCompilation)
+ {
+ var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, logImportance);
+ if (compileRes == null && !patchCom)
+ return new CompileResult(true);
+ if (compileRes == false)
+ return new CompileResult(false);
+ }
+
+ if (patchCom)
+ ComInteropHelper.PatchAssembly(asm, typeSystem);
+
+ var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols };
+ if (!string.IsNullOrWhiteSpace(strongNameKey))
+ writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey);
+
+ asm.Write(output, writerParameters);
+
+ return new CompileResult(true, true);
+
+ }
- public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory,
- string output, bool verifyIl, MessageImportance logImportance)
+ static bool? CompileCore(IBuildEngine engine, CecilTypeSystem typeSystem,
+ string projectDirectory, bool verifyIl,
+ MessageImportance logImportance)
{
- var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input);
var asm = typeSystem.TargetAssemblyDefinition;
var emres = new EmbeddedResources(asm);
var avares = new AvaloniaResources(asm, projectDirectory);
if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0)
// Nothing to do
- return new CompileResult(true);
+ return null;
var clrPropertiesDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlHelpers",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
@@ -66,7 +98,8 @@ public static CompileResult Compile(IBuildEngine engine, string input, string[]
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",
@@ -345,28 +378,36 @@ bool CompileGroup(IResourceGroup group)
}
res.Remove();
}
+
+
+ // Technically that's a hack, but it fixes corert incompatibility caused by deterministic builds
+ int dupeCounter = 1;
+ foreach (var grp in typeDef.NestedTypes.GroupBy(x => x.Name))
+ {
+ if (grp.Count() > 1)
+ {
+ foreach (var dupe in grp)
+ dupe.Name += "_dup" + dupeCounter++;
+ }
+ }
+
+
return true;
}
if (emres.Resources.Count(CheckXamlName) != 0)
if (!CompileGroup(emres))
- return new CompileResult(false);
+ return false;
if (avares.Resources.Count(CheckXamlName) != 0)
{
if (!CompileGroup(avares))
- return new CompileResult(false);
+ return false;
avares.Save();
}
loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ldnull));
loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
-
- asm.Write(output, new WriterParameters
- {
- WriteSymbols = asm.MainModule.HasSymbols
- });
-
- return new CompileResult(true, true);
+ return true;
}
}
diff --git a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt b/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt
deleted file mode 100644
index 82472c505a4..00000000000
--- a/src/Avalonia.Controls.DataGrid/ApiCompatBaseline.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-Compat issues with assembly Avalonia.Controls.DataGrid:
-MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.DataGridTextColumn.FontFamilyProperty' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public System.String Avalonia.Controls.DataGridTextColumn.FontFamily.get()' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public void Avalonia.Controls.DataGridTextColumn.FontFamily.set(System.String)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 3
diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
index 92734b128db..fe6acdc5320 100644
--- a/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
+++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
@@ -877,7 +877,7 @@ public int PageSize
if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred))
{
// if the temporaryGroup was not created yet and is out of sync
- // then create it so that we can use it as a refernce while paging.
+ // then create it so that we can use it as a reference while paging.
if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count)
{
PrepareTemporaryGroups();
@@ -889,7 +889,7 @@ public int PageSize
else if (IsGrouping)
{
// if the temporaryGroup was not created yet and is out of sync
- // then create it so that we can use it as a refernce while paging.
+ // then create it so that we can use it as a reference while paging.
if (_temporaryGroup.ItemCount != InternalList.Count)
{
// update the groups that get created for the
@@ -1951,7 +1951,7 @@ public object GetItemAt(int index)
EnsureCollectionInSync();
VerifyRefreshNotDeferred();
- // for indicies larger than the count
+ // for indices larger than the count
if (index >= Count || index < 0)
{
throw new ArgumentOutOfRangeException("index");
@@ -2595,7 +2595,7 @@ private void AdjustCurrencyForRemove(int index)
/// Whether the specified flag is set
private bool CheckFlag(CollectionViewFlags flags)
{
- return (_flags & flags) != 0;
+ return _flags.HasAllFlags(flags);
}
///
@@ -3275,7 +3275,7 @@ private void ProcessAddEvent(object addedItem, int addIndex)
addIndex);
// next check if we need to add an item into the current group
- // bool needsGrouping = false;
+ bool needsGrouping = false;
if (Count == 1 && GroupDescriptions.Count > 0)
{
// if this is the first item being added
@@ -3302,7 +3302,7 @@ private void ProcessAddEvent(object addedItem, int addIndex)
// otherwise, we need to validate that it is within the current page.
if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex)
{
- //needsGrouping = true;
+ needsGrouping = true;
int pageStartIndex = PageIndex * PageSize;
@@ -3340,6 +3340,13 @@ private void ProcessAddEvent(object addedItem, int addIndex)
}
}
+ // if we need to add the item into the current group
+ // that will be displayed
+ if (needsGrouping)
+ {
+ this._group.AddToSubgroups(addedItem, false /*loading*/);
+ }
+
int addedIndex = IndexOf(addedItem);
// if the item is within the current page
@@ -3793,7 +3800,7 @@ private void SetCurrent(object newItem, int newPosition)
///
///
/// This method can be called from a constructor - it does not call
- /// any virtuals. The 'count' parameter is substitute for the real Count,
+ /// any virtuals. The 'count' parameter is substitute for the real Count,
/// used only when newItem is null.
/// In that case, this method sets IsCurrentAfterLast to true if and only
/// if newPosition >= count. This distinguishes between a null belonging
diff --git a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
index 662ff91329a..ca6020128c3 100644
--- a/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
+++ b/src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs
@@ -265,6 +265,43 @@ public static DataGridSortDescription FromPath(string propertyPath, ListSortDire
{
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 _comparer;
+
+ public IComparer SourceComparer => _innerComparer;
+ public override IComparer Comparer => _comparer;
+ public override ListSortDirection Direction => _direction;
+ public DataGridComparerSortDesctiption(IComparer comparer, ListSortDirection direction)
+ {
+ _innerComparer = comparer;
+ _direction = direction;
+ _comparer = Comparer.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
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index 5bb27635666..ab1aff9220f 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -1,6 +1,6 @@
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
-// All other rights reserved.
+// All other rights reserved.
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
@@ -25,6 +25,7 @@
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Controls.Metadata;
+using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Controls
{
@@ -67,7 +68,7 @@ public partial class DataGrid : TemplatedControl
private const double DATAGRID_minimumColumnHeaderHeight = 4;
internal const double DATAGRID_maximumStarColumnWidth = 10000;
internal const double DATAGRID_minimumStarColumnWidth = 0.001;
- private const double DATAGRID_mouseWheelDelta = 72.0;
+ private const double DATAGRID_mouseWheelDelta = 50.0;
private const double DATAGRID_maxHeadersThickness = 32768;
private const double DATAGRID_defaultRowHeight = 22;
@@ -75,7 +76,6 @@ public partial class DataGrid : TemplatedControl
private const double DATAGRID_defaultMinColumnWidth = 20;
private const double DATAGRID_defaultMaxColumnWidth = double.PositiveInfinity;
- private List _validationErrors;
private List _bindingValidationErrors;
private IDisposable _validationSubscription;
@@ -92,7 +92,7 @@ public partial class DataGrid : TemplatedControl
private ContentControl _topRightCornerHeader;
private Control _frozenColumnScrollBarSpacer;
- // the sum of the widths in pixels of the scrolling columns preceding
+ // the sum of the widths in pixels of the scrolling columns preceding
// the first displayed scrolling column
private double _horizontalOffset;
@@ -102,7 +102,6 @@ public partial class DataGrid : TemplatedControl
private bool _areHandlersSuspended;
private bool _autoSizingColumns;
private IndexToValueTable _collapsedSlotsTable;
- private DataGridCellCoordinates _currentCellCoordinates;
private Control _clickedElement;
// used to store the current column during a Reset
@@ -141,9 +140,8 @@ public partial class DataGrid : TemplatedControl
private DataGridSelectedItemsCollection _selectedItems;
private bool _temporarilyResetCurrentCell;
private object _uneditedValue; // Represents the original current cell value at the time it enters editing mode.
- private ICellEditBinding _currentCellEditBinding;
- // An approximation of the sum of the heights in pixels of the scrolling rows preceding
+ // An approximation of the sum of the heights in pixels of the scrolling rows preceding
// the first displayed scrolling row. Since the scrolled off rows are discarded, the grid
// does not know their actual height. The heights used for the approximation are the ones
// set as the rows were scrolled off.
@@ -162,7 +160,7 @@ public partial class DataGrid : TemplatedControl
AvaloniaProperty.Register(nameof(CanUserReorderColumns));
///
- /// Gets or sets a value that indicates whether the user can change
+ /// Gets or sets a value that indicates whether the user can change
/// the column display order by dragging column headers with the mouse.
///
public bool CanUserReorderColumns
@@ -247,8 +245,8 @@ public DataGridLength ColumnWidth
/// Gets or sets the that is used to paint the background of odd-numbered rows.
///
///
- /// The brush that is used to paint the background of odd-numbered rows. The default is a
- /// with a
+ /// The brush that is used to paint the background of odd-numbered rows. The default is a
+ /// with a
/// value of white (ARGB value #00FFFFFF).
///
public IBrush AlternatingRowBackground
@@ -379,8 +377,8 @@ private void OnAreRowGroupHeadersFrozenChanged(AvaloniaPropertyChangedEventArgs
public bool IsValid
{
get { return _isValid; }
- internal set
- {
+ internal set
+ {
SetAndRaise(IsValidProperty, ref _isValid, value);
PseudoClasses.Set(":invalid", !value);
}
@@ -398,7 +396,7 @@ private static bool IsValidColumnWidth(double value)
}
///
- /// Gets or sets the maximum width of columns in the .
+ /// Gets or sets the maximum width of columns in the .
///
public double MaxColumnWidth
{
@@ -418,7 +416,7 @@ private static bool IsValidMinColumnWidth(double value)
}
///
- /// Gets or sets the minimum width of columns in the .
+ /// Gets or sets the minimum width of columns in the .
///
public double MinColumnWidth
{
@@ -496,7 +494,7 @@ public DataGridSelectionMode SelectionMode
AvaloniaProperty.Register(nameof(VerticalGridLinesBrush));
///
- /// Gets or sets the that is used to paint grid lines separating columns.
+ /// Gets or sets the that is used to paint grid lines separating columns.
///
public IBrush VerticalGridLinesBrush
{
@@ -535,14 +533,15 @@ public ITemplate DropLocationIndicatorTemplate
AvaloniaProperty.RegisterDirect(
nameof(SelectedIndex),
o => o.SelectedIndex,
- (o, v) => o.SelectedIndex = v);
+ (o, v) => o.SelectedIndex = v,
+ defaultBindingMode: BindingMode.TwoWay);
///
/// Gets or sets the index of the current selection.
///
///
/// The index of the current selection, or -1 if the selection is empty.
- ///
+ ///
public int SelectedIndex
{
get { return _selectedIndex; }
@@ -553,7 +552,8 @@ public int SelectedIndex
AvaloniaProperty.RegisterDirect(
nameof(SelectedItem),
o => o.SelectedItem,
- (o, v) => o.SelectedItem = v);
+ (o, v) => o.SelectedItem = v,
+ defaultBindingMode: BindingMode.TwoWay);
///
/// Gets or sets the data item corresponding to the selected row.
@@ -582,7 +582,7 @@ public DataGridClipboardCopyMode ClipboardCopyMode
AvaloniaProperty.Register(nameof(AutoGenerateColumns));
///
- /// Gets or sets a value that indicates whether columns are created
+ /// Gets or sets a value that indicates whether columns are created
/// automatically when the property is set.
///
public bool AutoGenerateColumns
@@ -626,7 +626,7 @@ public IEnumerable Items
AvaloniaProperty.Register(nameof(AreRowDetailsFrozen));
///
- /// Gets or sets a value that indicates whether the row details sections remain
+ /// Gets or sets a value that indicates whether the row details sections remain
/// fixed at the width of the display area or can scroll horizontally.
///
public bool AreRowDetailsFrozen
@@ -881,7 +881,7 @@ private void OnSelectedIndexChanged(AvaloniaPropertyChangedEventArgs e)
{
int index = (int)e.NewValue;
- // GetDataItem returns null if index is >= Count, we do not check newValue
+ // GetDataItem returns null if index is >= Count, we do not check newValue
// against Count here to avoid enumerating through an Enumerable twice
// Setting SelectedItem coerces the finally value of the SelectedIndex
object newSelectedItem = (index < 0) ? null : DataConnection.GetDataItem(index);
@@ -910,6 +910,11 @@ private void OnSelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
// Clear all row selections
ClearRowSelection(resetAnchorSlot: true);
+
+ if (DataConnection.CollectionView != null)
+ {
+ DataConnection.CollectionView.MoveCurrentTo(null);
+ }
}
else
{
@@ -1168,14 +1173,14 @@ private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e)
}
///
- /// Occurs one time for each public, non-static property in the bound data type when the
- /// property is changed and the
+ /// Occurs one time for each public, non-static property in the bound data type when the
+ /// property is changed and the
/// property is true.
///
public event EventHandler AutoGeneratingColumn;
///
- /// Occurs before a cell or row enters editing mode.
+ /// Occurs before a cell or row enters editing mode.
///
public event EventHandler BeginningEdit;
@@ -1195,7 +1200,7 @@ private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e)
public event EventHandler CellPointerPressed;
///
- /// Occurs when the
+ /// Occurs when the
/// property of a column changes.
///
public event EventHandler ColumnDisplayIndexChanged;
@@ -1218,14 +1223,14 @@ private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e)
public event EventHandler CurrentCellChanged;
///
- /// Occurs after a
+ /// Occurs after a
/// is instantiated, so that you can customize it before it is used.
///
public event EventHandler LoadingRow;
///
/// Occurs when a cell in a enters editing mode.
- ///
+ ///
///
public event EventHandler PreparingCellForEdit;
@@ -1243,7 +1248,7 @@ private void OnCanUserResizeColumnsChanged(AvaloniaPropertyChangedEventArgs e)
RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble);
///
- /// Occurs when the or
+ /// Occurs when the or
/// property value changes.
///
public event EventHandler SelectionChanged
@@ -1258,19 +1263,19 @@ public event EventHandler SelectionChanged
public event EventHandler Sorting;
///
- /// Occurs when a
+ /// Occurs when a
/// object becomes available for reuse.
///
public event EventHandler UnloadingRow;
///
- /// Occurs when a new row details template is applied to a row, so that you can customize
+ /// Occurs when a new row details template is applied to a row, so that you can customize
/// the details section before it is used.
///
public event EventHandler LoadingRowDetails;
///
- /// Occurs when the
+ /// Occurs when the
/// property value changes.
///
public event EventHandler RowDetailsVisibilityChanged;
@@ -1282,7 +1287,7 @@ public event EventHandler SelectionChanged
///
/// Gets a collection that contains all the columns in the control.
- ///
+ ///
public ObservableCollection Columns
{
get
@@ -1456,7 +1461,7 @@ internal double AvailableSlotElementRoom
}
// Height currently available for cells this value is smaller. This height is reduced by the existence of ColumnHeaders
- // or a horizontal scrollbar. Layout is asynchronous so changes to the ColumnHeaders or the horizontal scrollbar are
+ // or a horizontal scrollbar. Layout is asynchronous so changes to the ColumnHeaders or the horizontal scrollbar are
// not reflected immediately.
internal double CellsHeight
{
@@ -1555,7 +1560,7 @@ internal double HorizontalAdjustment
internal static double HorizontalGridLinesThickness => DATAGRID_horizontalGridLinesThickness;
- // the sum of the widths in pixels of the scrolling columns preceding
+ // the sum of the widths in pixels of the scrolling columns preceding
// the first displayed scrolling column
internal double HorizontalOffset
{
@@ -2083,20 +2088,20 @@ protected override Size ArrangeOverride(Size finalSize)
}
///
- /// Measures the children of a to prepare for
- /// arranging them during the
- /// pass.
+ /// Measures the children of a to prepare for
+ /// arranging them during the
+ /// pass.
///
///
/// The size that the determines it needs during layout, based on its calculations of child object allocated sizes.
///
///
- /// The available size that this element can give to child elements. Indicates an upper limit that
+ /// The available size that this element can give to child elements. Indicates an upper limit that
/// child elements should not exceed.
///
protected override Size MeasureOverride(Size availableSize)
{
- // Delay layout until after the initial measure to avoid invalid calculations when the
+ // Delay layout until after the initial measure to avoid invalid calculations when the
// DataGrid is not part of the visual tree
if (!_measured)
{
@@ -2210,32 +2215,71 @@ protected virtual void OnLoadingRow(DataGridRowEventArgs e)
/// PointerWheelEventArgs
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
- if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0)
+ e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta);
+ }
+
+ internal bool UpdateScroll(Vector delta)
+ {
+ if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0)
{
- double scrollHeight = 0;
- if (e.Delta.Y > 0)
+ var handled = false;
+ var scrollHeight = 0d;
+
+ // Vertical scroll handling
+ if (delta.Y > 0)
{
- scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta);
+ scrollHeight = Math.Max(-_verticalOffset, -delta.Y);
}
- else if (e.Delta.Y < 0)
+ else if (delta.Y < 0)
{
if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible)
{
- scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta);
+ scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), -delta.Y);
}
else
{
double maximum = EdgedRowsHeightCalculated - CellsHeight;
- scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta);
+ scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), -delta.Y);
}
}
+
if (scrollHeight != 0)
{
DisplayData.PendingVerticalScrollHeight = scrollHeight;
+ handled = true;
+ }
+
+ // Horizontal scroll handling
+ if (delta.X != 0)
+ {
+ var originalHorizontalOffset = HorizontalOffset;
+ var horizontalOffset = originalHorizontalOffset - delta.X;
+ var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
+
+ if (horizontalOffset < 0)
+ {
+ horizontalOffset = 0;
+ }
+ if (horizontalOffset > widthNotVisible)
+ {
+ horizontalOffset = widthNotVisible;
+ }
+
+ if (horizontalOffset != originalHorizontalOffset)
+ {
+ HorizontalOffset = horizontalOffset;
+ handled = true;
+ }
+ }
+
+ if (handled)
+ {
InvalidateRowsMeasure(invalidateIndividualElements: false);
- e.Handled = true;
+ return true;
}
}
+
+ return false;
}
///
@@ -2285,6 +2329,17 @@ protected virtual void OnUnloadingRow(DataGridRowEventArgs e)
}
}
+ ///
+ /// Comparator class so we can sort list by the display index
+ ///
+ public class DisplayIndexComparer : IComparer
+ {
+ int IComparer.Compare(DataGridColumn x, DataGridColumn y)
+ {
+ return (x.DisplayIndexWithFiller < y.DisplayIndexWithFiller) ? -1 : 1;
+ }
+ }
+
///
/// Builds the visual tree for the column header when a new template is applied.
///
@@ -2309,8 +2364,11 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
ColumnsInternal.FillerColumn.IsRepresented = false;
}
_columnHeadersPresenter.OwningGrid = this;
- // Columns were added before before our Template was applied, add the ColumnHeaders now
- foreach (DataGridColumn column in ColumnsItemsInternal)
+
+ // Columns were added before our Template was applied, add the ColumnHeaders now
+ List sortedInternal = new List(ColumnsItemsInternal);
+ sortedInternal.Sort(new DisplayIndexComparer());
+ foreach (DataGridColumn column in sortedInternal)
{
InsertDisplayedColumnHeader(column);
}
@@ -3006,7 +3064,7 @@ internal void UpdateVerticalScrollBar()
/// If the editing element has focus, this method will set focus to the DataGrid itself
/// in order to force the element to lose focus. It will then wait for the editing element's
/// LostFocus event, at which point it will perform the specified action.
- ///
+ ///
/// NOTE: It is important to understand that the specified action will be performed when the editing
/// element loses focus only if this method returns true. If it returns false, then the action
/// will not be performed later on, and should instead be performed by the caller, if necessary.
@@ -3065,7 +3123,7 @@ internal void OnRowDetailsChanged()
{
if (!_scrollingByHeight)
{
- // Update layout when RowDetails are expanded or collapsed, just updating the vertical scroll bar is not enough
+ // Update layout when RowDetails are expanded or collapsed, just updating the vertical scroll bar is not enough
// since rows could be added or removed
InvalidateMeasure();
}
@@ -3278,7 +3336,7 @@ private bool CommitEditForOperation(int columnIndex, int slot, bool forCurrentCe
{
// Current cell was reset because the commit deleted row(s).
// Since the user wants to change the current cell, we don't
- // want to end up with no current cell. We pick the last row
+ // want to end up with no current cell. We pick the last row
// in the grid which may be the 'new row'.
int lastSlot = LastVisibleSlot;
if (forCurrentCellChange &&
@@ -3336,7 +3394,7 @@ private void ComputeScrollBarsLayout()
if (_ignoreNextScrollBarsLayout)
{
_ignoreNextScrollBarsLayout = false;
- //
+ //
}
@@ -3393,7 +3451,7 @@ private void ComputeScrollBarsLayout()
}
// Now cellsWidth is the width potentially available for displaying data cells.
- // Now cellsHeight is the height potentially available for displaying data cells.
+ // Now cellsHeight is the height potentially available for displaying data cells.
bool needHorizScrollbar = false;
bool needVertScrollbar = false;
@@ -3418,7 +3476,7 @@ private void ComputeScrollBarsLayout()
Debug.Assert(cellsHeight >= 0);
needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
- if (vertScrollBarWidth > 0 &&
+ if (vertScrollBarWidth > 0 &&
allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
{
@@ -3458,7 +3516,7 @@ private void ComputeScrollBarsLayout()
// we compute the number of visible columns only after we set up the vertical scroll bar.
ComputeDisplayedColumns();
- if ((vertScrollBarWidth > 0 || horizScrollBarHeight > 0) &&
+ if ((vertScrollBarWidth > 0 || horizScrollBarHeight > 0) &&
allowHorizScrollbar &&
needVertScrollbar && !needHorizScrollbar &&
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
@@ -3963,7 +4021,8 @@ void SetValidationStatus(ICellEditBinding binding)
{
var errorList =
binding.ValidationErrors
- .SelectMany(ex => ValidationUtil.UnpackException(ex))
+ .SelectMany(ValidationUtil.UnpackException)
+ .Select(ValidationUtil.UnpackDataValidationException)
.ToList();
DataValidationErrors.SetErrors(editingElement, errorList);
@@ -4124,7 +4183,7 @@ private void EnsureVerticalGridLines()
}
///
- /// Exits editing mode without trying to commit or revert the editing, and
+ /// Exits editing mode without trying to commit or revert the editing, and
/// without repopulating the edited row's cell.
///
//TODO TabStop
@@ -5103,9 +5162,9 @@ private bool ProcessTabKey(KeyEventArgs e, bool shift, bool ctrl)
{
if (ctrl || _editingColumnIndex == -1 || IsReadOnly)
{
- //Go to the next/previous control on the page when
+ //Go to the next/previous control on the page when
// - Ctrl key is used
- // - Potential current cell is not edited, or the datagrid is read-only.
+ // - Potential current cell is not edited, or the datagrid is read-only.
return false;
}
@@ -5337,7 +5396,7 @@ private void ResetFocusedRow()
_focusedRow = null;
}
- private void SelectAll()
+ public void SelectAll()
{
SetRowsSelection(0, SlotCount - 1);
}
@@ -5516,11 +5575,11 @@ private void UpdateHorizontalScrollBar(bool needHorizScrollbar, bool forceHorizS
// v---v
//|<|_____|###|>|
// ^ ^
- // min max
+ // min max
// we want to make the relative size of the thumb reflect the relative size of the viewing area
// viewportSize / (max + viewportSize) = cellsWidth / max
- // -> viewportSize = max * cellsWidth / (max - cellsWidth)
+ // -> viewportSize = max * cellsWidth / (max - cellsWidth)
// always zero
_hScrollBar.Minimum = 0;
@@ -5572,7 +5631,7 @@ private void UpdateHorizontalScrollBar(bool needHorizScrollbar, bool forceHorizS
_hScrollBar.Maximum = 0;
if (_hScrollBar.IsVisible)
{
- // This will trigger a call to this method via Cells_SizeChanged for
+ // This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_hScrollBar.IsVisible = false;
_ignoreNextScrollBarsLayout = true;
@@ -5591,14 +5650,14 @@ private void UpdateVerticalScrollBar(bool needVertScrollbar, bool forceVertScrol
// v---v
//|<|_____|###|>|
// ^ ^
- // min max
+ // min max
// we want to make the relative size of the thumb reflect the relative size of the viewing area
// viewportSize / (max + viewportSize) = cellsWidth / max
// -> viewportSize = max * cellsHeight / (totalVisibleHeight - cellsHeight)
// -> = max * cellsHeight / (totalVisibleHeight - cellsHeight)
// -> = max * cellsHeight / max
- // -> = cellsHeight
+ // -> = cellsHeight
// always zero
_vScrollBar.Minimum = 0;
@@ -5621,7 +5680,7 @@ private void UpdateVerticalScrollBar(bool needVertScrollbar, bool forceVertScrol
if (!_vScrollBar.IsVisible)
{
- // This will trigger a call to this method via Cells_SizeChanged for
+ // This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_vScrollBar.IsVisible = true;
if (_vScrollBar.DesiredSize.Width == 0)
@@ -5637,7 +5696,7 @@ private void UpdateVerticalScrollBar(bool needVertScrollbar, bool forceVertScrol
_vScrollBar.Maximum = 0;
if (_vScrollBar.IsVisible)
{
- // This will trigger a call to this method via Cells_SizeChanged for
+ // This will trigger a call to this method via Cells_SizeChanged for
// which no processing is needed.
_vScrollBar.IsVisible = false;
_ignoreNextScrollBarsLayout = true;
@@ -5660,8 +5719,8 @@ private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPre
Debug.Assert(slot >= 0);
// Before changing selection, check if the current cell needs to be committed, and
- // check if the current row needs to be committed. If any of those two operations are required and fail,
- // do not change selection, and do not change current cell.
+ // check if the current row needs to be committed. If any of those two operations are required and fail,
+ // do not change selection, and do not change current cell.
bool wasInEdit = EditingColumnIndex != -1;
@@ -5712,7 +5771,7 @@ private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPre
{
if (SelectionMode == DataGridSelectionMode.Single || !ctrl)
{
- // Unselect the currectly selected rows except the new selected row
+ // Unselect the currently selected rows except the new selected row
action = DataGridSelectionAction.SelectCurrent;
}
else
diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
index 1e72a077606..90401a00a26 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
@@ -10,7 +10,8 @@
using System.Reactive.Subjects;
using Avalonia.Reactive;
using System.Diagnostics;
-using Avalonia.Controls.Utils;
+using Avalonia.Controls.Utils;
+using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls
{
@@ -47,14 +48,15 @@ public virtual IBinding Binding
if (_binding != null)
{
- if(_binding is Avalonia.Data.Binding binding)
+ if(_binding is BindingBase binding)
{
if (binding.Mode == BindingMode.OneWayToSource)
{
throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead.");
}
- if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default)
+ var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
+ if (!string.IsNullOrEmpty(path) && binding.Mode == BindingMode.Default)
{
binding.Mode = BindingMode.TwoWay;
}
@@ -136,13 +138,16 @@ private static ICellEditBinding BindEditingElement(IAvaloniaObject target, Avalo
internal void SetHeaderFromBinding()
{
if (OwningGrid != null && OwningGrid.DataConnection.DataType != null
- && Header == null && Binding != null && Binding is Binding binding
- && !String.IsNullOrWhiteSpace(binding.Path))
+ && Header == null && Binding != null && Binding is BindingBase binding)
{
- string header = OwningGrid.DataConnection.DataType.GetDisplayName(binding.Path);
- if (header != null)
+ var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
+ if (!string.IsNullOrWhiteSpace(path))
{
- Header = header;
+ var header = OwningGrid.DataConnection.DataType.GetDisplayName(path);
+ if (header != null)
+ {
+ Header = header;
+ }
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
index 445dc541a7b..7dda9363171 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
@@ -173,7 +173,15 @@ private void DataGridCell_PointerPressed(PointerPressedEventArgs e)
}
if (OwningRow != null)
{
- e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
+ var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
+
+ // Do not handle PointerPressed with touch,
+ // so we can start scroll gesture on the same event.
+ if (e.Pointer.Type != PointerType.Touch)
+ {
+ e.Handled = handled;
+ }
+
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
}
@@ -197,7 +205,7 @@ internal void UpdatePseudoClasses()
}
// Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the
- // right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column
+ // right gridline will be collapsed if this cell belongs to the lastVisibleColumn and there is no filler column
internal void EnsureGridLine(DataGridColumn lastVisibleColumn)
{
if (OwningGrid != null && _rightGridLine != null)
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs
index 2f723154be4..26f0b539520 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs
@@ -40,7 +40,7 @@ public override bool Equals(object o)
return false;
}
- // There is build warning if this is missiing
+ // There is build warning if this is missing
public override int GetHashCode()
{
return base.GetHashCode();
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
index e2a067ac61d..ccf1f3f77a5 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs
@@ -17,7 +17,6 @@ namespace Avalonia.Controls
///
public class DataGridCheckBoxColumn : DataGridBoundColumn
{
- private bool _beganEditWithKeyboard;
private CheckBox _currentCheckBox;
private DataGrid _owningGrid;
@@ -153,23 +152,7 @@ protected override object PrepareCellForEdit(IControl editingElement, RoutedEven
{
if (editingElement is CheckBox editingCheckBox)
{
- bool? uneditedValue = editingCheckBox.IsChecked;
- bool editValue = false;
- if(editingEventArgs is PointerPressedEventArgs args)
- {
- // Editing was triggered by a mouse click
- Point position = args.GetPosition(editingCheckBox);
- Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
- editValue = rect.Contains(position);
- }
- else if (_beganEditWithKeyboard)
- {
- // Editing began by a user pressing spacebar
- editValue = true;
- _beganEditWithKeyboard = false;
- }
-
- if (editValue)
+ void EditValue()
{
// User clicked the checkbox itself or pressed space, let's toggle the IsChecked value
if (editingCheckBox.IsThreeState)
@@ -192,6 +175,40 @@ protected override object PrepareCellForEdit(IControl editingElement, RoutedEven
editingCheckBox.IsChecked = !editingCheckBox.IsChecked;
}
}
+
+ bool? uneditedValue = editingCheckBox.IsChecked;
+ if(editingEventArgs is PointerPressedEventArgs args)
+ {
+ void ProcessPointerArgs()
+ {
+ // Editing was triggered by a mouse click
+ Point position = args.GetPosition(editingCheckBox);
+ Rect rect = new Rect(0, 0, editingCheckBox.Bounds.Width, editingCheckBox.Bounds.Height);
+ if(rect.Contains(position))
+ {
+ EditValue();
+ }
+ }
+
+ void OnLayoutUpdated(object sender, EventArgs e)
+ {
+ if(!editingCheckBox.Bounds.IsEmpty)
+ {
+ editingCheckBox.LayoutUpdated -= OnLayoutUpdated;
+ ProcessPointerArgs();
+ }
+ }
+
+ if(editingCheckBox.Bounds.IsEmpty)
+ {
+ editingCheckBox.LayoutUpdated += OnLayoutUpdated;
+ }
+ else
+ {
+ ProcessPointerArgs();
+ }
+ }
+
return uneditedValue;
}
return false;
@@ -284,13 +301,10 @@ private void OwningGrid_KeyDown(object sender, KeyEventArgs e)
CheckBox checkBox = GetCellContent(row) as CheckBox;
if (checkBox == _currentCheckBox)
{
- _beganEditWithKeyboard = true;
OwningGrid.BeginEdit();
- return;
}
}
}
- _beganEditWithKeyboard = false;
}
private void OwningGrid_LoadingRow(object sender, DataGridRowEventArgs e)
diff --git a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs
index a4bab8b304f..ee69f1c768b 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridClipboard.cs
@@ -189,7 +189,7 @@ public bool IsColumnHeadersRow
}
///
- /// DataGrid row item used for proparing the ClipboardRowContent.
+ /// DataGrid row item used for preparing the ClipboardRowContent.
///
public object Item
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
index 23c4acdf6c0..4ab28691380 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
@@ -12,6 +12,7 @@
using System.Linq;
using System.Diagnostics;
using Avalonia.Controls.Utils;
+using Avalonia.Markup.Xaml.MarkupExtensions;
namespace Avalonia.Controls
{
@@ -26,7 +27,6 @@ public abstract class DataGridColumn : AvaloniaObject
private double? _minWidth;
private bool _settingWidthInternally;
private int _displayIndexWithFiller;
- private bool _isVisible;
private object _header;
private DataGridColumnHeader _headerCell;
private IControl _editingElement;
@@ -39,7 +39,6 @@ public abstract class DataGridColumn : AvaloniaObject
///
protected internal DataGridColumn()
{
- _isVisible = true;
_displayIndexWithFiller = -1;
IsInitialDesiredWidthDetermined = false;
InheritsWidth = true;
@@ -173,32 +172,42 @@ internal ICellEditBinding CellEditBinding
get => _editBinding;
}
+
+ ///
+ /// Defines the property.
+ ///
+ public static StyledProperty IsVisibleProperty =
+ Control.IsVisibleProperty.AddOwner();
+
///
/// Determines whether or not this column is visible.
///
public bool IsVisible
{
- get
- {
- return _isVisible;
- }
- set
- {
- if (value != IsVisible)
- {
- OwningGrid?.OnColumnVisibleStateChanging(this);
- _isVisible = value;
+ get => GetValue(IsVisibleProperty);
+ set => SetValue(IsVisibleProperty, value);
+ }
- if (_headerCell != null)
- {
- _headerCell.IsVisible = value;
- }
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == IsVisibleProperty)
+ {
+ OwningGrid?.OnColumnVisibleStateChanging(this);
+ var isVisible = (change as AvaloniaPropertyChangedEventArgs).NewValue.Value;
- OwningGrid?.OnColumnVisibleStateChanged(this);
+ if (_headerCell != null)
+ {
+ _headerCell.IsVisible = isVisible;
}
+
+ OwningGrid?.OnColumnVisibleStateChanged(this);
+ NotifyPropertyChanged(change.Property.Name);
}
}
+
///
/// Actual visible width after Width, MinWidth, and MaxWidth setting at the Column level and DataGrid level
/// have been taken into account
@@ -665,6 +674,7 @@ protected virtual void CancelCellEdit(IControl editingElement, object uneditedVa
///
/// The data item represented by the row that contains the intended cell.
///
+ /// When the method returns, contains the applied binding.
///
/// A new editing element that is bound to the column's property value.
///
@@ -785,7 +795,7 @@ internal DataGridLength CoerceWidth(DataGridLength width)
}
///
- /// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to
+ /// If the DataGrid is using layout rounding, the pixel snapping will force all widths to
/// whole numbers. Since the column widths aren't visual elements, they don't go through the normal
/// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some
/// pixel gaps and/or overlaps between columns.
@@ -1007,6 +1017,14 @@ public string SortMemberPath
get;
set;
}
+ ///
+ /// Holds a Comparer to use for sorting, if not using the default.
+ ///
+ public System.Collections.IComparer CustomSortComparer
+ {
+ get;
+ set;
+ }
///
/// We get the sort description from the data source. We don't worry whether we can modify sort -- perhaps the sort description
@@ -1018,6 +1036,14 @@ internal DataGridSortDescription GetSortDescription()
&& OwningGrid.DataConnection != null
&& OwningGrid.DataConnection.SortDescriptions != null)
{
+ if(CustomSortComparer != null)
+ {
+ return
+ OwningGrid.DataConnection.SortDescriptions
+ .OfType()
+ .FirstOrDefault(s => s.SourceComparer == CustomSortComparer);
+ }
+
string propertyName = GetSortPropertyName();
return OwningGrid.DataConnection.SortDescriptions.FirstOrDefault(s => s.HasPropertyPath && s.PropertyPath == propertyName);
@@ -1032,13 +1058,16 @@ internal string GetSortPropertyName()
if (String.IsNullOrEmpty(result))
{
-
- if(this is DataGridBoundColumn boundColumn &&
- boundColumn.Binding != null &&
- boundColumn.Binding is Binding binding &&
- binding.Path != null)
+ if (this is DataGridBoundColumn boundColumn)
{
- result = binding.Path;
+ if (boundColumn.Binding is Binding binding)
+ {
+ result = binding.Path;
+ }
+ else if (boundColumn.Binding is CompiledBindingExtension compiledBinding)
+ {
+ result = compiledBinding.Path.ToString();
+ }
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
index 856d1f65667..6f957497cbf 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
@@ -274,6 +274,12 @@ internal void ProcessSort(KeyModifiers keyModifiers)
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();
@@ -340,8 +346,6 @@ internal void OnMouseLeftButtonDown(ref bool handled, PointerEventArgs args, Poi
if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
{
- args.Pointer.Capture(this);
-
_dragMode = DragMode.MouseDown;
_frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
_lastMousePositionHeaders = this.Translate(OwningGrid.ColumnHeaders, mousePosition);
@@ -413,8 +417,9 @@ internal void OnMouseLeftButtonUp(ref bool handled, PointerEventArgs args, Point
}
//TODO DragEvents
- internal void OnMouseMove(ref bool handled, Point mousePosition, Point mousePositionHeaders)
+ internal void OnMouseMove(PointerEventArgs args, Point mousePosition, Point mousePositionHeaders)
{
+ var handled = args.Handled;
if (handled || OwningGrid == null || OwningGrid.ColumnHeaders == null)
{
return;
@@ -438,7 +443,10 @@ internal void OnMouseMove(ref bool handled, Point mousePosition, Point mousePosi
}
_lastMousePositionHeaders = mousePositionHeaders;
-
+
+ if (args.Pointer.Captured != this && _dragMode == DragMode.Drag)
+ args.Pointer.Capture(this);
+
SetDragCursor(mousePosition);
}
@@ -506,8 +514,7 @@ private void DataGridColumnHeader_PointerMove(object sender, PointerEventArgs e)
Point mousePosition = e.GetPosition(this);
Point mousePositionHeaders = e.GetPosition(OwningGrid.ColumnHeaders);
- bool handled = false;
- OnMouseMove(ref handled, mousePosition, mousePositionHeaders);
+ OnMouseMove(e, mousePosition, mousePositionHeaders);
}
///
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
index 46bcd0d347c..a4577ee952c 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
@@ -5,6 +5,7 @@
using Avalonia.Controls.Utils;
using Avalonia.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
@@ -141,9 +142,9 @@ internal bool GetColumnReadOnlyState(DataGridColumn dataGridColumn, bool isReadO
Debug.Assert(dataGridColumn != null);
if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn &&
- dataGridBoundColumn.Binding is Binding binding)
+ dataGridBoundColumn.Binding is BindingBase binding)
{
- string path = binding.Path;
+ var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
if (string.IsNullOrWhiteSpace(path))
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs
index e659438b435..2b8055dd225 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs
@@ -79,7 +79,7 @@ internal double PendingVerticalScrollHeight
set;
}
- internal void AddRecylableRow(DataGridRow row)
+ internal void AddRecyclableRow(DataGridRow row)
{
Debug.Assert(!_recyclableRows.Contains(row));
row.DetachFromDataGrid(true);
@@ -120,7 +120,7 @@ internal void ClearElements(bool recycle)
{
if (row.IsRecyclable)
{
- AddRecylableRow(row);
+ AddRecyclableRow(row);
}
else
{
@@ -193,7 +193,7 @@ private int GetCircularListIndex(int slot, bool wrap)
internal void FullyRecycleElements()
{
- // Fully recycle Recycleable rows and transfer them to Recycled rows
+ // Fully recycle Recyclable rows and transfer them to Recycled rows
while (_recyclableRows.Count > 0)
{
DataGridRow row = _recyclableRows.Pop();
@@ -202,7 +202,7 @@ internal void FullyRecycleElements()
Debug.Assert(!_fullyRecycledRows.Contains(row));
_fullyRecycledRows.Push(row);
}
- // Fully recycle Recycleable GroupHeaders and transfer them to Recycled GroupHeaders
+ // Fully recycle Recyclable GroupHeaders and transfer them to Recycled GroupHeaders
while (_recyclableGroupHeaders.Count > 0)
{
DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop();
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index c3562c53a4f..75469704989 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
@@ -392,7 +392,7 @@ internal int Slot
set;
}
- // Height that the row will eventually end up at after a possible detalis animation has completed
+ // Height that the row will eventually end up at after a possible details animation has completed
internal double TargetHeight
{
get
@@ -517,7 +517,7 @@ protected override Size MeasureOverride(Size availableSize)
return base.MeasureOverride(availableSize);
}
- //Allow the DataGrid specific componets to adjust themselves based on new values
+ //Allow the DataGrid specific components to adjust themselves based on new values
if (_headerElement != null)
{
_headerElement.InvalidateMeasure();
@@ -722,7 +722,7 @@ internal void EnsureGridLines()
if (_bottomGridLine != null)
{
// It looks like setting Visibility sometimes has side effects so make sure the value is actually
- // diffferent before setting it
+ // different before setting it
bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All;
if (newVisibility != _bottomGridLine.IsVisible)
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
index a69b8eafe1c..1d5c8999932 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
@@ -425,7 +425,7 @@ internal bool ScrollSlotIntoView(int slot, bool scrolledHorizontally)
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, CellsHeight);
}
- if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot)
+ if (DisplayData.FirstScrollingSlot < slot && (DisplayData.LastScrollingSlot > slot || DisplayData.LastScrollingSlot == -1))
{
// The row is already displayed in its entirety
return true;
@@ -1193,7 +1193,7 @@ private void InsertDisplayedElement(int slot, Control element, bool wasNewlyAdde
else
{
groupHeader = element as DataGridRowGroupHeader;
- Debug.Assert(groupHeader != null); // Nothig other and Rows and RowGroups now
+ Debug.Assert(groupHeader != null); // Nothing other and Rows and RowGroups now
if (groupHeader != null)
{
groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1];
@@ -1636,7 +1636,7 @@ private bool SlotIsDisplayed(int slot)
if (slot >= DisplayData.FirstScrollingSlot &&
slot <= DisplayData.LastScrollingSlot)
{
- // Additional row takes the spot of a displayed row - it is necessarilly displayed
+ // Additional row takes the spot of a displayed row - it is necessarily displayed
return true;
}
else if (DisplayData.FirstScrollingSlot == -1 &&
@@ -1825,7 +1825,7 @@ private void ScrollSlotsByHeight(double height)
if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset))
{
// We've scrolled off more of the first row than what's possible. This can happen
- // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling
+ // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling
// cleanup issue. In this case, simply try to display the next row as the first row instead
if (newFirstScrollingSlot < SlotCount - 1)
{
@@ -2014,7 +2014,7 @@ private void UnloadRow(DataGridRow dataGridRow)
if (recycleRow)
{
- DisplayData.AddRecylableRow(dataGridRow);
+ DisplayData.AddRecyclableRow(dataGridRow);
}
else
{
@@ -2265,7 +2265,7 @@ private void CollectionViewGroup_CollectionChanged_Add(object sender, NotifyColl
if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1)
{
// We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed
- EnsureAnscestorsExpanderButtonChecked(parentGroupInfo);
+ EnsureAncestorsExpanderButtonChecked(parentGroupInfo);
}
}
}
@@ -2407,7 +2407,7 @@ private int CountAndPopulateGroupHeaders(object group, int rootSlot, int level)
return treeCount;
}
- private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo)
+ private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo)
{
if (IsSlotVisible(parentGroupInfo.Slot))
{
@@ -2789,11 +2789,11 @@ private DataGridRowGroupInfo GetParentGroupInfo(object collection)
return null;
}
- internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisibile, bool setCurrent)
+ internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisible, bool setCurrent)
{
Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0);
- if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisibile, setCurrent); }) || !CommitEdit())
+ if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisible, setCurrent); }) || !CommitEdit())
{
return;
}
@@ -2804,7 +2804,7 @@ internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool n
UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
}
- UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisibile, isDisplayed: true);
+ UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisible, isDisplayed: true);
ComputeScrollBarsLayout();
// We need force arrange since our Scrollings Rows could update without automatically triggering layout
diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
index e513a7b6785..7e95dd100cf 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
@@ -8,6 +8,7 @@
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
+using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@@ -22,6 +23,7 @@ public class DataGridTemplateColumn : DataGridColumn
o => o.CellTemplate,
(o, v) => o.CellTemplate = v);
+ [Content]
public IDataTemplate CellTemplate
{
get { return _cellTemplate; }
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
index 1c350a4f145..4eed119240d 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
@@ -140,7 +140,7 @@ protected override Size ArrangeOverride(Size finalSize)
if (dataGridColumn.IsFrozen)
{
columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height));
- columnHeader.Clip = null; // The layout system could have clipped this becaues it's not aware of our render transform
+ columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform
if (DragColumn == dataGridColumn && DragIndicator != null)
{
dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset;
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
index 6ad58bf2505..308ebc69d40 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
@@ -3,18 +3,27 @@
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
-using Avalonia.Media;
using System;
using System.Diagnostics;
+using Avalonia.Input;
+using Avalonia.Input.GestureRecognizers;
+using Avalonia.Layout;
+using Avalonia.Media;
+
namespace Avalonia.Controls.Primitives
{
///
- /// Used within the template of a to specify the
+ /// Used within the template of a to specify the
/// location in the control's visual tree where the rows are to be added.
///
public sealed class DataGridRowsPresenter : Panel
{
+ public DataGridRowsPresenter()
+ {
+ AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
+ }
+
internal DataGrid OwningGrid
{
get;
@@ -22,9 +31,10 @@ internal DataGrid OwningGrid
}
private double _measureHeightOffset = 0;
+
private double CalculateEstimatedAvailableHeight(Size availableSize)
{
- if(!Double.IsPositiveInfinity(availableSize.Height))
+ if (!Double.IsPositiveInfinity(availableSize.Height))
{
return availableSize.Height + _measureHeightOffset;
}
@@ -50,10 +60,10 @@ protected override Size ArrangeOverride(Size finalSize)
return base.ArrangeOverride(finalSize);
}
- if(OwningGrid.RowsPresenterAvailableSize.HasValue)
+ if (OwningGrid.RowsPresenterAvailableSize.HasValue)
{
var availableHeight = OwningGrid.RowsPresenterAvailableSize.Value.Height;
- if(!Double.IsPositiveInfinity(availableHeight))
+ if (!Double.IsPositiveInfinity(availableHeight))
{
_measureHeightOffset = finalSize.Height - availableHeight;
OwningGrid.RowsPresenterEstimatedAvailableHeight = finalSize.Height;
@@ -108,6 +118,18 @@ protected override Size ArrangeOverride(Size finalSize)
///
protected override Size MeasureOverride(Size availableSize)
{
+ if (double.IsInfinity(availableSize.Height))
+ {
+ if (VisualRoot is TopLevel topLevel)
+ {
+ double maxHeight = topLevel.IsArrangeValid ?
+ topLevel.Bounds.Height :
+ LayoutHelper.ApplyLayoutConstraints(topLevel, availableSize).Height;
+
+ availableSize = availableSize.WithHeight(maxHeight);
+ }
+ }
+
if (availableSize.Height == 0 || OwningGrid == null)
{
return base.MeasureOverride(availableSize);
@@ -162,6 +184,11 @@ protected override Size MeasureOverride(Size availableSize)
return new Size(totalCellsWidth + headerWidth, totalHeight);
}
+ private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
+ {
+ e.Handled = e.Handled || OwningGrid.UpdateScroll(-e.Delta);
+ }
+
#if DEBUG
internal void PrintChildren()
{
diff --git a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
index 82b01e99bb3..1b122996d20 100644
--- a/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
@@ -1,10 +1,13 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
-
+#if SIGNED_BUILD
+[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+#else
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
-
+#endif
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
index cf062e09207..ca0873e1834 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml
@@ -137,12 +137,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
new file mode 100644
index 00000000000..66fad557d58
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.cs
@@ -0,0 +1,52 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.Controls
+{
+ internal class FilterTextBox : TextBox, IStyleable
+ {
+ public static readonly DirectProperty UseRegexFilterProperty =
+ AvaloniaProperty.RegisterDirect(nameof(UseRegexFilter),
+ o => o.UseRegexFilter, (o, v) => o.UseRegexFilter = v,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly DirectProperty UseCaseSensitiveFilterProperty =
+ AvaloniaProperty.RegisterDirect(nameof(UseCaseSensitiveFilter),
+ o => o.UseCaseSensitiveFilter, (o, v) => o.UseCaseSensitiveFilter = v,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly DirectProperty UseWholeWordFilterProperty =
+ AvaloniaProperty.RegisterDirect(nameof(UseWholeWordFilter),
+ o => o.UseWholeWordFilter, (o, v) => o.UseWholeWordFilter = v,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
+
+ public FilterTextBox()
+ {
+ Classes.Add("filter-text-box");
+ }
+
+ public bool UseRegexFilter
+ {
+ get => _useRegexFilter;
+ set => SetAndRaise(UseRegexFilterProperty, ref _useRegexFilter, value);
+ }
+
+ public bool UseCaseSensitiveFilter
+ {
+ get => _useCaseSensitiveFilter;
+ set => SetAndRaise(UseCaseSensitiveFilterProperty, ref _useCaseSensitiveFilter, value);
+ }
+
+ public bool UseWholeWordFilter
+ {
+ get => _useWholeWordFilter;
+ set => SetAndRaise(UseWholeWordFilterProperty, ref _useWholeWordFilter, value);
+ }
+
+ Type IStyleable.StyleKey => typeof(TextBox);
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml
new file mode 100644
index 00000000000..c202d137781
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
new file mode 100644
index 00000000000..cb98fb70f30
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.cs
@@ -0,0 +1,125 @@
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.Controls
+{
+ internal class ThicknessEditor : ContentControl
+ {
+ public static readonly DirectProperty ThicknessProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Thickness), o => o.Thickness,
+ (o, v) => o.Thickness = v, defaultBindingMode: BindingMode.TwoWay);
+
+ public static readonly DirectProperty HeaderProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header,
+ (o, v) => o.Header = v);
+
+ public static readonly DirectProperty IsPresentProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Header), o => o.IsPresent,
+ (o, v) => o.IsPresent = v);
+
+ public static readonly DirectProperty LeftProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Left), o => o.Left, (o, v) => o.Left = v);
+
+ public static readonly DirectProperty TopProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Top), o => o.Top, (o, v) => o.Top = v);
+
+ public static readonly DirectProperty RightProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Right), o => o.Right,
+ (o, v) => o.Right = v);
+
+ public static readonly DirectProperty BottomProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Bottom), o => o.Bottom,
+ (o, v) => o.Bottom = v);
+
+ public static readonly StyledProperty HighlightProperty =
+ AvaloniaProperty.Register(nameof(Highlight));
+
+ private Thickness _thickness;
+ private string? _header;
+ private bool _isPresent = true;
+ private double _left;
+ private double _top;
+ private double _right;
+ private double _bottom;
+ private bool _isUpdatingThickness;
+
+ public Thickness Thickness
+ {
+ get => _thickness;
+ set => SetAndRaise(ThicknessProperty, ref _thickness, value);
+ }
+
+ public string? Header
+ {
+ get => _header;
+ set => SetAndRaise(HeaderProperty, ref _header, value);
+ }
+
+ public bool IsPresent
+ {
+ get => _isPresent;
+ set => SetAndRaise(IsPresentProperty, ref _isPresent, value);
+ }
+
+ public double Left
+ {
+ get => _left;
+ set => SetAndRaise(LeftProperty, ref _left, value);
+ }
+
+ public double Top
+ {
+ get => _top;
+ set => SetAndRaise(TopProperty, ref _top, value);
+ }
+
+ public double Right
+ {
+ get => _right;
+ set => SetAndRaise(RightProperty, ref _right, value);
+ }
+
+ public double Bottom
+ {
+ get => _bottom;
+ set => SetAndRaise(BottomProperty, ref _bottom, value);
+ }
+
+ public IBrush Highlight
+ {
+ get => GetValue(HighlightProperty);
+ set => SetValue(HighlightProperty, value);
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == ThicknessProperty)
+ {
+ try
+ {
+ _isUpdatingThickness = true;
+
+ var value = change.NewValue.GetValueOrDefault();
+
+ Left = value.Left;
+ Top = value.Top;
+ Right = value.Right;
+ Bottom = value.Bottom;
+ }
+ finally
+ {
+ _isUpdatingThickness = false;
+ }
+ }
+ else if (!_isUpdatingThickness &&
+ (change.Property == LeftProperty || change.Property == TopProperty ||
+ change.Property == RightProperty || change.Property == BottomProperty))
+ {
+ Thickness = new Thickness(Left, Top, Right, Bottom);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
deleted file mode 100644
index 37ba5155fdf..00000000000
--- a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Globalization;
-using Avalonia.Data.Converters;
-using Avalonia.Media;
-
-namespace Avalonia.Diagnostics.Converters
-{
- internal class BoolToBrushConverter : IValueConverter
- {
- public IBrush Brush { get; set; }
-
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- {
- return (bool)value ? Brush : Brushes.Transparent;
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
new file mode 100644
index 00000000000..0b9044e65ed
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Diagnostics.Converters
+{
+ internal class BoolToOpacityConverter : IValueConverter
+ {
+ public double Opacity { get; set; }
+
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool boolean && boolean)
+ {
+ return 1d;
+ }
+
+ return Opacity;
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
new file mode 100644
index 00000000000..4863782f442
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/EnumToCheckedConverter.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Globalization;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Diagnostics.Converters
+{
+ internal class EnumToCheckedConverter : IValueConverter
+ {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return Equals(value, parameter);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is bool isChecked && isChecked)
+ {
+ return parameter;
+ }
+
+ return BindingOperations.DoNothing;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
index 4899be29556..2a386f106e9 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
@@ -10,15 +10,24 @@ namespace Avalonia.Diagnostics
{
public static class DevTools
{
- private static readonly Dictionary s_open = new Dictionary();
+ private static readonly Dictionary s_open =
+ new Dictionary();
public static IDisposable Attach(TopLevel root, KeyGesture gesture)
{
- void PreviewKeyDown(object sender, KeyEventArgs e)
+ return Attach(root, new DevToolsOptions()
{
- if (gesture.Matches(e))
+ Gesture = gesture,
+ });
+ }
+
+ public static IDisposable Attach(TopLevel root, DevToolsOptions options)
+ {
+ void PreviewKeyDown(object? sender, KeyEventArgs e)
+ {
+ if (options.Gesture.Matches(e))
{
- Open(root);
+ Open(root, options);
}
}
@@ -28,7 +37,9 @@ void PreviewKeyDown(object sender, KeyEventArgs e)
RoutingStrategies.Tunnel);
}
- public static IDisposable Open(TopLevel root)
+ public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions());
+
+ public static IDisposable Open(TopLevel root, DevToolsOptions options)
{
if (s_open.TryGetValue(root, out var window))
{
@@ -38,15 +49,16 @@ public static IDisposable Open(TopLevel root)
{
window = new MainWindow
{
- Width = 1024,
- Height = 512,
Root = root,
+ Width = options.Size.Width,
+ Height = options.Size.Height,
};
+ window.SetOptions(options);
window.Closed += DevToolsClosed;
s_open.Add(root, window);
- if (root is Window inspectedWindow)
+ if (options.ShowAsChildWindow && root is Window inspectedWindow)
{
window.Show(inspectedWindow);
}
@@ -59,10 +71,10 @@ public static IDisposable Open(TopLevel root)
return Disposable.Create(() => window?.Close());
}
- private static void DevToolsClosed(object sender, EventArgs e)
+ private static void DevToolsClosed(object? sender, EventArgs e)
{
- var window = (MainWindow)sender;
- s_open.Remove(window.Root);
+ var window = (MainWindow)sender!;
+ s_open.Remove(window.Root!);
window.Closed -= DevToolsClosed;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
new file mode 100644
index 00000000000..5336dca65b0
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
@@ -0,0 +1,31 @@
+using Avalonia.Input;
+
+namespace Avalonia.Diagnostics
+{
+ ///
+ /// Describes options used to customize DevTools.
+ ///
+ public class DevToolsOptions
+ {
+ ///
+ /// Gets or sets the key gesture used to open DevTools.
+ ///
+ public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12);
+
+ ///
+ /// Gets or sets a value indicating whether DevTools should be displayed as a child window
+ /// of the window being inspected. The default value is true.
+ ///
+ public bool ShowAsChildWindow { get; set; } = true;
+
+ ///
+ /// Gets or sets the initial size of the DevTools window. The default value is 1280x720.
+ ///
+ public Size Size { get; set; } = new Size(1280, 720);
+
+ ///
+ /// Get or set the startup screen index where the DevTools window will be displayed.
+ ///
+ public int? StartupScreenIndex { get; set; }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
index 5927bd785e3..4f4579c7d9b 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs
@@ -22,8 +22,8 @@ public class ConsoleContext
clear(): Clear the output history
";
- public dynamic e { get; internal set; }
- public dynamic root { get; internal set; }
+ public dynamic? e { get; internal set; }
+ public dynamic? root { get; internal set; }
internal static object NoOutput { get; } = new object();
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
index 65117dc3d16..4f493bdcc20 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
@@ -7,15 +7,15 @@ internal class EventChainLink
{
public EventChainLink(object handler, bool handled, RoutingStrategies route)
{
- Contract.Requires(handler != null);
-
- Handler = handler;
+ Handler = handler ?? throw new ArgumentNullException(nameof(handler));
Handled = handled;
Route = route;
}
public object Handler { get; }
+ public bool BeginsNewRoute { get; set; }
+
public string HandlerName
{
get
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
index be3564e7816..16852001daf 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
@@ -9,12 +9,12 @@ internal class ViewLocator : IDataTemplate
{
public IControl Build(object data)
{
- var name = data.GetType().FullName.Replace("ViewModel", "View");
+ var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
- return (Control)Activator.CreateInstance(type);
+ return (Control)Activator.CreateInstance(type)!;
}
else
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
index a9353eba8bb..e4c4ca6115a 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
@@ -1,17 +1,17 @@
-using System.ComponentModel;
-using Avalonia.Collections;
-
namespace Avalonia.Diagnostics.ViewModels
{
internal class AvaloniaPropertyViewModel : PropertyViewModel
{
private readonly AvaloniaObject _target;
private string _type;
- private object _value;
+ private object? _value;
private string _priority;
private string _group;
+#nullable disable
+ // Remove "nullable disable" after MemberNotNull will work on our CI.
public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
+#nullable restore
{
_target = o;
Property = property;
@@ -20,25 +20,17 @@ public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property)
$"[{property.OwnerType.Name}.{property.Name}]" :
property.Name;
- if (property.IsDirect)
- {
- _group = "Properties";
- Priority = "Direct";
- }
-
Update();
}
public AvaloniaProperty Property { get; }
public override object Key => Property;
public override string Name { get; }
- public bool IsAttached => Property.IsAttached;
+ public override bool? IsAttached =>
+ Property.IsAttached;
- public string Priority
- {
- get => _priority;
- private set => RaiseAndSetIfChanged(ref _priority, value);
- }
+ public override string Priority =>
+ _priority;
public override string Type => _type;
@@ -56,40 +48,37 @@ public override string Value
}
}
- public override string Group
- {
- get => _group;
- }
+ public override string Group => _group;
+ // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
public override void Update()
{
if (Property.IsDirect)
{
RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value));
- RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _priority, "Direct", nameof(Priority));
+
+ _group = "Properties";
}
else
{
var val = _target.GetDiagnostic(Property);
RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value));
- RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
if (val != null)
{
- SetGroup(IsAttached ? "Attached Properties" : "Properties");
- Priority = val.Priority.ToString();
+ RaiseAndSetIfChanged(ref _priority, val.Priority.ToString(), nameof(Priority));
+ RaiseAndSetIfChanged(ref _group, IsAttached == true ? "Attached Properties" : "Properties", nameof(Group));
}
else
{
- SetGroup(Priority = "Unset");
+ RaiseAndSetIfChanged(ref _priority, "Unset", nameof(Priority));
+ RaiseAndSetIfChanged(ref _group, "Unset", nameof(Group));
}
}
}
-
- private void SetGroup(string group)
- {
- RaiseAndSetIfChanged(ref _group, group, nameof(Group));
- }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
index af5e2542044..65626aeea5e 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
@@ -1,5 +1,4 @@
-using System.ComponentModel;
-using System.Reflection;
+using System.Reflection;
namespace Avalonia.Diagnostics.ViewModels
{
@@ -7,14 +6,17 @@ internal class ClrPropertyViewModel : PropertyViewModel
{
private readonly object _target;
private string _type;
- private object _value;
+ private object? _value;
+#nullable disable
+ // Remove "nullable disable" after MemberNotNull will work on our CI.
public ClrPropertyViewModel(object o, PropertyInfo property)
+#nullable restore
{
_target = o;
Property = property;
- if (!property.DeclaringType.IsInterface)
+ if (property.DeclaringType == null || !property.DeclaringType.IsInterface)
{
Name = property.Name;
}
@@ -47,11 +49,18 @@ public override string Value
}
}
+ public override string Priority =>
+ string.Empty;
+
+ public override bool? IsAttached =>
+ default;
+
+ // [MemberNotNull(nameof(_type))]
public override void Update()
{
var val = Property.GetValue(_target);
RaiseAndSetIfChanged(ref _value, val, nameof(Value));
- RaiseAndSetIfChanged(ref _type, _value?.GetType().Name, nameof(Type));
+ RaiseAndSetIfChanged(ref _type, _value?.GetType().Name ?? Property.PropertyType.Name, nameof(Type));
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
index 0e0c44ded8a..717b49d0741 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs
@@ -15,11 +15,12 @@ internal class ConsoleViewModel : ViewModelBase
private int _historyIndex = -1;
private string _input;
private bool _isVisible;
- private ScriptState _state;
+ private ScriptState? _state;
public ConsoleViewModel(Action updateContext)
{
_context = new ConsoleContext(this);
+ _input = string.Empty;
_updateContext = updateContext;
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
index d4b988acd44..3790951b0c1 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -1,8 +1,15 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
+using System.Reflection;
using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
@@ -11,7 +18,10 @@ internal class ControlDetailsViewModel : ViewModelBase, IDisposable
{
private readonly IVisual _control;
private readonly IDictionary> _propertyIndex;
- private AvaloniaPropertyViewModel _selectedProperty;
+ private PropertyViewModel? _selectedProperty;
+ private bool _snapshotStyles;
+ private bool _showInactiveStyles;
+ private string? _styleStatus;
public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
@@ -32,6 +42,8 @@ public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
view.Filter = FilterProperty;
PropertiesView = view;
+ Layout = new ControlLayoutViewModel(control);
+
if (control is INotifyPropertyChanged inpc)
{
inpc.PropertyChanged += ControlPropertyChanged;
@@ -41,18 +53,148 @@ public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
{
ao.PropertyChanged += ControlPropertyChanged;
}
+
+ AppliedStyles = new ObservableCollection();
+ PseudoClasses = new ObservableCollection();
+
+ if (control is StyledElement styledElement)
+ {
+ styledElement.Classes.CollectionChanged += OnClassesChanged;
+
+ var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes(true);
+
+ foreach (var classAttribute in pseudoClassAttributes)
+ {
+ foreach (var className in classAttribute.PseudoClasses)
+ {
+ PseudoClasses.Add(new PseudoClassViewModel(className, styledElement));
+ }
+ }
+
+ var styleDiagnostics = styledElement.GetStyleDiagnostics();
+
+ foreach (var appliedStyle in styleDiagnostics.AppliedStyles)
+ {
+ var styleSource = appliedStyle.Source;
+
+ var setters = new List();
+
+ if (styleSource is Style style)
+ {
+ foreach (var setter in style.Setters)
+ {
+ if (setter is Setter regularSetter
+ && regularSetter.Property != null)
+ {
+ var setterValue = regularSetter.Value;
+
+ var resourceInfo = GetResourceInfo(setterValue);
+
+ SetterViewModel setterVm;
+
+ if (resourceInfo.HasValue)
+ {
+ var resourceKey = resourceInfo.Value.resourceKey;
+ var resourceValue = styledElement.FindResource(resourceKey);
+
+ setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic);
+ }
+ else
+ {
+ setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+ }
+
+ setters.Add(setterVm);
+ }
+ }
+
+ AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters));
+ }
+ }
+
+ UpdateStyles();
+ }
+ }
+
+ private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
+ {
+ if (value is StaticResourceExtension staticResource)
+ {
+ return (staticResource.ResourceKey, false);
+ }
+ else if (value is DynamicResourceExtension dynamicResource
+ && dynamicResource.ResourceKey != null)
+ {
+ return (dynamicResource.ResourceKey, true);
+ }
+
+ return null;
}
public TreePageViewModel TreePage { get; }
public DataGridCollectionView PropertiesView { get; }
- public AvaloniaPropertyViewModel SelectedProperty
+ public ObservableCollection AppliedStyles { get; }
+
+ public ObservableCollection PseudoClasses { get; }
+
+ public PropertyViewModel? SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
+ public bool SnapshotStyles
+ {
+ get => _snapshotStyles;
+ set => RaiseAndSetIfChanged(ref _snapshotStyles, value);
+ }
+
+ public bool ShowInactiveStyles
+ {
+ get => _showInactiveStyles;
+ set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
+ }
+
+ public string? StyleStatus
+ {
+ get => _styleStatus;
+ set => RaiseAndSetIfChanged(ref _styleStatus, value);
+ }
+
+ public ControlLayoutViewModel Layout { get; }
+
+ protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ base.OnPropertyChanged(e);
+
+ if (e.PropertyName == nameof(SnapshotStyles))
+ {
+ if (!SnapshotStyles)
+ {
+ UpdateStyles();
+ }
+ }
+ }
+
+ public void UpdateStyleFilters()
+ {
+ foreach (var style in AppliedStyles)
+ {
+ var hasVisibleSetter = false;
+
+ foreach (var setter in style.Setters)
+ {
+ setter.IsVisible = TreePage.SettersFilter.Filter(setter.Name);
+
+ hasVisibleSetter |= setter.IsVisible;
+ }
+
+ style.IsVisible = hasVisibleSetter;
+ }
+ }
+
public void Dispose()
{
if (_control is INotifyPropertyChanged inpc)
@@ -64,6 +206,11 @@ public void Dispose()
{
ao.PropertyChanged -= ControlPropertyChanged;
}
+
+ if (_control is StyledElement se)
+ {
+ se.Classes.CollectionChanged -= OnClassesChanged;
+ }
}
private IEnumerable GetAvaloniaProperties(object o)
@@ -103,7 +250,7 @@ private IEnumerable GetClrProperties(object o, Type t)
.Select(x => new ClrPropertyViewModel(o, x));
}
- private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+ private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_propertyIndex.TryGetValue(e.Property, out var properties))
{
@@ -112,42 +259,103 @@ private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventA
property.Update();
}
}
+
+ Layout.ControlPropertyChanged(sender, e);
}
- private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e)
+ private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (_propertyIndex.TryGetValue(e.PropertyName, out var properties))
+ if (e.PropertyName != null
+ && _propertyIndex.TryGetValue(e.PropertyName, out var properties))
{
foreach (var property in properties)
{
property.Update();
}
}
+
+ if (!SnapshotStyles)
+ {
+ UpdateStyles();
+ }
}
- private bool FilterProperty(object arg)
+ private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
- if (!string.IsNullOrWhiteSpace(TreePage.PropertyFilter) && arg is PropertyViewModel property)
+ if (!SnapshotStyles)
{
- if (TreePage.UseRegexFilter)
+ UpdateStyles();
+ }
+ }
+
+ private void UpdateStyles()
+ {
+ int activeCount = 0;
+
+ foreach (var style in AppliedStyles)
+ {
+ style.Update();
+
+ if (style.IsActive)
+ {
+ activeCount++;
+ }
+ }
+
+ var propertyBuckets = new Dictionary>();
+
+ foreach (var style in AppliedStyles)
+ {
+ if (!style.IsActive)
{
- return TreePage.FilterRegex?.IsMatch(property.Name) ?? true;
+ continue;
}
- return property.Name.IndexOf(TreePage.PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1;
+ foreach (var setter in style.Setters)
+ {
+ if (propertyBuckets.TryGetValue(setter.Property, out var setters))
+ {
+ foreach (var otherSetter in setters)
+ {
+ otherSetter.IsActive = false;
+ }
+
+ setter.IsActive = true;
+
+ setters.Add(setter);
+ }
+ else
+ {
+ setter.IsActive = true;
+
+ setters = new List { setter };
+
+ propertyBuckets.Add(setter.Property, setters);
+ }
+ }
}
- return true;
+ foreach (var pseudoClass in PseudoClasses)
+ {
+ pseudoClass.Update();
+ }
+
+ StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)";
+ }
+
+ private bool FilterProperty(object arg)
+ {
+ return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name);
}
private class PropertyComparer : IComparer
{
public static PropertyComparer Instance { get; } = new PropertyComparer();
- public int Compare(PropertyViewModel x, PropertyViewModel y)
+ public int Compare(PropertyViewModel? x, PropertyViewModel? y)
{
- var groupX = GroupIndex(x.Group);
- var groupY = GroupIndex(y.Group);
+ var groupX = GroupIndex(x?.Group);
+ var groupY = GroupIndex(y?.Group);
if (groupX != groupY)
{
@@ -155,11 +363,11 @@ public int Compare(PropertyViewModel x, PropertyViewModel y)
}
else
{
- return string.CompareOrdinal(x.Name, y.Name);
+ return string.CompareOrdinal(x?.Name, y?.Name);
}
}
- private int GroupIndex(string group)
+ private int GroupIndex(string? group)
{
switch (group)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
new file mode 100644
index 00000000000..4dc0c34c0a5
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
@@ -0,0 +1,240 @@
+using System;
+using System.ComponentModel;
+using System.Text;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class ControlLayoutViewModel : ViewModelBase
+ {
+ private readonly IVisual _control;
+ private Thickness _borderThickness;
+ private double _height;
+ private string? _heightConstraint;
+ private HorizontalAlignment _horizontalAlignment;
+ private Thickness _marginThickness;
+ private Thickness _paddingThickness;
+ private bool _updatingFromControl;
+ private VerticalAlignment _verticalAlignment;
+ private double _width;
+ private string? _widthConstraint;
+
+ public ControlLayoutViewModel(IVisual control)
+ {
+ _control = control;
+
+ HasPadding = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Decorator.PaddingProperty);
+ HasBorder = AvaloniaPropertyRegistry.Instance.IsRegistered(control, Border.BorderThicknessProperty);
+
+ if (control is AvaloniaObject ao)
+ {
+ MarginThickness = ao.GetValue(Layoutable.MarginProperty);
+
+ if (HasPadding)
+ {
+ PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
+ }
+
+ if (HasBorder)
+ {
+ BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
+ }
+
+ HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
+ VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
+ }
+
+ UpdateSize();
+ UpdateSizeConstraints();
+ }
+
+ public Thickness MarginThickness
+ {
+ get => _marginThickness;
+ set => RaiseAndSetIfChanged(ref _marginThickness, value);
+ }
+
+ public Thickness BorderThickness
+ {
+ get => _borderThickness;
+ set => RaiseAndSetIfChanged(ref _borderThickness, value);
+ }
+
+ public Thickness PaddingThickness
+ {
+ get => _paddingThickness;
+ set => RaiseAndSetIfChanged(ref _paddingThickness, value);
+ }
+
+ public double Width
+ {
+ get => _width;
+ private set => RaiseAndSetIfChanged(ref _width, value);
+ }
+
+ public double Height
+ {
+ get => _height;
+ private set => RaiseAndSetIfChanged(ref _height, value);
+ }
+
+ public string? WidthConstraint
+ {
+ get => _widthConstraint;
+ private set => RaiseAndSetIfChanged(ref _widthConstraint, value);
+ }
+
+ public string? HeightConstraint
+ {
+ get => _heightConstraint;
+ private set => RaiseAndSetIfChanged(ref _heightConstraint, value);
+ }
+
+ public HorizontalAlignment HorizontalAlignment
+ {
+ get => _horizontalAlignment;
+ private set => RaiseAndSetIfChanged(ref _horizontalAlignment, value);
+ }
+
+ public VerticalAlignment VerticalAlignment
+ {
+ get => _verticalAlignment;
+ private set => RaiseAndSetIfChanged(ref _verticalAlignment, value);
+ }
+
+ public bool HasPadding { get; }
+
+ public bool HasBorder { get; }
+
+ private void UpdateSizeConstraints()
+ {
+ if (_control is IAvaloniaObject ao)
+ {
+ string? CreateConstraintInfo(StyledProperty minProperty, StyledProperty maxProperty)
+ {
+ bool hasMin = ao.IsSet(minProperty);
+ bool hasMax = ao.IsSet(maxProperty);
+
+ if (hasMin || hasMax)
+ {
+ var builder = new StringBuilder();
+
+ if (hasMin)
+ {
+ var minValue = ao.GetValue(minProperty);
+ builder.AppendFormat("Min: {0}", Math.Round(minValue, 2));
+ builder.AppendLine();
+ }
+
+ if (hasMax)
+ {
+ var maxValue = ao.GetValue(maxProperty);
+ builder.AppendFormat("Max: {0}", Math.Round(maxValue, 2));
+ }
+
+ return builder.ToString();
+ }
+
+ return null;
+ }
+
+ WidthConstraint = CreateConstraintInfo(Layoutable.MinWidthProperty, Layoutable.MaxWidthProperty);
+ HeightConstraint = CreateConstraintInfo(Layoutable.MinHeightProperty, Layoutable.MaxHeightProperty);
+ }
+ }
+
+ protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ base.OnPropertyChanged(e);
+
+ if (_updatingFromControl)
+ {
+ return;
+ }
+
+ if (_control is AvaloniaObject ao)
+ {
+ if (e.PropertyName == nameof(MarginThickness))
+ {
+ ao.SetValue(Layoutable.MarginProperty, MarginThickness);
+ }
+ else if (HasPadding && e.PropertyName == nameof(PaddingThickness))
+ {
+ ao.SetValue(Decorator.PaddingProperty, PaddingThickness);
+ }
+ else if (HasBorder && e.PropertyName == nameof(BorderThickness))
+ {
+ ao.SetValue(Border.BorderThicknessProperty, BorderThickness);
+ }
+ else if (e.PropertyName == nameof(HorizontalAlignment))
+ {
+ ao.SetValue(Layoutable.HorizontalAlignmentProperty, HorizontalAlignment);
+ }
+ else if (e.PropertyName == nameof(VerticalAlignment))
+ {
+ ao.SetValue(Layoutable.VerticalAlignmentProperty, VerticalAlignment);
+ }
+ }
+ }
+
+ public void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ try
+ {
+ _updatingFromControl = true;
+
+ if (e.Property == Visual.BoundsProperty)
+ {
+ UpdateSize();
+ }
+ else
+ {
+ if (_control is IAvaloniaObject ao)
+ {
+ if (e.Property == Layoutable.MarginProperty)
+ {
+ MarginThickness = ao.GetValue(Layoutable.MarginProperty);
+ }
+ else if (e.Property == Decorator.PaddingProperty)
+ {
+ PaddingThickness = ao.GetValue(Decorator.PaddingProperty);
+ }
+ else if (e.Property == Border.BorderThicknessProperty)
+ {
+ BorderThickness = ao.GetValue(Border.BorderThicknessProperty);
+ }
+ else if (e.Property == Layoutable.MinWidthProperty ||
+ e.Property == Layoutable.MaxWidthProperty ||
+ e.Property == Layoutable.MinHeightProperty ||
+ e.Property == Layoutable.MaxHeightProperty)
+ {
+ UpdateSizeConstraints();
+ }
+ else if (e.Property == Layoutable.HorizontalAlignmentProperty)
+ {
+ HorizontalAlignment = ao.GetValue(Layoutable.HorizontalAlignmentProperty);
+ }
+ else if (e.Property == Layoutable.VerticalAlignmentProperty)
+ {
+ VerticalAlignment = ao.GetValue(Layoutable.VerticalAlignmentProperty);
+ }
+ }
+ }
+ }
+ finally
+ {
+ _updatingFromControl = false;
+ }
+ }
+
+ private void UpdateSize()
+ {
+ var size = _control.Bounds;
+
+ Width = Math.Round(size.Width, 2);
+ Height = Math.Round(size.Height, 2);
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
index 60f247ead1b..5b7ddc98ee6 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs
@@ -2,25 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections;
-using Avalonia.Controls;
-using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventOwnerTreeNode : EventTreeNodeBase
{
- private static readonly RoutedEvent[] s_defaultEvents =
- {
- Button.ClickEvent, InputElement.KeyDownEvent, InputElement.KeyUpEvent, InputElement.TextInputEvent,
- InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent
- };
-
public EventOwnerTreeNode(Type type, IEnumerable events, EventsPageViewModel vm)
: base(null, type.Name)
{
Children = new AvaloniaList(events.OrderBy(e => e.Name)
- .Select(e => new EventTreeNode(this, e, vm) { IsEnabled = s_defaultEvents.Contains(e) }));
+ .Select(e => new EventTreeNode(this, e, vm)));
IsExpanded = true;
}
@@ -35,7 +27,7 @@ public override bool? IsEnabled
if (_updateChildren && value != null)
{
- foreach (var child in Children)
+ foreach (var child in Children!)
{
try
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
index 9291b3bdf7b..65fd81cc78e 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
@@ -9,21 +9,19 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class EventTreeNode : EventTreeNodeBase
{
- private readonly RoutedEvent _event;
private readonly EventsPageViewModel _parentViewModel;
private bool _isRegistered;
- private FiredEvent _currentEvent;
+ private FiredEvent? _currentEvent;
public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm)
: base(parent, @event.Name)
{
- Contract.Requires(@event != null);
- Contract.Requires(vm != null);
-
- _event = @event;
- _parentViewModel = vm;
+ Event = @event ?? throw new ArgumentNullException(nameof(@event));
+ _parentViewModel = vm ?? throw new ArgumentNullException(nameof(vm));
}
+ public RoutedEvent Event { get; }
+
public override bool? IsEnabled
{
get => base.IsEnabled;
@@ -53,24 +51,26 @@ private void UpdateTracker()
{
if (IsEnabled.GetValueOrDefault() && !_isRegistered)
{
+ var allRoutes = RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble;
+
// FIXME: This leaks event handlers.
- _event.AddClassHandler(typeof(object), HandleEvent, (RoutingStrategies)7, handledEventsToo: true);
+ Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
_isRegistered = true;
}
}
- private void HandleEvent(object sender, RoutedEventArgs e)
+ private void HandleEvent(object? sender, RoutedEventArgs e)
{
if (!_isRegistered || IsEnabled == false)
return;
if (sender is IVisual v && BelongsToDevTool(v))
return;
- var s = sender;
+ var s = sender!;
var handled = e.Handled;
var route = e.Route;
- Action handler = delegate
+ void handler()
{
if (_currentEvent == null || !_currentEvent.IsPartOfSameEventChain(e))
{
@@ -95,14 +95,16 @@ private void HandleEvent(object sender, RoutedEventArgs e)
private static bool BelongsToDevTool(IVisual v)
{
- while (v != null)
+ var current = v;
+
+ while (current != null)
{
- if (v is MainView || v is MainWindow)
+ if (current is MainView || current is MainWindow)
{
return true;
}
- v = v.VisualParent;
+ current = current.VisualParent;
}
return false;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
index 1bd4117f23a..e6d73352972 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs
@@ -8,14 +8,16 @@ internal abstract class EventTreeNodeBase : ViewModelBase
internal bool _updateParent = true;
private bool _isExpanded;
private bool? _isEnabled = false;
+ private bool _isVisible;
- protected EventTreeNodeBase(EventTreeNodeBase parent, string text)
+ protected EventTreeNodeBase(EventTreeNodeBase? parent, string text)
{
Parent = parent;
Text = text;
+ IsVisible = true;
}
- public IAvaloniaReadOnlyList Children
+ public IAvaloniaReadOnlyList? Children
{
get;
protected set;
@@ -33,7 +35,13 @@ public virtual bool? IsEnabled
set => RaiseAndSetIfChanged(ref _isEnabled, value);
}
- public EventTreeNodeBase Parent
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
+ public EventTreeNodeBase? Parent
{
get;
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
index 361e82bc738..fbcedb2e749 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs
@@ -1,28 +1,44 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Globalization;
using System.Linq;
using Avalonia.Controls;
-using Avalonia.Data.Converters;
+using Avalonia.Diagnostics.Models;
+using Avalonia.Input;
using Avalonia.Interactivity;
-using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsPageViewModel : ViewModelBase
{
- private readonly IControl _root;
- private FiredEvent _selectedEvent;
+ private static readonly HashSet s_defaultEvents = new HashSet()
+ {
+ Button.ClickEvent,
+ InputElement.KeyDownEvent,
+ InputElement.KeyUpEvent,
+ InputElement.TextInputEvent,
+ InputElement.PointerReleasedEvent,
+ InputElement.PointerPressedEvent
+ };
+
+ private readonly MainViewModel _mainViewModel;
+ private FiredEvent? _selectedEvent;
+ private EventTreeNodeBase? _selectedNode;
- public EventsPageViewModel(IControl root)
+ public EventsPageViewModel(MainViewModel mainViewModel)
{
- _root = root;
+ _mainViewModel = mainViewModel;
Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
+
+ EventsFilter = new FilterViewModel();
+ EventsFilter.RefreshFilter += (s, e) => UpdateEventFilters();
+
+ EnableDefault();
}
public string Name => "Events";
@@ -31,15 +47,129 @@ public EventsPageViewModel(IControl root)
public ObservableCollection RecordedEvents { get; } = new ObservableCollection();
- public FiredEvent SelectedEvent
+ public FiredEvent? SelectedEvent
{
get => _selectedEvent;
set => RaiseAndSetIfChanged(ref _selectedEvent, value);
}
- private void Clear()
+ public EventTreeNodeBase? SelectedNode
+ {
+ get => _selectedNode;
+ set => RaiseAndSetIfChanged(ref _selectedNode, value);
+ }
+
+ public FilterViewModel EventsFilter { get; }
+
+ public void Clear()
{
RecordedEvents.Clear();
}
+
+ public void DisableAll()
+ {
+ EvaluateNodeEnabled(_ => false);
+ }
+
+ public void EnableDefault()
+ {
+ EvaluateNodeEnabled(node => s_defaultEvents.Contains(node.Event));
+ }
+
+ public void RequestTreeNavigateTo(EventChainLink navTarget)
+ {
+ if (navTarget.Handler is IControl control)
+ {
+ _mainViewModel.RequestTreeNavigateTo(control, true);
+ }
+ }
+
+ public void SelectEventByType(RoutedEvent evt)
+ {
+ foreach (var node in Nodes)
+ {
+ var result = FindNode(node, evt);
+
+ if (result != null && result.IsVisible)
+ {
+ SelectedNode = result;
+
+ break;
+ }
+ }
+
+ static EventTreeNodeBase? FindNode(EventTreeNodeBase node, RoutedEvent eventType)
+ {
+ if (node is EventTreeNode eventNode && eventNode.Event == eventType)
+ {
+ return node;
+ }
+
+ if (node.Children != null)
+ {
+ foreach (var child in node.Children)
+ {
+ var result = FindNode(child, eventType);
+
+ if (result != null)
+ {
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+
+ private void EvaluateNodeEnabled(Func eval)
+ {
+ void ProcessNode(EventTreeNodeBase node)
+ {
+ if (node is EventTreeNode eventNode)
+ {
+ node.IsEnabled = eval(eventNode);
+ }
+
+ if (node.Children != null)
+ {
+ foreach (var childNode in node.Children)
+ {
+ ProcessNode(childNode);
+ }
+ }
+ }
+
+ foreach (var node in Nodes)
+ {
+ ProcessNode(node);
+ }
+ }
+
+ private void UpdateEventFilters()
+ {
+ foreach (var node in Nodes)
+ {
+ FilterNode(node, false);
+ }
+
+ bool FilterNode(EventTreeNodeBase node, bool isParentVisible)
+ {
+ bool matchesFilter = EventsFilter.Filter(node.Text);
+ bool hasVisibleChild = false;
+
+ if (node.Children != null)
+ {
+ foreach (var childNode in node.Children)
+ {
+ hasVisibleChild |= FilterNode(childNode, matchesFilter);
+ }
+ }
+
+ node.IsVisible = hasVisibleChild || matchesFilter || isParentVisible;
+
+ return node.IsVisible;
+ }
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs
new file mode 100644
index 00000000000..5b27236f2e6
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FilterViewModel.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Text.RegularExpressions;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class FilterViewModel : ViewModelBase, INotifyDataErrorInfo
+ {
+ private readonly Dictionary _errors = new Dictionary();
+ private string _filterString = string.Empty;
+ private bool _useRegexFilter, _useCaseSensitiveFilter, _useWholeWordFilter;
+ private Regex? _filterRegex;
+
+ public event EventHandler? RefreshFilter;
+
+ public bool HasErrors => _errors.Count > 0;
+
+ public event EventHandler? ErrorsChanged;
+
+ public bool Filter(string input)
+ {
+ return _filterRegex?.IsMatch(input) ?? true;
+ }
+
+ private void UpdateFilterRegex()
+ {
+ void ClearError()
+ {
+ if (_errors.Remove(nameof(FilterString)))
+ {
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
+ }
+ }
+
+ try
+ {
+ var options = RegexOptions.Compiled;
+ var pattern = UseRegexFilter
+ ? FilterString.Trim() : Regex.Escape(FilterString.Trim());
+ if (!UseCaseSensitiveFilter)
+ {
+ options |= RegexOptions.IgnoreCase;
+ }
+ if (UseWholeWordFilter)
+ {
+ pattern = $"\\b(?:{pattern})\\b";
+ }
+
+ _filterRegex = new Regex(pattern, options);
+ ClearError();
+ }
+ catch (Exception exception)
+ {
+ _errors[nameof(FilterString)] = exception.Message;
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(FilterString)));
+ }
+ }
+
+ public string FilterString
+ {
+ get => _filterString;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _filterString, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public bool UseRegexFilter
+ {
+ get => _useRegexFilter;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public bool UseCaseSensitiveFilter
+ {
+ get => _useCaseSensitiveFilter;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _useCaseSensitiveFilter, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public bool UseWholeWordFilter
+ {
+ get => _useWholeWordFilter;
+ set
+ {
+ if (RaiseAndSetIfChanged(ref _useWholeWordFilter, value))
+ {
+ UpdateFilterRegex();
+ RefreshFilter?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ }
+
+ public IEnumerable GetErrors(string? propertyName)
+ {
+ if (propertyName != null
+ && _errors.TryGetValue(propertyName, out var error))
+ {
+ yield return error;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
index ae53cf61548..32df2f8745f 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
@@ -8,15 +8,12 @@ namespace Avalonia.Diagnostics.ViewModels
internal class FiredEvent : ViewModelBase
{
private readonly RoutedEventArgs _eventArgs;
- private EventChainLink _handledBy;
+ private EventChainLink? _handledBy;
public FiredEvent(RoutedEventArgs eventArgs, EventChainLink originator)
{
- Contract.Requires(eventArgs != null);
- Contract.Requires(originator != null);
-
- _eventArgs = eventArgs;
- Originator = originator;
+ _eventArgs = eventArgs ?? throw new ArgumentNullException(nameof(eventArgs));
+ Originator = originator ?? throw new ArgumentNullException(nameof(originator));
AddToChain(originator);
}
@@ -25,7 +22,7 @@ public bool IsPartOfSameEventChain(RoutedEventArgs e)
return e == _eventArgs;
}
- public RoutedEvent Event => _eventArgs.RoutedEvent;
+ public RoutedEvent Event => _eventArgs.RoutedEvent!;
public bool IsHandled => HandledBy?.Handled == true;
@@ -38,7 +35,7 @@ public string DisplayText
if (IsHandled)
{
return $"{Event.Name} on {Originator.HandlerName};" + Environment.NewLine +
- $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy.HandlerName}";
+ $"strategies: {Event.RoutingStrategies}; handled by: {HandledBy!.HandlerName}";
}
return $"{Event.Name} on {Originator.HandlerName}; strategies: {Event.RoutingStrategies}";
@@ -47,7 +44,7 @@ public string DisplayText
public EventChainLink Originator { get; }
- public EventChainLink HandledBy
+ public EventChainLink? HandledBy
{
get => _handledBy;
set
@@ -62,13 +59,18 @@ public EventChainLink HandledBy
}
}
- public void AddToChain(object handler, bool handled, RoutingStrategies route)
- {
- AddToChain(new EventChainLink(handler, handled, route));
- }
-
public void AddToChain(EventChainLink link)
{
+ if (EventChain.Count > 0)
+ {
+ var prevLink = EventChain[EventChain.Count-1];
+
+ if (prevLink.Route != link.Route)
+ {
+ link.BeginsNewRoute = true;
+ }
+ }
+
EventChain.Add(link);
if (HandledBy == null && link.Handled)
HandledBy = link;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
index 38788ef8ee6..04215fa8aea 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs
@@ -7,22 +7,24 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class LogicalTreeNode : TreeNode
{
- public LogicalTreeNode(ILogical logical, TreeNode parent)
+ public LogicalTreeNode(ILogical logical, TreeNode? parent)
: base((Control)logical, parent)
{
Children = new LogicalTreeNodeCollection(this, logical);
}
+ public override TreeNodeCollection Children { get; }
+
public static LogicalTreeNode[] Create(object control)
{
var logical = control as ILogical;
- return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
+ return logical != null ? new[] { new LogicalTreeNode(logical, null) } : Array.Empty();
}
internal class LogicalTreeNodeCollection : TreeNodeCollection
{
private readonly ILogical _control;
- private IDisposable _subscription;
+ private IDisposable? _subscription;
public LogicalTreeNodeCollection(TreeNode owner, ILogical control)
: base(owner)
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
index bf7d0e232a5..d0a4ad38c59 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
@@ -16,18 +16,22 @@ internal class MainViewModel : ViewModelBase, IDisposable
private readonly IDisposable _pointerOverSubscription;
private ViewModelBase _content;
private int _selectedTab;
- private string _focusedControl;
- private string _pointerOverElement;
+ private string? _focusedControl;
+ private string? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
+ private bool _freezePopups;
+#nullable disable
+ // Remove "nullable disable" after MemberNotNull will work on our CI.
public MainViewModel(TopLevel root)
+#nullable restore
{
_root = root;
_logicalTree = new TreePageViewModel(this, LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(this, VisualTreeNode.Create(root));
- _events = new EventsPageViewModel(root);
+ _events = new EventsPageViewModel(this);
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += KeyboardPropertyChanged;
@@ -37,6 +41,12 @@ public MainViewModel(TopLevel root)
Console = new ConsoleViewModel(UpdateConsoleContext);
}
+ public bool FreezePopups
+ {
+ get => _freezePopups;
+ set => RaiseAndSetIfChanged(ref _freezePopups, value);
+ }
+
public bool ShouldVisualizeMarginPadding
{
get => _shouldVisualizeMarginPadding;
@@ -83,6 +93,7 @@ public void ToggleFpsOverlay()
public ViewModelBase Content
{
get { return _content; }
+ // [MemberNotNull(nameof(_content))]
private set
{
if (_content is TreePageViewModel oldTree &&
@@ -113,39 +124,40 @@ value is TreePageViewModel newTree &&
public int SelectedTab
{
get { return _selectedTab; }
+ // [MemberNotNull(nameof(_content))]
set
{
_selectedTab = value;
switch (value)
{
- case 0:
- Content = _logicalTree;
- break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _events;
break;
+ default:
+ Content = _logicalTree;
+ break;
}
RaisePropertyChanged();
}
}
- public string FocusedControl
+ public string? FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
}
- public string PointerOverElement
+ public string? PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
}
-
+
private void UpdateConsoleContext(ConsoleContext context)
{
context.root = _root;
@@ -163,6 +175,14 @@ public void SelectControl(IControl control)
tree?.SelectControl(control);
}
+ public void EnableSnapshotStyles(bool enable)
+ {
+ if (Content is TreePageViewModel treeVm && treeVm.Details != null)
+ {
+ treeVm.Details.SnapshotStyles = enable;
+ }
+ }
+
public void Dispose()
{
KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged;
@@ -178,12 +198,33 @@ private void UpdateFocusedControl()
FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name;
}
- private void KeyboardPropertyChanged(object sender, PropertyChangedEventArgs e)
+ private void KeyboardPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
{
UpdateFocusedControl();
}
}
+
+ public void RequestTreeNavigateTo(IControl control, bool isVisualTree)
+ {
+ var tree = isVisualTree ? _visualTree : _logicalTree;
+
+ var node = tree.FindNode(control);
+
+ if (node != null)
+ {
+ SelectedTab = isVisualTree ? 1 : 0;
+
+ tree.SelectControl(control);
+ }
+ }
+
+ public int? StartupScreenIndex { get; private set; } = default;
+
+ public void SetOptions(DevToolsOptions options)
+ {
+ StartupScreenIndex = options.StartupScreenIndex;
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
index e23d6f14714..fdbd8c1aa33 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
@@ -16,9 +16,11 @@ internal abstract class PropertyViewModel : ViewModelBase
public abstract string Group { get; }
public abstract string Type { get; }
public abstract string Value { get; set; }
- public abstract void Update();
+ public abstract string Priority { get; }
+ public abstract bool? IsAttached { get; }
+ public abstract void Update();
- protected static string ConvertToString(object value)
+ protected static string ConvertToString(object? value)
{
if (value is null)
{
@@ -31,13 +33,13 @@ protected static string ConvertToString(object value)
if (!converter.CanConvertTo(typeof(string)) ||
converter.GetType() == typeof(CollectionConverter))
{
- return value.ToString();
+ return value.ToString() ?? "(null)";
}
return converter.ConvertToString(value);
}
- private static object InvokeParse(string s, Type targetType)
+ private static object? InvokeParse(string s, Type targetType)
{
var method = targetType.GetMethod("Parse", PublicStatic, null, StringIFormatProviderParameters, null);
@@ -56,7 +58,7 @@ private static object InvokeParse(string s, Type targetType)
throw new InvalidCastException("Unable to convert value.");
}
- protected static object ConvertFromString(string s, Type targetType)
+ protected static object? ConvertFromString(string s, Type targetType)
{
var converter = TypeDescriptor.GetConverter(targetType);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
new file mode 100644
index 00000000000..69126c2e2fc
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
@@ -0,0 +1,51 @@
+using Avalonia.Controls;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class PseudoClassViewModel : ViewModelBase
+ {
+ private readonly IPseudoClasses _pseudoClasses;
+ private readonly StyledElement _source;
+ private bool _isActive;
+ private bool _isUpdating;
+
+ public PseudoClassViewModel(string name, StyledElement source)
+ {
+ Name = name;
+ _source = source;
+ _pseudoClasses = _source.Classes;
+
+ Update();
+ }
+
+ public string Name { get; }
+
+ public bool IsActive
+ {
+ get => _isActive;
+ set
+ {
+ RaiseAndSetIfChanged(ref _isActive, value);
+
+ if (!_isUpdating)
+ {
+ _pseudoClasses.Set(Name, value);
+ }
+ }
+ }
+
+ public void Update()
+ {
+ try
+ {
+ _isUpdating = true;
+
+ IsActive = _source.Classes.Contains(Name);
+ }
+ finally
+ {
+ _isUpdating = false;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
new file mode 100644
index 00000000000..e93dc7361bf
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
@@ -0,0 +1,29 @@
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class ResourceSetterViewModel : SetterViewModel
+ {
+ public object Key { get; }
+
+ public IBrush Tint { get; }
+
+ public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object? resourceValue, bool isDynamic) : base(property, resourceValue)
+ {
+ Key = resourceKey;
+ Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
+ }
+
+ public void CopyResourceKey()
+ {
+ var textToCopy = Key?.ToString();
+
+ if (textToCopy is null)
+ {
+ return;
+ }
+
+ CopyToClipboard(textToCopy);
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
new file mode 100644
index 00000000000..38cbefcb933
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
@@ -0,0 +1,61 @@
+using Avalonia.Input.Platform;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class SetterViewModel : ViewModelBase
+ {
+ private bool _isActive;
+ private bool _isVisible;
+
+ public AvaloniaProperty Property { get; }
+
+ public string Name { get; }
+
+ public object? Value { get; }
+
+ public bool IsActive
+ {
+ get => _isActive;
+ set => RaiseAndSetIfChanged(ref _isActive, value);
+ }
+
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
+ public SetterViewModel(AvaloniaProperty property, object? value)
+ {
+ Property = property;
+ Name = property.Name;
+ Value = value;
+ IsActive = true;
+ IsVisible = true;
+ }
+
+ public void CopyValue()
+ {
+ var textToCopy = Value?.ToString();
+
+ if (textToCopy is null)
+ {
+ return;
+ }
+
+ CopyToClipboard(textToCopy);
+ }
+
+ public void CopyPropertyName()
+ {
+ CopyToClipboard(Property.Name);
+ }
+
+ protected static void CopyToClipboard(string value)
+ {
+ var clipboard = AvaloniaLocator.Current.GetService();
+
+ clipboard?.SetTextAsync(value);
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
new file mode 100644
index 00000000000..06e24098008
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+ internal class StyleViewModel : ViewModelBase
+ {
+ private readonly IStyleInstance _styleInstance;
+ private bool _isActive;
+ private bool _isVisible;
+
+ public StyleViewModel(IStyleInstance styleInstance, string name, List setters)
+ {
+ _styleInstance = styleInstance;
+ IsVisible = true;
+ Name = name;
+ Setters = setters;
+
+ Update();
+ }
+
+ public bool IsActive
+ {
+ get => _isActive;
+ set => RaiseAndSetIfChanged(ref _isActive, value);
+ }
+
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => RaiseAndSetIfChanged(ref _isVisible, value);
+ }
+
+ public string Name { get; }
+
+ public List Setters { get; }
+
+ public void Update()
+ {
+ IsActive = _styleInstance.IsActive;
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
index d9a0d17518f..94707ac189f 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
@@ -1,28 +1,33 @@
using System;
-using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
+using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
- internal class TreeNode : ViewModelBase, IDisposable
+ internal abstract class TreeNode : ViewModelBase, IDisposable
{
- private IDisposable _classesSubscription;
+ private readonly IDisposable? _classesSubscription;
private string _classes;
private bool _isExpanded;
- public TreeNode(IVisual visual, TreeNode parent)
+ protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null)
{
+ _classes = string.Empty;
Parent = parent;
- Type = visual.GetType().Name;
+ Type = customName ?? visual.GetType().Name;
Visual = visual;
+ FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control)
{
+ ElementName = control.Name;
+
var removed = Observable.FromEventPattern(
x => control.DetachedFromLogicalTree += x,
x => control.DetachedFromLogicalTree -= x);
@@ -49,10 +54,15 @@ public TreeNode(IVisual visual, TreeNode parent)
}
}
- public TreeNodeCollection Children
+ private bool IsRoot => Visual is TopLevel ||
+ Visual is ContextMenu ||
+ Visual is IPopupHost;
+
+ public FontWeight FontWeight { get; }
+
+ public abstract TreeNodeCollection Children
{
get;
- protected set;
}
public string Classes
@@ -61,6 +71,11 @@ public string Classes
private set { RaiseAndSetIfChanged(ref _classes, value); }
}
+ public string? ElementName
+ {
+ get;
+ }
+
public IVisual Visual
{
get;
@@ -72,7 +87,7 @@ public bool IsExpanded
set { RaiseAndSetIfChanged(ref _isExpanded, value); }
}
- public TreeNode Parent
+ public TreeNode? Parent
{
get;
}
@@ -85,23 +100,8 @@ public string Type
public void Dispose()
{
- _classesSubscription.Dispose();
+ _classesSubscription?.Dispose();
Children.Dispose();
}
-
- private static int IndexOf(IReadOnlyList collection, TreeNode item)
- {
- var count = collection.Count;
-
- for (var i = 0; i < count; ++i)
- {
- if (collection[i] == item)
- {
- return i;
- }
- }
-
- throw new AvaloniaInternalException("TreeNode was not present in parent Children collection.");
- }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
index 8b4f03bd23f..c007411f495 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNodeCollection.cs
@@ -3,46 +3,33 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
+
using Avalonia.Collections;
namespace Avalonia.Diagnostics.ViewModels
{
internal abstract class TreeNodeCollection : IAvaloniaReadOnlyList, IDisposable
{
- private AvaloniaList _inner;
+ private AvaloniaList? _inner;
public TreeNodeCollection(TreeNode owner) => Owner = owner;
- public TreeNode this[int index]
- {
- get
- {
- EnsureInitialized();
- return _inner[index];
- }
- }
+ public TreeNode this[int index] => EnsureInitialized()[index];
- public int Count
- {
- get
- {
- EnsureInitialized();
- return _inner.Count;
- }
- }
+ public int Count => EnsureInitialized().Count;
protected TreeNode Owner { get; }
- public event NotifyCollectionChangedEventHandler CollectionChanged
+ public event NotifyCollectionChangedEventHandler? CollectionChanged
{
- add => _inner.CollectionChanged += value;
- remove => _inner.CollectionChanged -= value;
+ add => EnsureInitialized().CollectionChanged += value;
+ remove => EnsureInitialized().CollectionChanged -= value;
}
- public event PropertyChangedEventHandler PropertyChanged
+ public event PropertyChangedEventHandler? PropertyChanged
{
- add => _inner.PropertyChanged += value;
- remove => _inner.PropertyChanged -= value;
+ add => EnsureInitialized().PropertyChanged += value;
+ remove => EnsureInitialized().PropertyChanged -= value;
}
public virtual void Dispose()
@@ -58,21 +45,21 @@ public virtual void Dispose()
public IEnumerator GetEnumerator()
{
- EnsureInitialized();
- return _inner.GetEnumerator();
+ return EnsureInitialized().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
protected abstract void Initialize(AvaloniaList nodes);
- private void EnsureInitialized()
+ private AvaloniaList EnsureInitialized()
{
if (_inner is null)
{
_inner = new AvaloniaList();
Initialize(_inner);
}
+ return _inner;
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
index bd65a3b06b3..4b18cf414ae 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
@@ -1,32 +1,35 @@
using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Text.RegularExpressions;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
- internal class TreePageViewModel : ViewModelBase, IDisposable, INotifyDataErrorInfo
+ internal class TreePageViewModel : ViewModelBase, IDisposable
{
- private readonly Dictionary _errors = new Dictionary();
- private TreeNode _selectedNode;
- private ControlDetailsViewModel _details;
- private string _propertyFilter = string.Empty;
- private bool _useRegexFilter;
+ private TreeNode? _selectedNode;
+ private ControlDetailsViewModel? _details;
public TreePageViewModel(MainViewModel mainView, TreeNode[] nodes)
{
MainView = mainView;
Nodes = nodes;
+
+ PropertiesFilter = new FilterViewModel();
+ PropertiesFilter.RefreshFilter += (s, e) => Details?.PropertiesView.Refresh();
+
+ SettersFilter = new FilterViewModel();
+ SettersFilter.RefreshFilter += (s, e) => Details?.UpdateStyleFilters();
}
public MainViewModel MainView { get; }
+ public FilterViewModel PropertiesFilter { get; }
+
+ public FilterViewModel SettersFilter { get; }
+
public TreeNode[] Nodes { get; protected set; }
- public TreeNode SelectedNode
+ public TreeNode? SelectedNode
{
get => _selectedNode;
private set
@@ -36,11 +39,12 @@ private set
Details = value != null ?
new ControlDetailsViewModel(this, value.Visual) :
null;
+ Details?.UpdateStyleFilters();
}
}
}
- public ControlDetailsViewModel Details
+ public ControlDetailsViewModel? Details
{
get => _details;
private set
@@ -54,63 +58,6 @@ private set
}
}
- public Regex FilterRegex { get; set; }
-
- private void UpdateFilterRegex()
- {
- void ClearError()
- {
- if (_errors.Remove(nameof(PropertyFilter)))
- {
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
- }
- }
-
- if (UseRegexFilter)
- {
- try
- {
- FilterRegex = new Regex(PropertyFilter, RegexOptions.Compiled);
- ClearError();
- }
- catch (Exception exception)
- {
- _errors[nameof(PropertyFilter)] = exception.Message;
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(PropertyFilter)));
- }
- }
- else
- {
- ClearError();
- }
- }
-
- public string PropertyFilter
- {
- get => _propertyFilter;
- set
- {
- if (RaiseAndSetIfChanged(ref _propertyFilter, value))
- {
- UpdateFilterRegex();
- Details.PropertiesView.Refresh();
- }
- }
- }
-
- public bool UseRegexFilter
- {
- get => _useRegexFilter;
- set
- {
- if (RaiseAndSetIfChanged(ref _useRegexFilter, value))
- {
- UpdateFilterRegex();
- Details.PropertiesView.Refresh();
- }
- }
- }
-
public void Dispose()
{
foreach (var node in Nodes)
@@ -121,7 +68,7 @@ public void Dispose()
_details?.Dispose();
}
- public TreeNode FindNode(IControl control)
+ public TreeNode? FindNode(IControl control)
{
foreach (var node in Nodes)
{
@@ -157,7 +104,7 @@ public void SelectControl(IControl control)
}
}
- private void ExpandNode(TreeNode node)
+ private void ExpandNode(TreeNode? node)
{
if (node != null)
{
@@ -166,7 +113,7 @@ private void ExpandNode(TreeNode node)
}
}
- private TreeNode FindNode(TreeNode node, IControl control)
+ private TreeNode? FindNode(TreeNode node, IControl control)
{
if (node.Visual == control)
{
@@ -187,17 +134,5 @@ private TreeNode FindNode(TreeNode node, IControl control)
return null;
}
-
- public IEnumerable GetErrors(string propertyName)
- {
- if (_errors.TryGetValue(propertyName, out var error))
- {
- yield return error;
- }
- }
-
- public bool HasErrors => _errors.Count > 0;
-
- public event EventHandler ErrorsChanged;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
index 66e9c346576..a2ee37c625f 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
@@ -1,16 +1,16 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ViewModelBase : INotifyPropertyChanged
{
- private PropertyChangedEventHandler _propertyChanged;
+ private PropertyChangedEventHandler? _propertyChanged;
private List events = new List();
- public event PropertyChangedEventHandler PropertyChanged
+ public event PropertyChangedEventHandler? PropertyChanged
{
add { _propertyChanged += value; events.Add("added"); }
remove { _propertyChanged -= value; events.Add("removed"); }
@@ -20,7 +20,7 @@ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
}
- protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null)
+ protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!)
{
if (!EqualityComparer.Default.Equals(field, value))
{
@@ -32,7 +32,7 @@ protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName]
return false;
}
- protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+ protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!)
{
var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
index bc40edf477d..6a430897bab 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
@@ -1,5 +1,10 @@
using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Diagnostics;
+using Avalonia.Controls.Primitives;
using Avalonia.Styling;
using Avalonia.VisualTree;
@@ -7,29 +12,30 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class VisualTreeNode : TreeNode
{
- public VisualTreeNode(IVisual visual, TreeNode parent)
- : base(visual, parent)
+ public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null)
+ : base(visual, parent, customName)
{
Children = new VisualTreeNodeCollection(this, visual);
- if ((Visual is IStyleable styleable))
- {
+ if (Visual is IStyleable styleable)
IsInTemplate = styleable.TemplatedParent != null;
- }
}
- public bool IsInTemplate { get; private set; }
+ public bool IsInTemplate { get; }
+
+ public override TreeNodeCollection Children { get; }
public static VisualTreeNode[] Create(object control)
{
- var visual = control as IVisual;
- return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
+ return control is IVisual visual ?
+ new[] { new VisualTreeNode(visual, null) } :
+ Array.Empty();
}
internal class VisualTreeNodeCollection : TreeNodeCollection
{
private readonly IVisual _control;
- private IDisposable _subscription;
+ private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2);
public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner)
@@ -39,15 +45,106 @@ public VisualTreeNodeCollection(TreeNode owner, IVisual control)
public override void Dispose()
{
- _subscription?.Dispose();
+ _subscriptions.Dispose();
+ }
+
+ private static IObservable? GetHostedPopupRootObservable(IVisual visual)
+ {
+ static IObservable GetPopupHostObservable(
+ IPopupHostProvider popupHostProvider,
+ string? providerName = null)
+ {
+ return Observable.FromEvent(
+ x => popupHostProvider.PopupHostChanged += x,
+ x => popupHostProvider.PopupHostChanged -= x)
+ .StartWith(popupHostProvider.PopupHost)
+ .Select(popupHost =>
+ {
+ if (popupHost is IControl control)
+ return new PopupRoot(
+ control,
+ providerName != null ? $"{providerName} ({control.GetType().Name})" : null);
+
+ return (PopupRoot?)null;
+ });
+ }
+
+ return visual switch
+ {
+ Popup p => GetPopupHostObservable(p),
+ Control c => Observable.CombineLatest(
+ c.GetObservable(Control.ContextFlyoutProperty),
+ c.GetObservable(Control.ContextMenuProperty),
+ c.GetObservable(FlyoutBase.AttachedFlyoutProperty),
+ c.GetObservable(ToolTipDiagnostics.ToolTipProperty),
+ (ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) =>
+ {
+ if (ContextMenu != null)
+ //Note: ContextMenus are special since all the items are added as visual children.
+ //So we don't need to go via Popup
+ return Observable.Return(new PopupRoot(ContextMenu));
+
+ if (ContextFlyout != null)
+ return GetPopupHostObservable(ContextFlyout, "ContextFlyout");
+
+ if (AttachedFlyout != null)
+ return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout");
+
+ if (ToolTip != null)
+ return GetPopupHostObservable(ToolTip, "ToolTip");
+
+ return Observable.Return(null);
+ })
+ .Switch(),
+ _ => null
+ };
}
protected override void Initialize(AvaloniaList nodes)
{
- _subscription = _control.VisualChildren.ForEachItem(
- (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
- (i, item) => nodes.RemoveAt(i),
- () => nodes.Clear());
+ _subscriptions.Clear();
+
+ if (GetHostedPopupRootObservable(_control) is { } popupRootObservable)
+ {
+ VisualTreeNode? childNode = null;
+
+ _subscriptions.Add(
+ popupRootObservable
+ .Subscribe(popupRoot =>
+ {
+ if (popupRoot != null)
+ {
+ childNode = new VisualTreeNode(
+ popupRoot.Value.Root,
+ Owner,
+ popupRoot.Value.CustomName);
+
+ nodes.Add(childNode);
+ }
+ else if (childNode != null)
+ {
+ nodes.Remove(childNode);
+ }
+ }));
+ }
+
+ _subscriptions.Add(
+ _control.VisualChildren.ForEachItem(
+ (i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
+ (i, item) => nodes.RemoveAt(i),
+ () => nodes.Clear()));
+ }
+
+ private struct PopupRoot
+ {
+ public PopupRoot(IControl root, string? customName = null)
+ {
+ Root = root;
+ CustomName = customName;
+ }
+
+ public IControl Root { get; }
+ public string? CustomName { get; }
}
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
index ae70b59fdeb..ab523fb75a5 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs
@@ -30,22 +30,26 @@ private void InitializeComponent()
AvaloniaXamlLoader.Load(this);
}
- private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e)
+ private void HistoryChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
- if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control)
+ if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems?[0] is IControl control)
{
DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero);
}
}
- private void InputKeyDown(object sender, KeyEventArgs e)
+ private void InputKeyDown(object? sender, KeyEventArgs e)
{
- var vm = (ConsoleViewModel)DataContext;
+ var vm = (ConsoleViewModel?)DataContext;
+ if (vm is null)
+ {
+ return;
+ }
switch (e.Key)
{
case Key.Enter:
- vm.Execute();
+ _ = vm.Execute();
e.Handled = true;
break;
case Key.Up:
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
index b2d3a8bddb3..4b374389933 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
@@ -1,32 +1,188 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
+ xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
+ xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
+ x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
+ x:Name="Main">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (
+
+
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
index b7f0860e702..a9c2688a188 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml
@@ -2,58 +2,136 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
- x:Class="Avalonia.Diagnostics.Views.EventsPageView">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:controls="clr-namespace:Avalonia.Diagnostics.Controls"
+ x:Class="Avalonia.Diagnostics.Views.EventsPageView"
+ Margin="2">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
index c1c78d38f6c..ba7ab41e352 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml.cs
@@ -1,7 +1,14 @@
-using System.Linq;
+using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
using Avalonia.Controls;
+using Avalonia.Diagnostics.Models;
using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views
{
@@ -12,13 +19,53 @@ internal class EventsPageView : UserControl
public EventsPageView()
{
InitializeComponent();
- _events = this.FindControl("events");
+ _events = this.FindControl("EventsList");
}
- private void RecordedEvents_CollectionChanged(object sender,
- System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ public void NavigateTo(object sender, TappedEventArgs e)
{
- _events.ScrollIntoView(_events.Items.OfType().LastOrDefault());
+ if (DataContext is EventsPageViewModel vm && sender is Control control)
+ {
+ switch (control.Tag)
+ {
+ case EventChainLink chainLink:
+ {
+ vm.RequestTreeNavigateTo(chainLink);
+ break;
+ }
+ case RoutedEvent evt:
+ {
+ vm.SelectEventByType(evt);
+
+ break;
+ }
+ }
+ }
+ }
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ if (DataContext is EventsPageViewModel vm)
+ {
+ vm.RecordedEvents.CollectionChanged += OnRecordedEventsChanged;
+ }
+ }
+
+ private void OnRecordedEventsChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (sender is ObservableCollection events)
+ {
+ var evt = events.LastOrDefault();
+
+ if (evt is null)
+ {
+ return;
+ }
+
+ Dispatcher.UIThread.Post(() => _events.ScrollIntoView(evt));
+ }
}
private void InitializeComponent()
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
new file mode 100644
index 00000000000..af6c84a76a2
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs
new file mode 100644
index 00000000000..d4a09f72960
--- /dev/null
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs
@@ -0,0 +1,128 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Diagnostics.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Diagnostics.Views
+{
+ internal class LayoutExplorerView : UserControl
+ {
+ private readonly ThicknessEditor _borderArea;
+ private readonly ThicknessEditor _paddingArea;
+ private readonly Rectangle _horizontalSizeBegin;
+ private readonly Rectangle _horizontalSizeEnd;
+ private readonly Rectangle _verticalSizeBegin;
+ private readonly Rectangle _verticalSizeEnd;
+ private readonly Grid _layoutRoot;
+ private readonly Border _horizontalSize;
+ private readonly Border _verticalSize;
+ private readonly Border _contentArea;
+
+ public LayoutExplorerView()
+ {
+ InitializeComponent();
+
+ _borderArea = this.FindControl("BorderArea");
+ _paddingArea = this.FindControl("PaddingArea");
+
+ _horizontalSizeBegin = this.FindControl("HorizontalSizeBegin");
+ _horizontalSizeEnd = this.FindControl("HorizontalSizeEnd");
+ _verticalSizeBegin = this.FindControl("VerticalSizeBegin");
+ _verticalSizeEnd = this.FindControl("VerticalSizeEnd");
+
+ _horizontalSize = this.FindControl("HorizontalSize");
+ _verticalSize = this.FindControl("VerticalSize");
+
+ _contentArea = this.FindControl("ContentArea");
+
+ _layoutRoot = this.FindControl("LayoutRoot");
+
+ void SubscribeToBounds(Visual visual)
+ {
+ visual.GetPropertyChangedObservable(TransformedBoundsProperty)
+ .Subscribe(UpdateSizeGuidelines);
+ }
+
+ SubscribeToBounds(_borderArea);
+ SubscribeToBounds(_paddingArea);
+ SubscribeToBounds(_contentArea);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e)
+ {
+ void UpdateGuidelines(Visual area)
+ {
+ if (area.TransformedBounds is TransformedBounds bounds)
+ {
+ // Horizontal guideline
+ {
+ var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft,
+ _horizontalSize);
+
+ var start = TranslateToRoot(bounds.Bounds.BottomLeft, area);
+
+ SetPosition(_horizontalSizeBegin, start);
+
+ var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
+
+ SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1));
+
+ var height = sizeArea.Y - start.Y + 2;
+
+ _horizontalSizeBegin.Height = height;
+ _horizontalSizeEnd.Height = height;
+ }
+
+ // Vertical guideline
+ {
+ var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize);
+
+ var start = TranslateToRoot(bounds.Bounds.TopRight, area);
+
+ SetPosition(_verticalSizeBegin, start);
+
+ var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
+
+ SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1));
+
+ var width = sizeArea.X - start.X + 2;
+
+ _verticalSizeBegin.Width = width;
+ _verticalSizeEnd.Width = width;
+ }
+ }
+ }
+
+ Point TranslateToRoot(Point point, IVisual from)
+ {
+ return from.TranslatePoint(point, _layoutRoot) ?? default;
+ }
+
+ static void SetPosition(Rectangle rect, Point start)
+ {
+ Canvas.SetLeft(rect, start.X);
+ Canvas.SetTop(rect, start.Y);
+ }
+
+ if (_borderArea.IsPresent)
+ {
+ UpdateGuidelines(_borderArea);
+ }
+ else if (_paddingArea.IsPresent)
+ {
+ UpdateGuidelines(_paddingArea);
+ }
+ else
+ {
+ UpdateGuidelines(_contentArea);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
index 8c4db33f913..6f2ac96a66c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
@@ -5,14 +5,14 @@
-
+
-
-
-
+
+
+
+ Content="{Binding Content}" />
-
+ IsVisible="False" />
+
+ IsVisible="{Binding IsVisible}" />
+
-
- Hold Ctrl+Shift over a control to inspect.
-
- Focused:
-
-
- Pointer Over:
-
-
+
+
+ Hold Ctrl+Shift over a control to inspect.
+
+ Focused:
+
+
+ Pointer Over:
+
+
+
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
index 783709e54b6..b688ad7676a 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs
@@ -27,7 +27,11 @@ public MainView()
public void ToggleConsole()
{
- var vm = (MainViewModel)DataContext;
+ var vm = (MainViewModel?)DataContext;
+ if (vm is null)
+ {
+ return;
+ }
if (_consoleHeight == -1)
{
@@ -54,7 +58,7 @@ private void InitializeComponent()
AvaloniaXamlLoader.Load(this);
}
- private void PreviewKeyDown(object sender, KeyEventArgs e)
+ private void PreviewKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
index 3623e955979..70d70f0b79c 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml
@@ -12,6 +12,11 @@
+
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
index 10861538ae5..ea06c33e4d9 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
@@ -1,8 +1,11 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls;
+using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
+using Avalonia.Data;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
@@ -15,7 +18,8 @@ namespace Avalonia.Diagnostics.Views
internal class MainWindow : Window, IStyleHost
{
private readonly IDisposable _keySubscription;
- private TopLevel _root;
+ private readonly Dictionary _frozenPopupStates;
+ private TopLevel? _root;
public MainWindow()
{
@@ -23,10 +27,30 @@ public MainWindow()
_keySubscription = InputManager.Instance.Process
.OfType()
+ .Where(x => x.Type == RawKeyEventType.KeyDown)
.Subscribe(RawKeyDown);
+
+ _frozenPopupStates = new Dictionary();
+
+ EventHandler? lh = default;
+ lh = (s, e) =>
+ {
+ this.Opened -= lh;
+ if ((DataContext as MainViewModel)?.StartupScreenIndex is { } index)
+ {
+ var screens = this.Screens;
+ if (index > -1 && index < screens.ScreenCount)
+ {
+ var screen = screens.All[index];
+ this.Position = screen.Bounds.TopLeft;
+ this.WindowState = WindowState.Maximized;
+ }
+ }
+ };
+ this.Opened += lh;
}
- public TopLevel Root
+ public TopLevel? Root
{
get => _root;
set
@@ -43,7 +67,7 @@ public TopLevel Root
if (_root != null)
{
_root.Closed += RootClosed;
- DataContext = new MainViewModel(value);
+ DataContext = new MainViewModel(_root);
}
else
{
@@ -53,15 +77,27 @@ public TopLevel Root
}
}
- IStyleHost IStyleHost.StylingParent => null;
+ IStyleHost? IStyleHost.StylingParent => null;
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
_keySubscription.Dispose();
- _root.Closed -= RootClosed;
- _root = null;
- ((MainViewModel)DataContext)?.Dispose();
+
+ foreach (var state in _frozenPopupStates)
+ {
+ state.Value.Dispose();
+ }
+
+ _frozenPopupStates.Clear();
+
+ if (_root != null)
+ {
+ _root.Closed -= RootClosed;
+ _root = null;
+ }
+
+ ((MainViewModel?)DataContext)?.Dispose();
}
private void InitializeComponent()
@@ -69,24 +105,134 @@ private void InitializeComponent()
AvaloniaXamlLoader.Load(this);
}
+ private IControl? GetHoveredControl(TopLevel topLevel)
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default;
+#pragma warning restore CS0618 // Type or member is obsolete
+
+ return (IControl?)topLevel.GetVisualsAt(point, x =>
+ {
+ if (x is AdornerLayer || !x.IsVisible)
+ {
+ return false;
+ }
+
+ return !(x is IInputElement ie) || ie.IsHitTestVisible;
+ })
+ .FirstOrDefault();
+ }
+
+ private static List GetPopupRoots(IVisual root)
+ {
+ var popupRoots = new List();
+
+ void ProcessProperty(IControl control, AvaloniaProperty property)
+ {
+ if (control.GetValue(property) is IPopupHostProvider popupProvider
+ && popupProvider.PopupHost is PopupRoot popupRoot)
+ {
+ popupRoots.Add(popupRoot);
+ }
+ }
+
+ foreach (var control in root.GetVisualDescendants().OfType())
+ {
+ if (control is Popup p && p.Host is PopupRoot popupRoot)
+ {
+ popupRoots.Add(popupRoot);
+ }
+
+ ProcessProperty(control, ContextFlyoutProperty);
+ ProcessProperty(control, ContextMenuProperty);
+ ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty);
+ ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty);
+ }
+
+ return popupRoots;
+ }
+
private void RawKeyDown(RawKeyEventArgs e)
{
- const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift;
+ var vm = (MainViewModel?)DataContext;
+ if (vm is null)
+ {
+ return;
+ }
- if (e.Modifiers == modifiers)
+ switch (e.Modifiers)
{
- var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default;
- var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible))
- .FirstOrDefault();
+ case RawInputModifiers.Control | RawInputModifiers.Shift:
+ {
+ IControl? control = null;
+
+ foreach (var popupRoot in GetPopupRoots(Root))
+ {
+ control = GetHoveredControl(popupRoot);
+
+ if (control != null)
+ {
+ break;
+ }
+ }
+
+ control ??= GetHoveredControl(Root);
- if (control != null)
+ if (control != null)
+ {
+ vm.SelectControl(control);
+ }
+
+ break;
+ }
+
+ case RawInputModifiers.Control | RawInputModifiers.Alt when e.Key == Key.F:
{
- var vm = (MainViewModel)DataContext;
- vm.SelectControl((IControl)control);
+ vm.FreezePopups = !vm.FreezePopups;
+
+ foreach (var popupRoot in GetPopupRoots(Root))
+ {
+ if (popupRoot.Parent is Popup popup)
+ {
+ if (vm.FreezePopups)
+ {
+ var lightDismissEnabledState = popup.SetValue(
+ Popup.IsLightDismissEnabledProperty,
+ !vm.FreezePopups,
+ BindingPriority.Animation);
+
+ if (lightDismissEnabledState != null)
+ {
+ _frozenPopupStates[popup] = lightDismissEnabledState;
+ }
+ }
+ else
+ {
+ //TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
+ if (_frozenPopupStates.ContainsKey(popup))
+ {
+ _frozenPopupStates[popup].Dispose();
+ _frozenPopupStates.Remove(popup);
+ }
+ }
+ }
+ }
+
+ break;
+ }
+
+ case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D:
+ {
+ vm.EnableSnapshotStyles(e.Key == Key.S);
+
+ break;
}
}
}
- private void RootClosed(object sender, EventArgs e) => Close();
+ private void RootClosed(object? sender, EventArgs e) => Close();
+
+ public void SetOptions(DevToolsOptions options) =>
+ (DataContext as MainViewModel)?.SetOptions(options);
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
index a1e6ca7d373..bb661f7f4c0 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
x:Class="Avalonia.Diagnostics.Views.TreePageView">
-
+
-
+
+
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
index 1b61986ce60..3543b1adea4 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Generators;
@@ -6,18 +7,20 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
+using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.Views
{
internal class TreePageView : UserControl
{
private readonly Panel _adorner;
- private AdornerLayer _currentLayer;
+ private AdornerLayer? _currentLayer;
private TreeView _tree;
public TreePageView()
{
InitializeComponent();
+ _tree = this.FindControl("tree");
_tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
_adorner = new Panel
@@ -35,9 +38,15 @@ public TreePageView()
};
}
- protected void AddAdorner(object sender, PointerEventArgs e)
+ protected void AddAdorner(object? sender, PointerEventArgs e)
{
- var node = (TreeNode)((Control)sender).DataContext;
+ var node = (TreeNode?)((Control)sender!).DataContext;
+ var vm = (TreePageViewModel?)DataContext;
+ if (node is null || vm is null)
+ {
+ return;
+ }
+
var visual = (Visual)node.Visual;
_currentLayer = AdornerLayer.GetAdornerLayer(visual);
@@ -51,8 +60,6 @@ protected void AddAdorner(object sender, PointerEventArgs e)
_currentLayer.Children.Add(_adorner);
AdornerLayer.SetAdornedElement(_adorner, visual);
- var vm = (TreePageViewModel) DataContext;
-
if (vm.MainView.ShouldVisualizeMarginPadding)
{
var paddingBorder = (Border)_adorner.Children[0];
@@ -72,7 +79,7 @@ private static Thickness InvertThickness(Thickness input)
return new Thickness(-input.Left, -input.Top, -input.Right, -input.Bottom);
}
- protected void RemoveAdorner(object sender, PointerEventArgs e)
+ protected void RemoveAdorner(object? sender, PointerEventArgs e)
{
foreach (var border in _adorner.Children.OfType())
{
@@ -88,24 +95,30 @@ protected void RemoveAdorner(object sender, PointerEventArgs e)
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
- _tree = this.FindControl("tree");
}
- private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e)
+ private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e)
{
var item = (TreeViewItem)e.Containers[0].ContainerControl;
item.TemplateApplied += TreeViewItemTemplateApplied;
}
- private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
+ private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
- var item = (TreeViewItem)sender;
- var headerPresenter = item.HeaderPresenter;
- headerPresenter.ApplyTemplate();
+ var item = (TreeViewItem)sender!;
+
+ // This depends on the default tree item template.
+ // We want to handle events in the item header but exclude events coming from children.
+ var header = item.FindDescendantOfType();
+
+ Debug.Assert(header != null);
+
+ if (header != null)
+ {
+ header.PointerEnter += AddAdorner;
+ header.PointerLeave += RemoveAdorner;
+ }
- var header = headerPresenter.Child;
- header.PointerEnter += AddAdorner;
- header.PointerLeave += RemoveAdorner;
item.TemplateApplied -= TreeViewItemTemplateApplied;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
index 6f699339e7d..4adcd32302b 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs
@@ -17,7 +17,7 @@ public static string PrintVisualTree(IVisual visual)
private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent)
{
- Control control = visual as Control;
+ Control? control = visual as Control;
builder.Append(Indent(indent - 1));
diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
index e51cc2f3ce2..08e8798cd50 100644
--- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
+++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml
@@ -79,8 +79,8 @@
-
-
+
+
@@ -99,7 +99,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
index 09fab0ed3f3..55e30396e1b 100644
--- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
+++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
@@ -1,3 +1,4 @@
+using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Avalonia.Controls;
@@ -7,12 +8,18 @@ namespace Avalonia.Dialogs
{
public class AboutAvaloniaDialog : Window
{
+ private static readonly Version s_version = typeof(AboutAvaloniaDialog).Assembly.GetName().Version;
+
+ public static string Version { get; } = s_version.ToString(2);
+
+ public static bool IsDevelopmentBuild { get; } = s_version.Revision == 999;
+
public AboutAvaloniaDialog()
{
AvaloniaXamlLoader.Load(this);
DataContext = this;
}
-
+
public static void OpenBrowser(string url)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
index dd476e2559c..665693a6541 100644
--- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
+++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
@@ -4,9 +4,6 @@
-
- Designer
-
diff --git a/src/Avalonia.Dialogs/ChildFitter.cs b/src/Avalonia.Dialogs/ChildFitter.cs
index 744d455d9df..4a6db62b327 100644
--- a/src/Avalonia.Dialogs/ChildFitter.cs
+++ b/src/Avalonia.Dialogs/ChildFitter.cs
@@ -4,7 +4,7 @@
namespace Avalonia.Dialogs
{
- internal class ChildFitter : Decorator
+ public class ChildFitter : Decorator
{
protected override Size MeasureOverride(Size availableSize)
{
diff --git a/src/Avalonia.Dialogs/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/FileSizeStringConverter.cs
index 5b41b9da35e..c2cdf1e5021 100644
--- a/src/Avalonia.Dialogs/FileSizeStringConverter.cs
+++ b/src/Avalonia.Dialogs/FileSizeStringConverter.cs
@@ -6,7 +6,7 @@
namespace Avalonia.Dialogs
{
- internal class FileSizeStringConverter : IValueConverter
+ public class FileSizeStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs
similarity index 66%
rename from src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
rename to src/Avalonia.Dialogs/ManagedFileChooser.cs
index 7f29407ed57..f9f38ac4748 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.cs
@@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@@ -10,17 +11,14 @@
namespace Avalonia.Dialogs
{
- internal class ManagedFileChooser : UserControl
+ public class ManagedFileChooser : TemplatedControl
{
private Control _quickLinksRoot;
private ListBox _filesView;
public ManagedFileChooser()
{
- AvaloniaXamlLoader.Load(this);
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
- _quickLinksRoot = this.FindControl("QuickLinks");
- _filesView = this.FindControl("Files");
}
ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel;
@@ -34,19 +32,22 @@ private void OnPointerPressed(object sender, PointerPressedEventArgs e)
return;
}
- var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
- if (e.ClickCount == 2 || isQuickLink)
+ if (_quickLinksRoot != null)
{
- if (model.ItemType == ManagedFileChooserItemType.File)
+ var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
+ if (e.ClickCount == 2 || isQuickLink)
{
- Model?.SelectSingleFile(model);
+ if (model.ItemType == ManagedFileChooserItemType.File)
+ {
+ Model?.SelectSingleFile(model);
+ }
+ else
+ {
+ Model?.Navigate(model.Path);
+ }
+
+ e.Handled = true;
}
- else
- {
- Model?.Navigate(model.Path);
- }
-
- e.Handled = true;
}
}
@@ -79,10 +80,16 @@ protected override async void OnDataContextChanged(EventArgs e)
// Workaround for ListBox bug, scroll to the previous file
var indexOfPreselected = model.Items.IndexOf(preselected);
- if (indexOfPreselected > 1)
+ if ((_filesView != null) && (indexOfPreselected > 1))
{
_filesView.ScrollIntoView(indexOfPreselected - 1);
}
}
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ _quickLinksRoot = e.NameScope.Get("QuickLinks");
+ _filesView = e.NameScope.Get("Files");
+ }
}
}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml
deleted file mode 100644
index 227cc1afc05..00000000000
--- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml
+++ /dev/null
@@ -1,144 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Show hidden files
-
-
-
-
-
-
- OK
- Cancel
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
index 0dc024c4ddd..050d618ce16 100644
--- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
+++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
@@ -67,7 +67,7 @@ public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots()
{
Directory.GetFiles(x.VolumePath);
}
- catch (UnauthorizedAccessException _)
+ catch (Exception _)
{
return null;
}
diff --git a/src/Avalonia.Dialogs/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs
index 9d8b6cb1c7d..c8226b98e4a 100644
--- a/src/Avalonia.Dialogs/ResourceSelectorConverter.cs
+++ b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs
@@ -5,7 +5,7 @@
namespace Avalonia.Dialogs
{
- internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter
+ public class ResourceSelectorConverter : ResourceDictionary, IValueConverter
{
public object Convert(object key, Type targetType, object parameter, CultureInfo culture)
{
diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
index 260d7350d25..81af76dc205 100644
--- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
+++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
@@ -6,9 +6,7 @@
+
-
-
-
diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs
new file mode 100644
index 00000000000..5cd748be027
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Avalonia.FreeDesktop
+{
+ class DBusCallQueue
+ {
+ private readonly Func _errorHandler;
+
+ class Item
+ {
+ public Func Callback;
+ public Action OnFinish;
+ }
+ private Queue- _q = new Queue
- ();
+ private bool _processing;
+
+ public DBusCallQueue(Func errorHandler)
+ {
+ _errorHandler = errorHandler;
+ }
+
+ public void Enqueue(Func cb)
+ {
+ _q.Enqueue(new Item
+ {
+ Callback = cb
+ });
+ Process();
+ }
+
+ public Task EnqueueAsync(Func cb)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _q.Enqueue(new Item
+ {
+ Callback = cb,
+ OnFinish = e =>
+ {
+ if (e == null)
+ tcs.TrySetResult(0);
+ else
+ tcs.TrySetException(e);
+ }
+ });
+ Process();
+ return tcs.Task;
+ }
+
+ public Task EnqueueAsync(Func> cb)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ _q.Enqueue(new Item
+ {
+ Callback = async () =>
+ {
+ var res = await cb();
+ tcs.TrySetResult(res);
+ },
+ OnFinish = e =>
+ {
+ if (e != null)
+ tcs.TrySetException(e);
+ }
+ });
+ Process();
+ return tcs.Task;
+ }
+
+ async void Process()
+ {
+ if(_processing)
+ return;
+ _processing = true;
+ try
+ {
+ while (_q.Count > 0)
+ {
+ var item = _q.Dequeue();
+ try
+ {
+ await item.Callback();
+ item.OnFinish?.Invoke(null);
+ }
+ catch(Exception e)
+ {
+ if (item.OnFinish != null)
+ item.OnFinish(e);
+ else
+ await _errorHandler(e);
+ }
+ }
+ }
+ finally
+ {
+ _processing = false;
+ }
+ }
+
+ public void FailAll()
+ {
+ while (_q.Count>0)
+ {
+ var item = _q.Dequeue();
+ item.OnFinish?.Invoke(new OperationCanceledException());
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs
index 91c4c28995e..7996a94dd03 100644
--- a/src/Avalonia.FreeDesktop/DBusHelper.cs
+++ b/src/Avalonia.FreeDesktop/DBusHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading;
+using Avalonia.Logging;
using Avalonia.Threading;
using Tmds.DBus;
@@ -43,13 +44,15 @@ public override void Send(SendOrPostCallback d, object state)
public void Initialized()
{
lock (_lock)
- _ctx = new AvaloniaSynchronizationContext(null);
+ _ctx = new AvaloniaSynchronizationContext();
}
}
public static Connection Connection { get; private set; }
- public static Exception TryInitialize(string dbusAddress = null)
+ public static Connection TryInitialize(string dbusAddress = null)
{
+ if (Connection != null)
+ return Connection;
var oldContext = SynchronizationContext.Current;
try
{
@@ -70,13 +73,15 @@ public static Exception TryInitialize(string dbusAddress = null)
}
catch (Exception e)
{
- return e;
+ Logger.TryGet(LogEventLevel.Error, "DBUS")
+ ?.Log(null, "Unable to connect to DBus: " + e);
}
finally
{
SynchronizationContext.SetSynchronizationContext(oldContext);
}
- return null;
+
+ return Connection;
}
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
new file mode 100644
index 00000000000..a7e83140ae4
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
@@ -0,0 +1,288 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.FreeDesktop.DBusIme.Fcitx;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Avalonia.Logging;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme
+{
+ internal class DBusInputMethodFactory : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl
+ {
+ private readonly Func _factory;
+
+ public DBusInputMethodFactory(Func factory)
+ {
+ _factory = factory;
+ }
+
+ public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid)
+ {
+ var im = _factory(xid);
+ return (im, im);
+ }
+ }
+
+ internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl
+ {
+ private List _disposables = new List();
+ private Queue _onlineNamesQueue = new Queue();
+ protected Connection Connection { get; }
+ private readonly string[] _knownNames;
+ private bool _connecting;
+ private string _currentName;
+ private DBusCallQueue _queue;
+ private bool _controlActive, _windowActive;
+ private bool? _imeActive;
+ private Rect _logicalRect;
+ private PixelRect? _lastReportedRect;
+ private double _scaling = 1;
+ private PixelPoint _windowPosition;
+
+ protected bool IsConnected => _currentName != null;
+
+ public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
+ {
+ _queue = new DBusCallQueue(QueueOnError);
+ Connection = connection;
+ _knownNames = knownNames;
+ Watch();
+ }
+
+ async void Watch()
+ {
+ foreach (var name in _knownNames)
+ _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
+ }
+
+ protected abstract Task Connect(string name);
+
+ protected string GetAppName() =>
+ Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
+
+ private async void OnNameChange(ServiceOwnerChangedEventArgs args)
+ {
+ if (args.NewOwner != null && _currentName == null)
+ {
+ _onlineNamesQueue.Enqueue(args.ServiceName);
+ if(!_connecting)
+ {
+ _connecting = true;
+ try
+ {
+ while (_onlineNamesQueue.Count > 0)
+ {
+ var name = _onlineNamesQueue.Dequeue();
+ try
+ {
+ if (await Connect(name))
+ {
+ _onlineNamesQueue.Clear();
+ _currentName = name;
+ return;
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.TryGet(LogEventLevel.Error, "IME")
+ ?.Log(this, "Unable to create IME input context:\n" + e);
+ }
+ }
+ }
+ finally
+ {
+ _connecting = false;
+ }
+ }
+
+ }
+
+ // IME has crashed
+ if (args.NewOwner == null && args.ServiceName == _currentName)
+ {
+ _currentName = null;
+ foreach(var s in _disposables)
+ s.Dispose();
+ _disposables.Clear();
+
+ OnDisconnected();
+ Reset();
+
+ // Watch again
+ Watch();
+ }
+ }
+
+ protected virtual Task Disconnect()
+ {
+ return Task.CompletedTask;
+ }
+
+ protected virtual void OnDisconnected()
+ {
+
+ }
+
+ protected virtual void Reset()
+ {
+ _lastReportedRect = null;
+ _imeActive = null;
+ }
+
+ async Task QueueOnError(Exception e)
+ {
+ Logger.TryGet(LogEventLevel.Error, "IME")
+ ?.Log(this, "Error:\n" + e);
+ try
+ {
+ await Disconnect();
+ }
+ catch (Exception ex)
+ {
+ Logger.TryGet(LogEventLevel.Error, "IME")
+ ?.Log(this, "Error while destroying the context:\n" + ex);
+ }
+ OnDisconnected();
+ _currentName = null;
+ }
+
+ protected void Enqueue(Func cb) => _queue.Enqueue(cb);
+
+ protected void AddDisposable(IDisposable d) => _disposables.Add(d);
+
+ public void Dispose()
+ {
+ foreach(var d in _disposables)
+ d.Dispose();
+ _disposables.Clear();
+ try
+ {
+ Disconnect().ContinueWith(_ => { });
+ }
+ catch
+ {
+ // fire and forget
+ }
+ _currentName = null;
+ }
+
+ protected abstract Task SetCursorRectCore(PixelRect rect);
+ protected abstract Task SetActiveCore(bool active);
+ protected abstract Task ResetContextCore();
+ protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode);
+
+ void UpdateActive()
+ {
+ _queue.Enqueue(async () =>
+ {
+ if(!IsConnected)
+ return;
+
+ var active = _windowActive && _controlActive;
+ if (active != _imeActive)
+ {
+ _imeActive = active;
+ await SetActiveCore(active);
+ }
+ });
+ }
+
+
+ void IX11InputMethodControl.SetWindowActive(bool active)
+ {
+ _windowActive = active;
+ UpdateActive();
+ }
+
+ void ITextInputMethodImpl.SetActive(bool active)
+ {
+ _controlActive = active;
+ UpdateActive();
+ }
+
+ bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true;
+
+ async ValueTask IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode)
+ {
+ try
+ {
+ return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode));
+ }
+ // Disconnected
+ catch (OperationCanceledException)
+ {
+ return false;
+ }
+ // Error, disconnect
+ catch (Exception e)
+ {
+ await QueueOnError(e);
+ return false;
+ }
+ }
+
+ private Action _onCommit;
+ event Action IX11InputMethodControl.Commit
+ {
+ add => _onCommit += value;
+ remove => _onCommit -= value;
+ }
+
+ protected void FireCommit(string s) => _onCommit?.Invoke(s);
+
+ private Action _onForward;
+ event Action IX11InputMethodControl.ForwardKey
+ {
+ add => _onForward += value;
+ remove => _onForward -= value;
+ }
+
+ protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k);
+
+ void UpdateCursorRect()
+ {
+ _queue.Enqueue(async () =>
+ {
+ if(!IsConnected)
+ return;
+ var cursorRect = PixelRect.FromRect(_logicalRect, _scaling);
+ cursorRect = cursorRect.Translate(_windowPosition);
+ if (cursorRect != _lastReportedRect)
+ {
+ _lastReportedRect = cursorRect;
+ await SetCursorRectCore(cursorRect);
+ }
+ });
+ }
+
+ void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling)
+ {
+ _windowPosition = position;
+ _scaling = scaling;
+ UpdateCursorRect();
+ }
+
+ void ITextInputMethodImpl.SetCursorRect(Rect rect)
+ {
+ _logicalRect = rect;
+ UpdateCursorRect();
+ }
+
+ public abstract void SetOptions(TextInputOptionsQueryEventArgs options);
+
+ void ITextInputMethodImpl.Reset()
+ {
+ Reset();
+ _queue.Enqueue(async () =>
+ {
+ if (!IsConnected)
+ return;
+ await ResetContextCore();
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
new file mode 100644
index 00000000000..7ce2339763d
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ [DBusInterface("org.fcitx.Fcitx.InputMethod")]
+ interface IFcitxInputMethod : IDBusObject
+ {
+ Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async(
+ string Appname, int Pid);
+ }
+
+
+ [DBusInterface("org.fcitx.Fcitx.InputContext")]
+ interface IFcitxInputContext : IDBusObject
+ {
+ Task EnableICAsync();
+ Task CloseICAsync();
+ Task FocusInAsync();
+ Task FocusOutAsync();
+ Task ResetAsync();
+ Task MouseEventAsync(int X);
+ Task SetCursorLocationAsync(int X, int Y);
+ Task SetCursorRectAsync(int X, int Y, int W, int H);
+ Task SetCapacityAsync(uint Caps);
+ Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
+ Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
+ Task DestroyICAsync();
+ Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
+ Task WatchEnableIMAsync(Action handler, Action onError = null);
+ Task WatchCloseIMAsync(Action handler, Action onError = null);
+ Task WatchCommitStringAsync(Action handler, Action onError = null);
+ Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null);
+ Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action onError = null);
+ Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null);
+ Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action onError = null);
+ Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action onError = null);
+ Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null);
+ }
+
+ [DBusInterface("org.fcitx.Fcitx.InputContext1")]
+ interface IFcitxInputContext1 : IDBusObject
+ {
+ Task FocusInAsync();
+ Task FocusOutAsync();
+ Task ResetAsync();
+ Task SetCursorRectAsync(int X, int Y, int W, int H);
+ Task SetCapabilityAsync(ulong Caps);
+ Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor);
+ Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
+ Task DestroyICAsync();
+ Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
+ Task WatchCommitStringAsync(Action handler, Action onError = null);
+ Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action onError = null);
+ Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action onError = null);
+ Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action onError = null);
+ Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action onError = null);
+ }
+
+ [DBusInterface("org.fcitx.Fcitx.InputMethod1")]
+ interface IFcitxInputMethod1 : IDBusObject
+ {
+ Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
new file mode 100644
index 00000000000..6510a5877a8
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs
@@ -0,0 +1,67 @@
+using System;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ enum FcitxKeyEventType
+ {
+ FCITX_PRESS_KEY,
+ FCITX_RELEASE_KEY
+ };
+
+ [Flags]
+ enum FcitxCapabilityFlags
+ {
+ CAPACITY_NONE = 0,
+ CAPACITY_CLIENT_SIDE_UI = (1 << 0),
+ CAPACITY_PREEDIT = (1 << 1),
+ CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2),
+ CAPACITY_PASSWORD = (1 << 3),
+ CAPACITY_FORMATTED_PREEDIT = (1 << 4),
+ CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5),
+ CAPACITY_SURROUNDING_TEXT = (1 << 6),
+ CAPACITY_EMAIL = (1 << 7),
+ CAPACITY_DIGIT = (1 << 8),
+ CAPACITY_UPPERCASE = (1 << 9),
+ CAPACITY_LOWERCASE = (1 << 10),
+ CAPACITY_NOAUTOUPPERCASE = (1 << 11),
+ CAPACITY_URL = (1 << 12),
+ CAPACITY_DIALABLE = (1 << 13),
+ CAPACITY_NUMBER = (1 << 14),
+ CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15),
+ CAPACITY_SPELLCHECK = (1 << 16),
+ CAPACITY_NO_SPELLCHECK = (1 << 17),
+ CAPACITY_WORD_COMPLETION = (1 << 18),
+ CAPACITY_UPPERCASE_WORDS = (1 << 19),
+ CAPACITY_UPPERCASE_SENTENCES = (1 << 20),
+ CAPACITY_ALPHA = (1 << 21),
+ CAPACITY_NAME = (1 << 22),
+ CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23),
+ CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24),
+ };
+
+ [Flags]
+ enum FcitxKeyState
+ {
+ FcitxKeyState_None = 0,
+ FcitxKeyState_Shift = 1 << 0,
+ FcitxKeyState_CapsLock = 1 << 1,
+ FcitxKeyState_Ctrl = 1 << 2,
+ FcitxKeyState_Alt = 1 << 3,
+ FcitxKeyState_Alt_Shift = FcitxKeyState_Alt | FcitxKeyState_Shift,
+ FcitxKeyState_Ctrl_Shift = FcitxKeyState_Ctrl | FcitxKeyState_Shift,
+ FcitxKeyState_Ctrl_Alt = FcitxKeyState_Ctrl | FcitxKeyState_Alt,
+
+ FcitxKeyState_Ctrl_Alt_Shift =
+ FcitxKeyState_Ctrl | FcitxKeyState_Alt | FcitxKeyState_Shift,
+ FcitxKeyState_NumLock = 1 << 4,
+ FcitxKeyState_Super = 1 << 6,
+ FcitxKeyState_ScrollLock = 1 << 7,
+ FcitxKeyState_MousePressed = 1 << 8,
+ FcitxKeyState_HandledMask = 1 << 24,
+ FcitxKeyState_IgnoredMask = 1 << 25,
+ FcitxKeyState_Super2 = 1 << 26,
+ FcitxKeyState_Hyper = 1 << 27,
+ FcitxKeyState_Meta = 1 << 28,
+ FcitxKeyState_UsedMask = 0x5c001fff
+ };
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
new file mode 100644
index 00000000000..a03ea213aa1
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ internal class FcitxICWrapper
+ {
+ private readonly IFcitxInputContext1 _modern;
+ private readonly IFcitxInputContext _old;
+
+ public FcitxICWrapper(IFcitxInputContext old)
+ {
+ _old = old;
+ }
+
+ public FcitxICWrapper(IFcitxInputContext1 modern)
+ {
+ _modern = modern;
+ }
+
+ public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync();
+
+ public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync();
+
+ public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync();
+
+ public Task SetCursorRectAsync(int x, int y, int w, int h) =>
+ _old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h);
+ public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync();
+
+ public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time)
+ {
+ if(_old!=null)
+ return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0;
+ return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time);
+ }
+
+ public Task WatchCommitStringAsync(Action handler) =>
+ _old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler);
+
+ public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler)
+ {
+ return _old?.WatchForwardKeyAsync(handler)
+ ?? _modern.WatchForwardKeyAsync(ev =>
+ handler((ev.keyval, ev.state, ev.type ? 1 : 0)));
+ }
+
+ public Task SetCapacityAsync(uint flags) =>
+ _old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
new file mode 100644
index 00000000000..31a061571fb
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Diagnostics;
+using System.Reactive.Concurrency;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme.Fcitx
+{
+ internal class FcitxX11TextInputMethod : DBusTextInputMethodBase
+ {
+ private FcitxICWrapper _context;
+ private FcitxCapabilityFlags? _lastReportedFlags;
+
+ public FcitxX11TextInputMethod(Connection connection) : base(connection,
+ "org.fcitx.Fcitx",
+ "org.freedesktop.portal.Fcitx"
+ )
+ {
+
+ }
+
+ protected override async Task Connect(string name)
+ {
+ if (name == "org.fcitx.Fcitx")
+ {
+ var method = Connection.CreateProxy(name, "/inputmethod");
+ var resp = await method.CreateICv3Async(GetAppName(),
+ Process.GetCurrentProcess().Id);
+
+ var proxy = Connection.CreateProxy(name,
+ "/inputcontext_" + resp.icid);
+
+ _context = new FcitxICWrapper(proxy);
+ }
+ else
+ {
+ var method = Connection.CreateProxy(name, "/inputmethod");
+ var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
+ var proxy = Connection.CreateProxy(name, resp.path);
+ _context = new FcitxICWrapper(proxy);
+ }
+
+ AddDisposable(await _context.WatchCommitStringAsync(OnCommitString));
+ AddDisposable(await _context.WatchForwardKeyAsync(OnForward));
+ return true;
+ }
+
+ protected override Task Disconnect() => _context.DestroyICAsync();
+
+ protected override void OnDisconnected() => _context = null;
+
+ protected override void Reset()
+ {
+ _lastReportedFlags = null;
+ base.Reset();
+ }
+
+ protected override Task SetCursorRectCore(PixelRect cursorRect) =>
+ _context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width),
+ Math.Max(1, cursorRect.Height));
+
+ protected override Task SetActiveCore(bool active)
+ {
+ if (active)
+ return _context.FocusInAsync();
+ else
+ return _context.FocusOutAsync();
+ }
+
+ protected override Task ResetContextCore() => _context.ResetAsync();
+
+ protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
+ {
+ FcitxKeyState state = default;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Control))
+ state |= FcitxKeyState.FcitxKeyState_Ctrl;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Alt))
+ state |= FcitxKeyState.FcitxKeyState_Alt;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Shift))
+ state |= FcitxKeyState.FcitxKeyState_Shift;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Meta))
+ state |= FcitxKeyState.FcitxKeyState_Super;
+
+ var type = args.Type == RawKeyEventType.KeyDown ?
+ FcitxKeyEventType.FCITX_PRESS_KEY :
+ FcitxKeyEventType.FCITX_RELEASE_KEY;
+
+ return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
+ (uint)args.Timestamp).ConfigureAwait(false);
+ }
+
+ public override void SetOptions(TextInputOptionsQueryEventArgs options) =>
+ Enqueue(async () =>
+ {
+ if(_context == null)
+ return;
+ FcitxCapabilityFlags flags = default;
+ if (options.Lowercase)
+ flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE;
+ if (options.Uppercase)
+ flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE;
+ if (!options.AutoCapitalization)
+ flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE;
+ if (options.ContentType == TextInputContentType.Email)
+ flags |= FcitxCapabilityFlags.CAPACITY_EMAIL;
+ else if (options.ContentType == TextInputContentType.Number)
+ flags |= FcitxCapabilityFlags.CAPACITY_NUMBER;
+ else if (options.ContentType == TextInputContentType.Password)
+ flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD;
+ else if (options.ContentType == TextInputContentType.Phone)
+ flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE;
+ else if (options.ContentType == TextInputContentType.Url)
+ flags |= FcitxCapabilityFlags.CAPACITY_URL;
+ if (flags != _lastReportedFlags)
+ {
+ _lastReportedFlags = flags;
+ await _context.SetCapacityAsync((uint)flags);
+ }
+ });
+
+ private void OnForward((uint keyval, uint state, int type) ev)
+ {
+ var state = (FcitxKeyState)ev.state;
+ KeyModifiers mods = default;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Ctrl))
+ mods |= KeyModifiers.Control;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Alt))
+ mods |= KeyModifiers.Alt;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Shift))
+ mods |= KeyModifiers.Shift;
+ if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Super))
+ mods |= KeyModifiers.Meta;
+ FireForward(new X11InputMethodForwardedKey
+ {
+ Modifiers = mods,
+ KeyVal = (int)ev.keyval,
+ Type = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY ?
+ RawKeyEventType.KeyDown :
+ RawKeyEventType.KeyUp
+ });
+ }
+
+ private void OnCommitString(string s) => FireCommit(s);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
new file mode 100644
index 00000000000..26c0d249f38
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+ [DBusInterface("org.freedesktop.IBus.InputContext")]
+ interface IIBusInputContext : IDBusObject
+ {
+ Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State);
+ Task SetCursorLocationAsync(int X, int Y, int W, int H);
+ Task FocusInAsync();
+ Task FocusOutAsync();
+ Task ResetAsync();
+ Task SetCapabilitiesAsync(uint Caps);
+ Task PropertyActivateAsync(string Name, int State);
+ Task SetEngineAsync(string Name);
+ Task GetEngineAsync();
+ Task DestroyAsync();
+ Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
+ Task WatchCommitTextAsync(Action cb, Action onError = null);
+ Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action onError = null);
+ Task WatchRequireSurroundingTextAsync(Action handler, Action onError = null);
+ Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action onError = null);
+ Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action onError = null);
+ Task WatchShowPreeditTextAsync(Action handler, Action onError = null);
+ Task WatchHidePreeditTextAsync(Action handler, Action onError = null);
+ Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action onError = null);
+ Task WatchShowAuxiliaryTextAsync(Action handler, Action onError = null);
+ Task WatchHideAuxiliaryTextAsync(Action handler, Action onError = null);
+ Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action onError = null);
+ Task WatchShowLookupTableAsync(Action handler, Action onError = null);
+ Task WatchHideLookupTableAsync(Action handler, Action onError = null);
+ Task WatchPageUpLookupTableAsync(Action handler, Action onError = null);
+ Task WatchPageDownLookupTableAsync(Action handler, Action onError = null);
+ Task WatchCursorUpLookupTableAsync(Action handler, Action onError = null);
+ Task WatchCursorDownLookupTableAsync(Action handler, Action onError = null);
+ Task WatchRegisterPropertiesAsync(Action handler, Action onError = null);
+ Task WatchUpdatePropertyAsync(Action handler, Action onError = null);
+ }
+
+
+ [DBusInterface("org.freedesktop.IBus.Portal")]
+ interface IIBusPortal : IDBusObject
+ {
+ Task CreateInputContextAsync(string Name);
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
new file mode 100644
index 00000000000..3070f51a8e8
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+ [Flags]
+ internal enum IBusModifierMask
+ {
+ ShiftMask = 1 << 0,
+ LockMask = 1 << 1,
+ ControlMask = 1 << 2,
+ Mod1Mask = 1 << 3,
+ Mod2Mask = 1 << 4,
+ Mod3Mask = 1 << 5,
+ Mod4Mask = 1 << 6,
+ Mod5Mask = 1 << 7,
+ Button1Mask = 1 << 8,
+ Button2Mask = 1 << 9,
+ Button3Mask = 1 << 10,
+ Button4Mask = 1 << 11,
+ Button5Mask = 1 << 12,
+
+ HandledMask = 1 << 24,
+ ForwardMask = 1 << 25,
+ IgnoredMask = ForwardMask,
+
+ SuperMask = 1 << 26,
+ HyperMask = 1 << 27,
+ MetaMask = 1 << 28,
+
+ ReleaseMask = 1 << 30,
+
+ ModifierMask = 0x5c001fff
+ }
+
+ [Flags]
+ internal enum IBusCapability
+ {
+ CapPreeditText = 1 << 0,
+ CapAuxiliaryText = 1 << 1,
+ CapLookupTable = 1 << 2,
+ CapFocus = 1 << 3,
+ CapProperty = 1 << 4,
+ CapSurroundingText = 1 << 5,
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
new file mode 100644
index 00000000000..a73de9dae8e
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
@@ -0,0 +1,105 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme.IBus
+{
+ internal class IBusX11TextInputMethod : DBusTextInputMethodBase
+ {
+ private IIBusInputContext _context;
+
+ public IBusX11TextInputMethod(Connection connection) : base(connection,
+ "org.freedesktop.portal.IBus")
+ {
+ }
+
+ protected override async Task Connect(string name)
+ {
+ var path =
+ await Connection.CreateProxy(name, "/org/freedesktop/IBus")
+ .CreateInputContextAsync(GetAppName());
+
+ _context = Connection.CreateProxy(name, path);
+ AddDisposable(await _context.WatchCommitTextAsync(OnCommitText));
+ AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey));
+ Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus));
+ return true;
+ }
+
+ private void OnForwardKey((uint keyval, uint keycode, uint state) k)
+ {
+ var state = (IBusModifierMask)k.state;
+ KeyModifiers mods = default;
+ if (state.HasAllFlags(IBusModifierMask.ControlMask))
+ mods |= KeyModifiers.Control;
+ if (state.HasAllFlags(IBusModifierMask.Mod1Mask))
+ mods |= KeyModifiers.Alt;
+ if (state.HasAllFlags(IBusModifierMask.ShiftMask))
+ mods |= KeyModifiers.Shift;
+ if (state.HasAllFlags(IBusModifierMask.Mod4Mask))
+ mods |= KeyModifiers.Meta;
+ FireForward(new X11InputMethodForwardedKey
+ {
+ KeyVal = (int)k.keyval,
+ Type = state.HasAllFlags(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown,
+ Modifiers = mods
+ });
+ }
+
+
+ private void OnCommitText(object wtf)
+ {
+ // Hello darkness, my old friend
+ var prop = wtf.GetType().GetField("Item3");
+ if (prop != null)
+ {
+ var text = (string)prop.GetValue(wtf);
+ if (!string.IsNullOrEmpty(text))
+ FireCommit(text);
+ }
+ }
+
+ protected override Task Disconnect() => _context.DestroyAsync();
+
+ protected override void OnDisconnected()
+ {
+ _context = null;
+ base.OnDisconnected();
+ }
+
+ protected override Task SetCursorRectCore(PixelRect rect)
+ => _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height);
+
+ protected override Task SetActiveCore(bool active)
+ => active ? _context.FocusInAsync() : _context.FocusOutAsync();
+
+ protected override Task ResetContextCore()
+ => _context.ResetAsync();
+
+ protected override Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
+ {
+ IBusModifierMask state = default;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Control))
+ state |= IBusModifierMask.ControlMask;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Alt))
+ state |= IBusModifierMask.Mod1Mask;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Shift))
+ state |= IBusModifierMask.ShiftMask;
+ if (args.Modifiers.HasAllFlags(RawInputModifiers.Meta))
+ state |= IBusModifierMask.Mod4Mask;
+
+ if (args.Type == RawKeyEventType.KeyUp)
+ state |= IBusModifierMask.ReleaseMask;
+
+ return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
+ }
+
+ public override void SetOptions(TextInputOptionsQueryEventArgs options)
+ {
+ // No-op, because ibus
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
new file mode 100644
index 00000000000..7f71ecf0ffc
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.FreeDesktop.DBusIme.Fcitx;
+using Avalonia.FreeDesktop.DBusIme.IBus;
+using Tmds.DBus;
+
+namespace Avalonia.FreeDesktop.DBusIme
+{
+ public class X11DBusImeHelper
+ {
+ private static readonly Dictionary> KnownMethods =
+ new Dictionary>
+ {
+ ["fcitx"] = conn =>
+ new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)),
+ ["ibus"] = conn =>
+ new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn))
+ };
+
+ static Func DetectInputMethod()
+ {
+ foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" })
+ {
+ var value = Environment.GetEnvironmentVariable(name);
+
+ if (value == "none")
+ return null;
+
+ if (value != null && KnownMethods.TryGetValue(value, out var factory))
+ return factory;
+ }
+
+ return null;
+ }
+
+ public static bool DetectAndRegister()
+ {
+ var factory = DetectInputMethod();
+ if (factory != null)
+ {
+ var conn = DBusHelper.TryInitialize();
+ if (conn != null)
+ {
+ AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn));
+ return true;
+ }
+ }
+
+ return false;
+
+ }
+ }
+}
diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs
index e93ca64d3ad..b5e35db9691 100644
--- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs
+++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs
@@ -192,7 +192,7 @@ object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
{
var (it, menu) = i;
- if (it is NativeMenuItemSeperator)
+ if (it is NativeMenuItemSeparator)
{
if (name == "type")
return "separator";
@@ -223,13 +223,13 @@ object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
return null;
var lst = new List();
var mod = item.Gesture;
- if ((mod.KeyModifiers & KeyModifiers.Control) != 0)
+ if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control))
lst.Add("Control");
- if ((mod.KeyModifiers & KeyModifiers.Alt) != 0)
+ if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
lst.Add("Alt");
- if ((mod.KeyModifiers & KeyModifiers.Shift) != 0)
+ if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
lst.Add("Shift");
- if ((mod.KeyModifiers & KeyModifiers.Meta) != 0)
+ if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
lst.Add("Super");
lst.Add(item.Gesture.Key.ToString());
return new[] { lst.ToArray() };
diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs
new file mode 100644
index 00000000000..5d911189784
--- /dev/null
+++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Threading.Tasks;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+
+namespace Avalonia.FreeDesktop
+{
+ public interface IX11InputMethodFactory
+ {
+ (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid);
+ }
+
+ public struct X11InputMethodForwardedKey
+ {
+ public int KeyVal { get; set; }
+ public KeyModifiers Modifiers { get; set; }
+ public RawKeyEventType Type { get; set; }
+ }
+
+ public interface IX11InputMethodControl : IDisposable
+ {
+ void SetWindowActive(bool active);
+ bool IsEnabled { get; }
+ ValueTask HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode);
+ event Action Commit;
+ event Action ForwardKey;
+
+ void UpdateWindowInfo(PixelPoint position, double scaling);
+ }
+}
diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
index 32f9f99709c..18c149ce2ee 100644
--- a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
+++ b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
@@ -33,11 +33,11 @@ int TranslateButton(VncButton vncButton) =>
{
Window?.MouseMove(pt);
foreach (var btn in CheckedButtons)
- if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn))
+ if (_previousButtons.HasAllFlags(btn) && !buttons.HasAllFlags(btn))
Window?.MouseUp(pt, TranslateButton(btn), modifiers);
foreach (var btn in CheckedButtons)
- if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn))
+ if (!_previousButtons.HasAllFlags(btn) && buttons.HasAllFlags(btn))
Window?.MouseDown(pt, TranslateButton(btn), modifiers);
_previousButtons = buttons;
}, DispatcherPriority.Input);
diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
index 1f750a0309d..fca2a1336f8 100644
--- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
+++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
@@ -58,7 +58,7 @@ internal static void Initialize()
AvaloniaLocator.CurrentMutable
.Bind().ToConstant(new HeadlessPlatformThreadingInterface())
.Bind().ToSingleton()
- .Bind().ToSingleton()
+ .Bind().ToSingleton()
.Bind().ToConstant(new HeadlessPlatformSettingsStub())
.Bind().ToSingleton()
.Bind().ToSingleton()
diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 4f6af0a41ba..268171d4675 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -70,6 +70,28 @@ public IBitmapImpl LoadBitmap(Stream stream)
return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
}
+ public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
+ BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
+ public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
+ {
+ return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
+ }
+
public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
{
return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96));
@@ -90,9 +112,8 @@ public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSiz
return new HeadlessBitmapStub(destinationSize, new Vector(96, 96));
}
- public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun)
{
- width = 100;
return new HeadlessGlyphRunStub();
}
@@ -104,6 +125,9 @@ public HeadlessGeometryStub(Rect bounds)
}
public Rect Bounds { get; set; }
+
+ public double ContourLength { get; } = 0;
+
public virtual bool FillContains(Point point) => Bounds.Contains(point);
public Rect GetRenderBounds(IPen pen)
@@ -126,6 +150,25 @@ public IGeometryImpl Intersect(IGeometryImpl geometry)
public ITransformedGeometryImpl WithTransform(Matrix transform) =>
new HeadlessTransformedGeometryStub(this, transform);
+
+ public bool TryGetPointAtDistance(double distance, out Point point)
+ {
+ point = new Point();
+ return false;
+ }
+
+ public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
+ {
+ point = new Point();
+ tangent = new Point();
+ return false;
+ }
+
+ public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry)
+ {
+ segmentGeometry = null;
+ return false;
+ }
}
class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl
@@ -238,7 +281,7 @@ public void SetFillRule(FillRule fillRule)
}
}
- class HeadlessBitmapStub : IBitmapImpl, IRenderTargetBitmapImpl, IWriteableBitmapImpl
+ class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl
{
public Size Size { get; }
@@ -267,6 +310,13 @@ public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrush
return new HeadlessDrawingContextStub();
}
+ public void Blit(IDrawingContextImpl context)
+ {
+
+ }
+
+ public bool CanBlit => false;
+
public Vector Dpi { get; }
public PixelSize PixelSize { get; }
public int Version { get; set; }
@@ -307,7 +357,7 @@ public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
}
- public IRenderTargetBitmapImpl CreateLayer(Size size)
+ public IDrawingContextLayerImpl CreateLayer(Size size)
{
return new HeadlessBitmapStub(size, new Vector(96, 96));
}
@@ -352,6 +402,16 @@ public void PopGeometryClip()
}
+ public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+ {
+
+ }
+
+ public void PopBitmapBlendMode()
+ {
+
+ }
+
public void Custom(ICustomDrawOperation custom)
{
diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
index 4c0e2982f48..ce4c31e27ea 100644
--- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs
@@ -52,12 +52,14 @@ public async Task GetDataAsync(string format)
}
}
- class HeadlessCursorFactoryStub : IStandardCursorFactory
+ class HeadlessCursorFactoryStub : ICursorFactory
{
+ public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub();
+ public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub();
- public IPlatformHandle GetCursor(StandardCursorType cursorType)
+ private class CursorStub : ICursorImpl
{
- return new PlatformHandle(new IntPtr((int)cursorType), "STUB");
+ public void Dispose() { }
}
}
diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs
index 8f4fa5e3041..31315848d10 100644
--- a/src/Avalonia.Headless/HeadlessWindowImpl.cs
+++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs
@@ -41,12 +41,13 @@ public void Dispose()
}
public Size ClientSize { get; set; }
+ public Size? FrameSize => null;
public double RenderScaling { get; } = 1;
public double DesktopScaling => RenderScaling;
public IEnumerable Surfaces { get; }
public Action Input { get; set; }
public Action Paint { get; set; }
- public Action Resized { get; set; }
+ public Action Resized { get; set; }
public Action ScalingChanged { get; set; }
public IRenderer CreateRenderer(IRenderRoot root)
@@ -67,7 +68,7 @@ public void SetInputRoot(IInputRoot inputRoot)
public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling);
- public void SetCursor(IPlatformHandle cursor)
+ public void SetCursor(ICursorImpl cursor)
{
}
@@ -75,9 +76,10 @@ public void SetCursor(IPlatformHandle cursor)
public Action Closed { get; set; }
public IMouseDevice MouseDevice { get; }
- public void Show()
+ public void Show(bool activate, bool isDialog)
{
- Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input);
+ if (activate)
+ Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input);
}
public void Hide()
@@ -106,7 +108,7 @@ public void Activate()
public Action Activated { get; set; }
public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB");
public Size MaxClientSize { get; } = new Size(1920, 1280);
- public void Resize(Size clientSize)
+ public void Resize(Size clientSize, PlatformResizeReason reason)
{
// Emulate X11 behavior here
if (IsPopup)
@@ -124,7 +126,7 @@ void DoResize(Size clientSize)
if (ClientSize != clientSize)
{
ClientSize = clientSize;
- Resized?.Invoke(clientSize);
+ Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified);
}
}
@@ -146,11 +148,6 @@ public void SetTitle(string title)
}
- public void ShowDialog(IWindowImpl parent)
- {
- Show();
- }
-
public void SetSystemDecorations(bool enabled)
{
diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs
index 660584e2ed8..5c4af68d796 100644
--- a/src/Avalonia.Input/AccessKeyHandler.cs
+++ b/src/Avalonia.Input/AccessKeyHandler.cs
@@ -177,7 +177,7 @@ protected virtual void OnKeyDown(object sender, KeyEventArgs e)
{
bool menuIsOpen = MainMenu?.IsOpen == true;
- if ((e.KeyModifiers & KeyModifiers.Alt) != 0 || menuIsOpen)
+ if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || menuIsOpen)
{
// If any other key is pressed with the Alt key held down, or the main menu is open,
// find all controls who have registered that access key.
diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt
new file mode 100644
index 00000000000..98eb8598d8f
--- /dev/null
+++ b/src/Avalonia.Input/ApiCompatBaseline.txt
@@ -0,0 +1,13 @@
+Compat issues with assembly Avalonia.Input:
+MembersMustExist : Member 'public Avalonia.Platform.IPlatformHandle Avalonia.Input.Cursor.PlatformCursor.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.RightTappedEvent' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.Gestures.TappedEvent' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.DoubleTappedEvent' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.Interactivity.RoutedEvent Avalonia.Interactivity.RoutedEvent Avalonia.Input.InputElement.TappedEvent' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Platform.IStandardCursorFactory' does not exist in the implementation but it does exist in the contract.
+Total Issues: 11
diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj
index c39c81a9658..69a80290d17 100644
--- a/src/Avalonia.Input/Avalonia.Input.csproj
+++ b/src/Avalonia.Input/Avalonia.Input.csproj
@@ -4,6 +4,9 @@
Enable
CS8600;CS8602;CS8603
+
+
+
diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursor.cs
similarity index 56%
rename from src/Avalonia.Input/Cursors.cs
rename to src/Avalonia.Input/Cursor.cs
index 920b598eacb..122838f6823 100644
--- a/src/Avalonia.Input/Cursors.cs
+++ b/src/Avalonia.Input/Cursor.cs
@@ -1,15 +1,11 @@
using System;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
+#nullable enable
+
namespace Avalonia.Input
{
- /*
- =========================================================================================
- NOTE: Cursors are NOT disposable and are cached in platform implementation.
- To support loading custom cursors some measures about that should be taken beforehand
- =========================================================================================
- */
-
public enum StandardCursorType
{
Arrow,
@@ -41,26 +37,33 @@ public enum StandardCursorType
BottomSize = BottomSide
// Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/
- // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image
+ // We might enable them later, preferably, by loading pixmax directly from theme with fallback image
// SizeNorthWestSouthEast,
// SizeNorthEastSouthWest,
}
- public class Cursor
+ public class Cursor : IDisposable
{
public static readonly Cursor Default = new Cursor(StandardCursorType.Arrow);
- internal Cursor(IPlatformHandle platformCursor)
+ internal Cursor(ICursorImpl platformImpl)
{
- PlatformCursor = platformCursor;
+ PlatformImpl = platformImpl;
}
public Cursor(StandardCursorType cursorType)
- : this(GetCursor(cursorType))
+ : this(GetCursorFactory().GetCursor(cursorType))
+ {
+ }
+
+ public Cursor(IBitmap cursor, PixelPoint hotSpot)
+ : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot))
{
}
- public IPlatformHandle PlatformCursor { get; }
+ public ICursorImpl PlatformImpl { get; }
+
+ public void Dispose() => PlatformImpl.Dispose();
public static Cursor Parse(string s)
{
@@ -69,16 +72,10 @@ public static Cursor Parse(string s)
throw new ArgumentException($"Unrecognized cursor type '{s}'.");
}
- private static IPlatformHandle GetCursor(StandardCursorType type)
+ private static ICursorFactory GetCursorFactory()
{
- var platform = AvaloniaLocator.Current.GetService();
-
- if (platform == null)
- {
- throw new Exception("Could not create Cursor: IStandardCursorFactory not registered.");
- }
-
- return platform.GetCursor(type);
+ return AvaloniaLocator.Current.GetService() ??
+ throw new Exception("Could not create Cursor: ICursorFactory not registered.");
}
}
}
diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs
index a1f1478f514..1432092ba13 100644
--- a/src/Avalonia.Input/FocusManager.cs
+++ b/src/Avalonia.Input/FocusManager.cs
@@ -75,7 +75,9 @@ public void Focus(
// If control is null, set focus to the topmost focus scope.
foreach (var scope in GetFocusScopeAncestors(Current).Reverse().ToList())
{
- if (_focusScopes.TryGetValue(scope, out var element) && element != null)
+ if (scope != Scope &&
+ _focusScopes.TryGetValue(scope, out var element) &&
+ element != null)
{
Focus(element, method);
return;
@@ -90,6 +92,17 @@ public void Focus(
}
}
+ public IInputElement? GetFocusedElement(IInputElement e)
+ {
+ if (e is IFocusScope scope)
+ {
+ _focusScopes.TryGetValue(scope, out var result);
+ return result;
+ }
+
+ return null;
+ }
+
///
/// Sets the currently focused element in the specified scope.
///
@@ -149,6 +162,8 @@ public void SetFocusScope(IFocusScope scope)
Focus(e);
}
+ public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope;
+
///
/// Checks if the specified element can be focused.
///
diff --git a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
index 3858cc04f2e..84a26a0cc30 100644
--- a/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
+++ b/src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
@@ -78,7 +78,7 @@ public void PointerPressed(PointerPressedEventArgs e)
// Arbitrary chosen value, probably need to move that to platform settings or something
private const double ScrollStartDistance = 30;
- // Pixels per second speed that is considered to be the stop of inertiall scroll
+ // Pixels per second speed that is considered to be the stop of inertial scroll
private const double InertialScrollSpeedEnd = 5;
public void PointerMoved(PointerEventArgs e)
diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs
index 1be2595ebe6..f2cc9e9072a 100644
--- a/src/Avalonia.Input/Gestures.cs
+++ b/src/Avalonia.Input/Gestures.cs
@@ -6,17 +6,17 @@ namespace Avalonia.Input
{
public static class Gestures
{
- public static readonly RoutedEvent TappedEvent = RoutedEvent.Register(
+ public static readonly RoutedEvent TappedEvent = RoutedEvent.Register(
"Tapped",
RoutingStrategies.Bubble,
typeof(Gestures));
- public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register(
+ public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register(
"DoubleTapped",
RoutingStrategies.Bubble,
typeof(Gestures));
- public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register(
+ public static readonly RoutedEvent RightTappedEvent = RoutedEvent.Register(
"RightTapped",
RoutingStrategies.Bubble,
typeof(Gestures));
@@ -24,7 +24,7 @@ public static class Gestures
public static readonly RoutedEvent ScrollGestureEvent =
RoutedEvent.Register(
"ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
-
+
public static readonly RoutedEvent ScrollGestureEndedEvent =
RoutedEvent.Register(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
@@ -89,7 +89,7 @@ private static void PointerPressed(RoutedEventArgs ev)
{
if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
{
- e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
+ e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e));
}
}
}
@@ -105,8 +105,14 @@ private static void PointerReleased(RoutedEventArgs ev)
{
if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right)
{
- var et = e.InitialPressMouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent;
- e.Source.RaiseEvent(new RoutedEventArgs(et));
+ if (e.InitialPressMouseButton == MouseButton.Right)
+ {
+ e.Source.RaiseEvent(new TappedEventArgs(RightTappedEvent, e));
+ }
+ else
+ {
+ e.Source.RaiseEvent(new TappedEventArgs(TappedEvent, e));
+ }
}
}
}
diff --git a/src/Avalonia.Input/ICommandSource.cs b/src/Avalonia.Input/ICommandSource.cs
new file mode 100644
index 00000000000..eed71759d5d
--- /dev/null
+++ b/src/Avalonia.Input/ICommandSource.cs
@@ -0,0 +1,36 @@
+using System.Windows.Input;
+
+namespace Avalonia.Input
+{
+ ///
+ /// An interface for classes that know how to invoke a Command.
+ ///
+ public interface ICommandSource
+ {
+ ///
+ /// The command that will be executed when the class is "invoked."
+ /// Classes that implement this interface should enable or disable based on the command's CanExecute return value.
+ /// The property may be implemented as read-write if desired.
+ ///
+ ICommand Command { get; }
+
+ ///
+ /// The parameter that will be passed to the command when executing the command.
+ /// The property may be implemented as read-write if desired.
+ ///
+ object CommandParameter { get; }
+
+
+ ///
+ /// Bor the behavior CanExecuteChanged
+ ///
+ ///
+ ///
+ void CanExecuteChanged(object sender, System.EventArgs e);
+
+ ///
+ /// Gets a value indicating whether this control and all its parents are enabled.
+ ///
+ bool IsEffectivelyEnabled { get; }
+ }
+}
diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
index 3d2927c6328..357395c42f8 100644
--- a/src/Avalonia.Input/ICustomKeyboardNavigation.cs
+++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
@@ -1,4 +1,5 @@
-
+#nullable enable
+
namespace Avalonia.Input
{
///
@@ -6,6 +7,18 @@ namespace Avalonia.Input
///
public interface ICustomKeyboardNavigation
{
- (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction);
+ ///
+ /// Gets the next element in the specified navigation direction.
+ ///
+ /// The element being navigated from.
+ /// The navigation direction.
+ ///
+ /// A tuple consisting of:
+ /// - A boolean indicating whether the request was handled. If false is returned then
+ /// custom navigation will be ignored and default navigation will take place.
+ /// - If handled is true: the next element in the navigation direction, or null if default
+ /// navigation should continue outside the element.
+ ///
+ (bool handled, IInputElement? next) GetNext(IInputElement element, NavigationDirection direction);
}
}
diff --git a/src/Avalonia.Input/IInputElement.cs b/src/Avalonia.Input/IInputElement.cs
index 12fec82368d..2245ff9986c 100644
--- a/src/Avalonia.Input/IInputElement.cs
+++ b/src/Avalonia.Input/IInputElement.cs
@@ -3,6 +3,8 @@
using Avalonia.Interactivity;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Input
{
///
@@ -89,6 +91,11 @@ public interface IInputElement : IInteractive, IVisual
/// value of this control and its parent controls.
///
bool IsEffectivelyEnabled { get; }
+
+ ///
+ /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
+ ///
+ bool IsKeyboardFocusWithin { get; }
///
/// Gets a value indicating whether the control is focused.
diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs
index 9ace7fd92de..63080e74e4f 100644
--- a/src/Avalonia.Input/InputElement.cs
+++ b/src/Avalonia.Input/InputElement.cs
@@ -5,9 +5,12 @@
using Avalonia.Controls.Metadata;
using Avalonia.Data;
using Avalonia.Input.GestureRecognizers;
+using Avalonia.Input.TextInput;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Input
{
///
@@ -42,6 +45,14 @@ public class InputElement : Interactive, IInputElement
public static readonly StyledProperty CursorProperty =
AvaloniaProperty.Register(nameof(Cursor), null, true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty IsKeyboardFocusWithinProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(IsKeyboardFocusWithin),
+ o => o.IsKeyboardFocusWithin);
+
///
/// Defines the property.
///
@@ -60,6 +71,12 @@ public class InputElement : Interactive, IInputElement
public static readonly DirectProperty IsPointerOverProperty =
AvaloniaProperty.RegisterDirect(nameof(IsPointerOver), o => o.IsPointerOver);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsTabStopProperty =
+ KeyboardNavigation.IsTabStopProperty.AddOwner();
+
///
/// Defines the event.
///
@@ -88,6 +105,12 @@ public class InputElement : Interactive, IInputElement
"KeyUp",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty TabIndexProperty =
+ KeyboardNavigation.TabIndexProperty.AddOwner();
+
///
/// Defines the event.
///
@@ -95,6 +118,22 @@ public class InputElement : Interactive, IInputElement
RoutedEvent.Register(
"TextInput",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent TextInputMethodClientRequestedEvent =
+ RoutedEvent.Register(
+ "TextInputMethodClientRequested",
+ RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent TextInputOptionsQueryEvent =
+ RoutedEvent.Register(
+ "TextInputOptionsQuery",
+ RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
///
/// Defines the event.
@@ -151,15 +190,16 @@ public class InputElement : Interactive, IInputElement
///
/// Defines the event.
///
- public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent;
+ public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent;
///
/// Defines the event.
///
- public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent;
+ public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
+ private bool _isKeyboardFocusWithin;
private bool _isFocusVisible;
private bool _isPointerOver;
private GestureRecognizerCollection? _gestureRecognizers;
@@ -234,6 +274,24 @@ public event EventHandler TextInput
add { AddHandler(TextInputEvent, value); }
remove { RemoveHandler(TextInputEvent, value); }
}
+
+ ///
+ /// Occurs when an input element gains input focus and input method is looking for the corresponding client
+ ///
+ public event EventHandler TextInputMethodClientRequested
+ {
+ add { AddHandler(TextInputMethodClientRequestedEvent, value); }
+ remove { RemoveHandler(TextInputMethodClientRequestedEvent, value); }
+ }
+
+ ///
+ /// Occurs when an input element gains input focus and input method is asking for required content options
+ ///
+ public event EventHandler TextInputOptionsQuery
+ {
+ add { AddHandler(TextInputOptionsQueryEvent, value); }
+ remove { RemoveHandler(TextInputOptionsQueryEvent, value); }
+ }
///
/// Occurs when the pointer enters the control.
@@ -302,7 +360,7 @@ public event EventHandler PointerWheelChanged
///
/// Occurs when a tap gesture occurs on the control.
///
- public event EventHandler Tapped
+ public event EventHandler Tapped
{
add { AddHandler(TappedEvent, value); }
remove { RemoveHandler(TappedEvent, value); }
@@ -311,7 +369,7 @@ public event EventHandler Tapped
///
/// Occurs when a double-tap gesture occurs on the control.
///
- public event EventHandler DoubleTapped
+ public event EventHandler DoubleTapped
{
add { AddHandler(DoubleTappedEvent, value); }
remove { RemoveHandler(DoubleTappedEvent, value); }
@@ -343,6 +401,15 @@ public Cursor? Cursor
get { return GetValue(CursorProperty); }
set { SetValue(CursorProperty, value); }
}
+
+ ///
+ /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
+ ///
+ public bool IsKeyboardFocusWithin
+ {
+ get => _isKeyboardFocusWithin;
+ internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value);
+ }
///
/// Gets a value indicating whether the control is focused.
@@ -371,6 +438,15 @@ public bool IsPointerOver
internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); }
}
+ ///
+ /// Gets or sets a value that indicates whether the control is included in tab navigation.
+ ///
+ public bool IsTabStop
+ {
+ get => GetValue(IsTabStopProperty);
+ set => SetValue(IsTabStopProperty, value);
+ }
+
///
public bool IsEffectivelyEnabled
{
@@ -382,6 +458,16 @@ private set
}
}
+ ///
+ /// Gets or sets a value that determines the order in which elements receive focus when the
+ /// user navigates through controls by pressing the Tab key.
+ ///
+ public int TabIndex
+ {
+ get => GetValue(TabIndexProperty);
+ set => SetValue(TabIndexProperty, value);
+ }
+
public List KeyBindings { get; } = new List();
///
@@ -544,6 +630,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs