diff --git a/src/Avalonia.Controls/Platform/IMountedDriveInfoProvider.cs b/src/Avalonia.Controls/Platform/IMountedDriveInfoProvider.cs deleted file mode 100644 index 18413ff591..0000000000 --- a/src/Avalonia.Controls/Platform/IMountedDriveInfoProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 drive mount information provider implementation. - /// - public interface IMountedDriveInfoProvider : IDisposable - { - /// - /// Observable list of currently-mounted drives. - /// - ObservableCollection MountedDrives { get; } - } -} 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 index 13b8bc45f5..2fa237c714 100644 --- a/src/Avalonia.Controls/Platform/MountedDriveInfo.cs +++ b/src/Avalonia.Controls/Platform/MountedDriveInfo.cs @@ -8,19 +8,19 @@ namespace Avalonia.Controls.Platform /// /// Describes a Drive's properties. /// - public class MountedDriveInfo : IEquatable + public class MountedVolumeInfo : IEquatable { - public string DriveLabel { get; set; } - public string DriveName { get; set; } - public ulong DriveSizeBytes { get; set; } + public string VolumeLabel { get; set; } + public string VolumeName { get; set; } + public ulong VolumeSizeBytes { get; set; } public string DevicePath { get; set; } public string MountPath { get; set; } - public bool Equals(MountedDriveInfo other) + public bool Equals(MountedVolumeInfo other) { - return this.DriveLabel.Equals(other.DriveLabel) && - this.DriveName.Equals(other.DriveName) && - this.DriveSizeBytes.Equals(other.DriveSizeBytes) && + return this.VolumeLabel.Equals(other.VolumeLabel) && + this.VolumeName.Equals(other.VolumeName) && + this.VolumeSizeBytes.Equals(other.VolumeSizeBytes) && this.DevicePath.Equals(other.DevicePath) && this.MountPath.Equals(other.MountPath); } diff --git a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs index 7d2698463d..a7723c6757 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserSources.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserSources.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Reactive.Linq; @@ -20,6 +21,7 @@ namespace Avalonia.Dialogs = DefaultGetAllItems; public ManagedFileChooserNavigationItem[] GetAllItems() => GetAllItemsDelegate(this); + public static readonly ObservableCollection MountedVolumes = new ObservableCollection(); public static ManagedFileChooserNavigationItem[] DefaultGetAllItems(ManagedFileChooserSources sources) { @@ -73,11 +75,7 @@ namespace Avalonia.Dialogs } else { - var drivesInfos = AvaloniaLocator.CurrentMutable - .GetService() - .MountedDrives; - - return drivesInfos + return MountedVolumes .Where(x => !x.MountPath.StartsWith("/boot")) .Select(x => { @@ -92,13 +90,13 @@ namespace Avalonia.Dialogs } else { - var dNameEmpty = string.IsNullOrEmpty(x.DriveLabel.Trim()); + var dNameEmpty = string.IsNullOrEmpty(x.VolumeLabel.Trim()); return new ManagedFileChooserNavigationItem { ItemType = ManagedFileChooserItemType.Volume, - DisplayName = dNameEmpty ? $"{ByteSizeHelper.ToString(x.DriveSizeBytes)} Volume" - : x.DriveLabel, + DisplayName = dNameEmpty ? $"{ByteSizeHelper.ToString(x.VolumeSizeBytes)} Volume" + : x.VolumeLabel, Path = x.MountPath }; } diff --git a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs index 93b9b14535..a6847939d7 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs @@ -36,7 +36,9 @@ namespace Avalonia.Dialogs private bool _selectingDirectory; private bool _savingFile; private bool _scheduledSelectionValidation; + private bool _alreadyCancelled = false; private string _defaultExtension; + private CompositeDisposable _disposables; public string Location { @@ -95,30 +97,36 @@ namespace Avalonia.Dialogs } } - private void RefreshQuickLinks(object _ = null) + private void RefreshQuickLinks(ManagedFileChooserSources quickSources) { - var quickSources = AvaloniaLocator.Current - .GetService() - ?? new ManagedFileChooserSources(); - QuickLinks.Clear(); QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i))); } public ManagedFileChooserViewModel(FileSystemDialog dialog) { - var drivesInfoSrv = AvaloniaLocator.Current - .GetService() - .MountedDrives; + _disposables = new CompositeDisposable(); + + var quickSources = AvaloniaLocator.Current + .GetService() + ?? new ManagedFileChooserSources(); + + var sub1 = AvaloniaLocator.Current + .GetService() + .Listen(ManagedFileChooserSources.MountedVolumes); - var sub1 = Observable.FromEventPattern(drivesInfoSrv, nameof(drivesInfoSrv.CollectionChanged)) + var sub2 = Observable.FromEventPattern(ManagedFileChooserSources.MountedVolumes, + nameof(ManagedFileChooserSources.MountedVolumes.CollectionChanged)) .ObserveOn(AvaloniaScheduler.Instance) - .Subscribe(RefreshQuickLinks); + .Subscribe(x => RefreshQuickLinks(quickSources)); - CompleteRequested += delegate { sub1?.Dispose(); }; - CancelRequested += delegate { sub1?.Dispose(); }; + _disposables.Add(sub1); + _disposables.Add(sub2); - RefreshQuickLinks(); + CompleteRequested += delegate { _disposables?.Dispose(); }; + CancelRequested += delegate { _disposables?.Dispose(); }; + + RefreshQuickLinks(quickSources); Title = dialog.Title ?? ( dialog is OpenFileDialog ? "Open file" @@ -318,7 +326,14 @@ namespace Avalonia.Dialogs public void Cancel() { - CancelRequested?.Invoke(); + 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() diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 3068b2ca91..771d2b1b5e 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -23,12 +23,16 @@ namespace Avalonia.Dialogs 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); diff --git a/src/Avalonia.FreeDesktop/LinuxMountedDriveInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedDriveInfoProvider.cs deleted file mode 100644 index 89faf63b3d..0000000000 --- a/src/Avalonia.FreeDesktop/LinuxMountedDriveInfoProvider.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -using Avalonia.Controls.Platform; -using Tmds.DBus; - -namespace Avalonia.FreeDesktop.Dbus -{ - public class LinuxMountedDriveInfoProvider : IMountedDriveInfoProvider - { - private IDisposable[] _disposables; - private readonly Connection _sysDbus; - private readonly IObjectManager _udisk2Manager; - - public LinuxMountedDriveInfoProvider() - { - this._sysDbus = Connection.System; - - this._udisk2Manager = _sysDbus.CreateProxy("org.freedesktop.UDisks2", "/org/freedesktop/UDisks2"); - - Start(); - } - - async void Start() - { - _disposables = new[] { - await _udisk2Manager.WatchInterfacesAddedAsync(delegate { Poll(); }), - await _udisk2Manager.WatchInterfacesRemovedAsync( delegate { Poll(); }) - }; - - Poll(); - } - - public ObservableCollection MountedDrives { get; } = new ObservableCollection(); - - private async void Poll() - { - var newDriveList = new List(); - - var fProcMounts = File.ReadAllLines("/proc/mounts"); - - var managedObj = await _udisk2Manager.GetManagedObjectsAsync(); - - var res_drives = managedObj.Where(x => x.Key.ToString().Contains("/org/freedesktop/UDisks2/drives/")) - .Select(x => x.Key); - - var res_blockdev = managedObj.Where(x => x.Key.ToString().Contains("/org/freedesktop/UDisks2/block_devices/")) - .Select(x => x); - - var res_fs = managedObj.Where(x => x.Key.ToString().Contains("system")) - .Select(x => x.Key) - .ToList(); - - foreach (var block in res_blockdev) - { - try - { - var iblock = _sysDbus.CreateProxy("org.freedesktop.UDisks2", block.Key); - var iblockProps = await iblock.GetAllAsync(); - - var block_drive = await iblock.GetDriveAsync(); - if (!res_drives.Contains(block_drive)) continue; - - var drive_key = res_drives.Single(x => x == block_drive); - var drives = _sysDbus.CreateProxy("org.freedesktop.UDisks2", drive_key); - var drivesProps = await drives.GetAllAsync(); - - var devRawBytes = iblockProps.Device.Take(iblockProps.Device.Length - 1).ToArray(); - var devPath = System.Text.Encoding.UTF8.GetString(devRawBytes); - - var blockLabel = iblockProps.IdLabel; - var blockSize = iblockProps.Size; - var driveName = drivesProps.Id; - - // HACK: There should be something in udisks2 to - // get this data but I have no idea where. - var mountPoint = fProcMounts.Select(x => x.Split(' ')) - .Where(x => x[0] == devPath) - .Select(x => x[1]) - .SingleOrDefault(); - - if (mountPoint is null) continue; - - var k = new MountedDriveInfo() - { - DriveLabel = blockLabel, - DriveName = driveName, - DriveSizeBytes = blockSize, - DevicePath = devPath, - MountPath = mountPoint - }; - - newDriveList.Add(k); - } - finally - { - - } - } - - UpdateCollection(newDriveList); - } - - // https://stackoverflow.com/questions/19558644/update-an-observablecollection-from-another-collection - private void UpdateCollection(IEnumerable newCollection) - { - var newCollectionEnumerator = newCollection.GetEnumerator(); - var collectionEnumerator = MountedDrives.GetEnumerator(); - - var itemsToDelete = new Collection(); - while (collectionEnumerator.MoveNext()) - { - var item = collectionEnumerator.Current; - - if (!newCollection.Contains(item)) - itemsToDelete.Add(item); - } - - foreach (var itemToDelete in itemsToDelete) - { - MountedDrives.Remove(itemToDelete); - } - - var i = 0; - - while (newCollectionEnumerator.MoveNext()) - { - var item = newCollectionEnumerator.Current; - - if (!MountedDrives.Contains(item)) - { - MountedDrives.Insert(i, item); - } - else - { - int oldIndex = MountedDrives.IndexOf(item); - MountedDrives.Move(oldIndex, i); - } - i++; - } - } - - private bool disposedValue = false; - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - foreach (var Disposable in _disposables) - Disposable.Dispose(); - - MountedDrives?.Clear(); - } - - disposedValue = true; - } - } - public void Dispose() - { - Dispose(true); - } - } -} diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs new file mode 100644 index 0000000000..691aae23e4 --- /dev/null +++ b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoListener.cs @@ -0,0 +1,172 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +using Avalonia.Controls.Platform; +using Tmds.DBus; +using System.Reactive.Disposables; + +namespace Avalonia.FreeDesktop.Dbus +{ + public partial class LinuxMountedVolumeInfoProvider + { + private class LinuxMountedVolumeInfoListener : IDisposable + { + private CompositeDisposable _disposables; + private readonly Connection _sysDbus; + private readonly IObjectManager _udisk2Manager; + private readonly ObservableCollection _targetObs; + private bool disposedValue = false; + + public LinuxMountedVolumeInfoListener(ref ObservableCollection target) + { + this._sysDbus = Connection.System; + this._udisk2Manager = _sysDbus.CreateProxy("org.freedesktop.UDisks2", "/org/freedesktop/UDisks2"); + this._targetObs = target; + Start(); + } + + private async void Poll() + { + var newDriveList = new List(); + + var fProcMounts = File.ReadAllLines("/proc/mounts"); + + var managedObj = await _udisk2Manager.GetManagedObjectsAsync(); + + var res_drives = managedObj.Where(x => x.Key.ToString().Contains("/org/freedesktop/UDisks2/drives/")) + .Select(x => x.Key); + + var res_blockdev = managedObj.Where(x => x.Key.ToString().Contains("/org/freedesktop/UDisks2/block_devices/")) + .Select(x => x); + + var res_fs = managedObj.Where(x => x.Key.ToString().Contains("system")) + .Select(x => x.Key) + .ToList(); + + foreach (var block in res_blockdev) + { + try + { + var iblock = _sysDbus.CreateProxy("org.freedesktop.UDisks2", block.Key); + var iblockProps = await iblock.GetAllAsync(); + + var block_drive = await iblock.GetDriveAsync(); + if (!res_drives.Contains(block_drive)) continue; + + var drive_key = res_drives.Single(x => x == block_drive); + var drives = _sysDbus.CreateProxy("org.freedesktop.UDisks2", drive_key); + var drivesProps = await drives.GetAllAsync(); + + var devRawBytes = iblockProps.Device.Take(iblockProps.Device.Length - 1).ToArray(); + var devPath = System.Text.Encoding.UTF8.GetString(devRawBytes); + + var blockLabel = iblockProps.IdLabel; + var blockSize = iblockProps.Size; + var driveName = drivesProps.Id; + + // HACK: There should be something in udisks2 to + // get this data but I have no idea where. + var mountPoint = fProcMounts.Select(x => x.Split(' ')) + .Where(x => x[0] == devPath) + .Select(x => x[1]) + .SingleOrDefault(); + + if (mountPoint is null) continue; + + var k = new MountedVolumeInfo() + { + VolumeLabel = blockLabel, + VolumeName = driveName, + VolumeSizeBytes = blockSize, + DevicePath = devPath, + MountPath = mountPoint + }; + + newDriveList.Add(k); + } + catch (Exception ex) + { + Logging.Logger.Warning("Linux Volume Listener", this, "Exception while enumerating DBus items: {0}; {1}" + , ex.Message, ex.StackTrace); + } + } + + UpdateCollection(_targetObs, newDriveList); + } + + private async void Start() + { + _disposables = new CompositeDisposable(); + + var sub1 = await _udisk2Manager.WatchInterfacesAddedAsync(delegate { Poll(); }); + var sub2 = await _udisk2Manager.WatchInterfacesRemovedAsync(delegate { Poll(); }); + + _disposables.Add(sub1); + _disposables.Add(sub2); + + Poll(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _disposables.Dispose(); + _targetObs.Clear(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + + // https://stackoverflow.com/questions/19558644/update-an-observablecollection-from-another-collection + private void UpdateCollection(ObservableCollection target, IEnumerable newCollection) + { + var newCollectionEnumerator = newCollection.GetEnumerator(); + var collectionEnumerator = target.GetEnumerator(); + + var itemsToDelete = new Collection(); + while (collectionEnumerator.MoveNext()) + { + var item = collectionEnumerator.Current; + + if (!newCollection.Contains(item)) + itemsToDelete.Add(item); + } + + foreach (var itemToDelete in itemsToDelete) + { + target.Remove(itemToDelete); + } + + var i = 0; + + while (newCollectionEnumerator.MoveNext()) + { + var item = newCollectionEnumerator.Current; + + if (!target.Contains(item)) + { + target.Insert(i, item); + } + else + { + int oldIndex = target.IndexOf(item); + target.Move(oldIndex, i); + } + i++; + } + } + } + } +} diff --git a/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs b/src/Avalonia.FreeDesktop/LinuxMountedVolumeInfoProvider.cs new file mode 100644 index 0000000000..9c0936f225 --- /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.Dbus +{ + public partial class LinuxMountedVolumeInfoProvider : IMountedVolumeInfoProvider + { + public IDisposable Listen(ObservableCollection mountedDrives) + { + Contract.Requires(mountedDrives != null); + return new LinuxMountedVolumeInfoListener(ref mountedDrives); + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index e670a83c76..d77d3f0dde 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -51,7 +51,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) .Bind().ToConstant(new GtkSystemDialog()) - .Bind().ToConstant(new LinuxMountedDriveInfoProvider()); + .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens);