committed by
GitHub
557 changed files with 34728 additions and 14975 deletions
@ -0,0 +1,5 @@ |
|||
<ProjectConfiguration> |
|||
<Settings> |
|||
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
|||
</Settings> |
|||
</ProjectConfiguration> |
|||
@ -0,0 +1,67 @@ |
|||
# 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: system_profiler SPDisplaysDataType |grep Resolution |
|||
|
|||
- script: | |
|||
pkill node |
|||
appium & |
|||
pkill IntegrationTestApp |
|||
./build.sh CompileNative |
|||
rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")") |
|||
pkill IntegrationTestApp |
|||
./samples/IntegrationTestApp/bundle.sh |
|||
open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app |
|||
pkill IntegrationTestApp |
|||
|
|||
- task: DotNetCoreCLI@2 |
|||
inputs: |
|||
command: 'test' |
|||
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' |
|||
|
|||
- script: | |
|||
pkill IntegrationTestApp |
|||
pkill node |
|||
|
|||
|
|||
- 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' |
|||
@ -0,0 +1,45 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:pages="clr-namespace:ControlCatalog.Pages" |
|||
x:Class="ControlCatalog.Pages.CompositionPage"> |
|||
<StackPanel> |
|||
<TextBlock Classes="h1">Implicit animations</TextBlock> |
|||
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0"> |
|||
<ItemsControl x:Name="Items"> |
|||
<ItemsControl.ItemsPanel> |
|||
<ItemsPanelTemplate> |
|||
<WrapPanel/> |
|||
</ItemsPanelTemplate> |
|||
</ItemsControl.ItemsPanel> |
|||
<ItemsControl.DataTemplates> |
|||
<DataTemplate DataType="pages:CompositionPageColorItem"> |
|||
<Border |
|||
pages:CompositionPage.EnableAnimations="True" |
|||
Padding="10" BorderBrush="Gray" BorderThickness="2" |
|||
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10"> |
|||
<TextBlock Text="{Binding ColorHexValue}"/> |
|||
</Border> |
|||
</DataTemplate> |
|||
</ItemsControl.DataTemplates> |
|||
</ItemsControl> |
|||
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray" |
|||
Background="#e0e0e0" Grid.Column="1" |
|||
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext" |
|||
/> |
|||
<Border Grid.Column="2"> |
|||
<LayoutTransformControl |
|||
HorizontalAlignment="Center" |
|||
|
|||
MinWidth="30"> |
|||
<LayoutTransformControl.LayoutTransform> |
|||
<RotateTransform Angle="90"/> |
|||
</LayoutTransformControl.LayoutTransform> |
|||
|
|||
<TextBlock>Resize me</TextBlock> |
|||
</LayoutTransformControl> |
|||
</Border> |
|||
</Grid> |
|||
|
|||
|
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,153 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Numerics; |
|||
using System.Threading.Tasks; |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Controls.Primitives; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.Markup.Xaml.Templates; |
|||
using Avalonia.Media; |
|||
using Avalonia.Rendering.Composition; |
|||
using Avalonia.Rendering.Composition.Animations; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class CompositionPage : UserControl |
|||
{ |
|||
private ImplicitAnimationCollection _implicitAnimations; |
|||
|
|||
public CompositionPage() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToVisualTree(e); |
|||
this.FindControl<ItemsControl>("Items").Items = CreateColorItems(); |
|||
} |
|||
|
|||
private List<CompositionPageColorItem> CreateColorItems() |
|||
{ |
|||
var list = new List<CompositionPageColorItem>(); |
|||
|
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 185, 0))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 231, 72, 86))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 120, 215))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 153, 188))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 122, 117, 116))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 118, 118, 118))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 141, 0))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 232, 17, 35))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 99, 177))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 45, 125, 154))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 93, 90, 88))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 76, 74, 72))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 247, 99, 12))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 234, 0, 94))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 142, 140, 216))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 183, 195))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 104, 118, 138))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 105, 121, 126))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 202, 80, 16))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 195, 0, 82))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 107, 105, 214))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 3, 131, 135))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 81, 92, 107))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 74, 84, 89))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 218, 59, 1))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 227, 0, 140))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 135, 100, 184))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 178, 148))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 86, 124, 115))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 100, 124, 100))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 239, 105, 80))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 191, 0, 119))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 116, 77, 169))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 1, 133, 116))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 72, 104, 96))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 82, 94, 84))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 209, 52, 56))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 194, 57, 179))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 177, 70, 194))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 0, 204, 106))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 73, 130, 5))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 132, 117, 69))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 255, 67, 67))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 154, 0, 137))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 136, 23, 152))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 137, 62))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 16, 124, 16))); |
|||
list.Add(new CompositionPageColorItem(Color.FromArgb(255, 126, 115, 95))); |
|||
|
|||
return list; |
|||
} |
|||
|
|||
private void EnsureImplicitAnimations() |
|||
{ |
|||
if (_implicitAnimations == null) |
|||
{ |
|||
var compositor = ElementComposition.GetElementVisual(this)!.Compositor; |
|||
|
|||
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); |
|||
offsetAnimation.Target = "Offset"; |
|||
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); |
|||
offsetAnimation.Duration = TimeSpan.FromMilliseconds(400); |
|||
|
|||
var rotationAnimation = compositor.CreateScalarKeyFrameAnimation(); |
|||
rotationAnimation.Target = "RotationAngle"; |
|||
rotationAnimation.InsertKeyFrame(.5f, 0.160f); |
|||
rotationAnimation.InsertKeyFrame(1f, 0f); |
|||
rotationAnimation.Duration = TimeSpan.FromMilliseconds(400); |
|||
|
|||
var animationGroup = compositor.CreateAnimationGroup(); |
|||
animationGroup.Add(offsetAnimation); |
|||
animationGroup.Add(rotationAnimation); |
|||
|
|||
_implicitAnimations = compositor.CreateImplicitAnimationCollection(); |
|||
_implicitAnimations["Offset"] = animationGroup; |
|||
} |
|||
} |
|||
|
|||
public static void SetEnableAnimations(Border border, bool value) |
|||
{ |
|||
|
|||
var page = border.FindAncestorOfType<CompositionPage>(); |
|||
if (page == null) |
|||
{ |
|||
border.AttachedToVisualTree += delegate { SetEnableAnimations(border, true); }; |
|||
return; |
|||
} |
|||
|
|||
if (ElementComposition.GetElementVisual(page) == null) |
|||
return; |
|||
|
|||
page.EnsureImplicitAnimations(); |
|||
ElementComposition.GetElementVisual((Visual)border.GetVisualParent()).ImplicitAnimations = |
|||
page._implicitAnimations; |
|||
} |
|||
} |
|||
|
|||
public class CompositionPageColorItem |
|||
{ |
|||
public Color Color { get; private set; } |
|||
|
|||
public SolidColorBrush ColorBrush |
|||
{ |
|||
get { return new SolidColorBrush(Color); } |
|||
} |
|||
|
|||
public String ColorHexValue |
|||
{ |
|||
get { return Color.ToString().Substring(3).ToUpperInvariant(); } |
|||
} |
|||
|
|||
public CompositionPageColorItem(Color color) |
|||
{ |
|||
Color = color; |
|||
} |
|||
} |
|||
@ -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> |
|||
|
|||
@ -0,0 +1,27 @@ |
|||
using Avalonia; |
|||
using MiniMvvm; |
|||
|
|||
namespace ControlCatalog.ViewModels |
|||
{ |
|||
public class ExpanderPageViewModel : ViewModelBase |
|||
{ |
|||
private object _cornerRadius = AvaloniaProperty.UnsetValue; |
|||
private bool _rounded; |
|||
|
|||
public object CornerRadius |
|||
{ |
|||
get => _cornerRadius; |
|||
private set => RaiseAndSetIfChanged(ref _cornerRadius, value); |
|||
} |
|||
|
|||
public bool Rounded |
|||
{ |
|||
get => _rounded; |
|||
set |
|||
{ |
|||
if (RaiseAndSetIfChanged(ref _rounded, value)) |
|||
CornerRadius = _rounded ? new CornerRadius(25) : AvaloniaProperty.UnsetValue; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,306 @@ |
|||
// ReSharper disable InconsistentNaming
|
|||
// Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc
|
|||
|
|||
using System; |
|||
using Avalonia.Utilities; |
|||
|
|||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
|||
// ReSharper disable CommentTypo
|
|||
// ReSharper disable MemberCanBePrivate.Global
|
|||
// ReSharper disable TooWideLocalVariableScope
|
|||
// ReSharper disable UnusedMember.Global
|
|||
#pragma warning disable 649
|
|||
|
|||
namespace Avalonia.Animation.Easings |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a cubic bezier curve and can compute Y coordinate for a given X
|
|||
/// </summary>
|
|||
internal unsafe struct CubicBezier |
|||
{ |
|||
const int CUBIC_BEZIER_SPLINE_SAMPLES = 11; |
|||
double ax_; |
|||
double bx_; |
|||
double cx_; |
|||
|
|||
double ay_; |
|||
double by_; |
|||
double cy_; |
|||
|
|||
double start_gradient_; |
|||
double end_gradient_; |
|||
|
|||
double range_min_; |
|||
double range_max_; |
|||
private bool monotonically_increasing_; |
|||
|
|||
fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES]; |
|||
|
|||
public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this() |
|||
{ |
|||
InitCoefficients(p1x, p1y, p2x, p2y); |
|||
InitGradients(p1x, p1y, p2x, p2y); |
|||
InitRange(p1y, p2y); |
|||
InitSpline(); |
|||
} |
|||
|
|||
public readonly double SampleCurveX(double t) |
|||
{ |
|||
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
|
|||
return ((ax_ * t + bx_) * t + cx_) * t; |
|||
} |
|||
|
|||
readonly double SampleCurveY(double t) |
|||
{ |
|||
return ((ay_ * t + by_) * t + cy_) * t; |
|||
} |
|||
|
|||
readonly double SampleCurveDerivativeX(double t) |
|||
{ |
|||
return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_; |
|||
} |
|||
|
|||
readonly double SampleCurveDerivativeY(double t) |
|||
{ |
|||
return (3.0 * ay_ * t + 2.0 * by_) * t + cy_; |
|||
} |
|||
|
|||
public readonly double SolveWithEpsilon(double x, double epsilon) |
|||
{ |
|||
if (x < 0.0) |
|||
return 0.0 + start_gradient_ * x; |
|||
if (x > 1.0) |
|||
return 1.0 + end_gradient_ * (x - 1.0); |
|||
return SampleCurveY(SolveCurveX(x, epsilon)); |
|||
} |
|||
|
|||
void InitCoefficients(double p1x, |
|||
double p1y, |
|||
double p2x, |
|||
double p2y) |
|||
{ |
|||
// Calculate the polynomial coefficients, implicit first and last control
|
|||
// points are (0,0) and (1,1).
|
|||
cx_ = 3.0 * p1x; |
|||
bx_ = 3.0 * (p2x - p1x) - cx_; |
|||
ax_ = 1.0 - cx_ - bx_; |
|||
|
|||
cy_ = 3.0 * p1y; |
|||
by_ = 3.0 * (p2y - p1y) - cy_; |
|||
ay_ = 1.0 - cy_ - by_; |
|||
|
|||
#if DEBUG
|
|||
// Bezier curves with x-coordinates outside the range [0,1] for internal
|
|||
// control points may have multiple values for t for a given value of x.
|
|||
// In this case, calls to SolveCurveX may produce ambiguous results.
|
|||
monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1; |
|||
#endif
|
|||
} |
|||
|
|||
void InitGradients(double p1x, |
|||
double p1y, |
|||
double p2x, |
|||
double p2y) |
|||
{ |
|||
// End-point gradients are used to calculate timing function results
|
|||
// outside the range [0, 1].
|
|||
//
|
|||
// There are four possibilities for the gradient at each end:
|
|||
// (1) the closest control point is not horizontally coincident with regard to
|
|||
// (0, 0) or (1, 1). In this case the line between the end point and
|
|||
// the control point is tangent to the bezier at the end point.
|
|||
// (2) the closest control point is coincident with the end point. In
|
|||
// this case the line between the end point and the far control
|
|||
// point is tangent to the bezier at the end point.
|
|||
// (3) both internal control points are coincident with an endpoint. There
|
|||
// are two special case that fall into this category:
|
|||
// CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are
|
|||
// equivalent to linear.
|
|||
// (4) the closest control point is horizontally coincident with the end
|
|||
// point, but vertically distinct. In this case the gradient at the
|
|||
// end point is Infinite. However, this causes issues when
|
|||
// interpolating. As a result, we break down to a simple case of
|
|||
// 0 gradient under these conditions.
|
|||
|
|||
if (p1x > 0) |
|||
start_gradient_ = p1y / p1x; |
|||
else if (p1y == 0 && p2x > 0) |
|||
start_gradient_ = p2y / p2x; |
|||
else if (p1y == 0 && p2y == 0) |
|||
start_gradient_ = 1; |
|||
else |
|||
start_gradient_ = 0; |
|||
|
|||
if (p2x < 1) |
|||
end_gradient_ = (p2y - 1) / (p2x - 1); |
|||
else if (p2y == 1 && p1x < 1) |
|||
end_gradient_ = (p1y - 1) / (p1x - 1); |
|||
else if (p2y == 1 && p1y == 1) |
|||
end_gradient_ = 1; |
|||
else |
|||
end_gradient_ = 0; |
|||
} |
|||
|
|||
const double kBezierEpsilon = 1e-7; |
|||
|
|||
void InitRange(double p1y, double p2y) |
|||
{ |
|||
range_min_ = 0; |
|||
range_max_ = 1; |
|||
if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1) |
|||
return; |
|||
|
|||
double epsilon = kBezierEpsilon; |
|||
|
|||
// Represent the function's derivative in the form at^2 + bt + c
|
|||
// as in sampleCurveDerivativeY.
|
|||
// (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros
|
|||
// but does not actually give the slope of the curve.)
|
|||
double a = 3.0 * ay_; |
|||
double b = 2.0 * by_; |
|||
double c = cy_; |
|||
|
|||
// Check if the derivative is constant.
|
|||
if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon) |
|||
return; |
|||
|
|||
// Zeros of the function's derivative.
|
|||
double t1; |
|||
double t2 = 0; |
|||
|
|||
if (Math.Abs(a) < epsilon) |
|||
{ |
|||
// The function's derivative is linear.
|
|||
t1 = -c / b; |
|||
} |
|||
else |
|||
{ |
|||
// The function's derivative is a quadratic. We find the zeros of this
|
|||
// quadratic using the quadratic formula.
|
|||
double discriminant = b * b - 4 * a * c; |
|||
if (discriminant < 0) |
|||
return; |
|||
double discriminant_sqrt = Math.Sqrt(discriminant); |
|||
t1 = (-b + discriminant_sqrt) / (2 * a); |
|||
t2 = (-b - discriminant_sqrt) / (2 * a); |
|||
} |
|||
|
|||
double sol1 = 0; |
|||
double sol2 = 0; |
|||
|
|||
// If the solution is in the range [0,1] then we include it, otherwise we
|
|||
// ignore it.
|
|||
|
|||
// An interesting fact about these beziers is that they are only
|
|||
// actually evaluated in [0,1]. After that we take the tangent at that point
|
|||
// and linearly project it out.
|
|||
if (0 < t1 && t1 < 1) |
|||
sol1 = SampleCurveY(t1); |
|||
|
|||
if (0 < t2 && t2 < 1) |
|||
sol2 = SampleCurveY(t2); |
|||
|
|||
range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2); |
|||
range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2); |
|||
} |
|||
|
|||
void InitSpline() |
|||
{ |
|||
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
|||
for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) |
|||
{ |
|||
spline_samples_[i] = SampleCurveX(i * delta_t); |
|||
} |
|||
} |
|||
|
|||
const int kMaxNewtonIterations = 4; |
|||
|
|||
|
|||
public readonly double SolveCurveX(double x, double epsilon) |
|||
{ |
|||
if (x < 0 || x > 1) |
|||
throw new ArgumentException(); |
|||
|
|||
double t0 = 0; |
|||
double t1 = 0; |
|||
double t2 = x; |
|||
double x2 = 0; |
|||
double d2; |
|||
int i; |
|||
|
|||
#if DEBUG
|
|||
if (!monotonically_increasing_) |
|||
throw new InvalidOperationException(); |
|||
#endif
|
|||
|
|||
// Linear interpolation of spline curve for initial guess.
|
|||
double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1); |
|||
for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++) |
|||
{ |
|||
if (x <= spline_samples_[i]) |
|||
{ |
|||
t1 = delta_t * i; |
|||
t0 = t1 - delta_t; |
|||
t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) / |
|||
(spline_samples_[i] - spline_samples_[i - 1]); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
// Perform a few iterations of Newton's method -- normally very fast.
|
|||
// See https://en.wikipedia.org/wiki/Newton%27s_method.
|
|||
double newton_epsilon = Math.Min(kBezierEpsilon, epsilon); |
|||
for (i = 0; i < kMaxNewtonIterations; i++) |
|||
{ |
|||
x2 = SampleCurveX(t2) - x; |
|||
if (Math.Abs(x2) < newton_epsilon) |
|||
return t2; |
|||
d2 = SampleCurveDerivativeX(t2); |
|||
if (Math.Abs(d2) < kBezierEpsilon) |
|||
break; |
|||
t2 = t2 - x2 / d2; |
|||
} |
|||
|
|||
if (Math.Abs(x2) < epsilon) |
|||
return t2; |
|||
|
|||
// Fall back to the bisection method for reliability.
|
|||
while (t0 < t1) |
|||
{ |
|||
x2 = SampleCurveX(t2); |
|||
if (Math.Abs(x2 - x) < epsilon) |
|||
return t2; |
|||
if (x > x2) |
|||
t0 = t2; |
|||
else |
|||
t1 = t2; |
|||
t2 = (t1 + t0) * .5; |
|||
} |
|||
|
|||
// Failure.
|
|||
return t2; |
|||
} |
|||
|
|||
public readonly double Solve(double x) |
|||
{ |
|||
return SolveWithEpsilon(x, kBezierEpsilon); |
|||
} |
|||
|
|||
public readonly double SlopeWithEpsilon(double x, double epsilon) |
|||
{ |
|||
x = MathUtilities.Clamp(x, 0.0, 1.0); |
|||
double t = SolveCurveX(x, epsilon); |
|||
double dx = SampleCurveDerivativeX(t); |
|||
double dy = SampleCurveDerivativeY(t); |
|||
return dy / dx; |
|||
} |
|||
|
|||
public readonly double Slope(double x) |
|||
{ |
|||
return SlopeWithEpsilon(x, kBezierEpsilon); |
|||
} |
|||
|
|||
public readonly double RangeMin => range_min_; |
|||
public readonly double RangeMax => range_max_; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Animation.Easings; |
|||
|
|||
public class CubicBezierEasing : IEasing |
|||
{ |
|||
private CubicBezier _bezier; |
|||
//cubic-bezier(0.25, 0.1, 0.25, 1.0)
|
|||
internal CubicBezierEasing(Point controlPoint1, Point controlPoint2) |
|||
{ |
|||
ControlPoint1 = controlPoint1; |
|||
ControlPoint2 = controlPoint2; |
|||
if (controlPoint1.X < 0 || controlPoint1.X > 1 || controlPoint2.X < 0 || controlPoint2.X > 1) |
|||
throw new ArgumentException(); |
|||
_bezier = new CubicBezier(controlPoint1.X, controlPoint1.Y, controlPoint2.X, controlPoint2.Y); |
|||
} |
|||
|
|||
public Point ControlPoint2 { get; set; } |
|||
public Point ControlPoint1 { get; set; } |
|||
|
|||
internal static IEasing Ease { get; } = new CubicBezierEasing(new Point(0.25, 0.1), new Point(0.25, 1)); |
|||
|
|||
double IEasing.Ease(double progress) |
|||
{ |
|||
return _bezier.Solve(progress); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -1,11 +1,11 @@ |
|||
namespace Avalonia.Markup.Xaml |
|||
namespace Avalonia.Metadata |
|||
{ |
|||
public interface IAddChild |
|||
{ |
|||
void AddChild(object child); |
|||
} |
|||
|
|||
public interface IAddChild<T> : IAddChild |
|||
public interface IAddChild<T> |
|||
{ |
|||
void AddChild(T child); |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Platform; |
|||
|
|||
[Unstable] |
|||
public interface IPlatformGpu |
|||
{ |
|||
IPlatformGpuContext PrimaryContext { get; } |
|||
} |
|||
|
|||
[Unstable] |
|||
public interface IPlatformGpuContext : IDisposable |
|||
{ |
|||
IDisposable EnsureCurrent(); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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" } |
|||
}; |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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 |
|||
{ |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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 |
|||
{ |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations; |
|||
|
|||
|
|||
/// <summary>
|
|||
/// The base class for both key-frame and expression animation instances
|
|||
/// Is responsible for activation tracking and for subscribing to properties used in dependencies
|
|||
/// </summary>
|
|||
internal abstract class AnimationInstanceBase : IAnimationInstance |
|||
{ |
|||
private List<(ServerObject obj, CompositionProperty member)>? _trackedObjects; |
|||
protected PropertySetSnapshot Parameters { get; } |
|||
public ServerObject TargetObject { get; } |
|||
protected CompositionProperty Property { get; private set; } = null!; |
|||
private bool _invalidated; |
|||
|
|||
public AnimationInstanceBase(ServerObject target, PropertySetSnapshot parameters) |
|||
{ |
|||
Parameters = parameters; |
|||
TargetObject = target; |
|||
} |
|||
|
|||
protected void Initialize(CompositionProperty property, HashSet<(string name, string member)> trackedObjects) |
|||
{ |
|||
if (trackedObjects.Count > 0) |
|||
{ |
|||
_trackedObjects = new (); |
|||
foreach (var t in trackedObjects) |
|||
{ |
|||
var obj = Parameters.GetObjectParameter(t.name); |
|||
if (obj is ServerObject tracked) |
|||
{ |
|||
var off = tracked.GetCompositionProperty(t.member); |
|||
if (off == null) |
|||
#if DEBUG
|
|||
throw new InvalidCastException("Attempting to subscribe to unknown field"); |
|||
#else
|
|||
continue; |
|||
#endif
|
|||
_trackedObjects.Add((tracked, off)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Property = property; |
|||
} |
|||
|
|||
public abstract void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property); |
|||
protected abstract ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue); |
|||
|
|||
public ExpressionVariant Evaluate(TimeSpan now, ExpressionVariant currentValue) |
|||
{ |
|||
_invalidated = false; |
|||
return EvaluateCore(now, currentValue); |
|||
} |
|||
|
|||
public virtual void Activate() |
|||
{ |
|||
if (_trackedObjects != null) |
|||
foreach (var tracked in _trackedObjects) |
|||
tracked.obj.SubscribeToInvalidation(tracked.member, this); |
|||
} |
|||
|
|||
public virtual void Deactivate() |
|||
{ |
|||
if (_trackedObjects != null) |
|||
foreach (var tracked in _trackedObjects) |
|||
tracked.obj.UnsubscribeFromInvalidation(tracked.member, this); |
|||
} |
|||
|
|||
public void Invalidate() |
|||
{ |
|||
if (_invalidated) |
|||
return; |
|||
_invalidated = true; |
|||
TargetObject.NotifyAnimatedValueChanged(Property); |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
// ReSharper disable InconsistentNaming
|
|||
// ReSharper disable CheckNamespace
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Numerics; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// This is the base class for ExpressionAnimation and KeyFrameAnimation.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
|
|||
/// Value parameters (as opposed to reference parameters which are set using <see cref="SetReferenceParameter"/>)
|
|||
/// are copied and "embedded" into an expression at the time CompositionObject.StartAnimation is called.
|
|||
/// Changing the value of the variable after <see cref="CompositionObject.StartAnimation"/> is called will not affect
|
|||
/// the value of the ExpressionAnimation.
|
|||
/// See the remarks section of ExpressionAnimation for additional information.
|
|||
/// </remarks>
|
|||
public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase |
|||
{ |
|||
private readonly CompositionPropertySet _propertySet; |
|||
internal CompositionAnimation(Compositor compositor) : base(compositor, null!) |
|||
{ |
|||
_propertySet = new CompositionPropertySet(compositor); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears all of the parameters of the animation.
|
|||
/// </summary>
|
|||
public void ClearAllParameters() => _propertySet.ClearAll(); |
|||
|
|||
/// <summary>
|
|||
/// Clears a parameter from the animation.
|
|||
/// </summary>
|
|||
public void ClearParameter(string key) => _propertySet.Clear(key); |
|||
|
|||
void SetVariant(string key, ExpressionVariant value) => _propertySet.Set(key, value); |
|||
|
|||
public void SetColorParameter(string key, Media.Color value) => SetVariant(key, value); |
|||
|
|||
public void SetMatrix3x2Parameter(string key, Matrix3x2 value) => SetVariant(key, value); |
|||
|
|||
public void SetMatrix4x4Parameter(string key, Matrix4x4 value) => SetVariant(key, value); |
|||
|
|||
public void SetQuaternionParameter(string key, Quaternion value) => SetVariant(key, value); |
|||
|
|||
public void SetReferenceParameter(string key, CompositionObject compositionObject) => |
|||
_propertySet.Set(key, compositionObject); |
|||
|
|||
public void SetScalarParameter(string key, float value) => SetVariant(key, value); |
|||
|
|||
public void SetVector2Parameter(string key, Vector2 value) => SetVariant(key, value); |
|||
|
|||
public void SetVector3Parameter(string key, Vector3 value) => SetVariant(key, value); |
|||
|
|||
public void SetVector4Parameter(string key, Vector4 value) => SetVariant(key, value); |
|||
|
|||
public string? Target { get; set; } |
|||
|
|||
internal abstract IAnimationInstance CreateInstance(ServerObject targetObject, |
|||
ExpressionVariant? finalValue); |
|||
|
|||
internal PropertySetSnapshot CreateSnapshot() => _propertySet.Snapshot(); |
|||
|
|||
void ICompositionAnimationBase.InternalOnly() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Transport; |
|||
|
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
public class CompositionAnimationGroup : CompositionObject, ICompositionAnimationBase |
|||
{ |
|||
internal List<CompositionAnimation> Animations { get; } = new List<CompositionAnimation>(); |
|||
void ICompositionAnimationBase.InternalOnly() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public void Add(CompositionAnimation value) => Animations.Add(value); |
|||
public void Remove(CompositionAnimation value) => Animations.Remove(value); |
|||
public void RemoveAll() => Animations.Clear(); |
|||
|
|||
public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
// ReSharper disable CheckNamespace
|
|||
using System; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
/// <summary>
|
|||
/// A Composition Animation that uses a mathematical equation to calculate the value for an animating property every frame.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The core of ExpressionAnimations allows a developer to define a mathematical equation that can be used to calculate the value
|
|||
/// of a targeted animating property each frame.
|
|||
/// This contrasts <see cref="KeyFrameAnimation"/>s, which use an interpolator to define how the animating
|
|||
/// property changes over time. The mathematical equation can be defined using references to properties
|
|||
/// of Composition objects, mathematical functions and operators and Input.
|
|||
/// Use the <see cref="CompositionObject.StartAnimation"/> method to start the animation.
|
|||
/// </remarks>
|
|||
public class ExpressionAnimation : CompositionAnimation |
|||
{ |
|||
private string? _expression; |
|||
private Expression? _parsedExpression; |
|||
|
|||
internal ExpressionAnimation(Compositor compositor) : base(compositor) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The mathematical equation specifying how the animated value is calculated each frame.
|
|||
/// The Expression is the core of an <see cref="ExpressionAnimation"/> and represents the equation
|
|||
/// the system will use to calculate the value of the animation property each frame.
|
|||
/// The equation is set on this property in the form of a string.
|
|||
/// Although expressions can be defined by simple mathematical equations such as "2+2",
|
|||
/// the real power lies in creating mathematical relationships where the input values can change frame over frame.
|
|||
/// </summary>
|
|||
public string? Expression |
|||
{ |
|||
get => _expression; |
|||
set |
|||
{ |
|||
_expression = value; |
|||
_parsedExpression = null; |
|||
} |
|||
} |
|||
|
|||
private Expression ParsedExpression => _parsedExpression ??= ExpressionParser.Parse(_expression.AsSpan()); |
|||
|
|||
internal override IAnimationInstance CreateInstance( |
|||
ServerObject targetObject, ExpressionVariant? finalValue) |
|||
=> new ExpressionAnimationInstance(ParsedExpression, |
|||
targetObject, finalValue, CreateSnapshot()); |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Rendering.Composition.Expressions; |
|||
using Avalonia.Rendering.Composition.Server; |
|||
|
|||
namespace Avalonia.Rendering.Composition.Animations |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Server-side counterpart of <see cref="ExpressionAnimation"/> with values baked-in.
|
|||
/// </summary>
|
|||
internal class ExpressionAnimationInstance : AnimationInstanceBase, IAnimationInstance |
|||
{ |
|||
private readonly Expression _expression; |
|||
private ExpressionVariant _startingValue; |
|||
private readonly ExpressionVariant? _finalValue; |
|||
|
|||
protected override ExpressionVariant EvaluateCore(TimeSpan now, ExpressionVariant currentValue) |
|||
{ |
|||
var ctx = new ExpressionEvaluationContext |
|||
{ |
|||
Parameters = Parameters, |
|||
Target = TargetObject, |
|||
ForeignFunctionInterface = BuiltInExpressionFfi.Instance, |
|||
StartingValue = _startingValue, |
|||
FinalValue = _finalValue ?? _startingValue, |
|||
CurrentValue = currentValue |
|||
}; |
|||
return _expression.Evaluate(ref ctx); |
|||
} |
|||
|
|||
public override void Initialize(TimeSpan startedAt, ExpressionVariant startingValue, CompositionProperty property) |
|||
{ |
|||
_startingValue = startingValue; |
|||
var hs = new HashSet<(string, string)>(); |
|||
_expression.CollectReferences(hs); |
|||
base.Initialize(property, hs); |
|||
} |
|||
|
|||
public ExpressionAnimationInstance(Expression expression, |
|||
ServerObject target, |
|||
ExpressionVariant? finalValue, |
|||
PropertySetSnapshot parameters) : base(target, parameters) |
|||
{ |
|||
_expression = expression; |
|||
_finalValue = finalValue; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue