committed by
GitHub
35 changed files with 1529 additions and 14 deletions
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a platform-specific mount volumes info provider implementation.
|
|||
/// </summary>
|
|||
public interface IMountedVolumeInfoProvider |
|||
{ |
|||
/// <summary>
|
|||
/// Listens to any changes in volume mounts and
|
|||
/// forwards updates to the referenced
|
|||
/// <see cref="ObservableCollection{MountedDriveInfo}"/>.
|
|||
/// </summary>
|
|||
IDisposable Listen(ObservableCollection<MountedVolumeInfo> mountedDrives); |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Describes a Drive's properties.
|
|||
/// </summary>
|
|||
public class MountedVolumeInfo : IEquatable<MountedVolumeInfo> |
|||
{ |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<IsPackable>false</IsPackable> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<AvaloniaResource Include="**\*.xaml"> |
|||
<SubType>Designer</SubType> |
|||
</AvaloniaResource> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\build\BuildTargets.targets" /> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" /> |
|||
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -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]); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
<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" |
|||
xmlns:internal="clr-namespace:Avalonia.Dialogs" |
|||
x:Class="Avalonia.Dialogs.ManagedFileChooser" Margin="10"> |
|||
<UserControl.Resources> |
|||
<internal:FileSizeStringConverter x:Key="FileSizeConverter" /> |
|||
<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> |
|||
<DrawingGroup x:Key="Icon_Volume"> |
|||
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> |
|||
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M0,12L0,6.5C0,5.122,1.122,4,2.5,4L13.5,4C14.879,4,16,5.122,16,6.5L16,12z" /> |
|||
<GeometryDrawing Brush="#FFEFEFF0" Geometry="F1M13,8L12,8 12,7 13,7z M11,8L10,8 10,7 11,7z M13.5,6L2.5,6C2.224,6,2,6.224,2,6.5L2,10 14,10 14,6.5C14,6.224,13.775,6,13.5,6" /> |
|||
<GeometryDrawing Brush="#FF424242" Geometry="F1M13,7L12,7 12,8 13,8z M11,7L10,7 10,8 11,8z M2,10L14,10 14,6.5C14,6.224,13.775,6,13.5,6L2.5,6C2.224,6,2,6.224,2,6.5z M15,11L1,11 1,6.5C1,5.673,1.673,5,2.5,5L13.5,5C14.327,5,15,5.673,15,6.5z" /> |
|||
</DrawingGroup> |
|||
</dialogs:ResourceSelectorConverter> |
|||
</UserControl.Resources> |
|||
<DockPanel> |
|||
<DockPanel DockPanel.Dock="Top" Margin="0 0 0 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" Text="{Binding Location}" Margin="0 0 5 0"> |
|||
<TextBox.KeyBindings> |
|||
<KeyBinding Command="{Binding EnterPressed}" Gesture="Enter"/> |
|||
</TextBox.KeyBindings> |
|||
</TextBox> |
|||
</DockPanel> |
|||
<DockPanel Margin="0 5 0 0" 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" Spacing="10"> |
|||
<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}" |
|||
Margin="0 5 0 0" /> |
|||
|
|||
<TextBox Text="{Binding FileName}" Watermark="File name" DockPanel.Dock="Bottom" IsVisible="{Binding !SelectingFolder}" /> |
|||
|
|||
<ListBox Margin="0 0 5 5" BorderBrush="Transparent" x:Name="QuickLinks" Items="{Binding QuickLinks}" |
|||
SelectedIndex="{Binding QuickLinksSelectedIndex}" |
|||
DockPanel.Dock="Left" Background="{DynamicResource ThemeControlMidBrush}" Focusable="False"> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<StackPanel Spacing="4" Orientation="Horizontal" Background="Transparent"> |
|||
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/> |
|||
<TextBlock Text="{Binding DisplayName}"/> |
|||
</StackPanel> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
<DockPanel Grid.IsSharedSizeScope="True"> |
|||
<Grid DockPanel.Dock="Top" Margin="15 5 0 0" HorizontalAlignment="Stretch"> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition Width="20" SharedSizeGroup="Icon" /> |
|||
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition Width="400" SharedSizeGroup="Name" /> |
|||
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition Width="200" SharedSizeGroup="Modified" /> |
|||
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition Width="150" SharedSizeGroup="Type" /> |
|||
<ColumnDefinition Width="16" SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition Width="200" SharedSizeGroup="Size" /> |
|||
</Grid.ColumnDefinitions> |
|||
<GridSplitter Grid.Column="1" /> |
|||
<TextBlock Grid.Column="2" Text="Name" /> |
|||
<GridSplitter Grid.Column="3" /> |
|||
<TextBlock Grid.Column="4" Text="Date Modified" /> |
|||
<GridSplitter Grid.Column="5" /> |
|||
<TextBlock Grid.Column="6" Text="Type" /> |
|||
<GridSplitter Grid.Column="7" /> |
|||
<TextBlock Grid.Column="8" Text="Size" /> |
|||
</Grid> |
|||
<ListBox x:Name="Files" |
|||
VirtualizationMode="Simple" |
|||
Items="{Binding Items}" |
|||
Margin="0 5" |
|||
SelectionMode="{Binding SelectionMode}" |
|||
SelectedItems="{Binding SelectedItems}" |
|||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"> |
|||
<ListBox.ItemTemplate> |
|||
<DataTemplate> |
|||
<Grid Background="Transparent"> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition SharedSizeGroup="Icon" /> |
|||
<ColumnDefinition SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition SharedSizeGroup="Name" /> |
|||
<ColumnDefinition SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition SharedSizeGroup="Modified" /> |
|||
<ColumnDefinition SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition SharedSizeGroup="Type" /> |
|||
<ColumnDefinition SharedSizeGroup="Splitter" /> |
|||
<ColumnDefinition SharedSizeGroup="Size" /> |
|||
</Grid.ColumnDefinitions> |
|||
<DrawingPresenter Width="16" Height="16" Drawing="{Binding IconKey, Converter={StaticResource Icons}}"/> |
|||
<TextBlock Grid.Column="2" Text="{Binding DisplayName}"/> |
|||
<TextBlock Grid.Column="4" Text="{Binding Modified}" /> |
|||
<TextBlock Grid.Column="6" Text="{Binding Type}" /> |
|||
<TextBlock Grid.Column="8" Text="{Binding Size, Converter={StaticResource FileSizeConverter}}" /> |
|||
</Grid> |
|||
</DataTemplate> |
|||
</ListBox.ItemTemplate> |
|||
</ListBox> |
|||
</DockPanel> |
|||
</DockPanel> |
|||
|
|||
</UserControl> |
|||
@ -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<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.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]); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Avalonia.Dialogs |
|||
{ |
|||
public enum ManagedFileChooserItemType |
|||
{ |
|||
File, |
|||
Folder, |
|||
Volume |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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<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 readonly ObservableCollection<MountedVolumeInfo> MountedVolumes = new ObservableCollection<MountedVolumeInfo>(); |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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<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; |
|||
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<ManagedFileChooserSources>() |
|||
?? new ManagedFileChooserSources(); |
|||
|
|||
var sub1 = AvaloniaLocator.Current |
|||
.GetService<IMountedVolumeInfoProvider>() |
|||
.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 }); |
|||
} |
|||
} |
|||
} |
|||
@ -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<T> : ISystemDialogImpl where T : Window, new() |
|||
{ |
|||
async Task<string[]> 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<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.AfterSetup(_ => |
|||
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<Window>>()); |
|||
return builder; |
|||
} |
|||
|
|||
public static TAppBuilder UseManagedSystemDialogs<TAppBuilder, TWindow>(this TAppBuilder builder) |
|||
where TAppBuilder : AppBuilderBase<TAppBuilder>, new() where TWindow : Window, new() |
|||
{ |
|||
builder.AfterSetup(_ => |
|||
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<TWindow>>()); |
|||
return builder; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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<MountedVolumeInfo> _targetObs; |
|||
private bool _beenDisposed = false; |
|||
|
|||
public LinuxMountedVolumeInfoListener(ref ObservableCollection<MountedVolumeInfo> 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<FileInfo>(); |
|||
|
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -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<MountedVolumeInfo> mountedDrives) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(mountedDrives != null); |
|||
return new LinuxMountedVolumeInfoListener(ref mountedDrives); |
|||
} |
|||
} |
|||
} |
|||
@ -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')); |
|||
} |
|||
} |
|||
} |
|||
@ -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<MountedVolumeInfo> _targetObs; |
|||
private bool _beenDisposed = false; |
|||
private ObservableCollection<MountedVolumeInfo> mountedDrives; |
|||
|
|||
public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> 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<MountedVolumeInfo> mountedDrives) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(mountedDrives != null); |
|||
return new WindowsMountedVolumeInfoListener(mountedDrives); |
|||
} |
|||
} |
|||
} |
|||
@ -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<MountedVolumeInfo> _targetObs; |
|||
private bool _beenDisposed = false; |
|||
private ObservableCollection<MountedVolumeInfo> mountedDrives; |
|||
|
|||
public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> 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); |
|||
} |
|||
} |
|||
} |
|||
@ -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<MountedVolumeInfo> mountedDrives) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(mountedDrives != null); |
|||
return new WindowsMountedVolumeInfoListener(mountedDrives); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue