Browse Source

Merge branch 'master' into fixes/macos-close-window-zorder

pull/10427/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
dcb18e1ac4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  2. 41
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  3. 3
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  4. 48
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  5. 10
      src/Avalonia.Base/Input/DataFormats.cs
  6. 25
      src/Avalonia.Base/Input/DataObject.cs
  7. 50
      src/Avalonia.Base/Input/DataObjectExtensions.cs
  8. 17
      src/Avalonia.Base/Input/IDataObject.cs
  9. 5
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  10. 9
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  11. 17
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  12. 2
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  13. 12
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  14. 41
      src/Avalonia.Native/ClipboardImpl.cs
  15. 55
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  16. 91
      src/Browser/Avalonia.Browser/BrowserDataObject.cs
  17. 9
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  18. 2
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  19. 21
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  20. 22
      src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs
  21. 5
      src/Browser/Avalonia.Browser/Interop/InputHelper.cs
  22. 3
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  23. 16
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  24. 4
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  25. 19
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts
  26. 22
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts
  27. 42
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
  28. 8
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
  29. 3
      src/Windows/Avalonia.Win32/ClipboardFormats.cs
  30. 15
      src/Windows/Avalonia.Win32/DataObject.cs
  31. 18
      src/Windows/Avalonia.Win32/OleDataObject.cs

2
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -238,7 +238,7 @@
-(BOOL)canBecomeKeyWindow
{
if(_canBecomeKeyWindow)
if(_canBecomeKeyWindow && !_closed)
{
// If the window has a child window being shown as a dialog then don't allow it to become the key window.
auto parent = dynamic_cast<WindowImpl*>(_parent.getRaw());

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

@ -306,25 +306,8 @@ namespace ControlCatalog.Pages
resultText += @$"
Content:
";
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#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);
}
resultText += await ReadTextFromFile(file, 10000);
}
openedFileContent.Text = resultText;
@ -354,6 +337,28 @@ namespace ControlCatalog.Pages
}
}
public static async Task<string> ReadTextFromFile(IStorageFile file, int length)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
return new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);

3
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -25,7 +25,6 @@
BorderThickness="2">
<TextBlock Name="DragStateCustom" TextWrapping="Wrap">Drag Me (custom)</TextBlock>
</Border>
<TextBlock Name="DropState" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Margin="8"
@ -47,5 +46,7 @@
</Border>
</StackPanel>
</WrapPanel>
<TextBlock x:Name="DropState" TextWrapping="Wrap" />
</StackPanel>
</UserControl>

48
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -1,27 +1,29 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
namespace ControlCatalog.Pages
{
public class DragAndDropPage : UserControl
{
TextBlock _DropState;
private readonly TextBlock _dropState;
private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
public DragAndDropPage()
{
this.InitializeComponent();
_DropState = this.Get<TextBlock>("DropState");
_dropState = this.Get<TextBlock>("DropState");
int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
$"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move);
SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
}
void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects)
@ -68,12 +70,12 @@ namespace ControlCatalog.Pages
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.FileNames)
&& !e.Data.Contains(DataFormats.Files)
&& !e.Data.Contains(CustomFormat))
e.DragEffects = DragDropEffects.None;
}
void Drop(object? sender, DragEventArgs e)
async void Drop(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
@ -85,11 +87,41 @@ namespace ControlCatalog.Pages
}
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
{
_dropState.Text = e.Data.GetText();
}
else if (e.Data.Contains(DataFormats.Files))
{
var files = e.Data.GetFiles() ?? Array.Empty<IStorageItem>();
var contentStr = "";
foreach (var item in files)
{
if (item is IStorageFile file)
{
var content = await DialogsPage.ReadTextFromFile(file, 1000);
contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}";
}
else if (item is IStorageFolder folder)
{
var items = await folder.GetItemsAsync();
contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}";
}
}
_dropState.Text = contentStr;
}
#pragma warning disable CS0618 // Type or member is obsolete
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty<string>());
{
var files = e.Data.GetFileNames();
_dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty<string>());
}
#pragma warning restore CS0618 // Type or member is obsolete
else if (e.Data.Contains(CustomFormat))
_DropState.Text = "Custom: " + e.Data.Get(CustomFormat);
{
_dropState.Text = "Custom: " + e.Data.Get(CustomFormat);
}
}
dragMe.PointerPressed += DoDrag;

