A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

273 lines
11 KiB

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using static Avalonia.X11.Interop.Glib;
using static Avalonia.X11.NativeDialogs.Gtk;
namespace Avalonia.X11.NativeDialogs
{
internal class GtkSystemDialog : BclStorageProvider
{
private static Task<bool>? _initialized;
private readonly X11Window _window;
private GtkSystemDialog(X11Window window)
{
_window = window;
}
public override bool CanOpen => true;
public override bool CanSave => true;
public override bool CanPickFolder => true;
internal static async Task<IStorageProvider?> TryCreate(X11Window window)
{
_initialized ??= StartGtk();
return await _initialized ? new GtkSystemDialog(window) : null;
}
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Open,
options.AllowMultiple, options.SuggestedStartLocation, null, options.FileTypeFilter, null, false)
.ConfigureAwait(false);
return files?.Where(f => File.Exists(f)).Select(f => new BclStorageFile(new FileInfo(f))).ToArray() ??
Array.Empty<IStorageFile>();
});
}
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return await await RunOnGlibThread(async () =>
{
var (folders, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.SelectFolder,
options.AllowMultiple, options.SuggestedStartLocation, null,
null, null, false)
.ConfigureAwait(false);
return folders?.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray() ??
Array.Empty<IStorageFolder>();
});
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return await await RunOnGlibThread(async () =>
{
var (files, _) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices,
options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
return files?.FirstOrDefault() is { } file
? new BclStorageFile(new FileInfo(file))
: null;
});
}
public override async Task<SaveFilePickerResult> SaveFilePickerWithResultAsync(FilePickerSaveOptions options)
{
return await await RunOnGlibThread(async () =>
{
var (files, selectedFilter) = await ShowDialog(options.Title, _window, GtkFileChooserAction.Save,
false, options.SuggestedStartLocation, options.SuggestedFileName, options.FileTypeChoices,
options.DefaultExtension, options.ShowOverwritePrompt ?? false)
.ConfigureAwait(false);
var file = files?.FirstOrDefault() is { } path
? new BclStorageFile(new FileInfo(path))
: null;
return new SaveFilePickerResult(file) { SelectedFileType = selectedFilter };
});
}
private unsafe Task<(string[]? files, FilePickerFileType? selectedFilter)> ShowDialog(string? title,
IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, IStorageFolder? initialFolder, string? initialFileName,
IEnumerable<FilePickerFileType>? filters, string? defaultExtension, bool overwritePrompt)
{
IntPtr dlg;
using (var name = new Utf8Buffer(title))
{
dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero);
}
UpdateParent(dlg, parent);
if (multiSelect)
{
gtk_file_chooser_set_select_multiple(dlg, true);
}
gtk_window_set_modal(dlg, true);
gtk_file_chooser_set_local_only(dlg, false);
var tcs = new TaskCompletionSource<(string[]?, FilePickerFileType?)>();
List<IDisposable>? disposables = null;
void Dispose()
{
if (disposables is null)
{
return;
}
foreach (var d in disposables)
{
d.Dispose();
}
disposables.Clear();
}
var filtersDic = new Dictionary<IntPtr, FilePickerFileType>();
FilePickerFileType? selectedFilter = null;
if (filters != null)
{
foreach (var f in filters)
{
if (f.Patterns?.Any() == true || f.MimeTypes?.Any() == true)
{
var filter = gtk_file_filter_new();
filtersDic[filter] = f;
using (var b = new Utf8Buffer(f.Name))
{
gtk_file_filter_set_name(filter, b);
}
if (f.Patterns is not null)
{
foreach (var e in f.Patterns)
{
using (var b = new Utf8Buffer(e))
{
gtk_file_filter_add_pattern(filter, b);
}
}
}
if (f.MimeTypes is not null)
{
foreach (var e in f.MimeTypes)
{
using (var b = new Utf8Buffer(e))
{
gtk_file_filter_add_mime_type(filter, b);
}
}
}
gtk_file_chooser_add_filter(dlg, filter);
}
}
}
disposables = new List<IDisposable>
{
ConnectSignal<signal_generic>(dlg, "close", delegate
{
tcs.TrySetResult((null, null));
Dispose();
return false;
}),
ConnectSignal<signal_dialog_response>(dlg, "response", (_, resp, __) =>
{
string[]? result = null;
if (resp == GtkResponseType.Accept)
{
var resultList = new List<string>();
var gs = gtk_file_chooser_get_filenames(dlg);
var cgs = gs;
while (cgs != null)
{
if (cgs->Data != IntPtr.Zero
&& Utf8Buffer.StringFromPtr(cgs->Data) is string str) { resultList.Add(str); } cgs = cgs->Next;
}
g_slist_free(gs);
result = resultList.ToArray();
// GTK doesn't auto-append the extension, so we need to do that manually
if (action == GtkFileChooserAction.Save)
{
var currentFilter = gtk_file_chooser_get_filter(dlg);
filtersDic.TryGetValue(currentFilter, out selectedFilter);
for (var c = 0; c < result.Length; c++)
{
result[c] = StorageProviderHelpers.NameWithExtension(result[c], defaultExtension,
selectedFilter);
}
}
}
gtk_widget_hide(dlg);
Dispose();
tcs.TrySetResult((result, selectedFilter));
return false;
})
};
using (var open = new Utf8Buffer(
action == GtkFileChooserAction.Save ? "Save"
: action == GtkFileChooserAction.SelectFolder ? "Select"
: "Open"))
{
gtk_dialog_add_button(dlg, open, GtkResponseType.Accept);
}
using (var open = new Utf8Buffer("Cancel"))
{
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
}
var folderLocalPath = initialFolder?.TryGetLocalPath();
if (folderLocalPath is not null)
{
using var dir = new Utf8Buffer(folderLocalPath);
gtk_file_chooser_set_current_folder(dlg, dir);
}
if (initialFileName != null)
{
// gtk_file_chooser_set_filename() expects full path
using var fn = action == GtkFileChooserAction.Open
? new Utf8Buffer(Path.Combine(folderLocalPath ?? "", initialFileName))
: new Utf8Buffer(initialFileName);
if (action == GtkFileChooserAction.Save)
{
gtk_file_chooser_set_current_name(dlg, fn);
}
else
{
gtk_file_chooser_set_filename(dlg, fn);
}
}
gtk_file_chooser_set_do_overwrite_confirmation(dlg, overwritePrompt);
gtk_window_present(dlg);
return tcs.Task;
}
private static void UpdateParent(IntPtr chooser, IWindowImpl parentWindow)
{
if (parentWindow.Handle is not { } handle)
return;
var xid = handle.Handle;
gtk_widget_realize(chooser);
var window = gtk_widget_get_window(chooser);
var parent = GetForeignWindow(xid);
if (window != IntPtr.Zero && parent != IntPtr.Zero)
{
gdk_window_set_transient_for(window, parent);
}
}
}
}