Browse Source

Merge branch 'master' into fix-button-flyout

pull/8448/head
Luis v.d.Eltz 4 years ago
committed by GitHub
parent
commit
bdb282b9d0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 48
      azure-pipelines-integrationtests.yml
  3. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  4. 52
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  5. 2
      samples/ControlCatalog/ControlCatalog.csproj
  6. 3
      samples/ControlCatalog/MainView.xaml
  7. 5
      samples/ControlCatalog/MainView.xaml.cs
  8. 76
      samples/ControlCatalog/Pages/DialogsPage.xaml
  9. 234
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  10. 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  11. 13
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  12. 27
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  13. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  14. 3
      src/Android/Avalonia.Android/AndroidPlatform.cs
  15. 32
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  16. 8
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  17. 244
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  18. 177
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  19. 20
      src/Android/Avalonia.Android/SystemDialogImpl.cs
  20. 53
      src/Avalonia.Base/Controls/Classes.cs
  21. 14
      src/Avalonia.Base/Controls/IClassesChangedListener.cs
  22. 2
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  23. 8
      src/Avalonia.Base/Layout/LayoutManager.cs
  24. 10
      src/Avalonia.Base/Logging/LogArea.cs
  25. 107
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  26. 88
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  27. 35
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  28. 40
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  29. 44
      src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
  30. 48
      src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs
  31. 19
      src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs
  32. 29
      src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
  33. 12
      src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs
  34. 20
      src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs
  35. 32
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  36. 11
      src/Avalonia.Base/Platform/Storage/IStorageFolder.cs
  37. 53
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  38. 56
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  39. 17
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  40. 43
      src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs
  41. 26
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  42. 3
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  43. 17
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  44. 27
      src/Avalonia.Controls/Converters/StringFormatConverter.cs
  45. 72
      src/Avalonia.Controls/Documents/InlineCollection.cs
  46. 70
      src/Avalonia.Controls/Documents/Span.cs
  47. 18
      src/Avalonia.Controls/Documents/TextElement.cs
  48. 12
      src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs
  49. 2
      src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs
  50. 74
      src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
  51. 11
      src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs
  52. 8
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  53. 4
      src/Avalonia.Controls/Primitives/AccessText.cs
  54. 3
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  55. 44
      src/Avalonia.Controls/ProgressBar.cs
  56. 1
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  57. 574
      src/Avalonia.Controls/RichTextBlock.cs
  58. 32
      src/Avalonia.Controls/SplitView.cs
  59. 57
      src/Avalonia.Controls/SystemDialog.cs
  60. 157
      src/Avalonia.Controls/TextBlock.cs
  61. 9
      src/Avalonia.Controls/TopLevel.cs
  62. 5
      src/Avalonia.Controls/TrayIcon.cs
  63. 6
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  64. 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  65. 32
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  66. 2
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
  67. 4
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  68. 35
      src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs
  69. 97
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  70. 142
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  71. 147
      src/Avalonia.Dialogs/ManagedStorageProvider.cs
  72. 4
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  73. 20
      src/Avalonia.FreeDesktop/DBusCallQueue.cs
  74. 4
      src/Avalonia.FreeDesktop/DBusHelper.cs
  75. 46
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  76. 28
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  77. 31
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  78. 38
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  79. 38
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
  80. 30
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  81. 2
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  82. 16
      src/Avalonia.FreeDesktop/DBusMenu.cs
  83. 36
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  84. 2
      src/Avalonia.FreeDesktop/DBusRequest.cs
  85. 159
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  86. 2
      src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs
  87. 1
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  88. 36
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  89. 6
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  90. 1
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  91. 118
      src/Avalonia.Native/SystemDialogs.cs
  92. 7
      src/Avalonia.Native/WindowImplBase.cs
  93. 7
      src/Avalonia.Native/avn.idl
  94. 1
      src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj
  95. 21
      src/Avalonia.Themes.Default/Controls/ProgressBar.xaml
  96. 2
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  97. 18
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  98. 3
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  99. 225
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  100. 7
      src/Avalonia.X11/X11Platform.cs

1
Avalonia.sln

@ -38,6 +38,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
EndProjectSection

48
azure-pipelines-integrationtests.yml

@ -0,0 +1,48 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- master
jobs:
- job: Mac
pool:
name: 'AvaloniaMacPool'
steps:
- script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
displayName: 'run integration tests'
- job: Windows
pool:
vmImage: 'windows-2022'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.202'
inputs:
version: 6.0.202
- task: Windows Application Driver@0
inputs:
OperationType: 'Start'
AgentResolution: '4K'
displayName: 'Start WinAppDriver'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
- task: Windows Application Driver@0
inputs:
OperationType: 'Stop'
displayName: 'Stop WinAppDriver'

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -49,6 +49,7 @@
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */; };
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -101,6 +102,7 @@
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platformthreading.mm; sourceTree = "<group>"; };
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; };
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -108,6 +110,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */,
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */,
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */,
AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */,
@ -122,6 +125,7 @@
AB661C1C2148230E00291242 /* Frameworks */ = {
isa = PBXGroup;
children = (
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */,
522D5958258159C1006F7F7A /* Carbon.framework */,
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */,
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */,

52
native/Avalonia.Native/src/OSX/SystemDialogs.mm

@ -1,5 +1,6 @@
#include "common.h"
#include "INSWindowHolder.h"
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
class SystemDialogs : public ComSingleObject<IAvnSystemDialogs, &IID_IAvnSystemDialogs>
{
@ -7,6 +8,7 @@ public:
FORWARD_IUNKNOWN()
virtual void SelectFolderDialog (IAvnWindow* parentWindowHandle,
IAvnSystemDialogEvents* events,
bool allowMultiple,
const char* title,
const char* initialDirectory) override
{
@ -14,6 +16,7 @@ public:
{
auto panel = [NSOpenPanel openPanel];
panel.allowsMultipleSelection = allowMultiple;
panel.canChooseDirectories = true;
panel.canCreateDirectories = true;
panel.canChooseFiles = false;
@ -118,7 +121,15 @@ public:
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
panel.allowedFileTypes = allowedTypes;
// Prefer allowedContentTypes if available
if (@available(macOS 11.0, *))
{
panel.allowedContentTypes = ConvertToUTType(allowedTypes);
}
else
{
panel.allowedFileTypes = allowedTypes;
}
}
}
@ -207,7 +218,18 @@ public:
{
auto allowedTypes = [filtersString componentsSeparatedByString:@";"];
panel.allowedFileTypes = allowedTypes;
// Prefer allowedContentTypes if available
if (@available(macOS 11.0, *))
{
panel.allowedContentTypes = ConvertToUTType(allowedTypes);
}
else
{
panel.allowedFileTypes = allowedTypes;
}
panel.allowsOtherFileTypes = false;
panel.extensionHidden = false;
}
}
@ -250,6 +272,32 @@ public:
}
}
}
private:
NSMutableArray* ConvertToUTType(NSArray<NSString*>* allowedTypes)
{
auto originalCount = [allowedTypes count];
auto mapped = [[NSMutableArray alloc] init];
if (@available(macOS 11.0, *))
{
for (int i = 0; i < originalCount; i++)
{
auto utTypeStr = allowedTypes[i];
auto utType = [UTType typeWithIdentifier:utTypeStr];
if (utType == nil)
{
utType = [UTType typeWithMIMEType:utTypeStr];
}
if (utType != nil)
{
[mapped addObject:utType];
}
}
}
return mapped;
}
};

2
samples/ControlCatalog/ControlCatalog.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
</PropertyGroup>

3
samples/ControlCatalog/MainView.xaml

@ -69,6 +69,9 @@
<TabItem Header="CalendarDatePicker">
<pages:CalendarDatePickerPage />
</TabItem>
<TabItem Header="Dialogs">
<pages:DialogsPage />
</TabItem>
<TabItem Header="Drag+Drop">
<pages:DragAndDropPage />
</TabItem>

5
samples/ControlCatalog/MainView.xaml.cs

@ -24,11 +24,6 @@ namespace ControlCatalog
{
IList tabItems = ((IList)sideBar.Items);
tabItems.Add(new TabItem()
{
Header = "Dialogs",
Content = new DialogsPage()
});
tabItems.Add(new TabItem()
{
Header = "Screens",
Content = new ScreenPage()

76
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -1,29 +1,57 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DialogsPage">
<StackPanel Orientation="Vertical" Spacing="4" Margin="4">
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Button Name="OpenFile">_Open File</Button>
<Button Name="OpenMultipleFiles">Open _Multiple File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
<UserControl x:Class="ControlCatalog.Pages.DialogsPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Margin="4"
Orientation="Vertical"
Spacing="4">
<TextBlock x:Name="PickerLastResultsVisible"
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsPresenter x:Name="PickerLastResults" />
<TextBlock Text="Windows:" />
<TextBlock Margin="0, 8, 0, 0"
Classes="h1"
Text="Window dialogs" />
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
<Expander Header="Window dialogs">
<StackPanel Spacing="4">
<Button Name="DecoratedWindow">Decorated _window</Button>
<Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
<Button Name="Dialog" ToolTip.Tip="Shows a dialog">_Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
<Button Name="OwnedWindow">Own_ed window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
</StackPanel>
</Expander>
<TextBlock Margin="0,20,0,0" Text="Pickers:" />
<CheckBox Name="UseFilters">Use filters</CheckBox>
<Expander Header="FilePicker API">
<StackPanel Spacing="4">
<CheckBox Name="ForceManaged">Force managed dialog</CheckBox>
<CheckBox Name="OpenMultiple">Open multiple</CheckBox>
<Button Name="OpenFolderPicker">Select Fo_lder</Button>
<Button Name="OpenFilePicker">_Open File</Button>
<Button Name="SaveFilePicker">_Save File</Button>
<Button Name="OpenFileFromBookmark">Open File Bookmark</Button>
<Button Name="OpenFolderFromBookmark">Open Folder Bookmark</Button>
</StackPanel>
</Expander>
<Expander Header="Legacy OpenFileDialog">
<StackPanel Spacing="4">
<Button Name="OpenFile">_Open File</Button>
<Button Name="OpenMultipleFiles">Open _Multiple File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
</StackPanel>
</Expander>
<TextBlock x:Name="PickerLastResultsVisible"
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsPresenter x:Name="PickerLastResults" />
<TextBox Name="BookmarkContainer" Watermark="Bookmark" />
<TextBox Name="OpenedFileContent"
MaxLines="10"
Watermark="Picked file content" />
</StackPanel>
</UserControl>

234
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -1,13 +1,21 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Dialogs;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
#pragma warning disable 4014
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
#pragma warning disable CS0618 // Type or member is obsolete
#nullable enable
namespace ControlCatalog.Pages
{
public class DialogsPage : UserControl
@ -18,13 +26,16 @@ namespace ControlCatalog.Pages
var results = this.Get<ItemsPresenter>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent");
var openMultiple = this.Get<CheckBox>("OpenMultiple");
string? lastSelectedDirectory = null;
IStorageFolder? lastSelectedDirectory = null;
List<FileDialogFilter>? GetFilters()
List<FileDialogFilter> GetFilters()
{
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return null;
return new List<FileDialogFilter>();
return new List<FileDialogFilter>
{
new FileDialogFilter
@ -39,12 +50,23 @@ namespace ControlCatalog.Pages
};
}
List<FilePickerFileType>? GetFileTypes()
{
if (this.Get<CheckBox>("UseFilters").IsChecked != true)
return null;
return new List<FilePickerFileType>
{
FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain
};
}
this.Get<Button>("OpenFile").Click += async delegate
{
// Almost guaranteed to exist
var fullPath = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
var initialFileName = fullPath == null ? null : System.IO.Path.GetFileName(fullPath);
var initialDirectory = fullPath == null ? null : System.IO.Path.GetDirectoryName(fullPath);
var uri = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
var initialFileName = uri == null ? null : System.IO.Path.GetFileName(uri);
var initialDirectory = uri == null ? null : System.IO.Path.GetDirectoryName(uri);
var result = await new OpenFileDialog()
{
@ -62,7 +84,7 @@ namespace ControlCatalog.Pages
{
Title = "Open multiple files",
Filters = GetFilters(),
Directory = lastSelectedDirectory,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.Items = result;
@ -70,11 +92,13 @@ namespace ControlCatalog.Pages
};
this.Get<Button>("SaveFile").Click += async delegate
{
var filters = GetFilters();
var result = await new SaveFileDialog()
{
Title = "Save file",
Filters = GetFilters(),
Directory = lastSelectedDirectory,
Filters = filters,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
results.Items = new[] { result };
@ -85,14 +109,9 @@ namespace ControlCatalog.Pages
var result = await new OpenFolderDialog()
{
Title = "Select folder",
Directory = lastSelectedDirectory,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null
}.ShowAsync(GetWindow());
if (!string.IsNullOrEmpty(result))
{
lastSelectedDirectory = result;
}
lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result));
results.Items = new [] { result };
resultsVisible.IsVisible = result != null;
};
@ -101,7 +120,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFileDialog()
{
Title = "Select both",
Directory = lastSelectedDirectory,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null,
AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{
@ -116,20 +135,20 @@ namespace ControlCatalog.Pages
};
this.Get<Button>("DecoratedWindowDialog").Click += delegate
{
new DecoratedWindow().ShowDialog(GetWindow());
_ = new DecoratedWindow().ShowDialog(GetWindow());
};
this.Get<Button>("Dialog").Click += delegate
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowDialog(GetWindow());
_ = window.ShowDialog(GetWindow());
};
this.Get<Button>("DialogNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
window.Height = 200;
window.ShowInTaskbar = false;
window.ShowDialog(GetWindow());
_ = window.ShowDialog(GetWindow());
};
this.Get<Button>("OwnedWindow").Click += delegate
{
@ -146,13 +165,166 @@ namespace ControlCatalog.Pages
window.Show(GetWindow());
};
this.Get<Button>("OpenFilePicker").Click += async delegate
{
var result = await GetStorageProvider().OpenFilePickerAsync(new FilePickerOpenOptions()
{
Title = "Open file",
FileTypeFilter = GetFileTypes(),
SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true
});
await SetPickerResult(result);
};
this.Get<Button>("SaveFilePicker").Click += async delegate
{
var fileTypes = GetFileTypes();
var file = await GetStorageProvider().SaveFilePickerAsync(new FilePickerSaveOptions()
{
Title = "Save file",
FileTypeChoices = fileTypes,
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
DefaultExtension = fileTypes?.Any() == true ? "txt" : null,
ShowOverwritePrompt = false
});
if (file is not null && file.CanOpenWrite)
{
// Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER
await using var stream = await file.OpenWrite();
await using var reader = new System.IO.StreamWriter(stream);
#else
using var stream = await file.OpenWrite();
using var reader = new System.IO.StreamWriter(stream);
#endif
await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync();
}
await SetPickerResult(file is null ? null : new [] {file});
};
this.Get<Button>("OpenFolderPicker").Click += async delegate
{
var folders = await GetStorageProvider().OpenFolderPickerAsync(new FolderPickerOpenOptions()
{
Title = "Folder file",
SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true
});
await SetPickerResult(folders);
lastSelectedDirectory = folders.FirstOrDefault();
};
this.Get<Button>("OpenFileFromBookmark").Click += async delegate
{
var file = bookmarkContainer.Text is not null
? await GetStorageProvider().OpenFileBookmarkAsync(bookmarkContainer.Text)
: null;
await SetPickerResult(file is null ? null : new[] { file });
};
this.Get<Button>("OpenFolderFromBookmark").Click += async delegate
{
var folder = bookmarkContainer.Text is not null
? await GetStorageProvider().OpenFolderBookmarkAsync(bookmarkContainer.Text)
: null;
await SetPickerResult(folder is null ? null : new[] { folder });
lastSelectedDirectory = folder;
};
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
{
items ??= Array.Empty<IStorageItem>();
var mappedResults = items.Select(FullPathOrName).ToList();
bookmarkContainer.Text = items.FirstOrDefault(f => f.CanBookmark) is { } f ? await f.SaveBookmark() : "Can't bookmark";
if (items.FirstOrDefault() is IStorageItem item)
{
var resultText = item is IStorageFile ? "File:" : "Folder:";
resultText += Environment.NewLine;
var props = await item.GetBasicPropertiesAsync();
resultText += @$"Size: {props.Size}
DateCreated: {props.DateCreated}
DateModified: {props.DateModified}
CanBookmark: {item.CanBookmark}
";
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.OpenRead();
#else
using var stream = await file.OpenRead();
#endif
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<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
}
openedFileContent.Text = resultText;
lastSelectedDirectory = await item.GetParentAsync();
if (lastSelectedDirectory is not null)
{
mappedResults.Insert(0, "Parent: " + FullPathOrName(lastSelectedDirectory));
}
}
results.Items = mappedResults;
resultsVisible.IsVisible = mappedResults.Any();
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var openedFileContent = this.Get<TextBox>("OpenedFileContent");
try
{
var storageProvider = GetStorageProvider();
openedFileContent.Text = $@"CanOpen: {storageProvider.CanOpen}
CanSave: {storageProvider.CanSave}
CanPickFolder: {storageProvider.CanPickFolder}";
}
catch (Exception ex)
{
openedFileContent.Text = "Storage provider is not available: " + ex.Message;
}
}
private Window CreateSampleWindow()
{
Button button;
Button dialogButton;
var window = new Window
{
Height = 200,
@ -191,7 +363,22 @@ namespace ControlCatalog.Pages
return window;
}
Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner");
private IStorageProvider GetStorageProvider()
{
var forceManaged = this.Get<CheckBox>("ForceManaged").IsChecked ?? false;
return forceManaged
? new ManagedStorageProvider<Window>(GetWindow(), null)
: GetTopLevel().StorageProvider;
}
private static string FullPathOrName(IStorageItem? item)
{
if (item is null) return "(null)";
return item.TryGetUri(out var uri) ? uri.ToString() : item.Name;
}
Window GetWindow() => this.VisualRoot as Window ?? throw new NullReferenceException("Invalid Owner");
TopLevel GetTopLevel() => this.VisualRoot as TopLevel ?? throw new NullReferenceException("Invalid Owner");
private void InitializeComponent()
{
@ -199,3 +386,4 @@ namespace ControlCatalog.Pages
}
}
}
#pragma warning restore CS0618 // Type or member is obsolete

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -76,21 +76,21 @@
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="upDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of decimal NumericUpDown:</Label>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Value="{Binding DecimalValue}"
VerticalAlignment="Center" Value="{Binding DecimalValue}"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="DoubleUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of double NumericUpDown:</Label>
<NumericUpDown Name="DoubleUpDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Value="{Binding DoubleValue}"
VerticalAlignment="Center" Value="{Binding DoubleValue}"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="ValidationUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">NumericUpDown with Validation Errors:</Label>
<NumericUpDown x:Name="ValidationUpDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center"
VerticalAlignment="Center"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}">
<DataValidationErrors.Error>
<sys:Exception />

13
samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@ -84,15 +84,10 @@ namespace ControlCatalog.Pages
}
}
public IList<CultureInfo> Cultures { get; } = new List<CultureInfo>()
{
new CultureInfo("en-US"),
new CultureInfo("en-GB"),
new CultureInfo("fr-FR"),
new CultureInfo("ar-DZ"),
new CultureInfo("zh-CN"),
new CultureInfo("cs-CZ")
};
// Trimmed-mode friendly where we might not have cultures
public IList<CultureInfo?> Cultures { get; } = CultureInfo.GetCultures(CultureTypes.SpecificCultures)
.Where(c => new[] { "en-US", "en-GB", "fr-FR", "ar-DZ", "zh-CH", "cs-CZ" }.Contains(c.Name))
.ToArray();
public FormatObject SelectedFormat
{

27
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@ -1,22 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ProgressBarPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A progress bar control</TextBlock>
<StackPanel>
<StackPanel Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Maximum</TextBlock>
<NumericUpDown x:Name="maximum" Value="100" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Minimum</TextBlock>
<NumericUpDown x:Name="minimum" Value="0" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Progress Text Format</TextBlock>
<TextBox x:Name="stringFormat" Text="{}{0:0}%" VerticalAlignment="Center"/>
</StackPanel>
<CheckBox x:Name="showProgress" Margin="10,16,0,0" Content="Show Progress Text" />
<CheckBox x:Name="isIndeterminate" Margin="10,16,0,0" Content="Toggle Indeterminate" />
<StackPanel Orientation="Horizontal" Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16">
<StackPanel Spacing="16">
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel>
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
<ProgressBar IsIndeterminate="{Binding #isIndeterminate.IsChecked}" ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" ProgressTextFormat="{Binding #stringFormat.Text}"/>
</StackPanel>
<StackPanel Margin="16">
<Slider Name="hprogress" Maximum="100" Value="40" />
<Slider Name="vprogress" Maximum="100" Value="60" />
<Slider Name="hprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="40" />
<Slider Name="vprogress" Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.Value}" Value="60" />
</StackPanel>
<StackPanel Spacing="10">
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True" />
<ProgressBar VerticalAlignment="Center" IsIndeterminate="True"
Minimum="{Binding #minimum.Value}" Maximum="{Binding #maximum.value}"/>
<ProgressBar VerticalAlignment="Center" Value="5" Maximum="10" />
<ProgressBar VerticalAlignment="Center" Value="50" />
<ProgressBar VerticalAlignment="Center" Value="50" Minimum="25" Maximum="75" />

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel>
</Border>
<Border>
<TextBlock Margin="10" TextWrapping="Wrap">
<RichTextBlock Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</TextBlock>
</RichTextBlock>
</Border>
</WrapPanel>
</StackPanel>

3
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -1,10 +1,8 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Android;
using Avalonia.Android.Platform;
using Avalonia.Android.Platform.Input;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL.Egl;
@ -55,7 +53,6 @@ namespace Avalonia.Android
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(new RenderLoop())

32
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -4,10 +4,14 @@ using Android.Content.Res;
using AndroidX.Lifecycle;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls;
using Android.Runtime;
using Android.App;
using Android.Content;
using System;
namespace Avalonia.Android
{
public abstract class AvaloniaActivity<TApp> : AppCompatActivity where TApp : Application, new()
public abstract class AvaloniaActivity : AppCompatActivity
{
internal class SingleViewLifetime : ISingleViewApplicationLifetime
{
@ -20,16 +24,15 @@ namespace Avalonia.Android
}
}
internal Action<int, Result, Intent> ActivityResult;
internal AvaloniaView View;
internal AvaloniaViewModel _viewModel;
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid();
protected abstract AppBuilder CreateAppBuilder();
protected override void OnCreate(Bundle savedInstanceState)
{
var builder = AppBuilder.Configure<TApp>();
CustomizeAppBuilder(builder);
var builder = CreateAppBuilder();
var lifetime = new SingleViewLifetime();
@ -79,5 +82,24 @@ namespace Avalonia.Android
base.OnDestroy();
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
ActivityResult?.Invoke(requestCode, resultCode, data);
}
}
public abstract class AvaloniaActivity<TApp> : AvaloniaActivity where TApp : Application, new()
{
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid();
protected override AppBuilder CreateAppBuilder()
{
var builder = AppBuilder.Configure<TApp>();
return CustomizeAppBuilder(builder);
}
}
}

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

@ -7,6 +7,7 @@ using Android.Views.InputMethods;
using Avalonia.Android.OpenGL;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
@ -16,11 +17,13 @@ using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
@ -46,6 +49,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
StorageProvider = new AndroidStorageProvider((AvaloniaActivity)avaloniaView.Context);
}
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -225,6 +229,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public INativeControlHostImpl NativeControlHost { get; }
public IStorageProvider StorageProvider { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{

244
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -0,0 +1,244 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Android.Content;
using Android.Provider;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using Java.Lang;
using AndroidUri = Android.Net.Uri;
using Exception = System.Exception;
using JavaFile = Java.IO.File;
namespace Avalonia.Android.Platform.Storage;
internal abstract class AndroidStorageItem : IStorageBookmarkItem
{
private Context? _context;
protected AndroidStorageItem(Context context, AndroidUri uri)
{
_context = context;
Uri = uri;
}
internal AndroidUri Uri { get; }
protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
public bool CanBookmark => true;
public Task<string?> SaveBookmark()
{
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.FromResult(Uri.ToString());
}
public Task ReleaseBookmark()
{
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.CompletedTask;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
uri = new Uri(Uri.ToString()!);
return true;
}
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync();
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
{
try
{
var projection = new[] { column };
using var cursor = context.ContentResolver!.Query(contentUri, projection, selection, selectionArgs, null);
if (cursor?.MoveToFirst() == true)
{
var columnIndex = cursor.GetColumnIndex(column);
if (columnIndex != -1)
return cursor.GetString(columnIndex);
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "File metadata reader failed: '{Exception}'", ex);
}
return null;
}
public Task<IStorageFolder?> GetParentAsync()
{
using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused.
if (javaFile.ParentFile is {} parentFile
&& AndroidUri.FromFile(parentFile) is {} androidUri)
{
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Context, androidUri));
}
return Task.FromResult<IStorageFolder?>(null);
}
public void Dispose()
{
_context = null;
}
}
internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{
public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri)
{
}
public override Task<StorageItemProperties> GetBasicPropertiesAsync()
{
return Task.FromResult(new StorageItemProperties());
}
}
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
{
public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri)
{
}
public bool CanOpenRead => true;
public bool CanOpenWrite => true;
public Task<Stream> OpenRead() => Task.FromResult(OpenContentStream(Context, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));
public Task<Stream> OpenWrite() => Task.FromResult(OpenContentStream(Context, Uri, true)
?? throw new InvalidOperationException("Failed to open content stream"));
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput)
{
var isVirtual = IsVirtualFile(context, uri);
if (isVirtual)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?.Log(this, "Content URI was virtual: '{Uri}'", uri);
return GetVirtualFileStream(context, uri, isOutput);
}
return isOutput
? context.ContentResolver?.OpenOutputStream(uri)
: context.ContentResolver?.OpenInputStream(uri);
}
private bool IsVirtualFile(Context context, AndroidUri uri)
{
if (!DocumentsContract.IsDocumentUri(context, uri))
return false;
var value = GetColumnValue(context, uri, DocumentsContract.Document.ColumnFlags);
if (!string.IsNullOrEmpty(value) && int.TryParse(value, out var flagsInt))
{
var flags = (DocumentContractFlags)flagsInt;
return flags.HasFlag(DocumentContractFlags.VirtualDocument);
}
return false;
}
private Stream? GetVirtualFileStream(Context context, AndroidUri uri, bool isOutput)
{
var mimeTypes = context.ContentResolver?.GetStreamTypes(uri, FilePickerFileTypes.All.MimeTypes![0]);
if (mimeTypes?.Length >= 1)
{
var mimeType = mimeTypes[0];
var asset = context.ContentResolver!
.OpenTypedAssetFileDescriptor(uri, mimeType, null);
var stream = isOutput
? asset?.CreateOutputStream()
: asset?.CreateInputStream();
return stream;
}
return null;
}
public override Task<StorageItemProperties> GetBasicPropertiesAsync()
{
ulong? size = null;
DateTimeOffset? itemDate = null;
DateTimeOffset? dateModified = null;
try
{
var projection = new[]
{
MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded,
MediaStore.IMediaColumns.DateModified
};
using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null);
if (cursor?.MoveToFirst() == true)
{
try
{
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.Size);
if (columnIndex != -1)
{
size = (ulong)cursor.GetLong(columnIndex);
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "File Size metadata reader failed: '{Exception}'", ex);
}
try
{
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateAdded);
if (columnIndex != -1)
{
var longValue = cursor.GetLong(columnIndex);
itemDate = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null;
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex);
}
try
{
var columnIndex = cursor.GetColumnIndex(MediaStore.IMediaColumns.DateModified);
if (columnIndex != -1)
{
var longValue = cursor.GetLong(columnIndex);
dateModified = longValue > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(longValue) : null;
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.AndroidPlatform)?
.Log(this, "File DateAdded metadata reader failed: '{Exception}'", ex);
}
}
}
catch (UnsupportedOperationException)
{
// It's not possible to get parameters of some files/folders.
}
return Task.FromResult(new StorageItemProperties(size, itemDate, dateModified));
}
}

177
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -0,0 +1,177 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Provider;
using Avalonia.Platform.Storage;
using AndroidUri = Android.Net.Uri;
namespace Avalonia.Android.Platform.Storage;
internal class AndroidStorageProvider : IStorageProvider
{
private readonly AvaloniaActivity _activity;
private int _lastRequestCode = 20000;
public AndroidStorageProvider(AvaloniaActivity activity)
{
_activity = activity;
}
public bool CanOpen => OperatingSystem.IsAndroidVersionAtLeast(19);
public bool CanSave => OperatingSystem.IsAndroidVersionAtLeast(19);
public bool CanPickFolder => OperatingSystem.IsAndroidVersionAtLeast(21);
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri));
}
public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
return Task.FromResult<IStorageBookmarkFile?>(new AndroidStorageFile(_activity, uri));
}
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var mimeTypes = options.FileTypeFilter?.Where(t => t != FilePickerFileTypes.All)
.SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>();
var intent = new Intent(Intent.ActionOpenDocument)
.AddCategory(Intent.CategoryOpenable)
.PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple)
.SetType(FilePickerFileTypes.All.MimeTypes![0]);
if (mimeTypes.Length > 0)
{
intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes);
}
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
{
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
}
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select file");
var uris = await StartActivity(pickerIntent, false);
return uris.Select(u => new AndroidStorageFile(_activity, u)).ToArray();
}
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var mimeTypes = options.FileTypeChoices?.Where(t => t != FilePickerFileTypes.All)
.SelectMany(f => f.MimeTypes ?? Array.Empty<string>()).Distinct().ToArray() ?? Array.Empty<string>();
var intent = new Intent(Intent.ActionCreateDocument)
.AddCategory(Intent.CategoryOpenable)
.SetType(FilePickerFileTypes.All.MimeTypes![0]);
if (mimeTypes.Length > 0)
{
intent = intent.PutExtra(Intent.ExtraMimeTypes, mimeTypes);
}
if (options.SuggestedFileName is { } fileName)
{
if (options.DefaultExtension is { } ext)
{
fileName += ext.StartsWith('.') ? ext : "." + ext;
}
intent = intent.PutExtra(Intent.ExtraTitle, fileName);
}
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
{
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
}
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Save file");
var uris = await StartActivity(pickerIntent, true);
return uris.Select(u => new AndroidStorageFile(_activity, u)).FirstOrDefault();
}
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var intent = new Intent(Intent.ActionOpenDocumentTree)
.PutExtra(Intent.ExtraAllowMultiple, options.AllowMultiple);
if (TryGetInitialUri(options.SuggestedStartLocation) is { } initialUri)
{
intent = intent.PutExtra(DocumentsContract.ExtraInitialUri, initialUri);
}
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder");
var uris = await StartActivity(pickerIntent, false);
return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray();
}
private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult)
{
var resultList = new List<AndroidUri>(1);
var tcs = new TaskCompletionSource<Intent?>();
var currentRequestCode = _lastRequestCode++;
_activity.ActivityResult += OnActivityResult;
_activity.StartActivityForResult(pickerIntent, currentRequestCode);
var result = await tcs.Task;
if (result != null)
{
// ClipData first to avoid issue with multiple files selection.
if (!singleResult && result.ClipData is { } clipData)
{
for (var i = 0; i < clipData.ItemCount; i++)
{
var uri = clipData.GetItemAt(i)?.Uri;
if (uri != null)
{
resultList.Add(uri);
}
}
}
else if (result.Data is { } uri)
{
resultList.Add(uri);
}
}
if (result?.HasExtra("error") == true)
{
throw new Exception(result.GetStringExtra("error"));
}
return resultList;
void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (currentRequestCode != requestCode)
{
return;
}
_activity.ActivityResult -= OnActivityResult;
_ = tcs.TrySetResult(resultCode == Result.Ok ? data : null);
}
}
private static AndroidUri? TryGetInitialUri(IStorageFolder? folder)
{
if (OperatingSystem.IsAndroidVersionAtLeast(26)
&& (folder as AndroidStorageItem)?.Uri is { } uri)
{
return uri;
}
return null;
}
}

20
src/Android/Avalonia.Android/SystemDialogImpl.cs

@ -1,20 +0,0 @@
using System;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
namespace Avalonia.Android
{
internal class SystemDialogImpl : ISystemDialogImpl
{
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
throw new NotImplementedException();
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
throw new NotImplementedException();
}
}
}

53
src/Avalonia.Base/Controls/Classes.cs

