diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 1521108278d..6f89e88cf3b 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -238,7 +238,7 @@ List GetFilters() SuggestedStartLocation = lastSelectedDirectory, SuggestedFileName = "FileName", DefaultExtension = fileTypes?.Any() == true ? "txt" : null, - ShowOverwritePrompt = false + ShowOverwritePrompt = true }); if (file is not null) @@ -436,7 +436,7 @@ private IStorageProvider GetStorageProvider() { var forceManaged = this.Get("ForceManaged").IsChecked ?? false; return forceManaged - ? new ManagedStorageProvider(GetWindow(), null) + ? new ManagedStorageProvider(GetWindow()) : GetTopLevel().StorageProvider; } diff --git a/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs b/src/Avalonia.Controls/Platform/Dialogs/IMountedVolumeInfoProvider.cs similarity index 100% rename from src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs rename to src/Avalonia.Controls/Platform/Dialogs/IMountedVolumeInfoProvider.cs diff --git a/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs b/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs index 3eee8e848ea..131aacc9844 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/IStorageProviderFactory.cs +++ b/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; /// /// Factory allows to register custom storage provider instead of native implementation. /// +[Unstable] public interface IStorageProviderFactory { IStorageProvider CreateProvider(TopLevel topLevel); diff --git a/src/Avalonia.Controls/Platform/MountedDriveInfo.cs b/src/Avalonia.Controls/Platform/Dialogs/MountedDriveInfo.cs similarity index 100% rename from src/Avalonia.Controls/Platform/MountedDriveInfo.cs rename to src/Avalonia.Controls/Platform/Dialogs/MountedDriveInfo.cs diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs index 37e6272abd1..b2d193fcaf9 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -4,8 +4,6 @@ using System.Threading.Tasks; using Avalonia.Platform.Storage; -#nullable enable - namespace Avalonia.Controls.Platform { /// diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index f9542c883a0..f1f94facbc2 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -14,7 +14,7 @@ public class OverlayLayer : Canvas 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().FirstOrDefault(); return layers?.OverlayLayer; diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs index ba432337dcc..ee52496866c 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs +++ b/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 @@ private static void ShellExec(string cmd, bool waitForExit = true) { if (waitForExit) { - process.WaitForExit(); + process?.WaitForExit(); } } } @@ -61,7 +61,7 @@ private void Button_OnClick(object sender, RoutedEventArgs e) } 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}" : "", diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index 1b0efed66f7..61f47aac4ac 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -21,4 +21,5 @@ + diff --git a/src/Avalonia.Dialogs/Internal/AvaloniaDialogsInternalViewModelBase.cs b/src/Avalonia.Dialogs/Internal/AvaloniaDialogsInternalViewModelBase.cs index 474f8ebd587..386b3ccfd98 100644 --- a/src/Avalonia.Dialogs/Internal/AvaloniaDialogsInternalViewModelBase.cs +++ b/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(ref T field, T value, [CallerMemberName] string propertyName = null) + internal protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (!EqualityComparer.Default.Equals(field, value)) { @@ -20,7 +21,7 @@ internal protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMem return false; } - internal protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + internal protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/src/Avalonia.Dialogs/Internal/BclMountedVolumeInfoProvider.cs b/src/Avalonia.Dialogs/Internal/BclMountedVolumeInfoProvider.cs new file mode 100644 index 00000000000..6dbd7f3a169 --- /dev/null +++ b/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 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; + } +} diff --git a/src/Avalonia.Dialogs/Internal/ChildFitter.cs b/src/Avalonia.Dialogs/Internal/ChildFitter.cs index 10cc106d339..1d237770483 100644 --- a/src/Avalonia.Dialogs/Internal/ChildFitter.cs +++ b/src/Avalonia.Dialogs/Internal/ChildFitter.cs @@ -11,7 +11,7 @@ protected override Size MeasureOverride(Size availableSize) protected override Size ArrangeOverride(Size finalSize) { - Child.Measure(finalSize); + Child?.Measure(finalSize); base.ArrangeOverride(finalSize); return finalSize; } diff --git a/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs index 0060c5e8b24..a5862d8c70b 100644 --- a/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs +++ b/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 @@ public object Convert(object value, Type targetType, object parameter, CultureIn 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(); } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs index 8e24da97a43..85b036ced1a 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs +++ b/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) diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs index b3fb7f71265..bf1bdc3d28e 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs +++ b/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 @@ public DateTime Modified set => this.RaiseAndSetIfChanged(ref _modified, value); } - public string Type + public string? Type { get => _type; set => this.RaiseAndSetIfChanged(ref _type, value); diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs index 6c578367fb4..f3b775f1637 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs +++ b/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; } } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs index 80e9c561fa8..1b2d490007a 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs @@ -64,7 +64,7 @@ public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() try { - Directory.GetFiles(x.VolumePath); + Directory.GetFiles(x.VolumePath!); } catch (Exception) { @@ -79,7 +79,7 @@ public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() }; }) .Where(x => x != null) - .ToArray(); + .ToArray()!; } } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 0cd86511ed1..ce2b8f2a497 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/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 CompleteRequested; - public event Action OverwritePrompt; + public event Action? CancelRequested; + public event Action? CompleteRequested; + public event Action? OverwritePrompt; public AvaloniaList QuickLinks { get; } = new AvaloniaList(); @@ -31,25 +31,25 @@ public class ManagedFileChooserViewModel : AvaloniaDialogsInternalViewModelBase public AvaloniaList SelectedItems { get; } = new AvaloniaList(); - 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 @@ public string FileName public bool ShowFilters { get; } public SelectionMode SelectionMode { get; } - public string Title { get; } + public string? Title { get; } public int QuickLinksSelectedIndex { @@ -80,7 +80,7 @@ public int QuickLinksSelectedIndex set => this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); } - public ManagedFileChooserFilterViewModel SelectedFilter + public ManagedFileChooserFilterViewModel? SelectedFilter { get => _selectedFilter; set @@ -115,9 +115,9 @@ public ManagedFileChooserViewModel(ManagedFileDialogOptions options) .GetService() ?? new ManagedFileChooserSources(); - var sub1 = AvaloniaLocator.Current - .GetRequiredService() - .Listen(ManagedFileChooserSources.MountedVolumes); + var sub1 = (AvaloniaLocator.Current.GetService() + ?? new BclMountedVolumeInfoProvider()) + .Listen(ManagedFileChooserSources.MountedVolumes); var sub2 = ManagedFileChooserSources.MountedVolumes.GetWeakCollectionChangedObservable() .Subscribe(x => Dispatcher.UIThread.Post(() => RefreshQuickLinks(quickSources))); @@ -200,7 +200,7 @@ public void EnterPressed() } } - private async void OnSelectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) + private async void OnSelectionChangedAsync(object? sender, NotifyCollectionChangedEventArgs e) { if (_scheduledSelectionValidation) { @@ -244,11 +244,11 @@ await Dispatcher.UIThread.InvokeAsync(() => }); } - 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 @@ void NavigateRoot(string initialSelectionName) 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 @@ public void Ok() { if (_selectingDirectory) { - CompleteRequested?.Invoke(new[] { Location }); + CompleteRequested?.Invoke(new[] { Location! }); } else if (_savingFile) { @@ -381,7 +381,7 @@ public void Ok() 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 @@ public void Ok() } 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! }); } } } diff --git a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs index e46b9276fc9..4613499d9d2 100644 --- a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs +++ b/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(); } diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.cs b/src/Avalonia.Dialogs/ManagedFileChooser.cs index 915f1e95081..97730e89096 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.cs +++ b/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; diff --git a/src/Avalonia.Dialogs/ManagedFileChooserOverwritePrompt.cs b/src/Avalonia.Dialogs/ManagedFileChooserOverwritePrompt.cs new file mode 100644 index 00000000000..117162b2a38 --- /dev/null +++ b/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? Result; + + private string _fileName = ""; + + public static readonly DirectProperty FileNameProperty = AvaloniaProperty.RegisterDirect( + "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); + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 86c0bfc5881..1157c5516f4 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/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 : 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(); - return new ManagedStorageProvider(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().ToSingleton>()); - return builder; + return builder.UseManagedSystemDialogs(null); } public static AppBuilder UseManagedSystemDialogs(this AppBuilder builder) where TWindow : Window, new() { - builder.AfterSetup(_ => - AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); - return builder; + return builder.UseManagedSystemDialogs(() => new TWindow()); } [Obsolete("Use Window.StorageProvider API or TopLevel.StorageProvider API"), EditorBrowsable(EditorBrowsableState.Never)] @@ -48,12 +49,41 @@ public static Task ShowManagedAsync(this OpenFileDialog dialog, Window public static async Task ShowManagedAsync(this OpenFileDialog dialog, Window parent, ManagedFileDialogOptions? options = null) where TWindow : Window, new() { - var impl = new ManagedStorageProvider(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? customRootFactory = null) + { + var options = optionsOverride ?? AvaloniaLocator.Current.GetService(); + if (options is not null && customRootFactory is not null) + { + options = options with { ContentRootFactory = customRootFactory }; + } + + return options; + } + + private static AppBuilder UseManagedSystemDialogs(this AppBuilder builder, Func? customFactory) + { + builder.AfterSetup(_ => + { + var options = PrepareOptions(null, customFactory); + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(new ManagedStorageProviderFactory(options)); + if (options?.CustomVolumeInfoProvider is not null) + { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(options.CustomVolumeInfoProvider); + } + }); + return builder; + } } } diff --git a/src/Avalonia.Dialogs/ManagedFileDialogOptions.cs b/src/Avalonia.Dialogs/ManagedFileDialogOptions.cs index 56a3eb91229..21cc8c3a4f6 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogOptions.cs +++ b/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; } + + /// + /// Allows to redefine how root volumes are populated in the dialog. + /// + public IMountedVolumeInfoProvider? CustomVolumeInfoProvider { get; set; } + + /// + /// Allows to redefine content root. + /// Can be a custom Window or any ContentControl (Popup hosted). + /// + public Func? ContentRootFactory { get; set; } } } diff --git a/src/Avalonia.Dialogs/ManagedStorageProvider.cs b/src/Avalonia.Dialogs/ManagedStorageProvider.cs index 3e7c58c6899..15b8bf81b4b 100644 --- a/src/Avalonia.Dialogs/ManagedStorageProvider.cs +++ b/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 : 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 @@ public ManagedStorageProvider(Window parent, ManagedFileDialogOptions? managedOp public override async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { var model = new ManagedFileChooserViewModel(options, _managedOptions); - var results = await ManagedStorageProvider.Show(model, _parent); + var results = await Show(model); return results.Select(f => new BclStorageFile(new FileInfo(f))).ToArray(); } @@ -37,7 +39,7 @@ public override async Task> OpenFilePickerAsync(File public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) { var model = new ManagedFileChooserViewModel(options, _managedOptions); - var results = await ManagedStorageProvider.Show(model, _parent); + var results = await Show(model); return results.FirstOrDefault() is { } result ? new BclStorageFile(new FileInfo(result)) @@ -47,102 +49,176 @@ public override async Task> OpenFilePickerAsync(File public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { var model = new ManagedFileChooserViewModel(options, _managedOptions); - var results = await ManagedStorageProvider.Show(model, _parent); + var results = await Show(model); return results.Select(f => new BclStorageFolder(new DirectoryInfo(f))).ToArray(); } - - private static async Task 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 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 ShowAsWindow(Window window, ManagedFileChooserViewModel model) + { + var tcs = new TaskCompletionSource(); + window.Title = model.Title; + window.Closed += delegate { + model.Cancel(); + tcs.TrySetResult(true); + }; + + var result = Array.Empty(); 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(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 ShowAsPopup(ContentControl root, ManagedFileChooserViewModel model) + { + var tcs = new TaskCompletionSource(); + var rootPanel = _parent.FindDescendantOfType()!; + + 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(); + + 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 ShowOverwritePrompt(string filename, ContentControl root) + { + var tcs = new TaskCompletionSource(); + 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(parent); - return result ?? Array.Empty(); + return promptResult; } } diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index 374daa7ab57..0d38ebabfd6 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -133,137 +133,145 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + - - - - - - - - - - - - - - Show hidden files - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + Show hidden files + + + + + + + + + + + + - - - + - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + +