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 <[email protected]>
Max Katz 2 years ago
parent
commit
1a798ff44c
26 changed files with 712 additions and 434 deletions
  1. 2 2
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  2. 0 0
      src/Avalonia.Controls/Platform/Dialogs/IMountedVolumeInfoProvider.cs
  3. 2 1
      src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs
  4. 0 0
      src/Avalonia.Controls/Platform/Dialogs/MountedDriveInfo.cs
  5. 0 2
      src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
  6. 1 1
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  7. 3 3
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  8. 1 0
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  9. 4 3
      src/Avalonia.Dialogs/Internal/AvaloniaDialogsInternalViewModelBase.cs
  10. 40 0
      src/Avalonia.Dialogs/Internal/BclMountedVolumeInfoProvider.cs
  11. 1 1
      src/Avalonia.Dialogs/Internal/ChildFitter.cs
  12. 2 2
      src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs
  13. 1 1
      src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs
  14. 6 6
      src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs
  15. 2 2
      src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs
  16. 2 2
      src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs
  17. 23 23
      src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs
  18. 3 3
      src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs
  19. 4 4
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  20. 31 0
      src/Avalonia.Dialogs/ManagedFileChooserOverwritePrompt.cs
  21. 47 17
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  22. 16 1
      src/Avalonia.Dialogs/ManagedFileDialogOptions.cs
  23. 150 74
      src/Avalonia.Dialogs/ManagedStorageProvider.cs
  24. 160 118
      src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml
  25. 210 168
      src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml
  26. 1 0
      src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs

+ 2 - 2
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 - 0
src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs → src/Avalonia.Controls/Platform/Dialogs/IMountedVolumeInfoProvider.cs


+ 2 - 1
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 - 0
src/Avalonia.Controls/Platform/MountedDriveInfo.cs → src/Avalonia.Controls/Platform/Dialogs/MountedDriveInfo.cs


+ 0 - 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>

+ 1 - 1
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;

+ 3 - 3
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 - 0
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>

+ 4 - 3
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 - 0
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;
+    }
+}

+ 1 - 1
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;
         }

+ 2 - 2
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();
         }

+ 1 - 1
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)

+ 6 - 6
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);

+ 2 - 2
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; }
     }
 }

+ 2 - 2
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()!;
         }
     }
 }

+ 23 - 23
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! });
         }
     }
 }

+ 3 - 3
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();
         }

+ 4 - 4
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 - 0
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);
+    }
+}

+ 47 - 17
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;
+        }
     }
 }

+ 16 - 1
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; } 
     }
 }

+ 150 - 74
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;
     }
 }

+ 160 - 118
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>

+ 210 - 168
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 - 0
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)