Browse Source

Window-less Managed File Dialog (#13683)

* Update Avalonia.Controls.Platform.Dialogs apis

* Enable nullable on dialogs project

* Implement ManagedFileChooserOverwritePrompt with new styles, instead of hardcoding prompt creation

* Add BclMountedVolumeInfoProvider fallback

* Update samples page to include ShowOverwritePrompt

* ManagedStorageProvider with different window and popup implementations

* Extend ManagedFileDialogOptions

* Fix #8437 because I can

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/13688/head
Max Katz 2 years ago
committed by GitHub
parent
commit
1a798ff44c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  2. 0
      src/Avalonia.Controls/Platform/Dialogs/IMountedVolumeInfoProvider.cs
  3. 3
      src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs
  4. 0
      src/Avalonia.Controls/Platform/Dialogs/MountedDriveInfo.cs
  5. 2
      src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
  6. 2
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  7. 6
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  8. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  9. 7
      src/Avalonia.Dialogs/Internal/AvaloniaDialogsInternalViewModelBase.cs
  10. 40
      src/Avalonia.Dialogs/Internal/BclMountedVolumeInfoProvider.cs
  11. 2
      src/Avalonia.Dialogs/Internal/ChildFitter.cs
  12. 4
      src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs
  13. 2
      src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs
  14. 12
      src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs
  15. 4
      src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs
  16. 4
      src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs
  17. 46
      src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs
  18. 6
      src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs
  19. 8
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  20. 31
      src/Avalonia.Dialogs/ManagedFileChooserOverwritePrompt.cs
  21. 64
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  22. 17
      src/Avalonia.Dialogs/ManagedFileDialogOptions.cs
  23. 224
      src/Avalonia.Dialogs/ManagedStorageProvider.cs
  24. 278
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  25. 378
      src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml
  26. 1
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs

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

@ -238,7 +238,7 @@ namespace ControlCatalog.Pages
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
DefaultExtension = fileTypes?.Any() == true ? "txt" : null,
ShowOverwritePrompt = false
ShowOverwritePrompt = true
});
if (file is not null)
@ -436,7 +436,7 @@ CanPickFolder: {storageProvider.CanPickFolder}";
{
var forceManaged = this.Get<CheckBox>("ForceManaged").IsChecked ?? false;
return forceManaged
? new ManagedStorageProvider<Window>(GetWindow(), null)
? new ManagedStorageProvider(GetWindow())
: GetTopLevel().StorageProvider;
}

0
src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs → src/Avalonia.Controls/Platform/Dialogs/IMountedVolumeInfoProvider.cs

3
src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs

@ -1,4 +1,4 @@
#nullable enable
using Avalonia.Metadata;
using Avalonia.Platform.Storage;
namespace Avalonia.Controls.Platform;
@ -6,6 +6,7 @@ namespace Avalonia.Controls.Platform;
/// <summary>
/// Factory allows to register custom storage provider instead of native implementation.
/// </summary>
[Unstable]
public interface IStorageProviderFactory
{
IStorageProvider CreateProvider(TopLevel topLevel);

0
src/Avalonia.Controls/Platform/MountedDriveInfo.cs → src/Avalonia.Controls/Platform/Dialogs/MountedDriveInfo.cs

2
src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs

@ -4,8 +4,6 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
#nullable enable
namespace Avalonia.Controls.Platform
{
/// <summary>

2
src/Avalonia.Controls/Primitives/OverlayLayer.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives
if(v is VisualLayerManager vlm)
if (vlm.OverlayLayer != null)
return vlm.OverlayLayer;
if (visual is TopLevel tl)
if (TopLevel.GetTopLevel(visual) is {} tl)
{
var layers = tl.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault();
return layers?.OverlayLayer;

6
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@ -10,7 +10,7 @@ namespace Avalonia.Dialogs
{
public class AboutAvaloniaDialog : Window
{
private static readonly Version s_version = typeof(AboutAvaloniaDialog).Assembly.GetName().Version;
private static readonly Version s_version = typeof(AboutAvaloniaDialog).Assembly.GetName().Version!;
public static string Version { get; } = $@"v{s_version.ToString(2)}";
@ -45,7 +45,7 @@ namespace Avalonia.Dialogs
{
if (waitForExit)
{
process.WaitForExit();
process?.WaitForExit();
}
}
}
@ -61,7 +61,7 @@ namespace Avalonia.Dialogs
}
else
{
using Process process = Process.Start(new ProcessStartInfo
using Process? process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -21,4 +21,5 @@
<Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project>

7
src/Avalonia.Dialogs/Internal/AvaloniaDialogsInternalViewModelBase.cs

@ -1,14 +1,15 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
namespace Avalonia.Dialogs.Internal
{
public class AvaloniaDialogsInternalViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
internal protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
internal protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
@ -20,7 +21,7 @@ namespace Avalonia.Dialogs.Internal
return false;
}
internal protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
internal protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

40
src/Avalonia.Dialogs/Internal/BclMountedVolumeInfoProvider.cs

@ -0,0 +1,40 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using Avalonia.Controls.Platform;
using Avalonia.Reactive;
namespace Avalonia.Dialogs.Internal;
internal class BclMountedVolumeInfoProvider : IMountedVolumeInfoProvider
{
public IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives)
{
foreach (var drive in DriveInfo.GetDrives())
{
string directory;
ulong totalSize;
try
{
if (!drive.IsReady)
continue;
totalSize = (ulong)drive.TotalSize;
directory = drive.RootDirectory.FullName;
_ = new DirectoryInfo(directory).EnumerateFileSystemInfos();
}
catch
{
continue;
}
mountedDrives.Add(new MountedVolumeInfo
{
VolumeLabel = string.IsNullOrEmpty(drive.VolumeLabel.Trim()) ? directory : drive.VolumeLabel,
VolumePath = directory,
VolumeSizeBytes = totalSize
});
}
return Disposable.Empty;
}
}

2
src/Avalonia.Dialogs/Internal/ChildFitter.cs

@ -11,7 +11,7 @@ namespace Avalonia.Dialogs.Internal
protected override Size ArrangeOverride(Size finalSize)
{
Child.Measure(finalSize);
Child?.Measure(finalSize);
base.ArrangeOverride(finalSize);
return finalSize;
}

4
src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs

@ -6,7 +6,7 @@ namespace Avalonia.Dialogs.Internal
{
public class FileSizeStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is long size && size > 0)
{
@ -16,7 +16,7 @@ namespace Avalonia.Dialogs.Internal
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

2
src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs

@ -6,7 +6,7 @@ namespace Avalonia.Dialogs.Internal
{
public class ManagedFileChooserFilterViewModel : AvaloniaDialogsInternalViewModelBase
{
private readonly Regex[] _patterns;
private readonly Regex[]? _patterns;
public string Name { get; }
public ManagedFileChooserFilterViewModel(FilePickerFileType filter)

12
src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs

@ -4,20 +4,20 @@ namespace Avalonia.Dialogs.Internal
{
public class ManagedFileChooserItemViewModel : AvaloniaDialogsInternalViewModelBase
{
private string _displayName;
private string _path;
private string? _displayName;
private string? _path;
private DateTime _modified;
private string _type;
private string? _type;
private long _size;
private ManagedFileChooserItemType _itemType;
public string DisplayName
public string? DisplayName
{
get => _displayName;
set => this.RaiseAndSetIfChanged(ref _displayName, value);
}
public string Path
public string? Path
{
get => _path;
set => this.RaiseAndSetIfChanged(ref _path, value);
@ -29,7 +29,7 @@ namespace Avalonia.Dialogs.Internal
set => this.RaiseAndSetIfChanged(ref _modified, value);
}
public string Type
public string? Type
{
get => _type;
set => this.RaiseAndSetIfChanged(ref _type, value);

4
src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs

@ -2,8 +2,8 @@
{
public class ManagedFileChooserNavigationItem
{
public string DisplayName { get; set; }
public string Path { get; set; }
public string? DisplayName { get; set; }
public string? Path { get; set; }
public ManagedFileChooserItemType ItemType { get; set; }
}
}

4
src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs

@ -64,7 +64,7 @@ namespace Avalonia.Dialogs.Internal
try
{
Directory.GetFiles(x.VolumePath);
Directory.GetFiles(x.VolumePath!);
}
catch (Exception)
{
@ -79,7 +79,7 @@ namespace Avalonia.Dialogs.Internal
};
})
.Where(x => x != null)
.ToArray();
.ToArray()!;
}
}
}

46
src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs

@ -15,9 +15,9 @@ namespace Avalonia.Dialogs.Internal
public class ManagedFileChooserViewModel : AvaloniaDialogsInternalViewModelBase
{
private readonly ManagedFileDialogOptions _options;
public event Action CancelRequested;
public event Action<string[]> CompleteRequested;
public event Action<string> OverwritePrompt;
public event Action? CancelRequested;
public event Action<string[]>? CompleteRequested;
public event Action<string>? OverwritePrompt;
public AvaloniaList<ManagedFileChooserItemViewModel> QuickLinks { get; } =
new AvaloniaList<ManagedFileChooserItemViewModel>();
@ -31,25 +31,25 @@ namespace Avalonia.Dialogs.Internal
public AvaloniaList<ManagedFileChooserItemViewModel> SelectedItems { get; } =
new AvaloniaList<ManagedFileChooserItemViewModel>();
string _location;
string _fileName;
string? _location;
string? _fileName;
private bool _showHiddenFiles;
private ManagedFileChooserFilterViewModel _selectedFilter;
private ManagedFileChooserFilterViewModel? _selectedFilter;
private readonly bool _selectingDirectory;
private readonly bool _savingFile;
private bool _scheduledSelectionValidation;
private bool _alreadyCancelled = false;
private string _defaultExtension;
private string? _defaultExtension;
private readonly bool _overwritePrompt;
private CompositeDisposable _disposables;
public string Location
public string? Location
{
get => _location;
set => this.RaiseAndSetIfChanged(ref _location, value);
}
public string FileName
public string? FileName
{
get => _fileName;
set => this.RaiseAndSetIfChanged(ref _fileName, value);
@ -59,7 +59,7 @@ namespace Avalonia.Dialogs.Internal
public bool ShowFilters { get; }
public SelectionMode SelectionMode { get; }
public string Title { get; }
public string? Title { get; }
public int QuickLinksSelectedIndex
{
@ -80,7 +80,7 @@ namespace Avalonia.Dialogs.Internal
set => this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex));
}
public ManagedFileChooserFilterViewModel SelectedFilter
public ManagedFileChooserFilterViewModel? SelectedFilter
{
get => _selectedFilter;
set
@ -115,9 +115,9 @@ namespace Avalonia.Dialogs.Internal
.GetService<ManagedFileChooserSources>()
?? new ManagedFileChooserSources();
var sub1 = AvaloniaLocator.Current
.GetRequiredService<IMountedVolumeInfoProvider>()
.Listen(ManagedFileChooserSources.MountedVolumes);
var sub1 = (AvaloniaLocator.Current.GetService<IMountedVolumeInfoProvider>()
?? new BclMountedVolumeInfoProvider())
.Listen(ManagedFileChooserSources.MountedVolumes);
var sub2 = ManagedFileChooserSources.MountedVolumes.GetWeakCollectionChangedObservable()
.Subscribe(x => Dispatcher.UIThread.Post(() => RefreshQuickLinks(quickSources)));
@ -200,7 +200,7 @@ namespace Avalonia.Dialogs.Internal
}
}
private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e)
private async void OnSelectionChangedAsync(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_scheduledSelectionValidation)
{
@ -244,11 +244,11 @@ namespace Avalonia.Dialogs.Internal
});
}
void NavigateRoot(string initialSelectionName)
void NavigateRoot(string? initialSelectionName)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName);
Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System))!, initialSelectionName);
}
else
{
@ -258,14 +258,14 @@ namespace Avalonia.Dialogs.Internal
public void Refresh() => Navigate(Location);
public void Navigate(IStorageFolder path, string initialSelectionName = null)
public void Navigate(IStorageFolder? path, string? initialSelectionName = null)
{
var fullDirectoryPath = path?.TryGetLocalPath() ?? Directory.GetCurrentDirectory();
Navigate(fullDirectoryPath, initialSelectionName);
}
public void Navigate(string path, string initialSelectionName = null)
public void Navigate(string? path, string? initialSelectionName = null)
{
if (!Directory.Exists(path))
{
@ -370,7 +370,7 @@ namespace Avalonia.Dialogs.Internal
{
if (_selectingDirectory)
{
CompleteRequested?.Invoke(new[] { Location });
CompleteRequested?.Invoke(new[] { Location! });
}
else if (_savingFile)
{
@ -381,7 +381,7 @@ namespace Avalonia.Dialogs.Internal
FileName = Path.ChangeExtension(FileName, _defaultExtension);
}
var fullName = Path.Combine(Location, FileName);
var fullName = Path.Combine(Location!, FileName);
if (_overwritePrompt && File.Exists(fullName))
{
@ -395,13 +395,13 @@ namespace Avalonia.Dialogs.Internal
}
else
{
CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray());
CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path!).ToArray());
}
}
public void SelectSingleFile(ManagedFileChooserItemViewModel item)
{
CompleteRequested?.Invoke(new[] { item.Path });
CompleteRequested?.Invoke(new[] { item.Path! });
}
}
}

