Skip to content

Commit

Permalink
feat: more complete file/folder pickers for macOS/skia
Browse files Browse the repository at this point in the history
  • Loading branch information
spouliot committed Mar 12, 2024
1 parent 8ea0d29 commit 8bf1f41
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 26 deletions.
28 changes: 24 additions & 4 deletions src/Uno.UI.Runtime.Skia.MacOS/MacOSFileOpenPickerExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,41 @@ internal class MacOSFileOpenPickerExtension : IFileOpenPickerExtension
{
private static readonly MacOSFileOpenPickerExtension _instance = new();

private static readonly string[] _asteriskArray = new string[] { "*" };

private MacOSFileOpenPickerExtension()
{
_filters = Array.Empty<string>();
}

public static void Register() => ApiExtensibility.Register<FileOpenPicker>(typeof(IFileOpenPickerExtension), _ => _instance);

// TODO: we need something more in IFileOpenPickerExtension so we can customize the native picker from the user-created FileOpenPicker
// Mapping
// WinUI AppKit (NSOpenPanel)
// --------------------------------------------------------------
// CommitButtonText (string) prompt (NSString)
// ContinuationData (ValueSet) n/a
// FileTypeFilter allowedFileTypes (NSArray)
// SettingsIdentifier (string) identifier (NSString)
// SuggestedStartLocation (enum) directoryURL (NSURL)
// User (User) n/a
// ViewMode (enum) n/a
public void Customize(FileOpenPicker picker)
{
// TODO: call native code
_prompt = picker.CommitButtonText.Length == 0 ? null : picker.CommitButtonText;
_filters = picker.FileTypeFilter.Except(_asteriskArray).Select(ext => ext.TrimStart('.')).ToArray();
_identifier = picker.SettingsIdentifier.Length == 0 ? null : picker.SettingsIdentifier;
_suggestedStartLocation = picker.SuggestedStartLocation;
}

private string? _prompt;
private string[] _filters;
private string? _identifier;
private PickerLocationId _suggestedStartLocation;

public async Task<IReadOnlyList<StorageFile>> PickMultipleFilesAsync(CancellationToken token)
{
var array = NativeUno.uno_pick_multiple_files(null);
var array = NativeUno.uno_pick_multiple_files(_prompt, _identifier, (int)_suggestedStartLocation, _filters, _filters.Length);
var files = new List<StorageFile>();
var ptr = Marshal.ReadIntPtr(array);
while (ptr != IntPtr.Zero)
Expand All @@ -44,7 +64,7 @@ public async Task<IReadOnlyList<StorageFile>> PickMultipleFilesAsync(Cancellatio

public async Task<StorageFile?> PickSingleFileAsync(CancellationToken token)
{
var file = NativeUno.uno_pick_single_file(null);
var file = NativeUno.uno_pick_single_file(_prompt, _identifier, (int)_suggestedStartLocation, _filters, _filters.Length);
return file is null ? null : await StorageFile.GetFileFromPathAsync(file);
}
}
44 changes: 43 additions & 1 deletion src/Uno.UI.Runtime.Skia.MacOS/MacOSFileSavePickerExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,55 @@ internal class MacOSFileSavePickerExtension : IFileSavePickerExtension

private MacOSFileSavePickerExtension()
{
_filters = Array.Empty<string>();
}

public static void Register() => ApiExtensibility.Register<FileSavePicker>(typeof(IFileSavePickerExtension), _ => _instance);

// Mapping
// WinUI AppKit (NSSavePanel)
// ----------------------------------------------------------------------
// CommitButtonText (string) prompt (NSString)
// ContinuationData (ValueSet) n/a
// DefaultFileExtension (string) _Do not set this property_ says WinUI
// EnterpriseId n/a
// FileTypeChoices (dictionary) allowedFileTypes (NSArray)
// SettingsIdentifier (string) identifier (NSString)
// SuggestedFileName (string) nameFieldStringValue (NSString)
// SuggestedSaveFile (StorageFile) n/a
// SuggestedStartLocation (enum) directoryURL (NSURL)
// User (User) n/a
public void Customize(FileSavePicker picker)
{
_prompt = picker.CommitButtonText.Length == 0 ? null : picker.CommitButtonText;
_identifier = picker.SettingsIdentifier.Length == 0 ? null : picker.SettingsIdentifier;
_suggestedFileName = picker.SuggestedFileName.Length == 0 ? null : picker.SuggestedFileName;
_suggestedStartLocation = picker.SuggestedStartLocation;
if (picker.FileTypeChoices.Count == 0)
{
return;
}

var list = new List<string>();
foreach (var value in picker.FileTypeChoices.Values)
{
foreach (var ext in value)
{
list.Add(ext.TrimStart('.'));
}
}
_filters = list.ToArray();
}

private string? _prompt;
private string[] _filters;
private string? _identifier;
private string? _suggestedFileName;
private PickerLocationId _suggestedStartLocation;

public async Task<StorageFile?> PickSaveFileAsync(CancellationToken token)
{
var file = NativeUno.uno_pick_save_file(null);
var file = NativeUno.uno_pick_save_file(_prompt, _identifier, _suggestedFileName, (int)_suggestedStartLocation, _filters, _filters.Length);
return file is null ? null : await StorageFile.GetFileFromPathAsync(file);
}
}
24 changes: 23 additions & 1 deletion src/Uno.UI.Runtime.Skia.MacOS/MacOSFolderPickerExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,31 @@ private MacOSFolderPickerExtension()

public static void Register() => ApiExtensibility.Register<FolderPicker>(typeof(IFolderPickerExtension), _ => _instance);

// Mapping
// WinUI AppKit (NSOpenPanel)
// --------------------------------------------------------------
// CommitButtonText (string) prompt (NSString)
// ContinuationData (ValueSet) n/a
// FileTypeFilter allowedFileTypes (NSArray)
// SettingsIdentifier (string) identifier (NSString)
// SuggestedStartLocation (enum) directoryURL (NSURL)
// User (User) n/a
// ViewMode (enum) n/a
public void Customize(FolderPicker picker)
{
_prompt = picker.CommitButtonText.Length == 0 ? null : picker.CommitButtonText;
// FileTypeFilter can be set but they are not doing anything with the native picker
_identifier = picker.SettingsIdentifier.Length == 0 ? null : picker.SettingsIdentifier;
_suggestedStartLocation = picker.SuggestedStartLocation;
}

private string? _prompt;
private string? _identifier;
private PickerLocationId _suggestedStartLocation;

public async Task<StorageFolder?> PickSingleFolderAsync(CancellationToken token)
{
var folder = NativeUno.uno_pick_single_folder();
var folder = NativeUno.uno_pick_single_folder(_prompt, _identifier, (int)_suggestedStartLocation);
return folder is null ? null : await StorageFolder.GetFolderFromPathAsync(folder);
}
}
11 changes: 7 additions & 4 deletions src/Uno.UI.Runtime.Skia.MacOS/NativeUno.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,19 @@ internal static unsafe partial void uno_set_window_close_callbacks(
internal static partial void uno_window_set_min_size(nint window, double width, double height);

[LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial string? /* const char* _Nullable */ uno_pick_single_folder();
internal static partial string? /* const char* _Nullable */ uno_pick_single_folder(string? prompt, string? identifier, int suggestedStartLocation);

[LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial string? /* const char* _Nullable */ uno_pick_single_file(string? prompt);
internal static partial string? /* const char* _Nullable */ uno_pick_single_file(string? prompt, string? identifier, int suggestedStartLocation,
string[] filters, int filterSize);

[LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial IntPtr /* const char* _Nullable * _Nullable */ uno_pick_multiple_files(string? prompt);
internal static partial IntPtr /* const char* _Nullable * _Nullable */ uno_pick_multiple_files(string? prompt, string? identifier, int suggestedStartLocation,
string[] filters, int filterSize);

[LibraryImport("libUnoNativeMac.dylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial string? /* const char* _Nullable */ uno_pick_save_file(string? prompt);
internal static partial string? /* const char* _Nullable */ uno_pick_save_file(string? prompt, string? identifier, string? suggestedFileName, int suggestedStartLocation,
string[] filters, int filtersSize);

[LibraryImport("libUnoNativeMac.dylib")]
internal static partial void uno_clipboard_clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,23 @@

NS_ASSUME_NONNULL_BEGIN

char* _Nullable uno_pick_single_folder(void);
char* _Nullable uno_pick_single_file(const char* _Nullable prompt);
char* _Nullable uno_pick_save_file(const char* _Nullable prompt);
char* _Nullable * _Nullable uno_pick_multiple_files(const char* _Nullable prompt);
// https://learn.microsoft.com/en-us/uwp/api/windows.storage.pickers.pickerlocationid?view=winrt-22621
typedef NS_ENUM(sint32, PickerLocationId) {
PickerLocationIdDocumentsLibrary = 0,
PickerLocationIdComputerFolder = 1,
PickerLocationIdDesktop = 2,
PickerLocationIdDownloads = 3,
PickerLocationIdHomeGroup = 4,
PickerLocationIdMusicLibrary = 5,
PickerLocationIdPicturesLibrary = 6,
PickerLocationIdVideosLibrary = 7,
PickerLocationIdObjects3D = 8,
PickerLocationIdUnspecified = 9,
};

char* _Nullable uno_pick_single_folder(const char* _Nullable prompt, const char* _Nullable identifier, PickerLocationId suggestedStartLocation);
char* _Nullable uno_pick_single_file(const char* _Nullable prompt, const char* _Nullable identifier, PickerLocationId suggestedStartLocation, char* _Nonnull filters[_Nullable], int filterSize);
char* _Nullable uno_pick_save_file(const char* _Nullable prompt, const char* _Nullable identifier, const char* _Nullable suggestedFileName, PickerLocationId suggestedStartLocation, char* _Nonnull filters[_Nullable], int filterSize);
char* _Nullable * _Nullable uno_pick_multiple_files(const char* _Nullable prompt, const char* _Nullable identifier, PickerLocationId suggestedStartLocation, char* _Nonnull filters[_Nullable], int filterSize);

NS_ASSUME_NONNULL_END
104 changes: 92 additions & 12 deletions src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/UNOPickers.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,80 @@

#import "UNOPickers.h"

char* uno_pick_single_folder(void)
NSURL* get_best_location(int32_t suggestedStartLocation)
{
NSSearchPathDirectory path;

switch (suggestedStartLocation) {
case PickerLocationIdDocumentsLibrary:
path = NSDocumentDirectory;
break;
case PickerLocationIdComputerFolder:
return [NSURL URLWithString:@"file:///"];
case PickerLocationIdDesktop:
path = NSDesktopDirectory;
break;
case PickerLocationIdDownloads:
path = NSDownloadsDirectory;
break;
case PickerLocationIdHomeGroup:
path = NSSharedPublicDirectory;
break;
case PickerLocationIdMusicLibrary:
path = NSMusicDirectory;
break;
case PickerLocationIdPicturesLibrary:
path = NSPicturesDirectory;
break;
case PickerLocationIdVideosLibrary:
path = NSMoviesDirectory;
break;
case PickerLocationIdObjects3D:
#if DEBUG
NSLog(@"get_best_location %d -> no extact match, suggesting Home directory", suggestedStartLocation);
#endif
return [NSURL URLWithString:NSHomeDirectory()];
case PickerLocationIdUnspecified:
#if DEBUG
NSLog(@"get_best_location %d -> unspecified", suggestedStartLocation);
#endif
return nil;
default:
#if DEBUG
NSLog(@"get_best_location %d -> unknown value", suggestedStartLocation);
#endif
return nil;
}

return [[NSFileManager defaultManager] URLsForDirectory:path inDomains:NSUserDomainMask][0];
}

NSMutableArray<NSString*>* get_allowed(char* filters[], int filterSize)
{
NSMutableArray<NSString*> *allowed = [[NSMutableArray alloc] initWithCapacity:filterSize];
for (int i=0; i < filterSize; i++) {
NSString *s = [NSString stringWithUTF8String:filters[i]];
[allowed addObject:s];
}
return allowed;
}

char* uno_pick_single_folder(const char* _Nullable prompt, const char* _Nullable identifier, int32_t suggestedStartLocation)
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
// based on settings from uno/src/Uno.UWP/Storage/Pickers/FolderPicker.macOS.cs
// note: `allowsOtherFileTypes` is only used on NSSavePanel (and does nothing on NSOpenPanel)
// filters are not applied in WinUI so we don't set them up here
panel.allowedFileTypes = [NSArray arrayWithObject:@"none"];
panel.canChooseDirectories = true;
panel.canChooseFiles = false;
panel.directoryURL = get_best_location(suggestedStartLocation);
if (identifier) {
panel.identifier = [NSString stringWithUTF8String:identifier];
}
if (prompt) {
panel.prompt = [NSString stringWithUTF8String:prompt];
}

if ([panel runModal] == NSModalResponseOK) {
NSURL *url = panel.URL;
if (url) {
Expand All @@ -28,18 +94,22 @@
return nil;
}

char* uno_pick_single_file(const char *prompt)
char* uno_pick_single_file(const char* _Nullable prompt, const char* _Nullable identifier, PickerLocationId suggestedStartLocation, char* filters[], int filterSize)
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
// based on settings from uno/src/Uno.UWP/Storage/Pickers/FileOpenPicker.macOS.cs
// note: `allowsOtherFileTypes` is only used on NSSavePanel (and does nothing on NSOpenPanel)
panel.allowedFileTypes = nil; // FIXME (nil means every types)
panel.allowedFileTypes = get_allowed(filters, filterSize);
panel.canChooseDirectories = false;
panel.canChooseFiles = true;
panel.allowsMultipleSelection = false;
panel.directoryURL = get_best_location(suggestedStartLocation);
if (identifier) {
panel.identifier = [NSString stringWithUTF8String:identifier];
}
if (prompt) {
panel.prompt = [NSString stringWithUTF8String:prompt];
}

if ([panel runModal] == NSModalResponseOK) {
NSURL *url = panel.URL;
if (url) {
Expand All @@ -56,18 +126,22 @@
return nil;
}

char** uno_pick_multiple_files(const char *prompt)
char** uno_pick_multiple_files(const char* _Nullable prompt, const char* _Nullable identifier, PickerLocationId suggestedStartLocation, char* filters[], int filterSize)
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
// based on settings from uno/src/Uno.UWP/Storage/Pickers/FileOpenPicker.macOS.cs
// note: `allowsOtherFileTypes` is only used on NSSavePanel (and does nothing on NSOpenPanel)
panel.allowedFileTypes = nil; // FIXME (nil means every types)
panel.allowedFileTypes = get_allowed(filters, filterSize);
panel.canChooseDirectories = false;
panel.canChooseFiles = true;
panel.allowsMultipleSelection = true;
panel.directoryURL = get_best_location(suggestedStartLocation);
if (identifier) {
panel.identifier = [NSString stringWithUTF8String:identifier];
}
if (prompt) {
panel.prompt = [NSString stringWithUTF8String:prompt];
}

if ([panel runModal] == NSModalResponseOK) {
NSArray<NSURL*> *urls = panel.URLs;
if (urls) {
Expand All @@ -91,16 +165,22 @@
return nil;
}

char* uno_pick_save_file(const char *prompt)
char* uno_pick_save_file(const char* _Nullable prompt, const char* _Nullable identifier, const char* _Nullable suggestedFileName, PickerLocationId suggestedStartLocation, char* filters[], int filterSize)
{
NSOpenPanel *panel = [NSOpenPanel openPanel];
NSSavePanel *panel = [NSSavePanel savePanel];
// based on settings from uno/src/Uno.UWP/Storage/Pickers/FileSavePicker.macOS.cs
panel.allowsOtherFileTypes = true;
panel.allowedFileTypes = nil; // FIXME (nil means every types)
panel.canChooseFiles = true;
panel.allowedFileTypes = get_allowed(filters, filterSize);
panel.directoryURL = get_best_location(suggestedStartLocation);
if (identifier) {
panel.identifier = [NSString stringWithUTF8String:identifier];
}
if (prompt) {
panel.prompt = [NSString stringWithUTF8String:prompt];
}
if (suggestedFileName) {
panel.nameFieldStringValue = [NSString stringWithUTF8String:suggestedFileName];
}
if ([panel runModal] == NSModalResponseOK) {
NSURL *url = panel.URL;
if (url) {
Expand Down
1 change: 1 addition & 0 deletions src/Uno.UWP/Storage/Pickers/FileSavePicker.skiaiOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public partial class FileSavePicker
throw new NotSupportedException("FileSavePicker extension is not registered.");
}

_fileSavePickerExtension.Customize(this);
return await _fileSavePickerExtension.PickSaveFileAsync(token);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Uno.UWP/Storage/Pickers/FolderPicker.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public partial class FolderPicker
throw new NotSupportedException("FolderPicker is not supported on this target.");
}

_folderPickerExtension.Customize(this);
return await _folderPickerExtension.PickSingleFolderAsync(token);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using System.Threading;
using Windows.Storage;
using Windows.Storage.Pickers;

namespace Uno.Extensions.Storage.Pickers
{
Expand All @@ -17,6 +18,12 @@ namespace Uno.Extensions.Storage.Pickers
public interface IFileSavePickerExtension
{
Task<StorageFile?> PickSaveFileAsync(CancellationToken token);

// called just before Pick* methods to allow the customization of the native picker to match,
// as much as possible, the user selected properties of the WinUI picker
void Customize(FileSavePicker picker)
{
}
}
}
#endif
Loading

0 comments on commit 8bf1f41

Please sign in to comment.