Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Files.App/Data/Contracts/ICommonDialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ public interface ICommonDialogService
/// <returns>True if the 'Open' button was clicked; otherwise, false.</returns>
bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath);

/// <summary>
/// Asynchronous <see cref="Open_FileOpenDialog"/>; implementations that can should avoid blocking the calling thread.
/// </summary>
Task<(bool Result, string FilePath)> OpenFileOpenDialogAsync(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, Guid? clientGuid = null)
{
var result = Open_FileOpenDialog(hWnd, pickFoldersOnly, filters, defaultFolder, out var filePath, clientGuid);
return Task.FromResult((result, filePath));
}

/// <summary>
/// Asynchronous <see cref="Open_FileSaveDialog"/>; implementations that can should avoid blocking the calling thread.
/// </summary>
Task<(bool Result, string FilePath)> OpenFileSaveDialogAsync(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder)
{
var result = Open_FileSaveDialog(hWnd, pickFoldersOnly, filters, defaultFolder, out var filePath);
return Task.FromResult((result, filePath));
}

/// <summary>
/// Opens a common dialog called NetworkConnectionDialog through native Win32API.
/// </summary>
Expand Down
65 changes: 60 additions & 5 deletions src/Files.App/Platforms/Desktop/Services/DesktopStubServices.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using Files.Platform.Abstractions.FileChooser;
using Microsoft.Extensions.Logging;
using System.Collections.Specialized;
using System.Threading;

namespace Files.App.Services.Desktop
{
Expand Down Expand Up @@ -59,22 +62,74 @@ internal sealed class DesktopJumpListService : IWindowsJumpListService
public Task<IEnumerable<string>> GetFoldersAsync() => Task.FromResult<IEnumerable<string>>([]);
}

// LINUX-TODO(pickers): open file/folder choosers through the xdg-desktop-portal FileChooser; until then pickers report "cancelled"
// Prefer the Async methods: the synchronous ones block the calling thread while the chooser is open.
internal sealed class DesktopCommonDialogService : ICommonDialogService
{
private readonly IFileChooserService _fileChooser;
private int _unavailableLogged;

public DesktopCommonDialogService(IFileChooserService fileChooser)
{
_fileChooser = fileChooser;
}

public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath, Guid? clientGuid = null)
{
filePath = string.Empty;
return false;
// Synchronous fallback for callers that cannot await; it blocks the calling thread
var (result, path) = Task.Run(() => OpenFileOpenDialogAsync(hWnd, pickFoldersOnly, filters, defaultFolder, clientGuid)).GetAwaiter().GetResult();
filePath = path;
return result;
}

public bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath)
{
filePath = string.Empty;
return false;
var (result, path) = Task.Run(() => OpenFileSaveDialogAsync(hWnd, pickFoldersOnly, filters, defaultFolder)).GetAwaiter().GetResult();
filePath = path;
return result;
}

public Task<(bool Result, string FilePath)> OpenFileOpenDialogAsync(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, Guid? clientGuid = null)
=> ChooseAsync(new FileChooserRequest { PickFolder = pickFoldersOnly, Filters = ParseFilters(filters), ParentWindowId = (ulong)hWnd });

public Task<(bool Result, string FilePath)> OpenFileSaveDialogAsync(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder)
{
var folder = Environment.GetFolderPath(defaultFolder);
return ChooseAsync(new FileChooserRequest
{
Save = true,
PickFolder = pickFoldersOnly,
Filters = ParseFilters(filters),
CurrentFolder = string.IsNullOrEmpty(folder) ? null : folder,
ParentWindowId = (ulong)hWnd,
});
}

public bool Open_NetworkConnectionDialog(nint hWnd, bool hideRestoreConnectionCheckBox = false, bool persistConnectionAtLogon = false, bool readOnlyPath = false, string? remoteNetworkName = null, bool useMostRecentPath = false) => false;

private async Task<(bool Result, string FilePath)> ChooseAsync(FileChooserRequest request)
{
var result = await _fileChooser.ChooseAsync(request).ConfigureAwait(true);
if (result.Status == FileChooserStatus.Unavailable && Interlocked.Exchange(ref _unavailableLogged, 1) == 0)
App.Logger.LogWarning("The xdg-desktop-portal FileChooser is not available; file pickers will report cancelled.");

return result.Status == FileChooserStatus.Selected && result.Paths.Count > 0
? (true, result.Paths[0])
: (false, string.Empty);
}

