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(parent); + return result; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return await Show(dialog, parent); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return (await Show(dialog, parent))?.FirstOrDefault(); + } + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); + return builder; + } + + public static TAppBuilder UseManagedSystemDialogs(this TAppBuilder builder) + where TAppBuilder : AppBuilderBase, new() where TWindow : Window, new() + { + builder.AfterSetup(_ => + AvaloniaLocator.CurrentMutable.Bind().ToSingleton>()); + return builder; + } + } +} diff --git a/src/Avalonia.Dialogs/ResourceSelectorConverter.cs b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs new file mode 100644 index 0000000000..9d8b6cb1c7 --- /dev/null +++ b/src/Avalonia.Dialogs/ResourceSelectorConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Avalonia.Dialogs +{ + internal class ResourceSelectorConverter : ResourceDictionary, IValueConverter + { + public object Convert(object key, Type targetType, object parameter, CultureInfo culture) + { + TryGetResource((string)key, out var value); + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj new file mode 100644 index 0000000000..d7e1d8cdb3 --- /dev/null +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs new file mode 100644 index 0000000000..8081528e55 --- /dev/null +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.FreeDesktop +{ + internal class LinuxMountedVolumeInfoListener : IDisposable + { + private const string DevByLabelDir = "/dev/disk/by-label/"; + private const string ProcPartitionsDir = "/proc/partitions"; + private const string ProcMountsDir = "/proc/mounts"; + private CompositeDisposable _disposables; + private ObservableCollection _targetObs; + private bool _beenDisposed = false; + + public LinuxMountedVolumeInfoListener(ref ObservableCollection target) + { + _disposables = new CompositeDisposable(); + this._targetObs = target; + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private string GetSymlinkTarget(string x) => Path.GetFullPath(Path.Combine(DevByLabelDir, NativeMethods.ReadLink(x))); + + private void Poll(long _) + { + var fProcPartitions = File.ReadAllLines(ProcPartitionsDir) + .Skip(1) + .Where(p => !string.IsNullOrEmpty(p)) + .Select(p => Regex.Replace(p, @"\s{2,}", " ").Trim().Split(' ')) + .Select(p => (p[2].Trim(), p[3].Trim())) + .Select(p => (Convert.ToUInt64(p.Item1) * 1024, "/dev/" + p.Item2)); + + var fProcMounts = File.ReadAllLines(ProcMountsDir) + .Select(x => x.Split(' ')) + .Select(x => (x[0], x[1])); + + var labelDirEnum = Directory.Exists(DevByLabelDir) ? + new DirectoryInfo(DevByLabelDir).GetFiles() : Enumerable.Empty(); + + var labelDevPathPairs = labelDirEnum + .Select(x => (GetSymlinkTarget(x.FullName), x.Name)); + + var q1 = from mount in fProcMounts + join device in fProcPartitions on mount.Item1 equals device.Item2 + join label in labelDevPathPairs on device.Item2 equals label.Item1 into labelMatches + from x in labelMatches.DefaultIfEmpty() + select new MountedVolumeInfo() + { + VolumePath = mount.Item2, + VolumeSizeBytes = device.Item1, + VolumeLabel = x.Name + }; + + var mountVolInfos = q1.ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + _disposables.Dispose(); + _targetObs.Clear(); + } + + _beenDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..d68c02bfd6 --- /dev/null +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.ObjectModel; + +using Avalonia.Controls.Platform; + +namespace Avalonia.FreeDesktop +{ + public class LinuxMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new LinuxMountedVolumeInfoListener(ref mountedDrives); + } + } +} diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs new file mode 100644 index 0000000000..d9b6dce082 --- /dev/null +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.FreeDesktop +{ + internal static class NativeMethods + { + [DllImport("libc", SetLastError = true)] + private static extern long readlink([MarshalAs(UnmanagedType.LPArray)] byte[] filename, + [MarshalAs(UnmanagedType.LPArray)] byte[] buffer, + long len); + + public static string ReadLink(string path) + { + var symlink = Encoding.UTF8.GetBytes(path); + var result = new byte[4095]; + readlink(symlink, result, result.Length); + var rawstr = Encoding.UTF8.GetString(result); + return rawstr.Substring(0, rawstr.IndexOf('\0')); + } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index edde2176bd..0da97b915c 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -84,8 +84,8 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature())) - .Bind() - .ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)); + .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) + .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..eea695d77e --- /dev/null +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls.Platform; + +namespace Avalonia.Native +{ + internal class WindowsMountedVolumeInfoListener : IDisposable + { + private readonly CompositeDisposable _disposables; + private readonly ObservableCollection _targetObs; + private bool _beenDisposed = false; + private ObservableCollection mountedDrives; + + public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + { + this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private void Poll(long _) + { + var mountVolInfos = Directory.GetDirectories("/Volumes") + .Select(p => new MountedVolumeInfo() + { + VolumeLabel = Path.GetFileName(p), + VolumePath = p, + VolumeSizeBytes = 0 + }) + .ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + + } + _beenDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + } + + public class MacOSMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new WindowsMountedVolumeInfoListener(mountedDrives); + } + } +} diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 59afc877de..c160fd7726 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e88a7d8db2..1d2290236c 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; @@ -12,6 +13,7 @@ using Avalonia.X11; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; + namespace Avalonia.X11 { class AvaloniaX11Platform : IWindowingPlatform @@ -48,7 +50,8 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) - .Bind().ToConstant(new GtkSystemDialog()); + .Bind().ToConstant(new GtkSystemDialog()) + .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index bc40ec2ff7..ac3fa021f1 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -90,7 +90,9 @@ namespace Avalonia.Win32 .Bind().ToSingleton() .Bind().ToConstant(s_instance) .Bind().ToSingleton() - .Bind().ToConstant(s_instance); + .Bind().ToConstant(s_instance) + .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()); + if (options.AllowEglInitialization) Win32GlManager.Initialize(); diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs new file mode 100644 index 0000000000..3e2941814f --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Controls.Platform; + +namespace Avalonia.Win32 +{ + internal class WindowsMountedVolumeInfoListener : IDisposable + { + private readonly CompositeDisposable _disposables; + private readonly ObservableCollection _targetObs; + private bool _beenDisposed = false; + private ObservableCollection mountedDrives; + + public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + { + this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); + + var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) + .Subscribe(Poll); + + _disposables.Add(pollTimer); + + Poll(0); + } + + private void Poll(long _) + { + var allDrives = DriveInfo.GetDrives(); + + var mountVolInfos = allDrives + .Select(p => new MountedVolumeInfo() + { + VolumeLabel = p.VolumeLabel, + VolumePath = p.RootDirectory.FullName, + VolumeSizeBytes = (ulong)p.TotalSize + }) + .ToArray(); + + if (_targetObs.SequenceEqual(mountVolInfos)) + return; + else + { + _targetObs.Clear(); + + foreach (var i in mountVolInfos) + _targetObs.Add(i); + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_beenDisposed) + { + if (disposing) + { + + } + _beenDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..e1b5f5a3a0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoProvider.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia.Controls.Platform; + +namespace Avalonia.Win32 +{ + public class WindowsMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new WindowsMountedVolumeInfoListener(mountedDrives); + } + } +}