From 95af64a8d055531126724c4da55d669191ce6b38 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 27 Dec 2018 20:16:50 +0300 Subject: [PATCH] [WIP] Managed file dialog --- Avalonia.sln | 25 ++ build/CoreLibraries.props | 1 + .../ControlCatalog.NetCore.csproj | 1 + samples/ControlCatalog.NetCore/Program.cs | 41 ++-- src/Avalonia.Controls/AppBuilderBase.cs | 11 + src/Avalonia.Controls/SystemDialog.cs | 16 +- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 20 ++ src/Avalonia.Dialogs/Internal/ChildFitter.cs | 20 ++ .../Internal/InternalViewModelBase.cs | 31 +++ .../Internal/ManagedFileChooser.xaml | 74 ++++++ .../Internal/ManagedFileChooser.xaml.cs | 65 +++++ .../ManagedFileChooserFilterViewModel.cs | 38 +++ .../ManagedFileChooserItemViewModel.cs | 45 ++++ .../Internal/ManagedFileChooserSources.cs | 75 ++++++ .../Internal/ManagedFileChooserViewModel.cs | 223 ++++++++++++++++++ .../Internal/ManagedFileDialog.xaml | 7 + .../Internal/ManagedFileDialog.xaml.cs | 18 ++ .../Internal/ResourceSelectorConverter.cs | 21 ++ .../ManagedFileDialogExtensions.cs | 54 +++++ 19 files changed, 765 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.Dialogs/Avalonia.Dialogs.csproj create mode 100644 src/Avalonia.Dialogs/Internal/ChildFitter.cs create mode 100644 src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml create mode 100644 src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs create mode 100644 src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs create mode 100644 src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs diff --git a/Avalonia.sln b/Avalonia.sln index df60ff4a75..14ecd76517 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -190,6 +190,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Desktop", "src\Avalonia.Desktop\Avalonia.Desktop.csproj", "{3C471044-3640-45E3-B1B2-16D2FF8399EE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks", "src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj", "{BF28998D-072C-439A-AFBB-2FE5021241E0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1714,6 +1715,30 @@ Global {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhone.Build.0 = Release|Any CPU {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhone.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhone.Build.0 = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/CoreLibraries.props b/build/CoreLibraries.props index d989e643b8..3923bdeeda 100644 --- a/build/CoreLibraries.props +++ b/build/CoreLibraries.props @@ -13,6 +13,7 @@ + diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 7e2c707e91..f3dce7fc0e 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -6,6 +6,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 1f53dedc14..2a455cb0b2 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Avalonia; +using Avalonia.Controls; +using Avalonia.Dialogs; +using Avalonia.Dialogs.Internal; using Avalonia.Skia; namespace ControlCatalog.NetCore @@ -13,31 +18,31 @@ namespace ControlCatalog.NetCore static void Main(string[] args) { Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); - if (args.Contains("--wait-for-attach")) + var b = BuildAvaloniaApp(); + b.SetupWithoutStarting(); + var window = new Window(); + window.Show(); + new OpenFileDialog() { - Console.WriteLine("Attach debugger and use 'Set next statement'"); - while (true) + Filters = new List { - Thread.Sleep(100); - if (Debugger.IsAttached) - break; - } - } - if (args.Contains("--fbdev")) - AppBuilder.Configure().InitializeWithLinuxFramebuffer(tl => - { - tl.Content = new MainView(); - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - }); - else - BuildAvaloniaApp().Start(); + new FileDialogFilter {Name = "All files", Extensions = {"*"}}, + new FileDialogFilter {Name = "Image files", Extensions = {"jpg", "png", "gif"}} + }, + Directory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + Title = "My dialog", + InitialFileName = "config.local.json", + AllowMultiple = true + }.ShowAsync(window).ContinueWith(_ => { window.Close(); }, TaskContinuationOptions.ExecuteSynchronously); + + b.Instance.Run(window); } /// /// This method is needed for IDE previewer infrastructure /// public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI(); + => AppBuilder.Configure().UsePlatformDetect().UseSkia().UseReactiveUI().UseManagedSystemDialogs(); static void ConsoleSilencer() { @@ -46,4 +51,4 @@ namespace ControlCatalog.NetCore Console.ReadKey(true); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 376714b20b..9ee9a2aea5 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Reflection; using System.Linq; using Avalonia.Platform; @@ -61,6 +62,8 @@ namespace Avalonia.Controls /// public Action BeforeStartCallback { get; private set; } = builder => { }; + public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { }; + protected AppBuilderBase(IRuntimePlatform platform, Action platformServices) { RuntimePlatform = platform; @@ -110,6 +113,13 @@ namespace Avalonia.Controls AfterSetupCallback = (Action)Delegate.Combine(AfterSetupCallback, callback); return Self; } + + + public TAppBuilder AfterPlatformServicesSetup(Action callback) + { + AfterPlatformServicesSetupCallback = (Action)Delegate.Combine(AfterPlatformServicesSetupCallback, callback); + return Self; + } /// /// Starts the application with an instance of . @@ -275,6 +285,7 @@ namespace Avalonia.Controls RuntimePlatformServicesInitializer(); WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); + AfterPlatformServicesSetupCallback(Self); Instance.RegisterServices(); Instance.Initialize(); AfterSetupCallback(Self); diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index e7cb4763ed..2f03db5aee 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -14,7 +14,13 @@ namespace Avalonia.Controls public abstract class FileSystemDialog : SystemDialog { - public string InitialDirectory { get; set; } + [Obsolete("Use Directory")] + public string InitialDirectory + { + get => Directory; + set => Directory = value; + } + public string Directory { get; set; } } public class SaveFileDialog : FileDialog @@ -45,8 +51,12 @@ namespace Avalonia.Controls public class OpenFolderDialog : FileSystemDialog { - public string DefaultDirectory { get; set; } - + [Obsolete("Use Directory")] + public string DefaultDirectory + { + get => Directory; + set => Directory = value; + } public Task ShowAsync(Window parent) { if(parent == null) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj new file mode 100644 index 0000000000..d5c6b1d920 --- /dev/null +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + false + + + + + Designer + + + Designer + + + + + + + + diff --git a/src/Avalonia.Dialogs/Internal/ChildFitter.cs b/src/Avalonia.Dialogs/Internal/ChildFitter.cs new file mode 100644 index 0000000000..d81dafcd99 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ChildFitter.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Layout; + +namespace Avalonia.Dialogs.Internal +{ + class ChildFitter : Decorator + { + protected override Size MeasureOverride(Size availableSize) + { + return new Size(0, 0); + } + + protected override Size ArrangeOverride(Size finalSize) + { + Child.Measure(finalSize); + base.ArrangeOverride(finalSize); + return finalSize; + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs b/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs new file mode 100644 index 0000000000..35e37eb810 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Avalonia.Dialogs.Internal +{ + class InternalViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (!EqualityComparer.Default.Equals(field, value)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } + + return false; + } + + [NotifyPropertyChangedInvocator] + protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml new file mode 100644 index 0000000000..51154f1161 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Show hidden files + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs new file mode 100644 index 0000000000..dd23a90922 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooser : UserControl + { + private Control _quickLinksRoot; + private ListBox _filesView; + + public ManagedFileChooser() + { + AvaloniaXamlLoader.Load(this); + AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel); + _quickLinksRoot = this.FindControl("QuickLinks"); + _filesView = this.FindControl("Files"); + } + + ManagedFileChooserViewModel Model => DataContext as ManagedFileChooserViewModel; + + private void OnPointerPressed(object sender, PointerPressedEventArgs e) + { + var model = (e.Source as StyledElement)?.DataContext as ManagedFileChooserItemViewModel; + if(model == null) + return; + + var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); + if (e.ClickCount == 2 || isQuickLink) + { + if (model.IsDirectory) + Model?.Navigate(model.Path); + else + Model?.SelectSingleFile(model); + e.Handled = true; + } + } + + protected override async void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + var model = (DataContext as ManagedFileChooserViewModel); + if (model == null) + return; + var preselected = model.SelectedItems.FirstOrDefault(); + if(preselected == null) + return; + + //Let everything to settle down and scroll to selected item + await Task.Delay(100); + if (preselected != model.SelectedItems.FirstOrDefault()) + return; + + // Workaround for ListBox bug, scroll to the previous file + var indexOfPreselected = model.Items.IndexOf(preselected); + if (indexOfPreselected > 1) + _filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs new file mode 100644 index 0000000000..1f561d0cd2 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooserFilterViewModel : InternalViewModelBase + { + private readonly string[] _extensions; + public string Name { get; } + + public ManagedFileChooserFilterViewModel(FileDialogFilter filter) + { + Name = filter.Name; + if (filter.Extensions.Contains("*")) + return; + _extensions = filter.Extensions?.Select(e => "." + e.ToLowerInvariant()).ToArray(); + } + + public ManagedFileChooserFilterViewModel() + { + Name = "All files"; + } + + public bool Match(string filename) + { + if (_extensions == null) + return true; + foreach(var ext in _extensions) + if (filename.EndsWith(ext, StringComparison.InvariantCultureIgnoreCase)) + return true; + return false; + } + + public override string ToString() => Name; + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs new file mode 100644 index 0000000000..d5f72cdbac --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs @@ -0,0 +1,45 @@ +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooserItemViewModel : InternalViewModelBase + { + private string _displayName; + private string _path; + private bool _isDirectory; + + public string DisplayName + { + get => _displayName; + set => RaiseAndSetIfChanged(ref _displayName, value); + } + + public string Path + { + get => _path; + set => RaiseAndSetIfChanged(ref _path, value); + } + + public string IconKey => IsDirectory ? "Icon_Folder" : "Icon_File"; + + public bool IsDirectory + { + get => _isDirectory; + set + { + if (RaiseAndSetIfChanged(ref _isDirectory, value)) + RaisePropertyChanged(nameof(IconKey)); + } + } + + public ManagedFileChooserItemViewModel() + { + + } + + public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item) + { + IsDirectory = true; + Path = item.Path; + DisplayName = item.DisplayName; + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs new file mode 100644 index 0000000000..587b32fe03 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Avalonia.Dialogs.Internal +{ + public class ManagedFileChooserSources + { + public Func GetUserDirectories { get; set; } + = DefaultGetUserDirectories; + + public Func GetFileSystemRoots { get; set; } + = DefaultGetFileSystemRoots; + + public Func GetAllItemsDelegate { get; set; } + = DefaultGetAllItems; + + public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); + + public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources) + { + return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).ToArray(); + } + + private static Environment.SpecialFolder[] s_folders = new[] + { + Environment.SpecialFolder.Desktop, + Environment.SpecialFolder.UserProfile, + Environment.SpecialFolder.MyDocuments, + Environment.SpecialFolder.MyMusic, + Environment.SpecialFolder.MyPictures, + Environment.SpecialFolder.MyVideos + }; + + public static ManagedFileChooserNavigationItem[] DefaultGetUserDirectories() + { + return s_folders.Select(Environment.GetFolderPath).Distinct() + .Where(d => !string.IsNullOrWhiteSpace(d)) + .Where(Directory.Exists) + .Select(d => new ManagedFileChooserNavigationItem + { + Path = d, + DisplayName = Path.GetFileName(d) + }).ToArray(); + } + + public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return DriveInfo.GetDrives().Select(d => new ManagedFileChooserNavigationItem + { + DisplayName = d.Name, + Path = d.RootDirectory.FullName + }).ToArray(); + } + + return new[] + { + new ManagedFileChooserNavigationItem + { + DisplayName = "File System", + Path = "/" + } + }; + } + } + + public class ManagedFileChooserNavigationItem + { + public string DisplayName { get; set; } + public string Path { get; set; } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs new file mode 100644 index 0000000000..72bff302bd --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileChooserViewModel : InternalViewModelBase + { + public event Action CancelRequested; + public event Action CompleteRequested; + + public AvaloniaList QuickLinks { get; } = + new AvaloniaList(); + + public AvaloniaList Items { get; } = + new AvaloniaList(); + + public AvaloniaList Filters { get; } = + new AvaloniaList(); + + public AvaloniaList SelectedItems { get; } = + new AvaloniaList(); + + string _location; + private bool _showHiddenFiles; + private ManagedFileChooserFilterViewModel _selectedFilter; + private bool _selectingDirectory; + private bool _scheduledSelectionValidation; + + public string Location + { + get => _location; + private set => RaiseAndSetIfChanged(ref _location, value); + } + + public bool ShowFilters { get; } + public SelectionMode SelectionMode { get; } + public string Title { get; } + + public int QuickLinksSelectedIndex + { + get + { + for (var index = 0; index < QuickLinks.Count; index++) + { + var i = QuickLinks[index]; + if (i.Path == Location) + return index; + } + + return -1; + } + set => RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + public ManagedFileChooserFilterViewModel SelectedFilter + { + get => _selectedFilter; + set + { + RaiseAndSetIfChanged(ref _selectedFilter, value); + Refresh(); + } + } + + public bool ShowHiddenFiles + { + get => _showHiddenFiles; + set + { + RaiseAndSetIfChanged(ref _showHiddenFiles, value); + Refresh(); + } + } + + public ManagedFileChooserViewModel(FileSystemDialog dialog) + { + var quickSources = AvaloniaLocator.Current.GetService() + ?? new ManagedFileChooserSources(); + QuickLinks.Clear(); + + QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); + Title = dialog.Title ?? ( + dialog is OpenFileDialog ? "Open file" + : dialog is SaveFileDialog ? "Save file" + : dialog is OpenFolderDialog ? "Select directory" + : throw new ArgumentException(nameof(dialog))); + + var directory = dialog.Directory; + if (directory == null || !Directory.Exists(directory)) + directory = Directory.GetCurrentDirectory(); + + if (dialog is FileDialog fd) + { + if (fd.Filters?.Count > 0) + { + Filters.AddRange(fd.Filters.Select(f => new ManagedFileChooserFilterViewModel(f))); + _selectedFilter = Filters[0]; + ShowFilters = true; + } + + if (dialog is OpenFileDialog ofd) + { + if (ofd.AllowMultiple) + SelectionMode = SelectionMode.Multiple; + } + } + + _selectingDirectory = dialog is OpenFolderDialog; + + Navigate(directory, (dialog as FileDialog)?.InitialFileName); + SelectedItems.CollectionChanged += OnSelectionChanged; + } + + private async void OnSelectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if(_scheduledSelectionValidation) + return; + _scheduledSelectionValidation = true; + await Dispatcher.UIThread.InvokeAsync(() => + { + try + { + if(_selectingDirectory) + SelectedItems.Clear(); + else + { + var invalidItems = SelectedItems.Where(i => i.IsDirectory).ToList(); + foreach (var item in invalidItems) + SelectedItems.Remove(item); + } + } + finally + { + _scheduledSelectionValidation = false; + } + }); + } + + void NavigateRoot(string initialSelectionName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + Navigate(Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), initialSelectionName); + else + Navigate("/", initialSelectionName); + } + + public void Refresh() => Navigate(Location); + + public void Navigate(string path, string initialSelectionName = null) + { + if (!Directory.Exists(path)) + NavigateRoot(initialSelectionName); + else + { + Location = path; + Items.Clear(); + SelectedItems.Clear(); + var infos = new DirectoryInfo(path).EnumerateFileSystemInfos(); + if (!ShowHiddenFiles) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + infos = infos.Where(i => (i.Attributes & (FileAttributes.Hidden | FileAttributes.System)) != 0); + else + infos = infos.Where(i => !i.Name.StartsWith(".")); + } + + if (SelectedFilter != null) + infos = infos.Where(i => i is DirectoryInfo || SelectedFilter.Match(i.Name)); + + Items.AddRange(infos.Select(info => new ManagedFileChooserItemViewModel + { + DisplayName = info.Name, + Path = info.FullName, + IsDirectory = info is DirectoryInfo + }).OrderByDescending(x => x.IsDirectory) + .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase)); + + if (initialSelectionName != null) + { + var sel = Items.FirstOrDefault(i => !i.IsDirectory && i.DisplayName == initialSelectionName); + if (sel != null) + SelectedItems.Add(sel); + } + + RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + + } + + public void GoUp() + { + var parent = Path.GetDirectoryName(Location); + if (string.IsNullOrWhiteSpace(parent)) + return; + Navigate(parent); + } + + public void Cancel() + { + CancelRequested?.Invoke(); + } + + public void Ok() + { + if (_selectingDirectory) + CompleteRequested?.Invoke(new[] {Location}); + else + CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); + } + + public void SelectSingleFile(ManagedFileChooserItemViewModel item) + { + CompleteRequested?.Invoke(new[] {item.Path}); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml new file mode 100644 index 0000000000..34c8dba363 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml @@ -0,0 +1,7 @@ + + + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs new file mode 100644 index 0000000000..43276f5e90 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Dialogs.Internal +{ + class ManagedFileDialog : Window + { + private ManagedFileChooserViewModel _model; + public ManagedFileDialog() + { + AvaloniaXamlLoader.Load(this); + #if DEBUG + this.AttachDevTools(); + #endif + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs new file mode 100644 index 0000000000..a492dfed3a --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Avalonia.Dialogs.Internal +{ + public class ResourceSelectorConverter : ResourceDictionary, IValueConverter + { + public object Convert(object key, Type targetType, object parameter, CultureInfo culture) + { + TryGetResource((string)key, out var value); + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs new file mode 100644 index 0000000000..26a11fc4ff --- /dev/null +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Dialogs.Internal; +using Avalonia.Platform; + +namespace Avalonia.Dialogs +{ + public static class ManagedFileDialogExtensions + { + class ManagedSystemDialogImpl : ISystemDialogImpl + { + async Task Show(SystemDialog d, IWindowImpl parent) + { + var model = new ManagedFileChooserViewModel((FileSystemDialog)d); + + var dialog = new ManagedFileDialog + { + DataContext = model + }; + + string[] result = null; + model.CompleteRequested += items => + { + result = items; + dialog.Close(); + }; + model.CancelRequested += dialog.Close; + + await dialog.ShowDialog(parent); + return result; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return await Show(dialog, parent); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return (await Show(dialog, parent))?.FirstOrDefault(); + } + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + builder.AfterPlatformServicesSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton()); + return builder; + } + } +}