// Filters come as [name, "*.a;*.b", name, ...] pairs
private static List<FileChooserFilter> ParseFilters(string[] filters)
{
var list = new List<FileChooserFilter>();
for (var i = 0; i + 1 < filters.Length; i += 2)
{
var patterns = filters[i + 1].Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (patterns.Length > 0)
list.Add(new FileChooserFilter(filters[i], patterns));
}

return list;
}
}

// Linux has no Start Menu pins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,11 @@ private string DefaultDestinationFolderPath()
return string.IsNullOrEmpty(directory) ? fileName : Path.Combine(directory, fileName);
}

private Task SelectDestinationAsync()
private async Task SelectDestinationAsync()
{
bool result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop);
if (result)
DestinationFolderPath = filePath;
return Task.CompletedTask;
}

private void RefreshEncodingOptions()
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/ViewModels/Properties/BasePropertiesPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
"PNG", "*.png",
];

var result = CommonDialogService.Open_FileOpenDialog(hWnd, false, extensions, Environment.SpecialFolder.Desktop, out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(hWnd, false, extensions, Environment.SpecialFolder.Desktop);
if (result)
{
ViewModel.IsAblumCoverModified = true;
Expand Down
7 changes: 1 addition & 6 deletions src/Files.App/ViewModels/Properties/HashesViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,7 @@ private async Task OnCompareFileAsync()
{
var hWnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(_appWindow.Id);

var result = CommonDialogService.Open_FileOpenDialog(
hWnd,
false,
[],
Environment.SpecialFolder.Desktop,
out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(hWnd, false, [], Environment.SpecialFolder.Desktop);

HashInput = result && filePath != null
? await CalculateFileHashAsync(filePath)
Expand Down
4 changes: 2 additions & 2 deletions src/Files.App/ViewModels/Settings/AdvancedViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ private async Task SetAsOpenFileDialogAsync()
private async Task ImportSettingsAsync()
{
string[] extensions = [Strings.ZipFileCapitalized.GetLocalizedResource(), "*.zip"];
bool result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop, out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop);
if (!result)
return;

Expand Down Expand Up @@ -212,7 +212,7 @@ private async Task ImportSettingsAsync()
private async Task ExportSettingsAsync()
{
string[] extensions = [Strings.ZipFileCapitalized.GetLocalizedResource(), "*.zip"];
bool result = CommonDialogService.Open_FileSaveDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop, out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileSaveDialogAsync(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.Desktop);
if (!result)
return;

Expand Down
6 changes: 3 additions & 3 deletions src/Files.App/ViewModels/Settings/AppearanceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,15 @@ public AppearanceViewModel(IUserSettingsService userSettingsService, IResourcesS

LoadAppThemeFontFamilyOptions();

SelectImageCommand = new RelayCommand(SelectBackgroundImage);
SelectImageCommand = new AsyncRelayCommand(SelectBackgroundImageAsync);
RemoveImageCommand = new RelayCommand(RemoveBackgroundImage);
CustomizeToolbarCommand = new AsyncRelayCommand(() => CommandManager.CustomizeToolbar.ExecuteAsync());
}

/// <summary>
/// Opens a file picker to select a background image
/// </summary>
private void SelectBackgroundImage()
private async Task SelectBackgroundImageAsync()
{
string[] extensions =
[
Expand All @@ -118,7 +118,7 @@ private void SelectBackgroundImage()
"WEBP", "*.webp",
];

var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.MyPictures, out var filePath, _backgroundImagePickerClientGuid);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(MainWindow.Instance.WindowHandle, false, extensions, Environment.SpecialFolder.MyPictures, _backgroundImagePickerClientGuid);
if (result)
AppThemeBackgroundImageSource = filePath;
}
Expand Down
10 changes: 5 additions & 5 deletions src/Files.App/ViewModels/Settings/GeneralViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Collections.Specialized;
using Windows.Storage;
using Windows.System;
using static Files.App.Helpers.MenuFlyoutHelper;

Check warning on line 7 in src/Files.App/ViewModels/Settings/GeneralViewModel.cs

View workflow job for this annotation

GitHub Actions / build-linux

'MenuFlyoutHelper' is obsolete: 'Must not use this helper to generate menu flyout any longer.'

Check warning on line 7 in src/Files.App/ViewModels/Settings/GeneralViewModel.cs

View workflow job for this annotation

GitHub Actions / build-linux-x64

'MenuFlyoutHelper' is obsolete: 'Must not use this helper to generate menu flyout any longer.'
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;

namespace Files.App.ViewModels.Settings
Expand All @@ -18,7 +18,7 @@

private ReadOnlyCollection<IMenuFlyoutItemViewModel>? addFlyoutItemsSource;

public RelayCommand ChangePageCommand { get; }
public AsyncRelayCommand ChangePageCommand { get; }
public RelayCommand<PageOnStartupViewModel> RemovePageCommand { get; }
public RelayCommand<string> AddPageCommand { get; }
public RelayCommand RestartCommand { get; }
Expand Down Expand Up @@ -86,7 +86,7 @@

public GeneralViewModel()
{
ChangePageCommand = new RelayCommand(ChangePageAsync);
ChangePageCommand = new AsyncRelayCommand(ChangePageAsync);
RemovePageCommand = new RelayCommand<PageOnStartupViewModel>(RemovePage);
AddPageCommand = new RelayCommand<string>(async (path) => await AddPageAsync(path));
RestartCommand = new RelayCommand(DoRestartAsync);
Expand Down Expand Up @@ -346,9 +346,9 @@
}
}

private void ChangePageAsync()
private async Task ChangePageAsync()
{
var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop);
if (result && SelectedPageIndex >= 0)
PagesOnStartupList[SelectedPageIndex] = new PageOnStartupViewModel(filePath);
}
Expand All @@ -363,7 +363,7 @@
{
if (string.IsNullOrWhiteSpace(path))
{
bool result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop);
if (!result)
return;

Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/Views/Properties/LibraryPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ protected override void Properties_Loaded(object sender, RoutedEventArgs e)

private async Task AddLocationAsync()
{
var result = CommonDialogService.Open_FileOpenDialog(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop, out var filePath);
var (result, filePath) = await CommonDialogService.OpenFileOpenDialogAsync(MainWindow.Instance.WindowHandle, true, [], Environment.SpecialFolder.Desktop);
if (!result)
return;

Expand Down
76 changes: 76 additions & 0 deletions src/Files.Platform.Abstractions/FileChooser/IFileChooserService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Files.Platform.Abstractions.FileChooser
{
/// <summary>
/// One entry of the file type list. Each pattern is a glob such as <c>*.png</c>.
/// </summary>
public sealed record FileChooserFilter(string Name, IReadOnlyList<string> Patterns);

/// <summary>
/// What to ask the user for.
/// </summary>
public sealed record FileChooserRequest
{
/// <summary>Pick a folder instead of a file.</summary>
public bool PickFolder { get; init; }

/// <summary>Ask for a destination to write to instead of existing items.</summary>
public bool Save { get; init; }

/// <summary>Allow several selections (open only).</summary>
public bool Multiple { get; init; }

/// <summary>The dialog title; the desktop picks a default when empty.</summary>
public string Title { get; init; } = string.Empty;

public IReadOnlyList<FileChooserFilter> Filters { get; init; } = [];

/// <summary>The suggested file name (save only).</summary>
public string? CurrentName { get; init; }

/// <summary>The folder to start in (save only; the portal ignores it for open).</summary>
public string? CurrentFolder { get; init; }

/// <summary>The X11 window id of the parent window, or zero when unknown.</summary>
public ulong ParentWindowId { get; init; }
}

/// <summary>
/// How the request ended.
/// </summary>
public enum FileChooserStatus
{
/// <summary>The user chose something.</summary>
Selected,

/// <summary>The user dismissed the dialog or it failed.</summary>
Cancelled,

/// <summary>No file chooser is available on this desktop.</summary>
Unavailable,
}

/// <summary>
/// The outcome of a chooser request; <see cref="Paths"/> are absolute local paths.
/// </summary>
public sealed record FileChooserResult(FileChooserStatus Status, IReadOnlyList<string> Paths)
{
public static FileChooserResult Cancelled { get; } = new(FileChooserStatus.Cancelled, []);

public static FileChooserResult Unavailable { get; } = new(FileChooserStatus.Unavailable, []);
}

/// <summary>
/// Shows the desktop's own file and folder chooser.
/// </summary>
public interface IFileChooserService
{
Task<FileChooserResult> ChooseAsync(FileChooserRequest request, CancellationToken cancellationToken = default);
}
}
Loading
Loading