6
src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs

@ -7,13 +7,13 @@ namespace Avalonia.Dialogs.Internal
{
public class ResourceSelectorConverter : ResourceDictionary, IValueConverter
{
public object Convert(object key, Type targetType, object parameter, CultureInfo culture)
public object? Convert(object? key, Type targetType, object? parameter, CultureInfo culture)
{
TryGetResource((string)key, null, out var value);
TryGetResource((string)key!, null, out var value);
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

8
src/Avalonia.Dialogs/ManagedFileChooser.cs

@ -15,17 +15,17 @@ namespace Avalonia.Dialogs
[TemplatePart("PART_Files", typeof(ListBox))]
public class ManagedFileChooser : TemplatedControl
{
private Control _quickLinksRoot;
private ListBox _filesView;
private Control? _quickLinksRoot;
private ListBox? _filesView;
public ManagedFileChooser()
{
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
}
ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel;
ManagedFileChooserViewModel? Model => DataContext as ManagedFileChooserViewModel;
private void OnPointerPressed(object sender, PointerPressedEventArgs e)
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel;

31
src/Avalonia.Dialogs/ManagedFileChooserOverwritePrompt.cs

@ -0,0 +1,31 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
namespace Avalonia.Dialogs;
public class ManagedFileChooserOverwritePrompt : TemplatedControl
{
internal event Action<bool>? Result;
private string _fileName = "";
public static readonly DirectProperty<ManagedFileChooserOverwritePrompt, string> FileNameProperty = AvaloniaProperty.RegisterDirect<ManagedFileChooserOverwritePrompt, string>(
"FileName", o => o.FileName, (o, v) => o.FileName = v);
public string FileName
{
get => _fileName;
set => SetAndRaise(FileNameProperty, ref _fileName, value);
}
public void Confirm()
{
Result?.Invoke(true);
}
public void Cancel()
{
Result?.Invoke(false);
}
}

64
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -1,43 +1,44 @@
#nullable enable
using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Platform.Storage;
namespace Avalonia.Dialogs
{
#if NET6_0_OR_GREATER
[SupportedOSPlatform("windows"), SupportedOSPlatform("macos"), SupportedOSPlatform("linux")]
#endif
public static class ManagedFileDialogExtensions
{
internal class ManagedStorageProviderFactory<T> : IStorageProviderFactory where T : Window, new()
internal class ManagedStorageProviderFactory : IStorageProviderFactory
{
private readonly ManagedFileDialogOptions? _options;
public ManagedStorageProviderFactory(ManagedFileDialogOptions? options)
{
_options = options;
}
public IStorageProvider CreateProvider(TopLevel topLevel)
{
if (topLevel is Window window)
{
var options = AvaloniaLocator.Current.GetService<ManagedFileDialogOptions>();
return new ManagedStorageProvider<T>(window, options);
}
throw new InvalidOperationException("Current platform doesn't support managed picker dialogs");
return new ManagedStorageProvider(topLevel, _options);
}
}
public static AppBuilder UseManagedSystemDialogs(this AppBuilder builder)
{
builder.AfterSetup(_ =>
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<Window>>());
return builder;
return builder.UseManagedSystemDialogs(null);
}
public static AppBuilder UseManagedSystemDialogs<TWindow>(this AppBuilder builder)
where TWindow : Window, new()
{
builder.AfterSetup(_ =>
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<TWindow>>());
return builder;
return builder.UseManagedSystemDialogs(() => new TWindow());
}
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)]
@ -48,12 +49,41 @@ namespace Avalonia.Dialogs
public static async Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions? options = null) where TWindow : Window, new()
{
var impl = new ManagedStorageProvider<TWindow>(parent, options);
var impl = new ManagedStorageProvider(parent, PrepareOptions(options, () => new TWindow()));
var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions());
return files
.Select(file => file.TryGetLocalPath() ?? file.Name)
.ToArray();
}
private static ManagedFileDialogOptions? PrepareOptions(
ManagedFileDialogOptions? optionsOverride = null,
Func<ContentControl>? customRootFactory = null)
{
var options = optionsOverride ?? AvaloniaLocator.Current.GetService<ManagedFileDialogOptions>();
if (options is not null && customRootFactory is not null)
{
options = options with { ContentRootFactory = customRootFactory };
}
return options;
}
private static AppBuilder UseManagedSystemDialogs(this AppBuilder builder, Func<ContentControl>? customFactory)
{
builder.AfterSetup(_ =>
{
var options = PrepareOptions(null, customFactory);
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>()
.ToConstant(new ManagedStorageProviderFactory(options));
if (options?.CustomVolumeInfoProvider is not null)
{
AvaloniaLocator.CurrentMutable.Bind<IMountedVolumeInfoProvider>()
.ToConstant(options.CustomVolumeInfoProvider);
}
});
return builder;
}
}
}

17
src/Avalonia.Dialogs/ManagedFileDialogOptions.cs

@ -1,7 +1,22 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
namespace Avalonia.Dialogs
{
public class ManagedFileDialogOptions
public record ManagedFileDialogOptions
{
public bool AllowDirectorySelection { get; set; }
/// <summary>
/// Allows to redefine how root volumes are populated in the dialog.
/// </summary>
public IMountedVolumeInfoProvider? CustomVolumeInfoProvider { get; set; }
/// <summary>
/// Allows to redefine content root.
/// Can be a custom Window or any ContentControl (Popup hosted).
/// </summary>
public Func<ContentControl>? ContentRootFactory { get; set; }
}
}

224
src/Avalonia.Dialogs/ManagedStorageProvider.cs

@ -1,22 +1,24 @@
#nullable enable
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Dialogs.Internal;
using Avalonia.Layout;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.VisualTree;
namespace Avalonia.Dialogs;
internal class ManagedStorageProvider<T> : BclStorageProvider where T : Window, new()
internal class ManagedStorageProvider : BclStorageProvider
{
private readonly Window _parent;
private readonly TopLevel? _parent;
private readonly ManagedFileDialogOptions _managedOptions;
public ManagedStorageProvider(Window parent, ManagedFileDialogOptions? managedOptions)
public ManagedStorageProvider(TopLevel? parent, ManagedFileDialogOptions? managedOptions = null)
{
_parent = parent;
_managedOptions = managedOptions ?? new ManagedFileDialogOptions();
@ -29,7 +31,7 @@ internal class ManagedStorageProvider<T> : BclStorageProvider where T : Window,
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await ManagedStorageProvider<T>.Show(model, _parent);
var results = await Show(model);
return results.Select(f => new BclStorageFile(new FileInfo(f))).ToArray();
}
@ -37,7 +39,7 @@ internal class ManagedStorageProvider<T> : BclStorageProvider where T : Window,
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await ManagedStorageProvider<T>.Show(model, _parent);
var results = await Show(model);
return results.FirstOrDefault() is { } result
? new BclStorageFile(new FileInfo(result))
@ -47,102 +49,176 @@ internal class ManagedStorageProvider<T> : BclStorageProvider where T : Window,
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await ManagedStorageProvider<T>.Show(model, _parent);
var results = await Show(model);
return results.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray();
}
private static async Task<string[]> Show(ManagedFileChooserViewModel model, Window parent)
private ContentControl PrepareRoot(ManagedFileChooserViewModel model)
{
var dialog = new T
var root = _managedOptions.ContentRootFactory?.Invoke();
if (root is null)
{
Content = new ManagedFileChooser(),
Title = model.Title,
DataContext = model
};
if (_parent is not null and not Window)
{
root = new ContentControl();
}
else
{
root = new Window();
}
}
dialog.Closed += delegate { model.Cancel(); };
root.Content = new ManagedFileChooser();
root.DataContext = model;
string[]? result = null;
return root;
}
private Task<string[]> Show(ManagedFileChooserViewModel model)
{
var root = PrepareRoot(model);
if (root is Window window)
{
return ShowAsWindow(window, model);
}
else if (_parent is not null)
{
return ShowAsPopup(root, model);
}
else
{
throw new InvalidOperationException(
"Managed File Chooser requires existing parent or compatible windowing system.");
}
}
private async Task<string[]> ShowAsWindow(Window window, ManagedFileChooserViewModel model)
{
var tcs = new TaskCompletionSource<bool>();
window.Title = model.Title;
window.Closed += delegate {
model.Cancel();
tcs.TrySetResult(true);
};
var result = Array.Empty<string>();
model.CompleteRequested += items =>
{
result = items;
dialog.Close();
window.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()
if (await ShowOverwritePrompt(filename, window))
{
HorizontalAlignment = Layout.HorizontalAlignment.Stretch
};
window.Close();
}
};
var label = new Label()
{
Content = $"{name} already exists.\nDo you want to replace it?"
};
model.CancelRequested += window.Close;
panel.Children.Add(label);
DockPanel.SetDock(label, Dock.Top);
if (_parent is Window parent)
{
await window.ShowDialog<object>(parent);
}
else
{
window.Show();
}
var buttonPanel = new StackPanel()
{
HorizontalAlignment = Layout.HorizontalAlignment.Right,
Orientation = Layout.Orientation.Horizontal,
Spacing = 10
};
await tcs.Task;
var button = new Button()
{
Content = "Yes",
HorizontalAlignment = Layout.HorizontalAlignment.Right
};
return result;
}
private async Task<string[]> ShowAsPopup(ContentControl root, ManagedFileChooserViewModel model)
{
var tcs = new TaskCompletionSource<bool>();
var rootPanel = _parent.FindDescendantOfType<Panel>()!;
var popup = new Popup();
popup.Placement = PlacementMode.Center;
popup.IsLightDismissEnabled = false;
popup.Child = root;
popup.Width = _parent!.Width;
popup.Height = _parent.Height;
popup.Closed += delegate {
model.Cancel();
tcs.TrySetResult(true);
};
var result = Array.Empty<string>();
model.CompleteRequested += items =>
{
result = items;
popup.Close();
};
button.Click += (sender, args) =>
model.OverwritePrompt += async (filename) =>
{
if (await ShowOverwritePrompt(filename, root))
{
result = new string[1] { filename };
overwritePromptDialog.Close();
dialog.Close();
};
popup.Close();
}
};
buttonPanel.Children.Add(button);
model.CancelRequested += delegate
{
popup.Close();
};
button = new Button()
{
Content = "No",
HorizontalAlignment = Layout.HorizontalAlignment.Right
};
rootPanel.Children.Add(popup);
_parent.SizeChanged += ParentOnSizeChanged;
try
{
popup.Open();
await tcs.Task;
}
finally
{
rootPanel.Children.Remove(popup);
_parent.SizeChanged -= ParentOnSizeChanged;
}
button.Click += (sender, args) =>
return result;
void ParentOnSizeChanged(object? sender, SizeChangedEventArgs e)
{
if (!popup.IsOpen)
{
overwritePromptDialog.Close();
};
buttonPanel.Children.Add(button);
panel.Children.Add(buttonPanel);
DockPanel.SetDock(buttonPanel, Dock.Bottom);
overwritePromptDialog.Content = panel;
_parent.SizeChanged -= ParentOnSizeChanged;
}
popup.Width = _parent!.Width;
popup.Height = _parent.Height;
}
}
await overwritePromptDialog.ShowDialog(dialog);
private static async Task<bool> ShowOverwritePrompt(string filename, ContentControl root)
{
var tcs = new TaskCompletionSource<bool>();
var prompt = new ManagedFileChooserOverwritePrompt
{
FileName = Path.GetFileName(filename)
};
prompt.Result += (r) => tcs.TrySetResult(r);
var flyout = new Flyout();
flyout.Closed += (_, _) => tcs.TrySetResult(false);
flyout.Content = prompt;
flyout.Placement = PlacementMode.Center;
flyout.ShowAt(root);
model.CancelRequested += dialog.Close;
var promptResult = await tcs.Task;
flyout.Hide();
await dialog.ShowDialog<object>(parent);
return result ?? Array.Empty<string>();
return promptResult;
}
}

278
src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml

@ -133,137 +133,145 @@
</internal:ResourceSelectorConverter>
</ResourceDictionary>
</ControlTheme.Resources>
<Setter Property="Background" Value="{DynamicResource SystemRegionBrush}"/>
<Setter Property="Template"
x:DataType="internal:ManagedFileChooserViewModel">
<ControlTemplate>
<DockPanel>
<ListBox x:Name="PART_QuickLinks" DockPanel.Dock="Left" ItemsSource="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
<Image Width="16" Height="16">
<Image.Source>
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
</Image.Source>
</Image>
<TextBlock Text="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DockPanel x:Name="NavBar" DockPanel.Dock="Top" Margin="8,5,8,0" VerticalAlignment="Center">
<Rectangle Fill="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}" Height="1" Margin="0,5,0,0" DockPanel.Dock="Bottom"/>
<DockPanel Margin="4,0">
<Button Command="{Binding GoUp}" DockPanel.Dock="Left" Margin="0,0,8,0">
<Path Data="M 0 7 L 7 0 L 14 7 M 7 0 L 7 16" Stroke="{CompiledBinding $parent[Button].Foreground}" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,1,0,-1"/>
</Button>
<Button Command="{Binding Refresh}" DockPanel.Dock="Right" Margin="8,0,0,0">
<Path Data="M18.62 3.32c.39 0 .7.29.76.66v3c0 .39-.28.7-.66.76h-3a.77.77 0 0 1-.1-1.52h1.08a7.42 7.42 0 1 0 2.65 4.37.77.77 0 1 1 1.5-.3 8.94 8.94 0 1 1-3-5.12V4.09c0-.43.35-.77.77-.77Z"
Fill="{CompiledBinding $parent[Button].Foreground}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="-2,-4,0,0"/>
</Button>
<TextBox x:Name="Location" Text="{Binding Location}">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding EnterPressed}" Gesture="Enter"/>
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
</DockPanel>
<DockPanel Margin="8,0,8,5" DockPanel.Dock="Bottom">
<Rectangle Fill="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}" Height="1" Margin="0,0,0,5" DockPanel.Dock="Top"/>
<DockPanel Margin="4,0">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,4">
<ComboBox DockPanel.Dock="Right"
IsVisible="{Binding ShowFilters}"
ItemsSource="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}" />
<TextBox Text="{Binding FileName}" Watermark="File name" IsVisible="{Binding !SelectingFolder}" />
</DockPanel>
<CheckBox IsChecked="{Binding ShowHiddenFiles}" Content="Show hidden files" DockPanel.Dock="Left"/>
<UniformGrid x:Name="Finalize" HorizontalAlignment="Right" Rows="1">
<Button Command="{Binding Ok}" MinWidth="80">OK</Button>
<Button Command="{Binding Cancel}" MinWidth="80">Cancel</Button>
</UniformGrid>
</DockPanel>
</DockPanel>
<DockPanel Grid.IsSharedSizeScope="True">
<Grid DockPanel.Dock="Top" Margin="15 5 0 0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" />
<ColumnDefinition Width="275" SharedSizeGroup="Name" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Modified" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="150" SharedSizeGroup="Type" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Size" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<Grid.Styles>
<Style Selector="GridSplitter">
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}"/>
<Setter Property="Template">
<ControlTemplate>
<Border VerticalAlignment="Stretch" BorderThickness="0" Background="#01000000">
<Rectangle Width="1" VerticalAlignment="Stretch" Fill="{TemplateBinding Background}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Grid.Styles>
<TextBlock Grid.Column="1" Text="Name" />
<GridSplitter Grid.Column="2" />
<TextBlock Grid.Column="3" Text="Date Modified" />
<GridSplitter Grid.Column="4" />
<TextBlock Grid.Column="5" Text="Type" />
<GridSplitter Grid.Column="6" />
<TextBlock Grid.Column="7" Text="Size" />
<GridSplitter Grid.Column="8" />
</Grid>
<ListBox x:Name="PART_Files"
ItemsSource="{Binding Items}"
Margin="0 5"
SelectionMode="{Binding SelectionMode}"
SelectedItems="{Binding SelectedItems}"
Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<DockPanel>
<ListBox x:Name="PART_QuickLinks" DockPanel.Dock="Left" ItemsSource="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False"
MaxWidth="200">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="internal:ManagedFileChooserItemViewModel">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Icon" />
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Modified" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Size" />
<ColumnDefinition SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<DataTemplate>
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent">
<Image Width="16" Height="16">
<Image.Source>
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
</Image.Source>
</Image>
<TextBlock Grid.Column="1" Text="{Binding DisplayName}"/>
<TextBlock Grid.Column="3" Text="{Binding Modified}" />
<TextBlock Grid.Column="5" Text="{Binding Type}" />
<TextBlock Grid.Column="7" HorizontalAlignment="Right">
<TextBlock.Text>
<Binding Path="Size">
<Binding.Converter>
<internal:FileSizeStringConverter/>
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
<TextBlock Text="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DockPanel x:Name="NavBar" DockPanel.Dock="Top" Margin="8,5,8,0" VerticalAlignment="Center">
<Rectangle Fill="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}" Height="1" Margin="0,5,0,0" DockPanel.Dock="Bottom"/>
<DockPanel Margin="4,0">
<Button Command="{Binding GoUp}" DockPanel.Dock="Left" Margin="0,0,8,0">
<Path Data="M 0 7 L 7 0 L 14 7 M 7 0 L 7 16" Stroke="{CompiledBinding $parent[Button].Foreground}" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,1,0,-1"/>
</Button>
<Button Command="{Binding Refresh}" DockPanel.Dock="Right" Margin="8,0,0,0">
<Path Data="M18.62 3.32c.39 0 .7.29.76.66v3c0 .39-.28.7-.66.76h-3a.77.77 0 0 1-.1-1.52h1.08a7.42 7.42 0 1 0 2.65 4.37.77.77 0 1 1 1.5-.3 8.94 8.94 0 1 1-3-5.12V4.09c0-.43.35-.77.77-.77Z"
Fill="{CompiledBinding $parent[Button].Foreground}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="-2,-4,0,0"/>
</Button>
<TextBox x:Name="Location" Text="{Binding Location}">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding EnterPressed}" Gesture="Enter"/>
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
</DockPanel>
<DockPanel Margin="8,0,8,5" DockPanel.Dock="Bottom">
<Rectangle Fill="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}" Height="1" Margin="0,0,0,5" DockPanel.Dock="Top"/>
<DockPanel Margin="4,0">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,4">
<ComboBox DockPanel.Dock="Right"
IsVisible="{Binding ShowFilters}"
ItemsSource="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}" />
<TextBox Text="{Binding FileName}" Watermark="File name" IsVisible="{Binding !SelectingFolder}" />
</DockPanel>
<CheckBox IsChecked="{Binding ShowHiddenFiles}" Content="Show hidden files" DockPanel.Dock="Left"/>
<UniformGrid x:Name="Finalize" HorizontalAlignment="Right" Rows="1">
<Button Command="{Binding Ok}" MinWidth="80">OK</Button>
<Button Command="{Binding Cancel}" MinWidth="80">Cancel</Button>
</UniformGrid>
</DockPanel>
</DockPanel>
<DockPanel Grid.IsSharedSizeScope="True">
<Grid DockPanel.Dock="Top" Margin="15 5 0 0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" />
<ColumnDefinition Width="275" SharedSizeGroup="Name" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Modified" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="150" SharedSizeGroup="Type" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Size" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<Grid.Styles>
<Style Selector="GridSplitter">
<Setter Property="Background" Value="{DynamicResource SystemControlHighlightAltBaseMediumLowBrush}"/>
<Setter Property="Template">
<ControlTemplate>
<Border VerticalAlignment="Stretch" BorderThickness="0" Background="#01000000">
<Rectangle Width="1" VerticalAlignment="Stretch" Fill="{TemplateBinding Background}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Grid.Styles>
<TextBlock Grid.Column="1" Text="Name" />
<GridSplitter Grid.Column="2" />
<TextBlock Grid.Column="3" Text="Date Modified" />
<GridSplitter Grid.Column="4" />
<TextBlock Grid.Column="5" Text="Type" />
<GridSplitter Grid.Column="6" />
<TextBlock Grid.Column="7" Text="Size" />
<GridSplitter Grid.Column="8" />
</Grid>
<ListBox x:Name="PART_Files"
ItemsSource="{Binding Items}"
Margin="0 5"
SelectionMode="{Binding SelectionMode}"
SelectedItems="{Binding SelectedItems}"
Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="internal:ManagedFileChooserItemViewModel">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Icon" />
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Modified" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Size" />
<ColumnDefinition SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<Image Width="16" Height="16">
<Image.Source>
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
</Image.Source>
</Image>
<TextBlock Grid.Column="1" Text="{Binding DisplayName}"/>
<TextBlock Grid.Column="3" Text="{Binding Modified}" />
<TextBlock Grid.Column="5" Text="{Binding Type}" />
<TextBlock Grid.Column="7" HorizontalAlignment="Right">
<TextBlock.Text>
<Binding Path="Size">
<Binding.Converter>
<internal:FileSizeStringConverter/>
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</DockPanel>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^ /template/ ListBox#QuickLinks">
@ -335,4 +343,38 @@
<Setter Property="Margin" Value="4,0,0,0"/>
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type dialogs:ManagedFileChooserOverwritePrompt}" TargetType="dialogs:ManagedFileChooserOverwritePrompt">
<Setter Property="MinWidth" Value="270" />
<Setter Property="MaxWidth" Value="400" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<StackPanel Spacing="10">
<TextBlock TextWrapping="Wrap"
Text="{Binding FileName, RelativeSource={RelativeSource TemplatedParent}, StringFormat='{}{0} already exists. Do you want to replace it?'}" />
<StackPanel HorizontalAlignment="Right"
Spacing="10"
Orientation="Horizontal">
<Button Classes="accent" Content="Yes"
MinWidth="80"
HorizontalContentAlignment="Center"
IsDefault="True"
Command="{Binding Confirm, RelativeSource={RelativeSource TemplatedParent}}" />
<Button Content="No"
MinWidth="80"
IsCancel="True"
HorizontalContentAlignment="Center"
Command="{Binding Cancel, RelativeSource={RelativeSource TemplatedParent}}" />
</StackPanel>
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

