@ -1,6 +1,6 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using System.Text ;
@ -10,7 +10,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO ;
using Avalonia.Threading ;
using Tmds.DBus.Protocol ;
using Tmds.DBus.SourceGenerator ;
using Avalonia.FreeDesktop.DBus ;
namespace Avalonia.FreeDesktop
{
@ -32,12 +32,12 @@ namespace Avalonia.FreeDesktop
using var restoreContext = AvaloniaSynchronizationContext . Ensure ( DispatcherPriority . Input ) ;
var dbusFileChooser = new OrgFreedesktopPortal FileChooserProxy ( conn , "org.freedesktop.portal.Desktop" ,
var dbusFileChooser = new FileChooser ( conn , "org.freedesktop.portal.Desktop" ,
"/org/freedesktop/portal/desktop" ) ;
uint version ;
try
{
version = await dbusFileChooser . GetVersionProperty Async ( ) ;
version = await dbusFileChooser . GetVersionAsync ( ) ;
}
catch
{
@ -47,13 +47,13 @@ namespace Avalonia.FreeDesktop
return new DBusSystemDialog ( conn , parentLeaseProvider , dbusFileChooser , version ) ;
}
private readonly Connection _ connection ;
private readonly OrgFreedesktopPortal FileChooserProxy _f ileChooser ;
private readonly DBus Connection _ connection ;
private readonly FileChooser _f ileChooser ;
private readonly Func < Task < IPortalParentLease ? > > ? _ parentLeaseProvider ;
private readonly uint _ version ;
private DBusSystemDialog ( Connection connection , Func < Task < IPortalParentLease ? > > ? parentLeaseProvider ,
OrgFreedesktopPortal FileChooserProxy fileChooser , uint version )
private DBusSystemDialog ( DBus Connection connection , Func < Task < IPortalParentLease ? > > ? parentLeaseProvider ,
FileChooser fileChooser , uint version )
{
_ connection = connection ;
_f ileChooser = fileChooser ;
@ -74,7 +74,7 @@ namespace Avalonia.FreeDesktop
{
await using var parentLease = await AcquireParentLeaseAsync ( ) . ConfigureAwait ( false ) ;
var parentWindow = parentLease ? . Handle ? ? string . Empty ;
ObjectPath objectPath ;
var ( expectedPath , token ) = CreateRequestToken ( ) ;
var chooserOptions = new Dictionary < string , VariantValue > ( ) ;
if ( TryParseFilters ( options . FileTypeFilter , options . SuggestedFileType , out var filters ,
@ -89,19 +89,20 @@ namespace Avalonia.FreeDesktop
chooserOptions . Add ( "current_folder" , VariantValue . Array ( Encoding . UTF8 . GetBytes ( folderPath + "\0" ) ) ) ;
chooserOptions . Add ( "multiple" , VariantValue . Bool ( options . AllowMultiple ) ) ;
chooserOptions . Add ( "handle_token" , token ) ;
objectPath = await _f ileChooser . OpenFileAsync ( parentWindow , options . Title ? ? string . Empty , chooserOptions ) ;
var request =
new OrgFreedesktopPortalRequestProxy ( _ connection , "org.freedesktop.portal.Desktop" , objectPath ) ;
var request = new Request ( _ connection , "org.freedesktop.portal.Desktop" , expectedPath ) ;
var tsc = new TaskCompletionSource < string [ ] ? > ( ) ;
using var disposable = await request . WatchResponseAsync ( ( e , x ) = >
using var disposable = await request . WatchResponseAsync ( notification = >
{
if ( e is not null )
tsc . TrySetException ( e ) ;
if ( notification . IsCompletion )
tsc . TrySetException ( notification . Exc eption ) ;
else
tsc . TrySetResult ( x . Results [ "uris" ] . GetArray < string > ( ) ) ;
} ) ;
tsc . TrySetResult ( notification . Value . Results [ "uris" ] . GetArray < string > ( ) ) ;
} , ObserverFlags . EmitAll ) ;
var actualPath = await _f ileChooser . OpenFileAsync ( parentWindow , options . Title ? ? string . Empty , chooserOptions ) ;
VerifyRequestPath ( tsc , expectedPath , actualPath ) ;
var uris = await tsc . Task ? ? [ ] ;
return uris . Select ( static path = > new BclStorageFile ( new FileInfo ( new Uri ( path ) . LocalPath ) ) ) . ToList ( ) ;
@ -124,7 +125,7 @@ namespace Avalonia.FreeDesktop
{
await using var parentLease = await AcquireParentLeaseAsync ( ) . ConfigureAwait ( false ) ;
var parentWindow = parentLease ? . Handle ? ? string . Empty ;
ObjectPath objectPath ;
var ( expectedPath , token ) = CreateRequestToken ( ) ;
var chooserOptions = new Dictionary < string , VariantValue > ( ) ;
if ( TryParseFilters ( options . FileTypeChoices , options . SuggestedFileType , out var filters ,
out var currentFilter ) )
@ -138,21 +139,20 @@ namespace Avalonia.FreeDesktop
chooserOptions . Add ( "current_name" , VariantValue . String ( currentName ) ) ;
if ( options . SuggestedStartLocation ? . TryGetLocalPath ( ) is { } folderPath )
chooserOptions . Add ( "current_folder" , VariantValue . Array ( Encoding . UTF8 . GetBytes ( folderPath + "\0" ) ) ) ;
chooserOptions . Add ( "handle_token" , token ) ;
objectPath = await _f ileChooser . SaveFileAsync ( parentWindow , options . Title ? ? string . Empty , chooserOptions )
. ConfigureAwait ( false ) ;
var request =
new OrgFreedesktopPortalRequestProxy ( _ connection , "org.freedesktop.portal.Desktop" , objectPath ) ;
var request = new Request ( _ connection , "org.freedesktop.portal.Desktop" , expectedPath ) ;
var tsc = new TaskCompletionSource < string [ ] ? > ( ) ;
FilePickerFileType ? selectedType = null ;
using var disposable = await request . WatchResponseAsync ( ( e , x ) = >
using var disposable = await request . WatchResponseAsync ( notification = >
{
if ( e is not null )
if ( notification . IsCompletion )
{
tsc . TrySetException ( e ) ;
tsc . TrySetException ( notification . Exc eption ) ;
}
else
{
var x = notification . Value ;
if ( x . Results . TryGetValue ( "current_filter" , out var currentFilter ) )
{
var name = currentFilter . GetItem ( 0 ) . GetString ( ) ;
@ -166,19 +166,23 @@ namespace Avalonia.FreeDesktop
else
patterns . Add ( t . GetItem ( 1 ) . GetString ( ) ) ;
}
// Reuse the file type objects from options
// so the consuming code can match exactly the
// file type selected instead of spawning one.
selectedType = options . FileTypeChoices ? . FirstOrDefault ( type = > type . Name = = name & & (
( type . MimeTypes ? . All ( y = > mimeTypes . Contains ( y ) ) ? ? false ) | |
( type . Patterns ? . All ( y = > patterns . Contains ( y ) ) ? ? false ) ) )
( type . Patterns ? . All ( y = > patterns . Contains ( y ) ) ? ? false ) ) )
? ? new FilePickerFileType ( name ) { MimeTypes = mimeTypes , Patterns = patterns } ;
}
tsc . TrySetResult ( x . Results [ "uris" ] . GetArray < string > ( ) ) ;
}
} ) . ConfigureAwait ( false ) ;
} , ObserverFlags . EmitAll ) . ConfigureAwait ( false ) ;
var actualPath = await _f ileChooser . SaveFileAsync ( parentWindow , options . Title ? ? string . Empty , chooserOptions )
. ConfigureAwait ( false ) ;
VerifyRequestPath ( tsc , expectedPath , actualPath ) ;
var uris = await tsc . Task . ConfigureAwait ( false ) ;
var path = uris ? . FirstOrDefault ( ) is { } filePath ? new Uri ( filePath ) . LocalPath : null ;
@ -198,6 +202,7 @@ namespace Avalonia.FreeDesktop
await using var parentLease = await AcquireParentLeaseAsync ( ) . ConfigureAwait ( false ) ;
var parentWindow = parentLease ? . Handle ? ? string . Empty ;
var ( expectedPath , token ) = CreateRequestToken ( ) ;
var chooserOptions = new Dictionary < string , VariantValue >
{
{ "directory" , VariantValue . Bool ( true ) } , { "multiple" , VariantValue . Bool ( options . AllowMultiple ) }
@ -207,19 +212,20 @@ namespace Avalonia.FreeDesktop
chooserOptions . Add ( "current_name" , VariantValue . String ( currentName ) ) ;
if ( options . SuggestedStartLocation ? . TryGetLocalPath ( ) is { } folderPath )
chooserOptions . Add ( "current_folder" , VariantValue . Array ( Encoding . UTF8 . GetBytes ( folderPath + "\0" ) ) ) ;
chooserOptions . Add ( "handle_token" , token ) ;
var objectPath =
await _f ileChooser . OpenFileAsync ( parentWindow , options . Title ? ? string . Empty , chooserOptions ) ;
var request =
new OrgFreedesktopPortalRequestProxy ( _ connection , "org.freedesktop.portal.Desktop" , objectPath ) ;
var request = new Request ( _ connection , "org.freedesktop.portal.Desktop" , expectedPath ) ;
var tsc = new TaskCompletionSource < string [ ] ? > ( ) ;
using var disposable = await request . WatchResponseAsync ( ( e , x ) = >
using var disposable = await request . WatchResponseAsync ( notification = >
{
if ( e is not null )
tsc . TrySetException ( e ) ;
if ( notification . IsCompletion )
tsc . TrySetException ( notification . Exc eption ) ;
else
tsc . TrySetResult ( x . Results [ "uris" ] . GetArray < string > ( ) ) ;
} ) ;
tsc . TrySetResult ( notification . Value . Results [ "uris" ] . GetArray < string > ( ) ) ;
} , ObserverFlags . EmitAll ) ;
var actualPath = await _f ileChooser . OpenFileAsync ( parentWindow , options . Title ? ? string . Empty , chooserOptions ) ;
VerifyRequestPath ( tsc , expectedPath , actualPath ) ;
var uris = await tsc . Task ? ? Array . Empty < string > ( ) ;
return uris
@ -273,5 +279,21 @@ namespace Avalonia.FreeDesktop
result = filters . AsVariantValue ( ) ;
return true ;
}
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
// Subscribe to the Response signal before making the portal call to avoid a race condition.
private ( ObjectPath ExpectedPath , string Token ) CreateRequestToken ( )
{
string sender = ( _ connection . UniqueName ? ? "" ) . TrimStart ( ':' ) . Replace ( "." , "_" ) ;
string token = "Avalonia_" + Stopwatch . GetTimestamp ( ) . ToString ( ) ;
ObjectPath expectedPath = $"/org/freedesktop/portal/desktop/request/{sender}/{token}" ;
return ( expectedPath , token ) ;
}
private static void VerifyRequestPath < T > ( TaskCompletionSource < T > tsc , ObjectPath expectedPath , ObjectPath actualPath )
{
if ( actualPath ! = expectedPath )
tsc . TrySetException ( new InvalidOperationException ( $"Portal returned unexpected request path '{actualPath}', expected '{expectedPath}'." ) ) ;
}
}
}