@ -14,6 +14,8 @@ namespace Avalonia.Controls
/// </remarks>
public class Classes : AvaloniaList<string>, IPseudoClasses
{
private List<IClassesChangedListener>? _listeners;
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
@ -39,6 +41,11 @@ namespace Avalonia.Controls
{
}
/// <summary>
/// Gets the number of listeners subscribed to this collection for unit testing purposes.
/// </summary>
internal int ListenerCount => _listeners?.Count ?? 0;
/// <summary>
/// Parses a classes string.
/// </summary>
@ -62,6 +69,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
NotifyChanged();
}
}
@ -89,6 +97,7 @@ namespace Avalonia.Controls
}
base.AddRange(c);
NotifyChanged();
}
/// <summary>
@ -103,6 +112,8 @@ namespace Avalonia.Controls
RemoveAt(i);
}
}
NotifyChanged();
}
/// <summary>
@ -122,6 +133,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Insert(index, name);
NotifyChanged();
}
}
@ -154,6 +166,7 @@ namespace Avalonia.Controls
if (toInsert != null)
{
base.InsertRange(index, toInsert);
NotifyChanged();
}
}
@ -169,7 +182,14 @@ namespace Avalonia.Controls
public override bool Remove(string name)
{
ThrowIfPseudoclass(name, "removed");
return base.Remove(name);
if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
}
/// <summary>
@ -197,6 +217,7 @@ namespace Avalonia.Controls
if (toRemove != null)
{
base.RemoveAll(toRemove);
NotifyChanged();
}
}
@ -214,6 +235,7 @@ namespace Avalonia.Controls
var name = this[index];
ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index);
NotifyChanged();
}
/// <summary>
@ -224,6 +246,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count)
{
base.RemoveRange(index, count);
NotifyChanged();
}
/// <summary>
@ -255,6 +278,7 @@ namespace Avalonia.Controls
}
base.AddRange(source);
NotifyChanged();
}
/// <inheritdoc/>
@ -263,13 +287,38 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
NotifyChanged();
}
}
/// <inheritdoc/>
bool IPseudoClasses.Remove(string name)
{
return base.Remove(name);
if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
}
internal void AddListener(IClassesChangedListener listener)
{
(_listeners ??= new()).Add(listener);
}
internal void RemoveListener(IClassesChangedListener listener)
{
_listeners?.Remove(listener);
}
private void NotifyChanged()
{
if (_listeners is null)
return;
foreach (var listener in _listeners)
listener.Changed();
}
private void ThrowIfPseudoclass(string name, string operation)

14
src/Avalonia.Base/Controls/IClassesChangedListener.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
/// performant manner than subscribing to CollectionChanged.
/// </summary>
internal interface IClassesChangedListener
{
/// <summary>
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
/// </summary>
void Changed();
}
}

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

@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
{
if (instance is IReflectableType reflectableType)
if (instance is IReflectableType reflectableType && instance is not Type)
return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
var type = instance.GetType();

8
src/Avalonia.Base/Layout/LayoutManager.cs

@ -350,7 +350,7 @@ namespace Avalonia.Layout
{
for (var i = 0; i < count; ++i)
{
var l = _effectiveViewportChangedListeners[i];
var l = listeners[i];
if (!l.Listener.IsAttachedToVisualTree)
{
@ -362,7 +362,7 @@ namespace Avalonia.Layout
if (viewport != l.Viewport)
{
l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
_effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
l.Viewport = viewport;
}
}
}
@ -414,7 +414,7 @@ namespace Avalonia.Layout
}
}
private readonly struct EffectiveViewportChangedListener
private class EffectiveViewportChangedListener
{
public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
{
@ -423,7 +423,7 @@ namespace Avalonia.Layout
}
public ILayoutable Listener { get; }
public Rect Viewport { get; }
public Rect Viewport { get; set; }
}
}
}

10
src/Avalonia.Base/Logging/LogArea.cs

@ -44,5 +44,15 @@ namespace Avalonia.Logging
/// The log event comes from X11Platform.
/// </summary>
public const string X11Platform = nameof(X11Platform);
/// <summary>
/// The log event comes from AndroidPlatform.
/// </summary>
public const string AndroidPlatform = nameof(AndroidPlatform);
/// <summary>
/// The log event comes from IOSPlatform.
/// </summary>
public const string IOSPlatform = nameof(IOSPlatform);
}
}

107
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -0,0 +1,107 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public class BclStorageFile : IStorageBookmarkFile
{
private readonly FileInfo _fileInfo;
public BclStorageFile(FileInfo fileInfo)
{
_fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
}
public bool CanOpenRead => true;
public bool CanOpenWrite => true;
public string Name => _fileInfo.Name;
public virtual bool CanBookmark => true;
public Task<StorageItemProperties> GetBasicPropertiesAsync()
{
var props = new StorageItemProperties();
if (_fileInfo.Exists)
{
props = new StorageItemProperties(
(ulong)_fileInfo.Length,
_fileInfo.CreationTimeUtc,
_fileInfo.LastAccessTimeUtc);
}
return Task.FromResult(props);
}
public Task<IStorageFolder?> GetParentAsync()
{
if (_fileInfo.Directory is { } directory)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
return Task.FromResult<IStorageFolder?>(null);
}
public Task<Stream> OpenRead()
{
return Task.FromResult<Stream>(_fileInfo.OpenRead());
}
public Task<Stream> OpenWrite()
{
return Task.FromResult<Stream>(_fileInfo.OpenWrite());
}
public virtual Task<string?> SaveBookmark()
{
return Task.FromResult<string?>(_fileInfo.FullName);
}
public Task ReleaseBookmark()
{
// No-op
return Task.CompletedTask;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
if (_fileInfo.Directory is not null)
{
uri = Path.IsPathRooted(_fileInfo.FullName) ?
new Uri(new Uri("file://"), _fileInfo.FullName) :
new Uri(_fileInfo.FullName, UriKind.Relative);
return true;
}
uri = null;
return false;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing)
{
}
~BclStorageFile()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

88
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -0,0 +1,88 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public class BclStorageFolder : IStorageBookmarkFolder
{
private readonly DirectoryInfo _directoryInfo;
public BclStorageFolder(DirectoryInfo directoryInfo)
{
_directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
if (!_directoryInfo.Exists)
{
throw new ArgumentException("Directory must exist", nameof(directoryInfo));
}
}
public string Name => _directoryInfo.Name;
public bool CanBookmark => true;
public Task<StorageItemProperties> GetBasicPropertiesAsync()
{
var props = new StorageItemProperties(
null,
_directoryInfo.CreationTimeUtc,
_directoryInfo.LastAccessTimeUtc);
return Task.FromResult(props);
}
public Task<IStorageFolder?> GetParentAsync()
{
if (_directoryInfo.Parent is { } directory)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
return Task.FromResult<IStorageFolder?>(null);
}
public virtual Task<string?> SaveBookmark()
{
return Task.FromResult<string?>(_directoryInfo.FullName);
}
public Task ReleaseBookmark()
{
// No-op
return Task.CompletedTask;
}
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
uri = Path.IsPathRooted(_directoryInfo.FullName) ?
new Uri(new Uri("file://"), _directoryInfo.FullName) :
new Uri(_directoryInfo.FullName, UriKind.Relative);
return true;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing)
{
}
~BclStorageFolder()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

35
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public abstract class BclStorageProvider : IStorageProvider
{
public abstract bool CanOpen { get; }
public abstract Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
public abstract bool CanSave { get; }
public abstract Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
public abstract bool CanPickFolder { get; }
public abstract Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);
public virtual Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
var file = new FileInfo(bookmark);
return file.Exists
? Task.FromResult<IStorageBookmarkFile?>(new BclStorageFile(file))
: Task.FromResult<IStorageBookmarkFile?>(null);
}
public virtual Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
var folder = new DirectoryInfo(bookmark);
return folder.Exists
? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder))
: Task.FromResult<IStorageBookmarkFolder?>(null);
}
}

40
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -0,0 +1,40 @@
using System;
using System.IO;
using System.Linq;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
[Unstable]
public static class StorageProviderHelpers
{
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
{
var name = Path.GetFileName(path);
if (name != null && !Path.HasExtension(name))
{
if (filter?.Patterns?.Count > 0)
{
if (defaultExtension != null
&& filter.Patterns.Contains(defaultExtension))
{
return Path.ChangeExtension(path, defaultExtension.TrimStart('.'));
}
var ext = filter.Patterns.FirstOrDefault(x => x != "*.*");
ext = ext?.Split(new[] { "*." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
if (ext != null)
{
return Path.ChangeExtension(path, ext);
}
}
if (defaultExtension != null)
{
return Path.ChangeExtension(path, defaultExtension);
}
}
return path;
}
}

44
src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs

@ -0,0 +1,44 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Represents a name mapped to the associated file types (extensions).
/// </summary>
public sealed class FilePickerFileType
{
public FilePickerFileType(string name)
{
Name = name;
}
/// <summary>
/// File type name.
/// </summary>
public string Name { get; }
/// <summary>
/// List of extensions in GLOB format. I.e. "*.png" or "*.*".
/// </summary>
/// <remarks>
/// Used on Windows and Linux systems.
/// </remarks>
public IReadOnlyList<string>? Patterns { get; set; }
/// <summary>
/// List of extensions in MIME format.
/// </summary>
/// <remarks>
/// Used on Android, Browser and Linux systems.
/// </remarks>
public IReadOnlyList<string>? MimeTypes { get; set; }
/// <summary>
/// List of extensions in Apple uniform format.
/// </summary>
/// <remarks>
/// Used only on Apple devices.
/// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
/// </remarks>
public IReadOnlyList<string>? AppleUniformTypeIdentifiers { get; set; }
}

48
src/Avalonia.Base/Platform/Storage/FilePickerFileTypes.cs

@ -0,0 +1,48 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Dictionary of well known file types.
/// </summary>
public static class FilePickerFileTypes
{
public static FilePickerFileType All { get; } = new("All")
{
Patterns = new[] { "*.*" },
MimeTypes = new[] { "*/*" }
};
public static FilePickerFileType TextPlain { get; } = new("Plain Text")
{
Patterns = new[] { "*.txt" },
AppleUniformTypeIdentifiers = new[] { "public.plain-text" },
MimeTypes = new[] { "text/plain" }
};
public static FilePickerFileType ImageAll { get; } = new("All Images")
{
Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp" },
AppleUniformTypeIdentifiers = new[] { "public.image" },
MimeTypes = new[] { "image/*" }
};
public static FilePickerFileType ImageJpg { get; } = new("JPEG image")
{
Patterns = new[] { "*.jpg", "*.jpeg" },
AppleUniformTypeIdentifiers = new[] { "public.jpeg" },
MimeTypes = new[] { "image/jpeg" }
};
public static FilePickerFileType ImagePng { get; } = new("PNG image")
{
Patterns = new[] { "*.png" },
AppleUniformTypeIdentifiers = new[] { "public.png" },
MimeTypes = new[] { "image/png" }
};
public static FilePickerFileType Pdf { get; } = new("PDF document")
{
Patterns = new[] { "*.pdf" },
AppleUniformTypeIdentifiers = new[] { "com.adobe.pdf" },
MimeTypes = new[] { "application/pdf" }
};
}

19
src/Avalonia.Base/Platform/Storage/FilePickerOpenOptions.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.OpenFilePickerAsync"/> method.
/// </summary>
public class FilePickerOpenOptions : PickerOptions
{
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple files.
/// </summary>
public bool AllowMultiple { get; set; }
/// <summary>
/// Gets or sets the collection of file types that the file open picker displays.
/// </summary>
public IReadOnlyList<FilePickerFileType>? FileTypeFilter { get; set; }
}

29
src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.SaveFilePickerAsync"/> method.
/// </summary>
public class FilePickerSaveOptions : PickerOptions
{
/// <summary>
/// Gets or sets the file name that the file save picker suggests to the user.
/// </summary>
public string? SuggestedFileName { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file.
/// </summary>
public string? DefaultExtension { get; set; }
/// <summary>
/// Gets or sets the collection of valid file types that the user can choose to assign to a file.
/// </summary>
public IReadOnlyList<FilePickerFileType>? FileTypeChoices { get; set; }
/// <summary>
/// Gets or sets a value indicating whether file open picker displays a warning if the user specifies the name of a file that already exists.
/// </summary>
public bool? ShowOverwritePrompt { get; set; }
}

12
src/Avalonia.Base/Platform/Storage/FolderPickerOpenOptions.cs

@ -0,0 +1,12 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Options class for <see cref="IStorageProvider.OpenFolderPickerAsync"/> method.
/// </summary>
public class FolderPickerOpenOptions : PickerOptions
{
/// <summary>
/// Gets or sets an option indicating whether open picker allows users to select multiple folders.
/// </summary>
public bool AllowMultiple { get; set; }
}

20
src/Avalonia.Base/Platform/Storage/IStorageBookmarkItem.cs

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
[NotClientImplementable]
public interface IStorageBookmarkItem : IStorageItem
{
Task ReleaseBookmark();
}
[NotClientImplementable]
public interface IStorageBookmarkFile : IStorageFile, IStorageBookmarkItem
{
}
[NotClientImplementable]
public interface IStorageBookmarkFolder : IStorageFolder, IStorageBookmarkItem
{
}

32
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@ -0,0 +1,32 @@
using System.IO;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Represents a file. Provides information about the file and its contents, and ways to manipulate them.
/// </summary>
[NotClientImplementable]
public interface IStorageFile : IStorageItem
{
/// <summary>
/// Returns true, if file is readable.
/// </summary>
bool CanOpenRead { get; }
/// <summary>
/// Opens a stream for read access.
/// </summary>
Task<Stream> OpenRead();
/// <summary>
/// Returns true, if file is writeable.
/// </summary>
bool CanOpenWrite { get; }
/// <summary>
/// Opens stream for writing to the file.
/// </summary>
Task<Stream> OpenWrite();
}

11
src/Avalonia.Base/Platform/Storage/IStorageFolder.cs

@ -0,0 +1,11 @@
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Manipulates folders and their contents, and provides information about them.
/// </summary>
[NotClientImplementable]
public interface IStorageFolder : IStorageItem
{
}

53
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

@ -0,0 +1,53 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Manipulates storage items (files and folders) and their contents, and provides information about them
/// </summary>
/// <remarks>
/// This interface inherits <see cref="IDisposable"/> . It's recommended to dispose <see cref="IStorageItem"/> when it's not used anymore.
/// </remarks>
[NotClientImplementable]
public interface IStorageItem : IDisposable
{
/// <summary>
/// Gets the name of the item including the file name extension if there is one.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the full file-system path of the item, if the item has a path.
/// </summary>
/// <remarks>
/// Android backend might return file path with "content:" scheme.
/// Browser and iOS backends might return relative uris.
/// </remarks>
bool TryGetUri([NotNullWhen(true)] out Uri? uri);
/// <summary>
/// Gets the basic properties of the current item.
/// </summary>
Task<StorageItemProperties> GetBasicPropertiesAsync();
/// <summary>
/// Returns true is item can be bookmarked and reused later.
/// </summary>
bool CanBookmark { get; }
/// <summary>
/// Saves items to a bookmark.
/// </summary>
/// <returns>
/// Returns identifier of a bookmark. Can be null if OS denied request.
/// </returns>
Task<string?> SaveBookmark();
/// <summary>
/// Gets the parent folder of the current storage item.
/// </summary>
Task<IStorageFolder?> GetParentAsync();
}

56
src/Avalonia.Base/Platform/Storage/IStorageProvider.cs

@ -0,0 +1,56 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage;
[NotClientImplementable]
public interface IStorageProvider
{
/// <summary>
/// Returns true if it's possible to open file picker on the current platform.
/// </summary>
bool CanOpen { get; }
/// <summary>
/// Opens file picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFile"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
/// <summary>
/// Returns true if it's possible to open save file picker on the current platform.
/// </summary>
bool CanSave { get; }
/// <summary>
/// Opens save file picker dialog.
/// </summary>
/// <returns>Saved <see cref="IStorageFile"/> or null if user canceled the dialog.</returns>
Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options);
/// <summary>
/// Returns true if it's possible to open folder picker on the current platform.
/// </summary>
bool CanPickFolder { get; }
/// <summary>
/// Opens folder picker dialog.
/// </summary>
/// <returns>Array of selected <see cref="IStorageFolder"/> or empty collection if user canceled the dialog.</returns>
Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options);
/// <summary>
/// Open <see cref="IStorageBookmarkFile"/> from the bookmark ID.
/// </summary>
/// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked file or null if OS denied request.</returns>
Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark);
/// <summary>
/// Open <see cref="IStorageBookmarkFolder"/> from the bookmark ID.
/// </summary>
/// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked folder or null if OS denied request.</returns>
Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark);
}

