diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf
index 741570061b..1d182b1357 100644
--- a/Avalonia.Desktop.slnf
+++ b/Avalonia.Desktop.slnf
@@ -8,9 +8,9 @@
"samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
+ "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
- "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
@@ -42,6 +42,7 @@
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
+ "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
diff --git a/Avalonia.sln b/Avalonia.sln
index 525e01c891..1e8ee85ffb 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -231,7 +231,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
EndProject
@@ -560,6 +567,10 @@ Global
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -626,6 +637,7 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props
index 14e4f6a563..7d021d051f 100644
--- a/build/DevAnalyzers.props
+++ b/build/DevAnalyzers.props
@@ -5,5 +5,10 @@
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
+
diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm
index d3b7b4ede6..23abf1d53f 100644
--- a/native/Avalonia.Native/src/OSX/AvnWindow.mm
+++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm
@@ -223,6 +223,19 @@
}
}
+// From chromium:
+//
+// > The delegate or the window class should implement this method so that
+// > -[NSWindow isZoomed] can be then determined by whether or not the current
+// > window frame is equal to the zoomed frame.
+//
+// If we don't implement this, then isZoomed always returns true for a non-
+// resizable window ¯\_(ツ)_/¯
+- (NSRect)windowWillUseStandardFrame:(NSWindow*)window
+ defaultFrame:(NSRect)newFrame {
+ return newFrame;
+}
+
-(BOOL)canBecomeKeyWindow
{
if(_canBecomeKeyWindow)
@@ -261,10 +274,6 @@
-(void) setEnabled:(bool)enable
{
_isEnabled = enable;
-
- [[self standardWindowButton:NSWindowCloseButton] setEnabled:enable];
- [[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable];
- [[self standardWindowButton:NSWindowZoomButton] setEnabled:enable];
}
-(void)becomeKeyWindow
diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm
index 9820a9f052..972d03d08c 100644
--- a/native/Avalonia.Native/src/OSX/PopupImpl.mm
+++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm
@@ -29,7 +29,7 @@ private:
[Window setLevel:NSPopUpMenuWindowLevel];
}
protected:
- virtual NSWindowStyleMask GetStyle() override
+ virtual NSWindowStyleMask CalculateStyleMask() override
{
return NSWindowStyleMaskBorderless;
}
diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h
index 4c2758f6c6..93decef136 100644
--- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h
+++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h
@@ -105,9 +105,8 @@ BEGIN_INTERFACE_MAP()
virtual void BringToFront ();
protected:
- virtual NSWindowStyleMask GetStyle();
-
- void UpdateStyle();
+ virtual NSWindowStyleMask CalculateStyleMask() = 0;
+ virtual void UpdateStyle();
private:
void CreateNSWindow (bool isDialog);
diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
index 038e9a048c..59102e15a6 100644
--- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
@@ -35,18 +35,14 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl,
lastSize = NSSize { 100, 100 };
lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX};
lastMinSize = NSSize { 0, 0 };
-
lastMenu = nullptr;
CreateNSWindow(usePanel);
[Window setContentView:StandardContainer];
- [Window setStyleMask:NSWindowStyleMaskBorderless];
[Window setBackingType:NSBackingStoreBuffered];
-
[Window setContentMinSize:lastMinSize];
[Window setContentMaxSize:lastMaxSize];
-
[Window setOpaque:false];
}
@@ -564,12 +560,8 @@ bool WindowBaseImpl::IsModal() {
return false;
}
-NSWindowStyleMask WindowBaseImpl::GetStyle() {
- return NSWindowStyleMaskBorderless;
-}
-
void WindowBaseImpl::UpdateStyle() {
- [Window setStyleMask:GetStyle()];
+ [Window setStyleMask:CalculateStyleMask()];
}
void WindowBaseImpl::CleanNSWindow() {
@@ -580,21 +572,12 @@ void WindowBaseImpl::CleanNSWindow() {
}
}
-void WindowBaseImpl::CreateNSWindow(bool isDialog) {
- if (isDialog) {
- if (![Window isKindOfClass:[AvnPanel class]]) {
- CleanNSWindow();
-
- Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()];
-
- [Window setHidesOnDeactivate:false];
- }
+void WindowBaseImpl::CreateNSWindow(bool usePanel) {
+ if (usePanel) {
+ Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless];
+ [Window setHidesOnDeactivate:false];
} else {
- if (![Window isKindOfClass:[AvnWindow class]]) {
- CleanNSWindow();
-
- Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()];
- }
+ Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless];
}
}
diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h
index 3861aaf170..29bb659039 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.h
+++ b/native/Avalonia.Native/src/OSX/WindowImpl.h
@@ -41,8 +41,6 @@ BEGIN_INTERFACE_MAP()
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl);
- void HideOrShowTrafficLights ();
-
virtual HRESULT Show (bool activate, bool isDialog) override;
virtual HRESULT SetEnabled (bool enable) override;
@@ -100,9 +98,11 @@ BEGIN_INTERFACE_MAP()
bool CanBecomeKeyWindow ();
protected:
- virtual NSWindowStyleMask GetStyle() override;
+ virtual NSWindowStyleMask CalculateStyleMask() override;
+ void UpdateStyle () override;
private:
+ void ZOrderChildWindows();
void OnInitialiseNSWindow();
NSString *_lastTitle;
};
diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm
index ce82f7d83f..cf1ee6943d 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm
@@ -30,19 +30,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase
OnInitialiseNSWindow();
}
-void WindowImpl::HideOrShowTrafficLights() {
- if (Window == nil) {
- return;
- }
-
- bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
- bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
-
- [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights];
- [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights];
- [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights];
-}
-
void WindowImpl::OnInitialiseNSWindow(){
[GetWindowProtocol() setCanBecomeKeyWindow:true];
@@ -67,8 +54,6 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
WindowBaseImpl::Show(activate, isDialog);
GetWindowState(&_actualWindowState);
- HideOrShowTrafficLights();
-
return SetWindowState(_lastWindowState);
}
}
@@ -134,14 +119,19 @@ void WindowImpl::BringToFront()
}
[Window invalidateShadow];
+ ZOrderChildWindows();
+ }
+}
+
+void WindowImpl::ZOrderChildWindows()
+{
+ for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
+ {
+ auto window = (*iterator)->Window;
- for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
- {
- auto window = (*iterator)->Window;
-
- // #9565: Only bring window to front if it's on the currently active space
- if ([window isOnActiveSpace])
- (*iterator)->BringToFront();
+ // #9565: Only bring window to front if it's on the currently active space
+ if ([window isOnActiveSpace]) {
+ (*iterator)->BringToFront();
}
}
}
@@ -161,13 +151,15 @@ bool WindowImpl::CanBecomeKeyWindow()
void WindowImpl::StartStateTransition() {
_transitioningWindowState = true;
+ UpdateStyle();
}
void WindowImpl::EndStateTransition() {
_transitioningWindowState = false;
-
+ UpdateStyle();
+
// Ensure correct order of child windows after fullscreen transition.
- BringToFront();
+ ZOrderChildWindows();
}
SystemDecorations WindowImpl::Decorations() {
@@ -225,16 +217,12 @@ bool WindowImpl::IsZoomed() {
}
void WindowImpl::DoZoom() {
- switch (_decorations) {
- case SystemDecorationsNone:
- case SystemDecorationsBorderOnly:
- [Window setFrame:[Window screen].visibleFrame display:true];
- break;
-
-
- case SystemDecorationsFull:
- [Window performZoom:Window];
- break;
+ if (_decorations == SystemDecorationsNone ||
+ _decorations == SystemDecorationsBorderOnly ||
+ _canResize == false) {
+ [Window setFrame:[Window screen].visibleFrame display:true];
+ } else {
+ [Window performZoom:Window];
}
}
@@ -261,8 +249,6 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
UpdateStyle();
- HideOrShowTrafficLights();
-
switch (_decorations) {
case SystemDecorationsNone:
[Window setHasShadow:NO];
@@ -419,9 +405,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) {
}
[GetWindowProtocol() setIsExtended:enable];
-
- HideOrShowTrafficLights();
-
UpdateStyle();
}
@@ -577,14 +560,16 @@ bool WindowImpl::IsOwned() {
return _parent != nullptr;
}
-NSWindowStyleMask WindowImpl::GetStyle() {
- unsigned long s = NSWindowStyleMaskBorderless;
+NSWindowStyleMask WindowImpl::CalculateStyleMask() {
+ // Use the current style mask and only clear the flags we're going to be modifying.
+ unsigned long s = [Window styleMask] &
+ ~(NSWindowStyleMaskFullSizeContentView |
+ NSWindowStyleMaskTitled |
+ NSWindowStyleMaskClosable |
+ NSWindowStyleMaskResizable |
+ NSWindowStyleMaskMiniaturizable |
+ NSWindowStyleMaskTexturedBackground);
- if(_actualWindowState == FullScreen)
- {
- s |= NSWindowStyleMaskFullScreen;
- }
-
switch (_decorations) {
case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView;
@@ -597,7 +582,7 @@ NSWindowStyleMask WindowImpl::GetStyle() {
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
- if (_canResize && _isEnabled) {
+ if ((_canResize && _isEnabled) || _transitioningWindowState) {
s = s | NSWindowStyleMaskResizable;
}
break;
@@ -612,3 +597,25 @@ NSWindowStyleMask WindowImpl::GetStyle() {
}
return s;
}
+
+void WindowImpl::UpdateStyle() {
+ WindowBaseImpl::UpdateStyle();
+
+ if (Window == nil) {
+ return;
+ }
+
+ bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
+ bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
+
+ NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
+ NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];
+ NSButton* zoomButton = [Window standardWindowButton:NSWindowZoomButton];
+
+ [closeButton setHidden:!hasTrafficLights];
+ [closeButton setEnabled:_isEnabled];
+ [miniaturizeButton setHidden:!hasTrafficLights];
+ [miniaturizeButton setEnabled:_isEnabled];
+ [zoomButton setHidden:!hasTrafficLights];
+ [zoomButton setEnabled:_isEnabled && _canResize];
+}
diff --git a/samples/ControlCatalog.Browser.Blazor/App.razor.cs b/samples/ControlCatalog.Browser.Blazor/App.razor.cs
index f38db2b055..c331625664 100644
--- a/samples/ControlCatalog.Browser.Blazor/App.razor.cs
+++ b/samples/ControlCatalog.Browser.Blazor/App.razor.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser.Blazor;
@@ -5,13 +7,4 @@ namespace ControlCatalog.Browser.Blazor;
public partial class App
{
- protected override void OnParametersSet()
- {
- AppBuilder.Configure()
- .UseBlazor()
- // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
- .SetupWithSingleViewLifetime();
-
- base.OnParametersSet();
- }
}
diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs
index eb99ca518e..e68e9b14d9 100644
--- a/samples/ControlCatalog.Browser.Blazor/Program.cs
+++ b/samples/ControlCatalog.Browser.Blazor/Program.cs
@@ -1,6 +1,8 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Browser.Blazor;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using ControlCatalog.Browser.Blazor;
@@ -9,9 +11,17 @@ public class Program
{
public static async Task Main(string[] args)
{
- await CreateHostBuilder(args).Build().RunAsync();
+ var host = CreateHostBuilder(args).Build();
+ await StartAvaloniaApp();
+ await host.RunAsync();
}
+ public static async Task StartAvaloniaApp()
+ {
+ await AppBuilder.Configure()
+ .StartBlazorAppAsync();
+ }
+
public static WebAssemblyHostBuilder CreateHostBuilder(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs
index 53b7c60a6f..e1a4500173 100644
--- a/samples/ControlCatalog.Browser/Program.cs
+++ b/samples/ControlCatalog.Browser/Program.cs
@@ -1,6 +1,8 @@
using System.Runtime.Versioning;
+using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser;
+using Avalonia.Controls;
using ControlCatalog;
using ControlCatalog.Browser;
@@ -8,15 +10,27 @@ using ControlCatalog.Browser;
internal partial class Program
{
- private static void Main(string[] args)
+ public static async Task Main(string[] args)
{
- BuildAvaloniaApp()
+ await BuildAvaloniaApp()
.AfterSetup(_ =>
{
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
- }).SetupBrowserApp("out");
+ })
+ .StartBrowserAppAsync("out");
}
+ // Example without a ISingleViewApplicationLifetime
+ // private static AvaloniaView _avaloniaView;
+ // public static async Task Main(string[] args)
+ // {
+ // await BuildAvaloniaApp()
+ // .SetupBrowserApp();
+ //
+ // _avaloniaView = new AvaloniaView("out");
+ // _avaloniaView.Content = new TextBlock { Text = "Hello world" };
+ // }
+
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure();
}
diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/main.js
index 87f8a4f943..9d90db8bd2 100644
--- a/samples/ControlCatalog.Browser/main.js
+++ b/samples/ControlCatalog.Browser/main.js
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './dotnet.js'
-import { registerAvaloniaModule } from './avalonia.js';
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
@@ -12,8 +11,6 @@ const dotnetRuntime = await dotnet
.withApplicationArgumentsFromQuery()
.create();
-await registerAvaloniaModule(dotnetRuntime);
-
const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]);
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index f7b020678d..e24860e3e1 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
@@ -40,7 +40,7 @@ namespace ControlCatalog.Pages
if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum))
{
- lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum);
+ lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum);
}
else
{
@@ -51,7 +51,7 @@ namespace ControlCatalog.Pages
if (folderLink is not null)
{
- lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink);
+ lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink);
}
}
};
@@ -82,7 +82,13 @@ namespace ControlCatalog.Pages
return new List
{
FilePickerFileTypes.All,
- FilePickerFileTypes.TextPlain
+ FilePickerFileTypes.TextPlain,
+ new("Binary Log")
+ {
+ Patterns = new[] { "*.binlog", "*.buildlog" },
+ MimeTypes = new[] { "application/binlog", "application/buildlog" },
+ AppleUniformTypeIdentifiers = new []{ "public.data" }
+ }
};
}
@@ -142,7 +148,7 @@ namespace ControlCatalog.Pages
}
else
{
- SetFolder(await GetStorageProvider().TryGetFolderFromPath(result));
+ SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result));
results.Items = new[] { result };
resultsVisible.IsVisible = true;
}
@@ -223,7 +229,7 @@ namespace ControlCatalog.Pages
ShowOverwritePrompt = false
});
- if (file is not null && file.CanOpenWrite)
+ if (file is not null)
{
// Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER
@@ -275,7 +281,7 @@ namespace ControlCatalog.Pages
{
ignoreTextChanged = true;
lastSelectedDirectory = folder;
- currentFolderBox.Text = folder?.Path.LocalPath;
+ currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString();
ignoreTextChanged = false;
}
async Task SetPickerResult(IReadOnlyCollection? items)
@@ -298,31 +304,26 @@ namespace ControlCatalog.Pages
if (item is IStorageFile file)
{
resultText += @$"
- CanOpenRead: {file.CanOpenRead}
- CanOpenWrite: {file.CanOpenWrite}
Content:
";
- if (file.CanOpenRead)
- {
#if NET6_0_OR_GREATER
- await using var stream = await file.OpenReadAsync();
+ await using var stream = await file.OpenReadAsync();
#else
- using var stream = await file.OpenReadAsync();
+ using var stream = await file.OpenReadAsync();
#endif
- using var reader = new System.IO.StreamReader(stream);
+ using var reader = new System.IO.StreamReader(stream);
- // 4GB file test, shouldn't load more than 10000 chars into a memory.
- const int length = 10000;
- var buffer = ArrayPool.Shared.Rent(length);
- try
- {
- var charsRead = await reader.ReadAsync(buffer, 0, length);
- resultText += new string(buffer, 0, charsRead);
- }
- finally
- {
- ArrayPool.Shared.Return(buffer);
- }
+ // 4GB file test, shouldn't load more than 10000 chars into a memory.
+ const int length = 10000;
+ var buffer = ArrayPool.Shared.Rent(length);
+ try
+ {
+ var charsRead = await reader.ReadAsync(buffer, 0, length);
+ resultText += new string(buffer, 0, charsRead);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
}
}
diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props
index 3b14f0ce12..ac78d9c739 100644
--- a/samples/Directory.Build.props
+++ b/samples/Directory.Build.props
@@ -6,4 +6,5 @@
11
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index 4e5a8463df..353e01dca7 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -140,6 +140,7 @@
Maximized
FullScreen
+ Can Resize
@@ -152,6 +153,9 @@
+
+
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs
index 3cd5350cce..087f25666b 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml.cs
+++ b/samples/IntegrationTestApp/MainWindow.axaml.cs
@@ -66,11 +66,13 @@ namespace IntegrationTestApp
var locationComboBox = this.GetControl("ShowWindowLocation");
var stateComboBox = this.GetControl("ShowWindowState");
var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
+ var canResizeCheckBox = this.GetControl("ShowWindowCanResize");
var owner = (Window)this.GetVisualRoot()!;
var window = new ShowWindowTest
{
WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex,
+ CanResize = canResizeCheckBox.IsChecked.Value,
};
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
diff --git a/samples/MobileSandbox/MainView.xaml b/samples/MobileSandbox/MainView.xaml
index 1eab13aa75..5d35ec3fec 100644
--- a/samples/MobileSandbox/MainView.xaml
+++ b/samples/MobileSandbox/MainView.xaml
@@ -5,8 +5,8 @@
x:DataType="mobileSandbox:MainView">
-
-
+
+
diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs
index df0c494648..27dcfe8645 100644
--- a/src/Android/Avalonia.Android/AndroidInputMethod.cs
+++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs
@@ -5,8 +5,10 @@ using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
+using Avalonia.Reactive;
namespace Avalonia.Android
{
@@ -68,23 +70,10 @@ namespace Avalonia.Android
public void SetClient(ITextInputMethodClient client)
{
- if (_client != null)
- {
- _client.SurroundingTextChanged -= SurroundingTextChanged;
- }
-
- if(_inputConnection != null)
- {
- _inputConnection.ComposingText = null;
- _inputConnection.ComposingRegion = default;
- }
-
_client = client;
if (IsActive)
{
- _client.SurroundingTextChanged += SurroundingTextChanged;
-
_host.RequestFocus();
_imm.RestartInput(View);
@@ -101,24 +90,6 @@ namespace Avalonia.Android
}
}
- private void SurroundingTextChanged(object sender, EventArgs e)
- {
- if (IsActive && _inputConnection != null)
- {
- var surroundingText = Client.SurroundingText;
-
- _inputConnection.SurroundingText = surroundingText;
-
- _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset);
-
- if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset)
- {
- _inputConnection.CommitText(_inputConnection.ComposingText, 0);
- _inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset);
- }
- }
- }
-
public void SetCursorRect(Rect rect)
{
@@ -157,11 +128,14 @@ namespace Avalonia.Android
TextInputReturnKeyType.Search => (ImeFlags)CustomImeFlags.ActionSearch,
TextInputReturnKeyType.Next => (ImeFlags)CustomImeFlags.ActionNext,
TextInputReturnKeyType.Previous => (ImeFlags)CustomImeFlags.ActionPrevious,
- _ => (ImeFlags)CustomImeFlags.ActionDone
+ TextInputReturnKeyType.Done => (ImeFlags)CustomImeFlags.ActionDone,
+ _ => options.Multiline ? ImeFlags.NoEnterAction : (ImeFlags)CustomImeFlags.ActionDone
};
outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi;
+ _client.TextEditable = _inputConnection.InputEditable;
+
return _inputConnection;
});
}
diff --git a/src/Android/Avalonia.Android/InputEditable.cs b/src/Android/Avalonia.Android/InputEditable.cs
new file mode 100644
index 0000000000..c5b68d2652
--- /dev/null
+++ b/src/Android/Avalonia.Android/InputEditable.cs
@@ -0,0 +1,127 @@
+using System;
+using Android.Runtime;
+using Android.Text;
+using Android.Views;
+using Android.Views.InputMethods;
+using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Controls.Presenters;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
+using Java.Lang;
+using static System.Net.Mime.MediaTypeNames;
+
+namespace Avalonia.Android
+{
+ internal class InputEditable : SpannableStringBuilder, ITextEditable
+ {
+ private readonly TopLevelImpl _topLevel;
+ private readonly IAndroidInputMethod _inputMethod;
+ private readonly AvaloniaInputConnection _avaloniaInputConnection;
+ private int _currentBatchLevel;
+ private string _previousText;
+ private int _previousSelectionStart;
+ private int _previousSelectionEnd;
+
+ public event EventHandler TextChanged;
+ public event EventHandler SelectionChanged;
+ public event EventHandler CompositionChanged;
+
+ public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod, AvaloniaInputConnection avaloniaInputConnection)
+ {
+ _topLevel = topLevel;
+ _inputMethod = inputMethod;
+ _avaloniaInputConnection = avaloniaInputConnection;
+ }
+
+ public InputEditable(ICharSequence text) : base(text)
+ {
+ }
+
+ public InputEditable(string text) : base(text)
+ {
+ }
+
+ public InputEditable(ICharSequence text, int start, int end) : base(text, start, end)
+ {
+ }
+
+ public InputEditable(string text, int start, int end) : base(text, start, end)
+ {
+ }
+
+ protected InputEditable(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
+ {
+ }
+
+ public int SelectionStart
+ {
+ get => Selection.GetSelectionStart(this); set
+ {
+ var end = SelectionEnd < 0 ? 0 : SelectionEnd;
+ _avaloniaInputConnection.SetSelection(value, end);
+ _inputMethod.IMM.UpdateSelection(_topLevel.View, value, end, value, end);
+ }
+ }
+ public int SelectionEnd
+ {
+ get => Selection.GetSelectionEnd(this); set
+ {
+ var start = SelectionStart < 0 ? 0 : SelectionStart;
+ _avaloniaInputConnection.SetSelection(start, value);
+ _inputMethod.IMM.UpdateSelection(_topLevel.View, start, value, start, value);
+ }
+ }
+
+ public string? Text
+ {
+ get => ToString(); set
+ {
+ if (Text != value)
+ {
+ Clear();
+ Insert(0, value ?? "");
+ }
+ }
+ }
+
+ public int CompositionStart => BaseInputConnection.GetComposingSpanStart(this);
+
+ public int CompositionEnd => BaseInputConnection.GetComposingSpanEnd(this);
+
+ public void BeginBatchEdit()
+ {
+ _currentBatchLevel++;
+
+ if (_currentBatchLevel == 1)
+ {
+ _previousText = ToString();
+ _previousSelectionStart = SelectionStart;
+ _previousSelectionEnd = SelectionEnd;
+ }
+ }
+
+ public void EndBatchEdit()
+ {
+ if (_currentBatchLevel == 1)
+ {
+ if(_previousText != Text)
+ {
+ TextChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ if (_previousSelectionStart != SelectionStart || _previousSelectionEnd != SelectionEnd)
+ {
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ _currentBatchLevel--;
+ }
+
+ public void RaiseCompositionChanged()
+ {
+ CompositionChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
index 3e2cf12801..2d547201e9 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -22,6 +22,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
+using Java.Util;
using Math = System.Math;
using AndroidRect = Android.Graphics.Rect;
using Window = Android.Views.Window;
@@ -414,159 +415,73 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private readonly TopLevelImpl _topLevel;
private readonly IAndroidInputMethod _inputMethod;
+ private readonly InputEditable _editable;
public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
{
_topLevel = topLevel;
_inputMethod = inputMethod;
+ _editable = new InputEditable(_topLevel, _inputMethod, this);
}
- public TextInputMethodSurroundingText SurroundingText { get; set; }
+ public override IEditable Editable => _editable;
- public string ComposingText { get; internal set; }
-
- public ComposingRegion? ComposingRegion { get; internal set; }
-
- public bool IsComposing => !string.IsNullOrEmpty(ComposingText);
- public bool IsCommiting { get; private set; }
+ internal InputEditable InputEditable => _editable;
public override bool SetComposingRegion(int start, int end)
{
- //System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}");
+ var ret = base.SetComposingRegion(start, end);
- ComposingRegion = new ComposingRegion(start, end);
+ InputEditable.RaiseCompositionChanged();
- return base.SetComposingRegion(start, end);
+ return ret;
}
public override bool SetComposingText(ICharSequence text, int newCursorPosition)
{
var composingText = text.ToString();
- ComposingText = composingText;
-
- _inputMethod.Client?.SetPreeditText(ComposingText);
-
- return base.SetComposingText(text, newCursorPosition);
- }
-
- public override bool FinishComposingText()
- {
- if (!string.IsNullOrEmpty(ComposingText))
+ if (string.IsNullOrEmpty(composingText))
{
- CommitText(ComposingText, ComposingText.Length);
+ return CommitText(text, newCursorPosition);
}
else
{
- ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset);
- }
-
- return base.FinishComposingText();
- }
-
- public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
- {
- if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0)
- {
- var start = System.Math.Max(SurroundingText.CursorOffset - length, 0);
+ var ret = base.SetComposingText(text, newCursorPosition);
- var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset);
+ InputEditable.RaiseCompositionChanged();
- var text = SurroundingText.Text.Substring(start, end - start);
-
- //System.Diagnostics.Debug.WriteLine($"Text Before: {text}");
-
- return new Java.Lang.String(text);
+ return ret;
}
-
- return null;
}
- public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags)
+ public override bool BeginBatchEdit()
{
- if (!string.IsNullOrEmpty(SurroundingText.Text))
- {
- var start = SurroundingText.CursorOffset;
-
- var end = System.Math.Min(start + length, SurroundingText.Text.Length);
-
- var text = SurroundingText.Text.Substring(start, end - start);
+ _editable.BeginBatchEdit();
- //System.Diagnostics.Debug.WriteLine($"Text After: {text}");
-
- return new Java.Lang.String(text);
- }
-
- return null;
+ return base.BeginBatchEdit();
}
- public override bool CommitText(ICharSequence text, int newCursorPosition)
+ public override bool EndBatchEdit()
{
- IsCommiting = true;
- var committedText = text.ToString();
-
- _inputMethod.Client.SetPreeditText(null);
+ var ret = base.EndBatchEdit();
+ _editable.EndBatchEdit();
- int? start, end;
-
- if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset)
- {
- start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
- end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset);
- }
- else if (ComposingRegion != null)
- {
- start = ComposingRegion?.Start;
- end = ComposingRegion?.End;
-
- ComposingRegion = null;
- }
- else
- {
- start = end = _inputMethod.Client.SurroundingText.CursorOffset;
- }
-
- _inputMethod.Client.SelectInSurroundingText((int)start, (int)end);
-
- var time = DateTime.Now.TimeOfDay;
-
- var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText);
-
- _topLevel.Input(rawTextEvent);
-
- ComposingText = null;
-
- ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition);
-
- return base.CommitText(text, newCursorPosition);
+ return ret;
}
- public override bool DeleteSurroundingText(int beforeLength, int afterLength)
+ public override bool FinishComposingText()
{
- var surroundingText = _inputMethod.Client.SurroundingText;
-
- var selectionStart = surroundingText.CursorOffset;
-
- _inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength);
-
- _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
-
- surroundingText = _inputMethod.Client.SurroundingText;
-
- selectionStart = surroundingText.CursorOffset;
-
- ComposingRegion = new ComposingRegion(selectionStart, selectionStart);
-
- return base.DeleteSurroundingText(beforeLength, afterLength);
+ var ret = base.FinishComposingText();
+ InputEditable.RaiseCompositionChanged();
+ return ret;
}
- public override bool SetSelection(int start, int end)
+ public override bool CommitText(ICharSequence text, int newCursorPosition)
{
- _inputMethod.Client.SelectInSurroundingText(start, end);
-
- ComposingRegion = new ComposingRegion(start, end);
-
- return base.SetSelection(start, end);
+ var ret = base.CommitText(text, newCursorPosition);
+ InputEditable.RaiseCompositionChanged();
+ return ret;
}
public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
index 9838bb06c8..9d6dd46d0e 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
@@ -177,11 +177,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
{
}
-
- public bool CanOpenRead => true;
-
- public bool CanOpenWrite => true;
-
+
public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
index f611f50164..e35bde0acd 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
@@ -37,7 +37,7 @@ internal class AndroidStorageProvider : IStorageProvider
return Task.FromResult(new AndroidStorageFolder(_activity, uri, false));
}
- public async Task TryGetFileFromPath(Uri filePath)
+ public async Task TryGetFileFromPathAsync(Uri filePath)
{
if (filePath is null)
{
@@ -70,7 +70,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFile(_activity, androidUri);
}
- public async Task TryGetFolderFromPath(Uri folderPath)
+ public async Task TryGetFolderFromPathAsync(Uri folderPath)
{
if (folderPath is null)
{
@@ -103,7 +103,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFolder(_activity, androidUri, false);
}
- public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
+ public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
var dirCode = wellKnownFolder switch
{
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 74dc55355b..d89d6f3690 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -118,7 +118,7 @@ namespace Avalonia
{
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
- _values.ClearLocalValue(property);
+ _values.ClearValue(property);
}
///
@@ -152,7 +152,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
- _values.ClearLocalValue(property);
+ _values.ClearValue(property);
}
///
@@ -329,7 +329,7 @@ namespace Avalonia
if (value is UnsetValueType)
{
if (priority == BindingPriority.LocalValue)
- _values.ClearLocalValue(property);
+ _values.ClearValue(property);
}
else if (value is not DoNothingType)
{
@@ -355,6 +355,57 @@ namespace Avalonia
SetDirectValueUnchecked(property, value);
}
+ ///
+ /// Sets the value of a dependency property without changing its value source.
+ ///
+ /// The property.
+ /// The value.
+ ///
+ /// This method is used by a component that programmatically sets the value of one of its
+ /// own properties without disabling an application's declared use of the property. The
+ /// method changes the effective value of the property, but existing data bindings and
+ /// styles will continue to work.
+ ///
+ /// The new value will have the property's current , even if
+ /// that priority is or
+ /// .
+ ///
+ public void SetCurrentValue(AvaloniaProperty property, object? value) =>
+ property.RouteSetCurrentValue(this, value);
+
+ ///
+ /// Sets the value of a dependency property without changing its value source.
+ ///
+ /// The type of the property.
+ /// The property.
+ /// The value.
+ ///
+ /// This method is used by a component that programmatically sets the value of one of its
+ /// own properties without disabling an application's declared use of the property. The
+ /// method changes the effective value of the property, but existing data bindings and
+ /// styles will continue to work.
+ ///
+ /// The new value will have the property's current , even if
+ /// that priority is or
+ /// .
+ ///
+ public void SetCurrentValue(StyledProperty property, T value)
+ {
+ _ = property ?? throw new ArgumentNullException(nameof(property));
+ VerifyAccess();
+
+ LogPropertySet(property, value, BindingPriority.LocalValue);
+
+ if (value is UnsetValueType)
+ {
+ _values.ClearValue(property);
+ }
+ else if (value is not DoNothingType)
+ {
+ _values.SetCurrentValue(property, value);
+ }
+ }
+
///
/// Binds a to an observable.
///
@@ -547,7 +598,8 @@ namespace Avalonia
property,
GetValue(property),
BindingPriority.LocalValue,
- null);
+ null,
+ false);
}
return _values.GetDiagnostic(property);
@@ -612,14 +664,12 @@ namespace Avalonia
/// The property that has changed.
/// The old property value.
/// The new property value.
- /// The priority of the binding that produced the value.
protected void RaisePropertyChanged(
DirectPropertyBase property,
- Optional oldValue,
- BindingValue newValue,
- BindingPriority priority = BindingPriority.LocalValue)
+ T oldValue,
+ T newValue)
{
- RaisePropertyChanged(property, oldValue, newValue, priority, true);
+ RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true);
}
///
@@ -668,7 +718,7 @@ namespace Avalonia
///
/// True if the value changed, otherwise false.
///
- protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value)
+ protected bool SetAndRaise(DirectPropertyBase property, ref T field, T value)
{
VerifyAccess();
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index 96268376cf..45ab293a89 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -523,6 +523,13 @@ namespace Avalonia
object? value,
BindingPriority priority);
+ ///
+ /// Routes an untyped SetCurrentValue call to a typed call.
+ ///
+ /// The object instance.
+ /// The value.
+ internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value);
+
///
/// Routes an untyped Bind call to a typed call.
///
diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
index 9ec256225b..57e4fa4a8e 100644
--- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
+++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Data.Core
if (target is INotifyPropertyChanged inpc)
{
- WeakEvents.PropertyChanged.Subscribe(inpc, this);
+ WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
}
ValueChanged(GetValue(target));
@@ -39,7 +39,7 @@ namespace Avalonia.Data.Core
if (target is INotifyPropertyChanged inpc)
{
- WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
+ WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
}
}
}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index 7c2caf02b4..e8e3e6d509 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
- WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
+ WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
}
private object? GetReferenceTarget()
@@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
- WeakEvents.PropertyChanged.Subscribe(inpc, this);
+ WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
}
}
}
diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs
index 4189fd5234..0b3e62f1cc 100644
--- a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs
+++ b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs
@@ -3,28 +3,23 @@ using Avalonia.Data;
namespace Avalonia.Diagnostics
{
///
- /// Holds diagnostic-related information about the value of a
- /// on a .
+ /// Holds diagnostic-related information about the value of an
+ /// on an .
///
public class AvaloniaPropertyValue
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The property.
- /// The current property value.
- /// The priority of the current value.
- /// A diagnostic string.
- public AvaloniaPropertyValue(
+ internal AvaloniaPropertyValue(
AvaloniaProperty property,
object? value,
BindingPriority priority,
- string? diagnostic)
+ string? diagnostic,
+ bool isOverriddenCurrentValue)
{
Property = property;
Value = value;
Priority = priority;
Diagnostic = diagnostic;
+ IsOverriddenCurrentValue = isOverriddenCurrentValue;
}
///
@@ -46,5 +41,11 @@ namespace Avalonia.Diagnostics
/// Gets a diagnostic string.
///
public string? Diagnostic { get; }
+
+ ///
+ /// Gets a value indicating whether the was overridden by a call to
+ /// .
+ ///
+ public bool IsOverriddenCurrentValue { get; }
}
}
diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs
index 9ee1eee0fa..94dfaaab01 100644
--- a/src/Avalonia.Base/DirectPropertyBase.cs
+++ b/src/Avalonia.Base/DirectPropertyBase.cs
@@ -152,6 +152,11 @@ namespace Avalonia
return null;
}
+ internal override void RouteSetCurrentValue(AvaloniaObject o, object? value)
+ {
+ RouteSetValue(o, value, BindingPriority.LocalValue);
+ }
+
///
/// Routes an untyped Bind call to a typed call.
///
diff --git a/src/Avalonia.Base/Input/TextInput/ITextEditable.cs b/src/Avalonia.Base/Input/TextInput/ITextEditable.cs
new file mode 100644
index 0000000000..f9f5ec8d40
--- /dev/null
+++ b/src/Avalonia.Base/Input/TextInput/ITextEditable.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Metadata;
+
+namespace Avalonia.Input.TextInput
+{
+ [NotClientImplementable]
+ public interface ITextEditable
+ {
+ event EventHandler TextChanged;
+ event EventHandler SelectionChanged;
+ event EventHandler CompositionChanged;
+ int SelectionStart { get; set; }
+ int SelectionEnd { get; set; }
+ int CompositionStart { get; }
+ int CompositionEnd { get; }
+
+ string? Text { get; set; }
+ }
+}
diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
index 531cf3c704..2cdcd33626 100644
--- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
+++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Media.TextFormatting;
using Avalonia.VisualTree;
namespace Avalonia.Input.TextInput
@@ -30,6 +31,11 @@ namespace Avalonia.Input.TextInput
///
void SetPreeditText(string? text);
+ ///
+ /// Sets the current composing region. This doesn't remove the composing text from the commited text.
+ ///
+ void SetComposingRegion(TextRange? region);
+
///
/// Indicates if text input client is capable of providing the text around the cursor
///
@@ -43,6 +49,11 @@ namespace Avalonia.Input.TextInput
///
event EventHandler? SurroundingTextChanged;
+ ///
+ /// Gets or sets a platform editable. Text and selection changes made in the editable are forwarded to the IM client.
+ ///
+ ITextEditable? TextEditable { get; set; }
+
void SelectInSurroundingText(int start, int end);
}
diff --git a/src/Avalonia.Base/Layout/LayoutInformation.cs b/src/Avalonia.Base/Layout/LayoutInformation.cs
new file mode 100644
index 0000000000..9b821053a2
--- /dev/null
+++ b/src/Avalonia.Base/Layout/LayoutInformation.cs
@@ -0,0 +1,27 @@
+namespace Avalonia.Layout;
+
+///
+/// Provides access to layout information of a control.
+///
+public static class LayoutInformation
+{
+ ///
+ /// Gets the available size constraint passed in the previous layout pass.
+ ///
+ /// The control.
+ /// Previous control measure constraint, if any.
+ public static Size? GetPreviousMeasureConstraint(Layoutable control)
+ {
+ return control.PreviousMeasure;
+ }
+
+ ///
+ /// Gets the control bounds used in the previous layout arrange pass.
+ ///
+ /// The control.
+ /// Previous control arrange bounds, if any.
+ public static Rect? GetPreviousArrangeBounds(Layoutable control)
+ {
+ return control.PreviousArrange;
+ }
+}
diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs
index 775b8adddd..4a273b0291 100644
--- a/src/Avalonia.Base/Layout/Layoutable.cs
+++ b/src/Avalonia.Base/Layout/Layoutable.cs
@@ -323,6 +323,9 @@ namespace Avalonia.Layout
set { SetValue(UseLayoutRoundingProperty, value); }
}
+ ///
+ /// Gets the available size passed in the previous layout pass, if any.
+ ///
internal Size? PreviousMeasure => _previousMeasure;
///
diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs
index 622181dba0..a37fa6fd32 100644
--- a/src/Avalonia.Base/Media/DrawingContext.cs
+++ b/src/Avalonia.Base/Media/DrawingContext.cs
@@ -361,11 +361,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
///
/// The opacity.
+ /// The bounds.
/// A disposable used to undo the opacity.
- public PushedState PushOpacity(double opacity)
+ public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
- PlatformImpl.PushOpacity(opacity);
+ PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}
diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs
index b7abda2c61..7b02649b6c 100644
--- a/src/Avalonia.Base/Media/DrawingGroup.cs
+++ b/src/Avalonia.Base/Media/DrawingGroup.cs
@@ -74,10 +74,12 @@ namespace Avalonia.Media
public override void Draw(DrawingContext context)
{
+ var bounds = GetBounds();
+
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
- using (context.PushOpacity(Opacity))
+ using (context.PushOpacity(Opacity, bounds))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
- using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default)
+ using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
{
foreach (var drawing in Children)
{
@@ -284,7 +286,7 @@ namespace Avalonia.Media
throw new NotImplementedException();
}
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
throw new NotImplementedException();
}
diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs
index 732f1957d0..07fd2d56fa 100644
--- a/src/Avalonia.Base/Media/IImageBrush.cs
+++ b/src/Avalonia.Base/Media/IImageBrush.cs
@@ -12,6 +12,6 @@ namespace Avalonia.Media
///
/// Gets the image to draw.
///
- IBitmap Source { get; }
+ IBitmap? Source { get; }
}
}
diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs
index 2f2a0fb627..718ebf1686 100644
--- a/src/Avalonia.Base/Media/ImageBrush.cs
+++ b/src/Avalonia.Base/Media/ImageBrush.cs
@@ -11,8 +11,8 @@ namespace Avalonia.Media
///
/// Defines the property.
///
- public static readonly StyledProperty SourceProperty =
- AvaloniaProperty.Register(nameof(Source));
+ public static readonly StyledProperty SourceProperty =
+ AvaloniaProperty.Register(nameof(Source));
static ImageBrush()
{
@@ -30,7 +30,7 @@ namespace Avalonia.Media
/// Initializes a new instance of the class.
///
/// The image to draw.
- public ImageBrush(IBitmap source)
+ public ImageBrush(IBitmap? source)
{
Source = source;
}
@@ -38,7 +38,7 @@ namespace Avalonia.Media
///
/// Gets or sets the image to draw.
///
- public IBitmap Source
+ public IBitmap? Source
{
get { return GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
index 7d9534c414..2564d89bac 100644
--- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
+++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
@@ -281,11 +281,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
///
/// The opacity.
+ /// The bounds.
/// A disposable used to undo the opacity.
- public PushedState PushOpacity(double opacity)
+ public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
- PlatformImpl.PushOpacity(opacity);
+ PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}
diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
index f9892bf60c..668a907fdf 100644
--- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
+++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
@@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable
/// The tile mode.
/// The bitmap interpolation mode.
public ImmutableImageBrush(
- IBitmap source,
+ IBitmap? source,
AlignmentX alignmentX = AlignmentX.Center,
AlignmentY alignmentY = AlignmentY.Center,
RelativeRect? destinationRect = null,
double opacity = 1,
ImmutableTransform? transform = null,
- RelativePoint transformOrigin = new RelativePoint(),
+ RelativePoint transformOrigin = default,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None,
@@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable
}
///
- public IBitmap Source { get; }
+ public IBitmap? Source { get; }
}
}
diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
index 0b625080e3..e9086eee37 100644
--- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
+++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
@@ -24,7 +24,7 @@ namespace Avalonia.Media.Immutable
/// The tile mode.
/// Controls the quality of interpolation.
public ImmutableVisualBrush(
- Visual visual,
+ Visual? visual,
AlignmentX alignmentX = AlignmentX.Center,
AlignmentY alignmentY = AlignmentY.Center,
RelativeRect? destinationRect = null,
diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
index b5e7298b7e..08fcdb50aa 100644
--- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
+++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
@@ -37,7 +37,7 @@ namespace Avalonia.Platform
};
}
- public event EventHandler? ColorValuesChanged;
+ public virtual event EventHandler? ColorValuesChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues)
{
diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
index 8509067cd0..8962bc1586 100644
--- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
@@ -128,7 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
///
/// The opacity.
- void PushOpacity(double opacity);
+ void PushOpacity(double opacity, Rect bounds);
///
/// Pops the latest pushed opacity value.
diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs
index 5826cfb2ff..d1964bf07e 100644
--- a/src/Avalonia.Base/Platform/IGeometryImpl.cs
+++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Metadata;
@@ -47,7 +48,7 @@ namespace Avalonia.Platform
/// The stroke to use.
/// The point.
/// true if the geometry contains the point; otherwise, false.
- bool StrokeContains(IPen pen, Point point);
+ bool StrokeContains(IPen? pen, Point point);
///
/// Makes a clone of the geometry with the specified transform.
@@ -87,6 +88,7 @@ namespace Avalonia.Platform
/// If ture, the resulting snipped path will start with a BeginFigure call.
/// The resulting snipped path.
/// If the snipping operation is successful.
- bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry);
+ bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure,
+ [NotNullWhen(true)] out IGeometryImpl? segmentGeometry);
}
}
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
index a4005d4f5f..5bf9ff9d9a 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
@@ -18,11 +18,7 @@ internal class BclStorageFile : IStorageBookmarkFile
}
public FileInfo FileInfo { get; }
-
- public bool CanOpenRead => true;
-
- public bool CanOpenWrite => true;
-
+
public string Name => FileInfo.Name;
public virtual bool CanBookmark => true;
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
index ee169d62a5..34409f5fda 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
@@ -34,7 +34,7 @@ internal abstract class BclStorageProvider : IStorageProvider
: Task.FromResult(null);
}
- public virtual Task TryGetFileFromPath(Uri filePath)
+ public virtual Task TryGetFileFromPathAsync(Uri filePath)
{
if (filePath.IsAbsoluteUri)
{
@@ -48,7 +48,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult(null);
}
- public virtual Task TryGetFolderFromPath(Uri folderPath)
+ public virtual Task TryGetFolderFromPathAsync(Uri folderPath)
{
if (folderPath.IsAbsoluteUri)
{
@@ -62,7 +62,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult(null);
}
- public virtual Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
+ public virtual Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
// Note, this BCL API returns different values depending on the .NET version.
// We should also document it.
diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
index f9c7f9685d..7b0446e224 100644
--- a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
+++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
@@ -1,4 +1,6 @@
using System.Collections.Generic;
+using System.IO;
+using System.Linq;
namespace Avalonia.Platform.Storage;
@@ -21,7 +23,7 @@ public sealed class FilePickerFileType
/// List of extensions in GLOB format. I.e. "*.png" or "*.*".
///
///
- /// Used on Windows and Linux systems.
+ /// Used on Windows, Linux and Browser platforms.
///
public IReadOnlyList? Patterns { get; set; }
@@ -29,7 +31,7 @@ public sealed class FilePickerFileType
/// List of extensions in MIME format.
///
///
- /// Used on Android, Browser and Linux systems.
+ /// Used on Android, Linux and Browser platforms.
///
public IReadOnlyList? MimeTypes { get; set; }
@@ -41,4 +43,14 @@ public sealed class FilePickerFileType
/// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
///
public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; }
+
+ internal IReadOnlyList? TryGetExtensions()
+ {
+ // Converts random glob pattern to a simple extension name.
+ // GetExtension should be sufficient here.
+ // Only exception is "*.*proj" patterns that should be filtered as well.
+ return Patterns?.Select(Path.GetExtension)
+ .Where(e => !string.IsNullOrEmpty(e) && !e.Contains('*') && e.StartsWith("."))
+ .ToArray()!;
+ }
}
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
index 4aa84e3ec4..2a0ce15279 100644
--- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
+++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs
@@ -10,22 +10,12 @@ namespace Avalonia.Platform.Storage;
[NotClientImplementable]
public interface IStorageFile : IStorageItem
{
- ///
- /// Returns true, if file is readable.
- ///
- bool CanOpenRead { get; }
-
///
/// Opens a stream for read access.
///
///
Task OpenReadAsync();
-
- ///
- /// Returns true, if file is writeable.
- ///
- bool CanOpenWrite { get; }
-
+
///
/// Opens stream for writing to the file.
///
diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
index 6922151e02..9d3c961e51 100644
--- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
+++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
@@ -66,7 +66,7 @@ public interface IStorageProvider
/// It also might ask user for the permission, and throw an exception if it was denied.
///
/// File or null if it doesn't exist.
- Task TryGetFileFromPath(Uri filePath);
+ Task TryGetFileFromPathAsync(Uri filePath);
///
/// Attempts to read folder from the file-system by its path.
@@ -78,12 +78,12 @@ public interface IStorageProvider
/// It also might ask user for the permission, and throw an exception if it was denied.
///
/// Folder or null if it doesn't exist.
- Task TryGetFolderFromPath(Uri folderPath);
+ Task TryGetFolderFromPathAsync(Uri folderPath);
///
/// Attempts to read folder from the file-system by its path
///
/// Well known folder identifier.
/// Folder or null if it doesn't exist.
- Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder);
+ Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder);
}
diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
index c7772d1196..6f8b945cd6 100644
--- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
+++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
@@ -8,48 +8,47 @@ namespace Avalonia.Platform.Storage;
///
public static class StorageProviderExtensions
{
- ///
- public static Task TryGetFileFromPath(this IStorageProvider provider, string filePath)
+ ///
+ public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{
- return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath));
+ return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
}
- ///
- public static Task TryGetFolderFromPath(this IStorageProvider provider, string folderPath)
+ ///
+ public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath)
{
- return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath));
+ return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
}
- internal static string? TryGetFullPath(this IStorageFolder folder)
+ ///
+ /// Gets the local file system path of the item as a string.
+ ///
+ /// Storage folder or file.
+ /// Full local path to the folder or file if possible, otherwise null.
+ ///
+ /// Android platform usually uses "content:" virtual file paths
+ /// and Browser platform has isolated access without full paths,
+ /// so on these platforms this method will return null.
+ ///
+ public static string? TryGetLocalPath(this IStorageItem item)
{
// We can avoid double escaping of the path by checking for BclStorageFolder.
// Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users.
- if (folder is BclStorageFolder storageFolder)
+ if (item is BclStorageFolder storageFolder)
{
return storageFolder.DirectoryInfo.FullName;
}
-
- if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
- {
- return absolutePath.LocalPath;
- }
-
- // android "content:", browser and ios relative links go here.
- return null;
- }
-
- internal static string? TryGetFullPath(this IStorageFile file)
- {
- if (file is BclStorageFile storageFolder)
+ if (item is BclStorageFile storageFile)
{
- return storageFolder.FileInfo.FullName;
+ return storageFile.FileInfo.FullName;
}
- if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
+ if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
{
return absolutePath.LocalPath;
}
+ // android "content:", browser and ios relative links go here.
return null;
}
}
diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
index 04d3c805c2..78f0ad46b7 100644
--- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
+++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
@@ -29,6 +29,12 @@ namespace Avalonia.PropertyStore
///
public BindingPriority BasePriority { get; protected set; }
+ ///
+ /// Gets a value indicating whether the was overridden by a call to
+ /// .
+ ///
+ public bool IsOverridenCurrentValue { get; set; }
+
///
/// Begins a reevaluation pass on the effective value.
///
diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
index 3e20dcce56..c469034f9b 100644
--- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
+++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
@@ -19,13 +19,16 @@ namespace Avalonia.PropertyStore
private T? _baseValue;
private UncommonFields? _uncommon;
- public EffectiveValue(AvaloniaObject owner, StyledProperty property)
+ public EffectiveValue(
+ AvaloniaObject owner,
+ StyledProperty property,
+ EffectiveValue? inherited)
{
Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset;
_metadata = property.GetMetadata(owner.GetType());
- var value = _metadata.DefaultValue;
+ var value = inherited is null ? _metadata.DefaultValue : inherited.Value;
if (property.HasCoercion && _metadata.CoerceValue is { } coerce)
{
@@ -57,7 +60,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(priority != BindingPriority.LocalValue);
UpdateValueEntry(value, priority);
- SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority);
+ SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false);
}
public void SetLocalValueAndRaise(
@@ -65,7 +68,16 @@ namespace Avalonia.PropertyStore
StyledProperty property,
T value)
{
- SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
+ SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false);
+ }
+
+ public void SetCurrentValueAndRaise(
+ ValueStore owner,
+ StyledProperty property,
+ T value)
+ {
+ IsOverridenCurrentValue = true;
+ SetAndRaiseCore(owner, property, value, Priority, true);
}
public bool TryGetBaseValue([MaybeNullWhen(false)] out T value)
@@ -98,7 +110,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(Priority != BindingPriority.Animation);
Debug.Assert(BasePriority != BindingPriority.Unset);
UpdateValueEntry(null, BindingPriority.Animation);
- SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority);
+ SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false);
}
public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
@@ -158,15 +170,16 @@ namespace Avalonia.PropertyStore
ValueStore owner,
StyledProperty property,
T value,
- BindingPriority priority)
+ BindingPriority priority,
+ bool isOverriddenCurrentValue)
{
- Debug.Assert(priority < BindingPriority.Inherited);
-
var oldValue = Value;
var valueChanged = false;
var baseValueChanged = false;
var v = value;
+ IsOverridenCurrentValue = isOverriddenCurrentValue;
+
if (_uncommon?._coerce is { } coerce)
v = coerce(owner.Owner, value);
@@ -209,7 +222,6 @@ namespace Avalonia.PropertyStore
T baseValue,
BindingPriority basePriority)
{
- Debug.Assert(priority < BindingPriority.Inherited);
Debug.Assert(basePriority > BindingPriority.Animation);
Debug.Assert(priority <= basePriority);
@@ -225,7 +237,7 @@ namespace Avalonia.PropertyStore
bv = coerce(owner.Owner, baseValue);
}
- if (priority != BindingPriority.Unset && !EqualityComparer.Default.Equals(Value, v))
+ if (!EqualityComparer.Default.Equals(Value, v))
{
Value = v;
valueChanged = true;
@@ -233,9 +245,7 @@ namespace Avalonia.PropertyStore
_uncommon._uncoercedValue = value;
}
- if (priority != BindingPriority.Unset &&
- (BasePriority == BindingPriority.Unset ||
- !EqualityComparer.Default.Equals(_baseValue, bv)))
+ if (!EqualityComparer.Default.Equals(_baseValue, bv))
{
_baseValue = v;
baseValueChanged = true;
diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs
index a758360545..ec6ed392c1 100644
--- a/src/Avalonia.Base/PropertyStore/ValueStore.cs
+++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs
@@ -7,7 +7,6 @@ using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Styling;
using Avalonia.Utilities;
-using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
@@ -156,11 +155,12 @@ namespace Avalonia.PropertyStore
return observer;
}
- public void ClearLocalValue(AvaloniaProperty property)
+ public void ClearValue(AvaloniaProperty property)
{
if (TryGetEffectiveValue(property, out var effective) &&
- effective.Priority == BindingPriority.LocalValue)
+ (effective.Priority == BindingPriority.LocalValue || effective.IsOverridenCurrentValue))
{
+ effective.IsOverridenCurrentValue = false;
ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true);
}
}
@@ -184,7 +184,7 @@ namespace Avalonia.PropertyStore
}
else
{
- var effectiveValue = new EffectiveValue(Owner, property);
+ var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, result, priority);
}
@@ -200,7 +200,7 @@ namespace Avalonia.PropertyStore
}
else
{
- var effectiveValue = new EffectiveValue(Owner, property);
+ var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value);
}
@@ -209,6 +209,20 @@ namespace Avalonia.PropertyStore
}
}
+ public void SetCurrentValue(StyledProperty property, T value)
+ {
+ if (TryGetEffectiveValue(property, out var v))
+ {
+ ((EffectiveValue)v).SetCurrentValueAndRaise(this, property, value);
+ }
+ else
+ {
+ var effectiveValue = CreateEffectiveValue(property);
+ AddEffectiveValue(property, effectiveValue);
+ effectiveValue.SetCurrentValueAndRaise(this, property, value);
+ }
+ }
+
public object? GetValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
@@ -235,12 +249,7 @@ namespace Avalonia.PropertyStore
return false;
}
- public bool IsSet(AvaloniaProperty property)
- {
- if (_effectiveValues.TryGetValue(property, out var v))
- return v.Priority < BindingPriority.Inherited;
- return false;
- }
+ public bool IsSet(AvaloniaProperty property) => _effectiveValues.TryGetValue(property, out _);
public void CoerceValue(AvaloniaProperty property)
{
@@ -278,6 +287,16 @@ namespace Avalonia.PropertyStore
return false;
}
+ public EffectiveValue CreateEffectiveValue(StyledProperty property)
+ {
+ EffectiveValue? inherited = null;
+
+ if (property.Inherits && TryGetInheritedValue(property, out var v))
+ inherited = (EffectiveValue)v;
+
+ return new EffectiveValue(Owner, property, inherited);
+ }
+
public void SetInheritanceParent(AvaloniaObject? newParent)
{
var values = AvaloniaPropertyDictionaryPool.Get();
@@ -490,7 +509,7 @@ namespace Avalonia.PropertyStore
if (existing == observer)
{
_localValueBindings?.Remove(property.Id);
- ClearLocalValue(property);
+ ClearValue(property);
}
}
}
@@ -616,11 +635,13 @@ namespace Avalonia.PropertyStore
{
object? value;
BindingPriority priority;
+ bool overridden = false;
if (_effectiveValues.TryGetValue(property, out var v))
{
value = v.Value;
priority = v.Priority;
+ overridden = v.IsOverridenCurrentValue;
}
else if (property.Inherits && TryGetInheritedValue(property, out v))
{
@@ -637,7 +658,8 @@ namespace Avalonia.PropertyStore
property,
value,
priority,
- null);
+ null,
+ overridden);
}
private int InsertFrame(ValueFrame frame)
@@ -787,7 +809,7 @@ namespace Avalonia.PropertyStore
// - The value is a non-animation value and its priority is higher than the current
// effective value's base priority
var isRelevantPriority = current is null ||
- priority < current.Priority ||
+ (priority < current.Priority && priority < current.BasePriority) ||
(priority > BindingPriority.Animation && priority < current.BasePriority);
if (foundEntry && isRelevantPriority && entry!.HasValue)
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
index b75d080cfd..6b380608fe 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
@@ -313,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
///
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
var next = NextDrawAs();
- if (next == null || !next.Item.Equals(opacity))
+ if (next == null || !next.Item.Equals(opacity, bounds))
{
- Add(new OpacityNode(opacity));
+ Add(new OpacityNode(opacity, bounds));
}
else
{
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
index 50df8bd32b..08e506536f 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
@@ -111,9 +111,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
_impl.PopClip();
}
- public void PushOpacity(double opacity)
+ public void PushOpacity(double opacity, Rect bounds)
{
- _impl.PushOpacity(opacity);
+ _impl.PushOpacity(opacity, bounds);
}
public void PopOpacity()
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
index e33dc999dc..f9492d0015 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
@@ -41,27 +41,29 @@ namespace Avalonia.Rendering.Composition.Server
return;
Root!.RenderedVisuals++;
-
- if (Opacity != 1)
- canvas.PushOpacity(Opacity);
+
+ var boundsRect = new Rect(new Size(Size.X, Size.Y));
+
if (AdornedVisual != null)
{
canvas.PostTransform = Matrix.Identity;
canvas.Transform = Matrix.Identity;
- canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
+ if (AdornerIsClipped)
+ canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
}
var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
-
- var boundsRect = new Rect(new Size(Size.X, Size.Y));
+
+ if (Opacity != 1)
+ canvas.PushOpacity(Opacity, boundsRect);
if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null)
canvas.PushGeometryClip(Clip);
if(OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
-
+
RenderCore(canvas, currentTransformedClip);
// Hack to force invalidation of SKMatrix
@@ -74,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopGeometryClip();
if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip();
- if (AdornedVisual != null)
+ if (AdornedVisual != null && AdornerIsClipped)
canvas.PopClip();
if(Opacity != 1)
canvas.PopOpacity();
diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
index 8e5dc38317..09d2d55ce3 100644
--- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
@@ -117,7 +117,7 @@ namespace Avalonia.Rendering
}
using (context.PushPostTransform(m))
- using (context.PushOpacity(opacity))
+ using (context.PushOpacity(opacity, bounds))
using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete
? visual is IVisualWithRoundRectClip roundClipVisual
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
index e41e639067..f76a055934 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
@@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph
/// opacity push.
///
/// The opacity to push.
- public OpacityNode(double opacity)
+ /// The bounds.
+ public OpacityNode(double opacity, Rect bounds)
{
Opacity = opacity;
+ Bounds = bounds;
}
///
@@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- public Rect Bounds => default;
+ public Rect Bounds { get; }
///
/// Gets the opacity to be pushed or null if the operation represents a pop.
@@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph
/// Determines if this draw operation equals another.
///
/// The opacity of the other draw operation.
+ /// The bounds of the other draw operation.
/// True if the draw operations are the same, otherwise false.
///
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(double? opacity) => Opacity == opacity;
+ public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds;
///
public void Render(IDrawingContextImpl context)
{
if (Opacity.HasValue)
{
- context.PushOpacity(Opacity.Value);
+ context.PushOpacity(Opacity.Value, Bounds);
}
else
{
diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs
index 2cdb973174..5b8dac2f53 100644
--- a/src/Avalonia.Base/StyledElement.cs
+++ b/src/Avalonia.Base/StyledElement.cs
@@ -524,13 +524,7 @@ namespace Avalonia
NotifyResourcesChanged();
}
-#nullable disable
- RaisePropertyChanged(
- ParentProperty,
- new Optional(old),
- new BindingValue(Parent),
- BindingPriority.LocalValue);
-#nullable enable
+ RaisePropertyChanged(ParentProperty, old, Parent);
}
}
diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs
index 79d1b9202d..8695918c18 100644
--- a/src/Avalonia.Base/StyledProperty.cs
+++ b/src/Avalonia.Base/StyledProperty.cs
@@ -171,7 +171,7 @@ namespace Avalonia
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
- return new EffectiveValue(o, this);
+ return o.GetValueStore().CreateEffectiveValue(this);
}
///
@@ -194,24 +194,48 @@ namespace Avalonia
}
///
- [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
internal override IDisposable? RouteSetValue(
AvaloniaObject target,
object? value,
BindingPriority priority)
+ {
+ if (ShouldSetValue(target, value, out var converted))
+ return target.SetValue(this, converted, priority);
+ return null;
+ }
+
+ internal override void RouteSetCurrentValue(AvaloniaObject target, object? value)
+ {
+ if (ShouldSetValue(target, value, out var converted))
+ target.SetCurrentValue(this, converted);
+ }
+
+ internal override IDisposable RouteBind(
+ AvaloniaObject target,
+ IObservable