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, SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName", SuggestedFileName = "FileName",
DefaultExtension = fileTypes?.Any() == true ? "txt" : null, DefaultExtension = fileTypes?.Any() == true ? "txt" : null,
ShowOverwritePrompt = false ShowOverwritePrompt = true
}); });
if (file is not null) if (file is not null)
@ -436,7 +436,7 @@ CanPickFolder: {storageProvider.CanPickFolder}";
{ {
var forceManaged = this.Get<CheckBox>("ForceManaged").IsChecked ?? false; var forceManaged = this.Get<CheckBox>("ForceManaged").IsChecked ?? false;
return forceManaged return forceManaged
? new ManagedStorageProvider<Window>(GetWindow(), null) ? new ManagedStorageProvider(GetWindow())
: GetTopLevel().StorageProvider; : 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; using Avalonia.Platform.Storage;
namespace Avalonia.Controls.Platform; namespace Avalonia.Controls.Platform;
@ -6,6 +6,7 @@ namespace Avalonia.Controls.Platform;
/// <summary> /// <summary>
/// Factory allows to register custom storage provider instead of native implementation. /// Factory allows to register custom storage provider instead of native implementation.
/// </summary> /// </summary>
[Unstable]
public interface IStorageProviderFactory public interface IStorageProviderFactory
{ {
IStorageProvider CreateProvider(TopLevel topLevel); 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 System.Threading.Tasks;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
#nullable enable
namespace Avalonia.Controls.Platform namespace Avalonia.Controls.Platform
{ {
/// <summary> /// <summary>

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

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

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

@ -10,7 +10,7 @@ namespace Avalonia.Dialogs
{ {
public class AboutAvaloniaDialog : Window 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)}"; public static string Version { get; } = $@"v{s_version.ToString(2)}";
@ -45,7 +45,7 @@ namespace Avalonia.Dialogs
{ {
if (waitForExit) if (waitForExit)
{ {
process.WaitForExit(); process?.WaitForExit();
} }
} }
} }
@ -61,7 +61,7 @@ namespace Avalonia.Dialogs
} }
else else
{ {
using Process process = Process.Start(new ProcessStartInfo using Process? process = Process.Start(new ProcessStartInfo
{ {
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open", FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "", 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\DevAnalyzers.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project> </Project>

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

@ -1,14 +1,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Metadata;
namespace Avalonia.Dialogs.Internal namespace Avalonia.Dialogs.Internal
{ {
public class AvaloniaDialogsInternalViewModelBase : INotifyPropertyChanged 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)) if (!EqualityComparer<T>.Default.Equals(field, value))
{ {
@ -20,7 +21,7 @@ namespace Avalonia.Dialogs.Internal
return false; return false;
} }
internal protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) internal protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 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) protected override Size ArrangeOverride(Size finalSize)
{ {
Child.Measure(finalSize); Child?.Measure(finalSize);
base.ArrangeOverride(finalSize); base.ArrangeOverride(finalSize);
return finalSize; return finalSize;
} }

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

@ -6,7 +6,7 @@ namespace Avalonia.Dialogs.Internal
{ {
public class FileSizeStringConverter : IValueConverter 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) if (value is long size && size > 0)
{ {
@ -16,7 +16,7 @@ namespace Avalonia.Dialogs.Internal
return ""; 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(); throw new NotImplementedException();
} }

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

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

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

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

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

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

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

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

8
src/Avalonia.Dialogs/ManagedFileChooser.cs

@ -15,17 +15,17 @@ namespace Avalonia.Dialogs
[TemplatePart("PART_Files", typeof(ListBox))] [TemplatePart("PART_Files", typeof(ListBox))]
public class ManagedFileChooser : TemplatedControl public class ManagedFileChooser : TemplatedControl
{ {
private Control _quickLinksRoot; private Control? _quickLinksRoot;
private ListBox _filesView; private ListBox? _filesView;
public ManagedFileChooser() public ManagedFileChooser()
{ {
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); 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; 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;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
namespace Avalonia.Dialogs namespace Avalonia.Dialogs
{ {
#if NET6_0_OR_GREATER
[SupportedOSPlatform("windows"), SupportedOSPlatform("macos"), SupportedOSPlatform("linux")]
#endif
public static class ManagedFileDialogExtensions 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) public IStorageProvider CreateProvider(TopLevel topLevel)
{ {
if (topLevel is Window window) return new ManagedStorageProvider(topLevel, _options);
{
var options = AvaloniaLocator.Current.GetService<ManagedFileDialogOptions>();
return new ManagedStorageProvider<T>(window, options);
}
throw new InvalidOperationException("Current platform doesn't support managed picker dialogs");
} }
} }
public static AppBuilder UseManagedSystemDialogs(this AppBuilder builder) public static AppBuilder UseManagedSystemDialogs(this AppBuilder builder)
{ {
builder.AfterSetup(_ => return builder.UseManagedSystemDialogs(null);
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<Window>>());
return builder;
} }
public static AppBuilder UseManagedSystemDialogs<TWindow>(this AppBuilder builder) public static AppBuilder UseManagedSystemDialogs<TWindow>(this AppBuilder builder)
where TWindow : Window, new() where TWindow : Window, new()
{ {
builder.AfterSetup(_ => return builder.UseManagedSystemDialogs(() => new TWindow());
AvaloniaLocator.CurrentMutable.Bind<IStorageProviderFactory>().ToSingleton<ManagedStorageProviderFactory<TWindow>>());
return builder;
} }
[Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] [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, public static async Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions? options = null) where TWindow : Window, new() 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()); var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions());
return files return files
.Select(file => file.TryGetLocalPath() ?? file.Name) .Select(file => file.TryGetLocalPath() ?? file.Name)
.ToArray(); .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 namespace Avalonia.Dialogs
{ {
public class ManagedFileDialogOptions public record ManagedFileDialogOptions
{ {
public bool AllowDirectorySelection { get; set; } 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.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Dialogs.Internal; using Avalonia.Dialogs.Internal;
using Avalonia.Layout;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO; using Avalonia.Platform.Storage.FileIO;
using Avalonia.VisualTree;
namespace Avalonia.Dialogs; 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; private readonly ManagedFileDialogOptions _managedOptions;
public ManagedStorageProvider(Window parent, ManagedFileDialogOptions? managedOptions) public ManagedStorageProvider(TopLevel? parent, ManagedFileDialogOptions? managedOptions = null)
{ {
_parent = parent; _parent = parent;
_managedOptions = managedOptions ?? new ManagedFileDialogOptions(); _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) public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{ {
var model = new ManagedFileChooserViewModel(options, _managedOptions); 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(); 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) public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{ {
var model = new ManagedFileChooserViewModel(options, _managedOptions); var model = new ManagedFileChooserViewModel(options, _managedOptions);
var results = await ManagedStorageProvider<T>.Show(model, _parent); var results = await Show(model);
return results.FirstOrDefault() is { } result return results.FirstOrDefault() is { } result
? new BclStorageFile(new FileInfo(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) public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{ {
var model = new ManagedFileChooserViewModel(options, _managedOptions); 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(); 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(), if (_parent is not null and not Window)
Title = model.Title, {
DataContext = model 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 => model.CompleteRequested += items =>
{ {
result = items; result = items;
dialog.Close(); window.Close();
}; };
model.OverwritePrompt += async (filename) => model.OverwritePrompt += async (filename) =>
{ {
var overwritePromptDialog = new Window() if (await ShowOverwritePrompt(filename, 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 window.Close();
}; }
};
var label = new Label() model.CancelRequested += window.Close;
{
Content = $"{name} already exists.\nDo you want to replace it?"
};
panel.Children.Add(label); if (_parent is Window parent)
DockPanel.SetDock(label, Dock.Top); {
await window.ShowDialog<object>(parent);
}
else
{
window.Show();
}
var buttonPanel = new StackPanel() await tcs.Task;
{
HorizontalAlignment = Layout.HorizontalAlignment.Right,
Orientation = Layout.Orientation.Horizontal,
Spacing = 10
};
var button = new Button() return result;
{ }
Content = "Yes",
HorizontalAlignment = Layout.HorizontalAlignment.Right 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 }; popup.Close();
overwritePromptDialog.Close(); }
dialog.Close(); };
};
buttonPanel.Children.Add(button); model.CancelRequested += delegate
{
popup.Close();
};
button = new Button() rootPanel.Children.Add(popup);
{ _parent.SizeChanged += ParentOnSizeChanged;
Content = "No", try
HorizontalAlignment = Layout.HorizontalAlignment.Right {
}; 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(); _parent.SizeChanged -= ParentOnSizeChanged;
}; }
buttonPanel.Children.Add(button); popup.Width = _parent!.Width;
popup.Height = _parent.Height;
panel.Children.Add(buttonPanel); }
DockPanel.SetDock(buttonPanel, Dock.Bottom); }
overwritePromptDialog.Content = panel;
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 promptResult;
return result ?? Array.Empty<string>();
} }
} }

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

@ -133,137 +133,145 @@
</internal:ResourceSelectorConverter> </internal:ResourceSelectorConverter>
</ResourceDictionary> </ResourceDictionary>
</ControlTheme.Resources> </ControlTheme.Resources>
<Setter Property="Background" Value="{DynamicResource SystemRegionBrush}"/>
<Setter Property="Template" <Setter Property="Template"
x:DataType="internal:ManagedFileChooserViewModel"> x:DataType="internal:ManagedFileChooserViewModel">
<ControlTemplate> <ControlTemplate>
<DockPanel> <Border Background="{TemplateBinding Background}"
<ListBox x:Name="PART_QuickLinks" DockPanel.Dock="Left" ItemsSource="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False"> BorderBrush="{TemplateBinding BorderBrush}"
<ListBox.ItemTemplate> BorderThickness="{TemplateBinding BorderThickness}"
<DataTemplate> CornerRadius="{TemplateBinding CornerRadius}"
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent"> Padding="{TemplateBinding Padding}">
<Image Width="16" Height="16"> <DockPanel>
<Image.Source> <ListBox x:Name="PART_QuickLinks" DockPanel.Dock="Left" ItemsSource="{Binding QuickLinks}" SelectedIndex="{Binding QuickLinksSelectedIndex}" Focusable="False"
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/> MaxWidth="200">
</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">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="internal:ManagedFileChooserItemViewModel"> <DataTemplate>
<Grid Background="Transparent"> <StackPanel Spacing="4" Orientation="Horizontal" 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 Width="16" Height="16">
<Image.Source> <Image.Source>
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/> <DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
</Image.Source> </Image.Source>
</Image> </Image>
<TextBlock Grid.Column="1" Text="{Binding DisplayName}"/> <TextBlock Text="{Binding DisplayName}"/>
<TextBlock Grid.Column="3" Text="{Binding Modified}" /> </StackPanel>
<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> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </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>
</DockPanel> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
<Style Selector="^ /template/ ListBox#QuickLinks"> <Style Selector="^ /template/ ListBox#QuickLinks">
@ -335,4 +343,38 @@
<Setter Property="Margin" Value="4,0,0,0"/> <Setter Property="Margin" Value="4,0,0,0"/>
</Style> </Style>
</ControlTheme> </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> </ResourceDictionary>

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

@ -48,188 +48,230 @@
<ControlTheme x:Key="{x:Type dialogs:ManagedFileChooser}" <ControlTheme x:Key="{x:Type dialogs:ManagedFileChooser}"
TargetType="dialogs:ManagedFileChooser"> TargetType="dialogs:ManagedFileChooser">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate x:DataType="internal:ManagedFileChooserViewModel"> <ControlTemplate x:DataType="internal:ManagedFileChooserViewModel">
<DockPanel Margin="5"> <Border Background="{TemplateBinding Background}"
<DockPanel Margin="0,0,0,5" BorderBrush="{TemplateBinding BorderBrush}"
DockPanel.Dock="Top"> BorderThickness="{TemplateBinding BorderThickness}"
<internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}" CornerRadius="{TemplateBinding CornerRadius}"
DockPanel.Dock="Right"> Padding="{TemplateBinding Padding}">
<Button Command="{Binding GoUp}"> <DockPanel Margin="5">
<Image Stretch="Fill"> <DockPanel Margin="0,0,0,5"
<DrawingImage Drawing="{StaticResource LevelUp}" /> DockPanel.Dock="Top">
</Image> <internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}"
</Button> DockPanel.Dock="Right">
</internal:ChildFitter> <Button Command="{Binding GoUp}">
<internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}" <Image Stretch="Fill">
DockPanel.Dock="Right"> <DrawingImage Drawing="{StaticResource LevelUp}" />
<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}}" />
</Image> </Image>
<TextBlock Text="{Binding DisplayName}" /> </Button>
</StackPanel> </internal:ChildFitter>
</DataTemplate> <internal:ChildFitter Width="{Binding ElementName=Location, Path=Bounds.Height}"
</ListBox.ItemTemplate> DockPanel.Dock="Right">
</ListBox> <Button Command="{Binding Refresh}">
<DockPanel Grid.IsSharedSizeScope="True"> <Image Stretch="Fill">
<Grid Margin="15,5,0,0" <DrawingImage Drawing="{StaticResource Refresh}" />
HorizontalAlignment="Stretch" </Image>
DockPanel.Dock="Top"> </Button>
<Grid.ColumnDefinitions> </internal:ChildFitter>
<ColumnDefinition Width="20" SharedSizeGroup="Icon" /> <TextBox x:Name="Location"
<ColumnDefinition Width="400" SharedSizeGroup="Name" /> Margin="0,0,5,0"
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> Text="{Binding Location}">
<ColumnDefinition Width="200" SharedSizeGroup="Modified" /> <TextBox.KeyBindings>
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> <KeyBinding Command="{Binding EnterPressed}"
<ColumnDefinition Width="150" SharedSizeGroup="Type" /> Gesture="Enter" />
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> </TextBox.KeyBindings>
<ColumnDefinition Width="200" SharedSizeGroup="Size" /> </TextBox>
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> </DockPanel>
</Grid.ColumnDefinitions> <DockPanel Margin="0,5,0,0"
<TextBlock Grid.Column="1" DockPanel.Dock="Bottom">
Text="Name" /> <StackPanel DockPanel.Dock="Left"
<GridSplitter Grid.Column="2" Orientation="Horizontal">
ResizeDirection="Columns" <CheckBox IsChecked="{Binding ShowHiddenFiles}">
Background="Transparent" /> <TextBlock>Show hidden files</TextBlock>
<Rectangle HorizontalAlignment="Left" Grid.Column="2" VerticalAlignment="Stretch" Width="1" Fill="{DynamicResource ThemeControlMidBrush}"/> </CheckBox>
<TextBlock Grid.Column="3" </StackPanel>
Text="Date Modified" /> <StackPanel HorizontalAlignment="Right"
<GridSplitter Grid.Column="4" Orientation="Horizontal"
ResizeDirection="Columns" Spacing="10">
Background="Transparent" /> <StackPanel.Styles>
<Rectangle HorizontalAlignment="Left" <Style Selector="Button">
Grid.Column="4" <Setter Property="Margin" Value="4" />
VerticalAlignment="Stretch" </Style>
Width="1" </StackPanel.Styles>
Fill="{DynamicResource ThemeControlMidBrush}"/> <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" <TextBox DockPanel.Dock="Bottom"
Text="Type" /> IsVisible="{Binding !SelectingFolder}"
<GridSplitter Grid.Column="6" ResizeDirection="Columns" Text="{Binding FileName}"
Background="Transparent" /> Watermark="File name" />
<Rectangle HorizontalAlignment="Left"
Grid.Column="6"
VerticalAlignment="Stretch"
Width="1"
Fill="{DynamicResource ThemeControlMidBrush}"/>
<TextBlock Grid.Column="7" <ListBox x:Name="PART_QuickLinks"
Text="Size" /> MaxWidth="200"
<GridSplitter Grid.Column="8" Margin="0,0,5,5"
ResizeDirection="Columns" BorderBrush="Transparent"
Background="Transparent" /> DockPanel.Dock="Left"
<Rectangle HorizontalAlignment="Left" Focusable="False"
Grid.Column="8" ItemsSource="{Binding QuickLinks}"
VerticalAlignment="Stretch" SelectedIndex="{Binding QuickLinksSelectedIndex}">
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> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid Background="Transparent"> <StackPanel Background="Transparent"
<Grid.ColumnDefinitions> Orientation="Horizontal"
<ColumnDefinition SharedSizeGroup="Icon" /> Spacing="4">
<ColumnDefinition SharedSizeGroup="Name" /> <Image Width="16"
<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"> Height="16">
<DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}" /> <DrawingImage Drawing="{Binding IconKey, Converter={StaticResource Icons}}" />
</Image> </Image>
<TextBlock Grid.Column="1" <TextBlock Text="{Binding DisplayName}" />
Text="{Binding DisplayName}" /> </StackPanel>
<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> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </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>
</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> </ControlTemplate>
</Setter> </Setter>
</ControlTheme> </ControlTheme>

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

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

Loading…
Cancel
Save