diff --git a/Avalonia.sln b/Avalonia.sln
index ac678ba9ba..568a16ce0e 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,7 +197,11 @@ 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("{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
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
@@ -1842,6 +1846,54 @@ 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
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhone.Build.0 = Release|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.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 6a40f7187d..7919c3ac5a 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -7,6 +7,7 @@
+
diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index 09d2612ac3..5aef0b5520 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -8,6 +8,9 @@ using Avalonia.Controls;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
+using Avalonia.Dialogs;
+using System.Collections.Generic;
+using System.Threading.Tasks;
namespace ControlCatalog.NetCore
{
@@ -51,21 +54,22 @@ namespace ControlCatalog.NetCore
else
return builder.StartWithClassicDesktopLifetime(args);
}
-
+
///
/// This method is needed for IDE previewer infrastructure
///
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
- .With(new X11PlatformOptions {EnableMultiTouch = true})
+ .With(new X11PlatformOptions { EnableMultiTouch = true })
.With(new Win32PlatformOptions
{
EnableMultitouch = true,
AllowEglInitialization = true
})
.UseSkia()
- .UseReactiveUI();
+ .UseReactiveUI()
+ .UseManagedSystemDialogs();
static void SilenceConsole()
{
@@ -74,7 +78,8 @@ namespace ControlCatalog.NetCore
Console.CursorVisible = false;
while (true)
Console.ReadKey(true);
- }) {IsBackground = true}.Start();
+ })
+ { IsBackground = true }.Start();
}
}
}
diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs
index 91d9f034a5..95c65ed92f 100644
--- a/samples/ControlCatalog/MainWindow.xaml.cs
+++ b/samples/ControlCatalog/MainWindow.xaml.cs
@@ -6,6 +6,7 @@ using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using ControlCatalog.ViewModels;
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace ControlCatalog
diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs
index 59ff35be76..307ddd284c 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.Controls.ApplicationLifetimes;
@@ -59,6 +60,8 @@ namespace Avalonia.Controls
public Action AfterSetupCallback { get; private set; } = builder => { };
+ public Action AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
+
protected AppBuilderBase(IRuntimePlatform platform, Action platformServices)
{
RuntimePlatform = platform;
@@ -97,6 +100,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 .
@@ -274,6 +284,7 @@ namespace Avalonia.Controls
RuntimePlatformServicesInitializer();
WindowingSubsystemInitializer();
RenderingSubsystemInitializer();
+ AfterPlatformServicesSetupCallback(Self);
Instance.RegisterServices();
Instance.Initialize();
AfterSetupCallback(Self);
diff --git a/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs b/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs
new file mode 100644
index 0000000000..1420fce3c2
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/IMountedVolumeInfoProvider.cs
@@ -0,0 +1,23 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+
+namespace Avalonia.Controls.Platform
+{
+ ///
+ /// Defines a platform-specific mount volumes info provider implementation.
+ ///
+ public interface IMountedVolumeInfoProvider
+ {
+ ///
+ /// Listens to any changes in volume mounts and
+ /// forwards updates to the referenced
+ /// .
+ ///
+ IDisposable Listen(ObservableCollection mountedDrives);
+ }
+}
diff --git a/src/Avalonia.Controls/Platform/MountedDriveInfo.cs b/src/Avalonia.Controls/Platform/MountedDriveInfo.cs
new file mode 100644
index 0000000000..b534d11d40
--- /dev/null
+++ b/src/Avalonia.Controls/Platform/MountedDriveInfo.cs
@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Controls.Platform
+{
+ ///
+ /// Describes a Drive's properties.
+ ///
+ public class MountedVolumeInfo : IEquatable
+ {
+ public string VolumeLabel { get; set; }
+ public string VolumePath { get; set; }
+ public ulong VolumeSizeBytes { get; set; }
+
+ public bool Equals(MountedVolumeInfo other)
+ {
+ return this.VolumeSizeBytes.Equals(other.VolumeSizeBytes) &&
+ this.VolumePath.Equals(other.VolumePath) &&
+ (this.VolumeLabel ?? string.Empty).Equals(other.VolumeLabel ?? string.Empty);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs
index f321625bcc..6ccaa3c742 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..8b48b4a92c
--- /dev/null
+++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
@@ -0,0 +1,19 @@
+
+
+ netstandard2.0
+ false
+
+
+
+
+ Designer
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Dialogs/ByteSizeHelper.cs b/src/Avalonia.Dialogs/ByteSizeHelper.cs
new file mode 100644
index 0000000000..d849e33399
--- /dev/null
+++ b/src/Avalonia.Dialogs/ByteSizeHelper.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace Avalonia.Dialogs
+{
+ internal static class ByteSizeHelper
+ {
+ private const string formatTemplate = "{0}{1:0.#} {2}";
+
+ private static readonly string[] Prefixes =
+ {
+ "B",
+ "KB",
+ "MB",
+ "GB",
+ "TB",
+ "PB",
+ "EB",
+ "ZB",
+ "YB"
+ };
+
+ public static string ToString(ulong bytes)
+ {
+ if (bytes == 0)
+ {
+ return string.Format(formatTemplate, null, 0, Prefixes[0]);
+ }
+
+ var absSize = Math.Abs((double)bytes);
+ var fpPower = Math.Log(absSize, 1000);
+ var intPower = (int)fpPower;
+ var iUnit = intPower >= Prefixes.Length
+ ? Prefixes.Length - 1
+ : intPower;
+ var normSize = absSize / Math.Pow(1000, iUnit);
+
+ return string.Format(formatTemplate,bytes < 0 ? "-" : null, normSize, Prefixes[iUnit]);
+ }
+ }
+}
diff --git a/src/Avalonia.Dialogs/ChildFitter.cs b/src/Avalonia.Dialogs/ChildFitter.cs
new file mode 100644
index 0000000000..744d455d9d
--- /dev/null
+++ b/src/Avalonia.Dialogs/ChildFitter.cs
@@ -0,0 +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);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ Child.Measure(finalSize);
+ base.ArrangeOverride(finalSize);
+ return finalSize;
+ }
+ }
+}
diff --git a/src/Avalonia.Dialogs/FileSizeStringConverter.cs b/src/Avalonia.Dialogs/FileSizeStringConverter.cs
new file mode 100644
index 0000000000..5b41b9da35
--- /dev/null
+++ b/src/Avalonia.Dialogs/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 class FileSizeStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is long size && size > 0)
+ {
+ return ByteSizeHelper.ToString((ulong)size);
+ }
+
+ return "";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Avalonia.Dialogs/InternalViewModelBase.cs b/src/Avalonia.Dialogs/InternalViewModelBase.cs
new file mode 100644
index 0000000000..520bc15bfe
--- /dev/null
+++ b/src/Avalonia.Dialogs/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/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml
new file mode 100644
index 0000000000..af0c91e7bd
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Show hidden files
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
new file mode 100644
index 0000000000..b967b40c0d
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+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.ItemType == ManagedFileChooserItemType.File)
+ {
+ Model?.SelectSingleFile(model);
+ }
+ else
+ {
+ Model?.Navigate(model.Path);
+ }
+
+ 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/ManagedFileChooserFilterViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs
new file mode 100644
index 0000000000..a0cb664b40
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooserFilterViewModel.cs
@@ -0,0 +1,50 @@
+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/ManagedFileChooserItemType.cs b/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs
new file mode 100644
index 0000000000..835e64a59a
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooserItemType.cs
@@ -0,0 +1,9 @@
+namespace Avalonia.Dialogs
+{
+ public enum ManagedFileChooserItemType
+ {
+ File,
+ Folder,
+ Volume
+ }
+}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs
new file mode 100644
index 0000000000..2801930028
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooserItemViewModel.cs
@@ -0,0 +1,77 @@
+using System;
+
+namespace Avalonia.Dialogs
+{
+ internal class ManagedFileChooserItemViewModel : InternalViewModelBase
+ {
+ private string _displayName;
+ private string _path;
+ private DateTime _modified;
+ private string _type;
+ private long _size;
+ private ManagedFileChooserItemType _itemType;
+
+ 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 ManagedFileChooserItemType ItemType
+ {
+ get => _itemType;
+ set => this.RaiseAndSetIfChanged(ref _itemType, value);
+ }
+
+ public string IconKey
+ {
+ get
+ {
+ switch (ItemType)
+ {
+ case ManagedFileChooserItemType.Folder:
+ return "Icon_Folder";
+ case ManagedFileChooserItemType.Volume:
+ return "Icon_Volume";
+ default:
+ return "Icon_File";
+ }
+ }
+ }
+
+ public ManagedFileChooserItemViewModel()
+ {
+ }
+
+ public ManagedFileChooserItemViewModel(ManagedFileChooserNavigationItem item)
+ {
+ ItemType = item.ItemType;
+ Path = item.Path;
+ DisplayName = item.DisplayName;
+ }
+ }
+}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs b/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs
new file mode 100644
index 0000000000..8dac14bf8b
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooserNavigationItem.cs
@@ -0,0 +1,9 @@
+namespace Avalonia.Dialogs
+{
+ internal class ManagedFileChooserNavigationItem
+ {
+ public string DisplayName { get; set; }
+ public string Path { get; set; }
+ public ManagedFileChooserItemType ItemType { get; set; }
+ }
+}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
new file mode 100644
index 0000000000..0dc024c4dd
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Controls.Platform;
+using Avalonia.Threading;
+
+namespace Avalonia.Dialogs
+{
+ internal 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 readonly ObservableCollection MountedVolumes = new ObservableCollection();
+
+ 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
+ {
+ ItemType = ManagedFileChooserItemType.Folder,
+ Path = d,
+ DisplayName = Path.GetFileName(d)
+ }).ToArray();
+ }
+
+ public static ManagedFileChooserNavigationItem[] DefaultGetFileSystemRoots()
+ {
+ return MountedVolumes
+ .Select(x =>
+ {
+ var displayName = x.VolumeLabel;
+
+ if (displayName == null & x.VolumeSizeBytes > 0)
+ {
+ displayName = $"{ByteSizeHelper.ToString(x.VolumeSizeBytes)} Volume";
+ };
+
+ try
+ {
+ Directory.GetFiles(x.VolumePath);
+ }
+ catch (UnauthorizedAccessException _)
+ {
+ return null;
+ }
+
+ return new ManagedFileChooserNavigationItem
+ {
+ ItemType = ManagedFileChooserItemType.Volume,
+ DisplayName = displayName,
+ Path = x.VolumePath
+ };
+ })
+ .Where(x => x != null)
+ .ToArray();
+ }
+ }
+}
diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
new file mode 100644
index 0000000000..a6847939d7
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
@@ -0,0 +1,368 @@
+using System;
+using System.Collections.Specialized;
+using System.IO;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+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;
+ string _fileName;
+ private bool _showHiddenFiles;
+ private ManagedFileChooserFilterViewModel _selectedFilter;
+ private bool _selectingDirectory;
+ private bool _savingFile;
+ private bool _scheduledSelectionValidation;
+ private bool _alreadyCancelled = false;
+ private string _defaultExtension;
+ private CompositeDisposable _disposables;
+
+ 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();
+ }
+ }
+
+ private void RefreshQuickLinks(ManagedFileChooserSources quickSources)
+ {
+ QuickLinks.Clear();
+ QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i)));
+ }
+
+ public ManagedFileChooserViewModel(FileSystemDialog dialog)
+ {
+ _disposables = new CompositeDisposable();
+
+ var quickSources = AvaloniaLocator.Current
+ .GetService()
+ ?? new ManagedFileChooserSources();
+
+ var sub1 = AvaloniaLocator.Current
+ .GetService()
+ .Listen(ManagedFileChooserSources.MountedVolumes);
+
+ var sub2 = Observable.FromEventPattern(ManagedFileChooserSources.MountedVolumes,
+ nameof(ManagedFileChooserSources.MountedVolumes.CollectionChanged))
+ .ObserveOn(AvaloniaScheduler.Instance)
+ .Subscribe(x => RefreshQuickLinks(quickSources));
+
+ _disposables.Add(sub1);
+ _disposables.Add(sub2);
+
+ CompleteRequested += delegate { _disposables?.Dispose(); };
+ CancelRequested += delegate { _disposables?.Dispose(); };
+
+ RefreshQuickLinks(quickSources);
+
+ 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;
+ }
+
+ public void EnterPressed()
+ {
+ 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.ItemType == ManagedFileChooserItemType.Folder).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;
+ })
+ .Where(x => x.Exists)
+ .Select(info => new ManagedFileChooserItemViewModel
+ {
+ DisplayName = info.Name,
+ Path = info.FullName,
+ Type = info is FileInfo ? info.Extension : "File Folder",
+ ItemType = info is FileInfo ? ManagedFileChooserItemType.File
+ : ManagedFileChooserItemType.Folder,
+ Size = info is FileInfo f ? f.Length : 0,
+ Modified = info.LastWriteTime
+ })
+ .OrderByDescending(x => x.ItemType == ManagedFileChooserItemType.Folder)
+ .ThenBy(x => x.DisplayName, StringComparer.InvariantCultureIgnoreCase));
+
+ if (initialSelectionName != null)
+ {
+ var sel = Items.FirstOrDefault(i => i.ItemType == ManagedFileChooserItemType.File && 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()
+ {
+ if (!_alreadyCancelled)
+ {
+ // INFO: Don't misplace this check or it might cause
+ // StackOverflowException because of recursive
+ // event invokes.
+ _alreadyCancelled = true;
+ 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 });
+ }
+ }
+}
diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
new file mode 100644
index 0000000000..771d2b1b5e
--- /dev/null
+++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Dialogs;
+using Avalonia.Platform;
+
+namespace Avalonia.Dialogs
+{
+ public static class ManagedFileDialogExtensions
+ {
+ class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new()
+ {
+ async Task Show(SystemDialog d, IWindowImpl parent)
+ {
+ var model = new ManagedFileChooserViewModel((FileSystemDialog)d);
+
+ var dialog = new T
+ {
+ Content = new ManagedFileChooser(),
+ DataContext = model
+ };
+
+ dialog.Closed += delegate { model.Cancel(); };
+
+ string[] result = null;
+
+ model.CompleteRequested += items =>
+ {
+ result = items;
+ dialog.Close();
+ };
+
+ model.CancelRequested += dialog.Close;
+
+ await dialog.ShowDialog