17
src/Avalonia.Base/Platform/Storage/PickerOptions.cs

@ -0,0 +1,17 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Common options for <see cref="IStorageProvider.OpenFolderPickerAsync"/>, <see cref="IStorageProvider.OpenFilePickerAsync"/> and <see cref="IStorageProvider.SaveFilePickerAsync"/> methods.
/// </summary>
public class PickerOptions
{
/// <summary>
/// Gets or sets the text that appears in the title bar of a folder dialog.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
}

43
src/Avalonia.Base/Platform/Storage/StorageItemProperties.cs

@ -0,0 +1,43 @@
using System;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Provides access to the content-related properties of an item (like a file or folder).
/// </summary>
public class StorageItemProperties
{
public StorageItemProperties(
ulong? size = null,
DateTimeOffset? dateCreated = null,
DateTimeOffset? dateModified = null)
{
Size = size;
DateCreated = dateCreated;
DateModified = dateModified;
}
/// <summary>
/// Gets the size of the file in bytes.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public ulong? Size { get; }
/// <summary>
/// Gets the date and time that the current folder was created.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public DateTimeOffset? DateCreated { get; }
/// <summary>
/// Gets the date and time of the last time the file was modified.
/// </summary>
/// <remarks>
/// Can be null if property is not available.
/// </remarks>
public DateTimeOffset? DateModified { get; }
}

26
src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Controls;
#nullable enable
@ -10,21 +11,17 @@ namespace Avalonia.Styling.Activators
/// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a
/// control.
/// </summary>
internal sealed class StyleClassActivator : StyleActivatorBase
internal sealed class StyleClassActivator : StyleActivatorBase, IClassesChangedListener
{
private readonly IList<string> _match;
private readonly IAvaloniaReadOnlyList<string> _classes;
private NotifyCollectionChangedEventHandler? _classesChangedHandler;
private readonly Classes _classes;
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match)
public StyleClassActivator(Classes classes, IList<string> match)
{
_classes = classes;
_match = match;
}
private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
_classesChangedHandler ??= ClassesChanged;
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{
int remainingMatches = toMatch.Count;
@ -55,23 +52,20 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0;
}
protected override void Initialize()
void IClassesChangedListener.Changed()
{
PublishNext(IsMatching());
_classes.CollectionChanged += ClassesChangedHandler;
}
protected override void Deinitialize()
protected override void Initialize()
{
_classes.CollectionChanged -= ClassesChangedHandler;
PublishNext(IsMatching());
_classes.AddListener(this);
}
private void ClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
protected override void Deinitialize()
{
if (e.Action != NotifyCollectionChangedAction.Move)
{
PublishNext(IsMatching());
}
_classes.RemoveListener(this);
}
private bool IsMatching() => AreClassesMatching(_classes, _match);

3
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Controls;
using Avalonia.Styling.Activators;
#nullable enable
@ -125,7 +126,7 @@ namespace Avalonia.Styling
{
if (subscribe)
{
var observable = new StyleClassActivator(control.Classes, _classes.Value);
var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value);
return new SelectorMatch(observable);
}

17
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -5990,15 +5990,14 @@ namespace Avalonia.Controls
/// <returns>The formatted string.</returns>
private string FormatClipboardContent(DataGridRowClipboardEventArgs e)
{
StringBuilder text = new StringBuilder();
for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++)
{
DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex];
if (cellContent != null)
{
text.Append(cellContent.Content);
}
if (cellIndex < e.ClipboardRowContent.Count - 1)
var text = new StringBuilder();
var clipboardRowContent = e.ClipboardRowContent;
var numberOfItem = clipboardRowContent.Count;
for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++)
{
var cellContent = clipboardRowContent[cellIndex];
text.Append(cellContent.Content);
if (cellIndex < numberOfItem - 1)
{
text.Append('\t');
}

27
src/Avalonia.Controls/Converters/StringFormatConverter.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters;
/// <summary>
/// Calls <see cref="string.Format(string, object[])"/> on the passed in values, where the first element in the list
/// is the string, and everything after it is passed into the object array in order.
/// </summary>
public class StringFormatConverter : IMultiValueConverter
{
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
try
{
return string.Format((string)values[0]!, values.Skip(1).ToArray());
}
catch (Exception e)
{
return new BindingNotification(e, BindingErrorType.Error);
}
}
}

72
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -12,39 +12,55 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline>
{
private readonly IInlineHost? _host;
private ILogical? _parent;
private IInlineHost? _inlineHost;
private string? _text = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
public InlineCollection(ILogical parent) : this(parent, null) { }
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0)
public InlineCollection()
{
_host = host;
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
((ISetLogicalParent)x).SetParent(parent);
x.InlineHost = host;
host?.Invalidate();
((ISetLogicalParent)x).SetParent(Parent);
x.InlineHost = InlineHost;
Invalidate();
},
x =>
{
((ISetLogicalParent)x).SetParent(null);
x.InlineHost = host;
host?.Invalidate();
x.InlineHost = InlineHost;
Invalidate();
},
() => throw new NotSupportedException());
}
internal ILogical? Parent
{
get => _parent;
set
{
_parent = value;
OnParentChanged(value);
}
}
internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
_inlineHost = value;
OnInlineHostChanged(value);
}
}
public bool HasComplexContent => Count > 0;
/// <summary>
@ -61,10 +77,10 @@ namespace Avalonia.Controls.Documents
{
return _text;
}
var builder = new StringBuilder();
foreach(var inline in this)
foreach (var inline in this)
{
inline.AppendText(builder);
}
@ -100,7 +116,7 @@ namespace Avalonia.Controls.Documents
}
else
{
_text += text;
_text = text;
}
}
@ -120,7 +136,7 @@ namespace Avalonia.Controls.Documents
base.Add(new Run(_text));
}
_text = string.Empty;
_text = null;
}
base.Add(item);
@ -136,14 +152,28 @@ namespace Avalonia.Controls.Documents
/// </summary>
protected void Invalidate()
{
if(_host != null)
if(InlineHost != null)
{
_host.Invalidate();
InlineHost.Invalidate();
}
Invalidated?.Invoke(this, EventArgs.Empty);
}
private void Invalidate(object? sender, EventArgs e) => Invalidate();
private void OnParentChanged(ILogical? parent)
{
foreach(var child in this)
{
((ISetLogicalParent)child).SetParent(parent);
}
}
private void OnInlineHostChanged(IInlineHost? inlineHost)
{
foreach (var child in this)
{
child.InlineHost = inlineHost;
}
}
}
}

70
src/Avalonia.Controls/Documents/Span.cs

@ -14,25 +14,27 @@ namespace Avalonia.Controls.Documents
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<Span, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<Span, InlineCollection>(
nameof(Inlines),
o => o.Inlines);
public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.Register<Span, InlineCollection>(
nameof(Inlines));
/// <summary>
/// Initializes a new instance of a Span element.
/// </summary>
public Span()
{
Inlines = new InlineCollection(this);
Inlines.Invalidated += (s, e) => InlineHost?.Invalidate();
Inlines = new InlineCollection
{
Parent = this
};
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
public InlineCollection Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
internal override void BuildTextRun(IList<TextRun> textRuns)
{
@ -52,7 +54,7 @@ namespace Avalonia.Controls.Documents
var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties);
textRuns.Add(textCharacters);
}
}
}
}
@ -65,10 +67,52 @@ namespace Avalonia.Controls.Documents
inline.AppendText(stringBuilder);
}
}
else
{
if (Inlines.Text is string text)
{
stringBuilder.Append(text);
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InlineHost?.Invalidate();
break;
}
}
internal override void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
base.OnInlineHostChanged(oldValue, newValue);
if(Inlines is not null)
{
Inlines.InlineHost = newValue;
}
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (Inlines.Text is string text)
if (newValue is not null)
{
stringBuilder.Append(text);
newValue.Parent = this;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
}
}
}

18
src/Avalonia.Controls/Documents/TextElement.cs

@ -67,6 +67,8 @@ namespace Avalonia.Controls.Documents
Brushes.Black,
inherits: true);
private IInlineHost? _inlineHost;
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
@ -250,7 +252,21 @@ namespace Avalonia.Controls.Documents
control.SetValue(ForegroundProperty, value);
}
internal IInlineHost? InlineHost { get; set; }
internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
var oldValue = _inlineHost;
_inlineHost = value;
OnInlineHostChanged(oldValue, value);
}
}
internal virtual void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{

12
src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs

@ -0,0 +1,12 @@
#nullable enable
using Avalonia.Platform.Storage;
namespace Avalonia.Controls.Platform;
/// <summary>
/// Factory allows to register custom storage provider instead of native implementation.
/// </summary>
public interface IStorageProviderFactory
{
IStorageProvider CreateProvider(TopLevel topLevel);
}

2
src/Avalonia.Controls/Platform/ISystemDialogImpl.cs → src/Avalonia.Controls/Platform/Dialogs/ISystemDialogImpl.cs

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using Avalonia.Metadata;
@ -6,6 +7,7 @@ namespace Avalonia.Controls.Platform
/// <summary>
/// Defines a platform-specific system dialog implementation.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
[Unstable]
public interface ISystemDialogImpl
{

74
src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs

@ -0,0 +1,74 @@
using System;
using System.Linq;
using System.Threading.Tasks;
#nullable enable
namespace Avalonia.Controls.Platform
{
/// <summary>
/// Defines a platform-specific system dialog implementation.
/// </summary>
[Obsolete]
internal class SystemDialogImpl : ISystemDialogImpl
{
public async Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
if (dialog is OpenFileDialog openDialog)
{
var filePicker = parent.StorageProvider;
if (!filePicker.CanOpen)
{
return null;
}
var options = openDialog.ToFilePickerOpenOptions();
var files = await filePicker.OpenFilePickerAsync(options);
return files
.Select(file => file.TryGetUri(out var fullPath)
? fullPath.LocalPath
: file.Name)
.ToArray();
}
else if (dialog is SaveFileDialog saveDialog)
{
var filePicker = parent.StorageProvider;
if (!filePicker.CanSave)
{
return null;
}
var options = saveDialog.ToFilePickerSaveOptions();
var file = await filePicker.SaveFilePickerAsync(options);
if (file is null)
{
return null;
}
var filePath = file.TryGetUri(out var fullPath)
? fullPath.LocalPath
: file.Name;
return new[] { filePath };
}
return null;
}
public async Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
var filePicker = parent.StorageProvider;
if (!filePicker.CanPickFolder)
{
return null;
}
var options = dialog.ToFolderPickerOpenOptions();
var folders = await filePicker.OpenFolderPickerAsync(options);
return folders
.Select(f => f.TryGetUri(out var uri) ? uri.LocalPath : null)
.FirstOrDefault(u => u is not null);
}
}
}

11
src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs

@ -0,0 +1,11 @@
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
namespace Avalonia.Controls.Platform;
[Unstable]
public interface ITopLevelImplWithStorageProvider : ITopLevelImpl
{
public IStorageProvider StorageProvider { get; }
}

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

@ -543,9 +543,11 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
if (finalSize.Width < TextLayout.Bounds.Width)
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if (finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
finalSize = finalSize.WithWidth(textWidth);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
@ -553,7 +555,7 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
_constraint = new Size(finalSize.Width, double.PositiveInfinity);
_constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity);
_textLayout = null;

4
src/Avalonia.Controls/Primitives/AccessText.cs

@ -79,9 +79,9 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
protected override TextLayout CreateTextLayout(Size constraint, string? text)
protected override TextLayout CreateTextLayout(string? text)
{
return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text));
return base.CreateTextLayout(RemoveAccessKeyMarker(text));
}
/// <inheritdoc/>

3
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -387,6 +387,7 @@ namespace Avalonia.Controls.Primitives
/// Sets the TemplatedParent property for the created template children.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="templatedParent">The templated parent to apply.</param>
internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent)
{
control.SetValue(TemplatedParentProperty, templatedParent);
@ -396,7 +397,7 @@ namespace Avalonia.Controls.Primitives
for (var i = 0; i < count; i++)
{
if (children[i] is IStyledElement child)
if (children[i] is IStyledElement child && child.TemplatedParent is null)
{
ApplyTemplatedParent(child, templatedParent);
}

44
src/Avalonia.Controls/ProgressBar.cs

@ -96,6 +96,7 @@ namespace Avalonia.Controls
}
}
private double _percentage;
private double _indeterminateStartingOffset;
private double _indeterminateEndingOffset;
private Border? _indicator;
@ -106,9 +107,17 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> ShowProgressTextProperty =
AvaloniaProperty.Register<ProgressBar, bool>(nameof(ShowProgressText));
public static readonly StyledProperty<string> ProgressTextFormatProperty =
AvaloniaProperty.Register<ProgressBar, string>(nameof(ProgressTextFormat), "{1:0}%");
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
public static readonly DirectProperty<ProgressBar, double> PercentageProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(Percentage),
o => o.Percentage);
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
@ -123,6 +132,12 @@ namespace Avalonia.Controls
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
public double Percentage
{
get { return _percentage; }
private set { SetAndRaise(PercentageProperty, ref _percentage, value); }
}
[Obsolete("To be removed when Avalonia.Themes.Default is discontinued.")]
public double IndeterminateStartingOffset
{
@ -165,6 +180,12 @@ namespace Avalonia.Controls
set => SetValue(ShowProgressTextProperty, value);
}
public string ProgressTextFormat
{
get => GetValue(ProgressTextFormatProperty);
set => SetValue(ProgressTextFormatProperty, value);
}
public Orientation Orientation
{
get => GetValue(OrientationProperty);
@ -174,7 +195,7 @@ namespace Avalonia.Controls
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
UpdateIndicator(finalSize);
UpdateIndicator();
return base.ArrangeOverride(finalSize);
}
@ -197,18 +218,21 @@ namespace Avalonia.Controls
{
_indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size);
UpdateIndicator();
}
private void UpdateIndicator(Size bounds)
private void UpdateIndicator()
{
// Gets the size of the parent indicator container
var barSize = _indicator?.Parent?.Bounds.Size ?? Bounds.Size;
if (_indicator != null)
{
if (IsIndeterminate)
{
// Pulled from ModernWPF.
var dim = Orientation == Orientation.Horizontal ? bounds.Width : bounds.Height;
var dim = Orientation == Orientation.Horizontal ? barSize.Width : barSize.Height;
var barIndicatorWidth = dim * 0.4; // Indicator width at 40% of ProgressBar
var barIndicatorWidth2 = dim * 0.6; // Indicator width at 60% of ProgressBar
@ -233,8 +257,8 @@ namespace Avalonia.Controls
new Rect(
padding.Left,
padding.Top,
bounds.Width - (padding.Right + padding.Left),
bounds.Height - (padding.Bottom + padding.Top)
barSize.Width - (padding.Right + padding.Left),
barSize.Height - (padding.Bottom + padding.Top)
));
}
else
@ -242,16 +266,18 @@ namespace Avalonia.Controls
double percent = Maximum == Minimum ? 1.0 : (Value - Minimum) / (Maximum - Minimum);
if (Orientation == Orientation.Horizontal)
_indicator.Width = bounds.Width * percent;
_indicator.Width = barSize.Width * percent;
else
_indicator.Height = bounds.Height * percent;
_indicator.Height = barSize.Height * percent;
Percentage = percent * 100;
}
}
}
private void UpdateIndicatorWhenPropChanged(AvaloniaPropertyChangedEventArgs e)
{
UpdateIndicator(Bounds.Size);
UpdateIndicator();
}
private void UpdatePseudoClasses(

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

@ -421,6 +421,7 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
InvalidateMeasure();
_viewportManager.ResetScrollers();
}

574
src/Avalonia.Controls/RichTextBlock.cs

