Browse Source

Update headless implementations, managed and samples

pull/8303/head
Max Katz 4 years ago
parent
commit
e717cce7e8
  1. 2
      samples/ControlCatalog/ControlCatalog.csproj
  2. 3
      samples/ControlCatalog/MainView.xaml
  3. 5
      samples/ControlCatalog/MainView.xaml.cs
  4. 76
      samples/ControlCatalog/Pages/DialogsPage.xaml
  5. 234
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  6. 6
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  7. 13
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  8. 6
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  9. 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  10. 32
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  11. 4
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  12. 35
      src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs
  13. 97
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  14. 142
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  15. 147
      src/Avalonia.Dialogs/ManagedStorageProvider.cs
  16. 1
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  17. 36
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  18. 6
      src/Avalonia.Headless/HeadlessWindowImpl.cs

2
samples/ControlCatalog/ControlCatalog.csproj

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

3
samples/ControlCatalog/MainView.xaml

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

5
samples/ControlCatalog/MainView.xaml.cs

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

76
samples/ControlCatalog/Pages/DialogsPage.xaml

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

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

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

6
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

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

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

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

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

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

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

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

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

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

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

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

35
src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs

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

97
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

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

142
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

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

147
src/Avalonia.Dialogs/ManagedStorageProvider.cs

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

1
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

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

36
src/Avalonia.Headless/HeadlessPlatformStubs.cs

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

6
src/Avalonia.Headless/HeadlessWindowImpl.cs

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

Loading…
Cancel
Save