10
src/Avalonia.Base/Input/DataFormats.cs

@ -1,4 +1,6 @@
namespace Avalonia.Input
using System;
namespace Avalonia.Input
{
public static class DataFormats
{
@ -7,9 +9,15 @@
/// </summary>
public static readonly string Text = nameof(Text);
/// <summary>
/// Dataformat for one or more files.
/// </summary>
public static readonly string Files = nameof(Files);
/// <summary>
/// Dataformat for one or more filenames
/// </summary>
[Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")]
public static readonly string FileNames = nameof(FileNames);
}
}

25
src/Avalonia.Base/Input/DataObject.cs

@ -2,37 +2,34 @@
namespace Avalonia.Input
{
/// <summary>
/// Specific and mutable implementation of the IDataObject interface.
/// </summary>
public class DataObject : IDataObject
{
private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
private readonly Dictionary<string, object> _items = new();
/// <inheritdoc />
public bool Contains(string dataFormat)
{
return _items.ContainsKey(dataFormat);
}
/// <inheritdoc />
public object? Get(string dataFormat)
{
if (_items.ContainsKey(dataFormat))
return _items[dataFormat];
return null;
return _items.TryGetValue(dataFormat, out var item) ? item : null;
}
/// <inheritdoc />
public IEnumerable<string> GetDataFormats()
{
return _items.Keys;
}
public IEnumerable<string>? GetFileNames()
{
return Get(DataFormats.FileNames) as IEnumerable<string>;
}
public string? GetText()
{
return Get(DataFormats.Text) as string;
}
/// <summary>
/// Sets a value to the internal store of the data object with <see cref="DataFormats"/> as a key.
/// </summary>
public void Set(string dataFormat, object value)
{
_items[dataFormat] = value;

50
src/Avalonia.Base/Input/DataObjectExtensions.cs

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
public static class DataObjectExtensions
{
/// <summary>
/// Returns a list of files if the DataObject contains files or filenames.
/// <seealso cref="DataFormats.Files"/>.
/// </summary>
/// <returns>
/// Collection of storage items - files or folders. If format isn't available, returns null.
/// </returns>
public static IEnumerable<IStorageItem>? GetFiles(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Files) as IEnumerable<IStorageItem>;
}
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
/// <returns>
/// Collection of file names. If format isn't available, returns null.
/// </returns>
[System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")]
public static IEnumerable<string>? GetFileNames(this IDataObject dataObject)
{
return (dataObject.Get(DataFormats.FileNames) as IEnumerable<string>)
?? dataObject.GetFiles()?
.Select(f => f.TryGetLocalPath())
.Where(p => !string.IsNullOrEmpty(p))
.OfType<string>();
}
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
/// <returns>
/// A text string. If format isn't available, returns null.
/// </returns>
public static string? GetText(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Text) as string;
}
}
}

17
src/Avalonia.Base/Input/IDataObject.cs

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
@ -19,21 +21,12 @@ namespace Avalonia.Input
/// </summary>
bool Contains(string dataFormat);
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
string? GetText();
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
IEnumerable<string>? GetFileNames();
/// <summary>
/// Tries to get the data of the given DataFormat.
/// </summary>
/// <returns>
/// Object data. If format isn't available, returns null.
/// </returns>
object? Get(string dataFormat);
}
}

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

@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFile : IStorageBookmarkFile
{
public BclStorageFile(string fileName)
{
FileInfo = new FileInfo(fileName);
}
public BclStorageFile(FileInfo fileInfo)
{
FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));

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

@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFolder : IStorageBookmarkFolder
{
public BclStorageFolder(string path)
{
DirectoryInfo = new DirectoryInfo(path);
if (!DirectoryInfo.Exists)
{
throw new ArgumentException("Directory must exist");
}
}
public BclStorageFolder(DirectoryInfo directoryInfo)
{
DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));

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

@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO;
internal static class StorageProviderHelpers
{
public static IStorageItem? TryCreateBclStorageItem(string path)
{
var directory = new DirectoryInfo(path);
if (directory.Exists)
{
return new BclStorageFolder(directory);
}
var file = new FileInfo(path);
if (file.Exists)
{
return new BclStorageFile(file);
}
return null;
}
public static Uri FilePathToUri(string path)
{
var uriPath = new StringBuilder(path)

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

@ -12,6 +12,8 @@ public class PickerOptions
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// Can be obtained from previously picked folder or using <see cref="IStorageProvider.TryGetFolderFromPathAsync"/>
/// or <see cref="IStorageProvider.TryGetWellKnownFolderAsync"/>.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
}

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

@ -11,12 +11,24 @@ public static class StorageProviderExtensions
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPathAsync"/>
public static Task<IStorageFile?> TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
}
return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
public static Task<IStorageFolder?> TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
}
return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
}

41
src/Avalonia.Native/ClipboardImpl.cs

@ -2,11 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Native
{
@ -56,8 +56,13 @@ namespace Avalonia.Native
{
if(fmt.String == NSPasteboardTypeString)
rv.Add(DataFormats.Text);
if(fmt.String == NSFilenamesPboardType)
rv.Add(DataFormats.FileNames);
if (fmt.String == NSFilenamesPboardType)
{
#pragma warning disable CS0618 // Type or member is obsolete
rv.Add(DataFormats.FileNames);
#pragma warning restore CS0618 // Type or member is obsolete
rv.Add(DataFormats.Files);
}
}
}
}
@ -74,7 +79,13 @@ namespace Avalonia.Native
public IEnumerable<string> GetFileNames()
{
using (var strings = _native.GetStrings(NSFilenamesPboardType))
return strings.ToStringArray();
return strings?.ToStringArray();
}
public IEnumerable<IStorageItem> GetFiles()
{
return GetFileNames()?.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!)
.Where(f => f is not null);
}
public unsafe Task SetDataObjectAsync(IDataObject data)
@ -102,8 +113,12 @@ namespace Avalonia.Native
{
if (format == DataFormats.Text)
return await GetTextAsync();
#pragma warning disable CS0618 // Type or member is obsolete
if (format == DataFormats.FileNames)
return GetFileNames();
#pragma warning restore CS0618 // Type or member is obsolete
if (format == DataFormats.Files)
return GetFiles();
using (var n = _native.GetBytes(format))
return n.Bytes;
}
@ -131,20 +146,16 @@ namespace Avalonia.Native
public bool Contains(string dataFormat) => Formats.Contains(dataFormat);
public string GetText()
{
// bad idea in general, but API is synchronous anyway
return _clipboard.GetTextAsync().Result;
}
public IEnumerable<string> GetFileNames() => _clipboard.GetFileNames();
public object Get(string dataFormat)
{
if (dataFormat == DataFormats.Text)
return GetText();
return _clipboard.GetTextAsync().Result;
if (dataFormat == DataFormats.Files)
return _clipboard.GetFiles();
#pragma warning disable CS0618
if (dataFormat == DataFormats.FileNames)
return GetFileNames();
#pragma warning restore CS0618
return _clipboard.GetFileNames();
return null;
}
}