@ -0,0 +1,574 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// A control that displays a block of formatted text.
/// </summary>
public class RichTextBlock : TextBlock, IInlineHost
{
public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty =
AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false);
public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, string>(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionForegroundBrush));
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.Register<RichTextBlock, InlineCollection>(
nameof(Inlines));
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
static RichTextBlock()
{
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty);
}
public RichTextBlock()
{
Inlines = new InlineCollection
{
Parent = this,
InlineHost = this
};
}
/// <summary>
/// Gets or sets the brush that highlights selected text.
/// </summary>
public IBrush? SelectionBrush
{
get => GetValue(SelectionBrushProperty);
set => SetValue(SelectionBrushProperty, value);
}
/// <summary>
/// Gets or sets a value that defines the brush used for selected text.
/// </summary>
public IBrush? SelectionForegroundBrush
{
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
/// <summary>
/// Gets or sets a character index for the beginning of the current selection.
/// </summary>
public int SelectionStart
{
get => _selectionStart;
set
{
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
}
}
}
/// <summary>
/// Gets or sets a character index for the end of the current selection.
/// </summary>
public int SelectionEnd
{
get => _selectionEnd;
set
{
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
}
}
}
/// <summary>
/// Gets the content of the current selection.
/// </summary>
public string SelectedText
{
get => GetSelection();
}
/// <summary>
/// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
/// </summary>
public bool IsTextSelectionEnabled
{
get => GetValue(IsTextSelectionEnabledProperty);
set => SetValue(IsTextSelectionEnabledProperty, value);
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
public bool CanCopy
{
get => _canCopy;
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
/// <summary>
/// Copies the current selection to the Clipboard.
/// </summary>
public async void Copy()
{
if (_canCopy || !IsTextSelectionEnabled)
{
return;
}
var text = GetSelection();
if (string.IsNullOrEmpty(text))
{
return;
}
var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text);
}
}
public override void Render(DrawingContext context)
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var selectionBrush = SelectionBrush;
var selectionEnabled = IsTextSelectionEnabled;
if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
{
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
var rects = TextLayout.HitTestTextRange(start, length);
foreach (var rect in rects)
{
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
}
}
base.Render(context);
}
/// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll()
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
SelectionStart = 0;
SelectionEnd = text?.Length ?? 0;
}
/// <summary>
/// Clears the current selection/>
/// </summary>
public void ClearSelection()
{
if (!IsTextSelectionEnabled)
{
return;
}
SelectionEnd = SelectionStart;
}
protected override string? GetText()
{
return _text ?? Inlines.Text;
}
protected override void SetText(string? text)
{
var oldValue = _text ?? Inlines?.Text;
if (Inlines is not null && Inlines.HasComplexContent)
{
Inlines.Text = text;
_text = null;
}
else
{
_text = text;
}
RaisePropertyChanged(TextProperty, oldValue, text);
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(string? text)
{
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
var inlines = Inlines;
if (inlines is not null && inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout(
textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
ClearSelection();
}
protected override void OnKeyDown(KeyEventArgs e)
{
var handled = false;
var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
if (Match(keymap.Copy))
{
Copy();
handled = true;
}
e.Handled = handled;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
if (text != null && clickInfo.Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(this);
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
var oldIndex = SelectionStart;
var hit = TextLayout.HitTestPoint(point);
var index = hit.TextPosition;
SelectionStart = SelectionEnd = index;
#pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete
{
case 1:
if (clickToSelect)
{
SelectionStart = Math.Min(oldIndex, index);
SelectionEnd = Math.Max(oldIndex, index);
}
else
{
SelectionStart = SelectionEnd = index;
}
break;
case 2:
if (!StringUtils.IsStartOfWord(text, index))
{
SelectionStart = StringUtils.PreviousWord(text, index);
}
SelectionEnd = StringUtils.NextWord(text, index);
break;
case 3:
SelectAll();
break;
}
}
e.Pointer.Capture(this);
e.Handled = true;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
// selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(this);
point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(Bounds.Height - 1, 0)));
var hit = TextLayout.HitTestPoint(point);
SelectionEnd = hit.TextPosition;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
if (e.Pointer.Captured != this)
{
return;
}
if (e.InitialPressMouseButton == MouseButton.Right)
{
var point = e.GetPosition(this);
var hit = TextLayout.HitTestPoint(point);
var caretIndex = hit.TextPosition;
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
SelectionStart = SelectionEnd = caretIndex;
}
}
e.Pointer.Capture(null);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
{
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InvalidateTextLayout();
break;
}
case nameof(TextProperty):
{
InvalidateTextLayout();
break;
}
}
}
private string GetSelection()
{
if (!IsTextSelectionEnabled)
{
return "";
}
var text = Inlines.Text ?? Text;
if (string.IsNullOrEmpty(text))
{
return "";
}
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
if (start == end || text.Length < end)
{
return "";
}
var length = Math.Max(0, end - start);
var selectedText = text.Substring(start, length);
return selectedText;
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
}
if (newValue is not null)
{
newValue.Parent = this;
newValue.InlineHost = this;
newValue.Invalidated += (s, e) => InvalidateTextLayout();
}
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
{
continue;
}
if (currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
}
}

32
src/Avalonia.Controls/SplitView.cs

@ -431,23 +431,45 @@ namespace Avalonia.Controls
}
}
private string GetPseudoClass(SplitViewDisplayMode mode)
{
return mode switch
{
SplitViewDisplayMode.Inline => "inline",
SplitViewDisplayMode.CompactInline => "compactinline",
SplitViewDisplayMode.Overlay => "overlay",
SplitViewDisplayMode.CompactOverlay => "compactoverlay",
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
};
}
private string GetPseudoClass(SplitViewPanePlacement placement)
{
return placement switch
{
SplitViewPanePlacement.Left => "left",
SplitViewPanePlacement.Right => "right",
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue!.ToString()!.ToLower();
var newState = e.NewValue!.ToString()!.ToLower();
var oldState = GetPseudoClass(e.GetOldValue<SplitViewPanePlacement>());
var newState = GetPseudoClass(e.GetNewValue<SplitViewPanePlacement>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
}
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue!.ToString()!.ToLower();
var newState = e.NewValue!.ToString()!.ToLower();
var oldState = GetPseudoClass(e.GetOldValue<SplitViewDisplayMode>());
var newState = GetPseudoClass(e.GetNewValue<SplitViewDisplayMode>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch
var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue<SplitViewDisplayMode>() switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),

57
src/Avalonia.Controls/SystemDialog.cs

@ -3,12 +3,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Controls
{
/// <summary>
/// Base class for system file dialogs.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class FileDialog : FileSystemDialog
{
/// <summary>
@ -26,6 +29,7 @@ namespace Avalonia.Controls
/// <summary>
/// Base class for system file and directory dialogs.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class FileSystemDialog : SystemDialog
{
[Obsolete("Use Directory")]
@ -45,6 +49,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a system dialog that prompts the user to select a location for saving a file.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class SaveFileDialog : FileDialog
{
/// <summary>
@ -73,11 +78,27 @@ namespace Avalonia.Controls
return (await service.ShowFileDialogAsync(this, parent) ??
Array.Empty<string>()).FirstOrDefault();
}
public FilePickerSaveOptions ToFilePickerSaveOptions()
{
return new FilePickerSaveOptions
{
SuggestedFileName = InitialFileName,
DefaultExtension = DefaultExtension,
FileTypeChoices = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(),
Title = Title,
SuggestedStartLocation = InitialDirectory is { } directory
? new BclStorageFolder(new System.IO.DirectoryInfo(directory))
: null,
ShowOverwritePrompt = ShowOverwritePrompt
};
}
}
/// <summary>
/// Represents a system dialog that allows the user to select one or more files to open.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class OpenFileDialog : FileDialog
{
/// <summary>
@ -100,11 +121,25 @@ namespace Avalonia.Controls
var service = AvaloniaLocator.Current.GetRequiredService<ISystemDialogImpl>();
return service.ShowFileDialogAsync(this, parent);
}
public FilePickerOpenOptions ToFilePickerOpenOptions()
{
return new FilePickerOpenOptions
{
AllowMultiple = AllowMultiple,
FileTypeFilter = Filters?.Select(f => new FilePickerFileType(f.Name!) { Patterns = f.Extensions.Select(e => $"*.{e}").ToArray() }).ToArray(),
Title = Title,
SuggestedStartLocation = InitialDirectory is { } directory
? new BclStorageFolder(new System.IO.DirectoryInfo(directory))
: null
};
}
}
/// <summary>
/// Represents a system dialog that allows the user to select a directory.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class OpenFolderDialog : FileSystemDialog
{
[Obsolete("Use Directory")]
@ -129,14 +164,35 @@ namespace Avalonia.Controls
var service = AvaloniaLocator.Current.GetRequiredService<ISystemDialogImpl>();
return service.ShowFolderDialogAsync(this, parent);
}
public FolderPickerOpenOptions ToFolderPickerOpenOptions()
{
return new FolderPickerOpenOptions
{
Title = Title,
SuggestedStartLocation = InitialDirectory is { } directory
? new BclStorageFolder(new System.IO.DirectoryInfo(directory))
: null
};
}
}
/// <summary>
/// Base class for system dialogs.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public abstract class SystemDialog
{
static SystemDialog()
{
if (AvaloniaLocator.Current.GetService<ISystemDialogImpl>() is null)
{
// Register default implementation.
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>();
}
}
/// <summary>
/// Gets or sets the dialog title.
/// </summary>
@ -146,6 +202,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a filter in an <see cref="OpenFileDialog"/> or an <see cref="SaveFileDialog"/>.
/// </summary>
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public class FileDialogFilter
{
/// <summary>

157
src/Avalonia.Controls/TextBlock.cs

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@ -14,7 +11,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control that displays a block of text.
/// </summary>
public class TextBlock : Control, IInlineHost
public class TextBlock : Control
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -101,14 +98,6 @@ namespace Avalonia.Controls
o => o.Text,
(o, v) => o.Text = v);
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<TextBlock, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection>(
nameof(Inlines),
o => o.Inlines);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
@ -139,8 +128,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
private TextLayout? _textLayout;
private Size _constraint;
protected string? _text;
protected TextLayout? _textLayout;
protected Size _constraint;
/// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class.
@ -152,14 +142,6 @@ namespace Avalonia.Controls
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="TextBlock"/> class.
/// </summary>
public TextBlock()
{
Inlines = new InlineCollection(this, this);
}
/// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -167,7 +149,7 @@ namespace Avalonia.Controls
{
get
{
return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text));
return _textLayout ??= CreateTextLayout(_text);
}
}
@ -194,28 +176,10 @@ namespace Avalonia.Controls
/// </summary>
public string? Text
{
get => Inlines.Text;
set
{
var old = Text;
if (value == old)
{
return;
}
Inlines.Text = value;
RaisePropertyChanged(TextProperty, old, value);
}
get => GetText();
set => SetText(value);
}
/// <summary>
/// Gets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
/// <summary>
/// Gets or sets the font family used to draw the control's text.
/// </summary>
@ -509,6 +473,10 @@ namespace Avalonia.Controls
control.SetValue(MaxLinesProperty, maxLines);
}
public void Add(string text)
{
_text = text;
}
/// <summary>
/// Renders the <see cref="TextBlock"/> to a drawing context.
@ -544,13 +512,21 @@ namespace Avalonia.Controls
TextLayout.Draw(context, new Point(padding.Left, top));
}
protected virtual string? GetText()
{
return _text;
}
protected virtual void SetText(string? text)
{
SetAndRaise(TextProperty, ref _text, text);
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
protected virtual TextLayout CreateTextLayout(string? text)
{
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
@ -561,30 +537,12 @@ namespace Avalonia.Controls
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
if (Inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in Inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout(
textSource,
new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
paragraphProperties,
TextTrimming,
constraint.Width,
constraint.Height,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
@ -601,11 +559,6 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize)
{
if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
{
return new Size();
}
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@ -623,9 +576,11 @@ namespace Avalonia.Controls
protected override Size ArrangeOverride(Size finalSize)
{
if(finalSize.Width < TextLayout.Bounds.Width)
var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if(finalSize.Width < textWidth)
{
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width);
finalSize = finalSize.WithWidth(textWidth);
}
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
@ -637,7 +592,7 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = new Size(finalSize.Deflate(padding).Width, double.PositiveInfinity);
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout = null;
@ -685,57 +640,7 @@ namespace Avalonia.Controls
}
}
private void InlinesChanged(object? sender, EventArgs e)
{
InvalidateTextLayout();
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if(textRun.TextSourceLength == 0)
{
continue;
}
if(currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
private readonly struct SimpleTextSource : ITextSource
protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties;

9
src/Avalonia.Controls/TopLevel.cs

@ -11,6 +11,7 @@ using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
@ -93,7 +94,8 @@ namespace Avalonia.Controls
private ILayoutManager? _layoutManager;
private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider;
/// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class.
/// </summary>
@ -319,6 +321,11 @@ namespace Avalonia.Controls
double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1;
IStyleHost IStyleHost.StylingParent => _globalStyles!;
public IStorageProvider StorageProvider => _storageProvider
??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider
?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget();

5
src/Avalonia.Controls/TrayIcon.cs

@ -189,7 +189,10 @@ namespace Avalonia.Controls
var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized.");
var trayIcons = GetIcons(app);
RemoveIcons(trayIcons);
if (trayIcons != null)
{
RemoveIcons(trayIcons);
}
}
private static void Icons_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)

6
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -1,15 +1,17 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Remote.Server;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Viewport;
using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl
class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl, ITopLevelImplWithStorageProvider
{
private readonly IAvaloniaRemoteTransportConnection _transport;
@ -90,6 +92,8 @@ namespace Avalonia.DesignerSupport.Remote
public bool NeedsManagedDecorations => false;
public IStorageProvider StorageProvider => new NoopStorageProvider();
public void Activate()
{
}

1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -55,7 +55,6 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();

32
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -11,6 +11,8 @@ using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Rendering;
namespace Avalonia.DesignerSupport.Remote
@ -222,15 +224,6 @@ namespace Avalonia.DesignerSupport.Remote
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub();
}
class SystemDialogsStub : ISystemDialogImpl
{
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) =>
Task.FromResult((string[])null);
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) =>
Task.FromResult((string)null);
}
class ScreenStub : IScreenImpl
{
public int ScreenCount => 1;
@ -253,4 +246,25 @@ namespace Avalonia.DesignerSupport.Remote
return ScreenHelper.ScreenFromWindow(window, AllScreens);
}
}
internal class NoopStorageProvider : BclStorageProvider
{
public override bool CanOpen => false;
public override Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
}
public override bool CanSave => false;
public override Task<IStorageFile> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromResult<IStorageFile>(null);
}
public override bool CanPickFolder => false;
public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());
}
}
}

2
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml

@ -19,7 +19,7 @@
Classes="textBoxClearButton"
ToolTip.Tip="Clear"
Cursor="Hand"
Command="{ReflectionBinding $parent[TextBox].Clear}"
Command="{Binding $parent[TextBox].Clear}"
Opacity="0.5" />
<ToggleButton Classes="filter-text-box-toggle"
ToolTip.Tip="Match Case"

4
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -14,10 +14,6 @@
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
</Project>

35
src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs

@ -1,48 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using System.Text.RegularExpressions;
using Avalonia.Platform.Storage;
namespace Avalonia.Dialogs
{
internal class ManagedFileChooserFilterViewModel : InternalViewModelBase
{
private readonly string[] _extensions;
private readonly Regex[] _patterns;
public string Name { get; }
public ManagedFileChooserFilterViewModel(FileDialogFilter filter)
public ManagedFileChooserFilterViewModel(FilePickerFileType filter)
{
Name = filter.Name;
if (filter.Extensions.Contains("*"))
if (filter.Patterns?.Contains("*.*") == true)
{
return;
}
_extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray();
}
public ManagedFileChooserFilterViewModel()
{
Name = "All files";
_patterns = filter.Patterns?
.Select(e => new Regex(Regex.Escape(e).Replace(@"\*", ".*").Replace(@"\?", "."), RegexOptions.Singleline | RegexOptions.IgnoreCase))
.ToArray();
}
public bool Match(string filename)
{
if (_extensions == null)
{
return true;
}
foreach (var ext in _extensions)
{
if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
}
return false;
return _patterns == null || _patterns.Any(ext => ext.IsMatch(filename));
}
public override string ToString() => Name;

97
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
namespace Avalonia.Dialogs
@ -106,7 +107,7 @@ namespace Avalonia.Dialogs
QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i)));
}
public ManagedFileChooserViewModel(FileSystemDialog dialog, ManagedFileDialogOptions options)
public ManagedFileChooserViewModel(ManagedFileDialogOptions options)
{
_options = options;
_disposables = new CompositeDisposable();
@ -131,50 +132,63 @@ namespace Avalonia.Dialogs
CancelRequested += delegate { _disposables?.Dispose(); };
RefreshQuickLinks(quickSources);
SelectedItems.CollectionChanged += OnSelectionChangedAsync;
}
Title = dialog.Title ?? (
dialog is OpenFileDialog ? "Open file"
: dialog is SaveFileDialog ? "Save file"
: dialog is OpenFolderDialog ? "Select directory"
: throw new ArgumentException(nameof(dialog)));
var directory = dialog.Directory;
if (directory == null || !Directory.Exists(directory))
public ManagedFileChooserViewModel(FilePickerOpenOptions filePickerOpen, ManagedFileDialogOptions options)
: this(options)
{
Title = filePickerOpen.Title ?? "Open file";
if (filePickerOpen.FileTypeFilter?.Count > 0)
{
directory = Directory.GetCurrentDirectory();
Filters.AddRange(filePickerOpen.FileTypeFilter.Select(f => new ManagedFileChooserFilterViewModel(f)));
_selectedFilter = Filters[0];
ShowFilters = true;
}
if (dialog is FileDialog fd)
if (filePickerOpen.AllowMultiple)
{
if (fd.Filters?.Count > 0)
{
Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f)));
_selectedFilter = Filters[0];
ShowFilters = true;
}
SelectionMode = SelectionMode.Multiple;
}
if (dialog is OpenFileDialog ofd)
{
if (ofd.AllowMultiple)
{
SelectionMode = SelectionMode.Multiple;
}
}
Navigate(filePickerOpen.SuggestedStartLocation);
}
public ManagedFileChooserViewModel(FilePickerSaveOptions filePickerSave, ManagedFileDialogOptions options)
: this(options)
{
Title = filePickerSave.Title ?? "Save file";
if (filePickerSave.FileTypeChoices?.Count > 0)
{
Filters.AddRange(filePickerSave.FileTypeChoices.Select(f => new ManagedFileChooserFilterViewModel(f)));
_selectedFilter = Filters[0];
ShowFilters = true;
}
_savingFile = true;
_defaultExtension = filePickerSave.DefaultExtension;
_overwritePrompt = filePickerSave.ShowOverwritePrompt ?? true;
FileName = filePickerSave.SuggestedFileName;
Navigate(filePickerSave.SuggestedStartLocation, FileName);
}
public ManagedFileChooserViewModel(FolderPickerOpenOptions folderPickerOpen, ManagedFileDialogOptions options)
: this(options)
{
Title = folderPickerOpen.Title ?? "Select directory";
_selectingDirectory = dialog is OpenFolderDialog;
_selectingDirectory = true;
if (dialog is SaveFileDialog sfd)
if (folderPickerOpen.AllowMultiple)
{
_savingFile = true;
_defaultExtension = sfd.DefaultExtension;
_overwritePrompt = sfd.ShowOverwritePrompt ?? true;
FileName = sfd.InitialFileName;
SelectionMode = SelectionMode.Multiple;
}
Navigate(directory, (dialog as FileDialog)?.InitialFileName);
SelectedItems.CollectionChanged += OnSelectionChangedAsync;
Navigate(folderPickerOpen.SuggestedStartLocation);
}
public void EnterPressed()
@ -247,6 +261,23 @@ namespace Avalonia.Dialogs
public void Refresh() => Navigate(Location);
public void Navigate(IStorageFolder path, string initialSelectionName = null)
{
string fullDirectoryPath;
if (path?.TryGetUri(out var fullDirectoryUri) == true
&& fullDirectoryUri.IsAbsoluteUri)
{
fullDirectoryPath = fullDirectoryUri.LocalPath;
}
else
{
fullDirectoryPath = Directory.GetCurrentDirectory();
}
Navigate(fullDirectoryPath, initialSelectionName);
}
public void Navigate(string path, string initialSelectionName = null)
{
if (!Directory.Exists(path))

142
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -1,125 +1,26 @@
using System.IO;
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Platform.Storage;
namespace Avalonia.Dialogs
{
public static class ManagedFileDialogExtensions
{
internal class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
internal class ManagedStorageProviderFactory<T> : IStorageProviderFactory where T : Window, new()
{
async Task<string[]> Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null)
public IStorageProvider CreateProvider(TopLevel topLevel)
{
var model = new ManagedFileChooserViewModel((FileSystemDialog)d,
options ?? new ManagedFileDialogOptions());
var dialog = new T
{
Content = new ManagedFileChooser(),
Title = d.Title,
DataContext = model
};
dialog.Closed += delegate { model.Cancel(); };
string[] result = null;
model.CompleteRequested += items =>
{
result = items;
dialog.Close();
};
model.OverwritePrompt += async (filename) =>
if (topLevel is Window window)
{
Window overwritePromptDialog = new Window()
{
Title = "Confirm Save As",
SizeToContent = SizeToContent.WidthAndHeight,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Padding = new Thickness(10),
MinWidth = 270
};
string name = Path.GetFileName(filename);
var panel = new DockPanel()
{
HorizontalAlignment = Layout.HorizontalAlignment.Stretch
};
var label = new Label()
{
Content = $"{name} already exists.\nDo you want to replace it?"
};
panel.Children.Add(label);
DockPanel.SetDock(label, Dock.Top);
var buttonPanel = new StackPanel()
{
HorizontalAlignment = Layout.HorizontalAlignment.Right,
Orientation = Layout.Orientation.Horizontal,
Spacing = 10
};
var button = new Button()
{
Content = "Yes",
HorizontalAlignment = Layout.HorizontalAlignment.Right
};
button.Click += (sender, args) =>
{
result = new string[1] { filename };
overwritePromptDialog.Close();
dialog.Close();
};
buttonPanel.Children.Add(button);
button = new Button()
{
Content = "No",
HorizontalAlignment = Layout.HorizontalAlignment.Right
};
button.Click += (sender, args) =>
{
overwritePromptDialog.Close();
};
buttonPanel.Children.Add(button);
panel.Children.Add(buttonPanel);
DockPanel.SetDock(buttonPanel, Dock.Bottom);
overwritePromptDialog.Content = panel;
await overwritePromptDialog.ShowDialog(dialog);
};
model.CancelRequested += dialog.Close;
await dialog.ShowDialog<object>(parent);
return result;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
return await Show(dialog, parent);
}
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
return (await Show(dialog, parent))?.FirstOrDefault();
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent, ManagedFileDialogOptions options)
{
return await Show(dialog, parent, options);
var options = AvaloniaLocator.Current.GetService<ManagedFileDialogOptions>();
return new ManagedStorageProvider<T>(window, options);
}
throw new InvalidOperationException("Current platform doesn't support managed picker dialogs");
}
}
@ -127,7 +28,7 @@ namespace Avalonia.Dialogs
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
builder.AfterSetup(_ =>
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<Window>>());
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<Window>>());
return builder;
}
@ -135,17 +36,26 @@ namespace Avalonia.Dialogs
where TAppBuilder : AppBuilderBase<TAppBuilder>, new() where TWindow : Window, new()
{
builder.AfterSetup(_ =>
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<TWindow>>());
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<TWindow>>());
return builder;
}
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public static Task<string[]> ShowManagedAsync(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) => ShowManagedAsync<Window>(dialog, parent, options);
ManagedFileDialogOptions? options = null) => ShowManagedAsync<Window>(dialog, parent, options);
public static Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) where TWindow : Window, new()
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API")]
public static async Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions? options = null) where TWindow : Window, new()
{
return new ManagedSystemDialogImpl<TWindow>().ShowFileDialogAsync(dialog, parent, options);
var impl = new ManagedStorageProvider<TWindow>(parent, options);
var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions());
return files
.Select(file => file.TryGetUri(out var fullPath)
? fullPath.LocalPath
: file.Name)
.ToArray();
}
}
}

147
src/Avalonia.Dialogs/ManagedStorageProvider.cs

@ -0,0 +1,147 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Dialogs;
public class ManagedStorageProvider<T> : BclStorageProvider where T : Window, new()
{
private readonly Window _parent;
private readonly ManagedFileDialogOptions _managedOptions;
public ManagedStorageProvider(Window parent, ManagedFileDialogOptions? managedOptions)
{
_parent = parent;
_managedOptions = managedOptions ?? new ManagedFileDialogOptions();
}
public override bool CanSave => true;
public override bool CanOpen => true;
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await Show(model, _parent);
return results.Select(f => new BclStorageFile(new FileInfo(f))).ToArray();
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await Show(model, _parent);
return results.FirstOrDefault() is { } result
? new BclStorageFile(new FileInfo(result))
: null;
}
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await Show(model, _parent);
return results.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray();
}
private async Task<string[]> Show(ManagedFileChooserViewModel model, Window parent)
{
var dialog = new T
{
Content = new ManagedFileChooser(),
Title = model.Title,
DataContext = model
};
dialog.Closed += delegate { model.Cancel(); };
string[]? result = null;
model.CompleteRequested += items =>
{
result = items;
dialog.Close();
};
model.OverwritePrompt += async (filename) =>
{
var overwritePromptDialog = new Window()
{
Title = "Confirm Save As",
SizeToContent = SizeToContent.WidthAndHeight,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Padding = new Thickness(10),
MinWidth = 270
};
string name = Path.GetFileName(filename);
var panel = new DockPanel()
{
HorizontalAlignment = Layout.HorizontalAlignment.Stretch
};
var label = new Label()
{
Content = $"{name} already exists.\nDo you want to replace it?"
};
panel.Children.Add(label);
DockPanel.SetDock(label, Dock.Top);
var buttonPanel = new StackPanel()
{
HorizontalAlignment = Layout.HorizontalAlignment.Right,
Orientation = Layout.Orientation.Horizontal,
Spacing = 10
};
var button = new Button()
{
Content = "Yes",
HorizontalAlignment = Layout.HorizontalAlignment.Right
};
button.Click += (sender, args) =>
{
result = new string[1] { filename };
overwritePromptDialog.Close();
dialog.Close();
};
buttonPanel.Children.Add(button);
button = new Button()
{
Content = "No",
HorizontalAlignment = Layout.HorizontalAlignment.Right
};
button.Click += (sender, args) =>
{
overwritePromptDialog.Close();
};
buttonPanel.Children.Add(button);
panel.Children.Add(buttonPanel);
DockPanel.SetDock(buttonPanel, Dock.Bottom);
overwritePromptDialog.Content = panel;
await overwritePromptDialog.ShowDialog(dialog);
};
model.CancelRequested += dialog.Close;
await dialog.ShowDialog<object>(parent);
return result ?? Array.Empty<string>();
}
}

4
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -5,6 +5,10 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />

20
src/Avalonia.FreeDesktop/DBusCallQueue.cs

@ -8,10 +8,9 @@ namespace Avalonia.FreeDesktop
{
private readonly Func<Exception, Task> _errorHandler;
class Item
record Item(Func<Task> Callback)
{
public Func<Task> Callback;
public Action<Exception> OnFinish;
public Action<Exception?>? OnFinish;
}
private Queue<Item> _q = new Queue<Item>();
private bool _processing;
@ -23,19 +22,15 @@ namespace Avalonia.FreeDesktop
public void Enqueue(Func<Task> cb)
{
_q.Enqueue(new Item
{
Callback = cb
});
_q.Enqueue(new Item(cb));
Process();
}
public Task EnqueueAsync(Func<Task> cb)
{
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
_q.Enqueue(new Item
_q.Enqueue(new Item(cb)
{
Callback = cb,
OnFinish = e =>
{
if (e == null)
@ -51,13 +46,12 @@ namespace Avalonia.FreeDesktop
public Task<T> EnqueueAsync<T>(Func<Task<T>> cb)
{
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
_q.Enqueue(new Item
{
Callback = async () =>
_q.Enqueue(new Item(async () =>
{
var res = await cb();
tcs.TrySetResult(res);
},
})
{
OnFinish = e =>
{
if (e != null)

4
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop
private readonly object _lock = new();
private SynchronizationContext? _ctx;
public override void Post(SendOrPostCallback d, object state)
public override void Post(SendOrPostCallback d, object? state)
{
lock (_lock)
{
@ -29,7 +29,7 @@ namespace Avalonia.FreeDesktop
}
}
public override void Send(SendOrPostCallback d, object state)
public override void Send(SendOrPostCallback d, object? state)
{
lock (_lock)
{

46
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -1,9 +1,7 @@
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;
@ -26,7 +24,7 @@ namespace Avalonia.FreeDesktop.DBusIme
return (im, im);
}
}
internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl
{
private List<IDisposable> _disposables = new List<IDisposable>();
@ -34,7 +32,7 @@ namespace Avalonia.FreeDesktop.DBusIme
protected Connection Connection { get; }
private readonly string[] _knownNames;
private bool _connecting;
private string _currentName;
private string? _currentName;
private DBusCallQueue _queue;
private bool _controlActive, _windowActive;
private bool? _imeActive;
@ -42,9 +40,9 @@ namespace Avalonia.FreeDesktop.DBusIme
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);
@ -58,18 +56,18 @@ namespace Avalonia.FreeDesktop.DBusIme
foreach (var name in _knownNames)
_disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange));
}
protected abstract Task<bool> Connect(string name);
protected string GetAppName() =>
Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
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)
if (!_connecting)
{
_connecting = true;
try
@ -98,25 +96,25 @@ namespace Avalonia.FreeDesktop.DBusIme
_connecting = false;
}
}
}
// IME has crashed
if (args.NewOwner == null && args.ServiceName == _currentName)
{
_currentName = null;
foreach(var s in _disposables)
foreach (var s in _disposables)
s.Dispose();
_disposables.Clear();
OnDisconnected();
Reset();
// Watch again
Watch();
}
}
protected virtual Task Disconnect()
{
return Task.CompletedTask;
@ -124,7 +122,7 @@ namespace Avalonia.FreeDesktop.DBusIme
protected virtual void OnDisconnected()
{
}
protected virtual void Reset()
@ -149,10 +147,14 @@ namespace Avalonia.FreeDesktop.DBusIme
OnDisconnected();
_currentName = null;
}
protected void Enqueue(Func<Task> cb) => _queue.Enqueue(cb);
protected void AddDisposable(IDisposable d) => _disposables.Add(d);
protected void AddDisposable(IDisposable? d)
{
if(d is { })
_disposables.Add(d);
}
public void Dispose()
{
@ -198,7 +200,7 @@ namespace Avalonia.FreeDesktop.DBusIme
UpdateActive();
}
void ITextInputMethodImpl.SetClient(ITextInputMethodClient client)
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
_controlActive = client is { };
UpdateActive();
@ -225,7 +227,7 @@ namespace Avalonia.FreeDesktop.DBusIme
}
}
private Action<string> _onCommit;
private Action<string>? _onCommit;
event Action<string> IX11InputMethodControl.Commit
{
add => _onCommit += value;
@ -234,7 +236,7 @@ namespace Avalonia.FreeDesktop.DBusIme
protected void FireCommit(string s) => _onCommit?.Invoke(s);
private Action<X11InputMethodForwardedKey> _onForward;
private Action<X11InputMethodForwardedKey>? _onForward;
event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey
{
add => _onForward += value;

28
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs

@ -31,15 +31,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
Task DestroyICAsync();
Task<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.fcitx.Fcitx.InputContext1")]
@ -54,11 +54,11 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor);
Task DestroyICAsync();
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null);
Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.fcitx.Fcitx.InputMethod1")]

31
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs

@ -5,8 +5,8 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
internal class FcitxICWrapper
{
private readonly IFcitxInputContext1 _modern;
private readonly IFcitxInputContext _old;
private readonly IFcitxInputContext1? _modern;
private readonly IFcitxInputContext? _old;
public FcitxICWrapper(IFcitxInputContext old)
{
@ -18,34 +18,37 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
_modern = modern;
}
public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync();
public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask;
public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync();
public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask;
public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync();
public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask;
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();
_old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask;
public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask;
public async Task<bool> 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);
return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false));
}
public Task<IDisposable> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler);
public Task<IDisposable?> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? Task.FromResult(default(IDisposable?));
public Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler)
public Task<IDisposable?> 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)));
?? _modern?.WatchForwardKeyAsync(ev =>
handler((ev.keyval, ev.state, ev.type ? 1 : 0)))
?? Task.FromResult(default(IDisposable?));
}
public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags);
_old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;
}
}

38
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
internal class FcitxX11TextInputMethod : DBusTextInputMethodBase
{
private FcitxICWrapper _context;
private FcitxICWrapper? _context;
private FcitxCapabilityFlags? _lastReportedFlags;
public FcitxX11TextInputMethod(Connection connection) : base(connection,
@ -49,7 +49,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
return true;
}
protected override Task Disconnect() => _context.DestroyICAsync();
protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask;
protected override void OnDisconnected() => _context = null;
@ -60,18 +60,18 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
}
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();
}
_context?.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width),
Math.Max(1, cursorRect.Height))
?? Task.CompletedTask;
protected override Task SetActiveCore(bool active)=> (active
? _context?.FocusInAsync()
: _context?.FocusOutAsync())
?? Task.CompletedTask;
protected override Task ResetContextCore() => _context.ResetAsync();
protected override Task ResetContextCore() => _context?.ResetAsync()
?? Task.CompletedTask;
protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
{
@ -88,9 +88,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
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);
if (_context is { })
{
return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type,
(uint)args.Timestamp).ConfigureAwait(false);
}
else
{
return false;
}
}
public override void SetOptions(TextInputOptions options) =>

