diff --git a/Avalonia.sln b/Avalonia.sln index cb9669fe6b..bf7710d7e8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -197,9 +197,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" -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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index 1208be34b8..3baf83f6b5 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/build/Rx.props b/build/Rx.props index 359ce53a92..d40849686f 100644 --- a/build/Rx.props +++ b/build/Rx.props @@ -1,5 +1,5 @@  - + diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index d5c6b1d920..b94b1db8bc 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 false @@ -13,6 +13,9 @@ + + + diff --git a/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs b/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs new file mode 100644 index 0000000000..3517f760b4 --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ByteSizeHelper.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Dialogs.Internal +{ + public static class ByteSizeHelper + { + private static readonly string[] Prefixes = + { + "B", + "KB", + "MB", + "GB", + "TB" + }; + + public static string ToString(long bytes) + { + var index = 0; + while (bytes >= 1000) + { + bytes /= 1000; + ++index; + } + return $"{bytes:N} {Prefixes[index]}"; + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ChildFitter.cs b/src/Avalonia.Dialogs/Internal/ChildFitter.cs index d81dafcd99..6f9baff82f 100644 --- a/src/Avalonia.Dialogs/Internal/ChildFitter.cs +++ b/src/Avalonia.Dialogs/Internal/ChildFitter.cs @@ -1,20 +1,21 @@ +using Avalonia; using Avalonia.Controls; using Avalonia.Layout; namespace Avalonia.Dialogs.Internal { - class ChildFitter : Decorator - { - protected override Size MeasureOverride(Size availableSize) - { - return new Size(0, 0); - } + 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; - } - } + protected override Size ArrangeOverride(Size 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 new file mode 100644 index 0000000000..6de931c89d --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/FileSizeStringConverter.cs @@ -0,0 +1,26 @@ +using Avalonia.Data.Converters; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Avalonia.Dialogs.Internal +{ + public class FileSizeStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if(value is long size && size > 0) + { + return ByteSizeHelper.ToString(size); + } + + return ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml index 51154f1161..cb7f2bef34 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml @@ -4,71 +4,127 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Show hidden files - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Show hidden files + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectedItems="{Binding SelectedItems}" + ScrollViewer.HorizontalScrollBarVisibility="Disabled"> + + + + + + + + + + + + + + + + + + + + + + + - + + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs index dd23a90922..7aba87115d 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -9,57 +10,79 @@ 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]); - } - } + 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 index 1f561d0cd2..d54945ac60 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs @@ -5,34 +5,46 @@ 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; - } + 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 index d5f72cdbac..b6535a93d0 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs @@ -1,45 +1,69 @@ +using System; + 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; - } - } + class ManagedFileChooserItemViewModel : InternalViewModelBase + { + private string _displayName; + private string _path; + private bool _isDirectory; + private DateTime _modified; + private string _type; + private long _size; + + public string DisplayName + { + get => _displayName; + set => this.RaiseAndSetIfChanged(ref _displayName, value); + } + + public string Path + { + get => _path; + set => this.RaiseAndSetIfChanged(ref _path, value); + } + + public DateTime Modified + { + get => _modified; + set => this.RaiseAndSetIfChanged(ref _modified, value); + } + + public string Type + { + get => _type; + set => this.RaiseAndSetIfChanged(ref _type, value); + } + + public long Size + { + get => _size; + set => this.RaiseAndSetIfChanged(ref _size, value); + } + + public string IconKey => IsDirectory ? "Icon_Folder" : "Icon_File"; + + public bool IsDirectory + { + get => _isDirectory; + set + { + if (this.RaiseAndSetIfChanged(ref _isDirectory, value)) + { + this.RaisePropertyChanged(nameof(IconKey)); + } + } + } + + public ManagedFileChooserItemViewModel() + { + } + + public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item) + { + IsDirectory = true; + Path = item.Path; + DisplayName = item.DisplayName; + } + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs new file mode 100644 index 0000000000..b04a36692c --- /dev/null +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserNavigationItem.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Dialogs.Internal +{ + public class ManagedFileChooserNavigationItem + { + public string DisplayName { get; set; } + public string Path { get; set; } + } +} diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs index 587b32fe03..84a3173eac 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs @@ -5,71 +5,85 @@ using System.Runtime.InteropServices; namespace Avalonia.Dialogs.Internal { - public class ManagedFileChooserSources - { - public Func GetUserDirectories { get; set; } - = DefaultGetUserDirectories; + public class ManagedFileChooserSources + { + public Func GetUserDirectories { get; set; } + = DefaultGetUserDirectories; - public Func GetFileSystemRoots { get; set; } - = DefaultGetFileSystemRoots; + public Func GetFileSystemRoots { get; set; } + = DefaultGetFileSystemRoots; - public Func GetAllItemsDelegate { get; set; } - = DefaultGetAllItems; + public Func GetAllItemsDelegate { get; set; } + = DefaultGetAllItems; - public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); + 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[] DefaultGetAllItems(ManagedFileChooserSources sources) + { + return sources.GetUserDirectories().Concat(sources.GetFileSystemRoots()).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(); - } + 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 + }; - return new[] - { - new ManagedFileChooserNavigationItem - { - DisplayName = "File System", - Path = "/" - } - }; - } - } + 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 class ManagedFileChooserNavigationItem - { - public string DisplayName { get; set; } - public string Path { get; set; } - } + public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return DriveInfo.GetDrives().Select(d => new ManagedFileChooserNavigationItem + { + DisplayName = d.Name, + Path = d.RootDirectory.FullName + }).ToArray(); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var paths = Directory.GetDirectories("/Volumes"); + + return paths.Select(x => new ManagedFileChooserNavigationItem + { + DisplayName = Path.GetFileName(x), + Path = x + }).ToArray(); + } + else + { + var paths = Directory.GetDirectories("/media/"); + + var drives = new ManagedFileChooserNavigationItem[] + { + new ManagedFileChooserNavigationItem + { + DisplayName = "File System", + Path = "/" + } + }.Concat(paths.Select(x => new ManagedFileChooserNavigationItem + { + DisplayName = Path.GetFileName(x), + Path = x + })).ToArray(); + + return drives; + } + } + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 72bff302bd..8f557a212d 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -2,222 +2,334 @@ using System; using System.Collections.Specialized; using System.IO; using System.Linq; +using System.Reactive; using System.Runtime.InteropServices; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; +using ReactiveUI; 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}); - } - } + { + 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; + string _fileName; + private bool _showHiddenFiles; + private ManagedFileChooserFilterViewModel _selectedFilter; + private bool _selectingDirectory; + private bool _savingFile; + private bool _scheduledSelectionValidation; + private string _defaultExtension; + + public string Location + { + get => _location; + private set => this.RaiseAndSetIfChanged(ref _location, value); + } + + public string FileName + { + get => _fileName; + private set => this.RaiseAndSetIfChanged(ref _fileName, value); + } + + public bool SelectingFolder => _selectingDirectory; + + 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 => this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + + public ManagedFileChooserFilterViewModel SelectedFilter + { + get => _selectedFilter; + set + { + this.RaiseAndSetIfChanged(ref _selectedFilter, value); + Refresh(); + } + } + + public bool ShowHiddenFiles + { + get => _showHiddenFiles; + set + { + this.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.InitialDirectory; + + 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; + + if(dialog is SaveFileDialog sfd) + { + _savingFile = true; + _defaultExtension = sfd.DefaultExtension; + FileName = sfd.InitialFileName; + } + + Navigate(directory, (dialog as FileDialog)?.InitialFileName); + SelectedItems.CollectionChanged += OnSelectionChangedAsync; + + EnterLocationCommand = ReactiveCommand.Create(() => + { + if (Directory.Exists(Location)) + { + Navigate(Location); + } + else if (File.Exists(Location)) + { + CompleteRequested?.Invoke(new[] { Location }); + } + }); + } + + private async void OnSelectionChangedAsync(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); + } + + if(!_selectingDirectory) + { + FileName = SelectedItems.FirstOrDefault()?.DisplayName; + } + } + } + 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(); + + try + { + 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.Where(x => + { + if (_selectingDirectory) + { + if (!(x is DirectoryInfo)) + { + return false; + } + } + + return true; + }).Select(info => new ManagedFileChooserItemViewModel + { + DisplayName = info.Name, + Path = info.FullName, + IsDirectory = info is DirectoryInfo, + Type = info is FileInfo ? info.Extension : "File Folder", + Size = info is FileInfo f ? f.Length : 0, + Modified = info.LastWriteTime + }) + .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); + } + } + + this.RaisePropertyChanged(nameof(QuickLinksSelectedIndex)); + } + catch (System.UnauthorizedAccessException) + { + } + } + } + + 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 if(_savingFile) + { + if (!string.IsNullOrWhiteSpace(FileName)) + { + if (!Path.HasExtension(FileName) && !string.IsNullOrWhiteSpace(_defaultExtension)) + { + FileName = Path.ChangeExtension(FileName, _defaultExtension); + } + + CompleteRequested?.Invoke(new[] { Path.Combine(Location, FileName) }); + } + } + else + { + CompleteRequested?.Invoke(SelectedItems.Select(i => i.Path).ToArray()); + } + } + + public void SelectSingleFile(ManagedFileChooserItemViewModel item) + { + CompleteRequested?.Invoke(new[] { item.Path }); + } + + public ReactiveCommand EnterLocationCommand { get; } + } } diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml index 34c8dba363..002f35ae24 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml @@ -2,6 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal" xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal" - Title="{Binding Title}" MinWidth="700" MinHeight="500"> - + Icon="resm:WalletWasabi.Gui.Assets.WasabiLogo256.png?assembly=WalletWasabi.Gui" + Title="{Binding Title}" Width="1100" Height="500" MinWidth="1100" MinHeight="500" + FontFamily="{DynamicResource UiFont}" FontSize="14" + Foreground="{DynamicResource ThemeForegroundBrush}"> + diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs index 43276f5e90..34b0163808 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs @@ -1,18 +1,20 @@ using System; +using System.Runtime.InteropServices; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.Media; namespace Avalonia.Dialogs.Internal { - class ManagedFileDialog : Window - { - private ManagedFileChooserViewModel _model; - public ManagedFileDialog() - { - AvaloniaXamlLoader.Load(this); - #if DEBUG - this.AttachDevTools(); - #endif - } - } + class ManagedFileDialog : Window + { + 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 index a492dfed3a..8346ba952c 100644 --- a/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs +++ b/src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs @@ -5,17 +5,17 @@ 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 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(); - } - } + 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 index 26a11fc4ff..8c0aba5942 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Dialogs.Internal; @@ -7,48 +8,48 @@ 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 - }; + public static class ManagedFileDialogExtensions + { + class ManagedSystemDialogImpl : ISystemDialogImpl + { + async Task Show(SystemDialog d, IWindowImpl parent) + { + var model = new ManagedFileChooserViewModel((FileSystemDialog)d); - string[] result = null; - model.CompleteRequested += items => - { - result = items; - dialog.Close(); - }; - model.CancelRequested += dialog.Close; + var dialog = new ManagedFileDialog + { + DataContext = model + }; - await dialog.ShowDialog(parent); - return result; - } - - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) - { - return await Show(dialog, parent); - } + string[] result = null; + model.CompleteRequested += items => + { + result = items; + dialog.Close(); + }; + model.CancelRequested += dialog.Close; - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) - { - return (await Show(dialog, parent))?.FirstOrDefault(); - } - } + await dialog.ShowDialog(parent); + return result; + } - public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() - { - builder.AfterPlatformServicesSetup(_ => - AvaloniaLocator.CurrentMutable.Bind().ToSingleton()); - return builder; - } - } + 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.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton()); + return builder; + } + } }