55
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -106,6 +106,8 @@ namespace Avalonia.Browser
InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp,
OnPointerCancel, OnWheel);
InputHelper.SubscribeDropEvents(_containerElement, OnDragEvent);
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
_dpi = DomHelper.ObserveDpi(OnDpiChanged);
@ -293,6 +295,59 @@ namespace Avalonia.Browser
return modifiers;
}
public bool OnDragEvent(JSObject args)
{
var eventType = args?.GetPropertyAsString("type") switch
{
"dragenter" => RawDragEventType.DragEnter,
"dragover" => RawDragEventType.DragOver,
"dragleave" => RawDragEventType.DragLeave,
"drop" => RawDragEventType.Drop,
_ => (RawDragEventType)(int)-1
};
var dataObject = args?.GetPropertyAsJSObject("dataTransfer");
if (args is null || eventType < 0 || dataObject is null)
{
return false;
}
// If file is dropped, we need storage js to be referenced.
// TODO: restructure JS files, so it's not needed.
_ = AvaloniaModule.ImportStorage();
var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY"));
var modifiers = GetModifiers(args);
var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none";
var effectAllowed = DragDropEffects.None;
if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Copy;
}
if (effectAllowedStr.Contains("link", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Link;
}
if (effectAllowedStr.Contains("move", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Move;
}
if (effectAllowedStr.Equals("all", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link;
}
if (effectAllowed == DragDropEffects.None)
{
return false;
}
var dropEffect = _topLevelImpl.RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed);
dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant());
return eventType is RawDragEventType.Drop or RawDragEventType.DragOver
&& dropEffect != DragDropEffects.None;
}
private bool OnKeyDown (string code, string key, int modifier)
{
var handled = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier);

91
src/Browser/Avalonia.Browser/BrowserDataObject.cs

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using Avalonia.Browser.Interop;
using Avalonia.Browser.Storage;
using Avalonia.Input;
using Avalonia.Platform.Storage;
namespace Avalonia.Browser;
internal class BrowserDataObject : IDataObject
{
private readonly JSObject _dataObject;
public BrowserDataObject(JSObject dataObject)
{
_dataObject = dataObject;
}
public IEnumerable<string> GetDataFormats()
{
var types = new HashSet<string>(_dataObject.GetPropertyAsStringArray("types"));
var dataFormats = new HashSet<string>(types.Count);
foreach (var type in types)
{
if (type.StartsWith("text/", StringComparison.Ordinal))
{
dataFormats.Add(DataFormats.Text);
}
else if (type.Equals("Files", StringComparison.Ordinal))
{
dataFormats.Add(DataFormats.Files);
}
dataFormats.Add(type);
}
// If drag'n'drop an image from the another web page, if won't add "Files" to the supported types, but only a "text/uri-list".
// With "text/uri-list" browser can add actual file as well.
var filesCount = _dataObject.GetPropertyAsJSObject("files")?.GetPropertyAsInt32("count");
if (filesCount > 0)
{
dataFormats.Add(DataFormats.Files);
}
return dataFormats;
}
public bool Contains(string dataFormat)
{
return GetDataFormats().Contains(dataFormat);
}
public object? Get(string dataFormat)
{
if (dataFormat == DataFormats.Files)
{
var files = _dataObject.GetPropertyAsJSObject("files");
if (files is not null)
{
return StorageHelper.FilesToItemsArray(files)
.Select(reference => reference.GetPropertyAsString("kind") switch
{
"directory" => (IStorageItem)new JSStorageFolder(reference),
"file" => new JSStorageFile(reference),
_ => null
})
.Where(i => i is not null)
.ToArray()!;
}
return null;
}
if (dataFormat == DataFormats.Text)
{
if (_dataObject.CallMethodString("getData", "text/plain") is { Length :> 0 } textData)
{
return textData;
}
}
if (_dataObject.CallMethodString("getData", dataFormat) is { Length: > 0 } data)
{
return data;
}
return null;
}
}