38
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs

@ -22,25 +22,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
Task<object> GetEngineAsync();
Task DestroyAsync();
Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception> onError = null);
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception> onError = null);
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception> onError = null);
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception>? onError = null);
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception>? onError = null);
}

30
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@ -9,7 +9,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
{
internal class IBusX11TextInputMethod : DBusTextInputMethodBase
{
private IIBusInputContext _context;
private IIBusInputContext? _context;
public IBusX11TextInputMethod(Connection connection) : base(connection,
"org.freedesktop.portal.IBus")
@ -53,16 +53,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
private void OnCommitText(object wtf)
{
// Hello darkness, my old friend
var prop = wtf.GetType().GetField("Item3");
if (prop != null)
if (wtf.GetType().GetField("Item3") is { } prop)
{
var text = (string)prop.GetValue(wtf);
var text = prop.GetValue(wtf) as string;
if (!string.IsNullOrEmpty(text))
FireCommit(text);
FireCommit(text!);
}
}
protected override Task Disconnect() => _context.DestroyAsync();
protected override Task Disconnect() => _context?.DestroyAsync()
?? Task.CompletedTask;
protected override void OnDisconnected()
{
@ -71,13 +71,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
}
protected override Task SetCursorRectCore(PixelRect rect)
=> _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height);
=> _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height)
?? Task.CompletedTask;
protected override Task SetActiveCore(bool active)
=> active ? _context.FocusInAsync() : _context.FocusOutAsync();
=> (active ? _context?.FocusInAsync() : _context?.FocusOutAsync())
?? Task.CompletedTask;
protected override Task ResetContextCore()
=> _context.ResetAsync();
=> _context?.ResetAsync() ?? Task.CompletedTask;
protected override Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode)
{
@ -94,7 +96,15 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
if (args.Type == RawKeyEventType.KeyUp)
state |= IBusModifierMask.ReleaseMask;
return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
if(_context is { })
{
return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state);
}
else
{
return Task.FromResult(false);
}
}
public override void SetOptions(TextInputOptions options)

2
src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs

@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop.DBusIme
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
};
static Func<Connection, IX11InputMethodFactory> DetectInputMethod()
static Func<Connection, IX11InputMethodFactory>? DetectInputMethod()
{
foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" })
{

16
src/Avalonia.FreeDesktop/DBusMenu.cs

@ -28,18 +28,18 @@ namespace Avalonia.FreeDesktop.DBusMenu
Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events);
Task<bool> AboutToShowAsync(int Id);
Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids);
Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? onError = null);
}
[Dictionary]
class DBusMenuProperties
{
public uint Version { get; set; } = default (uint);
public string TextDirection { get; set; } = default (string);
public string Status { get; set; } = default (string);
public string[] IconThemePath { get; set; } = default (string[]);
public string? TextDirection { get; set; } = default (string);
public string? Status { get; set; } = default (string);
public string[]? IconThemePath { get; set; } = default (string[]);
}
@ -50,7 +50,7 @@ namespace Avalonia.FreeDesktop.DBusMenu
Task UnregisterWindowAsync(uint WindowId);
Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId);
Task<(uint, string, ObjectPath)[]> GetMenusAsync();
Task<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception> onError = null);
Task<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception>? onError = null);
}
}

36
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -17,7 +17,7 @@ namespace Avalonia.FreeDesktop
{
public class DBusMenuExporter
{
public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid)
public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid)
{
if (DBusHelper.Connection == null)
return null;
@ -37,10 +37,10 @@ namespace Avalonia.FreeDesktop
{
private readonly Connection _dbus;
private readonly uint _xid;
private IRegistrar _registrar;
private IRegistrar? _registrar;
private bool _disposed;
private uint _revision = 1;
private NativeMenu _menu;
private NativeMenu? _menu;
private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
@ -73,10 +73,10 @@ namespace Avalonia.FreeDesktop
if (_appMenu)
{
await _dbus.RegisterObjectAsync(this);
_registrar = DBusHelper.Connection.CreateProxy<IRegistrar>(
_registrar = DBusHelper.Connection?.CreateProxy<IRegistrar>(
"com.canonical.AppMenu.Registrar",
"/com/canonical/AppMenu/Registrar");
if (!_disposed)
if (!_disposed && _registrar is { })
await _registrar.RegisterWindowAsync(_xid, ObjectPath);
}
else
@ -109,9 +109,9 @@ namespace Avalonia.FreeDesktop
public bool IsNativeMenuExported { get; private set; }
public event EventHandler OnIsNativeMenuExportedChanged;
public event EventHandler? OnIsNativeMenuExportedChanged;
public void SetNativeMenu(NativeMenu menu)
public void SetNativeMenu(NativeMenu? menu)
{
if (menu == null)
menu = new NativeMenu();
@ -153,7 +153,7 @@ namespace Avalonia.FreeDesktop
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
}
private (NativeMenuItemBase item, NativeMenu menu) GetMenu(int id)
private (NativeMenuItemBase? item, NativeMenu? menu) GetMenu(int id)
{
if (id == 0)
return (null, _menu);
@ -161,7 +161,7 @@ namespace Avalonia.FreeDesktop
return (item, (item as NativeMenuItem)?.Menu);
}
private void EnsureSubscribed(NativeMenu menu)
private void EnsureSubscribed(NativeMenu? menu)
{
if(menu!=null && _menus.Add(menu))
((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged;
@ -180,12 +180,12 @@ namespace Avalonia.FreeDesktop
return id;
}
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
QueueReset();
}
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
QueueReset();
}
@ -216,7 +216,7 @@ namespace Avalonia.FreeDesktop
"type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
};
object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name)
{
var (it, menu) = i;
@ -302,7 +302,7 @@ namespace Avalonia.FreeDesktop
}
private List<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>();
KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase item, NativeMenu menu) i, string[] names)
KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
{
if (names?.Length > 0 != true)
names = AllProperties;
@ -336,7 +336,7 @@ namespace Avalonia.FreeDesktop
return Task.FromResult(rv);
}
(int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase item, NativeMenu menu, int depth, string[] propertyNames)
(int, KeyValuePair<string, object>[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames)
{
var id = item == null ? 0 : GetId(item);
var props = GetProperties((item, menu), propertyNames);
@ -414,22 +414,22 @@ namespace Avalonia.FreeDesktop
private event Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)>
ItemsPropertiesUpdated { add { } remove { } }
private event Action<(uint revision, int parent)> LayoutUpdated;
private event Action<(uint revision, int parent)>? LayoutUpdated;
private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } }
private event Action<PropertyChanges> PropertiesChanged { add { } remove { } }
async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception> onError)
async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError)
{
ItemsPropertiesUpdated += handler;
return Disposable.Create(() => ItemsPropertiesUpdated -= handler);
}
async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception> onError)
async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError)
{
LayoutUpdated += handler;
return Disposable.Create(() => LayoutUpdated -= handler);
}
async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception> onError)
async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? onError)
{
ItemActivationRequested+= handler;
return Disposable.Create(() => ItemActivationRequested -= handler);

2
src/Avalonia.FreeDesktop/DBusRequest.cs

@ -11,6 +11,6 @@ namespace Avalonia.FreeDesktop
internal interface IRequest : IDBusObject
{
Task CloseAsync();
Task<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception> onError = null);
Task<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception>? onError = null);
}
}

159
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -1,102 +1,171 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Tmds.DBus;
namespace Avalonia.FreeDesktop
{
internal class DBusSystemDialog : ISystemDialogImpl
internal class DBusSystemDialog : BclStorageProvider
{
private readonly IFileChooser _fileChooser;
internal static DBusSystemDialog? TryCreate()
private static readonly Lazy<IFileChooser?> s_fileChooser = new(() =>
{
var fileChooser = DBusHelper.Connection?.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
if (fileChooser is null)
return null;
try
{
fileChooser.GetVersionAsync().GetAwaiter().GetResult();
return new DBusSystemDialog(fileChooser);
_ = fileChooser.GetVersionAsync();
return fileChooser;
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}");
return null;
}
});
internal static DBusSystemDialog? TryCreate(IPlatformHandle handle)
{
return handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser
? new DBusSystemDialog(fileChooser, handle) : null;
}
private DBusSystemDialog(IFileChooser fileChooser)
private readonly IFileChooser _fileChooser;
private readonly IPlatformHandle _handle;
private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle)
{
_fileChooser = fileChooser;
_handle = handle;
}
public async Task<string[]?> ShowFileDialogAsync(FileDialog dialog, Window parent)
public override bool CanOpen => true;
public override bool CanSave => true;
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var options = new Dictionary<string, object>();
if (dialog.Filters is not null)
options.Add("filters", ParseFilters(dialog));
var chooserOptions = new Dictionary<string, object>();
var filters = ParseFilters(options.FileTypeFilter);
if (filters.Any())
{
chooserOptions.Add("filters", filters);
}
chooserOptions.Add("multiple", options.AllowMultiple);
objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task ?? Array.Empty<string>();
return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).AbsolutePath))).ToList();
}
switch (dialog)
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath;
var chooserOptions = new Dictionary<string, object>();
var filters = ParseFilters(options.FileTypeChoices);
if (filters.Any())
{
case OpenFileDialog openFileDialog:
options.Add("multiple", openFileDialog.AllowMultiple);
objectPath = await _fileChooser.OpenFileAsync(parentWindow, openFileDialog.Title ?? string.Empty, options);
break;
case SaveFileDialog saveFileDialog:
if (saveFileDialog.InitialFileName is not null)
options.Add("current_name", saveFileDialog.InitialFileName);
if (saveFileDialog.Directory is not null)
options.Add("current_folder", Encoding.UTF8.GetBytes(saveFileDialog.Directory));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, saveFileDialog.Title ?? string.Empty, options);
break;
chooserOptions.Add("filters", filters);
}
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", currentName);
if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true)
chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString()));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task;
if (uris is null)
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).AbsolutePath : null;
if (path is null)
{
return null;
for (var i = 0; i < uris.Length; i++)
uris[i] = new Uri(uris[i]).AbsolutePath;
return uris;
}
else
{
// WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually.
path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null);
return new BclStorageFile(new FileInfo(path));
}
}
public async Task<string?> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var parentWindow = $"x11:{parent.PlatformImpl!.Handle.Handle.ToString("X")}";
var options = new Dictionary<string, object>
var parentWindow = $"x11:{_handle.Handle:X}";
var chooserOptions = new Dictionary<string, object>
{
{ "directory", true }
{ "directory", true },
{ "multiple", options.AllowMultiple }
};
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, dialog.Title ?? string.Empty, options);
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = DBusHelper.Connection!.CreateProxy<IRequest>("org.freedesktop.portal.Request", objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task;
if (uris is null)
return null;
return uris.Length != 1 ? string.Empty : new Uri(uris[0]).AbsolutePath;
var uris = await tsc.Task ?? Array.Empty<string>();
return uris
.Select(path => new Uri(path).AbsolutePath)
// WSL2 freedesktop allows to select files as well in directory picker, filter it out.
.Where(Directory.Exists)
.Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
}
private static (string name, (uint style, string extension)[])[] ParseFilters(FileDialog dialog)
private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes)
{
var filters = new (string name, (uint style, string extension)[])[dialog.Filters!.Count];
for (var i = 0; i < filters.Length; i++)
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
if (fileTypes is null)
{
var extensions = dialog.Filters[i].Extensions.Select(static x => (0u, x)).ToArray();
filters[i] = (dialog.Filters[i].Name ?? string.Empty, extensions);
return Array.Empty<(string name, (uint style, string extension)[])>();
}
var filters = new List<(string name, (uint style, string extension)[])>();
foreach (var fileType in fileTypes)
{
const uint globStyle = 0u;
const uint mimeStyle = 1u;
var extensions = Enumerable.Empty<(uint, string)>();
if (fileType.Patterns is { } patterns)
{
extensions = extensions.Concat(patterns.Select(static x => (globStyle, x)));
}
else if (fileType.MimeTypes is { } mimeTypes)
{
extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x)));
}
if (extensions.Any())
{
filters.Add((fileType.Name, extensions.ToArray()));
}
}
return filters;
return filters.ToArray();
}
}
}

2
src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs

@ -10,7 +10,7 @@ namespace Avalonia.FreeDesktop
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
Contract.Requires<ArgumentNullException>(mountedDrives != null);
return new LinuxMountedVolumeInfoListener(ref mountedDrives);
return new LinuxMountedVolumeInfoListener(ref mountedDrives!);
}
}
}

1
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -62,7 +62,6 @@ namespace Avalonia.Headless
.Bind<IClipboard>().ToSingleton<HeadlessClipboardStub>()
.Bind<ICursorFactory>().ToSingleton<HeadlessCursorFactoryStub>()
.Bind<IPlatformSettings>().ToConstant(new HeadlessPlatformSettingsStub())
.Bind<ISystemDialogImpl>().ToSingleton<HeadlessSystemDialogsStub>()
.Bind<IPlatformIconLoader>().ToSingleton<HeadlessIconLoaderStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IRenderLoop>().ToConstant(new RenderLoop())

36
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -12,6 +12,8 @@ using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Utilities;
namespace Avalonia.Headless
@ -73,19 +75,6 @@ namespace Avalonia.Headless
public TimeSpan TouchDoubleClickTime => DoubleClickTime;
}
class HeadlessSystemDialogsStub : ISystemDialogImpl
{
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
return Task.Run(() => (string[])null);
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
return Task.Run(() => (string)null);
}
}
class HeadlessGlyphTypefaceImpl : IGlyphTypefaceImpl
{
public short DesignEmHeight => 10;
@ -219,4 +208,25 @@ namespace Avalonia.Headless
return ScreenHelper.ScreenFromWindow(window, AllScreens);
}
}
internal class NoopStorageProvider : BclStorageProvider
{
public override bool CanOpen => false;
public override Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
}
public override bool CanSave => false;
public override Task<IStorageFile> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromResult<IStorageFile>(null);
}
public override bool CanPickFolder => false;
public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());
}
}
}

6
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -3,19 +3,21 @@ using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Headless
{
class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow
class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow, ITopLevelImplWithStorageProvider
{
private IKeyboardDevice _keyboard;
private Stopwatch _st = Stopwatch.StartNew();
@ -245,6 +247,8 @@ namespace Avalonia.Headless
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
public IStorageProvider StorageProvider => new NoopStorageProvider();
void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers)
{
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers));

1
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -112,7 +112,6 @@ namespace Avalonia.Native
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta, wholeWordTextActionModifiers: KeyModifiers.Alt))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))

118
src/Avalonia.Native/SystemDialogs.cs

@ -1,70 +1,114 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Native.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Native
{
internal class SystemDialogs : ISystemDialogImpl
internal class SystemDialogs : BclStorageProvider
{
IAvnSystemDialogs _native;
private readonly WindowBaseImpl _window;
private readonly IAvnSystemDialogs _native;
public SystemDialogs(IAvnSystemDialogs native)
public SystemDialogs(WindowBaseImpl window, IAvnSystemDialogs native)
{
_window = window;
_native = native;
}
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
public override bool CanOpen => true;
public override bool CanSave => true;
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var events = new SystemDialogEvents();
using var events = new SystemDialogEvents();
var nativeParent = GetNativeWindow(parent);
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true
? suggestedDirectoryTmp.LocalPath : string.Empty;
if (dialog is OpenFileDialog ofd)
{
_native.OpenFileDialog(nativeParent,
events, ofd.AllowMultiple.AsComBool(),
ofd.Title ?? "",
ofd.Directory ?? "",
ofd.InitialFileName ?? "",
string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty<string>()));
}
else
{
_native.SaveFileDialog(nativeParent,
events,
dialog.Title ?? "",
dialog.Directory ?? "",
dialog.InitialFileName ?? "",
string.Join(";", dialog.Filters?.SelectMany(f => f.Extensions) ?? Array.Empty<string>()));
}
_native.OpenFileDialog((IAvnWindow)_window.Native,
events,
options.AllowMultiple.AsComBool(),
options.Title ?? string.Empty,
suggestedDirectory,
string.Empty,
PrepareFilterParameter(options.FileTypeFilter));
var result = await events.Task.ConfigureAwait(false);
return result?.Select(f => new BclStorageFile(new FileInfo(f))).ToArray()
?? Array.Empty<IStorageFile>();
}
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; });
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true
? suggestedDirectoryTmp.LocalPath : string.Empty;
_native.SaveFileDialog((IAvnWindow)_window.Native,
events,
options.Title ?? string.Empty,
suggestedDirectory,
options.SuggestedFileName ?? string.Empty,
PrepareFilterParameter(options.FileTypeChoices));
var result = await events.Task.ConfigureAwait(false);
return result.FirstOrDefault() is string file
? new BclStorageFile(new FileInfo(file))
: null;
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var events = new SystemDialogEvents();
using var events = new SystemDialogEvents();
var nativeParent = GetNativeWindow(parent);
var suggestedDirectory = options.SuggestedStartLocation?.TryGetUri(out var suggestedDirectoryTmp) == true
? suggestedDirectoryTmp.LocalPath : string.Empty;
_native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.Directory ?? "");
_native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory);
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); });
var result = await events.Task.ConfigureAwait(false);
return result?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray()
?? Array.Empty<IStorageFolder>();
}
private IAvnWindow GetNativeWindow(Window window)
private static string PrepareFilterParameter(IReadOnlyList<FilePickerFileType>? fileTypes)
{
return (window?.PlatformImpl as WindowImpl)?.Native;
return string.Join(";",
fileTypes?.SelectMany(f =>
{
// On the native side we will try to parse identifiers or mimetypes.
if (f.AppleUniformTypeIdentifiers?.Any() == true)
{
return f.AppleUniformTypeIdentifiers;
}
else if (f.MimeTypes?.Any() == true)
{
// MacOS doesn't accept "all" type, so it's pointless to pass it.
return f.MimeTypes.Where(t => t != "*/*");
}
return Array.Empty<string>();
}) ??
Array.Empty<string>());
}
}
internal unsafe class SystemDialogEvents : NativeCallbackBase, IAvnSystemDialogEvents
{
private TaskCompletionSource<string[]> _tcs;
private readonly TaskCompletionSource<string[]> _tcs;
public SystemDialogEvents()
{
@ -83,7 +127,7 @@ namespace Avalonia.Native
for (int i = 0; i < numResults; i++)
{
results[i] = Marshal.PtrToStringAnsi(*ptr);
results[i] = Marshal.PtrToStringAnsi(*ptr) ?? string.Empty;
ptr++;
}

7
src/Avalonia.Native/WindowImplBase.cs

@ -11,6 +11,7 @@ using Avalonia.Input.Raw;
using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Threading;
@ -45,7 +46,7 @@ namespace Avalonia.Native
}
internal abstract class WindowBaseImpl : IWindowBaseImpl,
IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost
IFramebufferPlatformSurface, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{
protected readonly IAvaloniaNativeFactory _factory;
protected IInputRoot _inputRoot;
@ -73,6 +74,7 @@ namespace Avalonia.Native
_keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
_mouse = new MouseDevice();
_cursorFactory = AvaloniaLocator.Current.GetService<ICursorFactory>();
StorageProvider = new SystemDialogs(this, _factory.CreateSystemDialogs());
}
protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext)
@ -84,6 +86,7 @@ namespace Avalonia.Native
if (_gpu)
_glSurface = new GlPlatformSurface(window, _glContext);
Screen = new ScreenImpl(screens);
_savedLogicalSize = ClientSize;
_savedScaling = RenderScaling;
_nativeControlHost = new NativeControlHostImpl(_native.CreateNativeControlHost());
@ -514,5 +517,7 @@ namespace Avalonia.Native
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0, 0);
public IPlatformHandle Handle { get; private set; }
public IStorageProvider StorageProvider { get; }
}
}

7
src/Avalonia.Native/avn.idl

@ -641,9 +641,10 @@ interface IAvnSystemDialogEvents : IUnknown
interface IAvnSystemDialogs : IUnknown
{
void SelectFolderDialog(IAvnWindow* parentWindowHandle,
IAvnSystemDialogEvents* events,
[const] char* title,
[const] char* initialPath);
IAvnSystemDialogEvents* events,
bool allowMultiple,
[const] char* title,
[const] char* initialPath);
void OpenFileDialog(IAvnWindow* parentWindowHandle,
IAvnSystemDialogEvents* events,

1
src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj

@ -11,6 +11,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
<Compile Include="..\Shared\SourceGeneratorAttributes.cs" />
</ItemGroup>

21
src/Avalonia.Themes.Default/Controls/ProgressBar.xaml

@ -1,4 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls">
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="10">
@ -11,10 +13,13 @@
</Border>
</Design.PreviewWith>
<Style Selector="ProgressBar">
<Style.Resources>
<converters:StringFormatConverter x:Key="StringFormatConverter"/>
</Style.Resources>
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/>
<Setter Property="Template">
<ControlTemplate>
<ControlTemplate TargetType="ProgressBar">
<Grid>
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
@ -23,7 +28,17 @@
</Panel>
</Border>
<LayoutTransformControl HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}" Name="PART_LayoutTransformControl">
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}" Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" />
<TextBlock Foreground="{DynamicResource ThemeForegroundBrush}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StringFormatConverter}">
<TemplateBinding Property="ProgressTextFormat"/>
<Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
<TemplateBinding Property="Percentage"/>
<TemplateBinding Property="Minimum"/>
<TemplateBinding Property="Maximum"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</LayoutTransformControl>
</Grid>
</ControlTemplate>

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

@ -160,7 +160,7 @@
</ContentPresenter>
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
MinWidth="{ReflectionBinding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
OverlayInputPassThroughElement="{Binding $parent[Menu]}">

18
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@ -1,5 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
x:CompileBindings="True">
<Design.PreviewWith>
<Border Padding="20">
@ -13,6 +14,9 @@
</Border>
</Design.PreviewWith>
<Style Selector="ProgressBar">
<Style.Resources>
<converters:StringFormatConverter x:Key="StringFormatConverter"/>
</Style.Resources>
<Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAccentBrush}" />
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundBaseLowBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ProgressBarBorderThemeThickness}" />
@ -21,7 +25,7 @@
<Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<ControlTemplate TargetType="ProgressBar">
<Border x:Name="ProgressBarRoot" ClipToBounds="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<Panel x:Name="DeterminateRoot">
@ -32,7 +36,17 @@
<Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
</Panel>
<LayoutTransformControl x:Name="PART_LayoutTransformControl" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{TemplateBinding ShowProgressText}">
<TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" Text="{ReflectionBinding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" />
<TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource StringFormatConverter}">
<TemplateBinding Property="ProgressTextFormat"/>
<Binding Path="Value" RelativeSource="{RelativeSource TemplatedParent}"/>
<TemplateBinding Property="Percentage"/>
<TemplateBinding Property="Minimum"/>
<TemplateBinding Property="Maximum"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</LayoutTransformControl>
</Panel>
</Border>

3
src/Avalonia.X11/NativeDialogs/Gtk.cs

@ -208,6 +208,9 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GtkName)]
public static extern IntPtr gtk_file_filter_add_pattern(IntPtr filter, Utf8Buffer pattern);
[DllImport(GtkName)]
public static extern IntPtr gtk_file_filter_add_mime_type (IntPtr filter, Utf8Buffer mimeType);
[DllImport(GtkName)]
public static extern IntPtr gtk_file_chooser_add_filter(IntPtr chooser, IntPtr filter);

225
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -1,57 +1,147 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using static Avalonia.X11.NativeDialogs.Glib;
using static Avalonia.X11.NativeDialogs.Gtk;
// ReSharper disable AccessToModifiedClosure
namespace Avalonia.X11.NativeDialogs
{
class GtkSystemDialog : ISystemDialogImpl
internal class GtkSystemDialog : BclStorageProvider
{
private Task<bool> _initialized;
private Task<bool>? _initialized;
private readonly X11Window _window;
public GtkSystemDialog(X11Window window)
{
_window = window;
}
public override bool CanOpen => true;
public override bool CanSave => true;
public override bool CanPickFolder => true;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
options.AllowMultiple, options.SuggestedStartLocation, null, options.FileTypeFilter, null, false)
.ConfigureAwait(false);
return res?.Select(f => new BclStorageFile(new FileInfo(f))).ToArray() ?? Array.Empty<IStorageFile>();
});
}
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
options.AllowMultiple, options.SuggestedStartLocation, null,
null, null, false).ConfigureAwait(false);
return res?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray() ?? Array.Empty<IStorageFolder>();
});
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
await EnsureInitialized();
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices, options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
return res?.FirstOrDefault() is { } file
? new BclStorageFile(new FileInfo(file))
: null;
});
}
private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, string initialDirectory, string initialFileName, IEnumerable<FileDialogFilter> filters, string defaultExtension, bool overwritePrompt)
private unsafe Task<string[]?> ShowDialog(string? title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, IStorageFolder? initialFolder, string? initialFileName,
IEnumerable<FilePickerFileType>? filters, string? defaultExtension, bool overwritePrompt)
{
IntPtr dlg;
using (var name = new Utf8Buffer(title))
{
dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero);
}
UpdateParent(dlg, parent);
if (multiSelect)
{
gtk_file_chooser_set_select_multiple(dlg, true);
}
gtk_window_set_modal(dlg, true);
var tcs = new TaskCompletionSource<string[]>();
List<IDisposable> disposables = null;
var tcs = new TaskCompletionSource<string[]?>();
List<IDisposable>? disposables = null;
void Dispose()
{
// ReSharper disable once PossibleNullReferenceException
foreach (var d in disposables) d.Dispose();
foreach (var d in disposables!)
{
d.Dispose();
}
disposables.Clear();
}
var filtersDic = new Dictionary<IntPtr, FileDialogFilter>();
if(filters != null)
var filtersDic = new Dictionary<IntPtr, FilePickerFileType>();
if (filters != null)
{
foreach (var f in filters)
{
var filter = gtk_file_filter_new();
filtersDic[filter] = f;
using (var b = new Utf8Buffer(f.Name))
gtk_file_filter_set_name(filter, b);
if (f.Patterns?.Any() == true || f.MimeTypes?.Any() == true)
{
var filter = gtk_file_filter_new();
filtersDic[filter] = f;
using (var b = new Utf8Buffer(f.Name))
{
gtk_file_filter_set_name(filter, b);
}
foreach (var e in f.Extensions)
using (var b = new Utf8Buffer("*." + e))
gtk_file_filter_add_pattern(filter, b);
if (f.Patterns is not null)
{
foreach (var e in f.Patterns)
{
using (var b = new Utf8Buffer(e))
{
gtk_file_filter_add_pattern(filter, b);
}
}
}
gtk_file_chooser_add_filter(dlg, filter);
if (f.MimeTypes is not null)
{
foreach (var e in f.MimeTypes)
{
using (var b = new Utf8Buffer(e))
{
gtk_file_filter_add_mime_type(filter, b);
}
}
}
gtk_file_chooser_add_filter(dlg, filter);
}
}
}
disposables = new List<IDisposable>
{
@ -63,7 +153,7 @@ namespace Avalonia.X11.NativeDialogs
}),
ConnectSignal<signal_dialog_response>(dlg, "response", (_, resp, __) =>
{
string[] result = null;
string[]? result = null;
if (resp == GtkResponseType.Accept)
{
var resultList = new List<string>();
@ -71,20 +161,18 @@ namespace Avalonia.X11.NativeDialogs
var cgs = gs;
while (cgs != null)
{
if (cgs->Data != IntPtr.Zero)
resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data));
cgs = cgs->Next;
if (cgs->Data != IntPtr.Zero
&& Utf8Buffer.StringFromPtr(cgs->Data) is string str) { resultList.Add(str); } cgs = cgs->Next;
}
g_slist_free(gs);
result = resultList.ToArray();
// GTK doesn't auto-append the extension, so we need to do that manually
if (action == GtkFileChooserAction.Save)
{
var currentFilter = gtk_file_chooser_get_filter(dlg);
filtersDic.TryGetValue(currentFilter, out var selectedFilter);
for (var c = 0; c < result.Length; c++)
result[c] = NameWithExtension(result[c], defaultExtension, selectedFilter);
for (var c = 0; c < result.Length; c++) { result[c] = StorageProviderHelpers.NameWithExtension(result[c], defaultExtension, selectedFilter); }
}
}
@ -98,13 +186,19 @@ namespace Avalonia.X11.NativeDialogs
action == GtkFileChooserAction.Save ? "Save"
: action == GtkFileChooserAction.SelectFolder ? "Select"
: "Open"))
{
gtk_dialog_add_button(dlg, open, GtkResponseType.Accept);
}
using (var open = new Utf8Buffer("Cancel"))
{
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
}
if (initialDirectory != null)
Uri? folderPath = null;
if (initialFolder?.TryGetUri(out folderPath) == true)
{
using var dir = new Utf8Buffer(initialDirectory);
using var dir = new Utf8Buffer(folderPath.LocalPath);
gtk_file_chooser_set_current_folder(dlg, dir);
}
@ -112,7 +206,7 @@ namespace Avalonia.X11.NativeDialogs
{
// gtk_file_chooser_set_filename() expects full path
using var fn = action == GtkFileChooserAction.Open
? new Utf8Buffer(Path.Combine(initialDirectory ?? "", initialFileName))
? new Utf8Buffer(Path.Combine(folderPath?.LocalPath ?? "", initialFileName))
: new Utf8Buffer(initialFileName);
if (action == GtkFileChooserAction.Save)
@ -131,84 +225,29 @@ namespace Avalonia.X11.NativeDialogs
return tcs.Task;
}
string NameWithExtension(string path, string defaultExtension, FileDialogFilter filter)
private async Task EnsureInitialized()
{
var name = Path.GetFileName(path);
if (name != null && !name.Contains("."))
if (_initialized == null)
{
if (filter?.Extensions?.Count > 0)
{
if (defaultExtension != null
&& filter.Extensions.Contains(defaultExtension))
return path + "." + defaultExtension.TrimStart('.');
var ext = filter.Extensions.FirstOrDefault(x => x != "*");
if (ext != null)
return path + "." + ext.TrimStart('.');
}
if (defaultExtension != null)
path += "." + defaultExtension.TrimStart('.');
_initialized = StartGtk();
}
return path;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
{
await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread(() => ShowDialog(
dialog.Title, platformImpl,
dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
(dialog as OpenFileDialog)?.AllowMultiple ?? false,
dialog.Directory,
dialog.InitialFileName,
dialog.Filters,
(dialog as SaveFileDialog)?.DefaultExtension,
(dialog as SaveFileDialog)?.ShowOverwritePrompt ?? false));
}
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
{
await EnsureInitialized();
var platformImpl = parent?.PlatformImpl;
return await await RunOnGlibThread(async () =>
{
var res = await ShowDialog(
dialog.Title,
platformImpl,GtkFileChooserAction.SelectFolder,
false,
dialog.Directory,
null,
null,
null,
false);
return res?.FirstOrDefault();
});
}
async Task EnsureInitialized()
{
if (_initialized == null) _initialized = StartGtk();
if (!(await _initialized))
{
throw new Exception("Unable to initialize GTK on separate thread");
}
}
void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
private static void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
var xid = parentWindow.Handle.Handle;
gtk_widget_realize(chooser);
var window = gtk_widget_get_window(chooser);
var parent = GetForeignWindow(xid);
if (window != IntPtr.Zero && parent != IntPtr.Zero)
{
gdk_window_set_transient_for(window, parent);
}
}
}
}

7
src/Avalonia.X11/X11Platform.cs

@ -80,7 +80,6 @@ namespace Avalonia.X11
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IPlatformSettings>().ToConstant(new PlatformSettingsStub())
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader(Info))
.Bind<ISystemDialogImpl>().ToConstant(DBusSystemDialog.TryCreate() as ISystemDialogImpl ?? new ManagedFileDialogExtensions.ManagedSystemDialogImpl<Window>())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(new X11PlatformLifetimeEvents(this));
@ -214,6 +213,12 @@ namespace Avalonia
/// </summary>
public bool UseDBusMenu { get; set; } = true;
/// <summary>
/// Enables GTK file picker instead of default FreeDesktop.
/// The default value is true. And FreeDesktop file picker is used instead if available.
/// </summary>
public bool UseGtkFilePicker { get; set; } = false;
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
/// </summary>

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

Loading…
Cancel
Save