Browse Source

Merge branch 'managed-file-dialog' of https://github.com/AvaloniaUI/Avalonia into managed-file-dialog

pull/2777/head
Dan Walmsley 7 years ago
parent
commit
67f3b03a21
  1. 26
      Avalonia.sln
  2. 1
      build/CoreLibraries.props
  3. 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  4. 11
      samples/ControlCatalog.NetCore/Program.cs
  5. 11
      src/Avalonia.Controls/AppBuilderBase.cs
  6. 16
      src/Avalonia.Controls/SystemDialog.cs
  7. 20
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  8. 20
      src/Avalonia.Dialogs/Internal/ChildFitter.cs
  9. 31
      src/Avalonia.Dialogs/Internal/InternalViewModelBase.cs
  10. 74
      src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml
  11. 65
      src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml.cs
  12. 38
      src/Avalonia.Dialogs/Internal/ManagedFileChooserFilterViewModel.cs
  13. 45
      src/Avalonia.Dialogs/Internal/ManagedFileChooserItemViewModel.cs
  14. 75
      src/Avalonia.Dialogs/Internal/ManagedFileChooserSources.cs
  15. 223
      src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs
  16. 7
      src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml
  17. 18
      src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml.cs
  18. 21
      src/Avalonia.Dialogs/Internal/ResourceSelectorConverter.cs
  19. 54
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

26
Avalonia.sln

@ -198,6 +198,8 @@ 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}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
@ -1842,6 +1844,30 @@ Global
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.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

1
build/CoreLibraries.props

@ -13,6 +13,7 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Styling/Avalonia.Styling.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.OpenGL/Avalonia.OpenGL.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.Dialogs/Avalonia.Dialogs.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup/Avalonia.Markup.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj" Condition="'$(TargetFramework)' != 'netstandard2.0'" />

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />

11
samples/ControlCatalog.NetCore/Program.cs

@ -1,7 +1,9 @@
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.Skia;
@ -15,10 +17,13 @@ namespace ControlCatalog.NetCore
static int 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<FileDialogFilter>
{
Thread.Sleep(100);
if (Debugger.IsAttached)

11
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.Controls.ApplicationLifetimes;
@ -59,6 +60,8 @@ namespace Avalonia.Controls
public Action<TAppBuilder> AfterSetupCallback { get; private set; } = builder => { };
public Action<TAppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
protected AppBuilderBase(IRuntimePlatform platform, Action<TAppBuilder> platformServices)
{
RuntimePlatform = platform;
@ -97,6 +100,13 @@ namespace Avalonia.Controls
AfterSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
return Self;
}
public TAppBuilder AfterPlatformServicesSetup(Action<TAppBuilder> callback)
{
AfterPlatformServicesSetupCallback = (Action<TAppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
return Self;
}
/// <summary>
/// Starts the application with an instance of <typeparamref name="TMainWindow"/>.
@ -274,6 +284,7 @@ namespace Avalonia.Controls
RuntimePlatformServicesInitializer();
WindowingSubsystemInitializer();
RenderingSubsystemInitializer();
AfterPlatformServicesSetupCallback(Self);
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);

16
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<string> ShowAsync(Window parent)
{
if(parent == null)

20
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Internal\ManagedFileChooser.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Internal\ManagedFileDialog.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
</ItemGroup>
</Project>

20
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;
}
}
}

31
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<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.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));
}
}
}

74
src/Avalonia.Dialogs/Internal/ManagedFileChooser.xaml

@ -0,0 +1,74 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dialogs="clr-namespace:Avalonia.Dialogs.Internal"
xmlns:internal="clr-namespace:Avalonia.Dialogs.Internal">
<UserControl.Resources>
<DrawingGroup x:Key="LevelUp">
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M14.5,0L6.39,0 5.39,2 2.504,2C1.677,2,1,2.673,1,3.5L1,10.582 1,10.586 1,15.414 3,13.414 3,16 7,16 7,13.414 9,15.414 9,13 14.5,13C15.327,13,16,12.327,16,11.5L16,1.5C16,0.673,15.327,0,14.5,0" />
<GeometryDrawing Brush="#FFDCB679" Geometry="F1M14,3L7.508,3 8.008,2 8.012,2 14,2z M14.5,1L7.008,1 6.008,3 2.504,3C2.227,3,2,3.224,2,3.5L2,9.582 4.998,6.586 9,10.586 9,12 14.5,12C14.775,12,15,11.776,15,11.5L15,1.5C15,1.224,14.775,1,14.5,1" />
<GeometryDrawing Brush="#FF00529C" Geometry="F1M8,11L5,8 2,11 2,13 4,11 4,15 6,15 6,11 8,13z" />
<GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M8.0001,1.9996L7.5001,3.0006 14.0001,3.0006 14.0001,1.9996z" />
</DrawingGroup>
<dialogs:ResourceSelectorConverter x:Key="Icons">
<DrawingGroup x:Key="Icon_Folder"><GeometryDrawing Brush="#00FFFFFF" Geometry="F1M0,0L16,0 16,16 0,16z" /><GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M1.5,1L9.61,1 10.61,3 13.496,3C14.323,3,14.996,3.673,14.996,4.5L14.996,12.5C14.996,13.327,14.323,14,13.496,14L1.5,14C0.673,14,0,13.327,0,12.5L0,2.5C0,1.673,0.673,1,1.5,1" /><GeometryDrawing Brush="#FFDCB67A" Geometry="F1M2,3L8.374,3 8.874,4 2,4z M13.496,4L10,4 9.992,4 8.992,2 1.5,2C1.225,2,1,2.224,1,2.5L1,12.5C1,12.776,1.225,13,1.5,13L13.496,13C13.773,13,13.996,12.776,13.996,12.5L13.996,4.5C13.996,4.224,13.773,4,13.496,4" /><GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M2,3L8.374,3 8.874,4 2,4z" /></DrawingGroup>
<DrawingGroup x:Key="Icon_File"><GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /><GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M4,15C3.03,15,2,14.299,2,13L2,3C2,1.701,3.03,1,4,1L10.061,1 14,4.556 14,13C14,13.97,13.299,15,12,15z" /><GeometryDrawing Brush="#FF9B4E96" Geometry="F1M12,13L4,13 4,3 9,3 9,6 12,6z M9.641,2L3.964,2C3.964,2,3,2,3,3L3,13C3,14,3.964,14,3.964,14L11.965,14C12.965,14,13,13,13,13L13,5z" /><GeometryDrawing Brush="#FFF0EFF1" Geometry="F1M4,3L9,3 9,6 12,6 12,13 4,13z" /></DrawingGroup>
</dialogs:ResourceSelectorConverter>
</UserControl.Resources>
<UserControl.DataTemplates>
<DataTemplate DataType="internal:ManagedFileChooserItemViewModel">
<StackPanel Background="Transparent" Orientation="Horizontal"
Cursor="Hand">
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/>
<TextBlock Text="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
</UserControl.DataTemplates>
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="5">
<dialogs:ChildFitter DockPanel.Dock="Right" Width="{Binding ElementName=Location, Path=Bounds.Height}">
<Button Command="{Binding GoUp}" >
<DrawingPresenter Drawing="{StaticResource LevelUp}" Stretch="Fill"/>
</Button>
</dialogs:ChildFitter>
<TextBox x:Name="Location" IsReadOnly="True" Text="{Binding Location}" Margin="0 0 5 0"/>
</DockPanel>
<DockPanel DockPanel.Dock="Bottom">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<CheckBox IsChecked="{Binding ShowHiddenFiles}">
<TextBlock>Show hidden files</TextBlock>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin">4</Setter>
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}">OK</Button>
<Button Command="{Binding Cancel}">Cancel</Button>
</StackPanel>
</DockPanel>
<DropDown DockPanel.Dock="Bottom"
IsVisible="{Binding ShowFilters}"
Items="{Binding Filters}"
SelectedItem="{Binding SelectedFilter}"
/>
<ListBox Margin="5" BorderBrush="Transparent" x:Name="QuickLinks" Items="{Binding QuickLinks}"
SelectedIndex="{Binding QuickLinksSelectedIndex}"
DockPanel.Dock="Left" Background="{DynamicResource ThemeControlMidBrush}" Focusable="False" />
<ListBox x:Name="Files"
VirtualizationMode="Simple"
Items="{Binding Items}"
Margin="5"
SelectionMode="{Binding SelectionMode}"
SelectedItems="{Binding SelectedItems}"/>
</DockPanel>
</UserControl>

65
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<Control>("QuickLinks");
_filesView = this.FindControl<ListBox>("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]);
}
}
}

38
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;
}
}

45
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;
}
}
}

75
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<ManagedFileChooserNavigationItem[]> GetUserDirectories { get; set; }
= DefaultGetUserDirectories;
public Func<ManagedFileChooserNavigationItem[]> GetFileSystemRoots { get; set; }
= DefaultGetFileSystemRoots;
public Func<ManagedFileChooserSources, ManagedFileChooserNavigationItem[]> 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; }
}
}

223
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<string[]> CompleteRequested;
public AvaloniaList<ManagedFileChooserItemViewModel> QuickLinks { get; } =
new AvaloniaList<ManagedFileChooserItemViewModel>();
public AvaloniaList<ManagedFileChooserItemViewModel> Items { get; } =
new AvaloniaList<ManagedFileChooserItemViewModel>();
public AvaloniaList<ManagedFileChooserFilterViewModel> Filters { get; } =
new AvaloniaList<ManagedFileChooserFilterViewModel>();
public AvaloniaList<ManagedFileChooserItemViewModel> SelectedItems { get; } =
new AvaloniaList<ManagedFileChooserItemViewModel>();
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<ManagedFileChooserSources>()
?? 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});
}
}
}

7
src/Avalonia.Dialogs/Internal/ManagedFileDialog.xaml

@ -0,0 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
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">
<internal:ManagedFileChooser Margin="4"/>
</Window>

18
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
}
}
}

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

54
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<string[]> 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<object>(parent);
return result;
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
{
return await Show(dialog, parent);
}
public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
{
return (await Show(dialog, parent))?.FirstOrDefault();
}
}
public static TAppBuilder UseManagedSystemDialogs<TAppBuilder>(this TAppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
builder.AfterPlatformServicesSetup(_ =>
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl>());
return builder;
}
}
}
Loading…
Cancel
Save