9
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -164,6 +164,15 @@ namespace Avalonia.Browser
return false;
}
public DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers, BrowserDataObject dataObject, DragDropEffects dropEffect)
{
var device = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>();
var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
Console.WriteLine($"{eventArgs.Location} {eventArgs.Effects} {eventArgs.Type} {eventArgs.KeyModifiers}");
Input?.Invoke(eventArgs);
return eventArgs.Effects;
}
public void Dispose()
{

2
src/Browser/Avalonia.Browser/ClipboardImpl.cs

@ -24,6 +24,6 @@ namespace Avalonia.Browser
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object?> GetDataAsync(string format) => Task.FromResult<object?>(new());
public Task<object?> GetDataAsync(string format) => Task.FromResult<object?>(null);
}
}

21
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -1,24 +1,29 @@
using System.Runtime.InteropServices.JavaScript;
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Avalonia.Browser.Interop;
internal static partial class AvaloniaModule
{
public const string MainModuleName = "avalonia";
public const string StorageModuleName = "storage";
public static Task ImportMain()
private static readonly Lazy<Task> s_importMain = new(() =>
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js"));
}
});
public static Task ImportStorage()
private static readonly Lazy<Task> s_importStorage = new(() =>
{
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js"));
}
});
public const string MainModuleName = "avalonia";
public const string StorageModuleName = "storage";
public static Task ImportMain() => s_importMain.Value;
public static Task ImportStorage() => s_importStorage.Value;
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();

22
src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs

@ -0,0 +1,22 @@
using System.Runtime.InteropServices.JavaScript;
namespace Avalonia.Browser.Interop;
internal static partial class GeneralHelpers
{
[JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)]
public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key);
public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key);
[JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)]
public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key);
public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key);
[JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)]
public static partial string IntCallMethodString(JSObject jsObject, string name);
[JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)]
public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1);
public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name);
public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1);
}

5
src/Browser/Avalonia.Browser/Interop/InputHelper.cs

@ -43,13 +43,16 @@ internal static partial class InputHelper
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>]
Func<JSObject, bool> wheel);
[JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeInputEvents(
JSObject htmlElement,
[JSMarshalAs<JSType.Function<JSType.String, JSType.Boolean>>]
Func<string, bool> input);
[JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)]
public static partial void SubscribeDropEvents(JSObject containerElement,
[JSMarshalAs<JSType.Function<JSType.Object, JSType.Boolean>>] Func<JSObject, bool> dragEvent);
[JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)]
[return: JSMarshalAs<JSType.Array<JSType.Object>>]
public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent);

3
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -46,6 +46,9 @@ internal static partial class StorageHelper
[JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)]
public static partial JSObject[] ItemsArray(JSObject item);
[JSImport("StorageItems.filesToItemsArray", AvaloniaModule.StorageModuleName)]
public static partial JSObject[] FilesToItemsArray(JSObject item);
[JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions);

16
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage;
@ -18,15 +16,13 @@ internal class BrowserStorageProvider : IStorageProvider
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
private readonly Lazy<Task> _lazyModule = new(() => AvaloniaModule.ImportStorage());
public bool CanOpen => true;
public bool CanSave => StorageHelper.HasNativeFilePicker();
public bool CanPickFolder => true;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, excludeAll) = ConvertFileTypes(options.FileTypeFilter);
@ -60,7 +56,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
var (types, excludeAll) = ConvertFileTypes(options.FileTypeChoices);
@ -88,7 +84,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle;
try
@ -104,14 +100,14 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFile(item) : null;
}
public async Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var item = await StorageHelper.OpenBookmark(bookmark);
return item is not null ? new JSStorageFolder(item) : null;
}
@ -128,7 +124,7 @@ internal class BrowserStorageProvider : IStorageProvider
public async Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
await _lazyModule.Value;
await AvaloniaModule.ImportStorage();
var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch
{
WellKnownFolder.Desktop => "desktop",

4
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -5,6 +5,7 @@ import { Caniuse } from "./avalonia/caniuse";
import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
export {
Caniuse,
@ -15,5 +16,6 @@ export {
AvaloniaDOM,
StreamHelper,
NativeControlHost,
NavigationHelper
NavigationHelper,
GeneralHelpers
};

19
src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts

@ -0,0 +1,19 @@
export class GeneralHelpers {
public static itemsArrayAt(instance: any, key: string): any[] {
const items = instance[key];
if (!items) {
return [];
}
const retItems = [];
for (let i = 0; i < items.length; i++) {
retItems[i] = items[i];
}
return retItems;
}
public static callMethod(instance: any, name: string /*, args */): any {
const args = Array.prototype.slice.call(arguments, 2);
return instance[name].apply(instance, args);
}
}

22
src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts

@ -174,6 +174,28 @@ export class InputHelper {
};
}
public static subscribeDropEvents(
element: HTMLInputElement,
dragEvent: (args: any) => boolean
) {
const dragHandler = (args: Event) => {
if (dragEvent(args as any)) {
args.preventDefault();
}
};
element.addEventListener("dragover", dragHandler);
element.addEventListener("dragenter", dragHandler);
element.addEventListener("dragleave", dragHandler);
element.addEventListener("drop", dragHandler);
return () => {
element.removeEventListener("dragover", dragHandler);
element.removeEventListener("dragenter", dragHandler);
element.removeEventListener("dragleave", dragHandler);
element.removeEventListener("drop", dragHandler);
};
}
public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] {
return pointerEvent.getCoalescedEvents();
}