378
src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml

@ -48,188 +48,230 @@
<ControlTheme x:Key="{x:Type dialogs:ManagedFileChooser}"
TargetType="dialogs:ManagedFileChooser">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="Template">
<ControlTemplate x:DataType="internal:ManagedFileChooserViewModel">
<DockPanel Margin="5">
<DockPanel Margin="0,0,0,5"
DockPanel.Dock="Top">
<internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}"
DockPanel.Dock="Right">
<Button Command="{Binding GoUp}">
<Image Stretch="Fill">
<DrawingImage Drawing="{StaticResource LevelUp}" />
</Image>
</Button>
</internal:ChildFitter>
<internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}"
DockPanel.Dock="Right">
<Button Command="{Binding Refresh}">
<Image Stretch="Fill">
<DrawingImage Drawing="{StaticResource Refresh}" />
</Image>
</Button>
</internal:ChildFitter>
<TextBox x:Name="Location"
Margin="0,0,5,0"
Text="{Binding Location}">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding EnterPressed}"
Gesture="Enter" />
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
<DockPanel Margin="0,5,0,0"
DockPanel.Dock="Bottom">
<StackPanel DockPanel.Dock="Left"
Orientation="Horizontal">
<CheckBox IsChecked="{Binding ShowHiddenFiles}">
<TextBlock>Show hidden files</TextBlock>
</CheckBox>
</StackPanel>
<StackPanel HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="10">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="4" />
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}" MinWidth="60">OK</Button>
<Button Command="{Binding Cancel}" MinWidth="60">Cancel</Button>
</StackPanel>
</DockPanel>
<ComboBox Margin="0,5,0,0"
DockPanel.Dock="Bottom"
IsVisible="{Binding ShowFilters}"
ItemsSource="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}" />
<TextBox DockPanel.Dock="Bottom"
IsVisible="{Binding !SelectingFolder}"
Text="{Binding FileName}"
Watermark="File name" />
<ListBox x:Name="PART_QuickLinks"
Margin="0,0,5,5"
BorderBrush="Transparent"
DockPanel.Dock="Left"
Focusable="False"
ItemsSource="{Binding QuickLinks}"
SelectedIndex="{Binding QuickLinksSelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Background="Transparent"
Orientation="Horizontal"
Spacing="4">
<Image Width="16"
Height="16">
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}" />
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<DockPanel Margin="5">
<DockPanel Margin="0,0,0,5"
DockPanel.Dock="Top">
<internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}"
DockPanel.Dock="Right">
<Button Command="{Binding GoUp}">
<Image Stretch="Fill">
<DrawingImage Drawing="{StaticResource LevelUp}" />
</Image>
<TextBlock Text="{Binding DisplayName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DockPanel Grid.IsSharedSizeScope="True">
<Grid Margin="15,5,0,0"
HorizontalAlignment="Stretch"
DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" />
<ColumnDefinition Width="400" SharedSizeGroup="Name" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Modified" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="150" SharedSizeGroup="Type" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Size" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1"
Text="Name" />
<GridSplitter Grid.Column="2"
ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left" Grid.Column="2" VerticalAlignment="Stretch" Width="1" Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="3"
Text="Date Modified" />
<GridSplitter Grid.Column="4"
ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left"
Grid.Column="4"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource ThemeControlMidBrush}"/>
</Button>
</internal:ChildFitter>
<internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}"
DockPanel.Dock="Right">
<Button Command="{Binding Refresh}">
<Image Stretch="Fill">
<DrawingImage Drawing="{StaticResource Refresh}" />
</Image>
</Button>
</internal:ChildFitter>
<TextBox x:Name="Location"
Margin="0,0,5,0"
Text="{Binding Location}">
<TextBox.KeyBindings>
<KeyBinding Command="{Binding EnterPressed}"
Gesture="Enter" />
</TextBox.KeyBindings>
</TextBox>
</DockPanel>
<DockPanel Margin="0,5,0,0"
DockPanel.Dock="Bottom">
<StackPanel DockPanel.Dock="Left"
Orientation="Horizontal">
<CheckBox IsChecked="{Binding ShowHiddenFiles}">
<TextBlock>Show hidden files</TextBlock>
</CheckBox>
</StackPanel>
<StackPanel HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="10">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin" Value="4" />
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}" MinWidth="60">OK</Button>
<Button Command="{Binding Cancel}" MinWidth="60">Cancel</Button>
</StackPanel>
</DockPanel>
<ComboBox Margin="0,5,0,0"
DockPanel.Dock="Bottom"
IsVisible="{Binding ShowFilters}"
ItemsSource="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}" />
<TextBlock Grid.Column="5"
Text="Type" />
<GridSplitter Grid.Column="6" ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left"
Grid.Column="6"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBox DockPanel.Dock="Bottom"
IsVisible="{Binding !SelectingFolder}"
Text="{Binding FileName}"
Watermark="File name" />
<TextBlock Grid.Column="7"
Text="Size" />
<GridSplitter Grid.Column="8"
ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left"
Grid.Column="8"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource ThemeControlMidBrush}"/>
</Grid>
<ListBox x:Name="PART_Files"
Margin="0,5"
ItemsSource="{Binding Items}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItems="{Binding SelectedItems}"
SelectionMode="{Binding SelectionMode}">
<ListBox x:Name="PART_QuickLinks"
MaxWidth="200"
Margin="0,0,5,5"
BorderBrush="Transparent"
DockPanel.Dock="Left"
Focusable="False"
ItemsSource="{Binding QuickLinks}"
SelectedIndex="{Binding QuickLinksSelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Icon" />
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Modified" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Size" />
<ColumnDefinition SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
Width="16"
<StackPanel Background="Transparent"
Orientation="Horizontal"
Spacing="4">
<Image Width="16"
Height="16">
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}" />
</Image>
<TextBlock Grid.Column="1"
Text="{Binding DisplayName}" />
<TextBlock Grid.Column="3"
Text="{Binding Modified}" />
<TextBlock Grid.Column="5"
Text="{Binding Type}" />
<TextBlock Grid.Column="7" HorizontalAlignment="Right">
<TextBlock.Text>
<Binding Path="Size">
<Binding.Converter>
<internal:FileSizeStringConverter />
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
<TextBlock Text="{Binding DisplayName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<DockPanel Grid.IsSharedSizeScope="True">
<Grid Margin="15,5,0,0"
HorizontalAlignment="Stretch"
DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" />
<ColumnDefinition Width="400" SharedSizeGroup="Name" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Modified" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="150" SharedSizeGroup="Type" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
<ColumnDefinition Width="200" SharedSizeGroup="Size" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="1"
Text="Name" />
<GridSplitter Grid.Column="2"
ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left" Grid.Column="2" VerticalAlignment="Stretch" Width="1" Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="3"
Text="Date Modified" />
<GridSplitter Grid.Column="4"
ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left"
Grid.Column="4"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="5"
Text="Type" />
<GridSplitter Grid.Column="6" ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left"
Grid.Column="6"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="7"
Text="Size" />
<GridSplitter Grid.Column="8"
ResizeDirection="Columns"
Background="Transparent" />
<Rectangle HorizontalAlignment="Left"
Grid.Column="8"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource ThemeControlMidBrush}"/>
</Grid>
<ListBox x:Name="PART_Files"
Margin="0,5"
ItemsSource="{Binding Items}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItems="{Binding SelectedItems}"
SelectionMode="{Binding SelectionMode}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Icon" />
<ColumnDefinition SharedSizeGroup="Name" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Modified" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Type" />
<ColumnDefinition SharedSizeGroup="Splitter" />
<ColumnDefinition SharedSizeGroup="Size" />
<ColumnDefinition SharedSizeGroup="Splitter" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0"
Width="16"
Height="16">
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}" />
</Image>
<TextBlock Grid.Column="1"
Text="{Binding DisplayName}" />
<TextBlock Grid.Column="3"
Text="{Binding Modified}" />
<TextBlock Grid.Column="5"
Text="{Binding Type}" />
<TextBlock Grid.Column="7" HorizontalAlignment="Right">
<TextBlock.Text>
<Binding Path="Size">
<Binding.Converter>
<internal:FileSizeStringConverter />
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</DockPanel>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type dialogs:ManagedFileChooserOverwritePrompt}" TargetType="dialogs:ManagedFileChooserOverwritePrompt">
<Setter Property="MinWidth" Value="270" />
<Setter Property="MaxWidth" Value="400" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<StackPanel Spacing="10">
<TextBlock TextWrapping="Wrap"
Text="{Binding FileName, RelativeSource={RelativeSource TemplatedParent}, StringFormat='{}{0} already exists. Do you want to replace it?'}" />
<StackPanel HorizontalAlignment="Right"
Spacing="10"
Orientation="Horizontal">
<Button Classes="accent" Content="Yes"
MinWidth="80"
HorizontalContentAlignment="Center"
IsDefault="True"
Command="{Binding Confirm, RelativeSource={RelativeSource TemplatedParent}}" />
<Button Content="No"
MinWidth="80"
IsCancel="True"
HorizontalContentAlignment="Center"
Command="{Binding Cancel, RelativeSource={RelativeSource TemplatedParent}}" />
</StackPanel>
</StackPanel>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>

1
src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs

@ -33,6 +33,7 @@ namespace Avalonia.Win32
try
{
var ret = p.IsReady;
_ = p.TotalSize; // try to read size as a proof of read access.
return ret;
}
catch (Exception e)

Loading…
Cancel
Save