42
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@ -3,8 +3,9 @@ import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFile
import { Caniuse } from "../avalonia";
export class StorageItem {
constructor(
private constructor(
public handle?: FileSystemFileHandle | FileSystemDirectoryHandle,
private readonly file?: File,
private readonly bookmarkId?: string,
public wellKnownType?: WellKnownDirectory
) {
@ -14,6 +15,9 @@ export class StorageItem {
if (this.handle) {
return this.handle.name;
}
if (this.file) {
return this.file.name;
}
return this.wellKnownType ?? "";
}
@ -21,14 +25,29 @@ export class StorageItem {
if (this.handle) {
return this.handle.kind;
}
if (this.file) {
return "file";
}
return "directory";
}
public static createFromHandle(handle: FileSystemFileHandle | FileSystemDirectoryHandle, bookmarkId?: string) {
return new StorageItem(handle, undefined, bookmarkId, undefined);
}
public static createFromFile(file: File) {
return new StorageItem(undefined, file, undefined, undefined);
}
public static createWellKnownDirectory(type: WellKnownDirectory) {
return new StorageItem(undefined, undefined, type);
return new StorageItem(undefined, undefined, undefined, type);
}
public static async openRead(item: StorageItem): Promise<Blob> {
if (item.file) {
return item.file;
}
if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file");
}
@ -41,7 +60,7 @@ export class StorageItem {
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> {
if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file");
throw new Error("StorageItem is not a writeable file");
}
await item.verityPermissions("readwrite");
@ -52,8 +71,9 @@ export class StorageItem {
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
// getFile can fail with an exception depending if we use polyfill with a save file dialog or not.
try {
const file = item.handle instanceof FileSystemFileHandle &&
await item.handle.getFile();
const file = item.handle && "getFile" in item.handle
? await item.handle.getFile()
: item.file;
if (!file) {
return null;
@ -144,4 +164,16 @@ export class StorageItems {
public static itemsArray(instance: StorageItems): StorageItem[] {
return instance.items;
}
public static filesToItemsArray(files: File[]): StorageItem[] {
if (!files) {
return [];
}
const retItems = [];
for (let i = 0; i < files.length; i++) {
retItems[i] = StorageItem.createFromFile(files[i]);
}
return retItems;
}
}

8
src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts

@ -19,7 +19,7 @@ export class StorageProvider {
};
const handle = await showDirectoryPicker(options as any);
return new StorageItem(handle);
return StorageItem.createFromHandle(handle);
}
public static async openFileDialog(
@ -33,7 +33,7 @@ export class StorageProvider {
};
const handles = await showOpenFilePicker(options);
return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle)));
return new StorageItems(handles.map((handle: FileSystemFileHandle) => StorageItem.createFromHandle(handle)));
}
public static async saveFileDialog(
@ -48,14 +48,14 @@ export class StorageProvider {
// Always prefer native save file picker, as polyfill solutions are not reliable.
const handle = await (globalThis as any).showSaveFilePicker(options);
return new StorageItem(handle);
return StorageItem.createFromHandle(handle);
}
public static async openBookmark(key: string): Promise<StorageItem | null> {
const connection = await avaloniaDb.connect();
try {
const handle = await connection.get(fileBookmarksStore, key);
return handle && new StorageItem(handle, key);
return handle && StorageItem.createFromHandle(handle, key);
} finally {
connection.close();
}

3
src/Windows/Avalonia.Win32/ClipboardFormats.cs

@ -29,7 +29,10 @@ namespace Avalonia.Win32
private static readonly List<ClipboardFormat> s_formatList = new()
{
new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT),
new ClipboardFormat(DataFormats.Files, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP),
#pragma warning disable CS0618 // Type or member is obsolete
new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP),
#pragma warning restore CS0618 // Type or member is obsolete
};

15
src/Windows/Avalonia.Win32/DataObject.cs

@ -10,6 +10,7 @@ using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Formatters.Binary;
using Avalonia.Input;
using Avalonia.MicroCom;
using Avalonia.Platform.Storage;
using Avalonia.Win32.Interop;
using FORMATETC = Avalonia.Win32.Interop.FORMATETC;
@ -124,16 +125,6 @@ namespace Avalonia.Win32
return _wrapped.GetDataFormats();
}
IEnumerable<string>? IDataObject.GetFileNames()
{
return _wrapped.GetFileNames();
}
string? IDataObject.GetText()
{
return _wrapped.GetText();
}
object? IDataObject.Get(string dataFormat)
{
return _wrapped.Get(dataFormat);
@ -260,8 +251,12 @@ namespace Avalonia.Win32
object data = _wrapped.Get(dataFormat)!;
if (dataFormat == DataFormats.Text || data is string)
return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data) ?? string.Empty);
#pragma warning disable CS0618 // Type or member is obsolete
if (dataFormat == DataFormats.FileNames && data is IEnumerable<string> files)
return WriteFileListToHGlobal(ref hGlobal, files);
#pragma warning restore CS0618 // Type or member is obsolete
if (dataFormat == DataFormats.Files && data is IEnumerable<IStorageItem> items)
return WriteFileListToHGlobal(ref hGlobal, items.Select(f => f.TryGetLocalPath()).Where(f => f is not null)!);
if (data is Stream stream)
{
var length = (int)(stream.Length - stream.Position);

18
src/Windows/Avalonia.Win32/OleDataObject.cs

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Serialization.Formatters.Binary;
using Avalonia.Input;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Utilities;
using Avalonia.Win32.Interop;
using MicroCom.Runtime;
@ -34,16 +35,6 @@ namespace Avalonia.Win32
return GetDataFormatsCore().Distinct();
}
public string? GetText()
{
return (string?)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT);
}
public IEnumerable<string>? GetFileNames()
{
return (IEnumerable<string>?)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT);
}
public object? Get(string dataFormat)
{
return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT);
@ -67,8 +58,15 @@ namespace Avalonia.Win32
{
if (format == DataFormats.Text)
return ReadStringFromHGlobal(medium.unionmember);
#pragma warning disable CS0618
if (format == DataFormats.FileNames)
#pragma warning restore CS0618
return ReadFileNamesFromHGlobal(medium.unionmember);
if (format == DataFormats.Files)
return ReadFileNamesFromHGlobal(medium.unionmember)
.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!)
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
.Where(f => f is not null);
byte[] data = ReadBytesFromHGlobal(medium.unionmember);

Loading…
Cancel
Save